mostly finish making polycount.cell1 renderable
This commit is contained in:
parent
9838e0d5cf
commit
5cb572e3e1
1
polycount/cell1/.gitignore
vendored
Normal file
1
polycount/cell1/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.mp4
|
||||
96
polycount/cell1/carry2d/expansion.py
Normal file
96
polycount/cell1/carry2d/expansion.py
Normal 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}"
|
||||
@ -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
5
polycount/cell1/todo.txt
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user