Skip to content

Commit 563e7b9

Browse files
committed
feat: refine quiver defaults with shorter dimmer arrows and add high-performance animate() method
1 parent cce450f commit 563e7b9

4 files changed

Lines changed: 118 additions & 4 deletions

File tree

-680 KB
Loading

pivpy/graphics.py

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ def plot(
4242
levels: int = 80,
4343
skip: int | None = None,
4444
arrow_scale: float | None = None,
45-
arrow_width: float = 0.005,
46-
arrow_color: str = "#0a0a0a",
47-
arrow_alpha: float = 0.9,
45+
arrow_width: float = 0.0038,
46+
arrow_color: str = "#252525",
47+
arrow_alpha: float = 0.65,
4848
streamline_density: float = 1.1,
4949
streamline_color: str | None = None,
5050
streamline_alpha: float = 0.55,
@@ -290,7 +290,7 @@ def plot(
290290
med_speed = 1.0
291291

292292
if arrow_scale is None:
293-
target_len = 1.8 * step * dx
293+
target_len = 0.85 * step * dx
294294
auto_scale = (med_speed / target_len) if target_len > 0 else 1.0
295295
else:
296296
auto_scale = float(arrow_scale)
@@ -1323,6 +1323,106 @@ def to_movie(
13231323
)
13241324

13251325

1326+
def animate(
1327+
data: xr.Dataset,
1328+
*,
1329+
background: str | bool | None = "vorticity",
1330+
quiver: bool = True,
1331+
blur: float = 1.5,
1332+
skip: int | None = None,
1333+
arrow_scale: float | None = None,
1334+
arrow_width: float = 0.0038,
1335+
arrow_color: str = "#252525",
1336+
arrow_alpha: float = 0.65,
1337+
interval: int = 100,
1338+
repeat: bool = True,
1339+
blit: bool = False,
1340+
ax: Axes | None = None,
1341+
**kwargs,
1342+
):
1343+
"""Create a high-performance, beautiful Matplotlib FuncAnimation for time-series flow fields.
1344+
1345+
Uses in-place artist vector updates (``quiver.set_UVC``) for smooth animation.
1346+
1347+
Parameters
1348+
----------
1349+
data : xr.Dataset
1350+
Dataset with time dimension 't' and velocity components ('u', 'v').
1351+
background : str, bool, or None, default "vorticity"
1352+
Background scalar field.
1353+
quiver : bool, default True
1354+
Whether to draw velocity vectors.
1355+
blur : float, default 1.5
1356+
Gaussian filter smoothing for background.
1357+
interval : int, default 100
1358+
Delay between frames in milliseconds.
1359+
repeat : bool, default True
1360+
Whether the animation repeats when finished.
1361+
blit : bool, default False
1362+
Whether blitting is used to optimize drawing.
1363+
ax : Axes, optional
1364+
Target matplotlib axes.
1365+
1366+
Returns
1367+
-------
1368+
matplotlib.animation.FuncAnimation
1369+
The animation object (can be viewed in Jupyter/Marimo or saved via ``anim.save()``).
1370+
"""
1371+
from matplotlib.animation import FuncAnimation
1372+
from matplotlib.quiver import Quiver
1373+
1374+
n_frames = int(data.sizes.get("t", 1))
1375+
1376+
# Initialize frame 0 using publication-quality plot()
1377+
fig, target_ax = plot(
1378+
data,
1379+
background=background,
1380+
quiver=quiver,
1381+
streamlines=False, # streamlines recreate per frame; disabled in fast animation
1382+
blur=blur,
1383+
skip=skip,
1384+
arrow_scale=arrow_scale,
1385+
arrow_width=arrow_width,
1386+
arrow_color=arrow_color,
1387+
arrow_alpha=arrow_alpha,
1388+
t_idx=0,
1389+
ax=ax,
1390+
**kwargs,
1391+
)
1392+
1393+
# Find quiver artist
1394+
q_artist = None
1395+
for child in target_ax.get_children():
1396+
if isinstance(child, Quiver):
1397+
q_artist = child
1398+
break
1399+
1400+
# Subsampling step used
1401+
u_0 = data["u"].isel(t=0) if "t" in data["u"].dims else data["u"]
1402+
ny, nx = u_0.shape
1403+
step = max(1, int(skip)) if skip is not None else max(1, int(round(max(nx, ny) / 32)))
1404+
1405+
def update_frame(frame_i):
1406+
ds_t = data.isel(t=frame_i) if "t" in data.dims else data
1407+
u_t = np.asarray(ds_t["u"].values)
1408+
v_t = np.asarray(ds_t["v"].values)
1409+
if q_artist is not None:
1410+
q_artist.set_UVC(u_t[::step, ::step], v_t[::step, ::step])
1411+
t_coord = float(ds_t["t"].values) if "t" in ds_t.coords and ds_t["t"].size else frame_i
1412+
target_ax.set_title(f"Flow Field (t = {t_coord:.2f})", fontsize=11)
1413+
return [q_artist] if (blit and q_artist is not None) else []
1414+
1415+
anim = FuncAnimation(
1416+
fig,
1417+
update_frame,
1418+
frames=n_frames,
1419+
interval=interval,
1420+
repeat=repeat,
1421+
blit=blit,
1422+
)
1423+
return anim
1424+
1425+
13261426
def imvectomovie(
13271427
filename: str | list[str] | tuple[str, ...],
13281428
output: str | pathlib.Path | None,

pivpy/pivpy.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from scipy.ndimage import gaussian_filter
1919

2020
from pivpy.graphics import plot as gplot
21+
from pivpy.graphics import animate as ganimate
2122
from pivpy.graphics import quiver as gquiver
2223
from pivpy.graphics import showf as gshowf
2324
from pivpy.graphics import showscal as gshowscal
@@ -2463,6 +2464,10 @@ def plot(self, **kwargs):
24632464
"""
24642465
return gplot(self._obj, **kwargs)
24652466

2467+
def animate(self, **kwargs):
2468+
"""High-performance FuncAnimation for time-series flow fields (graphics.animate)."""
2469+
return ganimate(self._obj, **kwargs)
2470+
24662471
def quiver(self, **kwargs):
24672472
"""graphics.quiver() as a flow_property"""
24682473
fig, ax = gquiver(self._obj, **kwargs)

tests/test_graphics.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,15 @@ def test_plot():
5555
assert fig4 is not None
5656

5757

58+
def test_animate():
59+
"""tests fast FuncAnimation flow field generation"""
60+
anim = graphics.animate(_d, interval=50)
61+
assert anim is not None
62+
63+
anim2 = _d.piv.animate(interval=50)
64+
assert anim2 is not None
65+
66+
5867
def test_quiver():
5968
""" tests quiver
6069
"""

0 commit comments

Comments
 (0)