dkist-processing-trend 0.1.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.
- changelog/.gitempty +0 -0
- dkist_processing_trend/__init__.py +10 -0
- dkist_processing_trend/config.py +11 -0
- dkist_processing_trend/models/__init__.py +1 -0
- dkist_processing_trend/models/constants.py +143 -0
- dkist_processing_trend/models/fit_options.py +15 -0
- dkist_processing_trend/models/fits_access.py +65 -0
- dkist_processing_trend/models/instrument.py +35 -0
- dkist_processing_trend/models/instrument_options.py +35 -0
- dkist_processing_trend/models/parameters.py +134 -0
- dkist_processing_trend/models/tags.py +117 -0
- dkist_processing_trend/models/task_name.py +20 -0
- dkist_processing_trend/parsers/__init__.py +1 -0
- dkist_processing_trend/parsers/arm_id.py +103 -0
- dkist_processing_trend/parsers/instrument_unique_bud.py +40 -0
- dkist_processing_trend/parsers/time.py +27 -0
- dkist_processing_trend/parsers/trend_l0_fits_access.py +123 -0
- dkist_processing_trend/tasks/__init__.py +27 -0
- dkist_processing_trend/tasks/arm_task_factory.py +57 -0
- dkist_processing_trend/tasks/dark.py +56 -0
- dkist_processing_trend/tasks/gain.py +74 -0
- dkist_processing_trend/tasks/initialize_arm_tasks.py +50 -0
- dkist_processing_trend/tasks/parse.py +169 -0
- dkist_processing_trend/tasks/prepare_fit_data_base.py +257 -0
- dkist_processing_trend/tasks/run_pac_fitter.py +392 -0
- dkist_processing_trend/tasks/trend_base.py +97 -0
- dkist_processing_trend/tasks/trend_output_data.py +167 -0
- dkist_processing_trend/tasks/visp/__init__.py +6 -0
- dkist_processing_trend/tasks/visp/visp_dmpd.py +410 -0
- dkist_processing_trend/tasks/visp/visp_extract_beam.py +14 -0
- dkist_processing_trend/tasks/visp/visp_geometric.py +260 -0
- dkist_processing_trend/tasks/visp/visp_prep_fit_data.py +162 -0
- dkist_processing_trend/tasks/visp/visp_process_demod.py +236 -0
- dkist_processing_trend/tasks/write_trend.py +663 -0
- dkist_processing_trend/tests/__init__.py +1 -0
- dkist_processing_trend/tests/conftest.py +718 -0
- dkist_processing_trend/tests/local_trial_workflows/__init__.py +0 -0
- dkist_processing_trend/tests/local_trial_workflows/l0_to_trend_visp_polcal.py +294 -0
- dkist_processing_trend/tests/local_trial_workflows/local_trial_helpers.py +488 -0
- dkist_processing_trend/tests/test_arm_task_factory.py +82 -0
- dkist_processing_trend/tests/test_base_tasks.py +86 -0
- dkist_processing_trend/tests/test_constants.py +120 -0
- dkist_processing_trend/tests/test_dark.py +97 -0
- dkist_processing_trend/tests/test_gain.py +135 -0
- dkist_processing_trend/tests/test_parameters.py +149 -0
- dkist_processing_trend/tests/test_parse.py +276 -0
- dkist_processing_trend/tests/test_prep_fit_data_base.py +233 -0
- dkist_processing_trend/tests/test_publish_catalog_messages.py +45 -0
- dkist_processing_trend/tests/test_run_pac_fitter.py +371 -0
- dkist_processing_trend/tests/test_stems.py +75 -0
- dkist_processing_trend/tests/test_transfer_output_data.py +76 -0
- dkist_processing_trend/tests/test_trend_fits_access.py +173 -0
- dkist_processing_trend/tests/test_visp.py +874 -0
- dkist_processing_trend/tests/test_workflows.py +10 -0
- dkist_processing_trend/tests/test_write_trend.py +460 -0
- dkist_processing_trend/workflows/__init__.py +3 -0
- dkist_processing_trend/workflows/visp.py +58 -0
- dkist_processing_trend-0.1.0.dist-info/METADATA +549 -0
- dkist_processing_trend-0.1.0.dist-info/RECORD +66 -0
- dkist_processing_trend-0.1.0.dist-info/WHEEL +5 -0
- dkist_processing_trend-0.1.0.dist-info/top_level.txt +3 -0
- docs/conf.py +57 -0
- docs/index.rst +10 -0
- docs/l0_to_trend_visp_polcal.rst +4 -0
- docs/landing_page.rst +11 -0
- docs/requirements_table.rst +8 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
"""Task for running processed POLCAL data through the `dkist_processing_pac` fitter."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from dkist_processing_common.codecs.asdf import asdf_decoder
|
|
5
|
+
from dkist_processing_common.codecs.fits import fits_array_encoder
|
|
6
|
+
from dkist_processing_pac.fitter.fitter_parameters import CU_PARAMS
|
|
7
|
+
from dkist_processing_pac.fitter.fitting_core import compare_I
|
|
8
|
+
from dkist_processing_pac.fitter.polcal_fitter import PolcalFitter
|
|
9
|
+
from dkist_processing_pac.input_data.drawer import Drawer
|
|
10
|
+
from dkist_processing_pac.input_data.dresser import Dresser
|
|
11
|
+
from dkist_service_configuration.logging import logger
|
|
12
|
+
|
|
13
|
+
from dkist_processing_trend.models.tags import TrendTag
|
|
14
|
+
from dkist_processing_trend.parsers.trend_l0_fits_access import TrendL0FitsAccess
|
|
15
|
+
from dkist_processing_trend.tasks.trend_base import TrendArmTaskBase
|
|
16
|
+
|
|
17
|
+
__all__ = ["RunPacFitter"]
|
|
18
|
+
|
|
19
|
+
# The are the names of parameters as used by `dkist-processing-pac`, which are slightly different than their "common" names
|
|
20
|
+
ORDERED_PARAMETER_NAMES = [
|
|
21
|
+
"x12",
|
|
22
|
+
"t12",
|
|
23
|
+
"x34",
|
|
24
|
+
"t34",
|
|
25
|
+
"x56",
|
|
26
|
+
"t56",
|
|
27
|
+
"Q_in",
|
|
28
|
+
"U_in",
|
|
29
|
+
"V_in",
|
|
30
|
+
"t_pol",
|
|
31
|
+
"t_ret",
|
|
32
|
+
"ret0r",
|
|
33
|
+
"ret045",
|
|
34
|
+
"ret0h",
|
|
35
|
+
"py",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class RunPacFitter(TrendArmTaskBase):
|
|
40
|
+
"""
|
|
41
|
+
Run prepared POLCAL data through `dkist_processing_pac` to fit the CU parameters and produce demodulation matrices.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
arm_id
|
|
46
|
+
id of the instrument arm to operate on
|
|
47
|
+
|
|
48
|
+
recipe_run_id
|
|
49
|
+
id of the recipe run used to identify the workflow run this task is part of
|
|
50
|
+
|
|
51
|
+
workflow_name
|
|
52
|
+
name of the workflow to which this instance of the task belongs
|
|
53
|
+
|
|
54
|
+
workflow_version
|
|
55
|
+
version of the workflow to which this instance of the task belongs
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
record_provenance = True
|
|
59
|
+
|
|
60
|
+
def run(self):
|
|
61
|
+
"""
|
|
62
|
+
Run all prepared POLCAL datasets through the `~dkist_processing_pac.fitter.polcal_fitter.PolcalFitter` and save the results.
|
|
63
|
+
|
|
64
|
+
Here is where we loop over all PA&C fitting options described in the
|
|
65
|
+
`~dkist_processing_trend.models.parameters.TrendParameters.fit_options_list`. Each run gets its own unique set of
|
|
66
|
+
outputs.
|
|
67
|
+
"""
|
|
68
|
+
num_beams = 1 if self.arm_id == "CI" else 2
|
|
69
|
+
cs_config_saved = False
|
|
70
|
+
|
|
71
|
+
for inst_options in self.parameters.instrument_processing_options:
|
|
72
|
+
inst_option_name = inst_options.name
|
|
73
|
+
for beam in range(1, num_beams + 1):
|
|
74
|
+
logger.info(f"Loading PAC input data for {beam = }")
|
|
75
|
+
global_input_dict = next(
|
|
76
|
+
self.read(
|
|
77
|
+
tags=[
|
|
78
|
+
TrendTag.intermediate(),
|
|
79
|
+
TrendTag.arm_id(self.arm_id),
|
|
80
|
+
TrendTag.instrument_processing_options(inst_option_name),
|
|
81
|
+
TrendTag.task_global_pac_input(),
|
|
82
|
+
TrendTag.beam(beam),
|
|
83
|
+
],
|
|
84
|
+
decoder=asdf_decoder,
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
local_input_dict = next(
|
|
88
|
+
self.read(
|
|
89
|
+
tags=[
|
|
90
|
+
TrendTag.intermediate(),
|
|
91
|
+
TrendTag.arm_id(self.arm_id),
|
|
92
|
+
TrendTag.instrument_processing_options(inst_option_name),
|
|
93
|
+
TrendTag.task_local_pac_input(),
|
|
94
|
+
TrendTag.beam(beam),
|
|
95
|
+
],
|
|
96
|
+
decoder=asdf_decoder,
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if not cs_config_saved:
|
|
101
|
+
logger.info("Writing CS GOS configuration")
|
|
102
|
+
self.write_calibration_sequence(global_input_dict)
|
|
103
|
+
cs_config_saved = True
|
|
104
|
+
|
|
105
|
+
for fit_options in self.parameters.fit_options_list:
|
|
106
|
+
with self.telemetry_span(
|
|
107
|
+
f"Run PAC fit for inst processing option = {inst_option_name}, {beam = }, and fit options = {fit_options.name}"
|
|
108
|
+
):
|
|
109
|
+
logger.info(
|
|
110
|
+
f"Running fits for inst processing option = {inst_option_name}, {beam = }, and fit options = {fit_options.name}"
|
|
111
|
+
)
|
|
112
|
+
global_dresser = self.populate_dresser(
|
|
113
|
+
global_input_dict, remove_I_trend=fit_options.remove_I_trend
|
|
114
|
+
)
|
|
115
|
+
local_dresser = self.populate_dresser(
|
|
116
|
+
local_input_dict, remove_I_trend=fit_options.remove_I_trend
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
pac_fitter = PolcalFitter(
|
|
120
|
+
local_dresser=local_dresser,
|
|
121
|
+
global_dresser=global_dresser,
|
|
122
|
+
fit_mode=fit_options.fit_mode_name,
|
|
123
|
+
init_set=fit_options.init_set_name,
|
|
124
|
+
inherit_global_vary_in_local_fit=True,
|
|
125
|
+
suppress_local_starting_values=True,
|
|
126
|
+
fit_TM=False,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
with self.telemetry_span(
|
|
130
|
+
f"Saving fit results for {beam = } and fit options = {fit_options.name}"
|
|
131
|
+
):
|
|
132
|
+
common_kwargs = {
|
|
133
|
+
"beam": beam,
|
|
134
|
+
"inst_options_name": inst_option_name,
|
|
135
|
+
"fit_options_name": fit_options.name,
|
|
136
|
+
}
|
|
137
|
+
logger.info("Writing best-fit parameters")
|
|
138
|
+
self.write_best_fit_parameters(pac_fitter, **common_kwargs)
|
|
139
|
+
|
|
140
|
+
logger.info("Writing best-fit demodulation matrices")
|
|
141
|
+
self.write_best_fit_demodulation_matrices(pac_fitter, **common_kwargs)
|
|
142
|
+
|
|
143
|
+
logger.info("Writing best-fit flux and residuals")
|
|
144
|
+
self.write_best_fit_flux_and_residuals(pac_fitter, **common_kwargs)
|
|
145
|
+
|
|
146
|
+
def populate_dresser(
|
|
147
|
+
self,
|
|
148
|
+
pac_input_dict: dict[int, list[dict[str, dict | np.ndarray]]],
|
|
149
|
+
remove_I_trend: bool,
|
|
150
|
+
skip_darks: bool = True,
|
|
151
|
+
) -> Dresser:
|
|
152
|
+
"""
|
|
153
|
+
Convert a raw set of dicts that live on scratch as an ASDF file into a `~dkist_processing_pac.input_data.dresser.Dresser`.
|
|
154
|
+
|
|
155
|
+
`Drawers <dkist_processing_pac.input_data.drawer.Drawer>` expect to be instantiated with a dict containing lists
|
|
156
|
+
of `~dkist_processing_trend.parsers.trend_l0_fits_access.TrendL0FitsAccess` objects but we store these
|
|
157
|
+
"pac input data" as lists of dicts containing "header" and "data" keys. So the first step is to convert these
|
|
158
|
+
raw dictionaries into `~dkist_processing_trend.parsers.trend_l0_fits_access.TrendL0FitsAccess` objects, then we
|
|
159
|
+
can use them to instantiate `Drawers <dkist_processing_pac.input_data.drawer.Drawer>` and finally a
|
|
160
|
+
`~dkist_processing_pac.input_data.dresser.Dresser`.
|
|
161
|
+
"""
|
|
162
|
+
dresser = Dresser()
|
|
163
|
+
fits_access_dict = dict()
|
|
164
|
+
|
|
165
|
+
for cs_step in range(self.constants.num_cs_steps):
|
|
166
|
+
cs_step_dict_list = pac_input_dict[cs_step]
|
|
167
|
+
cs_step_fits_access_list = []
|
|
168
|
+
|
|
169
|
+
for cs_step_dict in cs_step_dict_list:
|
|
170
|
+
fits_access_obj = TrendL0FitsAccess(
|
|
171
|
+
header=cs_step_dict["header"], data=cs_step_dict["data"]
|
|
172
|
+
)
|
|
173
|
+
cs_step_fits_access_list.append(fits_access_obj)
|
|
174
|
+
|
|
175
|
+
fits_access_dict[cs_step] = cs_step_fits_access_list
|
|
176
|
+
|
|
177
|
+
dresser.add_drawer(
|
|
178
|
+
Drawer(fits_access_dict, remove_I_trend=remove_I_trend, skip_darks=skip_darks)
|
|
179
|
+
)
|
|
180
|
+
return dresser
|
|
181
|
+
|
|
182
|
+
def write_calibration_sequence(
|
|
183
|
+
self, pac_input_dict: dict[int, list[dict[str, dict | np.ndarray]]]
|
|
184
|
+
) -> None:
|
|
185
|
+
"""
|
|
186
|
+
Write the Calibration Sequence configuration to scratch.
|
|
187
|
+
|
|
188
|
+
The result has shape ``(K, 8, N)``, where ``K`` is the number of polcal OPs/calibration sequences, and ``N`` is
|
|
189
|
+
the number of steps per OP/sequence. The length-8 dimension corresponds to the 8 optical parameters that describe the
|
|
190
|
+
calibration sequence.
|
|
191
|
+
|
|
192
|
+
If the different polcal OPs have a different number of steps then ``N`` is the length of the longest OP and the data
|
|
193
|
+
for shorter OPs is padded with NaNs.
|
|
194
|
+
"""
|
|
195
|
+
dresser = self.populate_dresser(pac_input_dict, remove_I_trend=False, skip_darks=False)
|
|
196
|
+
|
|
197
|
+
longest_cs_num_steps = max(dresser.drawer_step_list)
|
|
198
|
+
full_data = np.empty((dresser.numdrawers, 8, longest_cs_num_steps))
|
|
199
|
+
|
|
200
|
+
for cs_num, drawer in enumerate(dresser.drawers):
|
|
201
|
+
drawer_data_stack = np.vstack(
|
|
202
|
+
[
|
|
203
|
+
drawer.pol_in,
|
|
204
|
+
drawer.theta_pol_steps,
|
|
205
|
+
drawer.ret_in,
|
|
206
|
+
drawer.theta_ret_steps,
|
|
207
|
+
drawer.dark_in,
|
|
208
|
+
drawer.azimuth,
|
|
209
|
+
drawer.elevation,
|
|
210
|
+
drawer.table_angle,
|
|
211
|
+
]
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
if step_deficit := drawer.numsteps - longest_cs_num_steps > 0:
|
|
215
|
+
drawer_data_stack = np.pad(
|
|
216
|
+
drawer_data_stack, ((0, 0), (0, step_deficit)), constant_values=np.nan
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
full_data[cs_num, :, :] = drawer_data_stack
|
|
220
|
+
|
|
221
|
+
self.write(
|
|
222
|
+
data=full_data,
|
|
223
|
+
tags=[
|
|
224
|
+
TrendTag.intermediate(),
|
|
225
|
+
TrendTag.arm_id(self.arm_id),
|
|
226
|
+
TrendTag.task_calibration_sequence(),
|
|
227
|
+
],
|
|
228
|
+
encoder=fits_array_encoder,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
def write_best_fit_parameters(
|
|
232
|
+
self, fitter: PolcalFitter, beam: int, inst_options_name: str, fit_options_name: str
|
|
233
|
+
):
|
|
234
|
+
"""
|
|
235
|
+
Collect ALL best-fit parameter values (including those fixed in the fit) and write to a single array.
|
|
236
|
+
|
|
237
|
+
The full output shape is ``(*FOV_shape, K, 15 + N, 3)``, where FOV_shape is the shape of the input POLCAL data,
|
|
238
|
+
K is the number of polcal OPs/calibration sequences, and N is the number of steps in each sequence. The last
|
|
239
|
+
dimension contains the best-fit value in its 0th index, the initial value at index 1, and whether that parameter
|
|
240
|
+
varied in the fit at index 2. Note that many parameters are held to be the same across polcal OPs; these
|
|
241
|
+
parameters will have the same values for all slices of the "K" index.
|
|
242
|
+
|
|
243
|
+
If the different polcal OPs have a different number of steps then N is the length of the longest OP and the data
|
|
244
|
+
for shorter OPs is padded with NaNs.
|
|
245
|
+
"""
|
|
246
|
+
fov_shape = fitter.local_objects.dresser.shape
|
|
247
|
+
num_cs = fitter.local_objects.dresser.numdrawers
|
|
248
|
+
steps_per_cs = fitter.local_objects.dresser.drawer_step_list
|
|
249
|
+
longest_cs_num_steps = max(steps_per_cs)
|
|
250
|
+
init_parameters = fitter.local_objects.init_parameters
|
|
251
|
+
fit_parameters = fitter.fit_parameters
|
|
252
|
+
|
|
253
|
+
full_output = np.empty(fov_shape + (num_cs, 15 + longest_cs_num_steps, 3))
|
|
254
|
+
|
|
255
|
+
num_points = np.prod(fov_shape)
|
|
256
|
+
for point in range(num_points):
|
|
257
|
+
idx = np.unravel_index(point, fov_shape)
|
|
258
|
+
point_parameters = fit_parameters[idx]
|
|
259
|
+
init_point_parameters = init_parameters[idx]
|
|
260
|
+
|
|
261
|
+
for cs in range(num_cs):
|
|
262
|
+
cs_data = np.empty((15 + steps_per_cs[cs], 3))
|
|
263
|
+
|
|
264
|
+
# These are all parameters except for the I_sys params, which we'll add in a moment
|
|
265
|
+
for p, param_name in enumerate(ORDERED_PARAMETER_NAMES):
|
|
266
|
+
|
|
267
|
+
if param_name in CU_PARAMS:
|
|
268
|
+
param_name = f"{param_name}_CS{cs:02n}"
|
|
269
|
+
|
|
270
|
+
if param_name == "py":
|
|
271
|
+
value = fitter.global_objects.calibration_unit.py
|
|
272
|
+
vary = False
|
|
273
|
+
init_value = value
|
|
274
|
+
else:
|
|
275
|
+
parameter = point_parameters[param_name]
|
|
276
|
+
value = parameter.value
|
|
277
|
+
vary = parameter.vary
|
|
278
|
+
init_value = init_point_parameters[param_name].value
|
|
279
|
+
|
|
280
|
+
cs_data[p] = np.array([value, init_value, vary])
|
|
281
|
+
|
|
282
|
+
# Add I_sys parameters, the number of which depends on the number of *total* CS steps (across all
|
|
283
|
+
# calibration sequences).
|
|
284
|
+
for p, s in enumerate(range(steps_per_cs[cs]), start=15):
|
|
285
|
+
param_name = f"I_sys_CS{cs:02n}_step{s:02n}"
|
|
286
|
+
parameter = point_parameters[param_name]
|
|
287
|
+
value = parameter.value
|
|
288
|
+
vary = parameter.vary
|
|
289
|
+
init_value = init_point_parameters[param_name].value
|
|
290
|
+
cs_data[p] = np.array([value, init_value, vary])
|
|
291
|
+
|
|
292
|
+
if step_deficit := steps_per_cs[cs] - longest_cs_num_steps > 0:
|
|
293
|
+
cs_data = np.pad(cs_data, ((0, step_deficit), (0, 0)), constant_values=np.nan)
|
|
294
|
+
|
|
295
|
+
full_output[*idx, cs] = cs_data
|
|
296
|
+
|
|
297
|
+
self.write(
|
|
298
|
+
data=full_output,
|
|
299
|
+
tags=[
|
|
300
|
+
TrendTag.intermediate(),
|
|
301
|
+
TrendTag.arm_id(self.arm_id),
|
|
302
|
+
TrendTag.beam(beam),
|
|
303
|
+
TrendTag.instrument_processing_options(inst_options_name),
|
|
304
|
+
TrendTag.pac_fit_options(fit_options_name),
|
|
305
|
+
TrendTag.task_best_fit_parameters(),
|
|
306
|
+
],
|
|
307
|
+
encoder=fits_array_encoder,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
def write_best_fit_demodulation_matrices(
|
|
311
|
+
self, fitter: PolcalFitter, beam: int, inst_options_name: str, fit_options_name: str
|
|
312
|
+
):
|
|
313
|
+
"""
|
|
314
|
+
Write the best-fit demodulation matrices to scratch.
|
|
315
|
+
|
|
316
|
+
The shape will be ``(*FOV_shape, 4, M)``, where FOV_shape is the shape of the input data and M is the number of
|
|
317
|
+
modulation states.
|
|
318
|
+
"""
|
|
319
|
+
demod_matrices = fitter.demodulation_matrices
|
|
320
|
+
|
|
321
|
+
self.write(
|
|
322
|
+
data=demod_matrices,
|
|
323
|
+
tags=[
|
|
324
|
+
TrendTag.intermediate(),
|
|
325
|
+
TrendTag.arm_id(self.arm_id),
|
|
326
|
+
TrendTag.beam(beam),
|
|
327
|
+
TrendTag.instrument_processing_options(inst_options_name),
|
|
328
|
+
TrendTag.pac_fit_options(fit_options_name),
|
|
329
|
+
TrendTag.task_best_fit_demodulation_matrices(),
|
|
330
|
+
],
|
|
331
|
+
encoder=fits_array_encoder,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
def write_best_fit_flux_and_residuals(
|
|
335
|
+
self, fitter: PolcalFitter, beam: int, inst_options_name: str, fit_options_name: str
|
|
336
|
+
):
|
|
337
|
+
"""Compute the best-fit flux and fit residuals and write them to scratch."""
|
|
338
|
+
fit_container = fitter.local_objects
|
|
339
|
+
TM = fit_container.telescope
|
|
340
|
+
CM = fit_container.calibration_unit
|
|
341
|
+
fov_shape = fit_container.dresser.shape
|
|
342
|
+
num_mod = fit_container.dresser.nummod
|
|
343
|
+
num_steps = fit_container.dresser.numsteps
|
|
344
|
+
num_points = np.prod(fov_shape)
|
|
345
|
+
|
|
346
|
+
flux_array = np.zeros(fov_shape + (num_mod, num_steps))
|
|
347
|
+
residual_array = np.zeros_like(flux_array)
|
|
348
|
+
|
|
349
|
+
for i in range(num_points):
|
|
350
|
+
idx = np.unravel_index(i, fov_shape)
|
|
351
|
+
I_cal, I_unc = fit_container.dresser[idx]
|
|
352
|
+
fit_params = fit_container.fit_parameters[idx]
|
|
353
|
+
modmat = np.zeros((I_cal.shape[0], 4), dtype=np.float64)
|
|
354
|
+
flat_residual = compare_I(
|
|
355
|
+
params=fit_params,
|
|
356
|
+
I_cal=I_cal,
|
|
357
|
+
I_unc=I_unc,
|
|
358
|
+
TM=TM,
|
|
359
|
+
CM=CM,
|
|
360
|
+
modmat=modmat,
|
|
361
|
+
use_M12=True,
|
|
362
|
+
)
|
|
363
|
+
diff = np.reshape(flat_residual, (num_mod, num_steps))
|
|
364
|
+
residual_array[*idx, :, :] = diff
|
|
365
|
+
|
|
366
|
+
flux = diff * I_unc + I_cal
|
|
367
|
+
flux_array[*idx, :, :] = flux
|
|
368
|
+
|
|
369
|
+
self.write(
|
|
370
|
+
data=flux_array,
|
|
371
|
+
tags=[
|
|
372
|
+
TrendTag.intermediate(),
|
|
373
|
+
TrendTag.arm_id(self.arm_id),
|
|
374
|
+
TrendTag.beam(beam),
|
|
375
|
+
TrendTag.instrument_processing_options(inst_options_name),
|
|
376
|
+
TrendTag.pac_fit_options(fit_options_name),
|
|
377
|
+
TrendTag.task_best_fit_flux(),
|
|
378
|
+
],
|
|
379
|
+
encoder=fits_array_encoder,
|
|
380
|
+
)
|
|
381
|
+
self.write(
|
|
382
|
+
data=residual_array,
|
|
383
|
+
tags=[
|
|
384
|
+
TrendTag.intermediate(),
|
|
385
|
+
TrendTag.arm_id(self.arm_id),
|
|
386
|
+
TrendTag.beam(beam),
|
|
387
|
+
TrendTag.instrument_processing_options(inst_options_name),
|
|
388
|
+
TrendTag.pac_fit_options(fit_options_name),
|
|
389
|
+
TrendTag.task_fit_residuals(),
|
|
390
|
+
],
|
|
391
|
+
encoder=fits_array_encoder,
|
|
392
|
+
)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Base classes for all Trend science tasks."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC
|
|
4
|
+
|
|
5
|
+
from dkist_processing_common.tasks.base import WorkflowTaskBase
|
|
6
|
+
from dkist_service_configuration.logging import logger
|
|
7
|
+
|
|
8
|
+
from dkist_processing_trend.models.constants import TrendConstants
|
|
9
|
+
from dkist_processing_trend.models.parameters import TrendParameters
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TrendTaskBase(
|
|
13
|
+
WorkflowTaskBase,
|
|
14
|
+
ABC,
|
|
15
|
+
):
|
|
16
|
+
"""
|
|
17
|
+
Task class for base Trend tasks.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
recipe_run_id
|
|
22
|
+
id of the recipe run used to identify the workflow run this task is part of
|
|
23
|
+
|
|
24
|
+
workflow_name
|
|
25
|
+
name of the workflow to which this instance of the task belongs
|
|
26
|
+
|
|
27
|
+
workflow_version
|
|
28
|
+
version of the workflow to which this instance of the task belongs
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
# So tab completion shows all the constants
|
|
32
|
+
constants: TrendConstants
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def constants_model_class(self):
|
|
36
|
+
"""Get Trend pipeline constants."""
|
|
37
|
+
return TrendConstants
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
recipe_run_id: int,
|
|
42
|
+
workflow_name: str,
|
|
43
|
+
workflow_version: str,
|
|
44
|
+
):
|
|
45
|
+
super().__init__(
|
|
46
|
+
recipe_run_id=recipe_run_id,
|
|
47
|
+
workflow_name=workflow_name,
|
|
48
|
+
workflow_version=workflow_version,
|
|
49
|
+
)
|
|
50
|
+
self.parameters = TrendParameters(
|
|
51
|
+
scratch=self.scratch,
|
|
52
|
+
obs_ip_start_time=self.constants.earliest_ip_start_time,
|
|
53
|
+
instrument=self.constants.instrument,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TrendArmTaskBase(TrendTaskBase, ABC):
|
|
58
|
+
"""
|
|
59
|
+
Task class base for Trend tasks that operate on a single instrument arm.
|
|
60
|
+
|
|
61
|
+
Similar to `TrendTaskBase` except it exposes the `self.arm_id` parameter that returns the arm ID this class was
|
|
62
|
+
instantiated with.
|
|
63
|
+
|
|
64
|
+
Another important feature of this class is that the `run` method will NOT be called if `self.arm_id <arm_id>` is not found
|
|
65
|
+
in `self.constants.arm_id_list <dkist_processing_trend.models.constants.TrendConstants.arm_id_list>` (because data for this arm doesn't exist in the input dataset).
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
arm_id
|
|
70
|
+
id of the arm that this task will operate on
|
|
71
|
+
|
|
72
|
+
recipe_run_id
|
|
73
|
+
id of the recipe run used to identify the workflow run this task is part of
|
|
74
|
+
|
|
75
|
+
workflow_name
|
|
76
|
+
name of the workflow to which this instance of the task belongs
|
|
77
|
+
|
|
78
|
+
workflow_version
|
|
79
|
+
version of the workflow to which this instance of the task belongs
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self, arm_id: str | int, recipe_run_id: int, workflow_name: str, workflow_version: str
|
|
84
|
+
):
|
|
85
|
+
super().__init__(
|
|
86
|
+
recipe_run_id=recipe_run_id,
|
|
87
|
+
workflow_name=workflow_name,
|
|
88
|
+
workflow_version=workflow_version,
|
|
89
|
+
)
|
|
90
|
+
self.arm_id = arm_id
|
|
91
|
+
|
|
92
|
+
def pre_run(self) -> None:
|
|
93
|
+
"""Check if `self.arm_id <arm_id>` is in the list of arms present in the dataset and run `run` if so."""
|
|
94
|
+
super().pre_run()
|
|
95
|
+
if self.arm_id not in self.constants.arm_id_list:
|
|
96
|
+
logger.info(f"This dataset has no data for arm {self.arm_id}. Nothing to do.")
|
|
97
|
+
self.run = lambda: None
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Task for transferring trend data from scratch to datacenter stores."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Iterable
|
|
6
|
+
|
|
7
|
+
from dkist_processing_common.models.message import CatalogFrameMessage
|
|
8
|
+
from dkist_processing_common.models.message import CatalogFrameMessageBody
|
|
9
|
+
from dkist_processing_common.tasks.mixin.globus import GlobusMixin
|
|
10
|
+
from dkist_processing_common.tasks.mixin.interservice_bus import InterserviceBusMixin
|
|
11
|
+
from dkist_processing_common.tasks.output_data_base import OutputDataBase
|
|
12
|
+
from dkist_processing_common.tasks.output_data_base import TransferDataBase
|
|
13
|
+
from dkist_service_configuration.logging import logger
|
|
14
|
+
|
|
15
|
+
from dkist_processing_trend.models.constants import TrendConstants
|
|
16
|
+
|
|
17
|
+
__all__ = ["TransferTrendData", "PublishTrendCatalogMessages"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TrendOutputDataBase(OutputDataBase):
|
|
21
|
+
"""
|
|
22
|
+
Base class that defines the destination folders for trend data.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
recipe_run_id
|
|
27
|
+
id of the recipe run used to identify the workflow run this task is part of
|
|
28
|
+
|
|
29
|
+
workflow_name
|
|
30
|
+
name of the workflow to which this instance of the task belongs
|
|
31
|
+
|
|
32
|
+
workflow_version
|
|
33
|
+
version of the workflow to which this instance of the task belongs
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
# So tab completion shows all the constants
|
|
37
|
+
constants: TrendConstants
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def constants_model_class(self):
|
|
41
|
+
"""Define the constants class used to access the constants db."""
|
|
42
|
+
return TrendConstants
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def destination_root_folder(self) -> Path:
|
|
46
|
+
"""
|
|
47
|
+
Define the root folder.
|
|
48
|
+
|
|
49
|
+
"trend/polcal"
|
|
50
|
+
"""
|
|
51
|
+
return Path("trend") / "polcal"
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def destination_folder(self) -> Path:
|
|
55
|
+
"""
|
|
56
|
+
Define the destination folder for this trend run.
|
|
57
|
+
|
|
58
|
+
"trend/polcal/{EARLIEST_IP_START_TIME}/{INSTRUMENT}"
|
|
59
|
+
"""
|
|
60
|
+
# E.g., 1999-01-02T12:34:56.12352 -> 19990102T123456
|
|
61
|
+
formatted_date = datetime.fromisoformat(self.constants.earliest_ip_start_time).strftime(
|
|
62
|
+
"%Y%m%dT%H%M%S"
|
|
63
|
+
)
|
|
64
|
+
return self.destination_root_folder / formatted_date / self.constants.instrument
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TransferTrendData(TrendOutputDataBase, TransferDataBase, GlobusMixin):
|
|
68
|
+
"""
|
|
69
|
+
Task class for transferring processed trend data to the object store.
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
recipe_run_id
|
|
74
|
+
id of the recipe run used to identify the workflow run this task is part of
|
|
75
|
+
|
|
76
|
+
workflow_name
|
|
77
|
+
name of the workflow to which this instance of the task belongs
|
|
78
|
+
|
|
79
|
+
workflow_version
|
|
80
|
+
version of the workflow to which this instance of the task belongs
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def transfer_objects(self):
|
|
84
|
+
"""Transfer output frames."""
|
|
85
|
+
with self.telemetry_span("Upload output frames"):
|
|
86
|
+
self.transfer_output_frames()
|
|
87
|
+
|
|
88
|
+
def transfer_output_frames(self):
|
|
89
|
+
"""Create a Globus transfer for all output data, as well as any available dataset extras."""
|
|
90
|
+
output_transfer_items = self.build_output_frame_transfer_list()
|
|
91
|
+
|
|
92
|
+
logger.info(
|
|
93
|
+
f"Preparing globus transfer {len(output_transfer_items)} items. "
|
|
94
|
+
f"recipe_run_id={self.recipe_run_id}. "
|
|
95
|
+
f"transfer_items={output_transfer_items[:3]}..."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
self.globus_transfer_scratch_to_object_store(
|
|
99
|
+
transfer_items=output_transfer_items,
|
|
100
|
+
label=f"Transfer trend output frames for recipe_run_id {self.recipe_run_id}",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class PublishTrendCatalogMessages(TrendOutputDataBase, InterserviceBusMixin):
|
|
105
|
+
"""
|
|
106
|
+
Task class for publishing catalog messages related to the frames transferred to the object store.
|
|
107
|
+
|
|
108
|
+
Parameters
|
|
109
|
+
----------
|
|
110
|
+
recipe_run_id
|
|
111
|
+
id of the recipe run used to identify the workflow run this task is part of
|
|
112
|
+
|
|
113
|
+
workflow_name
|
|
114
|
+
name of the workflow to which this instance of the task belongs
|
|
115
|
+
|
|
116
|
+
workflow_version
|
|
117
|
+
version of the workflow to which this instance of the task belongs
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
def run(self) -> None:
|
|
121
|
+
"""Run method for this task."""
|
|
122
|
+
with self.telemetry_span("Gather output data"):
|
|
123
|
+
frames = self.read(tags=self.output_frame_tags)
|
|
124
|
+
|
|
125
|
+
with self.telemetry_span("Create message objects"):
|
|
126
|
+
messages = self.frame_messages(paths=frames)
|
|
127
|
+
frame_message_count = len(messages)
|
|
128
|
+
|
|
129
|
+
with self.telemetry_span(f"Publish messages: {frame_message_count = }"):
|
|
130
|
+
self.interservice_bus_publish(messages=messages)
|
|
131
|
+
|
|
132
|
+
def frame_messages(self, paths: Iterable[Path]) -> list[CatalogFrameMessage]:
|
|
133
|
+
"""
|
|
134
|
+
Create the frame messages.
|
|
135
|
+
|
|
136
|
+
Parameters
|
|
137
|
+
----------
|
|
138
|
+
paths
|
|
139
|
+
The input paths for which to publish frame messages
|
|
140
|
+
folder_modifier
|
|
141
|
+
A subdirectory to use if the files in paths are not in the base directory
|
|
142
|
+
|
|
143
|
+
Returns
|
|
144
|
+
-------
|
|
145
|
+
A list of frame messages
|
|
146
|
+
"""
|
|
147
|
+
message_bodies = [
|
|
148
|
+
CatalogFrameMessageBody(
|
|
149
|
+
objectName=self.format_object_key(path=p),
|
|
150
|
+
conversationId=str(self.recipe_run_id),
|
|
151
|
+
bucket=self.destination_bucket,
|
|
152
|
+
)
|
|
153
|
+
for p in paths
|
|
154
|
+
]
|
|
155
|
+
messages = [CatalogFrameMessage(body=body) for body in message_bodies]
|
|
156
|
+
return messages
|
|
157
|
+
|
|
158
|
+
def rollback(self):
|
|
159
|
+
"""
|
|
160
|
+
Warn that the metadata-store and the interservice bus retain the effect of this tasks execution.
|
|
161
|
+
|
|
162
|
+
Rolling back this task may not be achievable without other action.
|
|
163
|
+
"""
|
|
164
|
+
super().rollback()
|
|
165
|
+
logger.warning(
|
|
166
|
+
f"Modifications to the metadata store and the interservice bus were not rolled back."
|
|
167
|
+
)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Package for tasks specific to ViSP data."""
|
|
2
|
+
|
|
3
|
+
from dkist_processing_trend.tasks.visp.visp_dmpd import *
|
|
4
|
+
from dkist_processing_trend.tasks.visp.visp_geometric import *
|
|
5
|
+
from dkist_processing_trend.tasks.visp.visp_prep_fit_data import *
|
|
6
|
+
from dkist_processing_trend.tasks.visp.visp_process_demod import *
|