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.
- pivtools-0.1.3.dist-info/METADATA +222 -0
- pivtools-0.1.3.dist-info/RECORD +127 -0
- pivtools-0.1.3.dist-info/WHEEL +5 -0
- pivtools-0.1.3.dist-info/entry_points.txt +3 -0
- pivtools-0.1.3.dist-info/top_level.txt +3 -0
- pivtools_cli/__init__.py +5 -0
- pivtools_cli/_build_marker.c +25 -0
- pivtools_cli/_build_marker.cp311-win_amd64.pyd +0 -0
- pivtools_cli/cli.py +225 -0
- pivtools_cli/example.py +139 -0
- pivtools_cli/lib/PIV_2d_cross_correlate.c +334 -0
- pivtools_cli/lib/PIV_2d_cross_correlate.h +22 -0
- pivtools_cli/lib/common.h +36 -0
- pivtools_cli/lib/interp2custom.c +146 -0
- pivtools_cli/lib/interp2custom.h +48 -0
- pivtools_cli/lib/peak_locate_gsl.c +711 -0
- pivtools_cli/lib/peak_locate_gsl.h +40 -0
- pivtools_cli/lib/peak_locate_gsl_print.c +736 -0
- pivtools_cli/lib/peak_locate_lm.c +751 -0
- pivtools_cli/lib/peak_locate_lm.h +27 -0
- pivtools_cli/lib/xcorr.c +342 -0
- pivtools_cli/lib/xcorr.h +31 -0
- pivtools_cli/lib/xcorr_cache.c +78 -0
- pivtools_cli/lib/xcorr_cache.h +26 -0
- pivtools_cli/piv/interp2custom/interp2custom.py +69 -0
- pivtools_cli/piv/piv.py +240 -0
- pivtools_cli/piv/piv_backend/base.py +825 -0
- pivtools_cli/piv/piv_backend/cpu_instantaneous.py +1005 -0
- pivtools_cli/piv/piv_backend/factory.py +28 -0
- pivtools_cli/piv/piv_backend/gpu_instantaneous.py +15 -0
- pivtools_cli/piv/piv_backend/infilling.py +445 -0
- pivtools_cli/piv/piv_backend/outlier_detection.py +306 -0
- pivtools_cli/piv/piv_backend/profile_cpu_instantaneous.py +230 -0
- pivtools_cli/piv/piv_result.py +40 -0
- pivtools_cli/piv/save_results.py +342 -0
- pivtools_cli/piv_cluster/cluster.py +108 -0
- pivtools_cli/preprocessing/filters.py +399 -0
- pivtools_cli/preprocessing/preprocess.py +79 -0
- pivtools_cli/tests/helpers.py +107 -0
- pivtools_cli/tests/instantaneous_piv/test_piv_integration.py +167 -0
- pivtools_cli/tests/instantaneous_piv/test_piv_integration_multi.py +553 -0
- pivtools_cli/tests/preprocessing/test_filters.py +41 -0
- pivtools_core/__init__.py +5 -0
- pivtools_core/config.py +703 -0
- pivtools_core/config.yaml +135 -0
- pivtools_core/image_handling/__init__.py +0 -0
- pivtools_core/image_handling/load_images.py +464 -0
- pivtools_core/image_handling/readers/__init__.py +53 -0
- pivtools_core/image_handling/readers/generic_readers.py +50 -0
- pivtools_core/image_handling/readers/lavision_reader.py +190 -0
- pivtools_core/image_handling/readers/registry.py +24 -0
- pivtools_core/paths.py +49 -0
- pivtools_core/vector_loading.py +248 -0
- pivtools_gui/__init__.py +3 -0
- pivtools_gui/app.py +687 -0
- pivtools_gui/calibration/__init__.py +0 -0
- pivtools_gui/calibration/app/__init__.py +0 -0
- pivtools_gui/calibration/app/views.py +1186 -0
- pivtools_gui/calibration/calibration_planar/planar_calibration_production.py +570 -0
- pivtools_gui/calibration/vector_calibration_production.py +544 -0
- pivtools_gui/config.py +703 -0
- pivtools_gui/image_handling/__init__.py +0 -0
- pivtools_gui/image_handling/load_images.py +464 -0
- pivtools_gui/image_handling/readers/__init__.py +53 -0
- pivtools_gui/image_handling/readers/generic_readers.py +50 -0
- pivtools_gui/image_handling/readers/lavision_reader.py +190 -0
- pivtools_gui/image_handling/readers/registry.py +24 -0
- pivtools_gui/masking/__init__.py +0 -0
- pivtools_gui/masking/app/__init__.py +0 -0
- pivtools_gui/masking/app/views.py +123 -0
- pivtools_gui/paths.py +49 -0
- pivtools_gui/piv_runner.py +261 -0
- pivtools_gui/pivtools.py +58 -0
- pivtools_gui/plotting/__init__.py +0 -0
- pivtools_gui/plotting/app/__init__.py +0 -0
- pivtools_gui/plotting/app/views.py +1671 -0
- pivtools_gui/plotting/plot_maker.py +220 -0
- pivtools_gui/post_processing/POD/__init__.py +0 -0
- pivtools_gui/post_processing/POD/app/__init__.py +0 -0
- pivtools_gui/post_processing/POD/app/views.py +647 -0
- pivtools_gui/post_processing/POD/pod_decompose.py +979 -0
- pivtools_gui/post_processing/POD/views.py +1096 -0
- pivtools_gui/post_processing/__init__.py +0 -0
- pivtools_gui/static/404.html +1 -0
- pivtools_gui/static/_next/static/chunks/117-d5793c8e79de5511.js +2 -0
- pivtools_gui/static/_next/static/chunks/484-cfa8b9348ce4f00e.js +1 -0
- pivtools_gui/static/_next/static/chunks/869-320a6b9bdafbb6d3.js +1 -0
- pivtools_gui/static/_next/static/chunks/app/_not-found/page-12f067ceb7415e55.js +1 -0
- pivtools_gui/static/_next/static/chunks/app/layout-b907d5f31ac82e9d.js +1 -0
- pivtools_gui/static/_next/static/chunks/app/page-334cc4e8444cde2f.js +1 -0
- pivtools_gui/static/_next/static/chunks/fd9d1056-ad15f396ddf9b7e5.js +1 -0
- pivtools_gui/static/_next/static/chunks/framework-f66176bb897dc684.js +1 -0
- pivtools_gui/static/_next/static/chunks/main-a1b3ced4d5f6d998.js +1 -0
- pivtools_gui/static/_next/static/chunks/main-app-8a63c6f5e7baee11.js +1 -0
- pivtools_gui/static/_next/static/chunks/pages/_app-72b849fbd24ac258.js +1 -0
- pivtools_gui/static/_next/static/chunks/pages/_error-7ba65e1336b92748.js +1 -0
- pivtools_gui/static/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- pivtools_gui/static/_next/static/chunks/webpack-4a8ca7c99e9bb3d8.js +1 -0
- pivtools_gui/static/_next/static/css/7d3f2337d7ea12a5.css +3 -0
- pivtools_gui/static/_next/static/vQeR20OUdSSKlK4vukC4q/_buildManifest.js +1 -0
- pivtools_gui/static/_next/static/vQeR20OUdSSKlK4vukC4q/_ssgManifest.js +1 -0
- pivtools_gui/static/file.svg +1 -0
- pivtools_gui/static/globe.svg +1 -0
- pivtools_gui/static/grid.svg +8 -0
- pivtools_gui/static/index.html +1 -0
- pivtools_gui/static/index.txt +8 -0
- pivtools_gui/static/next.svg +1 -0
- pivtools_gui/static/vercel.svg +1 -0
- pivtools_gui/static/window.svg +1 -0
- pivtools_gui/stereo_reconstruction/__init__.py +0 -0
- pivtools_gui/stereo_reconstruction/app/__init__.py +0 -0
- pivtools_gui/stereo_reconstruction/app/views.py +1985 -0
- pivtools_gui/stereo_reconstruction/stereo_calibration_production.py +606 -0
- pivtools_gui/stereo_reconstruction/stereo_reconstruction_production.py +544 -0
- pivtools_gui/utils.py +63 -0
- pivtools_gui/vector_loading.py +248 -0
- pivtools_gui/vector_merging/__init__.py +1 -0
- pivtools_gui/vector_merging/app/__init__.py +1 -0
- pivtools_gui/vector_merging/app/views.py +759 -0
- pivtools_gui/vector_statistics/app/__init__.py +1 -0
- pivtools_gui/vector_statistics/app/views.py +710 -0
- pivtools_gui/vector_statistics/ensemble_statistics.py +49 -0
- pivtools_gui/vector_statistics/instantaneous_statistics.py +311 -0
- pivtools_gui/video_maker/__init__.py +0 -0
- pivtools_gui/video_maker/app/__init__.py +0 -0
- pivtools_gui/video_maker/app/views.py +436 -0
- 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
|