refactored notes into markdown, add gitignore
This commit is contained in:
parent
e3f4bb83c8
commit
a15fca6599
@ -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
1
posts/stereo/2/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.mp4
|
||||
67
posts/stereo/2/anim.py
Normal file
67
posts/stereo/2/anim.py
Normal 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
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
Loading…
x
Reference in New Issue
Block a user