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
|
center figures
|
||||||
|
|
||||||
make sure to object-fit: scale
|
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>
|
</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
|
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.
|
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}
|
```{python}
|
||||||
#| echo: false
|
#| echo: false
|
||||||
#| fig-cap: |
|
#| 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).
|
#| 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
|
@classmethod
|
||||||
from sympy.abc import t
|
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
|
$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
|
#| echo: false
|
||||||
#| fig-cap: p/q polar roses as rational curves
|
#| 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}
|
```{python}
|
||||||
#| echo: false
|
#| 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,
|
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