reflspeckit 0.1.4__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Zach
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.4
2
+ Name: reflspeckit
3
+ Version: 0.1.4
4
+ Summary: Utility package for working with spectral data cubes.
5
+ License: LICENSE
6
+ License-File: LICENSE
7
+ Keywords: hyperspectral,processing,spectral cube,image
8
+ Author: Z.M. Vig
9
+ Author-email: zvig@umd.edu
10
+ Requires-Python: >=3.13
11
+ Classifier: License :: Other/Proprietary License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Requires-Dist: astropy (>=7.1.0,<8.0.0)
16
+ Requires-Dist: numpy (>=2.2,<3.0)
17
+ Project-URL: Homepage, https://github.com/z-vig/spectralcubekit.git
18
+ Project-URL: Repository, https://github.com/z-vig/spectralcubekit.git
19
+ Description-Content-Type: text/markdown
20
+
21
+ # 🛰️ **spectralcubekit**
22
+
23
+ > ⚙️ *A modern toolkit for working with any and all flavors of spectral data cubes.*
24
+
25
+ ---
26
+
27
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/spectralcubekit)](https://pypi.org/project/spectralcubekit/)
28
+ [![PyPI - License](https://img.shields.io/pypi/l/spectralcubekit)](https://opensource.org/licenses/MIT)
29
+ [![PyPI - Version](https://img.shields.io/pypi/v/spectralcubekit)](https://pypi.org/project/spectralcubekit/)
30
+
31
+ ---
32
+
33
+ ## 🧠 What is `spectralcubekit`?
34
+
35
+ `spectralcubekit` is a lightweight, modular Python package designed to make analysis of **spectral data cubes** simple, flexible, and fun. Whether you're exploring planetary hyperspectral data, performing band analysis, or building your own spectral pipelines — this toolkit’s got you covered.
36
+
37
+ ---
38
+
39
+ ## 🧰 **Currently Available Modules**
40
+
41
+ | Module | Description |
42
+ |--------|--------------|
43
+ | 🧪 `band_parameters` | Provides parameters that describe **spectral band shapes** and sizes. |
44
+ | 📈 `linear_fitting` | Fits lines to entire spectral cubes along the **spectral domain** (z-axis). |
45
+ | 🧩 `misc_utils` | Miscellaneous utility functions for **spectral cube operations**. |
46
+
47
+ *And more modules coming soon! As a work through my Ph.D., I will add all the utility functions I write for hyperspectral data processing here!*
48
+
49
+ ---
50
+
51
+ ## 🚀 **Quick Start**
52
+
53
+ ```bash
54
+ pip install spectralcubekit
55
+ ```
56
+
57
+ ```python
58
+ import spectralcubekit as sck
59
+
60
+ fit_result = sck.fit_linear_cube(cube)
61
+ sck.save_fit(fit_result, "path/to/save.fits")
62
+ ```
63
+
64
+ ## 🔗 Links
65
+
66
+ - **GitHub**: [https://github.com/z-vig/spectralcubekit.git](https://github.com/z-vig/spectralcubekit.git)
67
+ - **Docs**: (coming soon!)
68
+
@@ -0,0 +1,47 @@
1
+ # 🛰️ **spectralcubekit**
2
+
3
+ > ⚙️ *A modern toolkit for working with any and all flavors of spectral data cubes.*
4
+
5
+ ---
6
+
7
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/spectralcubekit)](https://pypi.org/project/spectralcubekit/)
8
+ [![PyPI - License](https://img.shields.io/pypi/l/spectralcubekit)](https://opensource.org/licenses/MIT)
9
+ [![PyPI - Version](https://img.shields.io/pypi/v/spectralcubekit)](https://pypi.org/project/spectralcubekit/)
10
+
11
+ ---
12
+
13
+ ## 🧠 What is `spectralcubekit`?
14
+
15
+ `spectralcubekit` is a lightweight, modular Python package designed to make analysis of **spectral data cubes** simple, flexible, and fun. Whether you're exploring planetary hyperspectral data, performing band analysis, or building your own spectral pipelines — this toolkit’s got you covered.
16
+
17
+ ---
18
+
19
+ ## 🧰 **Currently Available Modules**
20
+
21
+ | Module | Description |
22
+ |--------|--------------|
23
+ | 🧪 `band_parameters` | Provides parameters that describe **spectral band shapes** and sizes. |
24
+ | 📈 `linear_fitting` | Fits lines to entire spectral cubes along the **spectral domain** (z-axis). |
25
+ | 🧩 `misc_utils` | Miscellaneous utility functions for **spectral cube operations**. |
26
+
27
+ *And more modules coming soon! As a work through my Ph.D., I will add all the utility functions I write for hyperspectral data processing here!*
28
+
29
+ ---
30
+
31
+ ## 🚀 **Quick Start**
32
+
33
+ ```bash
34
+ pip install spectralcubekit
35
+ ```
36
+
37
+ ```python
38
+ import spectralcubekit as sck
39
+
40
+ fit_result = sck.fit_linear_cube(cube)
41
+ sck.save_fit(fit_result, "path/to/save.fits")
42
+ ```
43
+
44
+ ## 🔗 Links
45
+
46
+ - **GitHub**: [https://github.com/z-vig/spectralcubekit.git](https://github.com/z-vig/spectralcubekit.git)
47
+ - **Docs**: (coming soon!)
@@ -0,0 +1,23 @@
1
+ [project]
2
+ name = "reflspeckit"
3
+ version = "0.1.4"
4
+ description = """Utility package for working with spectral data cubes."""
5
+ authors = [
6
+ {name = "Z.M. Vig",email = "zvig@umd.edu"}
7
+ ]
8
+ keywords = ["hyperspectral", "processing", "spectral cube", "image"]
9
+ license = {text = "LICENSE"}
10
+ readme = "README.md"
11
+ requires-python = ">=3.13"
12
+ dependencies = [
13
+ "numpy (>=2.2,<3.0)",
14
+ "astropy (>=7.1.0,<8.0.0)"
15
+ ]
16
+
17
+ [project.urls]
18
+ Homepage = "https://github.com/z-vig/spectralcubekit.git"
19
+ Repository = "https://github.com/z-vig/spectralcubekit.git"
20
+
21
+ [build-system]
22
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
23
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,16 @@
1
+ """
2
+ # spectralcubekit
3
+
4
+ A toolkit for working with any and all flavors of spectral data cubes.
5
+ """
6
+
7
+ from .misc_utils import last_nonzero_val_3D
8
+ from .linear_fitting import fit_linear_cube, fit_linear_cube_with_error
9
+ from .band_parameters import three_band_depth
10
+
11
+ __all__ = [
12
+ "last_nonzero_val_3D",
13
+ "three_band_depth",
14
+ "fit_linear_cube",
15
+ "fit_linear_cube_with_error",
16
+ ]
@@ -0,0 +1,34 @@
1
+ # Dependencies
2
+ import numpy as np
3
+
4
+
5
+ def three_band_depth(
6
+ Ra: np.ndarray,
7
+ Rb: np.ndarray,
8
+ Rc: np.ndarray,
9
+ Wa: float,
10
+ Wb: float,
11
+ Wc: float,
12
+ ):
13
+ """
14
+ Finds the band depth given two shoulder bands (Ra, Rc) and a central band
15
+ (Rb) and their corresponding wavelengths (Wa, Wb, Wc).
16
+
17
+ Parameters
18
+ ----------
19
+ Ra: np.ndarray
20
+ Reflectance at lower shoulder.
21
+ Rb: np.ndarray
22
+ Reflectance at the band center.
23
+ Rc: np.ndarray
24
+ Reflectance at upper shoulder.
25
+ Wa: float
26
+ Wavelength at lower shoulder.
27
+ Wb: float
28
+ Wavelength at the band center.
29
+ Wc: float
30
+ Reflectance at upper shoulder.
31
+
32
+ """
33
+ Rb_proj = (((Rc - Ra) / (Wc - Wa)) * (Wb - Wa)) + Ra
34
+ return Rb_proj - Rb
@@ -0,0 +1,337 @@
1
+ # Standard Libraries
2
+ from typing import Annotated, Callable
3
+ from dataclasses import dataclass
4
+ import os
5
+ from pathlib import Path
6
+
7
+ # Dependencies
8
+ import numpy as np
9
+ import numpy.typing as npt
10
+ from astropy.io import fits # type: ignore
11
+
12
+ Numpy4D = Annotated[npt.NDArray[np.float32], (4,)]
13
+ Numpy3D = Annotated[npt.NDArray[np.float32], (3,)]
14
+ Numpy2D = Annotated[npt.NDArray[np.float32], (2,)]
15
+ Numpy1D = Annotated[npt.NDArray[np.float32], (1,)]
16
+
17
+ PathLike = str | os.PathLike | Path
18
+
19
+
20
+ @dataclass
21
+ class FitResult:
22
+ """
23
+ Base class for linear fitting results.
24
+
25
+ Attributes
26
+ ----------
27
+ xdata: Numpy1D or Numpy3D
28
+ X data (wavelengths) used in the fit. 1D if uniform, 3D if variable by
29
+ pixel.
30
+ ydata: Numpy3D
31
+ Y data (reflectance) used in the fit.
32
+ model: Numpy3D
33
+ Cube of fitted lines.
34
+ beta: Numpy3D
35
+ Cube of slope and intercept.
36
+ res: Numpy3D
37
+ Residual errors on the fit.
38
+ """
39
+
40
+ xdata: Numpy1D | Numpy3D
41
+ ydata: Numpy3D
42
+ model: Numpy3D
43
+ beta: Numpy3D
44
+ res: Numpy3D
45
+
46
+ def r_squared(self) -> Numpy2D:
47
+ """
48
+ Returns the coefficient of determination (R<sup>2</sup>)
49
+ """
50
+ ss_res = np.sum(self.res**2, axis=2)
51
+ ss_tot = np.sum(
52
+ (self.ydata - np.mean(self.ydata, axis=-1)[:, :, None]) ** 2
53
+ )
54
+ return 1 - (ss_res / ss_tot)
55
+
56
+
57
+ @dataclass
58
+ class ErrorFitResult(FitResult):
59
+ """
60
+ Specialized fit result class for returning linear fits with errors.
61
+
62
+ Attributes
63
+ ----------
64
+ yerr: Callable
65
+ A function that returns the error on the y value at a given x value.
66
+ beta_err: Numpy3D
67
+ A cube of errors on the slope and intercept.
68
+ """
69
+
70
+ yerr: Callable
71
+ beta_err: Numpy3D
72
+
73
+
74
+ def save_fit(fit: FitResult, fits_file: PathLike) -> None:
75
+ """
76
+ Saves a FitResult object to a *.fits file.
77
+
78
+ Parameters
79
+ ----------
80
+ fit: FitResult
81
+ FitResult object to be saved.
82
+ fits_file: PathLike
83
+ Path to *.fits file save.
84
+ """
85
+ fits_file = Path(fits_file).with_suffix(".fits")
86
+ primary_HDU = fits.PrimaryHDU(np.transpose(fit.beta, (2, 0, 1)))
87
+ if isinstance(fit, ErrorFitResult):
88
+ param_error = fits.ImageHDU(
89
+ np.transpose(fit.beta_err, (2, 0, 1)), name="ERROR"
90
+ )
91
+ hdul = fits.HDUList([primary_HDU, param_error])
92
+ hdul.writeto(fits_file, overwrite=True)
93
+ return
94
+
95
+ hdul = fits.HDUList([primary_HDU])
96
+ hdul.writeto(fits_file, overwrite=True)
97
+
98
+
99
+ def _fit_cube_sharedX(G: Numpy2D, d: Numpy3D) -> FitResult:
100
+ """
101
+ Solves a pixel-by-pixel linear least squares regression where each pixel
102
+ has one shared design matrix.
103
+
104
+ G: 2D numpy array
105
+ A shared design matrix for all pixels of shape [NxM] where N is the
106
+ number of samples and M is the number of fit parameters.
107
+ d: 3D number array
108
+ Data cube to fit with shape [IxJxN] where I and J are pixel dimensions
109
+ and N is the number of samples.
110
+ """
111
+ prefix = np.linalg.inv(G.T @ G) @ G.T
112
+ beta = np.einsum("ij,...j->...i", prefix, d)
113
+ model = np.einsum("ij,...j->...i", G, beta)
114
+ res = model - d
115
+ return FitResult(G[:, 0], d, model, beta, res)
116
+
117
+
118
+ def _fit_cube_varX(G: Numpy4D, d: Numpy3D) -> FitResult:
119
+ """
120
+ Solves a pixel-by-pixel linear least squares regression where each pixel
121
+ has a different design matrix.
122
+
123
+ G: 4D numpy array
124
+ Pixel-by-pixel design matrices with shape [IxJxNxM] where I and J are
125
+ pixel dimensions, N is the number of samples and M is the number of
126
+ fit parameters.
127
+ d: 3D numpy array
128
+ Data cube to fit with shape [IxJxN] where I and J are pixel dimensions
129
+ and N is the number of samples.
130
+ """
131
+ GtG = np.einsum("...ij,...jk->...ik", G.transpose(0, 1, 3, 2), G)
132
+ GtG_inv = np.linalg.inv(GtG)
133
+ prefix = np.einsum("...ij,...jk->...ik", GtG_inv, G.transpose(0, 1, 3, 2))
134
+
135
+ beta = np.einsum("...ij,...j->...i", prefix, d)
136
+ model = np.einsum("...ij,...j->...i", G, beta)
137
+ res = model - d
138
+ return FitResult(G[..., 0], d, model, beta, res)
139
+
140
+
141
+ def _fit_cube_sharedX_weighted(
142
+ G: Numpy2D, d: Numpy3D, W: Numpy4D
143
+ ) -> ErrorFitResult:
144
+ """
145
+ Same as `_fit_cubed_sharedX` but allows for a 2D weights matrix.
146
+ """
147
+ GtW = np.einsum("ij,...jk->...ik", G.T, W)
148
+ GtWG = np.einsum("...ij,jk->...ik", GtW, G)
149
+ GtWG_inv = np.linalg.inv(GtWG)
150
+ prefix = np.einsum("...ij,...jk->...ik", GtWG_inv, GtW)
151
+
152
+ beta = np.einsum("...ij,...j->...i", prefix, d)
153
+ model = np.einsum("...ij,...j->...i", G, beta)
154
+ res = model - d
155
+ beta_err = np.sqrt(np.abs(np.diagonal(GtWG_inv, axis1=-2, axis2=-1)))
156
+
157
+ def yerr(x: np.ndarray) -> np.ndarray:
158
+ return (
159
+ GtWG[:, :, 1, 1][:, :, None]
160
+ + 2 * GtWG[:, :, 0, 1][:, :, None] * x[None, None, :]
161
+ + GtWG[:, :, 0, 0][:, :, None] * x[None, None, :] ** 2
162
+ )
163
+
164
+ return ErrorFitResult(G[..., 0], d, model, beta, res, yerr, beta_err)
165
+
166
+
167
+ def _fit_cube_varX_weighted(
168
+ G: Numpy4D, d: Numpy3D, W: Numpy4D
169
+ ) -> ErrorFitResult:
170
+ """
171
+ Same as `_fit_cube_varX` but allows for a 4D weights matrix.
172
+ """
173
+ GtW = np.einsum("...ij,...jk->...ik", G.transpose(0, 1, 3, 2), W)
174
+ GtWG = np.einsum("...ij,...jk->...ik", GtW, G)
175
+ GtWG_inv = np.linalg.inv(GtWG)
176
+ prefix = np.einsum("...ij,...jk->...ik", GtWG_inv, GtW)
177
+
178
+ beta = np.einsum("...ij,...j->...i", prefix, d)
179
+ model = np.einsum("...ij,...j->...i", G, beta)
180
+ res = model - d
181
+ beta_err = np.sqrt(np.abs(np.diagonal(GtWG_inv, axis1=-2, axis2=-1)))
182
+
183
+ def yerr(x: float) -> float:
184
+ return GtWG[1, 1] + 2 * GtWG[0, 1] * x + GtWG[0, 0] * x**2
185
+
186
+ return ErrorFitResult(G[..., 0], d, model, beta, res, yerr, beta_err)
187
+
188
+
189
+ def fit_linear_cube(
190
+ cube: Numpy3D, xdata: Numpy1D | Numpy3D, bandsfirst: bool = False
191
+ ) -> FitResult:
192
+ """
193
+ Fits a line to each pixel in an image cube. Each pixel can either have
194
+ unique X-data or the entire image can have one set of shared X-data for
195
+ every pixel.
196
+
197
+ Parameters
198
+ ----------
199
+ cube: 3D numpy array
200
+ Data cube to fit with shape [IxJxN] where I and J are pixel dimensions
201
+ and N is the number of samples.
202
+ xdata: 1D or 3D numpy array
203
+ Either a shared set of X-data for all pixels of shape [N] where N
204
+ is the number of samples or a cube of X-data of shape [IxJxN] where I
205
+ and J are the pixel dimensions and N is the number of samples.
206
+
207
+ Returns
208
+ -------
209
+ model: FitResult
210
+ Results of the fitting. No errors included.
211
+ """
212
+ if bandsfirst:
213
+ cube = np.transpose(cube, (1, 2, 0))
214
+ xdata = np.transpose(xdata, (1, 2, 0))
215
+
216
+ if xdata.ndim == 1:
217
+ G = np.stack([xdata, np.ones(xdata.shape)], axis=1, dtype=np.float32)
218
+ d = cube.astype(np.float32)
219
+ fitresult = _fit_cube_sharedX(G, d)
220
+ elif xdata.ndim == 3:
221
+ G = np.stack([xdata, np.ones(xdata.shape)], axis=3, dtype=np.float32)
222
+ d = cube.astype(np.float32)
223
+ fitresult = _fit_cube_varX(G, d)
224
+ else:
225
+ raise ValueError(f"{xdata.ndim} is an invalid number of dimensions.")
226
+
227
+ return fitresult
228
+
229
+
230
+ def fit_linear_cube_with_error(
231
+ cube: Numpy3D,
232
+ xdata: Numpy1D | Numpy3D,
233
+ yerr: Numpy3D,
234
+ bandsfirst: bool = False,
235
+ ) -> ErrorFitResult:
236
+ """
237
+ Same as `fit_linear_cube` but allows for the input of y errors.
238
+
239
+ Parameters
240
+ ----------
241
+ cube: 3D numpy array
242
+ Data cube to fit with shape [IxJxN] where I and J are pixel dimensions
243
+ and N is the number of samples.
244
+ xdata: 1D or 3D numpy array
245
+ Either a shared set of X-data for all pixels of shape [N] where N
246
+ is the number of samples or a cube of X-data of shape [IxJxN] where I
247
+ and J are the pixel dimensions and N is the number of samples.
248
+ yerr: 3D numpy array
249
+
250
+ Returns
251
+ -------
252
+ """
253
+ if bandsfirst:
254
+ cube = np.transpose(cube, (1, 2, 0))
255
+ yerr = np.transpose(yerr, (1, 2, 0))
256
+ if xdata.ndim == 3:
257
+ xdata = np.transpose(xdata, (1, 2, 0))
258
+
259
+ weights = np.ones(yerr.shape[:2])[:, :, None] / yerr**2
260
+ W = weights[..., None].astype(np.float32) * np.eye(
261
+ yerr.shape[2], dtype=np.float32
262
+ )
263
+ if xdata.ndim == 1:
264
+ G = np.stack([xdata, np.ones(xdata.shape)], axis=1, dtype=np.float32)
265
+ d = cube.astype(np.float32)
266
+ fitresult = _fit_cube_sharedX_weighted(G, d, W)
267
+ elif xdata.ndim == 3:
268
+ G = np.stack([xdata, np.ones(xdata.shape)], axis=3, dtype=np.float32)
269
+ d = cube.astype(np.float32)
270
+ fitresult = _fit_cube_varX_weighted(G, d, W)
271
+ else:
272
+ raise ValueError(f"{xdata.ndim} is an invalid number of dims.")
273
+
274
+ return fitresult
275
+
276
+
277
+ def fit_single_line(x: np.ndarray, y: np.ndarray):
278
+ n = x.size
279
+ sum_x = np.sum(x)
280
+ sum_y = np.sum(y)
281
+ sum_xx = np.sum(x * x)
282
+ sum_xy = np.sum(x * y)
283
+
284
+ denominator = n * sum_xx - sum_x**2
285
+ if denominator == 0:
286
+ raise ValueError("Denominator in least squares fit is zero.")
287
+
288
+ slope = (n * sum_xy - sum_x * sum_y) / denominator
289
+ intercept = (sum_y * sum_xx - sum_x * sum_xy) / denominator
290
+
291
+ model = slope * x + intercept
292
+
293
+ sum_squared_res = np.sum((y - model) ** 2)
294
+ x_variance = np.sum((x - np.mean(x)) ** 2)
295
+ sum_squared_meandiff = np.sum((y - np.mean(y)) ** 2)
296
+
297
+ r_squared = 1 - (sum_squared_res / sum_squared_meandiff)
298
+ slope_err = np.sqrt((1 / (n - 2)) * sum_squared_res / x_variance)
299
+
300
+ return slope, slope_err, intercept, r_squared
301
+
302
+
303
+ def fit_single_line_with_error(x: np.ndarray, y: np.ndarray, yerr: np.ndarray):
304
+ """
305
+ Fits a single line.
306
+
307
+ Parameters
308
+ ----------
309
+ x: np.ndarray
310
+ X Data
311
+ y: np.ndarray
312
+ Y Data
313
+ yerr: np.ndarray
314
+ Errors on Y Data
315
+
316
+ Returns
317
+ -------
318
+ xfit, yfit, error_envelope, slope, slope_err, intercept, intercept_err
319
+ """
320
+ G = np.concat([x[:, np.newaxis], np.ones([len(x), 1])], axis=1)
321
+ b = y[:, None] / yerr[:, None]
322
+ A = G / yerr[:, None]
323
+ C = np.linalg.inv(A.T @ A)
324
+ m = C @ A.T @ b
325
+
326
+ slope = m[0, 0]
327
+ slope_err = np.sqrt(C[0, 0])
328
+ intercept = m[1, 0]
329
+ intercept_err = np.sqrt(C[1, 1])
330
+
331
+ def fit(x):
332
+ return slope * x + intercept
333
+
334
+ def error_envelope(x):
335
+ return np.sqrt(C[1, 1] + 2 * C[0, 1] * x + C[0, 0] * x**2)
336
+
337
+ return fit, error_envelope, slope, slope_err, intercept, intercept_err
@@ -0,0 +1,27 @@
1
+ import numpy as np
2
+
3
+
4
+ def last_nonzero_val_3D(cube, return_index=False):
5
+ """
6
+ If you have an empty 3D image array with the first two dimensions being
7
+ pixels and the third dimension of size N, and each pixel is filled in to a
8
+ certain depth, M <= N, this function returns a 2D image array that picks
9
+ out all the pixel values at position M.
10
+ """
11
+ nx, ny, _ = cube.shape
12
+ result = np.full((nx, ny), np.nan, dtype=cube.dtype)
13
+ for i in range(nx):
14
+ for j in range(ny):
15
+ vals = cube[i, j, :]
16
+ nonzero_indices = np.nonzero(np.isfinite(vals))[0]
17
+ if return_index:
18
+ result[i, j] = (
19
+ nonzero_indices[-1] if nonzero_indices.size > 0 else np.nan
20
+ )
21
+ else:
22
+ result[i, j] = (
23
+ vals[nonzero_indices[-1]]
24
+ if nonzero_indices.size > 0
25
+ else np.nan
26
+ )
27
+ return result
File without changes