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.
- reflspeckit-0.1.4/LICENSE +21 -0
- reflspeckit-0.1.4/PKG-INFO +68 -0
- reflspeckit-0.1.4/README.md +47 -0
- reflspeckit-0.1.4/pyproject.toml +23 -0
- reflspeckit-0.1.4/src/reflspeckit/__init__.py +16 -0
- reflspeckit-0.1.4/src/reflspeckit/band_parameters.py +34 -0
- reflspeckit-0.1.4/src/reflspeckit/linear_fitting.py +337 -0
- reflspeckit-0.1.4/src/reflspeckit/misc_utils.py +27 -0
- reflspeckit-0.1.4/src/reflspeckit/py.typed +0 -0
|
@@ -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
|
+
[](https://pypi.org/project/spectralcubekit/)
|
|
28
|
+
[](https://opensource.org/licenses/MIT)
|
|
29
|
+
[](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
|
+
[](https://pypi.org/project/spectralcubekit/)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
[](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
|