From 5cb572e3e16f3ff6e71e105238190898237248aa Mon Sep 17 00:00:00 2001 From: queue-miscreant Date: Thu, 20 Feb 2025 20:16:19 -0600 Subject: [PATCH] mostly finish making polycount.cell1 renderable --- polycount/cell1/.gitignore | 1 + polycount/cell1/carry2d/expansion.py | 96 ++++++++ polycount/cell1/index.qmd | 326 ++++++++++++++------------- polycount/cell1/todo.txt | 5 + 4 files changed, 275 insertions(+), 153 deletions(-) create mode 100644 polycount/cell1/.gitignore create mode 100644 polycount/cell1/carry2d/expansion.py create mode 100644 polycount/cell1/todo.txt diff --git a/polycount/cell1/.gitignore b/polycount/cell1/.gitignore new file mode 100644 index 0000000..2641667 --- /dev/null +++ b/polycount/cell1/.gitignore @@ -0,0 +1 @@ +*.mp4 diff --git a/polycount/cell1/carry2d/expansion.py b/polycount/cell1/carry2d/expansion.py new file mode 100644 index 0000000..7c3b25d --- /dev/null +++ b/polycount/cell1/carry2d/expansion.py @@ -0,0 +1,96 @@ +from sympy.abc import x, y +import numpy as np + +from .carry import Carry + + +def add( + expansion: np.ndarray, carry: Carry, val: int = 1, center: tuple[int, int] = (0, 0) +): + """ + Add `val` to a position in an `expansion` designated the `center`, then apply `carry`. + """ + expansion[center] += val + carry.apply(expansion) + return expansion + + +def times(expansion: np.ndarray, carry: Carry, val: int): + """ + Multiply all digits in an `expansion` by `val`, then apply `carry`. + """ + expansion *= val + carry.apply(expansion) + return expansion + + +def poly_from_array( + array: np.ndarray, center: tuple[int, int] = (0, 0), x_var=x, y_var=y +): + """ + Convert a numpy array to a polynomial in two variables. + Note that indices are row-major in Numpy, so center is in the form (y, x). + """ + ret = 0 + for i, row in enumerate(array): + for j, val in enumerate(row): + ret += val * (x_var ** (i - center[1]) * y_var ** (j - center[0])) + return ret + + +def _first_nonzero(arr: np.ndarray, axis: int): + """ + Find the index of the first nonzero entry of `arr` along `axis`. + Modified from https://stackoverflow.com/questions/47269390 + """ + mask = arr != 0 + return min( + np.where(mask.any(axis=axis), mask.argmax(axis=axis), arr.shape[axis] + 1) + ) + + +def latex_polynumber( + arr: np.ndarray, center: tuple[int, int] | None = None, show_zero: bool = False +): + """ + Convert a 2D Numpy array into a LaTeX array representing a polynomial in two variables. + Vertical lines and `\\hline`s are placed to indicate the transition between negative + and nonnegative powers, similar to a typical fractional separator (decimal point). + + By default, this assumes the constant term is in position (0, 0) of the array. + It may be alternatively supplied by `center`, which designates `arr[center]` as the constant term. + Note that indices are row-major in Numpy, so `center` is in the form (y, x). + + `show_zero` defaults to False, which causes entries in `arr` which are 0 to not be drawn. + When true, all 0 entries will be drawn. + """ + upper_left = _first_nonzero(arr, 0), _first_nonzero(arr, 1) + lower_right = ( + len(arr) - _first_nonzero(np.flip(arr, 0), 0) - 1, + len(arr[1]) - _first_nonzero(np.flip(arr, 1), 1) - 1, + ) + + center_left, center_top = center or (0, 0) + if center is not None: + # this does not need offset, since we iterate over this range + center_top = center[0] + # but this does for the array environment argument + center_left = center[1] - upper_left[1] + + num_columns = lower_right[1] - upper_left[1] + column_layout = ("c" * center_left) + "|" + ("c" * (num_columns - center_left)) + + # build array output + ret = "\\begin{array}{" + column_layout + "}" + for i in range(upper_left[0], lower_right[0] + 1): + if i == center_top: + ret += " \\hline " + ret += " & ".join( + [ + str(arr[i, j] if arr[i, j] != 0 or show_zero else "") + for j in range(upper_left[1], lower_right[1] + 1) + ] + ) + # next row + ret += " \\\\ " + return ret + "\\end{array}" diff --git a/polycount/cell1/index.qmd b/polycount/cell1/index.qmd index 1cc9385..fc7a037 100644 --- a/polycount/cell1/index.qmd +++ b/polycount/cell1/index.qmd @@ -1,20 +1,21 @@ --- title: "Counting in 2D: Lines, Leaves, and Sand" +draft: true format: html: html-math-method: katex date: "2021-02-23" -date-modified: "2025-2-14" +date-modified: "2025-2-20" jupyter: python3 categories: - algebra - python execute: - eval: false + echo: false --- ```{python} -#| echo: false +from pathlib import Path import sympy from sympy.abc import x, y @@ -40,7 +41,8 @@ from carry2d.expansion import ( Previously, I've written about positional systems and so-called "[polynomial counting](../1)". -Basically, this generalizes familiar integer bases like two and ten into less familiar irrational ones like the golden ratio. +Basically, this generalizes familiar integer bases like two and ten into less familiar irrational ones + like the golden ratio. However, all systems I presented have something in common: they all use polynomials of a single variable. Does it make sense to count in two variables? @@ -259,21 +261,23 @@ An additional property of this system is that the number of "1"s that appear in These two properties make it a sort of hybrid between unary and binary, bridging their binary expansion and its [Hamming weight](https://en.wikipedia.org/wiki/Hamming_weight). -This notation is very heavy, but fortunately lends itself well visualization. +This notation is very heavy, but fortunately lends itself well to visualization. Each integer's expansion is more or less an image, so we can arrange each as frames in a video: ```{python} -#| code-fold: true +if not Path("./count_xy2.mp4").exists(): + animate_carry_count( + carry = Carry([ + [-2, 1], + [ 1, 0] + ]), + frames=list(range(1024)) + ).save( + "./count_xy2.mp4", + fps=60 + ) -animate_carry_count( - carry = Carry([ - [-2, 1], - [ 1, 0] - ]), - frames=list(range(200)) -).save("count_xy2.mp4") - -Video("count_xy2.mp4") +Video("./count_xy2.mp4") ``` Starting at the expansion of twelve, the furthest extent is not in the first column or row. @@ -289,32 +293,45 @@ Fortunately, this is a simple enough system that we can scalar-multiply base exp Ascending the powers of *n* in this manner for the carries where *n* = 3 and 4: ```{python} -#| code-fold: true #| layout-ncol: 2 -animate_carry_count( - carry = Carry([ - [-3, 1], - [ 1, 0] - ]), - operation="multiply", - op_val=3, - frames=list(range(200)) -).save("count_xy3.mp4") +# Run the animations until we overflow in the standard 100x100 range +if not Path("./count_xy3.mp4").exists(): + try: + animate_carry_count( + carry = Carry([ + [-3, 1], + [ 1, 0] + ]), + operation="multiply", + op_val=3, + frames=list(range(200)) + ).save("count_xy3.mp4") + except ValueError: + pass -animate_carry_count( - carry = Carry([ - [-4, 1], - [ 1, 0] - ]), - operation="multiply", - op_val=4, - frames=list(range(200)), -).save("count_xy4.mp4") +if not Path("./count_xy4.mp4").exists(): + try: + animate_carry_count( + carry = Carry([ + [-4, 1], + [ 1, 0] + ]), + operation="multiply", + op_val=4, + frames=list(range(200)), + ).save("count_xy4.mp4") + except ValueError: + pass display( - Video("count_xy3.mp4"), - Video("count_xy4.mp4") + Video("./count_xy3.mp4"), + Video("./count_xy4.mp4") +) + +display_latex( + Latex("$$x + y = 3$$"), + Latex("$$x + y = 4$$"), ) ``` @@ -342,10 +359,9 @@ Taking *x* or *y* to higher powers will dilate expansions along that axis. The carry is not the only curve which exists in a system, as each integer is also a polynomial unto itself. Adding a constant term will not change anything unless it causes carries to occur. -Starting at the carry digit, some of the curves made by the carry $x + y = 2$ are shown below. +Starting at the carry digit, some of the *incremented curves* made by the carry $x + y = 2$ are shown below. ```{python} -#| code-fold: true #| layout: [[3, 3], [2, 2, 2]] expansion = np.zeros((50, 50), dtype=np.int64) @@ -386,12 +402,11 @@ This curve happens to intersect with the *x* and *y* axes, which we can interpre For dibinary, these are the aforementioned "typical binary expansions". We can also evaluate the polynomial at $(x, y) = (1, 1)$, a point on the carry curve, to compute its digital root, which is also equal to the integer it represents. - This underlies a key difference from the 1D case. +1D systems are special because the carry can be equated with a base (or bases), which + is a discrete, zero-dimensional point (one dimension less) on a number line. In higher dimensions, there is no proper "base" we can plug in to "test" that an expansion is valid like we can with the golden ratio in phinary. -1D systems are special in this regard: the carry can be equated with a base (or bases), which - is a discrete zero-dimensional point on a number line. Casting the Line @@ -449,8 +464,6 @@ The expansion in the *y* column looks trivially correct, since $y^2 = 9$ when $x We can check the base of the expansion of nine ("2100") by factoring a polynomial: ```{python} -#| code-fold: true - nine_x2y3 = 2*x**3 + x**2 - 9 display_latex( @@ -469,32 +482,38 @@ This transposes the carry (and hence the expansion). For good measure, let's see what it looks like when we count in the $n = 3$ and $n = 4$ cases. ```{python} -#| code-fold: true #| layout-ncol: 2 -animate_carry_count( - carry = Carry([ - [-3, 2], - [ 1, 0] - ]), - operation="add", - op_val=3, - frames=list(range(200)) -).save("count_x2y3.mp4") +if not Path("./count_x2y3.mp4").exists(): + animate_carry_count( + carry = Carry([ + [-3, 2], + [ 1, 0] + ]), + operation="add", + op_val=3, + frames=list(range(200)) + ).save("./count_x2y3.mp4") -animate_carry_count( - carry = Carry([ - [-4, 3], - [ 1, 0] - ]), - operation="add", - op_val=4, - frames=list(range(200)), -).save("count_x3y4.mp4") +if not Path("./count_x3y4.mp4").exists(): + animate_carry_count( + carry = Carry([ + [-4, 3], + [ 1, 0] + ]), + operation="add", + op_val=4, + frames=list(range(200)), + ).save("./count_x3y4.mp4") display( - Video("count_x2y3.mp4"), - Video("count_x3y4.mp4") + Video("./count_x2y3.mp4"), + Video("./count_x3y4.mp4"), +) + +display_latex( + Latex("$$2x + y = 3$$"), + Latex("$$3x + y = 4$$"), ) ``` @@ -512,12 +531,11 @@ The binary alphabet is the smallest possible, so the minimal shrinkage is in dib ### Incremented Curves -Below are the incremented curves for $n = 3$. +Below are some incremented curves for $n = 3$. These curves show an interesting phenomenon: an extra line is present in in the expansion of six. This line turns into a spike at twelve, then at fifteen, it seems to grow to encompass the point at infinity. ```{python} -#| code-fold: true #| layout-ncol: 2 x2y3 = Carry([ @@ -525,17 +543,15 @@ x2y3 = Carry([ [ 1, 0] ]) -expansion = np.zeros((50, 50), dtype=np.int64) -last_i = 0 for i in [3, 6, 12, 15]: - carry_add(expansion, x2y3, i - last_i) + expansion = np.zeros((50, 50), dtype=np.int64) + carry_add(expansion, x2y3, i) plot_implicit( poly_from_array(expansion) - i, (x, -10, 10), (y, -10, 10), title=f"{i}", ) - last_i = i ``` @@ -543,77 +559,13 @@ Leaves in the Mirror -------------------- Algebraic curves are not my forte. -However, I believe it would be a good idea to briefly examine some classical examples. - -All Fermat curves $x^n + y^n = 1$ are not suitable as carries, since their digital root is unbounded. -Scaling up the curve, as by $x^n + y^n = 2$ will just degenerate back to the previous examples with lines, - but spaced out by zeros between entries. - -The [folium of Descartes](https://en.wikipedia.org/wiki/Folium_of_Descartes) is another - classical curve that played a role in the development of calculus, described by the equation: - -$$ -\begin{gather*} - x^3 + y^3 - 3axy = 0 - \\ \\ - \begin{array}{|c} \hline - 0 & 0 & 0 & 1 \\ - 0 & -3a \\ - 0 & \\ - 1 - \end{array} -\end{gather*} -$$ - -The position of the 3*a* term may be confusing. -As the negative coefficient, we must place it atop the digit we are carrying, meaning that - expansions will propagate toward negative powers of *x* and *y* as well as positive. -While the shape of the curve is different from an ordinary line, the coefficients are arranged - too similarly; for $a = 2/3$ (so the curve passes through the point (1, 1)) incrementing appears as: - -```{python} -#| echo: false -#| layout-ncol: 2 - -folium = Carry([ - [ 0, 0, 0, 1], - [ 0,-2, 0, 0], - [ 0, 0, 0, 0], - [ 1, 0, 0, 0], -]) -dims = (50, 50) -center = (dims[0] // 2, dims[1] // 2) -two_folium = carry_add(np.zeros(dims, dtype=np.int64), folium, 2, center=center) -four_folium = carry_add(np.zeros(dims, dtype=np.int64), folium, 4, center=center) - -display_latex( - Latex( - r"\begin{gather*}" - + "2: &" - + latex_polynumber(two_folium, center=center, show_zero=True) + r"\\" - + r"\\ \\ 4: &" - + latex_polynumber(four_folium, center=center, show_zero=True) + r"\\" - + r"\end{gather*}" - ) -) - -animate_carry_count( - carry=folium, - operation="add", - op_val=4, - frames=list(range(200)), -).save("count_folium.mp4") - -display(Video("count_folium.mp4")) -``` - -Clearly, this also tends back toward dibinary, but with the yellow digits spaced out more. +Still, we can very briefly look at some classical examples from the perspective of carries. -### Discounting Curves +### Quadratic Curves -The interpretation of the carry as a curve is secondary, so I'd like to disqualify - some archetypal quadratic objects. +Unfortunately, the most common quadratic curves are largely uninteresting. +Briefly, here are some comments about some the simplest cases: - Unit circle ($x^2 + y^2 - 1 = 0$) and hyperbola ($\pm x^2 \mp y^2 - 1 = 0$) - The sum of coefficients is positive, which means that expansions are unbounded. @@ -632,11 +584,81 @@ Other polynomials in *x* and *y* may produce rotations or dilations of these cur What truly matters is that one cell affects relatively close cells in a certain way. +### Higher-Order Curves + +All unit Fermat curves $x^n + y^n = 1$ are not suitable as carries, since their digital root is unbounded. +Scaling up the curve, for example to $x^n + y^n = 2$, will just degenerate back to the previous + examples with lines, but spaced out by zeros between entries. + +The [folium of Descartes](https://en.wikipedia.org/wiki/Folium_of_Descartes) is another + classical curve that played a role in the development of calculus, described by the equation: + +$$ +\begin{gather*} + x^3 + y^3 - 3axy = 0 + \\ \\ + \begin{array}{|c} \hline + 0 & 0 & 0 & 1 \\ + 0 & -3a \\ + 0 & \\ + 1 + \end{array} +\end{gather*} +$$ + +As written above, the position of the 3*a* term may be confusing. +As the negative term in an explicit carry, we must place it atop the digit we are carrying, + meaning that expansions will propagate toward negative powers of *x* and *y* as well as positive. +While the shape of the curve is different from an ordinary line, the coefficients are arranged + too similarly; for $a = 2/3$ (so the curve passes through the point $(1, 1)$) incrementing appears as: + +```{python} +#| layout-ncol: 2 + +folium_carry = Carry([ + [ 0, 0, 0, 1], + [ 0,-2, 0, 0], + [ 0, 0, 0, 0], + [ 1, 0, 0, 0], +]) +folium_dims = (50, 50) +folium_center = (folium_dims[0] // 2, folium_dims[1] // 2) +folium_two = carry_add(np.zeros(folium_dims, dtype=np.int64), folium_carry, 2, center=folium_center) +folium_four = carry_add(np.zeros(folium_dims, dtype=np.int64), folium_carry, 4, center=folium_center) + +display_latex( + Latex( + r"\begin{gather*}" + + "2: &" + + latex_polynumber(folium_two, center=folium_center, show_zero=True) + r"\\" + + r"\\ \\ 4: &" + + latex_polynumber(folium_four, center=folium_center, show_zero=True) + r"\\" + + r"\end{gather*}" + ) +) + +if not Path("./count_folium.mp4").exists(): + try: + animate_carry_count( + carry=folium_carry, + operation="add", + op_val=4, + frames=list(range(200)), + ).save("./count_folium.mp4") + except ValueError: + pass + +display(Video("./count_folium.mp4")) +``` + +Clearly, this also tends back toward dibinary, but with the yellow digits spaced out more. + + Laplace's Sandstorm ------------------- As stated previously, polynomials naturally multiply by convolution. -In this context, a form which frequently arises is an approximation of the 2D Laplacian. +In the context of this operation, the discrete 2D Laplacian is frequently used While commonly presented as a matrix, it has very little to do with the machinery of linear algebra which normally empowers matrices. @@ -659,8 +681,6 @@ $$ $$ ```{python} -#| echo: false - plot_implicit(x + y + x*y**2 + y*x**2 - 4*x*y, (x, -10, 10), (y, -10, 10)) ``` ::: @@ -718,21 +738,21 @@ As a cellular automaton, it is rather popular as a coding challenge These videos discuss toppling the initial value, but do not point out the analogy to polynomial expansions. ```{python} -#| code-fold: true #| layout-ncol: 2 -animate_carry_count( - carry = Carry([ - [ 0, 1, 0], - [ 1,-4, 1], - [ 0, 1, 0], - ]), - operation="add", - op_val=4, - frames=list(range(200)), -).save("count_laplace.mp4") +if not Path("./count_x2y3.mp4").exists(): + animate_carry_count( + carry = Carry([ + [ 0, 1, 0], + [ 1,-4, 1], + [ 0, 1, 0], + ]), + operation="add", + op_val=4, + frames=list(range(200)), + ).save("./count_laplace.mp4") -Video("count_laplace.mp4") +Video("./count_laplace.mp4") ``` The shapes produced by this pattern are well-known, with larger numbers appearing as fractals. diff --git a/polycount/cell1/todo.txt b/polycount/cell1/todo.txt new file mode 100644 index 0000000..b9bd8d8 --- /dev/null +++ b/polycount/cell1/todo.txt @@ -0,0 +1,5 @@ +mp4s saved do not get copied or linked over to the rendering folder + +center figures + +make sure to object-fit: scale