flowtracks 1.6.2.dev6__py3-none-any.whl → 1.6.2.dev12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. flowtracks/_version.py +3 -3
  2. flowtracks/clean.py +202 -0
  3. flowtracks/graphics.py +194 -0
  4. flowtracks/merge.py +150 -0
  5. {flowtracks-1.6.2.dev6.dist-info → flowtracks-1.6.2.dev12.dist-info}/METADATA +1 -1
  6. flowtracks-1.6.2.dev12.dist-info/RECORD +55 -0
  7. flowtracks-1.6.2.dev6.dist-info/RECORD +0 -53
  8. {flowtracks-1.6.2.dev6.data → flowtracks-1.6.2.dev12.data}/data/flowtracks-examples/batch_Lagrangian_to_Eulerian.py +0 -0
  9. {flowtracks-1.6.2.dev6.data → flowtracks-1.6.2.dev12.data}/data/flowtracks-examples/hdf5_scene_analysis.ipynb +0 -0
  10. {flowtracks-1.6.2.dev6.data → flowtracks-1.6.2.dev12.data}/data/flowtracks-examples/linking_trajectories.ipynb +0 -0
  11. {flowtracks-1.6.2.dev6.data → flowtracks-1.6.2.dev12.data}/data/flowtracks-examples/marimo_dual_rig_trajectories.py +0 -0
  12. {flowtracks-1.6.2.dev6.data → flowtracks-1.6.2.dev12.data}/data/flowtracks-examples/marimo_flowtracks.py +0 -0
  13. {flowtracks-1.6.2.dev6.data → flowtracks-1.6.2.dev12.data}/data/flowtracks-examples/marimo_hdf5_vs_zarr_scene_analysis.py +0 -0
  14. {flowtracks-1.6.2.dev6.data → flowtracks-1.6.2.dev12.data}/data/flowtracks-examples/marimo_linking_trajectories_zarr.py +0 -0
  15. {flowtracks-1.6.2.dev6.data → flowtracks-1.6.2.dev12.data}/data/flowtracks-examples/marimo_phase_averaging_demo.py +0 -0
  16. {flowtracks-1.6.2.dev6.data → flowtracks-1.6.2.dev12.data}/data/flowtracks-examples/marimo_phase_metrics.py +0 -0
  17. {flowtracks-1.6.2.dev6.data → flowtracks-1.6.2.dev12.data}/data/flowtracks-examples/marimo_vtk_writers_preview.py +0 -0
  18. {flowtracks-1.6.2.dev6.data → flowtracks-1.6.2.dev12.data}/data/flowtracks-examples/marimo_zarr_dashboard.py +0 -0
  19. {flowtracks-1.6.2.dev6.data → flowtracks-1.6.2.dev12.data}/data/flowtracks-examples/repeated_interpolation.ipynb +0 -0
  20. {flowtracks-1.6.2.dev6.data → flowtracks-1.6.2.dev12.data}/data/flowtracks-examples/run_pipeline.py +0 -0
  21. {flowtracks-1.6.2.dev6.data → flowtracks-1.6.2.dev12.data}/data/flowtracks-examples/visualize_marimo.py +0 -0
  22. {flowtracks-1.6.2.dev6.data → flowtracks-1.6.2.dev12.data}/data/flowtracks-examples/visualize_results.py +0 -0
  23. {flowtracks-1.6.2.dev6.data → flowtracks-1.6.2.dev12.data}/scripts/analyse_fhdf.py +0 -0
  24. {flowtracks-1.6.2.dev6.dist-info → flowtracks-1.6.2.dev12.dist-info}/WHEEL +0 -0
  25. {flowtracks-1.6.2.dev6.dist-info → flowtracks-1.6.2.dev12.dist-info}/entry_points.txt +0 -0
  26. {flowtracks-1.6.2.dev6.dist-info → flowtracks-1.6.2.dev12.dist-info}/licenses/LICENSE.txt +0 -0
  27. {flowtracks-1.6.2.dev6.dist-info → flowtracks-1.6.2.dev12.dist-info}/licenses/LICENSES/BSD-3-Clause.txt +0 -0
  28. {flowtracks-1.6.2.dev6.dist-info → flowtracks-1.6.2.dev12.dist-info}/licenses/LICENSES/GPL-3.0-only.txt +0 -0
  29. {flowtracks-1.6.2.dev6.dist-info → flowtracks-1.6.2.dev12.dist-info}/top_level.txt +0 -0
flowtracks/_version.py CHANGED
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '1.6.2.dev6'
22
- __version_tuple__ = version_tuple = (1, 6, 2, 'dev6')
21
+ __version__ = version = '1.6.2.dev12'
22
+ __version_tuple__ = version_tuple = (1, 6, 2, 'dev12')
23
23
 
