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 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) def animate_carry_count( carry: Carry, dims: int = 100, operation: Literal["add", "multiply", "log"] = "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 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) else: center = (0, 0) 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) else: raise ValueError(f"Cannot use {repr(operation)} for animation") expansion = np.zeros((dims, dims)) expansion[center] = 1 state = [1, op_val, op_val] 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"{state[0]}") image.set_clim(0, carry.overflow - 1) fig.tight_layout() @animate(fig, frames, interval=interval, init_func=init) def ret(_): title, op_val, old_op_val = state plt.title(f"{title}") image.set_data(expansion) fig.tight_layout() next_func(expansion, val=op_val) if operation == "add": 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 # 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