From 4f09c750064c781f0503eb5e0ff320dd998cbf5a Mon Sep 17 00:00:00 2001 From: queue-miscreant Date: Wed, 26 Feb 2025 07:04:25 -0600 Subject: [PATCH] begin writing 4-appendix --- posts/polycount/4/appendix/index.qmd | 165 ++++++++++++++++++++ posts/polycount/4/appendix/kadic.py | 225 +++++++++++++++++++++++++++ posts/polycount/4/cendree.hs | 79 ++++++---- 3 files changed, 443 insertions(+), 26 deletions(-) create mode 100644 posts/polycount/4/appendix/index.qmd create mode 100644 posts/polycount/4/appendix/kadic.py diff --git a/posts/polycount/4/appendix/index.qmd b/posts/polycount/4/appendix/index.qmd new file mode 100644 index 0000000..53746a3 --- /dev/null +++ b/posts/polycount/4/appendix/index.qmd @@ -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). diff --git a/posts/polycount/4/appendix/kadic.py b/posts/polycount/4/appendix/kadic.py new file mode 100644 index 0000000..bfd9364 --- /dev/null +++ b/posts/polycount/4/appendix/kadic.py @@ -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() diff --git a/posts/polycount/4/cendree.hs b/posts/polycount/4/cendree.hs index 50ae59d..1e07f77 100644 --- a/posts/polycount/4/cendree.hs +++ b/posts/polycount/4/cendree.hs @@ -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"