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