begin writing 4-appendix
This commit is contained in:
parent
f864bea4d6
commit
4f09c75006
165
posts/polycount/4/appendix/index.qmd
Normal file
165
posts/polycount/4/appendix/index.qmd
Normal file
@ -0,0 +1,165 @@
|
||||
---
|
||||
title: "Polynomial Counting 4, Addendum"
|
||||
description: |
|
||||
Complex embeddings of irrational -adic expansions.
|
||||
format:
|
||||
html:
|
||||
html-math-method: katex
|
||||
date: "2021-02-09"
|
||||
date-modified: "2025-02-12"
|
||||
categories:
|
||||
- algebra
|
||||
- python
|
||||
execute:
|
||||
eval: false
|
||||
---
|
||||
|
||||
|
||||
After converting my original [Two 2's post](../), I found myself much more pleased after making the
|
||||
diagrams it more reproducible.
|
||||
However, I noticed some things which required further examination.
|
||||
|
||||
|
||||
Balanced vs. Binary *κ*-adics
|
||||
-----------------------------
|
||||
|
||||
In the parent post, it was discussed that the integer four has a non-repeating expansion
|
||||
when expressed as an *κ*-adic integer.
|
||||
It followed in multiple ways from the repeating expansion of the integer two in the balanced ternary alphabet.
|
||||
A more unusual consequence of this is that despite the initial choice of alphabet,
|
||||
[negative numerals could be cleared](../#all-positive)
|
||||
from the expansion by using an extra-greedy borrow.
|
||||
|
||||
These choices actually only differ in only one significant way: the choice of integer division function.
|
||||
In Haskell, there exists both `quotRem` and `divMod`, with the two disagreeing on negative remainders:
|
||||
|
||||
::: {.row layout-ncol="2"}
|
||||
```{haskell}
|
||||
quotRem (-27) 5
|
||||
```
|
||||
|
||||
```{haskell}
|
||||
divMod (-27) 5
|
||||
```
|
||||
:::
|
||||
|
||||
We can factor our choice out of our two digit-wide carry function by presenting it as an argument:
|
||||
|
||||
```{haskell}
|
||||
-- Widened carry of a particular repeated amount
|
||||
-- i.e., for carry qr 2, the carry is 22 = 100
|
||||
carry2' qr b = carry' []
|
||||
where carry' zs (x:y:z:xs)
|
||||
| q == 0 = carry' (x:zs) (y:z:xs) -- try carrying at a higher place value
|
||||
| otherwise = foldl (flip (:)) ys zs -- carry here
|
||||
where ys = r : y-x+r : z+q : xs
|
||||
(q, r) = x `qr` b
|
||||
```
|
||||
|
||||
Finally, let's show the iterates of applying this function to "4".
|
||||
We happen to know the root for $\langle 2, 2|$ is $\sqrt 3 + 1$, so using an approximation,
|
||||
we can check that we get a roughly constant value by evaluating at each step.
|
||||
|
||||
```{haskell}
|
||||
{-| layout-ncol: 2 -}
|
||||
{-| code-fold: true -}
|
||||
|
||||
import IHaskell.Display
|
||||
|
||||
-- Horner evaluation on polynomials of ascending powers
|
||||
hornerEval x = foldr (\c a -> a * x + c) 0
|
||||
-- Pair a polynomial with its evaluation at x
|
||||
pairEval x = (,) <*> hornerEval x . map fromIntegral
|
||||
|
||||
-- Directly expand the integer `n`, using the `carry` showing each step for `count` steps
|
||||
expandSteps count carry n = take count $ iterate carry $ n:replicate count 0
|
||||
|
||||
cendree4QuotRem10Steps = map (pairEval (sqrt 3 + 1)) $ expandSteps 10 (carry2' quotRem 2) 4
|
||||
cendree4DivMod10Steps = map (pairEval (sqrt 3 + 1)) $ expandSteps 10 (carry2' divMod 2) 4
|
||||
|
||||
markdown "`quotRem`"
|
||||
markdown "`divMod`"
|
||||
putStrLn . unlines . map show $ cendreeQuotRem10Steps
|
||||
putStrLn . unlines . map show $ cendreeDivMod10Steps
|
||||
```
|
||||
|
||||
And fortunately, regardless of which function we pick, the iterates are all roughly four.
|
||||
Note that since `quotRem` allows negative remainders, implementing the carry with it causes
|
||||
negative numbers to show up in our expansions.
|
||||
Conversely, negative numbers *cannot* show up if we use `divMod`.
|
||||
|
||||
|
||||
### Chaos before Four
|
||||
|
||||
Recall the series for two in the *κ*-adics:
|
||||
|
||||
$$
|
||||
2 = ...1\bar{1}1\bar{1}1\bar{1}100_{\kappa}
|
||||
$$
|
||||
|
||||
Since the `divMod` implementation clears negative numbers from expansions, we can try using it on this series.
|
||||
The result is another chaotic series:
|
||||
|
||||
```{haskell}
|
||||
cendree2DivModCycleExpansion = take 11 $ iterate (carry2' divMod 2) $ take 15 $ 0:0:cycle [1,-1]
|
||||
putStrLn . unlines . map show $ cendree2DivModCycleExpansion
|
||||
```
|
||||
|
||||
We get the same series if we expand 2 directly (which we can also use to check its validity):
|
||||
|
||||
```{haskell}
|
||||
cendree2DivMod15Steps = map (pairEval (sqrt 3 + 1)) $ expandSteps 15 (carry2' divMod 2) 2
|
||||
putStrLn . unlines . map show $ cendree2DivMod15Steps
|
||||
```
|
||||
|
||||
We can *also* use this to get a series for negative one, a number which has a terminating expansion
|
||||
in the balanced alphabet.
|
||||
|
||||
```{haskell}
|
||||
cendreeNeg1DivMod15Steps = map (pairEval (sqrt 3 + 1)) $ expandSteps 15 (carry2' divMod 2) (-1)
|
||||
putStrLn . unlines . map show $ cendreeNeg1DivMod15Steps
|
||||
```
|
||||
|
||||
The most natural property of negative one should be that if we add one to it, we get zero.
|
||||
If we take the last iterate of this, increment the zeroth place value, and apply the carry,
|
||||
we find that everything clears properly.
|
||||
|
||||
```{haskell}
|
||||
cendree0FromNeg1DivMod = (\(x:xs) -> (x + 1):xs) $ snd $ last cendreeNeg1DivMod15Steps
|
||||
cendree0IncrementSteps = map (pairEval (sqrt 3 + 1)) $ take 15 $ iterate (carry2' divMod 2) cendree0FromNeg1DivMod
|
||||
|
||||
putStrLn . unlines . map show $ cendree0IncrementSteps
|
||||
```
|
||||
|
||||
Naturally, it should be possible to use the the expansions of negative one and two in tandem on any
|
||||
series in the balanced alphabet to convert it to the binary alphabet.
|
||||
Actually demonstrating this and proving it is left as an exercise.
|
||||
|
||||
|
||||
Are these really -adic?
|
||||
-----------------------
|
||||
|
||||
Perhaps it is still unconvincing that expanding the integers in this way gives something that is
|
||||
indeed related to *p*-adics.
|
||||
The Wikipedia article on the [*p*-adic valuation](https://en.wikipedia.org/wiki/P-adic_valuation)
|
||||
contains [a figure](https://commons.wikimedia.org/wiki/File:2adic12480.svg) whose description
|
||||
provides a way to map *p*-adics into the complex numbers[^1].
|
||||
The gist is to construct a Fourier series over truncations of numbers.
|
||||
Each term of the series is weighted by a geometrically decreasing coefficient *c*.
|
||||
|
||||
$$
|
||||
[...d_2 d_1 d_0]_p \mapsto e^{2\pi i [d_0] / p}
|
||||
+ c e^{2\pi i [d_1 d_0] / p^2}
|
||||
+ c^2 e^{2\pi i [d_2 d_1 d_0] / p^2}
|
||||
+ ... \\
|
||||
f(d; p) = \sum_{n = 0}^N c^n e^{2\pi i \cdot [d_{n:0}] / p^{n + 1}}
|
||||
$$
|
||||
|
||||
Assuming the first term dominates, one way of interpreting this is that we place numbers
|
||||
around the unit circle according to their one's place.
|
||||
Then, we offset each by smaller circles, each centered on the last, using more and more digits.
|
||||
This produces a fractal pattern that looks like a wheel with *p* spokes.
|
||||
At each point on where the spokes meet the rim, there is another wheel with *p* spokes, ad infinitum.
|
||||
|
||||
[^1]: Taken from the paper "Fractal geometry for images of continuous embeddings of p-adic
|
||||
numbers and solenoids into Euclidean spaces" (DOI: 10.1007/BF02073866).
|
||||
225
posts/polycount/4/appendix/kadic.py
Normal file
225
posts/polycount/4/appendix/kadic.py
Normal file
@ -0,0 +1,225 @@
|
||||
import argparse
|
||||
from itertools import accumulate, chain, repeat
|
||||
from pathlib import Path
|
||||
from typing import Any, Generator, TypeVar
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.widgets import Slider
|
||||
from matplotlib.animation import FuncAnimation
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def to_base(b: int, x: int) -> list[int]:
|
||||
"""Convert x to an integer base b."""
|
||||
if x == 0:
|
||||
return [0]
|
||||
|
||||
ret = []
|
||||
while x > 0:
|
||||
ret.append(x % b)
|
||||
x = x // b
|
||||
return ret
|
||||
|
||||
|
||||
def from_base(b: float, xs: list[int]) -> float:
|
||||
"""
|
||||
Interpret xs as a string in base `b`.
|
||||
For genericness, the base and return are both floats.
|
||||
"""
|
||||
ret = 0
|
||||
plval = 1
|
||||
for i in xs:
|
||||
ret += i * plval
|
||||
plval *= b
|
||||
return ret
|
||||
|
||||
|
||||
class Expansions:
|
||||
"""List of base-b expansions of a number"""
|
||||
|
||||
def __init__(self, base: float, name: str, xs: list[list[int]]):
|
||||
self.xs = xs
|
||||
self.name = name
|
||||
self.base = base
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, base: float, expansions_file: Path, name: str | None = None):
|
||||
ret = []
|
||||
with open(expansions_file) as f:
|
||||
for line in f.readlines():
|
||||
ret.append(eval(line))
|
||||
|
||||
# Extract name of base from filename
|
||||
if name is None:
|
||||
name_from_file = expansions_file.name.removesuffix(expansions_file.suffix)
|
||||
second_underscore = name_from_file.find("_", name_from_file.find("_") + 1)
|
||||
name = (
|
||||
name_from_file[:second_underscore]
|
||||
if second_underscore >= 0
|
||||
else name_from_file
|
||||
)
|
||||
|
||||
return cls(base, name, ret)
|
||||
|
||||
@classmethod
|
||||
def from_integer(cls, base: int, count: int, name: str | None = None):
|
||||
return cls(
|
||||
float(base),
|
||||
str(base) if name is None else name,
|
||||
[to_base(base, i) for i in range(count)],
|
||||
)
|
||||
|
||||
|
||||
def slices(xs):
|
||||
for i in range(1, len(xs) + 1):
|
||||
yield xs[:i]
|
||||
|
||||
|
||||
def extend_last(xs: Generator[T, Any, Any]) -> Generator[T, Any, Any]:
|
||||
i = next(xs)
|
||||
yield i
|
||||
for i in xs:
|
||||
yield i
|
||||
while True:
|
||||
yield i
|
||||
|
||||
|
||||
# More efficient embedding
|
||||
def to_angle(xs: list[int], len_: int, b: float) -> complex:
|
||||
return np.exp(2j * np.pi * from_base(b, xs) / b ** (len_ + 1))
|
||||
|
||||
|
||||
def _adic_angles(b: float, expansion: list[int], maxpow: int = 10) -> list[complex]:
|
||||
return [
|
||||
to_angle(x, i, b) for i, x in zip(range(maxpow), extend_last(slices(expansion)))
|
||||
]
|
||||
|
||||
|
||||
def geosum(c, phis: list[complex]) -> complex:
|
||||
return sum(
|
||||
i * j
|
||||
for i, j in zip(phis, accumulate(chain([1], repeat(c)), lambda x, y: x * y))
|
||||
)
|
||||
|
||||
|
||||
def interactive(expansions: Expansions):
|
||||
phiss = [
|
||||
[_adic_angles(expansions.base, expansion, 200) for expansion in expansions.xs]
|
||||
]
|
||||
embedding: NDArray[np.complex64] = np.array(
|
||||
[geosum(0.9, phis) for phis in phiss[0]]
|
||||
)
|
||||
|
||||
# Create the figure and the line that we will manipulate
|
||||
fig, ax = plt.subplots()
|
||||
|
||||
line = ax.plot(embedding.real, embedding.imag, linestyle="None", marker=".")[0]
|
||||
|
||||
# adjust the main plot to make room for the sliders
|
||||
fig.subplots_adjust(bottom=0.25)
|
||||
|
||||
# Make a horizontal slider to control the frequency.
|
||||
axfreq = fig.add_axes([0.1, 0.1, 0.75, 0.03]) # type: ignore
|
||||
freq_slider = Slider(
|
||||
ax=axfreq,
|
||||
label="$c$",
|
||||
valmin=0.001,
|
||||
valmax=0.999,
|
||||
valinit=0.9,
|
||||
)
|
||||
|
||||
# The function to be called anytime a slider's value changes
|
||||
def update(val):
|
||||
embedding: NDArray[np.complex64] = np.array(
|
||||
[geosum(val, phis) for phis in phiss[0]]
|
||||
)
|
||||
line.set_data(embedding.real, embedding.imag) # type: ignore
|
||||
ax.set_xlim(min(embedding.real), max(embedding.real))
|
||||
ax.set_ylim(min(embedding.imag), max(embedding.imag))
|
||||
fig.canvas.draw_idle()
|
||||
|
||||
# register the update function with each slider
|
||||
freq_slider.on_changed(update)
|
||||
|
||||
plt.show()
|
||||
|
||||
|
||||
def anim(expansions: Expansions):
|
||||
phiss = [
|
||||
[_adic_angles(expansions.base, expansion, 200) for expansion in expansions.xs]
|
||||
]
|
||||
embedding: NDArray[np.complex64] = np.array(
|
||||
[geosum(0.9, phis) for phis in phiss[0]]
|
||||
)
|
||||
|
||||
# Create the figure and the line that we will manipulate
|
||||
fig, ax = plt.subplots()
|
||||
|
||||
line = ax.plot(embedding.real, embedding.imag, linestyle="None", marker=".")[0]
|
||||
|
||||
vals = np.linspace(0.9, 0.1, 100)
|
||||
i = [0]
|
||||
|
||||
# The function to be called anytime a slider's value changes
|
||||
def update(_):
|
||||
embedding: NDArray[np.complex64] = np.array(
|
||||
[geosum(vals[i[0]], phis) for phis in phiss[0]]
|
||||
)
|
||||
line.set_data(embedding.real, embedding.imag) # type: ignore
|
||||
ax.set_xlim(min(embedding.real), max(embedding.real))
|
||||
ax.set_ylim(min(embedding.imag), max(embedding.imag))
|
||||
ax.set_title(f"{vals[i[0]]}")
|
||||
i[0] = i[0] + 1
|
||||
|
||||
FuncAnimation(fig, update, frames=99).save( # type: ignore
|
||||
f"{expansions.name}-adics_count_{len(expansions.xs)}_{len(expansions.xs[-1])}_digits.mp4"
|
||||
)
|
||||
|
||||
|
||||
cendree_divmod = Expansions.from_file(
|
||||
1 + 3**0.5, Path("./cendree_DivMod_count_1024_256_digits.txt"), "divMod_cendree"
|
||||
)
|
||||
cendree_quotrem = Expansions.from_file(
|
||||
1 + 3**0.5, Path("./cendree_QuotRem_count_1024_256_digits.txt"), "quotRem_cendree"
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--interactive",
|
||||
"-i",
|
||||
action="store_true",
|
||||
help="Interactively set the geometric constant of the Fourier series.",
|
||||
)
|
||||
parser.add_argument("--base", "-p", type=int, help="Use p-adic integers")
|
||||
parser.add_argument(
|
||||
"--count",
|
||||
"-c",
|
||||
default=1024,
|
||||
type=int,
|
||||
help="Number of numbers to use. Only applies when using -p.",
|
||||
)
|
||||
parser.add_argument("--cendree", "-k", type=Path, help="Use cendree-adic integers")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
expansions: Expansions
|
||||
if args.cendree:
|
||||
if not args.cendree.exists():
|
||||
raise ValueError(f"Received cendree-adic expansion file: {args.cendree}")
|
||||
expansions = Expansions.from_file(1 + 3**0.5, args.cendree)
|
||||
else:
|
||||
expansions = Expansions.from_integer(args.base, args.count)
|
||||
|
||||
if args.interactive:
|
||||
interactive(expansions)
|
||||
else:
|
||||
anim(expansions)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,44 +1,71 @@
|
||||
-- widened borrow of a particular repeated amount
|
||||
-- i.e., for borrow2 2, the borrow is 22 = 100
|
||||
borrow2 b | b == 1 = error "Cannot borrow 1: implies positional symbol zero disallowed"
|
||||
| otherwise = borrow' [] b
|
||||
where borrow' zs b (x:y:z:xs) | abs x >= b = zipUp ys zs
|
||||
| otherwise = borrow' (x:zs) b (y:z:xs)
|
||||
where ys = r : y-x+r : z+q : xs
|
||||
(q, r) = x `quotRem` b
|
||||
zipUp = foldl (flip (:))
|
||||
-- Widened borrow of a particular repeated amount
|
||||
-- i.e., for borrow2 qr 2, the borrow is 22 = 100
|
||||
-- `qr` is an integer division function returning the quotient and remainder.
|
||||
-- i.e., either `divMod` or `quotRem`
|
||||
borrow2' qr b
|
||||
| b == 1 = error "Cannot borrow 1: implies positional symbol zero disallowed"
|
||||
| otherwise = borrow' []
|
||||
where borrow' zs (x:y:z:xs)
|
||||
| abs x >= b = zipUp ys zs -- carry here
|
||||
| otherwise = borrow' (x:zs) (y:z:xs) -- try borrowing at a higher place value
|
||||
where ys = r : y-x+r : z+q : xs
|
||||
(q, r) = x `qr` b
|
||||
zipUp = foldl (flip (:))
|
||||
|
||||
--degenerate `borrow2 1 `that remedies inadequacies in below for 1, i.e., the borrow is 11 = 100
|
||||
borrow2 = borrow2' quotRem
|
||||
|
||||
--degenerate `borrow2 1` that remedies inadequacies in below for 1, i.e., the borrow is 11 = 100
|
||||
--this system is VERY bad because we invoke the place value '0' to expand into
|
||||
--borrow1' zs (x:y:z:xs) | abs x > 1 = zipUp ys zs
|
||||
-- | otherwise = borrow1' (x:zs) (y:z:xs)
|
||||
-- where ys = (signum x):(y-x+signum x):z+(x-1):xs
|
||||
|
||||
-- truncate the adic expansion to n digits, for a system where borrows are 2 wide and both b
|
||||
truncadic b n = take n . (!! n) . iterate (borrow2 b)
|
||||
-- Truncate the adic expansion to n digits, for a system where borrows are two digits wide and both b
|
||||
truncadic' n b = (!! n) . iterate (borrow2 b)
|
||||
truncadic n = (take n .) . truncadic' n
|
||||
|
||||
--same but, produce a list of expansions, taking modulo m in between borrows
|
||||
truncadicmod b m n = map (take n) . take n . iterate (map (`rem` m) . borrow2 2)
|
||||
-- same but, produce a list of expansions, taking modulo m in between borrows
|
||||
truncadicMod b m n = map (take n) . take n . iterate (map (`rem` m) . borrow2 2)
|
||||
|
||||
-- find moduli starting from s where truncations agree
|
||||
-- caveat: the last of truncadicmod is not guaranteed to exhaust terms outside alphabet
|
||||
-- Find moduli starting from `s` where truncations agree
|
||||
-- Caveat: the last of truncadicMod is not guaranteed to exhaust terms outside alphabet
|
||||
-- i.e., for b = 2, the alphabet {-1, 0, 1}
|
||||
findaccurate b n s xs = map fst $ filter ((==good) . snd) bads
|
||||
where good = truncadic b n xs
|
||||
bads = map (\m -> (,) m $ last $ truncadicmod b m n xs) [s..]
|
||||
findAccurate n b s xs = map fst $ filter ((==good) . snd) bads
|
||||
where good = truncadic n b xs
|
||||
bads = map (\m -> (,) m $ last $ truncadicMod b m n xs) [s..]
|
||||
|
||||
--given an amount of digits `n`, a 2-wide borrow `b`, and a canonical representation of `b`
|
||||
--construct integer multiples of `b`
|
||||
-- Given an amount of digits `n`, a 2-wide borrow `b`, and a canonical representation of `b`
|
||||
-- construct integer multiples of `b`
|
||||
evens n b = ([0]:) . map (take n) . iterate addb
|
||||
--add b to an adic expansion
|
||||
where addb = (!! n ) . iterate (borrow2 b) . (b:) . tail
|
||||
-- add b to an adic expansion
|
||||
where addb = truncadic' n b . (b:) . tail
|
||||
|
||||
--first 200 digits of cendree-adic expansions of even numbers
|
||||
-- first 200 digits of cendree-adic expansions of even numbers
|
||||
adics = evens 200 2 $ 0:0:cycle [1,-1]
|
||||
adic2 = adics !! 1
|
||||
-- Expansion of 4, generated from incrementing the expansion of 2 twice
|
||||
adic4 = adics !! 2
|
||||
adic4' = truncadic 2 200 $ (++repeat 0) $ map (*2) adic2
|
||||
-- Expansion of 4, generated from pointwise multiplication of the expansion of 2
|
||||
adic4' = truncadic 200 2 $ (++repeat 0) $ map (*2) adic2
|
||||
|
||||
-- note: `truncadic 2 n $ 4:repeat 0` is the aggressive application of the carry to 4
|
||||
-- Note: `truncadic 200 n $ 4:repeat 0` is the aggressive application of the carry to 4
|
||||
-- but this could be done better since there are only 3 terms in the head, and no higher
|
||||
-- series terms get in the way; the remainders would be emitted while the thunk continued
|
||||
|
||||
truncadicQR' qr n b = (!! n) . iterate (borrow2' qr b)
|
||||
truncadicQR qr n b = take n . truncadicQR' qr n b
|
||||
|
||||
-- Given an amount of digits `n`, a 2-wide borrow `b`, and a canonical representation of `b`
|
||||
-- construct integer multiples of `b`
|
||||
evensQR qr n b = ([0]:) . map (take n) . iterate addb
|
||||
-- add b to an adic expansion
|
||||
where addb = truncadicQR' qr n b . (b:) . tail
|
||||
|
||||
data QRMethod = QuotRem | DivMod deriving Show
|
||||
qrMethod QuotRem = quotRem
|
||||
qrMethod DivMod = divMod
|
||||
|
||||
cendreeEvens :: QRMethod -> Int -> Int -> IO ()
|
||||
cendreeEvens qr m n = writeFile fn $ unlines $ take m $ map show $ evensQR qr' n 2 $ truncadicQR qr' (n + 2) 2 $ 2:repeat 0
|
||||
where qr' = qrMethod qr
|
||||
fn = "cendree_" ++ show qr ++ "_count_" ++ show m ++ "_" ++ show n ++ "_digits.txt"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user