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,825 @@
1
+ from abc import ABC, abstractmethod
2
+ import sys
3
+ from pathlib import Path
4
+ from typing import List
5
+
6
+ import cv2
7
+ import dask.array as da
8
+ import numpy as np
9
+ import scipy.sparse as sp
10
+ import scipy.sparse.linalg as spla
11
+ from scipy.interpolate import griddata
12
+ from skimage.restoration import inpaint_biharmonic
13
+
14
+ # Try to import line_profiler for detailed profiling
15
+ try:
16
+ from line_profiler import profile
17
+ except ImportError:
18
+ profile = lambda f: f
19
+
20
+
21
+ from pivtools_core.config import Config
22
+
23
+
24
+ class CrossCorrelator(ABC):
25
+
26
+ @abstractmethod
27
+ def correlate_batch(self, images: np.ndarray, config: Config, vector_masks: List[np.ndarray] = None):
28
+ pass
29
+
30
+ def _window_weight_fun(
31
+ self,
32
+ wsize: tuple[int, int],
33
+ window_type: str,
34
+ SumWindow: tuple[int, int] = None,
35
+ ) -> np.ndarray:
36
+ """
37
+ Generate a 2D cross-correlation weighting window.
38
+
39
+ Parameters
40
+ ----------
41
+ wsize : tuple[int, int]
42
+ Size of the window (rows, cols)
43
+ window_type : str
44
+ Type of window: 'blackman', 'square', 'bsingle', 'gaussian', 'gaussianTOP', 'singlepix'
45
+ SumWindow : tuple[int, int] | None
46
+ Only used for 'bsingle' and 'singlepix' types to define the outer matrix size.
47
+
48
+ Returns
49
+ -------
50
+ weight : np.ndarray
51
+ 2D array of shape wsize representing the weighting window
52
+ """
53
+ m, n = wsize
54
+ dist_to_cen_max = np.sqrt(((m + 1) / 2) ** 2 + ((n + 1) / 2) ** 2)
55
+ weight = np.zeros((m, n), dtype=np.float32)
56
+
57
+ if window_type.lower() == "blackman":
58
+ a0, a1, a2 = 0.42659, 0.49656, 0.076849
59
+ for ii in range(m):
60
+ for jj in range(n):
61
+ dist_to_cen = np.sqrt(
62
+ (ii - (m - 1) / 2) ** 2 + (jj - (n - 1) / 2) ** 2
63
+ )
64
+ theta = dist_to_cen / dist_to_cen_max * np.pi + np.pi
65
+ weight[ii, jj] = a0 - a1 * np.cos(theta) + a2 * np.cos(2 * theta)
66
+ weight[weight <= 0] = 0.01
67
+
68
+ elif window_type.lower() == "square":
69
+ weight[:] = 1.0
70
+
71
+ elif window_type.lower() == "bsingle":
72
+ if SumWindow is None:
73
+ raise ValueError("SumWindow must be provided for 'bsingle'")
74
+ weight = np.ones(SumWindow, dtype=np.float32)
75
+
76
+ elif window_type.lower() == "gaussian" or window_type.lower() == "gaussiantop":
77
+ win_x = np.arange(-m / 2 + 0.5, m / 2, dtype=np.float32)
78
+ win_y = np.arange(-n / 2 + 0.5, n / 2, dtype=np.float32)
79
+ xm, ym = np.meshgrid(win_x, win_y, indexing="ij")
80
+ alpha = 1.0
81
+ weight = np.exp(
82
+ -0.5 * ((alpha * xm / (m / 2)) ** 2 + (alpha * ym / (n / 2)) ** 2)
83
+ )
84
+
85
+ if window_type.lower() == "gaussiantop":
86
+ if m > 6 and n > 6:
87
+ # Create checkerboard mask
88
+ C_mask = np.tile(
89
+ np.array([[1, 0], [0, 0]], dtype=np.float32), (m // 2, n // 2)
90
+ )
91
+ shift_x, shift_y = int(np.ceil(m / 4)), int(np.ceil(n / 4))
92
+ C_mask = np.roll(np.roll(C_mask, shift_x, axis=0), shift_y, axis=1)
93
+ weight *= C_mask
94
+ else:
95
+ C_mask = np.zeros((m, n), dtype=np.float32)
96
+ r0 = m // 2 - 1
97
+ c0 = n // 2 - 1
98
+ C_mask[r0 : r0 + 4, c0 : c0 + 4] = 1
99
+ weight *= C_mask
100
+
101
+ elif window_type.lower() == "singlepix":
102
+ if SumWindow is None:
103
+ raise ValueError("SumWindow must be provided for 'singlepix'")
104
+ weight = np.zeros(SumWindow, dtype=np.float32)
105
+ start_row = int(np.ceil((SumWindow[0] - m) / 2))
106
+ end_row = start_row + m
107
+ start_col = int(np.ceil((SumWindow[1] - n) / 2))
108
+ end_col = start_col + n
109
+ weight[start_row:end_row, start_col:end_col] = 1.0
110
+
111
+ else:
112
+ raise ValueError(f"Unrecognized window type '{window_type}'")
113
+
114
+ return weight
115
+
116
+ @profile
117
+ def _inpaint_nans_biharm(self, A):
118
+ mask = np.isnan(A)
119
+ A_filled = np.copy(A)
120
+
121
+ y, x = np.indices(A.shape)
122
+ known_points = np.array([y[~mask], x[~mask]]).T
123
+ known_values = A[~mask]
124
+ nan_points = np.array([y[mask], x[mask]]).T
125
+
126
+ A_filled[mask] = griddata(
127
+ known_points, known_values, nan_points, method="nearest"
128
+ )
129
+
130
+ return inpaint_biharmonic(A_filled, mask)
131
+ @profile
132
+ def _inpaint_nans_griddata(self, A):
133
+ """
134
+ Fast NaN inpainting using scipy.interpolate.griddata.
135
+
136
+ This uses a two-pass approach:
137
+ 1. 'linear' for smooth, fast interpolation.
138
+ 2. 'nearest' to fill any remaining NaNs (e.g., extrapolation).
139
+ """
140
+ mask = np.isnan(A)
141
+
142
+ # If there are no NaNs, return immediately
143
+ if not mask.any():
144
+ return A
145
+
146
+ y, x = np.indices(A.shape)
147
+ known_points = np.array([y[~mask], x[~mask]]).T
148
+ known_values = A[~mask]
149
+ nan_points = np.array([y[mask], x[mask]]).T
150
+
151
+ # If there are no known points, we can't interpolate.
152
+ # Return the array as-is (or return np.zeros_like(A))
153
+ if known_points.size == 0:
154
+ return A
155
+
156
+ # Pass 1: Linear interpolation
157
+ interp_values = griddata(
158
+ known_points, known_values, nan_points, method="linear"
159
+ )
160
+
161
+ # Pass 2: Find where linear failed (still NaN) and fill with nearest
162
+ nan_mask_pass1 = np.isnan(interp_values)
163
+ if nan_mask_pass1.any():
164
+ fallback_values = griddata(
165
+ known_points, known_values, nan_points[nan_mask_pass1], method="nearest"
166
+ )
167
+ interp_values[nan_mask_pass1] = fallback_values
168
+
169
+ # Fill the original array
170
+ A_filled = np.copy(A)
171
+ A_filled[mask] = interp_values
172
+
173
+ return A_filled
174
+
175
+ def _inpaint_nans_matlab(self, A):
176
+
177
+ A = np.array(A, dtype=float)
178
+ n, m = A.shape
179
+ nm = n * m
180
+
181
+ A_flat = A.ravel()
182
+ nan_mask = np.isnan(A_flat)
183
+ nan_list = np.where(nan_mask)[0]
184
+ known_list = np.where(~nan_mask)[0]
185
+
186
+ if nan_list.size == 0:
187
+ return A.copy()
188
+
189
+ rows, cols, vals = [], [], []
190
+
191
+ def add_entries(center, offsets, weights):
192
+ for off, w in zip(offsets, weights):
193
+ r = center
194
+ c = center + off
195
+ if 0 <= c < nm:
196
+ rows.append(r)
197
+ cols.append(c)
198
+ vals.append(w)
199
+
200
+ for idx in nan_list:
201
+
202
+ r = idx % n
203
+ c = idx // n
204
+
205
+ if 2 <= r < n - 2 and 2 <= c < m - 2:
206
+ offsets = [
207
+ -2 * n,
208
+ -n - 1,
209
+ -n,
210
+ -n + 1,
211
+ -2,
212
+ -1,
213
+ 0,
214
+ 1,
215
+ 2,
216
+ n - 1,
217
+ n,
218
+ n + 1,
219
+ 2 * n,
220
+ ]
221
+ weights = [1, 2, -8, 2, 1, -8, 20, -8, 1, 2, -8, 2, 1]
222
+ add_entries(idx, offsets, weights)
223
+
224
+ else:
225
+
226
+ offsets = [-n, 0, n]
227
+ weights = [1, -2, 1]
228
+ add_entries(idx, offsets, weights)
229
+ offsets = [-1, 0, 1]
230
+ weights = [1, -2, 1]
231
+ add_entries(idx, offsets, weights)
232
+
233
+ fda = sp.csr_matrix((vals, (rows, cols)), shape=(nm, nm))
234
+
235
+ rhs = -fda[:, known_list] @ A_flat[known_list]
236
+
237
+ k = nan_list
238
+ fda_sub = fda[k[:, None], k].tocsc()
239
+ rhs_sub = rhs[k]
240
+
241
+ x = spla.spsolve(fda_sub, rhs_sub)
242
+
243
+ B_flat = A_flat.copy()
244
+ B_flat[k] = x
245
+ return B_flat.reshape((n, m))
246
+
247
+ def _inpaint_nans_opencv(self, A):
248
+ """
249
+ Inpaint NaNs using OpenCV.
250
+ """
251
+ mask = np.isnan(A).astype(np.uint8)
252
+ A[np.isnan(A)] = 0.0
253
+
254
+ A_inpainted = cv2.inpaint(A, mask, inpaintRadius=5, flags=cv2.INPAINT_TELEA)
255
+
256
+ return A_inpainted
257
+
258
+ def _inpaint_nans(self, A, method=3):
259
+ """
260
+ Replicates John D’Errico’s inpaint_nans MATLAB function in Python.
261
+
262
+ Parameters
263
+ ----------
264
+ A : 2D array_like
265
+ Input array with NaNs to be filled.
266
+ method : int, optional
267
+ Which PDE/finite-difference scheme to use (0 to 5). Default is 0.
268
+
269
+ Returns
270
+ -------
271
+ B : 2D ndarray
272
+ Array with NaNs replaced/inpainted.
273
+ """
274
+ # Convert input A to a float array in Fortran (column-major) order
275
+ A = np.array(A, dtype=float, order="F")
276
+ n, m = A.shape
277
+ nm = n * m
278
+
279
+ # Flatten (column-major) so linear indexing matches MATLAB's
280
+ A_flat = A.ravel(order="F")
281
+
282
+ # Find NaNs
283
+ isnan = np.isnan(A_flat)
284
+ nan_list = np.where(isnan)[0] # these are 0-based linear indices
285
+ known_list = np.where(~isnan)[0]
286
+ nan_count = len(nan_list)
287
+ if nan_count == 0:
288
+ # No NaNs, nothing to do
289
+ return A # already a copy
290
+
291
+ # Convert those nan indices to row/col, 1-based analog to MATLAB
292
+ # In MATLAB: [nr, nc] = ind2sub([n,m], nan_list)
293
+ # We store them as 1-based to match the original boundary logic,
294
+ # then keep them in a combined array: [lin_idx, row, col]
295
+ # where row,col in [1..n], [1..m].
296
+ nr = (nan_list % n) + 1
297
+ nc = (nan_list // n) + 1
298
+ nan_list_info = np.column_stack([nan_list, nr, nc])
299
+
300
+ # --- Helper: build a function to do the solve or least-squares:
301
+ def sparse_solve(M, rhs):
302
+ """
303
+ Solve M x = rhs.
304
+ Uses direct solve if M is square and has full rank,
305
+ otherwise use lsqr for least-squares.
306
+ """
307
+ # M should be shape (R, C). If R==C, try spsolve:
308
+ R, C = M.shape
309
+ if R == C:
310
+ return spla.spsolve(M.tocsc(), rhs)
311
+ else:
312
+ # Use lsqr for least squares
313
+ sol = spla.lsqr(M, rhs, atol=1e-12, btol=1e-12, iter_lim=10_000)
314
+ return sol[0]
315
+
316
+ # We’ll need a function to eliminate known values from RHS, similarly to MATLAB
317
+ def eliminate_knowns(fda, known_idx, A_known):
318
+ """
319
+ Return the adjusted rhs after applying -fda[:, known_idx] * A_known,
320
+ i.e. the part that remains for the unknown columns.
321
+ """
322
+ rhs = -fda[:, known_idx].dot(A_known)
323
+ return rhs
324
+
325
+ # Switch on 'method' just like the original code
326
+ if method not in [0, 1, 2, 3, 4, 5]:
327
+ raise ValueError("method must be one of {0,1,2,3,4,5}.")
328
+
329
+ # We will build 'fda' (the finite-difference operator) as a sparse matrix
330
+ # in each method, then solve or least-squares for the unknowns.
331
+ B = A_flat.copy()
332
+
333
+ # ----- Subfunction from the original code, in Python:
334
+ def identify_neighbors(n, m, nan_list_3col, talks_to):
335
+ """
336
+ Identify neighbors of the NaN pixels, not including the NaNs themselves.
337
+
338
+ nan_list_3col: array of shape (N,3): [lin_index, row, col]
339
+ with row,col in 1-based coords.
340
+ talks_to: array of shape (p,2) of row/col offsets
341
+ """
342
+ if nan_list_3col.shape[0] == 0:
343
+ return np.empty((0, 3), dtype=int)
344
+
345
+ nan_count_local = nan_list_3col.shape[0]
346
+ talk_count = talks_to.shape[0]
347
+
348
+ # For each offset in talks_to, add to row,col
349
+ # shape => (nan_count_local * talk_count, 2)
350
+ repeated = np.repeat(
351
+ nan_list_3col[:, 1:3], talk_count, axis=0
352
+ ) # row,col repeated
353
+ offsets = np.tile(talks_to, (nan_count_local, 1))
354
+ nn = repeated + offsets # neighbor row,col (1-based)
355
+
356
+ # Filter out-of-bounds neighbors
357
+ in_bounds = (
358
+ (nn[:, 0] >= 1) & (nn[:, 0] <= n) & (nn[:, 1] >= 1) & (nn[:, 1] <= m)
359
+ )
360
+ nn = nn[in_bounds]
361
+
362
+ # Convert (row,col) back to linear index in 0-based
363
+ # MATLAB 1-based: lin = row + (col-1)*n
364
+ # Python 0-based: lin = (row-1) + (col-1)*n
365
+ # so if row,col is 1-based, we do:
366
+ lin_idx = (nn[:, 0] - 1) + (nn[:, 1] - 1) * n
367
+
368
+ neighbors_list = np.column_stack([lin_idx, nn])
369
+
370
+ # Unique rows and remove those that are in the NaN list
371
+ neighbors_list = np.unique(neighbors_list, axis=0)
372
+
373
+ # Build a set of all nan linear indices for easy filter
374
+ nan_lin_set = set(nan_list_3col[:, 0].tolist())
375
+
376
+ # Keep only those not themselves in the NaN set
377
+ mask_not_nan = np.array(
378
+ [(x[0] not in nan_lin_set) for x in neighbors_list], dtype=bool
379
+ )
380
+ neighbors_list = neighbors_list[mask_not_nan]
381
+ return neighbors_list
382
+
383
+ # Because the methods differ widely, we implement them in turn:
384
+ if method == 0 or method == 3:
385
+ # Methods 0 and 3 are similar to method 1 but build the matrix only
386
+ # around the nans and their neighbors, then do a del^2 or del^4 solve.
387
+
388
+ # If method=0 => del^2, if method=3 => del^4
389
+ if method == 0:
390
+ # del^2 using neighbors: up/down/left/right
391
+ # We only build around the nans + their immediate neighbors
392
+ # for 2D or 1D. If 1D, treat specially.
393
+
394
+ if n == 1 or m == 1:
395
+ # 1D case
396
+ # Identify the "work_list" as nan +/- 1
397
+ work_list = np.concatenate([nan_list, nan_list - 1, nan_list + 1])
398
+ work_list = work_list[(work_list >= 0) & (work_list < nm)]
399
+ work_list = np.unique(work_list)
400
+
401
+ # Build fda
402
+ # For each i in work_list, we want i-1, i, i+1 with [1, -2, 1]
403
+ # We'll do that in a lil_matrix
404
+ fda = sp.lil_matrix((len(work_list), nm), dtype=float)
405
+ # Fill row-by-row
406
+ for row_idx, i in enumerate(work_list):
407
+ # center
408
+ fda[row_idx, i] = -2.0
409
+ if i - 1 >= 0:
410
+ fda[row_idx, i - 1] = 1.0
411
+ if i + 1 < nm:
412
+ fda[row_idx, i + 1] = 1.0
413
+
414
+ # Eliminate knowns
415
+ rhs = eliminate_knowns(fda, known_list, A_flat[known_list])
416
+
417
+ # We only solve for columns in nan_list
418
+ unknown_idx = nan_list
419
+ # We only keep rows that reference those columns
420
+ # i.e. any row with a non-zero in unknown columns
421
+ mask_rows = fda[:, unknown_idx].sum(axis=1).A.ravel() != 0
422
+ row_sel = np.where(mask_rows)[0]
423
+
424
+ fda_sub = fda[row_sel, :][:, unknown_idx]
425
+ rhs_sub = rhs[row_sel]
426
+
427
+ sol = sparse_solve(fda_sub, rhs_sub)
428
+
429
+ # Place solution
430
+ B[unknown_idx] = sol
431
+
432
+ else:
433
+ # 2D case
434
+ # Horizontal and vertical neighbors only
435
+ talks_to = np.array([[-1, 0], [1, 0], [0, -1], [0, 1]])
436
+ neighbors_list = identify_neighbors(n, m, nan_list_info, talks_to)
437
+ all_list = np.vstack([nan_list_info, neighbors_list])
438
+
439
+ # Build fda
440
+ fda = sp.lil_matrix((nm, nm), dtype=float)
441
+
442
+ # second partials row-wise: (row > 1 & row < n)
443
+ L = np.where((all_list[:, 1] > 1) & (all_list[:, 1] < n))[0]
444
+ for i in L:
445
+ idx = all_list[i, 0]
446
+ fda[idx, idx] += -2.0
447
+ fda[idx, idx - 1] += 1.0
448
+ fda[idx, idx + 1] += 1.0
449
+
450
+ # second partials col-wise: (col > 1 & col < m)
451
+ L = np.where((all_list[:, 2] > 1) & (all_list[:, 2] < m))[0]
452
+ for i in L:
453
+ idx = all_list[i, 0]
454
+ fda[idx, idx] += -2.0
455
+ fda[idx, idx - n] += 1.0
456
+ fda[idx, idx + n] += 1.0
457
+
458
+ # Eliminate knowns
459
+ rhs = eliminate_knowns(fda, known_list, A_flat[known_list])
460
+
461
+ # Solve only for relevant rows & columns
462
+ unknown_idx = nan_list
463
+ mask_rows = fda[:, unknown_idx].sum(axis=1).A.ravel() != 0
464
+ row_sel = np.where(mask_rows)[0]
465
+
466
+ fda_sub = fda[row_sel, :][:, unknown_idx]
467
+ rhs_sub = rhs[row_sel]
468
+ sol = sparse_solve(fda_sub, rhs_sub)
469
+
470
+ B[unknown_idx] = sol
471
+
472
+ else:
473
+ # method == 3 => "better plate" using del^4
474
+ # We use bigger stencils
475
+ # The code is quite extensive, we replicate the logic:
476
+
477
+ # neighbors for the center region
478
+ talks_to = np.array(
479
+ [
480
+ [-2, 0],
481
+ [-1, -1],
482
+ [-1, 0],
483
+ [-1, 1],
484
+ [0, -2],
485
+ [0, -1],
486
+ [0, 1],
487
+ [0, 2],
488
+ [1, -1],
489
+ [1, 0],
490
+ [1, 1],
491
+ [2, 0],
492
+ ]
493
+ )
494
+ neighbors_list = identify_neighbors(n, m, nan_list_info, talks_to)
495
+ all_list = np.vstack([nan_list_info, neighbors_list])
496
+
497
+ fda = sp.lil_matrix((nm, nm), dtype=float)
498
+
499
+ # main interior: row>=3 & row<=n-2 & col>=3 & col<=m-2
500
+ L = np.where(
501
+ (all_list[:, 1] >= 3)
502
+ & (all_list[:, 1] <= n - 2)
503
+ & (all_list[:, 2] >= 3)
504
+ & (all_list[:, 2] <= m - 2)
505
+ )[0]
506
+ # fill with the big 13-point stencil
507
+ # Coeffs: [1 2 -8 2 1 -8 20 -8 1 2 -8 2 1],
508
+ # Offsets in linear indices: [-2n, -(n+1), -n, -(n-1), -2, -1, 0, +1, +2, (n-1), +n, (n+1), +2n]
509
+ base_offsets = np.array(
510
+ [
511
+ -2 * n,
512
+ -n - 1,
513
+ -n,
514
+ -n + 1,
515
+ -2,
516
+ -1,
517
+ 0,
518
+ 1,
519
+ 2,
520
+ n - 1,
521
+ n,
522
+ n + 1,
523
+ 2 * n,
524
+ ]
525
+ )
526
+ base_coeffs = np.array(
527
+ [1, 2, -8, 2, 1, -8, 20, -8, 1, 2, -8, 2, 1], dtype=float
528
+ )
529
+ for i in L:
530
+ idx = all_list[i, 0]
531
+ for off, coef in zip(base_offsets, base_coeffs):
532
+ fda[idx, idx + off] += coef
533
+
534
+ # boundaries near row=2 or row=n-1 or col=2 or col=m-1
535
+ # do a simpler 5-point Laplacian: [1 -4 1], etc.
536
+ # the original code lumps all boundary expansions. For brevity, replicate:
537
+
538
+ # row=2 or row=n-1 or col=2 or col=m-1
539
+ L = np.where(
540
+ (
541
+ ((all_list[:, 1] == 2) | (all_list[:, 1] == n - 1))
542
+ & (all_list[:, 2] >= 2)
543
+ & (all_list[:, 2] <= m - 1)
544
+ )
545
+ | (
546
+ ((all_list[:, 2] == 2) | (all_list[:, 2] == m - 1))
547
+ & (all_list[:, 1] >= 2)
548
+ & (all_list[:, 1] <= n - 1)
549
+ )
550
+ )[0]
551
+ # 5-point: offsets = [-n, -1, 0, +1, +n], coeff = [1,1,-4,1,1]
552
+ offsets_5 = np.array([-n, -1, 0, 1, n])
553
+ coeffs_5 = np.array([1, 1, -4, 1, 1], dtype=float)
554
+ for i in L:
555
+ idx = all_list[i, 0]
556
+ for off, c in zip(offsets_5, coeffs_5):
557
+ fda[idx, idx + off] += c
558
+
559
+ # row=1 or row=n, col in [2..m-1]
560
+ L = np.where(
561
+ ((all_list[:, 1] == 1) | (all_list[:, 1] == n))
562
+ & (all_list[:, 2] >= 2)
563
+ & (all_list[:, 2] <= m - 1)
564
+ )[0]
565
+ # 3-point vertical second derivative: offsets = [-n, 0, +n], coeffs = [1, -2, 1]
566
+ offsets_3v = np.array([-n, 0, n])
567
+ coeffs_3v = np.array([1, -2, 1], dtype=float)
568
+ for i in L:
569
+ idx = all_list[i, 0]
570
+ for off, c in zip(offsets_3v, coeffs_3v):
571
+ fda[idx, idx + off] += c
572
+
573
+ # col=1 or col=m, row in [2..n-1]
574
+ L = np.where(
575
+ ((all_list[:, 2] == 1) | (all_list[:, 2] == m))
576
+ & (all_list[:, 1] >= 2)
577
+ & (all_list[:, 1] <= n - 1)
578
+ )[0]
579
+ # 3-point horizontal second derivative: offsets = [-1, 0, +1], coeffs = [1, -2, 1]
580
+ offsets_3h = np.array([-1, 0, 1])
581
+ coeffs_3h = np.array([1, -2, 1], dtype=float)
582
+ for i in L:
583
+ idx = all_list[i, 0]
584
+ for off, c in zip(offsets_3h, coeffs_3h):
585
+ fda[idx, idx + off] += c
586
+
587
+ # Eliminate knowns
588
+ rhs = eliminate_knowns(fda, known_list, A_flat[known_list])
589
+
590
+ # Solve
591
+ unknown_idx = nan_list
592
+ mask_rows = fda[:, unknown_idx].sum(axis=1).A.ravel() != 0
593
+ row_sel = np.where(mask_rows)[0]
594
+ fda_sub = fda[row_sel, :][:, unknown_idx]
595
+ rhs_sub = rhs[row_sel]
596
+ sol = sparse_solve(fda_sub, rhs_sub)
597
+
598
+ B[unknown_idx] = sol
599
+
600
+ elif method == 1:
601
+ # Least squares with del^2 on the entire array
602
+ # Build the Laplacian operator for all points
603
+ if n == 1 or m == 1:
604
+ # 1D
605
+ # second difference for interior points
606
+ # row i => i=1..(nm-2) in 0-based => fill [i, i+1, i+2]
607
+ # but we'll do it more systematically:
608
+ fda = sp.lil_matrix((nm - 2, nm), dtype=float)
609
+ for i in range(nm - 2):
610
+ fda[i, i] = 1.0
611
+ fda[i, i + 1] = -2.0
612
+ fda[i, i + 2] = 1.0
613
+
614
+ else:
615
+ # 2D
616
+ fda = sp.lil_matrix((nm, nm), dtype=float)
617
+ # Row-second-derivatives for i=2..n-1 => index = i+(j-1)*n
618
+ # We'll just loop or systematically fill them:
619
+ for j in range(m):
620
+ for i in range(1, n - 1):
621
+ idx = i + j * n
622
+ # i => row, j => col in 0-based, so fda[idx, idx +/- 1]
623
+ fda[idx, idx] += -2.0
624
+ fda[idx, idx - 1] += 1.0
625
+ fda[idx, idx + 1] += 1.0
626
+
627
+ # Column-second-derivatives for j=2..m-1 => index i+(j-1)*n
628
+ for j in range(1, m - 1):
629
+ for i in range(n):
630
+ idx = i + j * n
631
+ fda[idx, idx] += -2.0
632
+ fda[idx, idx - n] += 1.0
633
+ fda[idx, idx + n] += 1.0
634
+
635
+ # Eliminate knowns
636
+ rhs = eliminate_knowns(fda, known_list, A_flat[known_list])
637
+
638
+ # Solve
639
+ unknown_idx = nan_list
640
+ mask_rows = fda[:, unknown_idx].sum(axis=1).A.ravel() != 0
641
+ row_sel = np.where(mask_rows)[0]
642
+ fda_sub = fda[row_sel, :][:, unknown_idx]
643
+ rhs_sub = rhs[row_sel]
644
+ sol = sparse_solve(fda_sub, rhs_sub)
645
+
646
+ B[unknown_idx] = sol
647
+
648
+ elif method == 2:
649
+ # Direct solve for del^2 BVP across holes only
650
+ if n == 1 or m == 1:
651
+ raise ValueError(
652
+ "Method 2 has problems for 1D input. Use another method."
653
+ )
654
+ else:
655
+ # 2D
656
+ fda = sp.lil_matrix((nm, nm), dtype=float)
657
+
658
+ # second partials on row index
659
+ L = np.where((nan_list_info[:, 1] > 1) & (nan_list_info[:, 1] < n))[0]
660
+ for i in L:
661
+ idx = nan_list_info[i, 0]
662
+ fda[idx, idx] += -2.0
663
+ fda[idx, idx - 1] += 1.0
664
+ fda[idx, idx + 1] += 1.0
665
+
666
+ # second partials on column index
667
+ L = np.where((nan_list_info[:, 2] > 1) & (nan_list_info[:, 2] < m))[0]
668
+ for i in L:
669
+ idx = nan_list_info[i, 0]
670
+ fda[idx, idx] += -2.0
671
+ fda[idx, idx - n] += 1.0
672
+ fda[idx, idx + n] += 1.0
673
+
674
+ # fix boundary corners if they are NaN
675
+ corners = [0, n - 1, nm - n, nm - 1] # 0-based corners in Fortran?
676
+ # Actually, in column-major: top-left = 0, bottom-left = n-1,
677
+ # top-right = (m-1)*n, bottom-right = nm-1
678
+ # The original code forces certain patterns if those corners are in nan_list
679
+ for c in corners:
680
+ if c in nan_list:
681
+ # replicate the code: fda(c, [c c+...]) = ...
682
+ # corner examples from the original
683
+ if c == 0:
684
+ fda[c, c] = -2.0
685
+ fda[c, c + 1] += 1.0
686
+ fda[c, c + n] += 1.0
687
+ elif c == n - 1:
688
+ fda[c, c] = -2.0
689
+ fda[c, c - 1] += 1.0
690
+ fda[c, c + n] += 1.0
691
+ elif c == (m - 1) * n:
692
+ fda[c, c] = -2.0
693
+ fda[c, c + 1] += 1.0
694
+ fda[c, c - n] += 1.0
695
+ elif c == nm - 1:
696
+ fda[c, c] = -2.0
697
+ fda[c, c - 1] += 1.0
698
+ fda[c, c - n] += 1.0
699
+
700
+ # Eliminate knowns
701
+ rhs = eliminate_knowns(fda, known_list, A_flat[known_list])
702
+
703
+ # Solve directly on the nan_list subset
704
+ unknown_idx = nan_list
705
+ fda_sub = fda[unknown_idx, :][:, unknown_idx]
706
+ rhs_sub = rhs[unknown_idx]
707
+ sol = sparse_solve(fda_sub, rhs_sub)
708
+
709
+ B[unknown_idx] = sol
710
+
711
+ elif method == 4:
712
+ # Spring analogy, only horizontal + vertical neighbors
713
+ # Diagonals in the original code are not used or are they? Actually,
714
+ # code has "hv_list=[-1 -1 0; 1 1 0; -n 0 -1; n 0 1]", but that’s
715
+ # for HV. (No diagonal in the original code method 4.)
716
+
717
+ # We'll build a matrix of "springs"
718
+ hv_list = np.array([[-1, 0], [1, 0], [0, -1], [0, 1]]) # row/col offsets
719
+ # But in the original code, it actually used:
720
+ # hv_list = [ -1 -1 0
721
+ # 1 1 0
722
+ # -n 0 -1
723
+ # n 0 1 ]
724
+ # That was building pairs of (index, index+...). It's simpler to replicate logic:
725
+
726
+ # Let’s explicitly gather pairs of neighbors among the nan_list:
727
+ springs = []
728
+ for i in range(nan_count):
729
+ idx = nan_list_info[i, 0]
730
+ row_i = nan_list_info[i, 1]
731
+ col_i = nan_list_info[i, 2]
732
+ # up/down/left/right neighbors
733
+ # up => if row_i>1 => idx-n
734
+ if row_i > 1:
735
+ springs.append([idx, idx - 1]) # in col-major, up is -1
736
+ if row_i < n:
737
+ springs.append([idx, idx + 1]) # down is +1
738
+ if col_i > 1:
739
+ springs.append([idx, idx - n]) # left is -n
740
+ if col_i < m:
741
+ springs.append([idx, idx + n]) # right is +n
742
+
743
+ # Unique + sort each pair
744
+ springs = np.array(springs)
745
+ # Sort rows so that [min,max]
746
+ springs.sort(axis=1)
747
+ # Unique
748
+ springs = np.unique(springs, axis=0)
749
+
750
+ # Build the sparse system: each spring => row in the system, [1 -1] for those two columns
751
+ n_springs = springs.shape[0]
752
+ S = sp.lil_matrix((n_springs, nm), dtype=float)
753
+ for i in range(n_springs):
754
+ i1, i2 = springs[i]
755
+ S[i, i1] = 1.0
756
+ S[i, i2] = -1.0
757
+
758
+ # Right side after eliminating known
759
+ rhs = -S[:, known_list].dot(B[known_list])
760
+
761
+ # Solve only for nan columns
762
+ unknown_idx = nan_list
763
+ S_sub = S[:, unknown_idx]
764
+ sol = sparse_solve(S_sub, rhs)
765
+
766
+ B[unknown_idx] = sol
767
+
768
+ elif method == 5:
769
+ # Average of 8 nearest neighbors
770
+ # The code builds an operator that enforces B(i) = average(8 neighbors).
771
+ # That translates to sum_of_neighbors - 8*B(i) = 0, or equivalently
772
+ # for each i in nan_list, sum_{neighbors} (B(neighbor)) - 8 B(i) = 0.
773
+
774
+ fda = sp.lil_matrix((nm, nm), dtype=float)
775
+
776
+ def add_avg_equation(center_idx, neighbor_idx):
777
+ # for eq: B(neighbor_idx) - B(center_idx)
778
+ fda[center_idx, neighbor_idx] += 1.0
779
+ fda[center_idx, center_idx] += -1.0
780
+
781
+ # We replicate the "if" blocks from MATLAB for the 8 neighbors
782
+ # We only do it for i in nan_list
783
+ for i in nan_list:
784
+ # row,col in 1-based
785
+ r = (i % n) + 1
786
+ c = (i // n) + 1
787
+ # top-left => if r>1,c>1 => i-(n+1)
788
+ if (r > 1) and (c > 1):
789
+ add_avg_equation(i, i - n - 1)
790
+ # top => if c>1 => i-n
791
+ if c > 1:
792
+ add_avg_equation(i, i - n)
793
+ # top-right => if r<n,c>1 => i-(n-1)
794
+ if (r < n) and (c > 1):
795
+ add_avg_equation(i, i - n + 1)
796
+ # left => if r>1 => i-1
797
+ if r > 1:
798
+ add_avg_equation(i, i - 1)
799
+ # right => if r<n => i+1
800
+ if r < n:
801
+ add_avg_equation(i, i + 1)
802
+ # bottom-left => if r>1,c<m => i+(n-1)
803
+ if (r > 1) and (c < m):
804
+ add_avg_equation(i, i + n - 1)
805
+ # bottom => if c<m => i+n
806
+ if c < m:
807
+ add_avg_equation(i, i + n)
808
+ # bottom-right => if r<n,c<m => i+(n+1)
809
+ if (r < n) and (c < m):
810
+ add_avg_equation(i, i + n + 1)
811
+
812
+ # Eliminate known
813
+ rhs = eliminate_knowns(fda, known_list, B[known_list])
814
+
815
+ # Solve for unknown
816
+ unknown_idx = nan_list
817
+ fda_sub = fda[unknown_idx, :][:, unknown_idx]
818
+ rhs_sub = rhs[unknown_idx]
819
+ sol = sparse_solve(fda_sub, rhs_sub)
820
+
821
+ B[unknown_idx] = sol
822
+
823
+ # Reshape back to (n,m) in Fortran order
824
+ B = np.reshape(B, (n, m), order="F")
825
+ return B