mostly finish making polycount.cell1 renderable

This commit is contained in:
queue-miscreant 2025-02-20 20:16:19 -06:00
parent 9838e0d5cf
commit 5cb572e3e1
4 changed files with 275 additions and 153 deletions

1
polycount/cell1/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.mp4

View File

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

View File

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

5
polycount/cell1/todo.txt Normal file
View File

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