181 lines
5.4 KiB
Python
181 lines
5.4 KiB
Python
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
|