refactored notes into markdown, add gitignore

This commit is contained in:
queue-miscreant 2025-07-03 03:53:49 -05:00
parent e3f4bb83c8
commit a15fca6599
5 changed files with 170 additions and 101 deletions

View File

@ -3,3 +3,5 @@ mp4s saved do not get copied or linked over to the rendering folder
center figures
make sure to object-fit: scale
use SympyAnimationWrapper from stereo/2

1
posts/stereo/2/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.mp4

67
posts/stereo/2/anim.py Normal file
View File

@ -0,0 +1,67 @@
import logging
import time
from pathlib import Path
from threading import Thread
from typing import Callable
from matplotlib import animation, pyplot as plt
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)
# 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
class SympyAnimationWrapper:
"""
Context manager for binding a figurebinfoinfoinfoinfoinfoinfo
"""
def __init__(self, filename: Path | str):
self._filename = filename if isinstance(filename, Path) else Path(filename)
self._current_figure = plt.gcf()
self._figure_temp = plt.figure
def __enter__(self):
plt.figure = bindfig(self._current_figure)
return self.animate
def __exit__(self, *_):
plt.figure = self._figure_temp
def animate(self, *args, **kwargs):
def wrapper(func):
ret = animate(self._current_figure, *args, **kwargs)(func)
current_save = ret.save
def save(*args, **kwargs):
if self._filename.exists():
logging.error("Ignoring saving animation '%s': file already exists", self._filename)
return
thread = Thread(target=lambda: time.sleep(1) or plt.close())
thread.run()
current_save(self._filename, *args, **kwargs)
ret.save = save
return ret
return wrapper

View File

