pivtools 0.1.3__cp311-cp311-win_amd64.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 (127) hide show
  1. pivtools-0.1.3.dist-info/METADATA +222 -0
  2. pivtools-0.1.3.dist-info/RECORD +127 -0
  3. pivtools-0.1.3.dist-info/WHEEL +5 -0
  4. pivtools-0.1.3.dist-info/entry_points.txt +3 -0
  5. pivtools-0.1.3.dist-info/top_level.txt +3 -0
  6. pivtools_cli/__init__.py +5 -0
  7. pivtools_cli/_build_marker.c +25 -0
  8. pivtools_cli/_build_marker.cp311-win_amd64.pyd +0 -0
  9. pivtools_cli/cli.py +225 -0
  10. pivtools_cli/example.py +139 -0
  11. pivtools_cli/lib/PIV_2d_cross_correlate.c +334 -0
  12. pivtools_cli/lib/PIV_2d_cross_correlate.h +22 -0
  13. pivtools_cli/lib/common.h +36 -0
  14. pivtools_cli/lib/interp2custom.c +146 -0
  15. pivtools_cli/lib/interp2custom.h +48 -0
  16. pivtools_cli/lib/peak_locate_gsl.c +711 -0
  17. pivtools_cli/lib/peak_locate_gsl.h +40 -0
  18. pivtools_cli/lib/peak_locate_gsl_print.c +736 -0
  19. pivtools_cli/lib/peak_locate_lm.c +751 -0
  20. pivtools_cli/lib/peak_locate_lm.h +27 -0
  21. pivtools_cli/lib/xcorr.c +342 -0
  22. pivtools_cli/lib/xcorr.h +31 -0
  23. pivtools_cli/lib/xcorr_cache.c +78 -0
  24. pivtools_cli/lib/xcorr_cache.h +26 -0
  25. pivtools_cli/piv/interp2custom/interp2custom.py +69 -0
  26. pivtools_cli/piv/piv.py +240 -0
  27. pivtools_cli/piv/piv_backend/base.py +825 -0
  28. pivtools_cli/piv/piv_backend/cpu_instantaneous.py +1005 -0
  29. pivtools_cli/piv/piv_backend/factory.py +28 -0
  30. pivtools_cli/piv/piv_backend/gpu_instantaneous.py +15 -0
  31. pivtools_cli/piv/piv_backend/infilling.py +445 -0
  32. pivtools_cli/piv/piv_backend/outlier_detection.py +306 -0
  33. pivtools_cli/piv/piv_backend/profile_cpu_instantaneous.py +230 -0
  34. pivtools_cli/piv/piv_result.py +40 -0
  35. pivtools_cli/piv/save_results.py +342 -0
  36. pivtools_cli/piv_cluster/cluster.py +108 -0
  37. pivtools_cli/preprocessing/filters.py +399 -0
  38. pivtools_cli/preprocessing/preprocess.py +79 -0
  39. pivtools_cli/tests/helpers.py +107 -0
  40. pivtools_cli/tests/instantaneous_piv/test_piv_integration.py +167 -0
  41. pivtools_cli/tests/instantaneous_piv/test_piv_integration_multi.py +553 -0
  42. pivtools_cli/tests/preprocessing/test_filters.py +41 -0
  43. pivtools_core/__init__.py +5 -0
  44. pivtools_core/config.py +703 -0
  45. pivtools_core/config.yaml +135 -0
  46. pivtools_core/image_handling/__init__.py +0 -0
  47. pivtools_core/image_handling/load_images.py +464 -0
  48. pivtools_core/image_handling/readers/__init__.py +53 -0
  49. pivtools_core/image_handling/readers/generic_readers.py +50 -0
  50. pivtools_core/image_handling/readers/lavision_reader.py +190 -0
  51. pivtools_core/image_handling/readers/registry.py +24 -0
  52. pivtools_core/paths.py +49 -0
  53. pivtools_core/vector_loading.py +248 -0
  54. pivtools_gui/__init__.py +3 -0
  55. pivtools_gui/app.py +687 -0
  56. pivtools_gui/calibration/__init__.py +0 -0
  57. pivtools_gui/calibration/app/__init__.py +0 -0
  58. pivtools_gui/calibration/app/views.py +1186 -0
  59. pivtools_gui/calibration/calibration_planar/planar_calibration_production.py +570 -0
  60. pivtools_gui/calibration/vector_calibration_production.py +544 -0
  61. pivtools_gui/config.py +703 -0
  62. pivtools_gui/image_handling/__init__.py +0 -0
  63. pivtools_gui/image_handling/load_images.py +464 -0
  64. pivtools_gui/image_handling/readers/__init__.py +53 -0
  65. pivtools_gui/image_handling/readers/generic_readers.py +50 -0
  66. pivtools_gui/image_handling/readers/lavision_reader.py +190 -0
  67. pivtools_gui/image_handling/readers/registry.py +24 -0
  68. pivtools_gui/masking/__init__.py +0 -0
  69. pivtools_gui/masking/app/__init__.py +0 -0
  70. pivtools_gui/masking/app/views.py +123 -0
  71. pivtools_gui/paths.py +49 -0
  72. pivtools_gui/piv_runner.py +261 -0
  73. pivtools_gui/pivtools.py +58 -0
  74. pivtools_gui/plotting/__init__.py +0 -0
  75. pivtools_gui/plotting/app/__init__.py +0 -0
  76. pivtools_gui/plotting/app/views.py +1671 -0
  77. pivtools_gui/plotting/plot_maker.py +220 -0
  78. pivtools_gui/post_processing/POD/__init__.py +0 -0
  79. pivtools_gui/post_processing/POD/app/__init__.py +0 -0
  80. pivtools_gui/post_processing/POD/app/views.py +647 -0
  81. pivtools_gui/post_processing/POD/pod_decompose.py +979 -0
  82. pivtools_gui/post_processing/POD/views.py +1096 -0
  83. pivtools_gui/post_processing/__init__.py +0 -0
  84. pivtools_gui/static/404.html +1 -0
  85. pivtools_gui/static/_next/static/chunks/117-d5793c8e79de5511.js +2 -0
  86. pivtools_gui/static/_next/static/chunks/484-cfa8b9348ce4f00e.js +1 -0
  87. pivtools_gui/static/_next/static/chunks/869-320a6b9bdafbb6d3.js +1 -0
  88. pivtools_gui/static/_next/static/chunks/app/_not-found/page-12f067ceb7415e55.js +1 -0
  89. pivtools_gui/static/_next/static/chunks/app/layout-b907d5f31ac82e9d.js +1 -0
  90. pivtools_gui/static/_next/static/chunks/app/page-334cc4e8444cde2f.js +1 -0
  91. pivtools_gui/static/_next/static/chunks/fd9d1056-ad15f396ddf9b7e5.js +1 -0
  92. pivtools_gui/static/_next/static/chunks/framework-f66176bb897dc684.js +1 -0
  93. pivtools_gui/static/_next/static/chunks/main-a1b3ced4d5f6d998.js +1 -0
  94. pivtools_gui/static/_next/static/chunks/main-app-8a63c6f5e7baee11.js +1 -0
  95. pivtools_gui/static/_next/static/chunks/pages/_app-72b849fbd24ac258.js +1 -0
  96. pivtools_gui/static/_next/static/chunks/pages/_error-7ba65e1336b92748.js +1 -0
  97. pivtools_gui/static/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  98. pivtools_gui/static/_next/static/chunks/webpack-4a8ca7c99e9bb3d8.js +1 -0
  99. pivtools_gui/static/_next/static/css/7d3f2337d7ea12a5.css +3 -0
  100. pivtools_gui/static/_next/static/vQeR20OUdSSKlK4vukC4q/_buildManifest.js +1 -0
  101. pivtools_gui/static/_next/static/vQeR20OUdSSKlK4vukC4q/_ssgManifest.js +1 -0
  102. pivtools_gui/static/file.svg +1 -0
  103. pivtools_gui/static/globe.svg +1 -0
  104. pivtools_gui/static/grid.svg +8 -0
  105. pivtools_gui/static/index.html +1 -0
  106. pivtools_gui/static/index.txt +8 -0
  107. pivtools_gui/static/next.svg +1 -0
  108. pivtools_gui/static/vercel.svg +1 -0
  109. pivtools_gui/static/window.svg +1 -0
  110. pivtools_gui/stereo_reconstruction/__init__.py +0 -0
  111. pivtools_gui/stereo_reconstruction/app/__init__.py +0 -0
  112. pivtools_gui/stereo_reconstruction/app/views.py +1985 -0
  113. pivtools_gui/stereo_reconstruction/stereo_calibration_production.py +606 -0
  114. pivtools_gui/stereo_reconstruction/stereo_reconstruction_production.py +544 -0
  115. pivtools_gui/utils.py +63 -0
  116. pivtools_gui/vector_loading.py +248 -0
  117. pivtools_gui/vector_merging/__init__.py +1 -0
  118. pivtools_gui/vector_merging/app/__init__.py +1 -0
  119. pivtools_gui/vector_merging/app/views.py +759 -0
  120. pivtools_gui/vector_statistics/app/__init__.py +1 -0
  121. pivtools_gui/vector_statistics/app/views.py +710 -0
  122. pivtools_gui/vector_statistics/ensemble_statistics.py +49 -0
  123. pivtools_gui/vector_statistics/instantaneous_statistics.py +311 -0
  124. pivtools_gui/video_maker/__init__.py +0 -0
  125. pivtools_gui/video_maker/app/__init__.py +0 -0
  126. pivtools_gui/video_maker/app/views.py +436 -0
  127. pivtools_gui/video_maker/video_maker.py +662 -0
