lvm-tools 0.0.2__py3-none-any.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.
- lvm_tools/__init__.py +10 -0
- lvm_tools/_version.py +21 -0
- lvm_tools/config/__init__.py +1 -0
- lvm_tools/config/data_config.py +206 -0
- lvm_tools/config/validation.py +58 -0
- lvm_tools/data/__init__.py +1 -0
- lvm_tools/data/coordinates.py +73 -0
- lvm_tools/data/helper.py +56 -0
- lvm_tools/data/tile.py +168 -0
- lvm_tools/fit_data/__init__.py +1 -0
- lvm_tools/fit_data/builder.py +51 -0
- lvm_tools/fit_data/clipping.py +37 -0
- lvm_tools/fit_data/filtering.py +129 -0
- lvm_tools/fit_data/fit_data.py +140 -0
- lvm_tools/fit_data/normalisation.py +48 -0
- lvm_tools/fit_data/processing.py +79 -0
- lvm_tools/physical_properties/__init__.py +1 -0
- lvm_tools/physical_properties/barycentric_corr.py +11 -0
- lvm_tools/py.typed +0 -0
- lvm_tools/utils/__init__.py +1 -0
- lvm_tools/utils/mask.py +40 -0
- lvm_tools-0.0.2.dist-info/METADATA +42 -0
- lvm_tools-0.0.2.dist-info/RECORD +25 -0
- lvm_tools-0.0.2.dist-info/WHEEL +4 -0
- lvm_tools-0.0.2.dist-info/licenses/LICENSE +21 -0
lvm_tools/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from lvm_tools.config.data_config import DataConfig
|
|
2
|
+
from lvm_tools.data.tile import LVMTile, LVMTileCollection
|
|
3
|
+
from lvm_tools.fit_data.builder import FitDataBuilder
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"LVMTile",
|
|
7
|
+
"LVMTileCollection",
|
|
8
|
+
"DataConfig",
|
|
9
|
+
"FitDataBuilder",
|
|
10
|
+
]
|
lvm_tools/_version.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
|
|
5
|
+
|
|
6
|
+
TYPE_CHECKING = False
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from typing import Tuple
|
|
9
|
+
from typing import Union
|
|
10
|
+
|
|
11
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
12
|
+
else:
|
|
13
|
+
VERSION_TUPLE = object
|
|
14
|
+
|
|
15
|
+
version: str
|
|
16
|
+
__version__: str
|
|
17
|
+
__version_tuple__: VERSION_TUPLE
|
|
18
|
+
version_tuple: VERSION_TUPLE
|
|
19
|
+
|
|
20
|
+
__version__ = version = '0.0.2'
|
|
21
|
+
__version_tuple__ = version_tuple = (0, 0, 2)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""config - subpackage for configurations used in data processing before fitting."""
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""data_config.py - Objects for specifying configuration of data processing before fitting."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict, dataclass
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from lvm_tools.config.validation import (
|
|
10
|
+
validate_apply_mask,
|
|
11
|
+
validate_excl_strategy,
|
|
12
|
+
validate_fib_status_incl,
|
|
13
|
+
validate_norm_strategy,
|
|
14
|
+
validate_offset,
|
|
15
|
+
validate_range,
|
|
16
|
+
validate_scale,
|
|
17
|
+
)
|
|
18
|
+
from lvm_tools.data.tile import LVMTileLike
|
|
19
|
+
from lvm_tools.fit_data.filtering import (
|
|
20
|
+
BAD_FLUX_THRESHOLD,
|
|
21
|
+
ExcludeStrategy,
|
|
22
|
+
FibreStatus,
|
|
23
|
+
)
|
|
24
|
+
from lvm_tools.fit_data.normalisation import NormaliseStrategy
|
|
25
|
+
from lvm_tools.fit_data.processing import get_normalisations, get_αδ_ranges, process_tile_data
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class DataConfig:
|
|
30
|
+
"""
|
|
31
|
+
Configuration object for data processing before fitting.
|
|
32
|
+
|
|
33
|
+
args:
|
|
34
|
+
λ_range: tuple[float, float] - Wavelength range to include.
|
|
35
|
+
α_range: tuple[float, float] - Right Ascension range to include.
|
|
36
|
+
δ_range: tuple[float, float] - Declination range to include.
|
|
37
|
+
nans_strategy: ExcludeStrategy - Strategy for handling NaN values.
|
|
38
|
+
F_bad_strategy: ExcludeStrategy - Strategy for handling bad flux values. For "pixel", the flux range is applied to each pixel. For "spaxel", the flux range is applied to the median of all pixels in a spaxel.
|
|
39
|
+
F_range: tuple[float, float] - Flux range to include.
|
|
40
|
+
fibre_status_include: tuple[FibreStatus] - Fibre status values to include.
|
|
41
|
+
apply_mask: bool - Whether to apply a mask to the data.
|
|
42
|
+
normalise_F_strategy: NormaliseStrategy - Strategy for normalising flux data.
|
|
43
|
+
normalise_F_offset: float - Offset for normalising flux data.
|
|
44
|
+
normalise_F_scale: float - Scale for normalising flux data.
|
|
45
|
+
normalise_αδ_strategy: NormaliseStrategy - Strategy for normalising α and δ data.
|
|
46
|
+
normalise_αδ_offset: float - Offset for normalising α and δ data.
|
|
47
|
+
normalise_αδ_scale: float - Scale for normalising α and δ data.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
# Data clipping ranges (aka choose data of interest)
|
|
51
|
+
λ_range: tuple[float, float] = (-np.inf, np.inf)
|
|
52
|
+
α_range: tuple[float, float] = (-np.inf, np.inf)
|
|
53
|
+
δ_range: tuple[float, float] = (-np.inf, np.inf)
|
|
54
|
+
# Bad data ranges and strategies (aka exclude bad data)
|
|
55
|
+
nans_strategy: ExcludeStrategy = "pixel"
|
|
56
|
+
F_bad_strategy: ExcludeStrategy = "spaxel"
|
|
57
|
+
F_range: tuple[float, float] = (BAD_FLUX_THRESHOLD, np.inf)
|
|
58
|
+
# Handling of flagged data
|
|
59
|
+
fibre_status_include: tuple[FibreStatus] = (0,)
|
|
60
|
+
apply_mask: bool = True
|
|
61
|
+
# Normalisation
|
|
62
|
+
normalise_F_strategy: NormaliseStrategy = "max only"
|
|
63
|
+
normalise_F_offset: float = 0.0
|
|
64
|
+
normalise_F_scale: float = 1.0
|
|
65
|
+
normalise_αδ_strategy: NormaliseStrategy = "padded"
|
|
66
|
+
normalise_α_offset: float = 0.0
|
|
67
|
+
normalise_α_scale: float = 1.0
|
|
68
|
+
normalise_δ_offset: float = 0.0
|
|
69
|
+
normalise_δ_scale: float = 1.0
|
|
70
|
+
|
|
71
|
+
def __post_init__(self) -> None:
|
|
72
|
+
validate_range(self.λ_range)
|
|
73
|
+
validate_range(self.α_range)
|
|
74
|
+
validate_range(self.δ_range)
|
|
75
|
+
validate_excl_strategy(self.nans_strategy)
|
|
76
|
+
validate_excl_strategy(self.F_bad_strategy)
|
|
77
|
+
validate_range(self.F_range)
|
|
78
|
+
validate_fib_status_incl(self.fibre_status_include)
|
|
79
|
+
validate_apply_mask(self.apply_mask)
|
|
80
|
+
validate_norm_strategy(self.normalise_F_strategy)
|
|
81
|
+
validate_norm_strategy(self.normalise_αδ_strategy)
|
|
82
|
+
validate_offset(self.normalise_F_offset)
|
|
83
|
+
validate_scale(self.normalise_F_scale)
|
|
84
|
+
validate_norm_strategy(self.normalise_αδ_strategy)
|
|
85
|
+
validate_offset(self.normalise_α_offset)
|
|
86
|
+
validate_scale(self.normalise_α_scale)
|
|
87
|
+
validate_offset(self.normalise_δ_offset)
|
|
88
|
+
validate_scale(self.normalise_δ_scale)
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def default() -> DataConfig:
|
|
92
|
+
return DataConfig()
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def from_tiles(
|
|
96
|
+
tiles: LVMTileLike,
|
|
97
|
+
λ_range: tuple[float, float] = (-np.inf, np.inf),
|
|
98
|
+
**overrides,
|
|
99
|
+
) -> DataConfig:
|
|
100
|
+
# λ_range cannot be set automatically
|
|
101
|
+
α_range, δ_range = get_αδ_ranges(tiles)
|
|
102
|
+
|
|
103
|
+
# Instantiate a data config with calc'd + default + overrides
|
|
104
|
+
config_dict = DataConfig(λ_range=λ_range, α_range=α_range, δ_range=δ_range).to_dict()
|
|
105
|
+
config = DataConfig.from_dict(config_dict | overrides)
|
|
106
|
+
|
|
107
|
+
# Clip and filter the data
|
|
108
|
+
ds = process_tile_data(tiles, config)
|
|
109
|
+
|
|
110
|
+
# Calculate the normalisation parameters
|
|
111
|
+
(
|
|
112
|
+
(normalise_F_offset, normalise_F_scale),
|
|
113
|
+
(normalise_α_offset, normalise_α_scale),
|
|
114
|
+
(normalise_δ_offset, normalise_δ_scale),
|
|
115
|
+
) = get_normalisations(ds, config)
|
|
116
|
+
|
|
117
|
+
# We want a square domain in the α, δ plane
|
|
118
|
+
norm_αδ_scale = max(normalise_α_scale, normalise_δ_scale)
|
|
119
|
+
|
|
120
|
+
# Update the config with the calculated values
|
|
121
|
+
norm_overrides = {
|
|
122
|
+
"normalise_F_offset": normalise_F_offset,
|
|
123
|
+
"normalise_F_scale": normalise_F_scale,
|
|
124
|
+
"normalise_α_offset": normalise_α_offset,
|
|
125
|
+
"normalise_α_scale": norm_αδ_scale,
|
|
126
|
+
"normalise_δ_offset": normalise_δ_offset,
|
|
127
|
+
"normalise_δ_scale": norm_αδ_scale,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# Merge partial config + norm + user overrides, with user overrides taking precedence
|
|
131
|
+
return DataConfig.from_dict(config.to_dict() | norm_overrides | overrides)
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def from_dict(config: dict) -> DataConfig:
|
|
135
|
+
if len(config) != len(DataConfig.default().to_dict()):
|
|
136
|
+
raise ValueError("config has the wrong number of entries.")
|
|
137
|
+
return DataConfig(**config)
|
|
138
|
+
|
|
139
|
+
def to_dict(self) -> dict:
|
|
140
|
+
return asdict(self)
|
|
141
|
+
|
|
142
|
+
def __repr__(self) -> str:
|
|
143
|
+
def format_tuple(t):
|
|
144
|
+
"""Format tuple with floats to 2 decimal places or scientific notation if very small."""
|
|
145
|
+
formatted = []
|
|
146
|
+
for x in t:
|
|
147
|
+
if isinstance(x, float):
|
|
148
|
+
if abs(x) < 1e-3 and x != 0:
|
|
149
|
+
formatted.append(f"{x:.2e}")
|
|
150
|
+
else:
|
|
151
|
+
formatted.append(f"{x:.2f}")
|
|
152
|
+
else:
|
|
153
|
+
formatted.append(str(x))
|
|
154
|
+
return f"({', '.join(formatted)})"
|
|
155
|
+
|
|
156
|
+
def format_float(f):
|
|
157
|
+
"""Format float to 2 decimal places, or scientific notation if very small."""
|
|
158
|
+
if abs(f) < 1e-3 and f != 0:
|
|
159
|
+
return f"{f:.2e}"
|
|
160
|
+
else:
|
|
161
|
+
return f"{f:.2f}"
|
|
162
|
+
|
|
163
|
+
lines = [f"{self.__class__.__name__}("]
|
|
164
|
+
|
|
165
|
+
lines = [f"{self.__class__.__name__} ({hex(id(self))}):"]
|
|
166
|
+
|
|
167
|
+
pad = 26
|
|
168
|
+
|
|
169
|
+
# Data clipping ranges
|
|
170
|
+
lines.append(" Data clipping ranges:")
|
|
171
|
+
lines.append(f" {'λ_range:':{pad}}{format_tuple(self.λ_range)}")
|
|
172
|
+
lines.append(f" {'α_range:':{pad}}{format_tuple(self.α_range)}")
|
|
173
|
+
lines.append(f" {'δ_range:':{pad}}{format_tuple(self.δ_range)}")
|
|
174
|
+
|
|
175
|
+
# Bad data handling
|
|
176
|
+
lines.append(" Bad data handling:")
|
|
177
|
+
lines.append(f" {'nans_strategy:':{pad}}'{self.nans_strategy}'")
|
|
178
|
+
lines.append(f" {'F_bad_strategy:':{pad}}'{self.F_bad_strategy}'")
|
|
179
|
+
lines.append(f" {'F_range:':{pad}}{format_tuple(self.F_range)}")
|
|
180
|
+
|
|
181
|
+
# Flagged data handling
|
|
182
|
+
lines.append(" Flagged data handling:")
|
|
183
|
+
lines.append(f" {'fibre_status_include:':{pad}}{self.fibre_status_include}")
|
|
184
|
+
lines.append(f" {'apply_mask:':{pad}}{self.apply_mask}")
|
|
185
|
+
|
|
186
|
+
# Flux normalisation
|
|
187
|
+
lines.append(" Flux normalisation:")
|
|
188
|
+
lines.append(f" {'normalise_F_strategy:':{pad}}'{self.normalise_F_strategy}'")
|
|
189
|
+
lines.append(
|
|
190
|
+
f" {'normalise_F_offset:':{pad}}{format_float(self.normalise_F_offset)}"
|
|
191
|
+
)
|
|
192
|
+
lines.append(f" {'normalise_F_scale:':{pad}}{format_float(self.normalise_F_scale)}")
|
|
193
|
+
|
|
194
|
+
# Coordinate normalisation
|
|
195
|
+
lines.append(" Coordinate normalisation:")
|
|
196
|
+
lines.append(f" {'normalise_αδ_strategy:':{pad}}'{self.normalise_αδ_strategy}'")
|
|
197
|
+
lines.append(
|
|
198
|
+
f" {'normalise_α_offset:':{pad}}{format_float(self.normalise_α_offset)}"
|
|
199
|
+
)
|
|
200
|
+
lines.append(f" {'normalise_α_scale:':{pad}}{format_float(self.normalise_α_scale)}")
|
|
201
|
+
lines.append(
|
|
202
|
+
f" {'normalise_δ_offset:':{pad}}{format_float(self.normalise_δ_offset)}"
|
|
203
|
+
)
|
|
204
|
+
lines.append(f" {'normalise_δ_scale:':{pad}}{format_float(self.normalise_δ_scale)}")
|
|
205
|
+
|
|
206
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""validation.py - Validation functions for data processing configuration."""
|
|
2
|
+
|
|
3
|
+
from typing import get_args
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from lvm_tools.fit_data.filtering import ExcludeStrategy, FibreStatus
|
|
8
|
+
from lvm_tools.fit_data.normalisation import NormaliseStrategy
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def validate_range(x_range: tuple[float, float]) -> None:
|
|
12
|
+
# if not isinstance(x_range, tuple):
|
|
13
|
+
# raise TypeError("Data range must be in a tuple.")
|
|
14
|
+
if len(x_range) != 2:
|
|
15
|
+
raise ValueError(
|
|
16
|
+
"Data range must be a tuple with exactly two values (min, max)."
|
|
17
|
+
)
|
|
18
|
+
if x_range[1] < x_range[0]:
|
|
19
|
+
raise ValueError("Requested data range restriction has max < min.")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def validate_excl_strategy(strategy: ExcludeStrategy) -> None:
|
|
23
|
+
if strategy not in get_args(ExcludeStrategy):
|
|
24
|
+
raise ValueError(f"Unknown exclusion strategy: {strategy}")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def validate_norm_strategy(strategy: NormaliseStrategy) -> None:
|
|
28
|
+
if strategy not in get_args(NormaliseStrategy):
|
|
29
|
+
raise ValueError(f"Unknown normalisation strategy: {strategy}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def validate_fib_status_incl(fibre_status_include: tuple[FibreStatus]) -> None:
|
|
33
|
+
# if not isinstance(fibre_status_include, tuple):
|
|
34
|
+
# raise TypeError("fibre_status_include must be a tuple.")
|
|
35
|
+
for fs in fibre_status_include:
|
|
36
|
+
if fs not in get_args(FibreStatus):
|
|
37
|
+
raise ValueError(f"Unknown fibre status: {fs}")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def validate_offset(offset: float) -> None:
|
|
41
|
+
if not isinstance(offset, (float, np.floating)):
|
|
42
|
+
raise TypeError("offset must be float.")
|
|
43
|
+
if not np.isfinite(offset):
|
|
44
|
+
raise Exception("Bad offset (nan or infty).")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def validate_scale(scale: float) -> None:
|
|
48
|
+
if not isinstance(scale, (float, np.floating)):
|
|
49
|
+
raise TypeError("scale must be float.")
|
|
50
|
+
if not np.isfinite(scale):
|
|
51
|
+
raise Exception("Bad scale (nan or infty).")
|
|
52
|
+
if scale <= 0:
|
|
53
|
+
raise Exception("Scale is not positive, but it must be.")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def validate_apply_mask(apply_mask: bool) -> None:
|
|
57
|
+
if not isinstance(apply_mask, bool):
|
|
58
|
+
raise TypeError("apply_mask must be a boolean.")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""data - subpackage for reading and encapsulating LVM data."""
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""coordinates.py - observation time and location extraction for LVM data processing."""
|
|
2
|
+
|
|
3
|
+
# NOTE: Code is generated by an LLM and may not be fully robust.
|
|
4
|
+
|
|
5
|
+
import astropy.units as u
|
|
6
|
+
from astropy.coordinates import EarthLocation
|
|
7
|
+
from astropy.io.fits import Header
|
|
8
|
+
from astropy.time import Time
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_mjd(header: Header) -> float:
|
|
12
|
+
# Method 1: Use INTSTART and INTEND if available (most precise)
|
|
13
|
+
if "INTSTART" in header and "INTEND" in header:
|
|
14
|
+
start_time = Time(header["INTSTART"], format="isot")
|
|
15
|
+
end_time = Time(header["INTEND"], format="isot")
|
|
16
|
+
mid_time = start_time + (end_time - start_time) / 2
|
|
17
|
+
# print(header["MJD"])
|
|
18
|
+
# mid_time = Time(header["MJD"], format="mjd")
|
|
19
|
+
# print(mid_time.mjd)
|
|
20
|
+
# print()
|
|
21
|
+
|
|
22
|
+
# Validation: check against header EXPTIME
|
|
23
|
+
# calculated_exptime = (end_time - start_time).to(u.second).value
|
|
24
|
+
# header_exptime = header.get("EXPTIME", 0.0)
|
|
25
|
+
# if abs(calculated_exptime - header_exptime) > 1.0: # Allow 1s tolerance
|
|
26
|
+
# print(
|
|
27
|
+
# f" WARNING: Calculated exptime ({calculated_exptime:.1f}s) differs from header EXPTIME ({header_exptime}s)"
|
|
28
|
+
# )
|
|
29
|
+
|
|
30
|
+
return mid_time.mjd
|
|
31
|
+
|
|
32
|
+
# # Method 2: Use OBSTIME + EXPTIME/2 as fallback
|
|
33
|
+
# elif "OBSTIME" in header and "EXPTIME" in header:
|
|
34
|
+
# start_time = Time(header["OBSTIME"], format="isot")
|
|
35
|
+
# exptime = header["EXPTIME"] * u.second
|
|
36
|
+
# mid_time = start_time + exptime / 2
|
|
37
|
+
|
|
38
|
+
# return mid_time.mjd
|
|
39
|
+
|
|
40
|
+
# # Method 3: Use header MJD + EXPTIME/2 (least precise, integer MJD)
|
|
41
|
+
# elif "MJD" in header and "EXPTIME" in header:
|
|
42
|
+
# mjd_start = header["MJD"] # This appears to be integer MJD
|
|
43
|
+
# exptime_days = header["EXPTIME"] / 86400.0 # Convert seconds to days
|
|
44
|
+
# mjd_mid = mjd_start + exptime_days / 2
|
|
45
|
+
|
|
46
|
+
# return mjd_mid
|
|
47
|
+
|
|
48
|
+
else:
|
|
49
|
+
raise ValueError("Could not find sufficient time information in header")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_observatory_code(header: Header) -> str:
|
|
53
|
+
observatory = header.get("OBSERVAT", "").strip()
|
|
54
|
+
|
|
55
|
+
# Just return the observatory code string
|
|
56
|
+
# You can convert to EarthLocation later when needed
|
|
57
|
+
known_observatories = ["LCO"] # Add other LVM observatory codes as needed
|
|
58
|
+
|
|
59
|
+
if observatory in known_observatories:
|
|
60
|
+
return observatory
|
|
61
|
+
else:
|
|
62
|
+
raise ValueError(
|
|
63
|
+
f"Unknown observatory: {observatory}. Known observatories: {known_observatories}"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_observatory_location(observatory: str) -> EarthLocation:
|
|
68
|
+
# LCO = Las Campanas Observatory
|
|
69
|
+
if observatory == "LCO":
|
|
70
|
+
return EarthLocation.of_site("Las Campanas Observatory")
|
|
71
|
+
else:
|
|
72
|
+
# Add other observatories as needed
|
|
73
|
+
raise ValueError(f"Unknown observatory: {observatory}")
|
lvm_tools/data/helper.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""helper.py - helper functions for LVM data processing."""
|
|
2
|
+
|
|
3
|
+
import dask.array as da
|
|
4
|
+
import numpy as np
|
|
5
|
+
from numpy.typing import ArrayLike, NDArray
|
|
6
|
+
from xarray import Dataset
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def daskify_native(array: ArrayLike, chunks: str | int | tuple) -> da.Array:
|
|
10
|
+
"""Convert input to a Dask array with native byte order."""
|
|
11
|
+
arr = np.asarray(array)
|
|
12
|
+
if arr.dtype.byteorder not in ("=", "|"):
|
|
13
|
+
arr = arr.astype(arr.dtype.newbyteorder("="))
|
|
14
|
+
return da.from_array(arr, chunks) # type: ignore[no-any-return]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def numpyfy_native(array: ArrayLike) -> NDArray:
|
|
18
|
+
"""Convert input to a NumPy array with native byte order."""
|
|
19
|
+
arr = np.asarray(array)
|
|
20
|
+
if arr.dtype.byteorder not in ("=", "|"):
|
|
21
|
+
arr = arr.astype(arr.dtype.newbyteorder("="))
|
|
22
|
+
return arr
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def summarize_with_units(ds: Dataset) -> str:
|
|
26
|
+
lines = [f" Data size: {ds.nbytes // 1024**2}MB"]
|
|
27
|
+
lines.append(f" Dimensions: {', '.join(f'{k}: {v}' for k, v in ds.sizes.items())}")
|
|
28
|
+
|
|
29
|
+
# Coordinates
|
|
30
|
+
lines.append(" Coordinates:")
|
|
31
|
+
for name, coord in ds.coords.items():
|
|
32
|
+
dims = f"({', '.join(coord.sizes)})"
|
|
33
|
+
dtype = str(coord.dtype)
|
|
34
|
+
size = f"{coord.nbytes // 1024}kB"
|
|
35
|
+
units = coord.attrs.get("units", "")
|
|
36
|
+
lines.append(f" {name:<13} {dims:<28} {dtype:<8} {size:<6} [{units}]")
|
|
37
|
+
|
|
38
|
+
# Data variables
|
|
39
|
+
lines.append(" Data:")
|
|
40
|
+
for name, var in ds.data_vars.items():
|
|
41
|
+
dims = f"({', '.join(var.sizes)})"
|
|
42
|
+
dtype = str(var.dtype)
|
|
43
|
+
size = f"{var.nbytes // 1024 // 1024}MB"
|
|
44
|
+
if hasattr(var.data, "chunks") and var.data.chunks is not None:
|
|
45
|
+
chunks = f"DaskArray<chunksize={var.data.chunks}>"
|
|
46
|
+
else:
|
|
47
|
+
chunks = "NDArray"
|
|
48
|
+
units = var.attrs.get("units", "")
|
|
49
|
+
lines.append(f" {name:<13} {dims:<28} {dtype:<8} {size:<6} {chunks} [{units}]")
|
|
50
|
+
|
|
51
|
+
return "\n".join(lines)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def convert_sci_to_int(arr: ArrayLike) -> NDArray:
|
|
55
|
+
mapping = {"Sci1": 0, "Sci2": 1, "Sci3": 2}
|
|
56
|
+
return np.array([mapping[item] for item in arr], dtype=int)
|
lvm_tools/data/tile.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""tile.py - Tile classes for LVM data processing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Mapping
|
|
8
|
+
|
|
9
|
+
import astropy.units as u # type: ignore[import]
|
|
10
|
+
import dask.array as da
|
|
11
|
+
import numpy as np
|
|
12
|
+
from astropy.io import fits # type: ignore[import]
|
|
13
|
+
from astropy.io.fits import FITS_rec, HDUList # type: ignore[import]
|
|
14
|
+
from astropy.units import Unit # type: ignore[import]
|
|
15
|
+
from numpy.typing import NDArray
|
|
16
|
+
from xarray import Dataset, concat
|
|
17
|
+
|
|
18
|
+
from lvm_tools.data.coordinates import get_mjd
|
|
19
|
+
from lvm_tools.data.helper import daskify_native, numpyfy_native, summarize_with_units
|
|
20
|
+
|
|
21
|
+
# Conversions between FWHM and Gaussian sigma
|
|
22
|
+
SIGMA_TO_FWHM: float = 2.0 * np.sqrt(2.0 * np.log(2))
|
|
23
|
+
FWHM_TO_SIGMA: float = 1.0 / SIGMA_TO_FWHM
|
|
24
|
+
|
|
25
|
+
# Physical units for the data
|
|
26
|
+
FLUX_UNIT: Unit = u.erg * u.cm**-2 * u.s**-1 * u.angstrom**-1
|
|
27
|
+
SPECTRAL_UNIT: Unit = u.angstrom
|
|
28
|
+
SPATIAL_UNIT: Unit = u.degree
|
|
29
|
+
|
|
30
|
+
# Default chunk size for Dask arrays
|
|
31
|
+
CHUNKSIZE: str = "auto"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_science_inds(slitmap: FITS_rec) -> NDArray:
|
|
35
|
+
return np.where(slitmap.field("targettype") == "science")[0]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class LVMTileMeta:
|
|
40
|
+
filename: str
|
|
41
|
+
tile_id: int
|
|
42
|
+
exp_num: int
|
|
43
|
+
drp_ver: str
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class LVMTile:
|
|
48
|
+
data: Dataset
|
|
49
|
+
meta: LVMTileMeta
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def from_file(cls, drp_file: Path | str) -> LVMTile:
|
|
53
|
+
file = Path(drp_file)
|
|
54
|
+
if not file.exists():
|
|
55
|
+
raise FileNotFoundError("Could not find DRP file.")
|
|
56
|
+
|
|
57
|
+
with fits.open(file, memmap=True) as hdul:
|
|
58
|
+
tile_id, exp_num, drp_ver, mjd = cls.get_metadata(hdul)
|
|
59
|
+
(flux, i_var, mask, lsf), (wave, ra, dec, fibre_id, fibre_status, ifu_label) = (
|
|
60
|
+
cls.get_science_data(hdul)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Conver the lsf from full width at half maximum (FWHM) to sigma
|
|
64
|
+
lsf *= FWHM_TO_SIGMA
|
|
65
|
+
|
|
66
|
+
# Common dimensions for cube data
|
|
67
|
+
pixel_dims = ("tile", "spaxel", "wavelength")
|
|
68
|
+
spaxel_dims = ("tile", "spaxel")
|
|
69
|
+
|
|
70
|
+
# Assemble data into xarray Dataset, containing both dask arrays and numpy arrays
|
|
71
|
+
data = Dataset(
|
|
72
|
+
data_vars={
|
|
73
|
+
"flux": (pixel_dims, flux[None, :, :], {"units": str(FLUX_UNIT)}),
|
|
74
|
+
"i_var": (pixel_dims, i_var[None, :, :], {"units": str(FLUX_UNIT**-2)}),
|
|
75
|
+
"lsf_sigma": (pixel_dims, lsf[None, :, :], {"units": str(SPECTRAL_UNIT)}),
|
|
76
|
+
"mask": (pixel_dims, mask[None, :, :]),
|
|
77
|
+
},
|
|
78
|
+
coords={
|
|
79
|
+
# Main dimensions/coordinates
|
|
80
|
+
"tile": ("tile", [exp_num]),
|
|
81
|
+
"spaxel": ("spaxel", np.arange(len(fibre_id))),
|
|
82
|
+
"wavelength": ("wavelength", wave, {"units": str(SPECTRAL_UNIT)}),
|
|
83
|
+
# More coordinates
|
|
84
|
+
"mjd": ("tile", [mjd], {"units": "day"}),
|
|
85
|
+
"ra": (spaxel_dims, ra[None, :], {"units": str(SPATIAL_UNIT)}),
|
|
86
|
+
"dec": (spaxel_dims, dec[None, :], {"units": str(SPATIAL_UNIT)}),
|
|
87
|
+
"fibre_id": (spaxel_dims, fibre_id[None, :]),
|
|
88
|
+
"ifu_label": (spaxel_dims, ifu_label[None, :]),
|
|
89
|
+
"fibre_status": (spaxel_dims, fibre_status[None, :]),
|
|
90
|
+
},
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Assemble metadata
|
|
94
|
+
meta = LVMTileMeta(
|
|
95
|
+
filename=file.name,
|
|
96
|
+
tile_id=tile_id,
|
|
97
|
+
exp_num=exp_num,
|
|
98
|
+
drp_ver=drp_ver,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return cls(data=data, meta=meta)
|
|
102
|
+
|
|
103
|
+
def __repr__(self) -> str:
|
|
104
|
+
prefix = f"LVMTile ({hex(id(self))}):"
|
|
105
|
+
prefix += f"\n Filename: {self.meta.filename}"
|
|
106
|
+
prefix += f"\n Exposure: {self.meta.exp_num}"
|
|
107
|
+
prefix += f"\n DRP version: {self.meta.drp_ver}"
|
|
108
|
+
prefix += f"\n Tile ID: {self.meta.tile_id}"
|
|
109
|
+
return f"{prefix}\n{summarize_with_units(self.data)}"
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def get_science_data(drp_hdulist: HDUList) -> tuple[tuple, tuple]:
|
|
113
|
+
slitmap = drp_hdulist[-1].data
|
|
114
|
+
science_inds = get_science_inds(slitmap)
|
|
115
|
+
# Lazily load cubes
|
|
116
|
+
flux: da.Array = daskify_native(drp_hdulist[1].data, CHUNKSIZE)[science_inds, :]
|
|
117
|
+
i_var: da.Array = daskify_native(drp_hdulist[2].data, CHUNKSIZE)[science_inds, :]
|
|
118
|
+
mask: da.Array = daskify_native(drp_hdulist[3].data, CHUNKSIZE)[science_inds, :]
|
|
119
|
+
lsf: da.Array = daskify_native(drp_hdulist[5].data, CHUNKSIZE)[science_inds, :]
|
|
120
|
+
# Eagerly coordinates
|
|
121
|
+
wave: NDArray = numpyfy_native(drp_hdulist[4].data)
|
|
122
|
+
ra: NDArray = numpyfy_native((slitmap["ra"])[science_inds])
|
|
123
|
+
dec: NDArray = numpyfy_native((slitmap["dec"])[science_inds])
|
|
124
|
+
fibre_id: NDArray = numpyfy_native((slitmap["fiberid"])[science_inds])
|
|
125
|
+
fibre_status: NDArray = numpyfy_native((slitmap["fibstatus"])[science_inds])
|
|
126
|
+
ifu_label: NDArray = numpyfy_native((slitmap["ifulabel"])[science_inds])
|
|
127
|
+
return (flux, i_var, mask, lsf), (wave, ra, dec, fibre_id, fibre_status, ifu_label)
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
def get_metadata(drp_hdulist: HDUList) -> tuple[int, int, str]:
|
|
131
|
+
try:
|
|
132
|
+
tile_id = int(drp_hdulist[0].header["OBJECT"].split("=")[1])
|
|
133
|
+
except IndexError:
|
|
134
|
+
try:
|
|
135
|
+
tile_id = int(drp_hdulist[0].header["OBJECT"])
|
|
136
|
+
except ValueError:
|
|
137
|
+
tile_id = str(drp_hdulist[0].header["OBJECT"])
|
|
138
|
+
exp_num = int(drp_hdulist[0].header["EXPOSURE"])
|
|
139
|
+
drp_ver = str(drp_hdulist[0].header["DRPVER"])
|
|
140
|
+
mjd = float(get_mjd(drp_hdulist[0].header))
|
|
141
|
+
return tile_id, exp_num, drp_ver, mjd
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass(frozen=True)
|
|
145
|
+
class LVMTileCollection:
|
|
146
|
+
data: Dataset
|
|
147
|
+
meta: Mapping[int, LVMTileMeta]
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def from_tiles(cls, tiles: list[LVMTile]) -> LVMTileCollection:
|
|
151
|
+
# Concatenate tile datasets along the 'tile' dimension
|
|
152
|
+
combined_data = concat([tile.data for tile in tiles], dim="tile")
|
|
153
|
+
|
|
154
|
+
# Construct metadata dictionary keyed by exposure number
|
|
155
|
+
meta_dict = {tile.meta.exp_num: tile.meta for tile in tiles}
|
|
156
|
+
|
|
157
|
+
return cls(data=combined_data, meta=meta_dict)
|
|
158
|
+
|
|
159
|
+
def __repr__(self) -> str:
|
|
160
|
+
prefix = f"LVMTileCollection ({hex(id(self))}):"
|
|
161
|
+
prefix += f"\n Tiles: {len(self.meta)}"
|
|
162
|
+
prefix += f"\n Exposures: {set(meta.exp_num for meta in self.meta.values())}"
|
|
163
|
+
prefix += f"\n DRP versions: {set(meta.drp_ver for meta in self.meta.values())}"
|
|
164
|
+
prefix += f"\n Tile IDs: {set(meta.tile_id for meta in self.meta.values())}"
|
|
165
|
+
return f"{prefix}\n{summarize_with_units(self.data)}"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
LVMTileLike = LVMTile | LVMTileCollection
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""fit_data - subpackage for filtering and otherwise preparing LVM data before fitting."""
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""builder.py - FitDataBuilder class for constructing FitData with reproducibility."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import asdict, dataclass
|
|
5
|
+
from hashlib import sha256
|
|
6
|
+
|
|
7
|
+
from lvm_tools.config.data_config import DataConfig
|
|
8
|
+
from lvm_tools.data.tile import LVMTile, LVMTileLike
|
|
9
|
+
from lvm_tools.fit_data.fit_data import FitData
|
|
10
|
+
from lvm_tools.fit_data.processing import (
|
|
11
|
+
flatten_tile_coord,
|
|
12
|
+
get_normalisation_functions,
|
|
13
|
+
process_tile_data,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class FitDataBuilder:
|
|
19
|
+
tiles: LVMTileLike
|
|
20
|
+
config: DataConfig
|
|
21
|
+
|
|
22
|
+
def build(self) -> FitData:
|
|
23
|
+
return FitData(
|
|
24
|
+
flatten_tile_coord(process_tile_data(self.tiles, self.config)),
|
|
25
|
+
*get_normalisation_functions(self.config),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def hash(self) -> str:
|
|
29
|
+
data = {
|
|
30
|
+
"config": json.dumps(self._configdict, sort_keys=True),
|
|
31
|
+
"tiles": json.dumps(self._metadict, sort_keys=True),
|
|
32
|
+
}
|
|
33
|
+
serialised = json.dumps(data, sort_keys=True)
|
|
34
|
+
return sha256(serialised.encode()).hexdigest()
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def _configdict(self) -> dict:
|
|
38
|
+
return self.config.to_dict()
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def _metadict(self) -> dict:
|
|
42
|
+
# Ensure meta is always a dict of LVMTileMeta
|
|
43
|
+
if isinstance(self.tiles, LVMTile):
|
|
44
|
+
meta = {self.tiles.meta.exp_num: self.tiles.meta.copy()}
|
|
45
|
+
else:
|
|
46
|
+
meta = self.tiles.meta.copy()
|
|
47
|
+
|
|
48
|
+
for key, value in meta.items():
|
|
49
|
+
meta[key] = asdict(value)
|
|
50
|
+
|
|
51
|
+
return meta
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""clipping.py - data clipping for data preparation."""
|
|
2
|
+
|
|
3
|
+
from xarray import DataArray, Dataset
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def bounding_square(
|
|
7
|
+
x_min: float,
|
|
8
|
+
x_max: float,
|
|
9
|
+
y_min: float,
|
|
10
|
+
y_max: float,
|
|
11
|
+
) -> tuple[tuple[float, float], tuple[float, float]]:
|
|
12
|
+
t_range = 1.01 * max(x_max - x_min, y_max - y_min)
|
|
13
|
+
x_mid = (x_min + x_max) / 2
|
|
14
|
+
y_mid = (y_min + y_max) / 2
|
|
15
|
+
x_min_ = x_mid - t_range / 2
|
|
16
|
+
x_max_ = x_mid + t_range / 2
|
|
17
|
+
y_min_ = y_mid - t_range / 2
|
|
18
|
+
y_max_ = y_mid + t_range / 2
|
|
19
|
+
return (x_min_, x_max_), (y_min_, y_max_)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def slice_mask(arr: DataArray, x_min: float, x_max: float) -> DataArray:
|
|
23
|
+
return (arr >= x_min) & (arr <= x_max)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def clip_dataset(
|
|
27
|
+
data: Dataset,
|
|
28
|
+
λ_range: tuple[float, float],
|
|
29
|
+
α_range: tuple[float, float],
|
|
30
|
+
δ_range: tuple[float, float],
|
|
31
|
+
) -> Dataset:
|
|
32
|
+
# Clip to wavelength range (simple since wavelength is an indexed coordinate)
|
|
33
|
+
data = data.sel(wavelength=slice(*λ_range))
|
|
34
|
+
# Clip to ra, dec range. Less simple since spaxel is the indexed coordinate
|
|
35
|
+
α_slice = slice_mask(data["ra"], *α_range)
|
|
36
|
+
δ_slice = slice_mask(data["dec"], *δ_range)
|
|
37
|
+
return data.where(α_slice & δ_slice, drop=True)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""filtering.py - data filtering for data preparation."""
|
|
2
|
+
|
|
3
|
+
import warnings
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from xarray import DataArray, Dataset
|
|
8
|
+
|
|
9
|
+
BAD_FLUX_THRESHOLD = -0.1e-13
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
ExcludeStrategy = Literal[None, "pixel", "spaxel"]
|
|
13
|
+
FibreStatus = Literal[0, 1, 2, 3] # I have no idea what these mean, but they're in the data
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_where_nan(arr: DataArray) -> DataArray:
|
|
17
|
+
return arr.isnull()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_where_bad(arr: DataArray, bad_range: tuple[float, float]) -> DataArray:
|
|
21
|
+
return ~(arr > bad_range[0]) & (arr < bad_range[1])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_where_bad_median(arr: DataArray, bad_range: tuple[float, float]) -> DataArray:
|
|
25
|
+
where_bad_median_l = arr.median(dim="wavelength") < bad_range[0]
|
|
26
|
+
where_bad_median_u = arr.median(dim="wavelength") > bad_range[1]
|
|
27
|
+
all_nan = arr.isnull().all(dim="wavelength")
|
|
28
|
+
return where_bad_median_l | where_bad_median_u | all_nan
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_where_badfib(fib_stat_arr: DataArray, fibre_status_incl: tuple[FibreStatus]) -> DataArray:
|
|
32
|
+
return ~fib_stat_arr.isin(fibre_status_incl)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_where_mask(arr_mask: DataArray) -> DataArray:
|
|
36
|
+
return arr_mask == 1
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def combine_wheres(list_where: list[DataArray]) -> DataArray:
|
|
40
|
+
combined_where = list_where[0]
|
|
41
|
+
for where in list_where[1:]:
|
|
42
|
+
combined_where = combined_where | where
|
|
43
|
+
return combined_where
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def filter_dataset(
|
|
47
|
+
data: Dataset,
|
|
48
|
+
nans_strategy: ExcludeStrategy,
|
|
49
|
+
F_bad_strategy: ExcludeStrategy,
|
|
50
|
+
F_bad_range: tuple[float, float],
|
|
51
|
+
fibre_status_include: tuple[FibreStatus],
|
|
52
|
+
apply_mask: bool,
|
|
53
|
+
) -> Dataset:
|
|
54
|
+
where_bad = []
|
|
55
|
+
|
|
56
|
+
# Nans
|
|
57
|
+
if nans_strategy == "pixel":
|
|
58
|
+
pass # no action needed
|
|
59
|
+
elif nans_strategy == "spaxel":
|
|
60
|
+
where_bad.append(get_where_nan(data["flux"]).any(dim="wavelength"))
|
|
61
|
+
else:
|
|
62
|
+
raise ValueError(f"Unknown nans strategy: {nans_strategy}")
|
|
63
|
+
|
|
64
|
+
# Fluxes
|
|
65
|
+
if F_bad_strategy == "pixel":
|
|
66
|
+
where_bad.append(get_where_bad(data["flux"], F_bad_range))
|
|
67
|
+
elif F_bad_strategy == "spaxel":
|
|
68
|
+
where_bad.append(get_where_bad_median(data["flux"], F_bad_range))
|
|
69
|
+
else:
|
|
70
|
+
raise ValueError(f"Unknown bad flux strategy: {F_bad_strategy}")
|
|
71
|
+
|
|
72
|
+
# Bad fibres
|
|
73
|
+
where_bad.append(get_where_badfib(data["fibre_status"], fibre_status_include))
|
|
74
|
+
|
|
75
|
+
# Filter using mask
|
|
76
|
+
if apply_mask:
|
|
77
|
+
where_bad.append(get_where_mask(data["mask"]))
|
|
78
|
+
|
|
79
|
+
return data.where(~combine_wheres(where_bad))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def filter_inspector(
|
|
83
|
+
data: Dataset,
|
|
84
|
+
F_bad_range: tuple[float, float],
|
|
85
|
+
fibre_status_include: tuple[FibreStatus],
|
|
86
|
+
):
|
|
87
|
+
# TODO: maybe plots instead of printing?
|
|
88
|
+
|
|
89
|
+
# ignore warnings about median of all nans
|
|
90
|
+
with warnings.catch_warnings():
|
|
91
|
+
warnings.simplefilter("ignore", category=RuntimeWarning)
|
|
92
|
+
|
|
93
|
+
# nans:
|
|
94
|
+
where_nan = get_where_nan(data["flux"])
|
|
95
|
+
n_nans = int(np.sum(where_nan))
|
|
96
|
+
n_spaxels_nan = int(np.sum(where_nan.any(dim="wavelength")))
|
|
97
|
+
|
|
98
|
+
# bad flux (per pix):
|
|
99
|
+
where_Fbad = get_where_bad(data["flux"], F_bad_range)
|
|
100
|
+
n_Fbad = int(np.sum(where_Fbad))
|
|
101
|
+
|
|
102
|
+
# bad flux (per spaxel):
|
|
103
|
+
where_Fbad_median = get_where_bad_median(data["flux"], F_bad_range)
|
|
104
|
+
n_Fbad_median = int(np.sum(where_Fbad_median))
|
|
105
|
+
|
|
106
|
+
# fibre status:
|
|
107
|
+
where_badfib = get_where_badfib(data["fibre_status"], fibre_status_include)
|
|
108
|
+
n_spaxels_badfib = int(np.sum(where_badfib))
|
|
109
|
+
|
|
110
|
+
# mask:
|
|
111
|
+
where_mask = get_where_mask(data["mask"])
|
|
112
|
+
n_mask = int(np.sum(where_mask))
|
|
113
|
+
n_spaxels_mask = int(np.sum(where_mask.any(dim="wavelength")))
|
|
114
|
+
|
|
115
|
+
# anything is bad
|
|
116
|
+
where_anybad = where_nan | where_Fbad | where_badfib | where_mask
|
|
117
|
+
n_anybad = int(np.sum(where_anybad))
|
|
118
|
+
|
|
119
|
+
where_anybad_spaxel = where_nan | where_Fbad_median | where_badfib | where_mask
|
|
120
|
+
n_spaxels_anybad = int(np.sum(where_anybad_spaxel.any(dim="wavelength")))
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
"nans": (n_nans, n_spaxels_nan),
|
|
124
|
+
"bad flux": (n_Fbad, None),
|
|
125
|
+
"bad flux median": (None, n_Fbad_median),
|
|
126
|
+
"fibre status": (None, n_spaxels_badfib),
|
|
127
|
+
"mask": (n_mask, n_spaxels_mask),
|
|
128
|
+
"any bad": (n_anybad, n_spaxels_anybad),
|
|
129
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""fit_data.py - FitData class for holding data ready to be fitted."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Callable
|
|
8
|
+
|
|
9
|
+
import jax.numpy as jnp
|
|
10
|
+
import numpy as np
|
|
11
|
+
from jax.numpy import pi as π
|
|
12
|
+
from jaxtyping import Array as JaxArray
|
|
13
|
+
from modelling_lib.model.data import SpatialDataLVM
|
|
14
|
+
from xarray import DataArray, Dataset
|
|
15
|
+
|
|
16
|
+
from lvm_tools.physical_properties.barycentric_corr import get_v_barycentric
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def to_π_domain(x):
|
|
20
|
+
# return x * 2 * π - π
|
|
21
|
+
return x * 2 * np.pi - np.pi
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def from_π_domain(x):
|
|
25
|
+
return (x + π) / (2 * π)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def to_jax_array(arr: DataArray, dtype=np.float64) -> JaxArray:
|
|
29
|
+
with warnings.catch_warnings():
|
|
30
|
+
warnings.filterwarnings("ignore", category=UserWarning)
|
|
31
|
+
return jnp.array(arr, dtype=dtype)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class FitData:
|
|
36
|
+
processed_data: Dataset
|
|
37
|
+
normalise_flux: Callable
|
|
38
|
+
predict_flux: Callable
|
|
39
|
+
normalise_ivar: Callable
|
|
40
|
+
predict_ivar: Callable
|
|
41
|
+
normalise_α: Callable
|
|
42
|
+
_predict_α: Callable
|
|
43
|
+
normalise_δ: Callable
|
|
44
|
+
_predict_δ: Callable
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def _flux(self) -> JaxArray:
|
|
48
|
+
return self.normalise_flux(to_jax_array(self.processed_data["flux"].values))
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def flux(self) -> JaxArray:
|
|
52
|
+
return jnp.nan_to_num(self._flux)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def _i_var(self) -> JaxArray:
|
|
56
|
+
return self.normalise_ivar(to_jax_array(self.processed_data["i_var"].values))
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def i_var(self) -> JaxArray:
|
|
60
|
+
return jnp.nan_to_num(self._i_var, nan=1e-4)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def _u_flux(self) -> JaxArray:
|
|
64
|
+
return self._i_var**-0.5
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def u_flux(self) -> JaxArray:
|
|
68
|
+
return jnp.nan_to_num(self._u_flux, nan=1e2)
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def α(self) -> JaxArray:
|
|
72
|
+
return to_π_domain(self.normalise_α(to_jax_array(self.processed_data["ra"].values)))
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def δ(self) -> JaxArray:
|
|
76
|
+
return to_π_domain(self.normalise_δ(to_jax_array(self.processed_data["dec"].values)))
|
|
77
|
+
|
|
78
|
+
def predict_α(self, x: JaxArray) -> JaxArray:
|
|
79
|
+
return self._predict_α(from_π_domain(x))
|
|
80
|
+
|
|
81
|
+
def predict_δ(self, x: JaxArray) -> JaxArray:
|
|
82
|
+
return self._predict_δ(from_π_domain(x))
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def αδ_data(self) -> SpatialDataLVM:
|
|
86
|
+
return SpatialDataLVM(self.α, self.δ, self.spaxel_idx, self.tile_idx, self.ifu_idx)
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def λ(self) -> JaxArray:
|
|
90
|
+
return to_jax_array(self.processed_data["wavelength"].values)
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def _lsf_σ(self) -> JaxArray:
|
|
94
|
+
return to_jax_array(self.processed_data["lsf_sigma"].values)
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def lsf_σ(self) -> JaxArray:
|
|
98
|
+
median_lsf_σ = jnp.nanmedian(self._lsf_σ)
|
|
99
|
+
return jnp.nan_to_num(self._lsf_σ, nan=median_lsf_σ)
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def mjd(self) -> JaxArray:
|
|
103
|
+
return to_jax_array(self.processed_data["mjd"].values)
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def mask(self) -> JaxArray:
|
|
107
|
+
return ~jnp.isnan(self._flux)
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def λ_idx(self) -> JaxArray:
|
|
111
|
+
return jnp.arange(len(self.λ), dtype=np.int64)
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def spaxel_idx(self) -> JaxArray:
|
|
115
|
+
return jnp.arange(len(self.α), dtype=np.int64)
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def tile_idx(self) -> JaxArray:
|
|
119
|
+
tile = to_jax_array(self.processed_data["tile"].values, dtype=np.int64)
|
|
120
|
+
return jnp.unique(tile, return_inverse=True)[1]
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def ifu_idx(self) -> JaxArray:
|
|
124
|
+
ifu = self.processed_data["ifu_label"].values
|
|
125
|
+
return to_jax_array(np.unique(ifu, return_inverse=True)[1], dtype=np.int64)
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def v_bary(self) -> JaxArray:
|
|
129
|
+
return to_jax_array(
|
|
130
|
+
get_v_barycentric(
|
|
131
|
+
mjd=self.mjd,
|
|
132
|
+
α=self.predict_α(self.α),
|
|
133
|
+
δ=self.predict_δ(self.δ),
|
|
134
|
+
unit="km/s",
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def __repr__(self):
|
|
139
|
+
# TODO: add something here
|
|
140
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""normalisation.py - data normalisation for data preparation."""
|
|
2
|
+
|
|
3
|
+
from functools import partial
|
|
4
|
+
from typing import Callable, Literal
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from numpy.typing import ArrayLike
|
|
8
|
+
|
|
9
|
+
NORM_PADDING = 0.01
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
NormaliseStrategy = Literal[None, "max only", "98 only", "extrema", "1σ", "2σ", "3σ", "padded"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def calc_normalisation(data: ArrayLike, strategy: NormaliseStrategy) -> tuple[float, float]:
|
|
16
|
+
offset = 0.0
|
|
17
|
+
scale = 1.0
|
|
18
|
+
if strategy is None:
|
|
19
|
+
pass
|
|
20
|
+
elif strategy == "max only":
|
|
21
|
+
scale = np.nanmax(data)
|
|
22
|
+
elif strategy == "98 only":
|
|
23
|
+
scale = np.nanpercentile(data, 98)
|
|
24
|
+
elif strategy == "extrema":
|
|
25
|
+
offset = np.nanmin(data)
|
|
26
|
+
scale = np.nanmax(data) - offset
|
|
27
|
+
elif strategy in ("1σ", "2σ", "3σ"):
|
|
28
|
+
offset = np.nanmean(data)
|
|
29
|
+
scale = 2.0 * int(strategy[0]) * np.nanstd(data)
|
|
30
|
+
elif strategy == "padded":
|
|
31
|
+
data_range = np.nanmax(data) - np.nanmin(data)
|
|
32
|
+
offset = np.nanmin(data) - NORM_PADDING * data_range
|
|
33
|
+
scale = (1 + 2 * NORM_PADDING) * data_range
|
|
34
|
+
else:
|
|
35
|
+
raise ValueError(f"Unknown normalisation strategy: {strategy}")
|
|
36
|
+
return float(offset), float(scale)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def normalise(data: ArrayLike, offset: float, scale: float) -> ArrayLike:
|
|
40
|
+
return (data - offset) / scale
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def denormalise(data: ArrayLike, offset: float, scale: float) -> ArrayLike:
|
|
44
|
+
return data * scale + offset
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_norm_funcs(offset: float, scale: float) -> Callable:
|
|
48
|
+
return (partial(f, offset=offset, scale=scale) for f in (normalise, denormalise))
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""processing.py - convenience wrappers for filtering and clipping given LVMTileLike and DataConfig."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
6
|
+
from typing import TYPE_CHECKING, Callable
|
|
7
|
+
|
|
8
|
+
from xarray import Dataset
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from lvm_tools.config.data_config import DataConfig
|
|
12
|
+
from lvm_tools.data.tile import LVMTileLike
|
|
13
|
+
from lvm_tools.fit_data.clipping import bounding_square, clip_dataset
|
|
14
|
+
from lvm_tools.fit_data.filtering import filter_dataset
|
|
15
|
+
from lvm_tools.fit_data.normalisation import calc_normalisation, get_norm_funcs
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def clip_data(tile_data: Dataset, config: DataConfig) -> Dataset:
|
|
19
|
+
return clip_dataset(
|
|
20
|
+
tile_data,
|
|
21
|
+
config.λ_range,
|
|
22
|
+
config.α_range,
|
|
23
|
+
config.δ_range,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def filter_tile_data(tile_data: Dataset, config: DataConfig) -> Dataset:
|
|
28
|
+
return filter_dataset(
|
|
29
|
+
tile_data,
|
|
30
|
+
config.nans_strategy,
|
|
31
|
+
config.F_bad_strategy,
|
|
32
|
+
config.F_range,
|
|
33
|
+
config.fibre_status_include,
|
|
34
|
+
config.apply_mask,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def process_tile_data(tiles: LVMTileLike, config: DataConfig) -> Dataset:
|
|
39
|
+
with warnings.catch_warnings():
|
|
40
|
+
warnings.simplefilter("ignore", category=RuntimeWarning)
|
|
41
|
+
ds = clip_data(tiles.data, config)
|
|
42
|
+
return filter_tile_data(ds, config)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_αδ_ranges(tiles: LVMTileLike) -> tuple[tuple[float, float], tuple[float, float]]:
|
|
46
|
+
return bounding_square(
|
|
47
|
+
tiles.data["ra"].values.min(),
|
|
48
|
+
tiles.data["ra"].values.max(),
|
|
49
|
+
tiles.data["dec"].values.min(),
|
|
50
|
+
tiles.data["dec"].values.max(),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_normalisations(
|
|
55
|
+
ds: Dataset, config: DataConfig
|
|
56
|
+
) -> tuple[tuple[float, float], tuple[float, float], tuple[float, float]]:
|
|
57
|
+
with warnings.catch_warnings():
|
|
58
|
+
warnings.simplefilter("ignore", category=RuntimeWarning)
|
|
59
|
+
return (
|
|
60
|
+
calc_normalisation(ds["flux"].values, config.normalise_F_strategy),
|
|
61
|
+
calc_normalisation(ds["ra"].values, config.normalise_αδ_strategy),
|
|
62
|
+
calc_normalisation(ds["dec"].values, config.normalise_αδ_strategy),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_normalisation_functions(
|
|
67
|
+
config: DataConfig,
|
|
68
|
+
) -> tuple[tuple[Callable, Callable], tuple[Callable, Callable], tuple[Callable, Callable]]:
|
|
69
|
+
return (
|
|
70
|
+
*get_norm_funcs(config.normalise_F_offset, config.normalise_F_scale),
|
|
71
|
+
*get_norm_funcs(0.0, config.normalise_F_scale**-2),
|
|
72
|
+
*get_norm_funcs(config.normalise_α_offset, config.normalise_α_scale),
|
|
73
|
+
*get_norm_funcs(config.normalise_δ_offset, config.normalise_δ_scale),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def flatten_tile_coord(ds: Dataset) -> Dataset:
|
|
78
|
+
"""Flatten the tile and spaxel coordinates into a single coordinate."""
|
|
79
|
+
return ds.stack(flat_spaxel=("tile", "spaxel")).reset_index("flat_spaxel")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""physical_properties - subpackage for calculating physical properties like velocities from either LVM data or models."""
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from astropy.coordinates import EarthLocation, SkyCoord
|
|
2
|
+
from astropy.time import Time
|
|
3
|
+
from numpy.typing import ArrayLike, NDArray
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# def get_v_barycentric(fit_data: FitData, unit="km/s") -> NDArray:
|
|
7
|
+
def get_v_barycentric(mjd: ArrayLike, α: ArrayLike, δ: ArrayLike, unit="km/s") -> NDArray:
|
|
8
|
+
times = Time(mjd, format="mjd")
|
|
9
|
+
coords = SkyCoord(ra=α, dec=δ, obstime=times, unit="deg", frame="icrs")
|
|
10
|
+
location = EarthLocation.of_site("Las Campanas Observatory")
|
|
11
|
+
return coords.radial_velocity_correction("barycentric", location=location).to_value(unit)
|
lvm_tools/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""utils - Dump for stuff that I need for plots/data manipulation like masking near points etc.."""
|
lvm_tools/utils/mask.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy.spatial import cKDTree
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def mask_near_points(xgrid, ygrid, xpoints, ypoints, threshold=None):
|
|
6
|
+
"""
|
|
7
|
+
Generate a boolean mask for a 2D grid, where True means the grid cell is close to at least one (xpoint, ypoint).
|
|
8
|
+
|
|
9
|
+
Parameters
|
|
10
|
+
----------
|
|
11
|
+
xgrid : 1D array
|
|
12
|
+
Grid coordinates along the x-axis (must be monotonically increasing).
|
|
13
|
+
ygrid : 1D array
|
|
14
|
+
Grid coordinates along the y-axis (must be monotonically increasing).
|
|
15
|
+
xpoints : 1D array
|
|
16
|
+
X-coordinates of the data points.
|
|
17
|
+
ypoints : 1D array
|
|
18
|
+
Y-coordinates of the data points.
|
|
19
|
+
threshold : float, optional
|
|
20
|
+
Maximum distance from a grid cell center to be considered "near" a data point.
|
|
21
|
+
If None, uses 1.5 × max(mean grid spacing in x and y).
|
|
22
|
+
|
|
23
|
+
Returns
|
|
24
|
+
-------
|
|
25
|
+
mask : 2D boolean array
|
|
26
|
+
Mask array with shape (len(ygrid), len(xgrid)), where True means "keep" (near a point).
|
|
27
|
+
"""
|
|
28
|
+
xx, yy = np.meshgrid(xgrid, ygrid, indexing="xy")
|
|
29
|
+
grid_centers = np.column_stack([xx.ravel(), yy.ravel()])
|
|
30
|
+
|
|
31
|
+
tree = cKDTree(np.column_stack([xpoints, ypoints]))
|
|
32
|
+
dists, _ = tree.query(grid_centers, k=1)
|
|
33
|
+
|
|
34
|
+
if threshold is None:
|
|
35
|
+
dx = np.mean(np.diff(xgrid))
|
|
36
|
+
dy = np.mean(np.diff(ygrid))
|
|
37
|
+
threshold = 1.5 * max(dx, dy)
|
|
38
|
+
|
|
39
|
+
mask = (dists < threshold).reshape(xx.shape)
|
|
40
|
+
return mask
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lvm_tools
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Lazily read/encapsulate LVM DRP data in a modular way
|
|
5
|
+
Author-email: Tom Hilder <Thomas.Hilder@monash.edu>
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.13
|
|
8
|
+
Requires-Dist: astropy>=7.1.0
|
|
9
|
+
Requires-Dist: dask>=2025.7.0
|
|
10
|
+
Requires-Dist: numpy>=2.3.2
|
|
11
|
+
Requires-Dist: scipy>=1.16.1
|
|
12
|
+
Requires-Dist: xarray>=2025.7.1
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# lvm_tools
|
|
16
|
+
|
|
17
|
+
Lightweight wrapper of [LVM DRP](https://github.com/sdss/lvmdrp) data with an emphasis on modularity. Allows for lazy reading via [`dask`](https://www.dask.org), especially useful for fitting large models. Designed for use with spectrospatial models via [`spectracles`](https://github.com/TomHilder/spectracles) but probably useful for other things too.
|
|
18
|
+
|
|
19
|
+
Feel free to contact me personally if you have any questions at all.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
TODO
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
TODO
|
|
28
|
+
|
|
29
|
+
## Citation
|
|
30
|
+
|
|
31
|
+
TODO
|
|
32
|
+
|
|
33
|
+
## Help
|
|
34
|
+
|
|
35
|
+
TODO
|
|
36
|
+
|
|
37
|
+
## TODO
|
|
38
|
+
|
|
39
|
+
- [ ] repr for FitData
|
|
40
|
+
- [ ] Logging/hashing
|
|
41
|
+
- [ ] Cache
|
|
42
|
+
- [ ] OptConfig
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
lvm_tools/__init__.py,sha256=iiZnberiJmfIQLJYFPi9fAVmJ5JSYNFQ0LSgxKkQQQU,260
|
|
2
|
+
lvm_tools/_version.py,sha256=wO7XWlZte1hxA4mMvRc6zhNdGm74Nhhn2bfWRAxaKbI,511
|
|
3
|
+
lvm_tools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
lvm_tools/config/__init__.py,sha256=KsDaLtZeYt9I867wbB068Li8YSXu1wHH-hcsQw20zYQ,85
|
|
5
|
+
lvm_tools/config/data_config.py,sha256=QJ1Vu2c_YPp9qQ7qF13ohXT3NS5dxh2VgLCblZvXdfk,8648
|
|
6
|
+
lvm_tools/config/validation.py,sha256=PKf4JprRHiLzmtJtzrpj57ccR8g3wU6zAHgCwvmh2Tg,2076
|
|
7
|
+
lvm_tools/data/__init__.py,sha256=m6SJxMoR7PlvHdvL0Ly21xtw0IUl3dpTDDnIsUuZoiQ,64
|
|
8
|
+
lvm_tools/data/coordinates.py,sha256=Rtc3YvNr68aE9xTYOynekY5NhKMm2r-dNNC7OrOWVoo,2778
|
|
9
|
+
lvm_tools/data/helper.py,sha256=EA4U13nvzX8b2ZnK9FzOkOz14nEUKM06-y2TykEip40,2063
|
|
10
|
+
lvm_tools/data/tile.py,sha256=5IvXb2JRANE0LSvmr35mevAT7EFtz2uOYOaL-j0SLXA,6646
|
|
11
|
+
lvm_tools/fit_data/__init__.py,sha256=EJ08IcqxOLaC5tnT5B1TbOhBBn1VmHMQfpXIzSYzmpI,91
|
|
12
|
+
lvm_tools/fit_data/builder.py,sha256=B3j_UjwoxsvU-VMCXnIn0o4_3GVeN1KW04BbgUeNH0w,1496
|
|
13
|
+
lvm_tools/fit_data/clipping.py,sha256=M1yltDQ7OOiZiyihAgyYKzFww9d9CqjfM20Fbv7CYPM,1181
|
|
14
|
+
lvm_tools/fit_data/filtering.py,sha256=5ODMcrx6HO20DgeVfAaY1Zl8UOqWhjy_wA2nlwND34Y,4147
|
|
15
|
+
lvm_tools/fit_data/fit_data.py,sha256=yldItAVzgL4S1E2kmv-kJKRPfv3qsf-zs8_lCFwk-0c,3945
|
|
16
|
+
lvm_tools/fit_data/normalisation.py,sha256=fJxpBhxb5bsmttoN1SIyQRT_WB0pKFzmoLOPSxDbJX4,1549
|
|
17
|
+
lvm_tools/fit_data/processing.py,sha256=BEJCtV1-_Wm0rhe2BxS-6_GfggT-6Q15BDeR9NGmJ0o,2754
|
|
18
|
+
lvm_tools/physical_properties/__init__.py,sha256=3la3E4O5z0dmp5cHPuuhBVOhrncNnxUnjzFLibwhx5k,123
|
|
19
|
+
lvm_tools/physical_properties/barycentric_corr.py,sha256=9FFN_eufKpv7nL2cV0vErDHbf0QKMNtldwBUwoYr81U,566
|
|
20
|
+
lvm_tools/utils/__init__.py,sha256=0t3biRCVmk6mpj5aRk8mYdxilxbBNos0Qluw3zRccd8,100
|
|
21
|
+
lvm_tools/utils/mask.py,sha256=vS4Kz0S6o9tKOt-h0X2Wpjj3gqkAJpiXqiz-l9z12Vc,1371
|
|
22
|
+
lvm_tools-0.0.2.dist-info/METADATA,sha256=l6dxghjDftxgjAWrpi0S78YuJeEAUzLB-c79LhsGQ1k,997
|
|
23
|
+
lvm_tools-0.0.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
24
|
+
lvm_tools-0.0.2.dist-info/licenses/LICENSE,sha256=sYg7u4aQzHWXxn0DBeMqndugw8BXcbUEZvIVKEq_PCE,1070
|
|
25
|
+
lvm_tools-0.0.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Thomas Hilder
|
|
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.
|