From bce371e2a8afe4a51d19ee4963edb7fbde953efc Mon Sep 17 00:00:00 2001 From: queue-miscreant Date: Sun, 23 Feb 2025 18:09:37 -0600 Subject: [PATCH] revisions to polycount.cell2 --- .gitignore | 1 + polycount/4/index.qmd | 2 +- polycount/5/index.qmd | 4 +- polycount/cell1/carry2d/anim.py | 27 +- polycount/cell1/carry2d/expansion.py | 39 ++ polycount/cell1/index.qmd | 2 +- polycount/cell2/.gitignore | 1 + polycount/cell2/carry2d | 1 + polycount/cell2/collinear_carries.png | Bin 0 -> 13938 bytes polycount/cell2/first_implicit.png | Bin 0 -> 19793 bytes polycount/cell2/index.qmd | 834 +++++++++++++++++++------- polycount/cell2/polyproduct.png | Bin 0 -> 10919 bytes 12 files changed, 663 insertions(+), 248 deletions(-) create mode 100644 polycount/cell2/.gitignore create mode 120000 polycount/cell2/carry2d create mode 100644 polycount/cell2/collinear_carries.png create mode 100644 polycount/cell2/first_implicit.png create mode 100644 polycount/cell2/polyproduct.png diff --git a/.gitignore b/.gitignore index e4b5016..0d27235 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ _site/ /.quarto/ index_files/ .jupyter_cache/ +*.quarto_ipynb __pycache__/ diff --git a/polycount/4/index.qmd b/polycount/4/index.qmd index 017e8f9..e2e9e4d 100644 --- a/polycount/4/index.qmd +++ b/polycount/4/index.qmd @@ -3,7 +3,7 @@ title: "Polynomial Counting 4: Two 2's" format: html: html-math-method: katex -date: "2021-02-06" +date: "2021-02-09" date-modified: "2025-02-12" jupyter: python3 categories: diff --git a/polycount/5/index.qmd b/polycount/5/index.qmd index 1249daf..72461d1 100644 --- a/polycount/5/index.qmd +++ b/polycount/5/index.qmd @@ -3,8 +3,8 @@ title: "Polynomial Counting 5: Pentamerous multiplication" format: html: html-math-method: katex -date: "2021-02-09" -date-modified: "2025-2-12" +date: "2021-02-12" +date-modified: "2025-02-12" jupyter: python3 categories: - algebra diff --git a/polycount/cell1/carry2d/anim.py b/polycount/cell1/carry2d/anim.py index db3bcb6..bc89949 100644 --- a/polycount/cell1/carry2d/anim.py +++ b/polycount/cell1/carry2d/anim.py @@ -22,7 +22,7 @@ def animate( def animate_carry_count( carry: Carry, dims: int = 100, - operation: Literal["add", "multiply"] = "add", + operation: Literal["add", "multiply", "log"] = "add", op_val=1, center: tuple[int, int] | None = None, # Animation parameters @@ -49,18 +49,18 @@ def animate_carry_count( else: center = (0, 0) - if operation == "add": - next_func = partial(add, carry=carry, val=op_val, center=center) + if operation == "add" or operation == "log": + next_func = partial(add, carry=carry, center=center) elif operation == "multiply": if op_val == 1: raise ValueError(f"too small value {op_val} for repeated multiplication") - next_func = partial(times, carry=carry, val=op_val) + next_func = partial(times, carry=carry) else: raise ValueError(f"Cannot use {repr(operation)} for animation") expansion = np.zeros((dims, dims)) expansion[center] = 1 - title = [1] + state = [1, op_val, op_val] fig = plt.gcf() @@ -69,21 +69,26 @@ def animate_carry_count( extent=[-center[0], dims - center[0], dims - center[1], -center[1]], # type: ignore ) def init(): - plt.title(f"{title[0]}") + plt.title(f"{state[0]}") image.set_clim(0, carry.overflow - 1) fig.tight_layout() @animate(fig, frames, interval=interval, init_func=init) def ret(_): - plt.title(f"{title[0]}") + title, op_val, old_op_val = state + plt.title(f"{title}") image.set_data(expansion) fig.tight_layout() - next_func(expansion) + next_func(expansion, val=op_val) if operation == "add": - title[0] += op_val - else: - title[0] *= op_val + state[0] += op_val + elif operation == "multiply": + state[0] *= op_val + elif operation == "log": + state[0] += op_val + if state[0] >= op_val * old_op_val * old_op_val: + state[1] *= old_op_val return ret diff --git a/polycount/cell1/carry2d/expansion.py b/polycount/cell1/carry2d/expansion.py index 7c3b25d..ef85267 100644 --- a/polycount/cell1/carry2d/expansion.py +++ b/polycount/cell1/carry2d/expansion.py @@ -1,4 +1,6 @@ from sympy.abc import x, y +import sympy +from sympy.polys.polytools import degree import numpy as np from .carry import Carry @@ -38,6 +40,43 @@ def poly_from_array( return ret +def poly_to_array( + poly: sympy.Expr, + alloc: np.ndarray | None = None, + x_var=x, + y_var=y +) -> np.ndarray: + '''Convert a polynomial of 2 variables to a 2D array''' + if isinstance(poly, (int, sympy.core.numbers.Zero)): + if alloc is None: + alloc = np.zeros((1, 1), dtype=np.int32) + alloc[0,0] = int(poly) + return alloc + alloc.fill(0) + alloc[0,0] = int(poly) + return alloc + + coeffs = list(sorted( + map( + lambda z: (z, (degree(z[0], x_var), degree(z[0], y_var))), # type: ignore + poly.as_coefficients_dict().items() + ), + key=lambda z: sum(z[1]) + )) + + max_x, max_y = coeffs[-1][1] + if alloc is None: + alloc = np.zeros((max_x + max_y + 1, max_x + max_y + 1), dtype=np.int32) + elif alloc.shape[0] < (max_x + 1) or alloc.shape[1] < (max_y + 1): + raise ValueError("return array has insufficient size") + else: + alloc.fill(0) + + for (_, coeff), (x_deg, y_deg) in coeffs: + alloc[y_deg, x_deg] = coeff + return alloc + + def _first_nonzero(arr: np.ndarray, axis: int): """ Find the index of the first nonzero entry of `arr` along `axis`. diff --git a/polycount/cell1/index.qmd b/polycount/cell1/index.qmd index fc7a037..3f8076f 100644 --- a/polycount/cell1/index.qmd +++ b/polycount/cell1/index.qmd @@ -5,7 +5,7 @@ format: html: html-math-method: katex date: "2021-02-23" -date-modified: "2025-2-20" +date-modified: "2025-02-20" jupyter: python3 categories: - algebra diff --git a/polycount/cell2/.gitignore b/polycount/cell2/.gitignore new file mode 100644 index 0000000..2641667 --- /dev/null +++ b/polycount/cell2/.gitignore @@ -0,0 +1 @@ +*.mp4 diff --git a/polycount/cell2/carry2d b/polycount/cell2/carry2d new file mode 120000 index 0000000..c76aa09 --- /dev/null +++ b/polycount/cell2/carry2d @@ -0,0 +1 @@ +../cell1/carry2d \ No newline at end of file diff --git a/polycount/cell2/collinear_carries.png b/polycount/cell2/collinear_carries.png new file mode 100644 index 0000000000000000000000000000000000000000..f73f0ec83b67e6ca7b503a1362a77bf4ae027f1d GIT binary patch literal 13938 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 literal 0 HcmV?d00001 diff --git a/polycount/cell2/first_implicit.png b/polycount/cell2/first_implicit.png new file mode 100644 index 0000000000000000000000000000000000000000..19f6a5728c870ec448a7aa47fcefbceaff506472 GIT binary patch literal 19793 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 rI`QX(-NgMR +![](./polyproduct.png) -It could obviously be improved by showing the coefficients in the picture and using color to only indicate sign. However, there is a benefit to hiding coefficients behind solid colors. For instance, I don't need to recognize numerals to pinpoint what the product should look like. +Above is a screenshot of (my admittedly low-effort) attempt to ameliorate these shortcomings. +The left image is clickable, with the coefficients incrementing with left clicks and decrementing with right. +The text box controls the second multiplicand in the center image by selecting a cyclotomic polynomial + and evaluating it with a particular expression. +The right image is the product. + +It could obviously be improved by showing the coefficients in the picture and using color to only indicate sign. +However, there is a benefit to hiding coefficients behind solid colors. +For instance, I don't need to recognize numerals to pinpoint what the product should look like. ### Degenerate Expressions -I have chosen to evaluate the right term (center image) at *x* + *y* for a reason. Choosing *x* or *y* on their own is a poor decision because they change nothing from the one dimensional case. Put another way, when arranged in a grid, the coefficients are all collinear. At first blush, *xy* seems like a decent pick, but it has the exact same problem; the line simply extends along the diagonal instead. The opposite case $\frac x y$ might not make sense, but the denominators can be cleared by multiplying by the appropriate power of *y*. However, this makes the polynomial homogeneous, or in other words, collinear along the anti-diagonals, and has the exact same problem as the product. +I have chosen to evaluate the right term (center image) at $x + y$ for a reason. +Choosing *x* or *y* on their own is a poor decision because they change nothing from the one dimensional case. +Put another way, when arranged in a grid, the coefficients are all collinear. +At first blush, *xy* seems like a decent pick, but it has the exact same problem; + the line simply extends along the diagonal instead. +$x/y$ has the same problem, but it is instead collinear along the anti-diagonals. +We allow negative exponents for the same reason we can carry at any location in an expansion -![]() +```{python} +import sympy +from sympy.abc import x, y, z -It should go without saying that these are all degenerate, and can only span a line. On the other hand, expressions like the sum of *x* and *y* form triangles, 2D shapes from which it is possible to build our goal polynomial. Since *x* + *y* is symmetric in *x* and *y*, it is expected that the products have a similar sense of symmetry. +def operation_plots(): + six = z**2 - z + 1 + x6 = six.replace(z, x).expand() + y6 = six.replace(z, y).expand() + xy6 = six.replace(z, x*y).expand() + x_y6 = (six.replace(z, x/y)).expand() + + image = np.zeros((lambda q: (q,q))(3), dtype=np.int32) + + for i, (poly, title) in enumerate(zip( + [x6, y6, xy6, x_y6], + ["x", "y", "xy", "\\frac{x}{y}"] + )): + plt.subplot(2, 2, i + 1) + if i != 3: + plt.imshow(poly_to_array(poly, image)) + plt.xticks([0,1,2]) + plt.yticks([0,1,2]) + else: + plt.imshow(poly_to_array((poly * y**2).expand(), image), extent=(-2.5, 0.5, 0.5, -2.5)) + plt.xticks([-2,-1,0]) + plt.yticks([-2,-1,0]) + plt.title(f"$\\Phi_6({title}) = {sympy.latex(poly)}$") + + plt.tight_layout() + +operation_plots() +``` + +It should go without saying that these are all degenerate, and can only span a line. +On the other hand, expressions like the sum of *x* and *y* form triangles, 2D shapes + from which it is possible to build our goal polynomial. +Since $x + y$ is symmetric in *x* and *y*, it is expected that the products have a similar sense of symmetry. First Results ------------- -![]() +![](./first_implicit.png) -The first explicit product I found is above, which is reminiscent of the Folium of Descartes. The implicit carry (the left multiplicand) describes the rearrangement of coefficients in a backwards L-shape to and from a sparser upside-down L-shape. The property of passing through (1, 1) comes from this factor; this also illustrates that unlike polynomials in a single variable, the sum of the coefficients being zero does not imply the existence of a one-variable factor like *x* - 1. +The first explicit product I was able to find is above. +It can be decomposed as the folium of Descartes, but with an extra term to the upper-left of the pseudo-base. +The implicit carry (the left multiplicand) describes the rearrangement of coefficients + in a backwards L-shape to and from a sparser upside-down L-shape. +Since the coefficients of this factor sum to zero, it passes through $(1, 1)$. $$ \begin{gather*} -\text{Folium} & ~ & \text{New carry} \\ -2xy - x^3 - y^3 & ~ & -3xy - x^3 - y^3 - 1 \\ -\begin{array}{|c} \hline -0 & 0 & 0 & \bar{1} \\ -0 & 2 \\ 0 \\ \bar{1} -\end{array} & ~ & -\begin{array}{|c} \hline -\bar{1} & 0 & 0 & \bar{1} \\ -0 & 3 \\ 0 \\ \bar{1} -\end{array} + \text{Folium} & ~ & \text{New carry} \\ + x^3 + y^3 - 2xy & ~ & + x^3 + y^3 + 1 - 3xy \\ + \begin{array}{|c} \hline + 0 & 0 & 0 & 1 \\ + 0 & \bar{2} \\ + 0 \\ + 1 + \end{array} & ~ & + \begin{array}{|c} \hline + 1 & 0 & 0 & 1 \\ + 0 & \bar{3} \\ + 0 \\ + 1 + \end{array} \end{gather*} $$ ### Cousin Systems -My first thought upon seeing this carry was to see what makes counting in this system different from "neighbors", by which I mean moving the $x^3$ and $y^3$ terms further inward and outward. +My first thought upon seeing this carry was to see what makes counting in this system different + from its "neighbors". +By this, I mean moving the $x^3$ and $y^3$ terms further inward and outward. $$ \begin{gather*} -3xy - x - y - 1 & -3xy - x^2 - y^2 - 1 \\ -\begin{array}{|c} \hline -\bar{1} & \bar{1} \vphantom{2^{2^2}} \\ -\bar{1} & 3 \\ -\end{array} & -\begin{array}{|c} \hline -\bar{1} & 0 & \bar{1} \vphantom{2^{2^2}} \\ -0 & 3 \\ \bar{1} -\end{array} \\ -\textcolor{red} {3xy - x^3 - y^3 - 1} & -3xy - x^4 - y^4 - 1 \\ -\textcolor{red} { \begin{array}{|c} \hline -\bar{1} & 0 & 0 & \bar{1} \vphantom{2^{2^2}} \\ -0 & 3 \\ 0 \\ \bar{1} -\end{array} } & -\begin{array}{|c} \hline -\bar{1} & 0 & 0 & 0 & \bar{1} \vphantom{2^{2^2}} \\ -0 & 3 \\ 0 \\ 0 \\ \bar{1} -\end{array} + x + y + 1 - 3xy & + x^2 + y^2 + 1 - 3xy \\ + \begin{array}{|c} \hline + 1 & 1 \\ + 1 & \bar{3} \\ + \end{array} & + \begin{array}{|c} \hline + 1 & 0 & 1 \\ + 0 & \bar{3} \\ + 1 + \end{array} \\ + \textcolor{red} {x^3 + y^3 + 1 - 3xy} & + x^4 + y^4 + 1 - 3xy \\ + \textcolor{red} { + \begin{array}{|c} \hline + 1 & 0 & 0 & 1 \\ + 0 & \bar{3} \\ + 0 \\ + 1 + \end{array} + } & + \begin{array}{|c} \hline + 1 & 0 & 0 & 0 & 1 \\ + 0 & \bar{3} \\ + 0 \\ + 0 \\ + 1 + \end{array} \end{gather*} $$ -All polynomials besides the one under consideration (highlighted in red) are irreducible. This means that these carries hide no implicit rules, and are as fundamental as the Laplacian or $x + y = 2$. +All polynomials besides the one under consideration (highlighted in red) are irreducible. +This means that these carries hide no implicit rules, and are as fundamental as the Laplacian or $x + y = 2$. Analyzing the carry vectors, it is clear that the pattern produced by each should be different: $$ \begin{gather*} -3xy - x - y - 1 & -3xy - x^2 - y^2 - 1 \\ -\begin{matrix} -\langle -1, 0 \rangle \\ -\langle -1, -1 \rangle \\ -\langle 0, -1 \rangle -\end{matrix} & -\begin{matrix} -\langle -1, 1 \rangle \\ -\langle -1, -1 \rangle \\ -\langle 1, -1 \rangle -\end{matrix} \\ -3xy - x^3 - y^3 - 1 & -3xy - x^4 - y^4 - 1 \\ -\begin{matrix} -\langle -1, 2 \rangle \\ -\langle -1, -1 \rangle \\ -\langle 2, -1 \rangle -\end{matrix} & -\begin{matrix} -\langle -1, 3 \rangle \\ -\langle -1, -1 \rangle \\ -\langle 3, -1 \rangle -\end{matrix} \\ + x^{-1} y^{0} + x^{-1} y^{-1} + x^{0} y^{-1} - 3 & + x^{-1} y^{1} + x^{-1} y^{-1} + x^{1} y^{-1} - 3 \\ + \begin{matrix} + \langle -1, 0 \rangle \\ + \langle -1, -1 \rangle \\ + \langle 0, -1 \rangle + \end{matrix} & + \begin{matrix} + \langle -1, 1 \rangle \\ + \langle -1, -1 \rangle \\ + \langle 1, -1 \rangle + \end{matrix} \\ + x^{-1} y^{2} + x^{-1} y^{-1} + x^{2} y^{-1} - 3 & + x^{-1} y^{3} + x^{-1} y^{-1} + x^{3} y^{-1} - 3 \\ + \begin{matrix} + \langle -1, 2 \rangle \\ + \langle -1, -1 \rangle \\ + \langle 2, -1 \rangle + \end{matrix} & + \begin{matrix} + \langle -1, 3 \rangle \\ + \langle -1, -1 \rangle \\ + \langle 3, -1 \rangle + \end{matrix} \\ \end{gather*} $$ -The top-left tends toward a similar shape as the $x + y = 2$ case, since we can't reach the vector inverses. In the top-right carry, all vectors are equidistant and meet at right (and straight) angles. This means that it is a 45° rotation of the altered Laplacian, which spans a half-plane. The remaining two should both propagate into the full plane. Is this reflected in the counting videos? +The vectors in the top-left carry contain only negative numbers, so it will tend toward + the quarter-plane case like $x + y = 2$. +In the top-right carry, all vectors are equidistant and meet at right (and straight) angles. +This means that it is a 45° rotation of the altered Laplacian, which spans a half-plane. +The remaining two should both propagate into the full plane. Is this reflected in the counting videos? -:::{} -Counting in each of the $\ldots - x^n - y^n + \ldots$ systems +::: {} +```{python} +def animate_triangle_carries(**kwargs): + dims = 80 + center = (dims // 2, dims // 2) + + carries = [ + Carry([ + [1, 1], + [1,-3] + ]), + Carry([ + [1, 0, 1], + [ 0,-3, 0], + [ 1, 0, 0] + ]), + Carry([ + [1, 0, 0, 1], + [ 0,-3, 0, 0], + [0, 0, 0, 0], + [1, 0, 0, 0] + ]), + Carry([ + [1, 0, 0, 0, 1], + [ 0,-3, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [1, 0, 0, 0, 0] + ]), + ] + expansions = [np.zeros((dims, dims), dtype=np.int64) for _ in range(4)] + subplots = [None for _ in range(4)] + + fig = plt.gcf() + def init(): + for i in range(len(subplots)): + plt.subplot(2, 2, i + 1) + plt.title(f"$x^{{{i+1}}} + y^{{{i+1}}} + ...$") + subplots[i] = plt.imshow( # type: ignore + expansions[i], + extent=[-center[0], dims - center[0], dims - center[1], -center[1]], # type: ignore + ) + subplots[i].set_clim(0, 2) # type: ignore + fig.tight_layout() + + @animate(fig, init_func=init, **kwargs) + def ret(_): + for i in range(len(expansions)): + add(expansions[i], carries[i], 3, center=center) + subplots[i].set_data(expansions[i]) # type: ignore + + return ret + + +if not Path("./count_triangle_carries.mp4").exists(): + try: + animate_triangle_carries( + frames=list(range(200)) + ).save("./count_triangle_carries.mp4") + except ValueError: + pass + +Video("./count_triangle_carries.mp4") +``` + +Counting in each of the $x^n + y^n + \ldots$ systems ::: -Indeed it is. The first two propagate toward the upper-left, and the remaining two span the entire plane. However, while the discovered carry remarkably remains centered, the final one tends more toward propagating to the lower right. It has somehow exceeded a threshold that the centered one has not. +Indeed it is. +However, while the discovered carry remarkably remains centered, the final one tends more toward + propagating to the lower right. +It has somehow exceeded a threshold that the centered one has not. -Notably, the L-shapes (which I noted can be interchanged) never turn up, and the "initial rule" is useless. Phinary, the simplest implicit case in one dimension, still requires the initial rule to go from the expansion of 2 (10.01) to 3 (100.01). +Incidentally, the L-shapes in the implicit carry (which I noted can be interchanged) never turn up, + and the "initial rule" is useless. +Phinary, the simplest implicit case in one dimension, still requires the initial rule to go + from the expansion of 2 (10.01) to 3 (100.01). Isosceles Rotations ------------------- -Since one of the similar carries produces a rotation of a familiar pattern, it provokes additional questions, such as if we can rotate the centered pattern. If so, is its polynomial also factorable? Naively, the following carry might look similar... +One of the similar carries is a rotation of a pattern we were already familiar with, + like the folium was to $x + y = 2$ +Does this mean we can rotate the centered pattern? +If so, is will the carry polynomial still be factorable? -:::{.column width="40%"} +Naively, the following carry might look similar... + +::: {layout-ncol="2"} $$ \begin{array}{|c} \hline -& & \textcolor{red}{\bar{1}} \\ -& & 3 \\ \textcolor{green}{\bar{1}} -&&& & \textcolor{blue}{\bar{1}} -\end{array} \\ -\begin{matrix} -\textcolor{red}{\langle 0,1 \rangle} \\ -\textcolor{green}{\langle -2,-1 \rangle} \\ -\textcolor{blue}{\langle 2,-1 \rangle} \\ -\text{Angle to top} \approx 116° + & & \textcolor{red}{1} \\ + & & \bar{3} \\ + \textcolor{green}{1} & & & & \textcolor{blue}{1} + \end{array} \\ + \begin{matrix} + \textcolor{red}{\langle 0,-1 \rangle} \\ + \textcolor{green}{\langle -2,1 \rangle} \\ + \textcolor{blue}{\langle 2,1 \rangle} \\ + \text{Angle to top} \approx 116° \end{matrix} $$ + +```{python} +if not Path("./count_bad_rotated_triangle.mp4").exists(): + try: + animate_carry_count( + carry = Carry([ + [ 0, 0, 1, 0, 0], + [ 0, 0,-3, 0, 0], + [ 1, 0, 0, 0, 1], + ]), + operation="add", + op_val=3, + frames=list(range(200)), + ).save("./count_bad_rotated_triangle.mp4") + except ValueError: + pass + +display( + Video("./count_bad_rotated_triangle.mp4"), +) +``` ::: -:::{.column width="40%"} -::: +But there are only even numbers in the first component of each vector +Therefore, the carry below will produce the same pattern. -There are only even numbers in the first component, so we can describe the carry more succinctly as... - -:::{.column width="40%"} +::: {layout-ncol="2"} $$ \begin{array}{|c} \hline -& \textcolor{red}{\bar{1}} \\ -& 3 \\ \textcolor{green}{\bar{1}} -&& \textcolor{blue}{\bar{1}} + & \textcolor{red}{1} \\ + & \bar{3} \\ \textcolor{green}{1} + && \textcolor{blue}{1} \end{array} \\ \begin{matrix} -\textcolor{red}{\langle 0,1 \rangle} \\ -\textcolor{green}{\langle -1,-1 \rangle} \\ -\textcolor{blue}{\langle 1,-1 \rangle} \\ -\text{Angle to top} = 135° + \textcolor{red}{\langle 0,-1 \rangle} \\ + \textcolor{green}{\langle -1,1 \rangle} \\ + \textcolor{blue}{\langle 1,1 \rangle} \\ + \text{Angle to top} = 135° \end{matrix} $$ + +```{python} +if not Path("./count_bad_rotated_triangle_reduced.mp4").exists(): + try: + animate_carry_count( + carry = Carry([ + [ 0, 1, 0], + [ 0,-3, 0], + [ 1, 0, 1], + ]), + operation="add", + op_val=3, + frames=list(range(200)), + ).save("./count_bad_rotated_triangle_reduced.mp4") + except ValueError: + pass + +display( + Video("./count_bad_rotated_triangle_reduced.mp4"), +) +``` ::: -:::{.column width="40%"} -::: - -Though both of these videos are the same, neither are similar to the original. For comparison, the largest angle in the nonrotated triangle is ~108°. In fact, the shape is much closer to the similar orientation with $\ldots - x^4 - y^4 + \ldots$ terms. +Neither of these are similar to the original. +For comparison, the largest angle in the nonrotated case is ~108°. +In fact, the shape is much closer to the similar orientation with $x^4 + y^4 + \ldots$ terms. ### Checking Magnitudes -Measuring these quantities as angles is a bit of a lie. More fundamentally, two of the vectors have equal magnitude, and the ratio between they and the final vector's magnitude is irrational. This means that the unequal lengths are incommensurable. Importantly though, they way in which they are incommensurable is different for each polynomial. +Measuring these quantities as angles is a bit of a lie. +Since all of these triangles are isosceles, two of the vectors have equal magnitude. +Further, each can be characterized by the ratio between them and the final vector's magnitude. +The previous two triangles have ratio: $$ -\textcolor{red}{ -\frac{m_1}{n_1} = -\frac{\| \langle 2, -1 \rangle \|}{\| \langle -1, -1 \rangle \|} = -\frac{\sqrt 5}{\sqrt 2} = -\frac{\sqrt {10}}{2} -} \\ -\frac{m_2}{n_2} = +\frac{m_1}{n_1} = \frac{\| \langle -2, -1 \rangle \|}{\| \langle 0, -1 \rangle \|} = \frac{\sqrt 5}{1} = \sqrt 5 \\ -\frac{m_3}{n_3} = +\frac{m_2}{n_2} = \frac{\| \langle -1, -1 \rangle \|}{\| \langle 0, -1 \rangle \|} = \frac{\sqrt 2}{1} = \sqrt 2 $$ -Obviously, one cannot express $\sqrt 10$ from only one of $\sqrt 2$ or $\sqrt 5$. Fortunately, this suggests the rotation: +The triangle we are trying to rotate has ratio: -:::{.column width="40%"} $$ -\begin{array}{|c} \hline -& \textcolor{red}{\bar{1}} \\ -& 3 \\ \textcolor{green}{\bar{1}} -&& \textcolor{blue}{\bar{1}} -\end{array} \\ -\begin{matrix} -\textcolor{red}{\langle 0,1 \rangle} \\ -\textcolor{green}{\langle -1,-1 \rangle} \\ -\textcolor{blue}{\langle 1,-1 \rangle} \\ -\text{Angle to top} = 135° -\end{matrix} +\frac{m}{n} = +\frac{\| \langle 2, -1 \rangle \|}{\| \langle -1, -1 \rangle \|} = +\frac{\sqrt 5}{\sqrt 2} = +\frac{\sqrt {10}}{2} $$ -::: -:::{.column width="40%"} -::: - -The counting video looks very promising, but can the polynomial factorized? Indeed it can, since all the vectors are in least terms and are incommensurable in the same way. +Obviously, one cannot express $\sqrt 10$ from only one of $\sqrt 2$ or $\sqrt 5$. +Fortunately, this suggests the rotation: +::: {layout-ncol="2"} $$ \begin{gather*} -3 x^{3} y^{2} - x^{6} y^{3} - x^{3} - y^{3} &=& -(- x^{4} y^{2} + x^{3} y + x^{2} y^{2} - x^{2} + x y - y^{2}) \\ -&& (x^2 y + x + y ) \\ -\begin{array}{|c} \hline -&&& \bar{1} \\ \\ -&&& 3 \\ \bar{1} -&&&&&& \bar{1} -\end{array} &=& -\begin{array}{|c} \hline -& & \bar{1} & \\ -& 1 & & 1 & \\ -\bar{1} & & 1 & & \bar{1} \\ -\end{array} ~~\cdot~~ -\begin{array}{|c} \hline -& 1 & \\ 1 & & 1 -\end{array} \\ -&&(x y^{2} + x + y ) = \Phi_2 \left ( \frac{y}{x} + xy \right ) x + \begin{array}{|c} \hline + & & & \textcolor{red}{\bar{1}} \\ + \\ + & & & \bar{3} \\ + \textcolor{green}{1} & & & & & & \textcolor{blue}{1} + \end{array} ~~ + \begin{matrix} + \textcolor{red}{\langle 0,-2 \rangle} \\ + \textcolor{green}{\langle -3,1 \rangle} \\ + \textcolor{blue}{\langle 3,1 \rangle} \\ + \end{matrix} \\ + \frac{\| \langle -3, 1 \rangle \|}{\| \langle 0, -2 \rangle \|} + = \frac{\sqrt {10}}{2} \\ \end{gather*} $$ -As noted, the the rightmost polynomial can be expressed from $\Phi_2$. +```{python} +if not Path("./count_good_rotated_triangle.mp4").exists(): + try: + animate_carry_count( + carry = Carry([ + [ 0, 0, 0, 1, 0, 0, 0], + [ 0, 0, 0, 0, 0, 0, 0], + [ 0, 0, 0,-3, 0, 0, 0], + [ 1, 0, 0, 0, 0, 0, 1], + ]), + operation="add", + op_val=3, + frames=list(range(200)), + ).save("./count_good_rotated_triangle.mp4") + except ValueError: + pass -All powers of *x* in the product are multiples of 3. We can divide these out to produce a similar carry, as we did before. Doing so preserves the shape of counting, but destroys the factorization, as the result is irreducible. +display( + Video("./count_good_rotated_triangle.mp4"), +) +``` +::: -:::{.column width="40%"} +The counting video looks very promising, but can the polynomial factored? +Fortunately, yes. + +$$ +\begin{gather*} + x^{6} y^{3} + x^{3} + y^{3} - 3 x^{3} y^{2} &=& + (x^{4} y^{2} - x^{3} y - x^{2} y^{2} + x^{2} - x y + y^{2}) \\ + && (x^2 y + x + y ) \\ + \begin{array}{|c} \hline + & & & 1 \\ + \\ + & & & \bar{3} \\ + 1 & & & & & & \bar{1} + \end{array} &=& + \begin{array}{|c} \hline + & & 1 & \\ + & \bar{1} & & \bar{1} & \\ + 1 & & \bar{1} & & 1 \\ + \end{array} ~~\cdot~~ + \begin{array}{|c} \hline + & 1 & \\ + 1 & & 1 + \end{array} \\ + & & (x y^{2} + x + y ) = \Phi_2 \left ( \frac{y}{x} + xy \right ) x +\end{gather*} +$$ + +As noted, the the rightmost polynomial can still be expressed from $\Phi_2$. + +All powers of *x* in the product are multiples of 3. +We can divide these out to produce a similar carry, as we did before. +Doing so preserves the shape of counting, but destroys the factorization, as the result is irreducible. + +::: {layout-ncol="2"} $$ \begin{array}{|c} \hline -& \textcolor{red}{\bar{1}} & \\ \\ -& 3 & \\ -\textcolor{green}{\bar{1}} & & \textcolor{blue}{\bar{1}} + & \textcolor{red}{1} & \\ + \\ + & \bar{3} & \\ + \textcolor{green}{1} & & \textcolor{blue}{1} \end{array} ~~ \begin{matrix} -\textcolor{red}{\langle 0,2 \rangle} \\ -\textcolor{green}{\langle -1,-1 \rangle} \\ -\textcolor{blue}{\langle 1,-1 \rangle} \\ + \textcolor{red}{\langle 0,2 \rangle} \\ + \textcolor{green}{\langle -1,-1 \rangle} \\ + \textcolor{blue}{\langle 1,-1 \rangle} \\ \end{matrix} \\ \frac{| \langle -1, -1 \rangle |}{| \langle 0,2 \rangle |} -= \frac{\sqrt 2}{2} + = \frac{\sqrt 2}{2} $$ + +```{python} +if not Path("./count_good_rotated_triangle_reduced.mp4").exists(): + try: + animate_carry_count( + carry = Carry([ + [ 0, 1, 0], + [ 0, 0, 0], + [ 0,-3, 0], + [ 1, 0, 1], + ]), + operation="add", + op_val=3, + frames=list(range(200)), + ).save("./count_good_rotated_triangle_reduced.mp4") + except ValueError: + pass + +display( + Video("./count_good_rotated_triangle_reduced.mp4"), +) +``` ::: -:::{.column width="40%"} -::: +All of these similar systems is share something in common: the pseudo-base is located at + the centroid of the triangle formed by the remaining digits. +This is most obvious from the fact that the sum of all three vectors equals $\vec 0$ in all three cases. +The "threshold we exceeded" when comparing the unrotated system's neighbors was the centroid transitioning + between being closer to the upper point or the lower two points. -All of these similar systems is share something in common: the pseudo-base is located at the centroid of the triangle formed by the remaining digits. This is most obvious from the fact that the sum of all three vectors equals $\vec 0$ in all three cases. The "threshold we exceeded" when comparing its neighbors was the centroid transitioning between being closer to the upper point or the lower two points. - -The position of the centroid is unchanged so long as the lower two points have the same midpoint as above. Therefore, all such barycentric carries share this similar pattern. +The position of the centroid is unchanged so long as the lower two points have the same midpoint as above. +Therefore, all triangular barycentric carries should share this similar pattern. Closing ------- -Since the pseudo-base will be positioned at the one's place to start, it makes sense to consider the vectors that stretch from it to the "spread" digits. If the carry is reducible, then preserving the ratios of the lengths of the vectors appears to preserve the reducibility of the polynomial (and obviously, the counting system). However, further compacting the counting system by dividing components by a common factor tends to destroy reducibility. +Carries intrinsically possess a few invariants. +For one, they must apply at any location in an expansion, a form of translational symmetry. +No matter if we multiply or divide by *x* or *y*, the polynomial retains its general appearance, + as well as whether or not it is irreducible. +The above method shows that rotational symmetry also exists for polynomials beyond interchanging *x* and *y*. -Two questions that immediately come to mind are: +Carries seem to possess a symmetry which polynomials do not: the ability to contract and expand bases, + meaning that in some sense, $P(x) \sim P(x^k)$ for any *k*. +This preserves the counting system despite expansions and contractions which pay no regard for reducibility. +This is also the reason that systems like base-$\sqrt 2$ are so uninteresting: + for integers, it's just spaced out binary. -- Not all integers can be written as the sum of two squares. Are there any (reducible) polynomials which have no rotation other than by swapping components and orientation? - - The [Brahmagupta-Fibonacci](https://en.wikipedia.org/wiki/Brahmagupta%E2%80%93Fibonacci_identity) identity implies that such numbers are closed under multiplication. This means that it is always possible to reorient the triangle in the method shown above. -- After compacting the counting system, can reducibility be recovered for other scaling factors than the one started with? What about from an irrational scaling (as by a rotation)? +Three questions that immediately come to mind are: + +- Not all integers can be written as the sum of two squares. + Are there any (reducible) polynomials which have no nontrivial rotation? + - The [Brahmagupta-Fibonacci](https://en.wikipedia.org/wiki/Brahmagupta%E2%80%93Fibonacci_identity) identity + implies that such numbers are closed under multiplication. + This means that it is always possible to reorient the triangle in the method shown above. +- After compacting the counting system, can reducibility be recovered for other scaling factors + than the one started with? + What about from an irrational scaling (as by a rotation)? - I am not equipped to answer this. +- Some geometry is impossible in the 2D square lattice -- for example, it contains no equilateral triangles. + Equilateral triangles do however exist in the 3D square lattice. + Are there 2D systems which *must* be embedded in 3D dimensions, or can they always be rotated and reduced? + - Again, I am not equipped to answer this. -It is very pleasing that the exponents of polynomials should correlate so well to the components of vectors. The primary geometrical approach to such objects is to plot all points which (at least approximately) satisfy the root, but basic coordinate geometry exists without needing to define evaluation. All at once, geometry, linear algebra, and polynomial number theory play a role in the story of these bizarre fractal patterns. +Regardless, it is pleasing that the exponents in polynomials can easily be interpreted as + the components of vectors. +The primary geometrical approach to such objects is to plot the curve or surface containing points which + (at least approximately) satisfies the equation. +Here however, basic coordinate geometry exists without needing to define evaluation. +Moreover, geometry, linear algebra, and polynomial number theory all at once have a role to play in + the story of these bizarre fractal patterns. diff --git a/polycount/cell2/polyproduct.png b/polycount/cell2/polyproduct.png new file mode 100644 index 0000000000000000000000000000000000000000..a6f6e0929ac689b80c39d06e0eaa7f953d4ae5b9 GIT binary patch literal 10919 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