@@ -0,0 +1,49 @@
1
+ import numpy as np
2
+
3
+ from ..config import Config
4
+ from ..paths import get_data_paths
5
+ from ..vector_loading import load_vectors_from_directory
6
+
7
+
8
+ def ensemble_statistics(cam_num: int, config: Config, base):
9
+ """
10
+ Placeholder ensemble stats: compute global mean (ux, uy) across all frames (same as instantaneous for now).
11
+ Extend later with RMS, turbulence quantities, etc.
12
+ """
13
+ if config.statistics_extraction is None:
14
+ return
15
+ for entry in config.statistics_extraction:
16
+ if entry.get("type") != "ensemble":
17
+ continue
18
+ endpoint = entry.get("endpoint", "")
19
+ use_merged = entry.get("use_merged", False)
20
+ if use_merged and cam_num != config.camera_numbers[0]:
21
+ continue
22
+ cam_folder_eff = "Merged" if use_merged else f"Cam{cam_num}"
23
+ paths = get_data_paths(
24
+ base_dir=base,
25
+ num_images=config.num_images,
26
+ cam_folder=cam_folder_eff,
27
+ type="ensemble",
28
+ endpoint=endpoint,
29
+ use_merged=use_merged,
30
+ )
31
+ if not paths["data_dir"].exists():
32
+ print(f"[ensemble] Data dir missing: {paths['data_dir']}")
33
+ continue
34
+ paths["stats_dir"].mkdir(parents=True, exist_ok=True)
35
+ arr = load_vectors_from_directory(paths["data_dir"], config)
36
+ ux = arr[:, 0]
37
+ uy = arr[:, 1]
38
+ mean_ux = ux.mean(axis=0).compute()
39
+ mean_uy = uy.mean(axis=0).compute()
40
+ out_file = paths["stats_dir"] / (
41
+ f"{'merged' if use_merged else f'Cam{cam_num}'}_ensemble_mean.npz"
42
+ )
43
+ np.savez_compressed(
44
+ out_file,
45
+ mean_ux=mean_ux,
46
+ mean_uy=mean_uy,
47
+ meta=dict(endpoint=endpoint, use_merged=use_merged, camera=cam_folder_eff),
48
+ )
49
+ print(f"[ensemble] Saved ensemble mean -> {out_file}")
@@ -0,0 +1,311 @@
1
+ import dask # added
2
+ import matplotlib.pyplot as plt
3
+ import numpy as np
4
+ from scipy.io import savemat
5
+
6
+ from ..config import Config
7
+ from ..paths import get_data_paths
8
+ from ..plotting.plot_maker import make_scalar_settings, plot_scalar_field
9
+ from ..vector_loading import (
10
+ load_coords_from_directory,
11
+ load_vectors_from_directory,
12
+ )
13
+
14
+
15
+ def instantaneous_statistics(cam_num: int, config: Config, base):
16
+ """
17
+ Compute mean ux, uy for each instantaneous statistics_extraction entry for a given camera.
18
+ Saves npz in stats_dir.
19
+ Skips duplicated merged runs (only first camera processed for merged).
20
+ """
21
+ print(f"[instantaneous] Starting statistics for cam_num={cam_num}")
22
+ if config.statistics_extraction is None:
23
+ return
24
+ for entry in config.statistics_extraction:
25
+ print(f"[instantaneous] Processing entry: {entry}")
26
+ if entry.get("type") != "instantaneous":
27
+ print("[instantaneous] Skipping entry (not instantaneous type)")
28
+ continue
29
+ endpoint = entry.get("endpoint", "")
30
+ use_merged = entry.get("use_merged", False)
31
+ # Avoid repeating merged computation per camera
32
+ if use_merged and cam_num != config.camera_numbers[0]:
33
+ print(
34
+ f"[instantaneous] Skipping merged for cam_num={cam_num} (only first camera processes merged)"
35
+ )
36
+ continue
37
+ cam_folder_eff = "Merged" if use_merged else f"Cam{cam_num}"
38
+ print(f"[instantaneous] cam_folder_eff: {cam_folder_eff}")
39
+ paths = get_data_paths(
40
+ base_dir=base,
41
+ num_images=config.num_images,
42
+ cam_folder=cam_folder_eff,
43
+ type_name="instantaneous", # renamed from type
44
+ endpoint=endpoint,
45
+ use_merged=use_merged,
46
+ )
47
+ print(f"[instantaneous] Data dir: {paths['data_dir']}")
48
+ if not paths["data_dir"].exists():
49
+ print(f"[instantaneous] Data dir missing: {paths['data_dir']}")
50
+ continue
51
+ paths["stats_dir"].mkdir(parents=True, exist_ok=True)
52
+ # Create nested mean_stats directory
53
+ mean_stats_dir = paths["stats_dir"] / "mean_stats"
54
+ mean_stats_dir.mkdir(parents=True, exist_ok=True)
55
+ print(f"[instantaneous] Loading vectors from {paths['data_dir']}")
56
+ # Load all requested passes at once; config.instantaneous_runs is 1-based
57
+ selected_runs_1based = (
58
+ list(config.instantaneous_runs) if config.instantaneous_runs else []
59
+ )
60
+ print(f"[instantaneous] Selected runs/passes (1-based): {selected_runs_1based}")
61
+ arr = load_vectors_from_directory(
62
+ paths["data_dir"], config, runs=selected_runs_1based
63
+ ) # (N,R,3,H,W) or (N,R,4,H,W) if stereo
64
+ # co-rodinates need to be loaded
65
+ print(f"[instantaneous] Loaded array shape: {arr.shape}")
66
+
67
+ # Load coordinates for the same selected runs
68
+ coords_x_list, coords_y_list = load_coords_from_directory(
69
+ paths["data_dir"], runs=selected_runs_1based
70
+ )
71
+
72
+ # Check for stereo (assume config.stereo is a boolean flag)
73
+ stereo = getattr(config, "stereo", False)
74
+ if stereo:
75
+ if arr.shape[2] < 4:
76
+ print("[instantaneous] Stereo enabled but array has fewer than 4 components; skipping uz stresses")
77
+ stereo = False
78
+ else:
79
+ print("[instantaneous] Stereo enabled; computing uz stresses")
80
+
81
+ # Components: 0=ux, 1=uy, 2=uz (if stereo), 3=b_mask (if stereo) or 2=b_mask
82
+ ux = arr[:, :, 0] # (N,R,H,W)
83
+ uy = arr[:, :, 1] # (N,R,H,W)
84
+ if stereo:
85
+ uz = arr[:, :, 2] # (N,R,H,W)
86
+ bmask = arr[:, :, 3] # (N,R,H,W)
87
+ else:
88
+ bmask = arr[:, :, 2] # (N,R,H,W)
89
+
90
+ print("[instantaneous] Computing mean ux and uy per selected pass")
91
+ # Build lazy reductions
92
+ mean_ux_da = ux.mean(axis=0) # (R,H,W)
93
+ mean_uy_da = uy.mean(axis=0) # (R,H,W)
94
+
95
+ # b_masks are identical across time -> take first time instance lazily
96
+ print("[instantaneous] Using b_mask from first time instance per selected pass")
97
+ b_mask_da = bmask[0] # (R,H,W)
98
+
99
+ # Compute Reynolds stress ingredients lazily
100
+ print("[instantaneous] Computing Reynolds stresses per selected pass")
101
+ E_ux2_da = (ux**2).mean(axis=0) # (R,H,W)
102
+ E_uy2_da = (uy**2).mean(axis=0) # (R,H,W)
103
+ E_uxuy_da = (ux * uy).mean(axis=0) # (R,H,W)
104
+ if stereo:
105
+ mean_uz_da = uz.mean(axis=0) # (R,H,W)
106
+ E_uz2_da = (uz**2).mean(axis=0) # (R,H,W)
107
+ E_uxuz_da = (ux * uz).mean(axis=0) # (R,H,W)
108
+ E_uyuz_da = (uy * uz).mean(axis=0) # (R,H,W)
109
+
110
+ # Execute all reductions in one graph run
111
+ if stereo:
112
+ mean_ux_all, mean_uy_all, mean_uz_all, b_mask_all, E_ux2, E_uy2, E_uxuy, E_uz2, E_uxuz, E_uyuz = dask.compute(
113
+ mean_ux_da, mean_uy_da, mean_uz_da, b_mask_da, E_ux2_da, E_uy2_da, E_uxuy_da, E_uz2_da, E_uxuz_da, E_uyuz_da
114
+ )
115
+ else:
116
+ mean_ux_all, mean_uy_all, b_mask_all, E_ux2, E_uy2, E_uxuy = dask.compute(
117
+ mean_ux_da, mean_uy_da, b_mask_da, E_ux2_da, E_uy2_da, E_uxuy_da
118
+ )
119
+
120
+ # Finish Reynolds stresses on NumPy arrays
121
+ uu_all = E_ux2 - mean_ux_all**2 # (R,H,W)
122
+ uv_all = E_uxuy - (mean_ux_all * mean_uy_all)
123
+ vv_all = E_uy2 - mean_uy_all**2
124
+ if stereo:
125
+ uw_all = E_uxuz - (mean_ux_all * mean_uz_all)
126
+ vw_all = E_uyuz - (mean_uy_all * mean_uz_all)
127
+ ww_all = E_uz2 - mean_uz_all**2
128
+
129
+ # Determine labels (1-based) for selected passes
130
+ if selected_runs_1based:
131
+ pass_labels = selected_runs_1based
132
+ else:
133
+ # No passes specified: assume all passes present in files
134
+ R = mean_ux_all.shape[0]
135
+ pass_labels = list(range(1, R + 1))
136
+
137
+ # Plot mean scalar fields for each selected pass (ux and uy)
138
+ print("[instantaneous] Generating mean scalar plots for ux and uy")
139
+ for lbl in pass_labels:
140
+ idx = lbl - 1 # aligns with array indexing when all passes selected
141
+ # If a subset was selected, map label to local index
142
+ if selected_runs_1based:
143
+ local_idx = selected_runs_1based.index(lbl)
144
+ else:
145
+ local_idx = idx
146
+ # Build boolean mask
147
+ mask_bool = np.asarray(b_mask_all[local_idx]).astype(bool)
148
+
149
+ # Per-pass coordinates if available
150
+ cx = coords_x_list[local_idx] if local_idx < len(coords_x_list) else None
151
+ cy = coords_y_list[local_idx] if local_idx < len(coords_y_list) else None
152
+
153
+ # ux
154
+ save_base_ux = mean_stats_dir / f"ux_{lbl}"
155
+ settings_ux = make_scalar_settings(
156
+ config,
157
+ variable="ux",
158
+ run_label=lbl,
159
+ save_basepath=save_base_ux, # used only for naming below
160
+ variable_units="m/s",
161
+ coords_x=cx,
162
+ coords_y=cy,
163
+ )
164
+ fig_ux, _, _ = plot_scalar_field(
165
+ mean_ux_all[local_idx], mask_bool, settings_ux
166
+ )
167
+ fig_ux.savefig(
168
+ f"{save_base_ux}{config.plot_save_extension}",
169
+ dpi=1200,
170
+ bbox_inches="tight",
171
+ )
172
+ if config.plot_save_pickle:
173
+ import pickle
174
+
175
+ with open(f"{save_base_ux}.pkl", "wb") as f:
176
+ pickle.dump(mean_ux_all[local_idx], f)
177
+ plt.close(fig_ux)
178
+
179
+ # uy
180
+ save_base_uy = mean_stats_dir / f"uy_{lbl}"
181
+ settings_uy = make_scalar_settings(
182
+ config,
183
+ variable="uy",
184
+ run_label=lbl,
185
+ save_basepath=save_base_uy, # used only for naming below
186
+ variable_units="m/s",
187
+ coords_x=cx,
188
+ coords_y=cy,
189
+ )
190
+ fig_uy, _, _ = plot_scalar_field(
191
+ mean_uy_all[local_idx], mask_bool, settings_uy
192
+ )
193
+ fig_uy.savefig(
194
+ f"{save_base_uy}{config.plot_save_extension}",
195
+ dpi=1200,
196
+ bbox_inches="tight",
197
+ )
198
+ if config.plot_save_pickle:
199
+ import pickle
200
+
201
+ with open(f"{save_base_uy}.pkl", "wb") as f:
202
+ pickle.dump(mean_uy_all[local_idx], f)
203
+ plt.close(fig_uy)
204
+
205
+ # Build piv_result as n-pass-deep MATLAB struct array; populate only selected passes
206
+ n_passes_cfg = len(config.instantaneous_window_sizes) or mean_ux_all.shape[0]
207
+ print(f"[instantaneous] Building piv_result with n_passes={n_passes_cfg}")
208
+ # Create a structured array with object-typed fields so each element can hold arrays
209
+ dt_fields = [
210
+ ("ux", object),
211
+ ("uy", object),
212
+ ("b_mask", object),
213
+ ("uu", object),
214
+ ("uv", object),
215
+ ("vv", object),
216
+ ]
217
+ if stereo:
218
+ dt_fields.extend(
219
+ [
220
+ ("uz", object),
221
+ ("uw", object),
222
+ ("vw", object),
223
+ ("ww", object),
224
+ ]
225
+ )
226
+ dt = np.dtype(dt_fields)
227
+ piv_result = np.empty((n_passes_cfg,), dtype=dt)
228
+
229
+ # Initialize all passes with empty 0x0 arrays
230
+ empty = np.empty((0, 0), dtype=mean_ux_all.dtype)
231
+ for p in range(n_passes_cfg):
232
+ piv_result["ux"][p] = empty
233
+ piv_result["uy"][p] = empty
234
+ piv_result["b_mask"][p] = empty
235
+ piv_result["uu"][p] = empty
236
+ piv_result["uv"][p] = empty
237
+ piv_result["vv"][p] = empty
238
+ if stereo:
239
+ piv_result["uz"][p] = empty
240
+ piv_result["uw"][p] = empty
241
+ piv_result["vw"][p] = empty
242
+ piv_result["ww"][p] = empty
243
+
244
+ # Fill only the selected passes
245
+ label_to_idx = {
246
+ lbl: i for i, lbl in enumerate(pass_labels)
247
+ } # 1-based label -> local index (selected order)
248
+ for lbl in pass_labels:
249
+ local_idx = label_to_idx[lbl]
250
+ pass_zero_based = lbl - 1
251
+ if 0 <= pass_zero_based < n_passes_cfg:
252
+ piv_result["ux"][pass_zero_based] = mean_ux_all[local_idx]
253
+ piv_result["uy"][pass_zero_based] = mean_uy_all[local_idx]
254
+ piv_result["b_mask"][pass_zero_based] = b_mask_all[local_idx]
255
+ piv_result["uu"][pass_zero_based] = uu_all[local_idx]
256
+ piv_result["uv"][pass_zero_based] = uv_all[local_idx]
257
+ piv_result["vv"][pass_zero_based] = vv_all[local_idx]
258
+ if stereo:
259
+ piv_result["uz"][pass_zero_based] = mean_uz_all[local_idx]
260
+ piv_result["uw"][pass_zero_based] = uw_all[local_idx]
261
+ piv_result["vw"][pass_zero_based] = vw_all[local_idx]
262
+ piv_result["ww"][pass_zero_based] = ww_all[local_idx]
263
+
264
+ # Build coordinates struct array (fields: x, y), aligned to n_passes_cfg; fill only selected passes
265
+ dt_coords = np.dtype([("x", object), ("y", object)])
266
+ coordinates = np.empty((n_passes_cfg,), dtype=dt_coords)
267
+ # Initialize empties
268
+ empty_xy = np.empty((0, 0), dtype=empty.dtype)
269
+ for p in range(n_passes_cfg):
270
+ coordinates["x"][p] = empty_xy
271
+ coordinates["y"][p] = empty_xy
272
+ # Fill selected using the same label order
273
+ for lbl in pass_labels:
274
+ local_idx = label_to_idx[lbl]
275
+ pass_zero_based = lbl - 1
276
+ if 0 <= pass_zero_based < n_passes_cfg and local_idx < len(coords_x_list):
277
+ coordinates["x"][pass_zero_based] = coords_x_list[local_idx]
278
+ coordinates["y"][pass_zero_based] = coords_y_list[local_idx]
279
+
280
+ # Save a single file per camera/merged with piv_result, coordinates and meta
281
+ out_file = mean_stats_dir / (
282
+ f"{'merged' if use_merged else f'Cam{cam_num}'}_mean.mat"
283
+ )
284
+ print(
285
+ f"[instantaneous] Saving piv_result (means and Reynolds stresses) -> {out_file}"
286
+ )
287
+ out_file.parent.mkdir(parents=True, exist_ok=True)
288
+ # Save piv_result and meta to main file
289
+ meta_dict = {
290
+ "endpoint": endpoint,
291
+ "use_merged": use_merged,
292
+ "camera": cam_folder_eff,
293
+ "selected_passes": pass_labels,
294
+ "n_passes": int(n_passes_cfg),
295
+ "stereo": stereo,
296
+ "definitions": "ux=<u>, uy=<v>, uu=<u'^2>, uv=<u'v'>, vv=<v'^2>"
297
+ + (", uz=<w>, uw=<u'w'>, vw=<v'w'>, ww=<w'^2>" if stereo else ""),
298
+ }
299
+ savemat(
300
+ out_file,
301
+ {
302
+ "piv_result": piv_result,
303
+ "meta": meta_dict,
304
+ },
305
+ )
306
+ # Save coordinates as a separate file into mean_stats folder
307
+ coords_file = mean_stats_dir / (
308
+ f"{'merged' if use_merged else f'Cam{cam_num}'}_coordinates.mat"
309
+ )
310
+ savemat(coords_file, {"coordinates": coordinates})
311
+ print(f"[instantaneous] Saved -> {out_file}")
File without changes
File without changes