diff --git a/polycount/cell1/carry2d/anim.py b/polycount/cell1/carry2d/anim.py index 0afdcc7..db3bcb6 100644 --- a/polycount/cell1/carry2d/anim.py +++ b/polycount/cell1/carry2d/anim.py @@ -1,29 +1,20 @@ from functools import partial +import time +from threading import Thread from typing import Callable, Literal from matplotlib import animation, pyplot as plt import numpy as np +from sympy.plotting import plot_implicit from .carry import Carry - -writer = animation.writers["ffmpeg"](fps=15, metadata={"artist": "Me"}) - - -def times(base: np.ndarray, carry: Carry, val): - base *= val - carry.apply(base) - return base - - -def add(base: np.ndarray, carry: Carry, val=1, center=(0, 0)): - base[center] += val - carry.apply(base) - return base +from .expansion import add, times, poly_from_array def animate( fig, frames: list[int] | None, **kwargs ) -> Callable[..., animation.FuncAnimation]: + """Decorator-style wrapper for FuncAnimation.""" # Why isn't this how the function is exposed by default? return lambda func: animation.FuncAnimation(fig, func, frames, **kwargs) @@ -38,6 +29,20 @@ def animate_carry_count( frames: list[int] | None = None, interval=200, ) -> animation.FuncAnimation: + """ + Create a video of integer expansions using a `carry`. + Expansions use a `dims` x `dims` numpy array. + + The video starts from the expansion of one, and subsequent frames come from + either adding `op_val` to the constant term or multiplying each term by `op_val`, + both followed by carrying. + `operation` decides which of these to use. + + The constant term of the expansion is specified with `center`. + print("Starting animation") + By default, the constant term is at position (0, 0), in the upper left corner. + If the carry expands leftward or upward, it is instead in the center of the array. + """ if center is None: if carry.over_pos != (0, 0): center = (dims // 2, dims // 2) @@ -81,3 +86,90 @@ def animate_carry_count( title[0] *= op_val return ret + +# writer = animation.writers["ffmpeg"](fps=15, metadata={"artist": "Me"}) + +def bindfig(fig): + """ + Create a function which is compatible with the signature of `pyplot.figure`, + but alters the already-existing figure `fig` instead. + """ + def ret(**kwargs): + for i, j in kwargs.items(): + if i == "figsize": + if j is not None: + fig.set_figwidth(j[0]) + fig.set_figheight(j[1]) + continue + fig.__dict__["set_" + i](j) + return fig + return ret + + +def animate_carry_curves( + carry: Carry, + dims: int = 100, + operation: Literal["add", "multiply"] = "add", + op_val=1, + center: tuple[int, int] | None = None, + # Animation parameters + frames: list[int] | None = None, + # interval=200, +) -> animation.FuncAnimation: + """ + Create a video of implicit plots of "incremented curves" based on a `carry`. + These are curves for which we re-interpret an expansion as a polynomial equal + equal to the integer it represents. + """ + if center is None: + if carry.over_pos != (0, 0): + center = (dims // 2, dims // 2) + else: + center = (0, 0) + + if operation == "add": + next_func = partial(add, carry=carry, val=op_val, 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) + else: + raise ValueError(f"Cannot use {repr(operation)} for animation") + + expansion = np.zeros((dims, dims), dtype=np.int32) + expansion[center] = 0 + count = [0] + + # I hate doing it, but there's no other way to control the figure uses + fig = plt.gcf() + plt.figure = bindfig(fig) + plt.tight_layout() + + # More sympy.plot_implicit nonsense + t = Thread(target=lambda: time.sleep(1) or plt.close()) + t.run() + plt.title(f"{count[0]}") + plot_implicit( + poly_from_array(carry._arr), + backend="matplotlib", + ) + + @animate(fig, frames) + def ret(_): + next_poly = poly_from_array(expansion) - count[0] + + if next_poly != 0: + fig.clf() + plot_implicit( + next_poly, + backend="matplotlib", + ) + plt.title(f"{count[0]}") + + if operation == "add": + count[0] += op_val + else: + count[0] *= op_val + next_func(expansion) + + return ret diff --git a/polycount/cell1/carry2d/carry.py b/polycount/cell1/carry2d/carry.py index 8461c49..82790b8 100644 --- a/polycount/cell1/carry2d/carry.py +++ b/polycount/cell1/carry2d/carry.py @@ -20,7 +20,7 @@ def _validate_carry(arr: np.ndarray) -> tuple[int, tuple[int, int]]: overflow = -val over_pos = (i, j) - if coeff_sum < 0: + if coeff_sum > 0: raise ValueError("Sum of coefficients too small") if overflow is None or over_pos is None: diff --git a/polycount/cell1/carry2d/curve.py b/polycount/cell1/carry2d/curve.py deleted file mode 100644 index a919cd6..0000000 --- a/polycount/cell1/carry2d/curve.py +++ /dev/null @@ -1,88 +0,0 @@ -import matplotlib.pyplot as plt -import numpy as np -from sympy.abc import x, y -from sympy.plotting import plot_implicit - -from .anim import animate - -def _first_nonzero(arr: np.ndarray, axis: int): - 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 -): - 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}" - - -def poly_from_array(array): - ret = 0 - for i, row in enumerate(array): - for j, val in enumerate(row): - ret += val*(x**i * y**j) - return ret - -def bindfig(fig): - def ret(**kwargs): - for i, j in kwargs.items(): - if i == "figsize": - if j is not None: - fig.set_figwidth(j[0]) - fig.set_figheight(j[1]) - continue - fig.__dict__["set_" + i](j) - return fig - return ret - -def anim_curves(next_func, dims=25, invalid=2, frames=None, interval=200): - zero = np.zeros((dims, dims), dtype=np.int32) - #zero[0,0] = 1 - fig = plt.gcf() - #plt.colorbar() - - #temp = plt.figure - #I hate doing it, but there's no other way to get the figure before it's plotted - plt.figure = bindfig(fig) - - @animate(fig, frames, interval=interval) - def ret(fr): - next_func(zero, invalid) - fig.clf() - plot_implicit( - poly_from_array(zero) - fr, - backend='matplotlib', - adaptive=False - ) - plt.title(f"{fr+1}") - print(fr) - - return ret diff --git a/polycount/cell1/carry2d/extra.py b/polycount/cell1/carry2d/extra.py index 1ae9cff..07c33cb 100644 --- a/polycount/cell1/carry2d/extra.py +++ b/polycount/cell1/carry2d/extra.py @@ -2,41 +2,41 @@ import numpy as np from .carry import Carry -xy_2 = Carry([[2,-1],[-1,0]]) -xy_3 = Carry([[3,-1],[-1,0]]) -x2y_3 = Carry([[3,-2],[-1,0]]) -x3y_4 = Carry([[4,-3],[-1,0]]) +xy_2 = Carry([[-2,1],[1,0]]) +xy_3 = Carry([[-3,1],[1,0]]) +x2y_3 = Carry([[-3,2],[1,0]]) +x3y_4 = Carry([[-4,3],[1,0]]) -laplace = Carry([[0,-1,0],[-1,4,-1],[0,-1,0]]) -laplace3 = Carry([[0,0,0],[-1,3,-1],[0,-1,0]]) +laplace = Carry([[0,1,0],[1,-4,1],[0,1,0]]) +laplace3 = Carry([[0,0,0],[1,-3,1],[0,1,0]]) -almost_folium = Carry([[0,0,-1], [0,2,0], [-1,0,0]]) +almost_folium = Carry([[0,0,1], [0,-2,0], [1,0,0]]) folium = Carry([ - [ 0,0,0,-1], - [ 0,2,0, 0], - [ 0,0,0, 0], - [-1,0,0, 0] + [ 0, 0,0, 1], + [ 0,-2,0, 0], + [ 0, 0,0, 0], + [ 1, 0,0, 0] ]) folium3 = Carry([ - [ 0,0,0,-1], - [ 0,3,0, 0], - [ 0,0,0, 0], - [-1,0,0, 0] + [ 0, 0,0, 1], + [ 0,-3,0, 0], + [ 0, 0,0, 0], + [ 1, 0,0, 0] ]) folium4 = Carry([ - [ 0, 0, 0,-1, 0], - [ 0, 0, 0, 0,-1], - [ 0, 0, 4, 0, 0], - [-1, 0, 0, 0, 0], - [ 0,-1, 0, 0, 0] + [ 0, 0, 0, 1, 0], + [ 0, 0, 0, 0, 1], + [ 0, 0,-4, 0, 0], + [ 1, 0, 0, 0, 0], + [ 0, 1, 0, 0, 0] ]) def triangle_spread(n): ret = np.zeros((n + 1, n + 1), dtype=np.int32) - ret[0, 0] = -1 - ret[1, 1] = 3 - ret[n, 0] = -1 - ret[0, n] = -1 + ret[0, 0] = 1 + ret[1, 1] = -3 + ret[n, 0] = 1 + ret[0, n] = 1 return ret triangle1 = Carry(triangle_spread(1)) @@ -45,34 +45,34 @@ triangle3 = Carry(triangle_spread(3)) #factorable! triangle4 = Carry(triangle_spread(4)) tri3_rot = Carry([ - [ 0, 0,-1, 0, 0], - [ 0, 0, 3, 0, 0], - [-1, 0, 0, 0,-1] + [ 0, 0, 1, 0, 0], + [ 0, 0,-3, 0, 0], + [ 1, 0, 0, 0, 1] ]) tri3_rot_real = Carry([ - [ 0, 0, 0,-1, 0, 0, 0], + [ 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] + [ 0, 0, 0,-3, 0, 0, 0], + [ 1, 0, 0, 0, 0, 0, 1] ]) tri3_rot2 = Carry([ - [ 0,-1, 0], - [ 0, 3, 0], - [-1, 0,-1] + [ 0, 1, 0], + [ 0,-3, 0], + [ 1, 0, 1] ]) tri3_tall = Carry([ - [ 0,-1, 0], + [ 0, 1, 0], [ 0, 0, 0], - [ 0, 3, 0], - [-1, 0,-1] + [ 0,-3, 0], + [ 1, 0, 1] ]) tri3_tall2 = Carry([ - [ 0,-1, 0], - [ 0, 3, 0], + [ 0, 1, 0], + [ 0,-3, 0], [ 0, 0, 0], - [-1, 0,-1] + [ 1, 0, 1] ])