24
- __commit_id__ = commit_id = 'g519243be4'
24
+ __commit_id__ = commit_id = 'g4ea7448e5'
flowtracks/clean.py ADDED
@@ -0,0 +1,202 @@
1
+ # SPDX-License-Identifier: BSD-3-Clause
2
+ # SPDX-FileCopyrightText: Alex Liberzon
3
+ """
4
+ Cleaning chain for raw tracker output: repair, kink split, smoothing, stitching.
5
+
6
+ Raw linkage from a tracker (one row per point, with a trajectory id) is turned
7
+ into smooth trajectories in one call, :func:`clean_trajectories`:
8
+
9
+ 1. repair (:func:`flowtracks.repair.repair_arrays`): cut wrong links, attach
10
+ single points, join pieces across short gaps;
11
+ 2. Savitzky-Golay smoothing (:func:`flowtracks.smoothing.savitzky_golay`);
12
+ 3. kink split: a fast, violent turn (larger than ``kink_deg`` on steps longer
13
+ than ``kink_step_mm``) is a wrong link the repair missed; the pieces get
14
+ fresh ids;
15
+ 4. smoothing, a first stitch (``gap1``, ``dist1``, ``vd1``), smoothing;
16
+ 5. a second kink split, smoothing, a second, tighter stitch (``gap2``,
17
+ ``dist2``, ``vd2``);
18
+ 6. final smoothing (optionally a longer window for long trajectories).
19
+
20
+ Positions are in metres, so velocities are in m/s at the given frame rate.
21
+ Every setting has a default in :data:`CLEAN_DEFAULTS`; ``fps`` has none on
22
+ purpose -- the frame rate must be stated. :func:`resolve_settings` merges a
23
+ parameter-file section and overrides onto the defaults and reports where each
24
+ value came from.
25
+ """
26
+
27
+ import numpy as np
28
+
29
+ from flowtracks.repair import repair_arrays
30
+ from flowtracks.smoothing import savitzky_golay
31
+ from flowtracks.stitching import stitch_trajectories
32
+ from flowtracks.trajectory import Trajectory
33
+
34
+ #: Default settings of :func:`clean_trajectories` (positions in metres).
35
+ CLEAN_DEFAULTS = dict(
36
+ fps=None, # frame rate [Hz]; required
37
+ repair_max_gap=1, # repair: max frame gap for attach / join,
38
+ repair_max_distance_mm=0.3, # max distance [mm],
39
+ repair_link_percentile=99.9, # link check calibrated on this percentile,
40
+ repair_cut_factor=2.0, # cut threshold factor,
41
+ repair_cut=True, # and which of the three repairs run
42
+ repair_attach=True,
43
+ repair_join=True,
44
+ sg_window=7, # Savitzky-Golay window / order
45
+ sg_order=2,
46
+ kink_deg=90.0, # kink split: turn angle ...
47
+ kink_step_mm=25.0, # ... on steps longer than this
48
+ gap1=12, # first stitch: max gap [frames],
49
+ dist1=0.05, # max distance [m],
50
+ vd1=0.5, # max velocity difference [m/s]
51
+ gap2=8, # second (tight) stitch on clean ends
52
+ dist2=0.025,
53
+ vd2=0.35,
54
+ win_long=0, # >0: final smoothing (win_long, order_long)
55
+ order_long=3, # for tracks with >= len_long points
56
+ len_long=25,
57
+ )
58
+
59
+
60
+ def resolve_settings(section=None, overrides=None, source_name="parameter file"):
61
+ """Settings of :func:`clean_trajectories`: defaults <- section <- overrides.
62
+
63
+ Arguments:
64
+ section - mapping read from a parameter file (may be None).
65
+ overrides - mapping of explicit values (e.g. command-line flags); None
66
+ values are ignored.
67
+ source_name - how ``section`` is named in the returned sources.
68
+
69
+ Returns:
70
+ (settings, sources) - two dicts keyed like :data:`CLEAN_DEFAULTS`; sources
71
+ say "override", ``source_name`` or "default" for every key.
72
+
73
+ Raises ValueError on an unknown key in ``section`` or a missing ``fps``.
74
+ """
75
+ section = dict(section or {})
76
+ overrides = {k: v for k, v in (overrides or {}).items() if v is not None}
77
+ unknown = sorted(set(section) - set(CLEAN_DEFAULTS))
78
+ if unknown:
79
+ raise ValueError(f"unknown cleaning settings: {unknown}")
80
+ settings, sources = {}, {}
81
+ for k, v in CLEAN_DEFAULTS.items():
82
+ cast = float if k == "fps" else type(v)
83
+ if k in overrides:
84
+ settings[k], sources[k] = cast(overrides[k]), "override"
85
+ elif k in section:
86
+ settings[k], sources[k] = cast(section[k]), source_name
87
+ else:
88
+ settings[k], sources[k] = v, "default"
89
+ if settings["fps"] is None:
90
+ raise ValueError("fps (frame rate) must be given")
91
+ return settings, sources
92
+
93
+
94
+ def trajectories_from_arrays(trajid, time, pos):
95
+ """Trajectory objects (zero velocity) from flat one-row-per-point arrays.
96
+
97
+ Rows are grouped by ``trajid`` and ordered by ``time`` inside each group;
98
+ groups of a single point are dropped.
99
+ """
100
+ trajid, time, pos = np.asarray(trajid), np.asarray(time), np.asarray(pos)
101
+ order = np.argsort(trajid, kind="stable")
102
+ tj, tm, pp = trajid[order], time[order], pos[order]
103
+ bounds = np.flatnonzero(np.diff(tj)) + 1
104
+ starts = np.r_[0, bounds]
105
+ out = []
106
+ for i, s in enumerate(starts):
107
+ e = int(bounds[i]) if i < len(bounds) else len(tj)
108
+ if e - s >= 2:
109
+ o = np.argsort(tm[s:e], kind="stable")
110
+ out.append(Trajectory(pp[s:e][o], np.zeros_like(pp[s:e][o]),
111
+ tm[s:e][o], int(tj[s])))
112
+ return out
113
+
114
+
115
+ def kink_split(trajs, max_deg, step_min, fresh_start=10_000_000):
116
+ """Split trajectories at fast, violent turns.
117
+
118
+ A point is a cut point when the velocity turns by more than ``max_deg``
119
+ degrees there and both the step into it and the step out of it are longer
120
+ than ``step_min`` (position units). Every piece of a cut trajectory gets a
121
+ fresh id counting from ``fresh_start``, so readers grouping by id cannot
122
+ put the pieces back together.
123
+
124
+ Returns:
125
+ (trajectories, number of cuts)
126
+ """
127
+ out, ncuts, nxt = [], 0, fresh_start
128
+ for tr in trajs:
129
+ v = tr.velocity()
130
+ n = np.linalg.norm(v, axis=1)
131
+ t, p = np.asarray(tr.time()), tr.pos()
132
+ st = np.linalg.norm(np.diff(p, axis=0), axis=1)
133
+ ok = (n[:-1] > 1e-12) & (n[1:] > 1e-12)
134
+ cosang = np.ones(len(v) - 1)
135
+ cosang[ok] = np.clip((v[:-1][ok] * v[1:][ok]).sum(1) / (n[:-1][ok] * n[1:][ok]),
136
+ -1, 1)
137
+ ang = np.degrees(np.arccos(cosang))
138
+ instep = np.concatenate([st[:1], st[:-1]])
139
+ bad = np.flatnonzero((ang > max_deg) & (instep > step_min) & (st > step_min)) + 1
140
+ ncuts += len(bad)
141
+ segs = np.split(np.arange(len(t)), bad)
142
+ if len(segs) == 1:
143
+ out.append(tr)
144
+ continue
145
+ for seg in segs:
146
+ if len(seg) >= 2:
147
+ out.append(Trajectory(p[seg], v[seg], t[seg], nxt))
148
+ nxt += 1
149
+ return out, ncuts
150
+
151
+
152
+ def clean_trajectories(trajid, time, pos, settings, log=print):
153
+ """Run the cleaning chain (module docstring) on flat tracker output.
154
+
155
+ Arguments:
156
+ trajid, time, pos - (N,), (N,), (N, 3) one row per point, pos in metres.
157
+ settings - complete settings, e.g. from :func:`resolve_settings`.
158
+ log - callable for progress lines, or None for silence.
159
+
160
+ Returns:
161
+ list of smoothed :class:`~flowtracks.trajectory.Trajectory` objects.
162
+ """
163
+ log = log or (lambda *a, **k: None)
164
+ c = settings
165
+ fps, sgw, sgo = c["fps"], c["sg_window"], c["sg_order"]
166
+ tj, rep = repair_arrays(
167
+ trajid, time, pos, max_gap=c["repair_max_gap"],
168
+ max_distance=c["repair_max_distance_mm"] * 1e-3,
169
+ link_percentile=c["repair_link_percentile"], cut=c["repair_cut"],
170
+ attach=c["repair_attach"], join=c["repair_join"],
171
+ cut_factor=c["repair_cut_factor"])
172
+ log(f"repair: cut={rep.get('links_cut')} joins={rep.get('joins')}")
173
+ s1 = savitzky_golay(trajectories_from_arrays(tj, time, pos), fps, sgw, sgo,
174
+ min_window=5)
175
+ split, ncuts = kink_split(s1, c["kink_deg"], c["kink_step_mm"] / 1000.0)
176
+ log(f"kinksplit(>{c['kink_deg']}deg, steps>{c['kink_step_mm']}mm): "
177
+ f"{len(s1)} -> {len(split)} objs, {ncuts} cuts")
178
+ s2 = savitzky_golay(split, fps, sgw, sgo, min_window=5)
179
+ st = stitch_trajectories(s2, fps=fps, max_gap=c["gap1"],
180
+ max_distance=c["dist1"], max_vel_diff=c["vd1"])
181
+ s2b = savitzky_golay(st, fps, sgw, sgo, min_window=5)
182
+ split2, _ = kink_split(s2b, c["kink_deg"], c["kink_step_mm"] / 1000.0,
183
+ fresh_start=20_000_000)
184
+ s2c = savitzky_golay(split2, fps, sgw, sgo, min_window=5)
185
+ st2 = stitch_trajectories(s2c, fps=fps, max_gap=c["gap2"],
186
+ max_distance=c["dist2"], max_vel_diff=c["vd2"])
187
+ if c["win_long"] > 0:
188
+ short = [t for t in st2 if len(t) < c["len_long"]]
189
+ long = [t for t in st2 if len(t) >= c["len_long"]]
190
+ s_short = savitzky_golay(short, fps, sgw, sgo, min_window=5)
191
+ s_long = savitzky_golay(long, fps, c["win_long"], c["order_long"],
192
+ min_window=7)
193
+ out = s_short + s_long
194
+ log(f"two-tier final smooth: {len(s_short)}x({sgw},{sgo}) + {len(s_long)}x"
195
+ f"({c['win_long']},{c['order_long']})")
196
+ else:
197
+ out = savitzky_golay(st2, fps, sgw, sgo, min_window=5)
198
+ lens = np.array([len(t) for t in out]) if out else np.zeros(1, int)
199
+ log(f"final: n={len(out)} joined1={len(s2) - len(st)} "
200
+ f"joined2={len(s2c) - len(st2)} med={np.median(lens):.0f} "
201
+ f"max={lens.max()} rows={lens.sum()}")
202
+ return out
flowtracks/graphics.py CHANGED
@@ -315,3 +315,197 @@ def plot_trajectories_3d(source, trajids=None, scalars="speed", context=0,
315
315
  if show:
316
316
  plotter.show()
317
317
  return plotter
318
+
319
+
320
+ def _far_walls(bounds, toward_camera, step):
321
+ """Grey back walls (the three box faces away from the camera) and their
322
+ grid lines every ``step``, as two PolyData."""
323
+ import pyvista as pv
324
+
325
+ lo, hi = np.asarray(bounds[0::2], float), np.asarray(bounds[1::2], float)
326
+ walls, lines = [], []
327
+ for ax in range(3):
328
+ level = lo[ax] if toward_camera[ax] > 0 else hi[ax]
329
+ u, v = [a for a in range(3) if a != ax]
330
+ corners = np.zeros((4, 3))
331
+ corners[:, ax] = level
332
+ corners[:, u] = [lo[u], hi[u], hi[u], lo[u]]
333
+ corners[:, v] = [lo[v], lo[v], hi[v], hi[v]]
334
+ walls.append(pv.PolyData(corners, faces=[4, 0, 1, 2, 3]))
335
+ # grid lines lifted a hair off the wall so they never z-fight it
336
+ lift = 1e-3 * (hi[ax] - lo[ax]) * (1 if level == lo[ax] else -1)
337
+ for a, b in ((u, v), (v, u)):
338
+ ticks = np.arange(np.ceil(lo[a] / step) * step, hi[a] + 1e-9, step)
339
+ for t in ticks:
340
+ seg = np.zeros((2, 3))
341
+ seg[:, ax] = level + lift
342
+ seg[:, a] = t
343
+ seg[:, b] = [lo[b], hi[b]]
344
+ lines.append(pv.Line(seg[0], seg[1]))
345
+ return pv.merge(walls), pv.merge(lines)
346
+
347
+
348
+ def _box_axis_labels(pl, bounds, toward_camera, step, titles, font_size):
349
+ """Tick labels every ``step`` and axis titles along the box edges nearest
350
+ the camera on the floor (x, z) and on the far vertical edge (y), as
351
+ ParaView draws them. (VTK's cube-axes actor caps the label count at its
352
+ own tick spacing, so it cannot label every ``step``.)"""
353
+ lo, hi = np.asarray(bounds[0::2], float), np.asarray(bounds[1::2], float)
354
+ near = np.where(np.asarray(toward_camera) > 0, hi, lo)
355
+ far = np.where(np.asarray(toward_camera) > 0, lo, hi)
356
+ out = np.where(np.asarray(toward_camera) > 0, 1.0, -1.0) # outward, camera side
357
+ off = 0.035 * np.max(hi - lo)
358
+ edges = ( # axis, anchor of the edge, outward offset of its labels
359
+ (0, np.array([0.0, lo[1], near[2]]), np.array([0.0, -off, out[2] * off])),
360
+ (2, np.array([near[0], lo[1], 0.0]), np.array([out[0] * off, -off, 0.0])),
361
+ (1, np.array([far[0], 0.0, near[2]]), np.array([-out[0] * off, 0.0, out[2] * off])),
362
+ )
363
+ for ax, anchor, shift in edges:
364
+ ticks = np.arange(np.ceil(lo[ax] / step) * step, hi[ax] + 1e-9, step)
365
+ pts = np.tile(anchor + shift, (len(ticks), 1))
366
+ pts[:, ax] = ticks
367
+ pl.add_point_labels(pts, [f"{t:.0f}" for t in ticks], font_size=font_size,
368
+ text_color="black", shape=None, show_points=False,
369
+ always_visible=True, name=f"ticks{ax}")
370
+ mid = anchor + 2.6 * shift
371
+ mid[ax] = (lo[ax] + hi[ax]) / 2
372
+ pl.add_point_labels([mid], [titles[ax]], font_size=int(font_size * 1.3),
373
+ text_color="black", shape=None, show_points=False,
374
+ always_visible=True, name=f"title{ax}")
375
+
376
+
377
+ def _use_font_file(pl, font_file):
378
+ """Point every text of the plotter at a TrueType file. (VTK's built-in
379
+ sans font draws '[' and ']' as parentheses.)"""
380
+ props = []
381
+ for actor in pl.actors.values():
382
+ if hasattr(actor, "GetTextProperty"):
383
+ props.append(actor.GetTextProperty())
384
+ mapper = actor.GetMapper() if hasattr(actor, "GetMapper") else None
385
+ if mapper is not None and hasattr(mapper, "GetLabelTextProperty"):
386
+ props.append(mapper.GetLabelTextProperty())
387
+ # add_point_labels: the style sits on the label hierarchy feeding the mapper
388
+ source = mapper.GetInputAlgorithm() if hasattr(mapper, "GetInputAlgorithm") else None
389
+ if source is not None and hasattr(source, "GetTextProperty"):
390
+ props.append(source.GetTextProperty())
391
+ for bar in pl.scalar_bars.values():
392
+ props += [bar.GetTitleTextProperty(), bar.GetLabelTextProperty()]
393
+ for prop in props:
394
+ prop.SetFontFamily(4) # VTK_FONT_FILE
395
+ prop.SetFontFile(str(font_file))
396
+
397
+
398
+ def animate_trajectories_3d(source, out_dir, frames=None, *, component=1, tail=20,
399
+ pos_scale=1.0, bounds=None, cmap="coolwarm",
400
+ clim=(-0.5, 0.5), bar_title=None, bar_labels=21,
401
+ bar_fmt="%.2f", view_direction=(-1.0, 1.0, 1.0),
402
+ view_up=(0.0, 1.0, 0.0), zoom=1.0, window_center=(0.0, 0.0),
403
+ window_size=(1920, 1080), point_size=6.0,
404
+ grid_step=500.0, axis_titles=("x", "y", "z"),
405
+ wall_color=(0.54, 0.54, 0.54), background="white",
406
+ font_size=20, font_file=None, outlines=(),
407
+ outline_color="black", outline_width=3.0, movie=None,
408
+ fps=10, label=None):
409
+ """Off-screen movie frames of trajectories as trailing beads (PyVista).
410
+
411
+ Every frame shows each particle's last ``tail`` positions as shaded
412
+ spheres coloured by one velocity component (``component``: 0, 1, 2), in a
413
+ box with grey back walls, a ``grid_step`` grid and labelled axes, seen
414
+ with a fixed parallel-projection camera -- the usual ParaView look.
415
+
416
+ source - zarr path / ZarrScene / Scene, as :func:`plot_trajectories_3d`.
417
+ out_dir - PNG frames are written as ``frame_0000.png`` ...
418
+ frames - frame numbers to render (default: every frame of the source).
419
+ pos_scale - multiplies positions (e.g. 1000 for m -> mm axes).
420
+ bounds - (x0, x1, y0, y1, z0, z1) of the box, in scaled units (default:
421
+ the data's 0.5-99.5 percentile box).
422
+ view_direction - from the scene towards the camera; ``view_up`` the
423
+ screen's up; ``zoom`` > 1 enlarges; ``window_center`` shifts the
424
+ projection centre in normalized window units (VTK SetWindowCenter).
425
+ movie - if given, the frames are also encoded to this .mp4 with ffmpeg.
426
+ label - optional ``str.format`` text drawn bottom-left, given ``frame``.
427
+ font_file - TrueType file for all text (VTK's built-in font has no
428
+ square brackets).
429
+ outlines - (N, 3) point arrays in scaled units, each drawn as a closed
430
+ line (e.g. the top and bottom rims of a cylindrical vessel).
431
+
432
+ Returns the list of PNG paths.
433
+ """
434
+ import shutil
435
+ import subprocess
436
+
437
+ import pyvista as pv
438
+
439
+ from flowtracks.writers import _trajectory_arrays
440
+
441
+ pos, vel, time, _ = _trajectory_arrays(source)
442
+ pos = pos * pos_scale
443
+ value = vel[:, component]
444
+ order = np.argsort(time, kind="stable")
445
+ pos, value, time = pos[order], value[order], time[order]
446
+ if frames is None:
447
+ frames = np.unique(time)
448
+ if bounds is None:
449
+ lo, hi = np.percentile(pos, 0.5, axis=0), np.percentile(pos, 99.5, axis=0)
450
+ bounds = (lo[0], hi[0], lo[1], hi[1], lo[2], hi[2])
451
+ out_dir = Path(out_dir)
452
+ out_dir.mkdir(parents=True, exist_ok=True)
453
+
454
+ direction = np.asarray(view_direction, float)
455
+ direction /= np.linalg.norm(direction)
456
+ walls, grid = _far_walls(bounds, direction, grid_step)
457
+
458
+ pl = pv.Plotter(off_screen=True, window_size=list(window_size))
459
+ pl.set_background(background)
460
+ pl.add_mesh(walls, color=wall_color, lighting=False, name="walls")
461
+ pl.add_mesh(grid, color="black", line_width=1.5, name="grid")
462
+ for k, ring in enumerate(outlines):
463
+ ring = np.asarray(ring, float)
464
+ line = pv.lines_from_points(np.vstack([ring, ring[:1]]))
465
+ pl.add_mesh(line, color=outline_color, line_width=outline_width,
466
+ render_lines_as_tubes=True, name=f"outline{k}")
467
+ _box_axis_labels(pl, bounds, direction, grid_step, axis_titles, font_size)
468
+ pl.add_axes(viewport=(0.0, 0.82, 0.12, 1.0))
469
+ centre = np.array([(bounds[0] + bounds[1]) / 2, (bounds[2] + bounds[3]) / 2,
470
+ (bounds[4] + bounds[5]) / 2])
471
+ size = np.linalg.norm(np.subtract(bounds[1::2], bounds[0::2]))
472
+ pl.enable_parallel_projection()
473
+ pl.camera.focal_point = centre
474
+ pl.camera.position = centre + direction * 4 * size
475
+ pl.camera.up = view_up
476
+ pl.reset_camera(bounds=list(bounds))
477
+ pl.camera.parallel_scale /= zoom
478
+ pl.camera.SetWindowCenter(*window_center)
479
+
480
+ bar = dict(title=bar_title or "", vertical=True, position_x=0.93, position_y=0.02,
481
+ height=0.96, width=0.012, n_labels=bar_labels, fmt=bar_fmt,
482
+ color="black", title_font_size=int(font_size * 1.2),
483
+ label_font_size=font_size)
484
+ paths = []
485
+ starts = np.searchsorted(time, np.asarray(frames) - tail + 1, side="left")
486
+ ends = np.searchsorted(time, np.asarray(frames), side="right")
487
+ for i, (frame, s, e) in enumerate(zip(frames, starts, ends)):
488
+ beads = pv.PolyData(pos[s:e] if e > s else np.zeros((0, 3)))
489
+ if e > s:
490
+ beads.point_data["value"] = value[s:e]
491
+ pl.add_mesh(beads, scalars="value", cmap=cmap, clim=clim,
492
+ point_size=point_size, render_points_as_spheres=True,
493
+ style="points", scalar_bar_args=bar, name="beads")
494
+ if label:
495
+ pl.add_text(label.format(frame=frame), position="lower_left",
496
+ font_size=font_size, color="black", name="label")
497
+ if font_file:
498
+ _use_font_file(pl, font_file)
499
+ path = out_dir / f"frame_{i:04d}.png"
500
+ pl.screenshot(str(path))
501
+ paths.append(path)
502
+ pl.close()
503
+
504
+ if movie is not None:
505
+ ffmpeg = shutil.which("ffmpeg")
506
+ if ffmpeg is None:
507
+ raise RuntimeError("ffmpeg not found; the PNG frames are in " + str(out_dir))
508
+ subprocess.run([ffmpeg, "-v", "error", "-y", "-framerate", str(fps),
509
+ "-i", str(out_dir / "frame_%04d.png"), "-c:v", "libx264",
510
+ "-pix_fmt", "yuv420p", "-crf", "18", str(movie)], check=True)
511
+ return paths
flowtracks/merge.py ADDED
@@ -0,0 +1,150 @@
1
+ # SPDX-License-Identifier: BSD-3-Clause
2
+ # SPDX-FileCopyrightText: Alex Liberzon
3
+ """
4
+ Merging trajectory datasets of two camera systems that see a common volume.
5
+
6
+ Two systems (two camera rigs, two recordings of one rig) each produce their
7
+ own trajectories in their own coordinate frame. Where their volumes overlap
8
+ the same particle appears twice. This module
9
+
10
+ 1. measures the rigid map between the two frames from particles both systems
11
+ see in the same frame (:func:`estimate_rigid_map`): for each candidate
12
+ rotation, the same-frame neighbour differences pile up at the true offset;
13
+ the rotation whose pile is sharpest wins and the offset is refined on tight
14
+ pairs. No sharp pile means no common particles, and it refuses
15
+ (ValueError) rather than guess;
16
+ 2. maps points of the second system into the first (:func:`apply_rigid`);
17
+ 3. finds the trajectory pairs that share same-frame points closer than a
18
+ tolerance (:func:`overlap_counts`), each trajectory's best partner
19
+ (:func:`best_partners`) and the duplicates a keep-the-longer rule removes
20
+ (:func:`duplicates`).
21
+
22
+ Units are whatever the inputs use; tolerances are in the same unit.
23
+ """
24
+
25
+ import numpy as np
26
+ from scipy.spatial import cKDTree
27
+
28
+
29
+ def estimate_rigid_map(pairs, rotations, search_radius=150.0, bin_size=6.0,
30
+ tight=8.0, min_peak=50, min_ratio=100.0, refine=2,
31
+ log=print):
32
+ """Rigid map from system B to system A: ``p_A = R @ p_B - offset``.
33
+
34
+ Arguments:
35
+ pairs - list of (points_A, points_B) arrays, (n, 3) and (m, 3), each pair
36
+ taken in the same frame (use well-triangulated points only).
37
+ rotations - list of (label, 3x3 matrix) candidates for R.
38
+ search_radius - neighbours of a point of A within this distance of the
39
+ rotated B points vote for the offset.
40
+ bin_size - histogram bin of the votes; the offset is the densest bin.
41
+ tight - pairs closer than this after the first estimate refine it.
42
+ min_peak, min_ratio - the densest bin must hold at least ``min_peak``
43
+ votes and ``min_ratio`` times the mean, or the map is refused.
44
+ refine - number of refinement passes (median of the tight pairs).
45
+ log - callable for progress lines, or None.
46
+
47
+ Returns:
48
+ dict with matrix (3x3), offset (3,), label of the chosen rotation,
49
+ n_pairs (tight pairs of the last pass) and spread (median absolute
50
+ deviation of their differences, per axis).
51
+ """
52
+ log = log or (lambda *a, **k: None)
53
+ nbins = int(round(2 * search_radius / bin_size))
54
+ best = None
55
+ for label, M in rotations:
56
+ M = np.asarray(M, dtype=float)
57
+ diffs = []
58
+ for pa, pb in pairs:
59
+ if not len(pa) or not len(pb):
60
+ continue
61
+ pbm = pb @ M.T
62
+ for k, js in enumerate(cKDTree(pbm).query_ball_point(pa, search_radius)):
63
+ if js:
64
+ diffs.append(pbm[js] - pa[k])
65
+ if not diffs:
66
+ continue
67
+ d = np.vstack(diffs)
68
+ H, edges = np.histogramdd(d, bins=nbins,
69
+ range=[(-search_radius, search_radius)] * 3)
70
+ i = np.unravel_index(H.argmax(), H.shape)
71
+ peak = np.array([edges[k][i[k]] + bin_size / 2 for k in range(3)])
72
+ score = H.max() / max(H.mean(), 1e-9)
73
+ log(f" rotation {label}: densest {bin_size:g} bin {int(H.max())} "
74
+ f"({score:.0f}x the mean)")
75
+ if best is None or H.max() > best[1]:
76
+ best = (M, H.max(), score, peak, label)
77
+ if best is None or best[1] < min_peak or best[2] < min_ratio:
78
+ raise ValueError("no common particles between the two systems")
79
+ M, _, _, off, label = best
80
+ for _ in range(refine):
81
+ dd = []
82
+ for pa, pb in pairs:
83
+ if not len(pa) or not len(pb):
84
+ continue
85
+ q = pb @ M.T - off
86
+ dist, j = cKDTree(q).query(pa, distance_upper_bound=tight)
87
+ ok = np.isfinite(dist)
88
+ dd.append(pb[j[ok]] @ M.T - pa[ok])
89
+ dd = np.vstack(dd)
90
+ off = np.median(dd, axis=0)
91
+ spread = np.median(np.abs(dd - off), axis=0)
92
+ log(f" B -> A: rotation {label}, offset {np.round(off, 2)}, "
93
+ f"{len(dd)} pairs, spread (MAD) {np.round(spread, 2)}")
94
+ return dict(matrix=M, offset=off, label=label, n_pairs=len(dd), spread=spread)
95
+
96
+
97
+ def apply_rigid(points, matrix, offset=0.0):
98
+ """``points @ matrix.T - offset``: system-B points (or, with offset 0,
99
+ velocities and accelerations) in system A's frame."""
100
+ return np.asarray(points, dtype=float) @ np.asarray(matrix).T - offset
101
+
102
+
103
+ def overlap_counts(tracks_a, tracks_b, tol):
104
+ """Same-frame shared points of every trajectory pair of A and B.
105
+
106
+ Arguments:
107
+ tracks_a, tracks_b - mappings id -> mapping with "time" (n,) and "pos"
108
+ (n, 3), both in one frame of reference.
109
+ tol - a point of A and the nearest point of B in the same frame closer
110
+ than this count as shared.
111
+
112
+ Returns:
113
+ {(id_a, id_b): number of shared points}
114
+ """
115
+ bframe = {}
116
+ for tid, t in tracks_b.items():
117
+ for f, p in zip(t["time"], t["pos"]):
118
+ bframe.setdefault(int(f), []).append((tid, p))
119
+ btree = {f: (cKDTree(np.array([p for _, p in v])), v) for f, v in bframe.items()}
120
+ shared = {}
121
+ for tid, t in tracks_a.items():
122
+ for f, p in zip(t["time"], t["pos"]):
123
+ if int(f) not in btree:
124
+ continue
125
+ tree, v = btree[int(f)]
126
+ dist, j = tree.query(p, k=1)
127
+ if dist < tol:
128
+ key = (tid, v[j][0])
129
+ shared[key] = shared.get(key, 0) + 1
130
+ return shared
131
+
132
+
133
+ def best_partners(shared):
134
+ """Each trajectory's partner in the other system: the one it shares most
135
+ points with. Returns ({id_a: (id_b, n)}, {id_b: (id_a, n)})."""
136
+ partner_a, partner_b = {}, {}
137
+ for (atid, btid), n in shared.items():
138
+ if n > partner_a.get(atid, (None, 0))[1]:
139
+ partner_a[atid] = (btid, n)
140
+ if n > partner_b.get(btid, (None, 0))[1]:
141
+ partner_b[btid] = (atid, n)
142
+ return partner_a, partner_b
143
+
144
+
145
+ def duplicates(shared, len_a, len_b):
146
+ """Ids of B trajectories that share any point with an A trajectory at
147
+ least as long (``len_a``, ``len_b``: id -> number of points). Dropping
148
+ them keeps one copy of every overlapping particle, the longer one, and
149
+ A on ties."""
150
+ return {btid for (atid, btid) in shared if len_a[atid] >= len_b[btid]}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowtracks
3
- Version: 1.6.2.dev6
3
+ Version: 1.6.2.dev12
4
4
  Summary: Library for handling 3D PTV trajectory databases and Lagrangian/Eulerian turbulence post-processing.
5
5
  Author-email: Yosef Meller <yosefm@gmail.com>
6
6
  License-Expression: GPL-3.0-only AND BSD-3-Clause
@@ -0,0 +1,55 @@
1
+ flowtracks/__init__.py,sha256=iCexskmRtpNJ0fNZWRFkQAqrku7csbKy5NtL2YQNYeo,1532
2
+ flowtracks/_version.py,sha256=MhSZ23Xb3S2pgcATsSw3UWCpsNnTFAH_LfLqatUUdYs,543
3
+ flowtracks/an_scene.py,sha256=QV_gtzp-5h1CqAW1Y6ieGco9BxXQDE4f_7-fcy3fluI,7361
4
+ flowtracks/analysis.py,sha256=nob5-pKxjPzB34_oQsxmxYALz4Q_s1BTpC5_VgXX3ss,6320
5
+ flowtracks/clean.py,sha256=IFwyoR7IfA98u0jjqp99EyeRqA1sOVlEiNH9My-pjzg,9055
6
+ flowtracks/combine.py,sha256=-UQtbTH4vjQS88_92h5UZlS60OfQofTWEV6SL1prXgI,4455
7
+ flowtracks/eulerian.py,sha256=PMt0_TtjrFwRPEvSEKm_wn8YZdClSux4yXEuxiB2EjM,41872
8
+ flowtracks/flux.py,sha256=w4K_jjDWTBIOj4BkoeNoKjIRcvg_6stCqNF8iivxF-A,15137
9
+ flowtracks/format_docstrings.py,sha256=iJVJmtTUYEDEcmF5XSRZ08zpvlVc9lU66CZeqSpEKic,1987
10
+ flowtracks/future_idea_xarray_dask_zarr.py,sha256=4ZS6QuZ2s_sUaiCW0XBo74GrP5fw_clVyhWNjT5gbds,6101
11
+ flowtracks/graphics.py,sha256=PMyfYPedfBumIJ9cPeTCukkFFPGNr9lyq49BsOyVBzA,23750
12
+ flowtracks/interpolation.py,sha256=njuB-bvWK5M2tc2Ibm6ZWNg6Tm6H-PTn0a63IldlJaA,39414
13
+ flowtracks/io.py,sha256=sujLgtY8WIymMZugDv2jlQeL9QR_hZn-MaZPPKsAPvA,48366
14
+ flowtracks/merge.py,sha256=hg5sYw2oL1YuxpbefiYAESUThKaAXs00RcsV9StIjSc,6445
15
+ flowtracks/nhist.py,sha256=3xdrfSTSpTojXJcyhwBgagHBmKrm0U_AEsP3BjP9H1o,4089
16
+ flowtracks/pairs.py,sha256=ehYDDTmjeRuzkSAMpdlwnwwAZOPNLKGF_e5HmcKw-2g,4034
17
+ flowtracks/particle.py,sha256=sVng1X8Xp3V9_wgJK2EGsDEx1OFQjd3L63Fzn--MWkM,671
18
+ flowtracks/phase_average.py,sha256=jjtrHYg0ukGRnJtRawJ6sWhdyrlV_16qBLlfNVxEQHw,3035
19
+ flowtracks/phasing.py,sha256=us7WfMYrx6dCCPNwucvN34G_0A7EKS9ieH1TrnpwM0I,4320
20
+ flowtracks/pipeline.py,sha256=0wOeeFqQGN_Lhgq-QFeQNg0oEKtJSGDhhZdv81M1PDg,5167
21
+ flowtracks/provenance.py,sha256=8L2BXmLdBcZRncUkr8oJbeU5JKdebj1Cgxrbzdy5o7A,2115
22
+ flowtracks/repair.py,sha256=rxaE1hcxQjNx19tLJOhL619lSfkB7Kfh6kXpJ_fSMfg,19122
23
+ flowtracks/scene.py,sha256=ovR1wXmgLGB9mZo62NPkcgxUzhlqiEcItLYJINAl2zk,19622
24
+ flowtracks/sequence.py,sha256=u_HOev5YydpgoknPQ1lB2WUG5ZZKSu_u9ySNGUu9D38,12984
25
+ flowtracks/smoothing.py,sha256=wXlvi3RBN6iZgaQRAVOxprhsjLMlxc4JIcuQMpRCQvs,7376
26
+ flowtracks/stitching.py,sha256=HFsIRdAfqyhmES1OwIFslyWTF9U7kPJ5OKtjBl4YWqA,16895
27
+ flowtracks/trajectory.py,sha256=uL4-byZ2tycdrAwGoET4AfLHuKaWRnc8VHLO5zv9MOg,11831
28
+ flowtracks/vortex.py,sha256=4lG3QkvvnWhAHy4tl1pJrBGFLdrlP4pxNdhCZI1VZ4Q,2529
29
+ flowtracks/wall_shear.py,sha256=k9Q5FEchIO9J_onXPOw4PNJQfmsrSyVVvX4RdW-rQlE,6788
30
+ flowtracks/writers.py,sha256=0LnRiuqmyhcw8Ju3v9vCOERymNmxHG7cXKy6r8uLOTo,10358
31
+ flowtracks/zarr_scene.py,sha256=PKCtiTT1t0Hj74Q572ra5DM3uM8lQK1A6DzzfUxvIzI,10468
32
+ flowtracks-1.6.2.dev12.data/data/flowtracks-examples/batch_Lagrangian_to_Eulerian.py,sha256=e7mQFqyqvCeAHRKiN1IaxdNJi0e2FU84c7QNrx0L-m0,2296
33
+ flowtracks-1.6.2.dev12.data/data/flowtracks-examples/hdf5_scene_analysis.ipynb,sha256=jn0Z_ZVL1-iXaiKXwyAeb74NbXsBpnocCxCSDh814eU,243097
34
+ flowtracks-1.6.2.dev12.data/data/flowtracks-examples/linking_trajectories.ipynb,sha256=udz0GLDiPv68BYySg4yO1HWzaB9Sdy3YA3IhnIHDeII,8820
35
+ flowtracks-1.6.2.dev12.data/data/flowtracks-examples/marimo_dual_rig_trajectories.py,sha256=dzeK0C-knwq1bPOwo0p1RhWrb0RPp-T31rBaGEptXcc,6737
36
+ flowtracks-1.6.2.dev12.data/data/flowtracks-examples/marimo_flowtracks.py,sha256=7FIJNwBx9aBRmaM5ofmYTXSWEjE0wZqDfs8J47Tmw6U,9440
37
+ flowtracks-1.6.2.dev12.data/data/flowtracks-examples/marimo_hdf5_vs_zarr_scene_analysis.py,sha256=bjXlMsLJTxJ0JkvGcVZbuDOFYZU4OvdabtDUgqb15So,4536
38
+ flowtracks-1.6.2.dev12.data/data/flowtracks-examples/marimo_linking_trajectories_zarr.py,sha256=Ny_WUbiZWo0aKw-u2KGFWrhMv-8BvNgBy0UHIqCobYg,8210
39
+ flowtracks-1.6.2.dev12.data/data/flowtracks-examples/marimo_phase_averaging_demo.py,sha256=vg6j2ayKGl-5uj08bdzzb6pujzJJQ_5bUsG7UuPP7dM,9270
40
+ flowtracks-1.6.2.dev12.data/data/flowtracks-examples/marimo_phase_metrics.py,sha256=kBc2e_b-orDrm3AnuUXCcxsvmfQWyCtE826pFeolMGE,9367
41
+ flowtracks-1.6.2.dev12.data/data/flowtracks-examples/marimo_vtk_writers_preview.py,sha256=Otrhhp-QFf6F94EOs6rWu5xOCpbS16EfWy_iMFp7e_I,14611
42
+ flowtracks-1.6.2.dev12.data/data/flowtracks-examples/marimo_zarr_dashboard.py,sha256=jVIum6hdEelH4ef7vRgsfLhrbmMsuErsJBzxrnEImnw,9268
43
+ flowtracks-1.6.2.dev12.data/data/flowtracks-examples/repeated_interpolation.ipynb,sha256=t9SfoIWRu5XP0SrphWbQy508uzyEibVz61FGjl51fjA,8861
44
+ flowtracks-1.6.2.dev12.data/data/flowtracks-examples/run_pipeline.py,sha256=5fGE2tIP6XjFZ1sEYmqBujuLaXFfRL-rNnbr2p2CDfM,7205
45
+ flowtracks-1.6.2.dev12.data/data/flowtracks-examples/visualize_marimo.py,sha256=jWrg5IWjY-bmSQwQjaO5qTr0t_G48CsqJYpevdQrxeY,13026
46
+ flowtracks-1.6.2.dev12.data/data/flowtracks-examples/visualize_results.py,sha256=SbPg3EoDcuIyo0rUz3E-AnT4sLWDVJFhgC6xVcracW4,7583
47
+ flowtracks-1.6.2.dev12.data/scripts/analyse_fhdf.py,sha256=QH08NdImGukTEGuZEKFxjStcSelXBv-8A-22G6KtXqM,1315
48
+ flowtracks-1.6.2.dev12.dist-info/licenses/LICENSE.txt,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
49
+ flowtracks-1.6.2.dev12.dist-info/licenses/LICENSES/BSD-3-Clause.txt,sha256=XiJR9O5snDB6sAz0bcncOU6msceZgjPRxejWEQGg40Q,1505
50
+ flowtracks-1.6.2.dev12.dist-info/licenses/LICENSES/GPL-3.0-only.txt,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
51
+ flowtracks-1.6.2.dev12.dist-info/METADATA,sha256=xq3JCpam1JKUGzMP8oeAQAaLdiRoF5LjVsH5nwQytz0,4774
52
+ flowtracks-1.6.2.dev12.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
53
+ flowtracks-1.6.2.dev12.dist-info/entry_points.txt,sha256=U6028E7yQZQiZoXWMl7w4xgZUPwPxAGiAuuMB__c-yQ,60
54
+ flowtracks-1.6.2.dev12.dist-info/top_level.txt,sha256=cR54RNb9y2Wfr_BfuOezmNJOMB4GfIHh1-luXTREnQA,11
55
+ flowtracks-1.6.2.dev12.dist-info/RECORD,,
@@ -1,53 +0,0 @@
1
- flowtracks/__init__.py,sha256=iCexskmRtpNJ0fNZWRFkQAqrku7csbKy5NtL2YQNYeo,1532
2
- flowtracks/_version.py,sha256=-2KICqAEdPWLo6oH9F7DHgtleuBtdR0ws16EQh2o6wM,541
3
- flowtracks/an_scene.py,sha256=QV_gtzp-5h1CqAW1Y6ieGco9BxXQDE4f_7-fcy3fluI,7361
4
- flowtracks/analysis.py,sha256=nob5-pKxjPzB34_oQsxmxYALz4Q_s1BTpC5_VgXX3ss,6320
5
- flowtracks/combine.py,sha256=-UQtbTH4vjQS88_92h5UZlS60OfQofTWEV6SL1prXgI,4455
6
- flowtracks/eulerian.py,sha256=PMt0_TtjrFwRPEvSEKm_wn8YZdClSux4yXEuxiB2EjM,41872
7
- flowtracks/flux.py,sha256=w4K_jjDWTBIOj4BkoeNoKjIRcvg_6stCqNF8iivxF-A,15137
8
- flowtracks/format_docstrings.py,sha256=iJVJmtTUYEDEcmF5XSRZ08zpvlVc9lU66CZeqSpEKic,1987
9
- flowtracks/future_idea_xarray_dask_zarr.py,sha256=4ZS6QuZ2s_sUaiCW0XBo74GrP5fw_clVyhWNjT5gbds,6101
10
- flowtracks/graphics.py,sha256=ulMTpPRGzQanAjna480XLMMQJL92Q4Wdtky8inqnwtE,13909
11
- flowtracks/interpolation.py,sha256=njuB-bvWK5M2tc2Ibm6ZWNg6Tm6H-PTn0a63IldlJaA,39414
12
- flowtracks/io.py,sha256=sujLgtY8WIymMZugDv2jlQeL9QR_hZn-MaZPPKsAPvA,48366
13
- flowtracks/nhist.py,sha256=3xdrfSTSpTojXJcyhwBgagHBmKrm0U_AEsP3BjP9H1o,4089
14
- flowtracks/pairs.py,sha256=ehYDDTmjeRuzkSAMpdlwnwwAZOPNLKGF_e5HmcKw-2g,4034
15
- flowtracks/particle.py,sha256=sVng1X8Xp3V9_wgJK2EGsDEx1OFQjd3L63Fzn--MWkM,671
16
- flowtracks/phase_average.py,sha256=jjtrHYg0ukGRnJtRawJ6sWhdyrlV_16qBLlfNVxEQHw,3035
17
- flowtracks/phasing.py,sha256=us7WfMYrx6dCCPNwucvN34G_0A7EKS9ieH1TrnpwM0I,4320
18
- flowtracks/pipeline.py,sha256=0wOeeFqQGN_Lhgq-QFeQNg0oEKtJSGDhhZdv81M1PDg,5167
19
- flowtracks/provenance.py,sha256=8L2BXmLdBcZRncUkr8oJbeU5JKdebj1Cgxrbzdy5o7A,2115
20
- flowtracks/repair.py,sha256=rxaE1hcxQjNx19tLJOhL619lSfkB7Kfh6kXpJ_fSMfg,19122
21
- flowtracks/scene.py,sha256=ovR1wXmgLGB9mZo62NPkcgxUzhlqiEcItLYJINAl2zk,19622
22
- flowtracks/sequence.py,sha256=u_HOev5YydpgoknPQ1lB2WUG5ZZKSu_u9ySNGUu9D38,12984
23
- flowtracks/smoothing.py,sha256=wXlvi3RBN6iZgaQRAVOxprhsjLMlxc4JIcuQMpRCQvs,7376
24
- flowtracks/stitching.py,sha256=HFsIRdAfqyhmES1OwIFslyWTF9U7kPJ5OKtjBl4YWqA,16895
25
- flowtracks/trajectory.py,sha256=uL4-byZ2tycdrAwGoET4AfLHuKaWRnc8VHLO5zv9MOg,11831
26
- flowtracks/vortex.py,sha256=4lG3QkvvnWhAHy4tl1pJrBGFLdrlP4pxNdhCZI1VZ4Q,2529
27
- flowtracks/wall_shear.py,sha256=k9Q5FEchIO9J_onXPOw4PNJQfmsrSyVVvX4RdW-rQlE,6788
28
- flowtracks/writers.py,sha256=0LnRiuqmyhcw8Ju3v9vCOERymNmxHG7cXKy6r8uLOTo,10358
29
- flowtracks/zarr_scene.py,sha256=PKCtiTT1t0Hj74Q572ra5DM3uM8lQK1A6DzzfUxvIzI,10468
30
- flowtracks-1.6.2.dev6.data/data/flowtracks-examples/batch_Lagrangian_to_Eulerian.py,sha256=e7mQFqyqvCeAHRKiN1IaxdNJi0e2FU84c7QNrx0L-m0,2296
31
- flowtracks-1.6.2.dev6.data/data/flowtracks-examples/hdf5_scene_analysis.ipynb,sha256=jn0Z_ZVL1-iXaiKXwyAeb74NbXsBpnocCxCSDh814eU,243097
32
- flowtracks-1.6.2.dev6.data/data/flowtracks-examples/linking_trajectories.ipynb,sha256=udz0GLDiPv68BYySg4yO1HWzaB9Sdy3YA3IhnIHDeII,8820
33
- flowtracks-1.6.2.dev6.data/data/flowtracks-examples/marimo_dual_rig_trajectories.py,sha256=dzeK0C-knwq1bPOwo0p1RhWrb0RPp-T31rBaGEptXcc,6737
34
- flowtracks-1.6.2.dev6.data/data/flowtracks-examples/marimo_flowtracks.py,sha256=7FIJNwBx9aBRmaM5ofmYTXSWEjE0wZqDfs8J47Tmw6U,9440
35
- flowtracks-1.6.2.dev6.data/data/flowtracks-examples/marimo_hdf5_vs_zarr_scene_analysis.py,sha256=bjXlMsLJTxJ0JkvGcVZbuDOFYZU4OvdabtDUgqb15So,4536
36
- flowtracks-1.6.2.dev6.data/data/flowtracks-examples/marimo_linking_trajectories_zarr.py,sha256=Ny_WUbiZWo0aKw-u2KGFWrhMv-8BvNgBy0UHIqCobYg,8210
37
- flowtracks-1.6.2.dev6.data/data/flowtracks-examples/marimo_phase_averaging_demo.py,sha256=vg6j2ayKGl-5uj08bdzzb6pujzJJQ_5bUsG7UuPP7dM,9270
38
- flowtracks-1.6.2.dev6.data/data/flowtracks-examples/marimo_phase_metrics.py,sha256=kBc2e_b-orDrm3AnuUXCcxsvmfQWyCtE826pFeolMGE,9367
39
- flowtracks-1.6.2.dev6.data/data/flowtracks-examples/marimo_vtk_writers_preview.py,sha256=Otrhhp-QFf6F94EOs6rWu5xOCpbS16EfWy_iMFp7e_I,14611
40
- flowtracks-1.6.2.dev6.data/data/flowtracks-examples/marimo_zarr_dashboard.py,sha256=jVIum6hdEelH4ef7vRgsfLhrbmMsuErsJBzxrnEImnw,9268
41
- flowtracks-1.6.2.dev6.data/data/flowtracks-examples/repeated_interpolation.ipynb,sha256=t9SfoIWRu5XP0SrphWbQy508uzyEibVz61FGjl51fjA,8861
42
- flowtracks-1.6.2.dev6.data/data/flowtracks-examples/run_pipeline.py,sha256=5fGE2tIP6XjFZ1sEYmqBujuLaXFfRL-rNnbr2p2CDfM,7205
43
- flowtracks-1.6.2.dev6.data/data/flowtracks-examples/visualize_marimo.py,sha256=jWrg5IWjY-bmSQwQjaO5qTr0t_G48CsqJYpevdQrxeY,13026
44
- flowtracks-1.6.2.dev6.data/data/flowtracks-examples/visualize_results.py,sha256=SbPg3EoDcuIyo0rUz3E-AnT4sLWDVJFhgC6xVcracW4,7583
45
- flowtracks-1.6.2.dev6.data/scripts/analyse_fhdf.py,sha256=QH08NdImGukTEGuZEKFxjStcSelXBv-8A-22G6KtXqM,1315
46
- flowtracks-1.6.2.dev6.dist-info/licenses/LICENSE.txt,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
47
- flowtracks-1.6.2.dev6.dist-info/licenses/LICENSES/BSD-3-Clause.txt,sha256=XiJR9O5snDB6sAz0bcncOU6msceZgjPRxejWEQGg40Q,1505
48
- flowtracks-1.6.2.dev6.dist-info/licenses/LICENSES/GPL-3.0-only.txt,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
49
- flowtracks-1.6.2.dev6.dist-info/METADATA,sha256=jdbbG4k4qcw6lRlEaa1CvQrpWv21S7wDBffoIcyH3Cs,4773
50
- flowtracks-1.6.2.dev6.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
51
- flowtracks-1.6.2.dev6.dist-info/entry_points.txt,sha256=U6028E7yQZQiZoXWMl7w4xgZUPwPxAGiAuuMB__c-yQ,60
52
- flowtracks-1.6.2.dev6.dist-info/top_level.txt,sha256=cR54RNb9y2Wfr_BfuOezmNJOMB4GfIHh1-luXTREnQA,11
53
- flowtracks-1.6.2.dev6.dist-info/RECORD,,