begin writing 4-appendix

This commit is contained in:
queue-miscreant 2025-02-26 07:04:25 -06:00
parent f864bea4d6
commit 4f09c75006
3 changed files with 443 additions and 26 deletions

View 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).

View 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()

View File

@ -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"