diff --git a/.gitignore b/.gitignore index 2c103bf..e4b5016 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ _site/ /.quarto/ index_files/ .jupyter_cache/ +__pycache__/ diff --git a/polycount/cell1/carry.py b/polycount/cell1/carry.py deleted file mode 100644 index d4f70ad..0000000 --- a/polycount/cell1/carry.py +++ /dev/null @@ -1,65 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt - -class Carry: - def __init__(self, arr): - if not isinstance(arr, np.ndarray): - arr = np.array(arr) - overflow = None - over_pos = None - coeff_sum = 0 - #find largest carry - for i, row in enumerate(arr): - for j, val in enumerate(row): - coeff_sum += val - if val > 0: - if overflow is not None: - raise ValueError("array supplied has more than one overflow digit") - overflow = val - over_pos = (i, j) - - if coeff_sum < 0: - raise ValueError("sum of coefficients too small") - - self._arr = arr - self.over_pos = over_pos - self.overflow = overflow - - def _update(self, base, update_tuples, zerowall): - #dim_x, dim_y = base.shape - pos_x, pos_y = self.over_pos - for (x,y) in update_tuples: - idx = base[(x,y)] - dec = (idx // self.overflow) # - 1 - - wall = zerowall.copy() - sh_x, sh_y = self._arr.shape - x -= pos_x - y -= pos_y - - wall[x:x + sh_x, y:y + sh_y] = dec * self._arr - base -= wall - - def update(self, base): - zerowall = np.zeros(base.shape, dtype=base.dtype) - while True: - update_tuples = list(zip(*np.where(base >= self.overflow))) - if not update_tuples: - break - self._update(base, update_tuples, zerowall) - - def times(self, base, val, center=(0,0)): - #perform multiplication using the carry - base[center] *= val - self.update(base) - return base - - def add(self, base, val=1, center=(0,0)): - base[center] += val - self.update(base) - return base - -laplace = Carry(np.array([[0,-1,0],[-1,4,-1],[0,-1,0]])) - -#test = np.zeros((100, 100)) -#test[50,50] = 4 diff --git a/polycount/cell1/carry2d/__init__.py b/polycount/cell1/carry2d/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/polycount/cell1/carry2d/anim.py b/polycount/cell1/carry2d/anim.py new file mode 100644 index 0000000..0afdcc7 --- /dev/null +++ b/polycount/cell1/carry2d/anim.py @@ -0,0 +1,83 @@ +from functools import partial +from typing import Callable, Literal + +from matplotlib import animation, pyplot as plt +import numpy as np + +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 + + +def animate( + fig, frames: list[int] | None, **kwargs +) -> Callable[..., animation.FuncAnimation]: + # Why isn't this how the function is exposed by default? + return lambda func: animation.FuncAnimation(fig, func, frames, **kwargs) + + +def animate_carry_count( + 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: + 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)) + expansion[center] = 1 + title = [1] + + fig = plt.gcf() + + image = plt.imshow( + expansion, + extent=[-center[0], dims - center[0], dims - center[1], -center[1]], # type: ignore + ) + def init(): + plt.title(f"{title[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]}") + image.set_data(expansion) + fig.tight_layout() + + next_func(expansion) + if operation == "add": + title[0] += op_val + else: + title[0] *= op_val + + return ret diff --git a/polycount/cell1/carry2d/carry.py b/polycount/cell1/carry2d/carry.py new file mode 100644 index 0000000..8461c49 --- /dev/null +++ b/polycount/cell1/carry2d/carry.py @@ -0,0 +1,81 @@ +from typing import Sequence +import numpy as np + + +def _validate_carry(arr: np.ndarray) -> tuple[int, tuple[int, int]]: + """ + Ensure that `arr` is interpretable as an explicit carry rule. + See `Carry` for more information. + """ + overflow = None + over_pos = None + coeff_sum = 0 + # find largest digit for carry + for i, row in enumerate(arr): + for j, val in enumerate(row): + coeff_sum += val + if val < 0: + if overflow is not None: + raise ValueError("Array supplied has more than one overflow digit") + overflow = -val + over_pos = (i, j) + + if coeff_sum < 0: + raise ValueError("Sum of coefficients too small") + + if overflow is None or over_pos is None: + raise ValueError("No dominating term supplied") + + return overflow, over_pos + + +class Carry: + """ + Two-dimensional carry rule. + + This is an (integer) Numpy array which is greedily "applied" to another (integer) Numpy array to + rid it of integers. + Only "explicit" carries are allowed -- these are carries that are all negative except for a + single positive term, such that the sum of all terms is non-negative. + + For example, the Laplacian kernel, as a carry rule is: + `laplace = Carry([[0,1,0],[1,-4,1],[0,1,0]])` + + Interpreted as a carry rule, this spreads integers above 4 into their neighboring cells. + """ + + def __init__(self, arr: np.ndarray | Sequence): + if not isinstance(arr, np.ndarray): + arr = np.array(arr) + overflow, over_pos = _validate_carry(arr) + + self._arr: np.ndarray = arr + self.over_pos: tuple[int, int] = over_pos + self.overflow: int = overflow + + def _apply( + self, + base: np.ndarray, + update_tuples: list[tuple[int, int]], + zerowall: np.ndarray, + ): + """Apply the carry at the locations in `base` specified by `update_tuples`.""" + pos_x, pos_y = self.over_pos + for x, y in update_tuples: + idx = base[(x, y)] + dec = idx // self.overflow # - 1 + + wall = zerowall.copy() + sh_x, sh_y = self._arr.shape + x -= pos_x + y -= pos_y + + wall[x : x + sh_x, y : y + sh_y] = dec * self._arr + base += wall + + def apply(self, base: np.ndarray) -> np.ndarray: + """Update an expansion `base` according to this carry.""" + zerowall = np.zeros(base.shape, dtype=base.dtype) + while update_tuples := list(zip(*np.where(base >= self.overflow))): + self._apply(base, update_tuples, zerowall) + return base diff --git a/polycount/cell1/carry2d/curve.py b/polycount/cell1/carry2d/curve.py new file mode 100644 index 0000000..a919cd6 --- /dev/null +++ b/polycount/cell1/carry2d/curve.py @@ -0,0 +1,88 @@ +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 new file mode 100644 index 0000000..1ae9cff --- /dev/null +++ b/polycount/cell1/carry2d/extra.py @@ -0,0 +1,78 @@ +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]]) + +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]]) +folium = Carry([ + [ 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] +]) +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] +]) + +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 + return ret + +triangle1 = Carry(triangle_spread(1)) +triangle2 = Carry(triangle_spread(2)) +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] +]) + +tri3_rot_real = 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] +]) + +tri3_rot2 = Carry([ + [ 0,-1, 0], + [ 0, 3, 0], + [-1, 0,-1] +]) + +tri3_tall = Carry([ + [ 0,-1, 0], + [ 0, 0, 0], + [ 0, 3, 0], + [-1, 0,-1] +]) + +tri3_tall2 = Carry([ + [ 0,-1, 0], + [ 0, 3, 0], + [ 0, 0, 0], + [-1, 0,-1] +]) diff --git a/polycount/cell1/circular.py b/polycount/cell1/circular.py deleted file mode 100644 index 6d40694..0000000 --- a/polycount/cell1/circular.py +++ /dev/null @@ -1,148 +0,0 @@ -from functools import partial - -import numpy as np -import sympy -import matplotlib.pyplot as plt -from sympy.plotting import plot_implicit -from matplotlib import animation - -from carry import Carry - -def make_animation(fig, frames, **kwargs): - fig.tight_layout() - #why isn't this how the function is exposed by default - return lambda func: animation.FuncAnimation(fig, func, frames, init_func=lambda: None, **kwargs) - -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 - -x, y = sympy.symbols("x y") - - -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]]) - -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]]) -folium3 = Carry([[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]]) - -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 - return ret - -triangle1 = Carry(triangle_spread(1)) -triangle2 = Carry(triangle_spread(2)) -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] ]) - -tri3_rot_real = 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] ]) - -tri3_rot2 = Carry([ [ 0,-1, 0], - [ 0, 3, 0], - [-1, 0,-1] ]) - -tri3_tall = Carry([ [ 0,-1, 0], - [ 0, 0, 0], - [ 0, 3, 0], - [-1, 0,-1] ]) - -tri3_tall2 = Carry([ [ 0,-1, 0], - [ 0, 3, 0], - [ 0, 0, 0], - [-1, 0,-1] ]) - -def start_anim(dims=100, carry=xy_2, frames=None, interval=200, inter='add', i_val=1, center=(0,0)): - if center == (0,0) and carry.over_pos != center: - center = (dims // 2, dims // 2) - - if inter == 'add': - next_func = partial(carry.add, val=i_val, center=center) - elif inter == 'mult': - if i_val == 1: - raise ValueError(f"too small value {i_val} for repeated multiplication") - next_func = partial(carry.mult, val=i_val, center=center) - else: - raise ValueError(f"Cannot use {repr(inter)} for animation") - - zero = np.zeros((dims, dims)) - zero[center] = 1 - val = [1] - - fig = plt.gcf() - plt.title('0') - image = plt.imshow(zero) - image.set_clim(0, carry.overflow-1) - - @make_animation(fig, frames, interval=interval) - def ret(fr): - next_func(zero) -# if next_func != xy_2.add: -# fr = sum(i*(invalid**j) for i,j in zip(zero[0], range(dims))) - 1 - if inter == 'add': - val[0] += i_val - else: - val[0] *= i_val - plt.title(f"{val[0]}") - image.set_data(zero) - fig.tight_layout() - - return ret - -def anim_curves(dims=25, invalid=2, frames=None, interval=200, next_func=xy_2.add): - 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) - - @make_animation(fig, frames, interval=interval) - def ret(fr): - next_func(zero, invalid) - fig.clf() - plot = plot_implicit(poly_from_array(zero) - fr, backend='matplotlib') - plt.title(f"{fr+1}") - print(fr) - - return ret - -writer = animation.writers['ffmpeg'](fps=15, metadata={'artist': 'Me'}) - -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