@ -23,6 +23,32 @@ categories:
}
</style>
```{python}
#| echo: false
from dataclasses import dataclass
import matplotlib.pyplot as plt
import sympy
from sympy.abc import t
from anim import SympyAnimationWrapper
# rational circle function
o = (1 + sympy.I*t) / (1 - sympy.I*t)
# cache some values for plotting later
def generate_os(amt):
ret = sympy.sympify(1)
for _ in range(amt):
yield ret
ret = (ret * o).expand().cancel().simplify()
os = list(generate_os(10))
cs = [sympy.re(i).simplify() for i in os]
ss = [sympy.im(i).simplify() for i in os]
```
In my previous post, I discussed the stereographic projection of a circle as it pertains
to complex numbers, as well as its applications in 2D and 3D rotation.
@ -340,15 +366,46 @@ will plot a $p/1$ polar rose as t ranges over $(-\infty, \infty)$.
```{python}
#| echo: false
#| fig-cap: |
#| p/1 polar roses as rational curves
#| p/1 polar roses as rational curves.
#| Since *t* never reaches infinity, a bite appears to be taken out of the graphs near (-1, 0).
# TODO: video
@dataclass
class Rose:
x: sympy.Expr
y: sympy.Expr
title: str | None = None
import sympy
from sympy.abc import t
@classmethod
def from_rational(cls, p: int, q: int):
return cls(
cs[p]*cs[q],
cs[p]*ss[q],
title=f"$x = c_{{{p}}}c_{{{q}}}; y = c_{{{p}}}s_{{{q}}}$",
)
o = (1 + sympy.I*t) / (1 - sympy.I*t)
def animate_roses(filename: str, arguments: list[Rose], interval=200):
with SympyAnimationWrapper(filename) as animate:
@animate(list(range(len(arguments))), interval=interval)
def ret(fr):
plt.clf()
argument = arguments[fr]
sympy.plot_parametric(
argument.x,
argument.y,
xlim=(-2,2),
ylim=(-2,2),
title=argument.title,
backend="matplotlib",
)
ret.save() # type: ignore
animate_roses(
"polar_roses_1.mp4",
[ Rose.from_rational(i, 1) for i in range(1, 11) ],
)
```
$q = 1$ happens to match the subscript *c* term of *x* and *s* term of *y*, so one might wonder
@ -365,7 +422,13 @@ will plot a $p/q$ polar rose as t ranges over $(-\infty, \infty)$.
#| echo: false
#| fig-cap: p/q polar roses as rational curves
# TODO: video
animate_roses(
"polar_roses_2.mp4",
[
Rose.from_rational(i,j)
for i,j in [(1,1),(1,2),(2,1),(3,1),(3,2),(2,3),(1,3),(1,4),(3,4),(4,3),(4,1)]
],
)
```
@ -637,9 +700,38 @@ Indeed, the sequence of curves with parametrization $R_n(t) = 2nt$ approximate t
```{python}
#| echo: false
#| fig-cap: $x = {c_1 \over 1 - 2c_1} \quad y = {s_1 \over 1 - 2c_1}$
#| fig-cap: Approximations to the Archimedean spiral
# TODO: video
with SympyAnimationWrapper("approximate_archimedes.mp4") as animate:
@animate(list(range(10)), interval=500)
def ret(fr):
plt.clf()
p = sympy.plot_parametric(
t*sympy.cos(t),
t*sympy.sin(t),
xlim=(-4,4),
ylim=(-4,4),
label="Archimedean Spiral",
backend="matplotlib",
show=False
)
i = fr + 1
p.extend(
sympy.plot_parametric(
2*i*t*cs[i],
2*i*t*ss[i],
(t, -5, 5),
line_color="black",
label=f"$R_{{{i}}}(t) = {2*i}t$",
backend="matplotlib",
show=False
)
)
p.show()
plt.legend()
ret.save() # type: ignore
```
Since R necessarily defines a rational curve, the curves will never be equal,

View File

@ -1,93 +0,0 @@
from sympy import symbols, I, im, re, sympify, plot_parametric, sin, cos
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import animation
class LazyList:
def __init__(self, generator):
self._list = []
self._generator = generator
def __getitem__(self, idx):
final = idx
if isinstance(idx, slice):
final = idx.stop
while final >= len(self._list):
self._list.append(next(self._generator))
return self._list[idx]
def __repr__(self):
if len(self._list):
return repr(self._list)[:-1] + ", ...]"
return "[...]"
t = symbols('t', real=True)
e = (1 + I*t) / (1 - I*t)
def generate_es():
ret = sympify(1)
while True:
yield ret
ret = (ret * e).expand().cancel().simplify()
es = LazyList(generate_es())
cs = LazyList(map(lambda x: re(x).simplify(), es))
ss = LazyList(map(lambda x: im(x).simplify(), es))
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 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(stuff=[], interval=200):
#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, len(stuff), interval=interval)
def ret(fr):
fig.clf()
idx1, idx2 = stuff[fr]
plot_parametric(idx1, idx2, xlim=(-2,2), ylim=(-2,2), backend='matplotlib')
#plt.title(f"$x = c_{{{idx1}}}c_{{{idx2}}}; y = c_{{{idx1}}}s_{{{idx2}}}$")
plt.tight_layout()
return ret
plot_roses = lambda roses: anim_curves([(cs[i]*cs[j], cs[i]*ss[j]) for i,j in roses], interval=500)
#plot_roses([(1,1),(1,2),(2,1),(3,1),(3,2),(2,3),(1,3),(1,4),(3,4),(4,3),(4,1)]).save()
def archimedes_frames():
writer = animation.writers['ffmpeg'](fps=15, metadata={'artist': 'Me'})
#fig = plt.gcf()
for i in range(10):
#for i in range(1):
i += 1
p = plot_parametric(t*cos(t), t*sin(t), xlim=(-4,4), ylim=(-4,4), label="Archimedean Spiral", show=False)
p.save(f"frame{i}.png")
p.extend(
plot_parametric(2*i*t*cs[i], 2*i*t*ss[i], line_color="black", label=f"$R_{{{i}}}(t) = {2*i}t$",
show=False)
)
p.legend = True
p.save(f"frame%02d.png" % i)