astro-otter 0.6.0__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.
- astro_otter-0.6.0.dist-info/METADATA +161 -0
- astro_otter-0.6.0.dist-info/RECORD +18 -0
- astro_otter-0.6.0.dist-info/WHEEL +5 -0
- astro_otter-0.6.0.dist-info/licenses/LICENSE +21 -0
- astro_otter-0.6.0.dist-info/top_level.txt +1 -0
- otter/__init__.py +19 -0
- otter/_version.py +5 -0
- otter/exceptions.py +74 -0
- otter/io/__init__.py +0 -0
- otter/io/data_finder.py +1045 -0
- otter/io/host.py +186 -0
- otter/io/otter.py +1594 -0
- otter/io/transient.py +1453 -0
- otter/plotter/__init__.py +0 -0
- otter/plotter/otter_plotter.py +76 -0
- otter/plotter/plotter.py +266 -0
- otter/schema.py +312 -0
- otter/util.py +850 -0
|
File without changes
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OtterPlotter Class which handles the backend of the plotting.
|
|
3
|
+
|
|
4
|
+
Currently supported backends are:
|
|
5
|
+
- matplotlib
|
|
6
|
+
- plotly
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
import importlib
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class OtterPlotter:
|
|
15
|
+
"""
|
|
16
|
+
Handles the backend for the "plotter" module
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
backend (string): a string of the module name to import and use
|
|
20
|
+
as the backend. Currently supported are "matplotlib",
|
|
21
|
+
"matplotlib.pyplot", "plotly", and "plotly.graph_objects"
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, backend):
|
|
25
|
+
if backend == "matplotlib.pyplot":
|
|
26
|
+
self.backend = backend
|
|
27
|
+
elif backend == "plotly.graph_objects":
|
|
28
|
+
self.backend = backend
|
|
29
|
+
elif "plotly" in backend and "graph_objects" not in backend:
|
|
30
|
+
self.backend = "plotly.graph_objects"
|
|
31
|
+
elif "matplotlib" in backend and "pyplot" not in backend:
|
|
32
|
+
self.backend = "matplotlib.pyplot"
|
|
33
|
+
else:
|
|
34
|
+
raise ValueError("Not a valid backend string!")
|
|
35
|
+
|
|
36
|
+
self.plotter = importlib.import_module(self.backend)
|
|
37
|
+
|
|
38
|
+
if self.backend == "matplotlib.pyplot":
|
|
39
|
+
self.plot = self._plot_matplotlib
|
|
40
|
+
elif self.backend == "plotly.graph_objects":
|
|
41
|
+
self.plot = self._plot_plotly
|
|
42
|
+
else:
|
|
43
|
+
raise ValueError("Unknown plotting backend!")
|
|
44
|
+
|
|
45
|
+
def _plot_matplotlib(self, x, y, xerr=None, yerr=None, ax=None, **kwargs):
|
|
46
|
+
"""
|
|
47
|
+
General plots using matplotlib, is called by _matplotlib_light_curve and
|
|
48
|
+
_matplotlib_sed
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
if ax is None:
|
|
52
|
+
_, ax = self.plotter.subplots()
|
|
53
|
+
|
|
54
|
+
if yerr is not None:
|
|
55
|
+
yerr = np.abs(np.array(yerr))
|
|
56
|
+
ax.errorbar(x, y, xerr=xerr, yerr=yerr, **kwargs)
|
|
57
|
+
return ax
|
|
58
|
+
|
|
59
|
+
def _plot_plotly(self, x, y, xerr=None, yerr=None, ax=None, *args, **kwargs):
|
|
60
|
+
"""
|
|
61
|
+
General plotting method using plotly, is called by _plotly_light_curve and
|
|
62
|
+
_plotly_sed
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
if ax is None:
|
|
66
|
+
go = self.plotter.Figure()
|
|
67
|
+
else:
|
|
68
|
+
go = ax
|
|
69
|
+
|
|
70
|
+
if yerr is not None:
|
|
71
|
+
yerr = np.abs(np.array(yerr))
|
|
72
|
+
fig = go.add_scatter(
|
|
73
|
+
x=x, y=y, error_x=dict(array=xerr), error_y=dict(array=yerr), **kwargs
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return fig
|
otter/plotter/plotter.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Some utilities to create common plots for transients that use the OtterPlotter
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
from warnings import warn
|
|
7
|
+
|
|
8
|
+
import matplotlib.pyplot as plt
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
from .otter_plotter import OtterPlotter
|
|
13
|
+
from ..exceptions import FailedQueryError
|
|
14
|
+
from ..io.otter import Transient, Otter
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def query_quick_view(
|
|
18
|
+
db: Otter,
|
|
19
|
+
ptype: str = "both",
|
|
20
|
+
sed_dim: str = "freq",
|
|
21
|
+
dt_over_t: float = 0,
|
|
22
|
+
plotting_kwargs: dict = {},
|
|
23
|
+
phot_cleaning_kwargs: dict = {},
|
|
24
|
+
result_length_tol=10,
|
|
25
|
+
**kwargs,
|
|
26
|
+
) -> list[plt.Figure]:
|
|
27
|
+
"""
|
|
28
|
+
Queries otter and then plots all of the transients. It will either query the Otter
|
|
29
|
+
object provided in db or construct an Otter object from otter_path.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
db (otter.Otter) : The otter object to query
|
|
33
|
+
ptype (str) : The plot type to generate. Valid options are
|
|
34
|
+
- both -> Plot both light curve and sed (default)
|
|
35
|
+
- sed -> Plot just the sed
|
|
36
|
+
- lc -> Plot just the light curve
|
|
37
|
+
sed_dim (str) : The x dimension to plot in the SED. Options are "freq" or
|
|
38
|
+
"wave". Default is "freq".
|
|
39
|
+
time_tol (float) : The tolerance to split the days by. Default is 1 day. must be
|
|
40
|
+
in units of days.
|
|
41
|
+
plotting_kwargs (dict) : dictionary of key word arguments to pass to
|
|
42
|
+
otter.plotter.plot_light_curve or
|
|
43
|
+
otter.plotter.plot_sed.
|
|
44
|
+
phot_cleaning_kwargs (dict) : Keyword arguments passed to
|
|
45
|
+
otter.Transient.clean_photometry
|
|
46
|
+
result_length_tol (int) : If the query result is longer than this it will throw
|
|
47
|
+
an erorr to prevent 100s of plots from spitting out
|
|
48
|
+
(and likely crashing your computer). Default is 10 but
|
|
49
|
+
can be adjusted.
|
|
50
|
+
**kwargs : Arguments to pass to otter.Otter.query
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
A list of matplotlib pyplot Figure objects that we plotted
|
|
54
|
+
|
|
55
|
+
"""
|
|
56
|
+
res = db.query(**kwargs)
|
|
57
|
+
|
|
58
|
+
if len(res) > result_length_tol:
|
|
59
|
+
raise RuntimeError(
|
|
60
|
+
f"This query returned {len(res)} results which is greater than the given "
|
|
61
|
+
+ f"tolerance of {result_length_tol}! Either increase the result_length_tol"
|
|
62
|
+
+ " keyword or pass in a stricter query!"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
figs = []
|
|
66
|
+
for t in res:
|
|
67
|
+
try:
|
|
68
|
+
fig = quick_view(
|
|
69
|
+
t, ptype, sed_dim, dt_over_t, plotting_kwargs, **phot_cleaning_kwargs
|
|
70
|
+
)
|
|
71
|
+
except (KeyError, FailedQueryError):
|
|
72
|
+
warn(f"No photometry associated with {t.default_name}, skipping!")
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
fig.suptitle(t.default_name)
|
|
76
|
+
figs.append(fig)
|
|
77
|
+
|
|
78
|
+
return figs
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def quick_view(
|
|
82
|
+
t: Transient,
|
|
83
|
+
ptype: str = "both",
|
|
84
|
+
sed_dim: str = "freq",
|
|
85
|
+
dt_over_t: float = 0,
|
|
86
|
+
plotting_kwargs: dict = {},
|
|
87
|
+
**kwargs,
|
|
88
|
+
) -> plt.Figure:
|
|
89
|
+
"""
|
|
90
|
+
Generate a quick view (not necessarily publication ready) of the transients light
|
|
91
|
+
curve, SED, or both. Default is to do both.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
t (otter.Transient) : An otter Transient object to grab photometry from
|
|
95
|
+
ptype (str) : The plot type to generate. Valid options are
|
|
96
|
+
- both -> Plot both light curve and sed (default)
|
|
97
|
+
- sed -> Plot just the sed
|
|
98
|
+
- lc -> Plot just the light curve
|
|
99
|
+
sed_dim (str) : The x dimension to plot in the SED. Options are "freq" or
|
|
100
|
+
"wave". Default is "freq".
|
|
101
|
+
dt_over_t (float) : The tolerance to split the days by. Default is 1 day. must
|
|
102
|
+
be unitless.
|
|
103
|
+
plotting_kwargs (dict) : dictionary of key word arguments to pass to
|
|
104
|
+
otter.plotter.plot_light_curve or
|
|
105
|
+
otter.plotter.plot_sed.
|
|
106
|
+
**kwargs : Any other arguments to pass to otter.Transient.clean_photometry
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
The matplotlib figure used for plotting.
|
|
110
|
+
"""
|
|
111
|
+
backend = plotting_kwargs.get("backend", "matplotlib.pyplot")
|
|
112
|
+
if backend not in {"matplotlib.pyplot", "matplotlib"}:
|
|
113
|
+
raise ValueError(
|
|
114
|
+
"Only matplotlib.pyplot backend is available for quick_view!"
|
|
115
|
+
+ " To use plotly, use the plotting functionality individually!"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
allphot = t.clean_photometry(**kwargs)
|
|
119
|
+
allphot = allphot.sort_values("converted_date")
|
|
120
|
+
allphot["time_tol"] = dt_over_t * allphot["converted_date"]
|
|
121
|
+
allphot["time_diff"] = allphot["converted_date"].diff().fillna(-np.inf)
|
|
122
|
+
allphot["time_grp"] = (allphot.time_diff > allphot.time_tol).cumsum()
|
|
123
|
+
|
|
124
|
+
plt_lc = (ptype == "both") or (ptype == "lc")
|
|
125
|
+
plt_sed = (ptype == "both") or (ptype == "sed")
|
|
126
|
+
|
|
127
|
+
if ptype == "both":
|
|
128
|
+
fig, (lc_ax, sed_ax) = plt.subplots(1, 2)
|
|
129
|
+
elif ptype == "sed":
|
|
130
|
+
fig, sed_ax = plt.subplots()
|
|
131
|
+
elif ptype == "lc":
|
|
132
|
+
fig, lc_ax = plt.subplots()
|
|
133
|
+
|
|
134
|
+
if np.all(pd.isna(allphot.converted_flux_err)):
|
|
135
|
+
flux_err = None
|
|
136
|
+
else:
|
|
137
|
+
flux_err = allphot.converted_flux_err
|
|
138
|
+
|
|
139
|
+
if plt_lc:
|
|
140
|
+
for filt, phot in allphot.groupby("filter_name"):
|
|
141
|
+
plot_light_curve(
|
|
142
|
+
date=phot.converted_date,
|
|
143
|
+
flux=phot.converted_flux,
|
|
144
|
+
flux_err=flux_err[allphot.filter_name == filt],
|
|
145
|
+
xlabel=f"Date [{phot.converted_date_unit.values[0]}]",
|
|
146
|
+
ylabel=f"Flux [{phot.converted_flux_unit.values[0]}]",
|
|
147
|
+
ax=lc_ax,
|
|
148
|
+
label=filt,
|
|
149
|
+
**plotting_kwargs,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
if plt_sed:
|
|
153
|
+
for grp_name, phot in allphot.groupby("time_grp"):
|
|
154
|
+
if sed_dim == "wave":
|
|
155
|
+
wave_or_freq = phot.converted_wave
|
|
156
|
+
xlab = f"Wavelength [{phot.converted_wave_unit.values[0]}]"
|
|
157
|
+
elif sed_dim == "freq":
|
|
158
|
+
wave_or_freq = phot.converted_freq
|
|
159
|
+
xlab = f"Frequency [{phot.converted_freq_unit.values[0]}]"
|
|
160
|
+
else:
|
|
161
|
+
raise ValueError("sed_dim value is not recognized!")
|
|
162
|
+
|
|
163
|
+
plot_sed(
|
|
164
|
+
wave_or_freq=wave_or_freq,
|
|
165
|
+
flux=phot.converted_flux,
|
|
166
|
+
flux_err=flux_err[allphot.time_grp == grp_name],
|
|
167
|
+
ax=sed_ax,
|
|
168
|
+
xlabel=xlab,
|
|
169
|
+
ylabel=f"Flux [{phot.converted_flux_unit.values[0]}]",
|
|
170
|
+
label=phot.converted_date.mean(),
|
|
171
|
+
**plotting_kwargs,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
sed_ax.set_xscale("log")
|
|
175
|
+
|
|
176
|
+
return fig
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def plot_light_curve(
|
|
180
|
+
date: float,
|
|
181
|
+
flux: float,
|
|
182
|
+
date_err: float = None,
|
|
183
|
+
flux_err: float = None,
|
|
184
|
+
fig=None,
|
|
185
|
+
ax=None,
|
|
186
|
+
backend: str = "matplotlib",
|
|
187
|
+
xlabel: str = "Date",
|
|
188
|
+
ylabel: str = "Flux",
|
|
189
|
+
**kwargs,
|
|
190
|
+
):
|
|
191
|
+
"""
|
|
192
|
+
Plot the light curve for the input data
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
date (float): MJD dates
|
|
196
|
+
flux (float): Flux
|
|
197
|
+
date_err (float): optional error on the MJD dates
|
|
198
|
+
flux_err (float): optional error on the flux
|
|
199
|
+
fig (float): matplotlib fig object, optional. Will be created if not provided.
|
|
200
|
+
ax (float): matplitlib axis object, optional. Will be created if not provided.
|
|
201
|
+
backend (str): backend for plotting. options: "matplotlib" (default) or "plotly"
|
|
202
|
+
xlabel (str): x-axis label
|
|
203
|
+
ylabel (str): y-axis label
|
|
204
|
+
**kwargs: keyword arguments to pass to either plotly.graph_objects.add_scatter
|
|
205
|
+
or matplotlib.pyplot.errorbar
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Either a matplotlib axis or plotly figure
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
plt = OtterPlotter(backend)
|
|
212
|
+
fig = plt.plot(date, flux, date_err, flux_err, ax=ax, **kwargs)
|
|
213
|
+
|
|
214
|
+
if backend == "matplotlib":
|
|
215
|
+
fig.set_ylabel(ylabel)
|
|
216
|
+
fig.set_xlabel(xlabel)
|
|
217
|
+
|
|
218
|
+
elif backend == "plotly":
|
|
219
|
+
fig.update_layout(xaxis_title=xlabel, yaxis_title=ylabel)
|
|
220
|
+
|
|
221
|
+
return fig
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def plot_sed(
|
|
225
|
+
wave_or_freq: float,
|
|
226
|
+
flux: float,
|
|
227
|
+
wave_or_freq_err: float = None,
|
|
228
|
+
flux_err: float = None,
|
|
229
|
+
fig=None,
|
|
230
|
+
ax=None,
|
|
231
|
+
backend: str = "matplotlib",
|
|
232
|
+
xlabel: str = "Frequency or Wavelength",
|
|
233
|
+
ylabel: str = "Flux",
|
|
234
|
+
**kwargs,
|
|
235
|
+
):
|
|
236
|
+
"""
|
|
237
|
+
Plot the SED for the input data
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
wave_or_freq (float): wave or frequency array
|
|
241
|
+
flux (float): Flux
|
|
242
|
+
wave_or_freq_err (float): optional error on the MJD dates
|
|
243
|
+
flux_err (float): optional error on the flux
|
|
244
|
+
fig (float): matplotlib fig object, optional. Will be created if not provided.
|
|
245
|
+
ax (float): matplitlib axis object, optional. Will be created if not provided.
|
|
246
|
+
backend (str): backend for plotting. Options: "matplotlib" (default) or "plotly"
|
|
247
|
+
xlabel (str): x-axis label
|
|
248
|
+
ylabel (str): y-axis label
|
|
249
|
+
**kwargs: keyword arguments to pass to either plotly.graph_objects.add_scatter
|
|
250
|
+
or matplotlib.pyplot.errorbar
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Either a matplotlib axis or plotly figure
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
plt = OtterPlotter(backend)
|
|
257
|
+
fig = plt.plot(wave_or_freq, flux, wave_or_freq_err, flux_err, ax=ax, **kwargs)
|
|
258
|
+
|
|
259
|
+
if backend == "matplotlib":
|
|
260
|
+
fig.set_ylabel(ylabel)
|
|
261
|
+
fig.set_xlabel(xlabel)
|
|
262
|
+
|
|
263
|
+
elif backend == "plotly":
|
|
264
|
+
fig.update_layout(xaxis_title=xlabel, yaxis_title=ylabel)
|
|
265
|
+
|
|
266
|
+
return fig
|
otter/schema.py
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pydantic Schema Model of our JSON schema
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, model_validator, field_validator
|
|
6
|
+
from typing import Optional, Union, List
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class VersionSchema(BaseModel):
|
|
10
|
+
value: Union[str, int] = None
|
|
11
|
+
comment: str = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class _AliasSchema(BaseModel):
|
|
15
|
+
value: str
|
|
16
|
+
reference: Union[str, List[str]]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _XrayModelSchema(BaseModel):
|
|
20
|
+
# the following two lines are needed to prevent annoying warnings
|
|
21
|
+
model_config: dict = {}
|
|
22
|
+
model_config["protected_namespaces"] = ()
|
|
23
|
+
|
|
24
|
+
# required keywords
|
|
25
|
+
model_name: str
|
|
26
|
+
param_names: List[str]
|
|
27
|
+
param_values: List[Union[float, int, str]]
|
|
28
|
+
param_units: List[Union[str, None]]
|
|
29
|
+
min_energy: Union[float, int, str]
|
|
30
|
+
max_energy: Union[float, int, str]
|
|
31
|
+
energy_units: str
|
|
32
|
+
|
|
33
|
+
# optional keywords
|
|
34
|
+
param_value_err_upper: Optional[List[Union[float, int, str]]] = None
|
|
35
|
+
param_value_err_lower: Optional[List[Union[float, int, str]]] = None
|
|
36
|
+
param_upperlimit: Optional[List[Union[float, int, str]]] = None
|
|
37
|
+
param_descriptions: Optional[List[str]] = None
|
|
38
|
+
model_reference: Optional[Union[str, List[str]]] = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _ErrDetailSchema(BaseModel):
|
|
42
|
+
# all optional keywords!
|
|
43
|
+
upper: Optional[List[Union[float, int, str]]] = None
|
|
44
|
+
lower: Optional[List[Union[float, int, str]]] = None
|
|
45
|
+
systematic: Optional[List[Union[float, int, str]]] = None
|
|
46
|
+
statistical: Optional[List[Union[float, int, str]]] = None
|
|
47
|
+
iss: Optional[List[Union[float, int, str]]] = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class NameSchema(BaseModel):
|
|
51
|
+
default_name: str
|
|
52
|
+
alias: list[_AliasSchema]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class CoordinateSchema(BaseModel):
|
|
56
|
+
reference: Union[List[str], str]
|
|
57
|
+
ra: Union[str, float] = None
|
|
58
|
+
dec: Union[str, float] = None
|
|
59
|
+
l: Union[str, float] = None # noqa: E741
|
|
60
|
+
b: Union[str, float] = None
|
|
61
|
+
lon: Union[str, float] = None
|
|
62
|
+
lat: Union[str, float] = None
|
|
63
|
+
ra_units: str = None
|
|
64
|
+
dec_units: str = None
|
|
65
|
+
l_units: str = None
|
|
66
|
+
b_units: str = None
|
|
67
|
+
lon_units: str = None
|
|
68
|
+
lat_units: str = None
|
|
69
|
+
ra_error: Union[str, float] = None
|
|
70
|
+
dec_error: Union[str, float] = None
|
|
71
|
+
l_error: Union[str, float] = None
|
|
72
|
+
b_error: Union[str, float] = None
|
|
73
|
+
lon_error: Union[str, float] = None
|
|
74
|
+
lat_error: Union[str, float] = None
|
|
75
|
+
epoch: str = None
|
|
76
|
+
frame: str = "J2000"
|
|
77
|
+
coord_type: str = None
|
|
78
|
+
computed: bool = False
|
|
79
|
+
default: bool = False
|
|
80
|
+
|
|
81
|
+
@model_validator(mode="after")
|
|
82
|
+
def _has_coordinate(self):
|
|
83
|
+
uses_ra_dec = self.ra is not None and self.dec is not None
|
|
84
|
+
uses_galactic = self.l is not None and self.b is not None
|
|
85
|
+
uses_lon_lat = self.lon is not None and self.lat is not None
|
|
86
|
+
|
|
87
|
+
if uses_ra_dec:
|
|
88
|
+
if self.ra_units is None:
|
|
89
|
+
raise ValueError("ra_units must be provided for RA!")
|
|
90
|
+
if self.dec_units is None:
|
|
91
|
+
raise ValueError("dec_units must be provided for Dec!")
|
|
92
|
+
|
|
93
|
+
elif uses_galactic:
|
|
94
|
+
if self.l_units is None:
|
|
95
|
+
raise ValueError("l_units must be provided for RA!")
|
|
96
|
+
if self.b_units is None:
|
|
97
|
+
raise ValueError("b_units must be provided for Dec!")
|
|
98
|
+
|
|
99
|
+
elif uses_lon_lat:
|
|
100
|
+
if self.lon_units is None:
|
|
101
|
+
raise ValueError("lon_units must be provided for RA!")
|
|
102
|
+
if self.lat_units is None:
|
|
103
|
+
raise ValueError("lat_units must be provided for Dec!")
|
|
104
|
+
|
|
105
|
+
else:
|
|
106
|
+
raise ValueError("Must have RA/Dec, l/b, and/or lon/lat!")
|
|
107
|
+
|
|
108
|
+
return self
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class DistanceSchema(BaseModel):
|
|
112
|
+
value: Union[str, float, int]
|
|
113
|
+
unit: str = None
|
|
114
|
+
reference: Union[str, List[str]]
|
|
115
|
+
distance_type: str
|
|
116
|
+
error: Union[str, float, int] = None
|
|
117
|
+
cosmology: str = None
|
|
118
|
+
computed: bool = False
|
|
119
|
+
uuid: str = None
|
|
120
|
+
default: bool = False
|
|
121
|
+
|
|
122
|
+
@model_validator(mode="after")
|
|
123
|
+
def _has_units(self):
|
|
124
|
+
if self.distance_type != "redshift" and self.unit is None:
|
|
125
|
+
raise ValueError("Need units if the distance_type is not redshift!")
|
|
126
|
+
|
|
127
|
+
return self
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class ClassificationSchema(BaseModel):
|
|
131
|
+
object_class: str
|
|
132
|
+
confidence: float
|
|
133
|
+
reference: Union[str, List[str]]
|
|
134
|
+
default: bool = False
|
|
135
|
+
class_type: str = None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class ClassificationDictSchema(BaseModel):
|
|
139
|
+
spec_classed: Optional[int] = None
|
|
140
|
+
unambiguous: Optional[bool] = None
|
|
141
|
+
value: list[ClassificationSchema]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class ReferenceSchema(BaseModel):
|
|
145
|
+
name: str
|
|
146
|
+
human_readable_name: str
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class DateSchema(BaseModel):
|
|
150
|
+
value: Union[str, int, float]
|
|
151
|
+
date_format: str
|
|
152
|
+
date_type: str
|
|
153
|
+
reference: Union[str, List[str]]
|
|
154
|
+
computed: bool = None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class PhotometrySchema(BaseModel):
|
|
158
|
+
reference: Union[List[str], str]
|
|
159
|
+
raw: list[Union[float, int]]
|
|
160
|
+
raw_err: Optional[List[float]] = []
|
|
161
|
+
raw_units: Union[str, List[str]]
|
|
162
|
+
value: Optional[list[Union[float, int]]] = None
|
|
163
|
+
value_err: Optional[list[Union[float, int]]] = None
|
|
164
|
+
value_units: Optional[Union[str, List[str]]] = None
|
|
165
|
+
epoch_zeropoint: Optional[Union[float, str, int]] = None
|
|
166
|
+
epoch_redshift: Optional[Union[float, int]] = None
|
|
167
|
+
filter: Optional[Union[str, List[str]]] = None
|
|
168
|
+
filter_key: Union[str, List[str]]
|
|
169
|
+
obs_type: Union[str, List[str]]
|
|
170
|
+
telescope_area: Optional[Union[float, List[float]]] = None
|
|
171
|
+
date: Union[str, float, List[Union[str, float]]]
|
|
172
|
+
date_format: Union[str, List[str]]
|
|
173
|
+
date_err: Optional[Union[str, float, List[Union[str, float]]]] = None
|
|
174
|
+
date_min: Optional[Union[str, float, List[Union[str, float]]]] = None
|
|
175
|
+
date_max: Optional[Union[str, float, List[Union[str, float]]]] = None
|
|
176
|
+
ignore: Optional[Union[bool, List[bool]]] = None
|
|
177
|
+
upperlimit: Optional[Union[bool, List[bool]]] = None
|
|
178
|
+
sigma: Optional[Union[str, float, List[Union[str, float]]]] = None
|
|
179
|
+
sky: Optional[Union[str, float, List[Union[str, float]]]] = None
|
|
180
|
+
telescope: Optional[Union[str, List[str]]] = None
|
|
181
|
+
instrument: Optional[Union[str, List[str]]] = None
|
|
182
|
+
phot_type: Optional[Union[str, List[str]]] = None
|
|
183
|
+
exptime: Optional[Union[str, int, float, List[Union[str, int, float]]]] = None
|
|
184
|
+
aperture: Optional[Union[str, int, float, List[Union[str, int, float]]]] = None
|
|
185
|
+
observer: Optional[Union[str, List[str]]] = None
|
|
186
|
+
reducer: Optional[Union[str, List[str]]] = None
|
|
187
|
+
pipeline: Optional[Union[str, List[str]]] = None
|
|
188
|
+
corr_k: Optional[Union[bool, str, List[Union[bool, str]]]] = None
|
|
189
|
+
corr_s: Optional[Union[bool, str, List[Union[bool, str]]]] = None
|
|
190
|
+
corr_av: Optional[Union[bool, str, List[Union[bool, str]]]] = None
|
|
191
|
+
corr_host: Optional[Union[bool, str, List[Union[bool, str]]]] = None
|
|
192
|
+
corr_hostav: Optional[Union[bool, str, List[Union[bool, str]]]] = None
|
|
193
|
+
val_k: Optional[Union[float, int, str, List[Union[float, int, str]]]] = None
|
|
194
|
+
val_s: Optional[Union[float, int, str, List[Union[float, int, str]]]] = None
|
|
195
|
+
val_av: Optional[Union[float, int, str, List[Union[float, int, str]]]] = None
|
|
196
|
+
val_host: Optional[Union[float, int, str, List[Union[float, int, str]]]] = None
|
|
197
|
+
val_hostav: Optional[Union[float, int, str, List[Union[float, int, str]]]] = None
|
|
198
|
+
xray_model: Optional[Union[List[_XrayModelSchema], List[None]]] = None
|
|
199
|
+
raw_err_detail: Optional[_ErrDetailSchema] = None
|
|
200
|
+
value_err_detail: Optional[_ErrDetailSchema] = None
|
|
201
|
+
|
|
202
|
+
@field_validator(
|
|
203
|
+
"raw_units",
|
|
204
|
+
"raw_err",
|
|
205
|
+
"filter_key",
|
|
206
|
+
"obs_type",
|
|
207
|
+
"date_format",
|
|
208
|
+
"upperlimit",
|
|
209
|
+
"date",
|
|
210
|
+
"telescope",
|
|
211
|
+
)
|
|
212
|
+
@classmethod
|
|
213
|
+
def ensure_list(cls, v):
|
|
214
|
+
if not isinstance(v, list):
|
|
215
|
+
return [v]
|
|
216
|
+
return v
|
|
217
|
+
|
|
218
|
+
@model_validator(mode="after")
|
|
219
|
+
def _ensure_min_and_max_date(self):
|
|
220
|
+
"""
|
|
221
|
+
This will make sure that if date_min is provided so is date_max
|
|
222
|
+
"""
|
|
223
|
+
if (self.date_min is not None and self.date_max is None) or (
|
|
224
|
+
self.date_min is None and self.date_max is not None
|
|
225
|
+
):
|
|
226
|
+
raise ValueError(
|
|
227
|
+
"If you provide date_min or date_max you must provide the other!"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
@model_validator(mode="after")
|
|
231
|
+
def _ensure_xray_model(self):
|
|
232
|
+
"""
|
|
233
|
+
This will eventually ensure the xray_model key is used if obs_type="xray"
|
|
234
|
+
|
|
235
|
+
It will be commented out until we get the data setup correctly
|
|
236
|
+
"""
|
|
237
|
+
# if self.obs_type == "xray" and self.xray_model is None:
|
|
238
|
+
# raise ValueError(
|
|
239
|
+
# "Need an xray_model for this xray data!"
|
|
240
|
+
# )
|
|
241
|
+
|
|
242
|
+
return self
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class FilterSchema(BaseModel):
|
|
246
|
+
filter_key: str
|
|
247
|
+
filter_name: str
|
|
248
|
+
wave_eff: Union[str, float, int] = None
|
|
249
|
+
wave_min: Union[str, float, int] = None
|
|
250
|
+
wave_max: Union[str, float, int] = None
|
|
251
|
+
freq_eff: Union[str, float, int] = None
|
|
252
|
+
freq_min: Union[str, float, int] = None
|
|
253
|
+
freq_max: Union[str, float, int] = None
|
|
254
|
+
zp: Union[str, float, int] = None
|
|
255
|
+
wave_units: Union[str, float, int] = None
|
|
256
|
+
freq_units: Union[str, float, int] = None
|
|
257
|
+
zp_units: Union[str, float, int] = None
|
|
258
|
+
zp_system: Union[str, float, int] = None
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class HostSchema(BaseModel):
|
|
262
|
+
reference: Union[str, List[str]]
|
|
263
|
+
host_ra: Optional[Union[str, float]] = None
|
|
264
|
+
host_dec: Optional[Union[str, float]] = None
|
|
265
|
+
host_ra_units: Optional[str] = None
|
|
266
|
+
host_dec_units: Optional[str] = None
|
|
267
|
+
host_z: Optional[Union[str, int, float]] = None
|
|
268
|
+
host_type: Optional[str] = None
|
|
269
|
+
host_name: Optional[str] = None
|
|
270
|
+
|
|
271
|
+
@model_validator(mode="after")
|
|
272
|
+
def _has_coordinate_or_name(self):
|
|
273
|
+
has_coordinate = self.host_ra is not None and self.host_dec is not None
|
|
274
|
+
has_name = self.host_name is not None
|
|
275
|
+
|
|
276
|
+
# if it has the RA/Dec keys, make sure it also has ra_unit, dec_unit keys
|
|
277
|
+
if has_coordinate:
|
|
278
|
+
if self.host_ra_units is None:
|
|
279
|
+
raise ValueError("Need RA unit if coordinates are provided!")
|
|
280
|
+
if self.host_dec_units is None:
|
|
281
|
+
raise ValueError("Need Dec unit if coordinates are provided!")
|
|
282
|
+
|
|
283
|
+
# we need either the coordinate or name to identify this object
|
|
284
|
+
# Both are okay too (more info is always better)
|
|
285
|
+
if not has_coordinate and not has_name:
|
|
286
|
+
raise ValueError("Need to provide a Host name and/or host coordinates!")
|
|
287
|
+
|
|
288
|
+
# Make sure that if one of RA/Dec is given then both are given
|
|
289
|
+
if (self.host_ra is None and self.host_dec is not None) or (
|
|
290
|
+
self.host_ra is not None and self.host_dec is None
|
|
291
|
+
):
|
|
292
|
+
raise ValueError("Please provide RA AND Dec, not just one or the other!")
|
|
293
|
+
|
|
294
|
+
return self
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class OtterSchema(BaseModel):
|
|
298
|
+
schema_version: Optional[VersionSchema] = None
|
|
299
|
+
name: NameSchema
|
|
300
|
+
coordinate: list[CoordinateSchema]
|
|
301
|
+
distance: Optional[list[DistanceSchema]] = None
|
|
302
|
+
classification: Optional[ClassificationDictSchema] = None
|
|
303
|
+
reference_alias: list[ReferenceSchema]
|
|
304
|
+
date_reference: Optional[list[DateSchema]] = None
|
|
305
|
+
photometry: Optional[list[PhotometrySchema]] = None
|
|
306
|
+
filter_alias: Optional[list[FilterSchema]] = None
|
|
307
|
+
host: Optional[list[HostSchema]] = None
|
|
308
|
+
|
|
309
|
+
@model_validator(mode="after")
|
|
310
|
+
def _verify_filter_alias(self):
|
|
311
|
+
if self.photometry is not None and self.filter_alias is None:
|
|
312
|
+
raise ValueError("filter_alias is needed if photometry is given!")
|