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,663 @@
|
|
|
1
|
+
"""Task class for writing trend output files."""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import Type
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from astropy.io import fits
|
|
8
|
+
from astropy.time import Time
|
|
9
|
+
from dkist_fits_specifications import __version__ as spec_version
|
|
10
|
+
from dkist_fits_specifications.utils.formatter import reformat_trend_header
|
|
11
|
+
from dkist_processing_common.codecs.fits import fits_array_decoder
|
|
12
|
+
from dkist_processing_common.codecs.fits import fits_hdulist_encoder
|
|
13
|
+
from dkist_processing_common.tasks.write_l1_base import WriteL1Base
|
|
14
|
+
from dkist_processing_pac.fitter.fitter_parameters import PolcalDresserParameters
|
|
15
|
+
|
|
16
|
+
from dkist_processing_trend.models.constants import TrendConstants
|
|
17
|
+
from dkist_processing_trend.models.fit_options import FitOptions
|
|
18
|
+
from dkist_processing_trend.models.instrument import InstrumentName
|
|
19
|
+
from dkist_processing_trend.models.instrument_options import CryonirspInstrumentOptions
|
|
20
|
+
from dkist_processing_trend.models.instrument_options import DlnirspInstrumentOptions
|
|
21
|
+
from dkist_processing_trend.models.instrument_options import InstrumentOptionsBase
|
|
22
|
+
from dkist_processing_trend.models.instrument_options import VispInstrumentOptions
|
|
23
|
+
from dkist_processing_trend.models.parameters import TrendParameters
|
|
24
|
+
from dkist_processing_trend.models.tags import TrendTag
|
|
25
|
+
|
|
26
|
+
__all__ = ["WriteTrendOutputs"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class WriteTrendOutputs(WriteL1Base):
|
|
30
|
+
"""Task for writing trend output files.
|
|
31
|
+
|
|
32
|
+
Parameters
|
|
33
|
+
----------
|
|
34
|
+
recipe_run_id
|
|
35
|
+
id of the recipe run used to identify the workflow run this task is part of
|
|
36
|
+
|
|
37
|
+
workflow_name
|
|
38
|
+
name of the workflow to which this instance of the task belongs
|
|
39
|
+
|
|
40
|
+
workflow_version
|
|
41
|
+
version of the workflow to which this instance of the task belongs
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
constants: TrendConstants
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def constants_model_class(self) -> Type[TrendConstants]:
|
|
48
|
+
"""Return the constants model class used by this class."""
|
|
49
|
+
return TrendConstants
|
|
50
|
+
|
|
51
|
+
def __init__(self, recipe_run_id: int, workflow_name: str, workflow_version: str):
|
|
52
|
+
super().__init__(
|
|
53
|
+
recipe_run_id=recipe_run_id,
|
|
54
|
+
workflow_name=workflow_name,
|
|
55
|
+
workflow_version=workflow_version,
|
|
56
|
+
)
|
|
57
|
+
self.parameters = TrendParameters(
|
|
58
|
+
scratch=self.scratch,
|
|
59
|
+
obs_ip_start_time=self.constants.earliest_ip_start_time,
|
|
60
|
+
instrument=self.constants.instrument,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def run(self):
|
|
64
|
+
"""Produce a single trend output file for each combination of arm ID, pac fit options, and instrument fit options."""
|
|
65
|
+
for arm_id in self.constants.arm_id_list:
|
|
66
|
+
# HDUs that are the same for all instrument/fit options
|
|
67
|
+
calibration_sequence_hdu = self.generate_calibration_sequence_hdu(arm_id=arm_id)
|
|
68
|
+
|
|
69
|
+
for fit_options in self.parameters.fit_options_list:
|
|
70
|
+
fit_info_hdu = self.generate_fit_info_hdu(fit_options)
|
|
71
|
+
for inst_options in self.parameters.instrument_processing_options:
|
|
72
|
+
hdu_list = fits.HDUList([fits.PrimaryHDU()])
|
|
73
|
+
|
|
74
|
+
hdu_list.append(
|
|
75
|
+
self.generate_inventory_hdu(
|
|
76
|
+
arm_id=arm_id, fit_options=fit_options, instrument_options=inst_options
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
hdu_list.append(calibration_sequence_hdu)
|
|
81
|
+
hdu_list.append(fit_info_hdu)
|
|
82
|
+
|
|
83
|
+
common_args = {
|
|
84
|
+
"arm_id": arm_id,
|
|
85
|
+
"fit_options": fit_options,
|
|
86
|
+
"instrument_options": inst_options,
|
|
87
|
+
}
|
|
88
|
+
hdu_list.append(self.generate_fit_parameters_hdu(**common_args))
|
|
89
|
+
hdu_list.append(self.generate_modulation_matrices_hdu(**common_args))
|
|
90
|
+
hdu_list.append(self.generate_best_fit_flux_hdu(**common_args))
|
|
91
|
+
hdu_list.append(self.generate_fit_residuals_hdu(**common_args))
|
|
92
|
+
if len(self.beam_list(arm_id)) == 2:
|
|
93
|
+
hdu_list.append(self.generate_single_beam_DMPD_hdu(beam=1, **common_args))
|
|
94
|
+
hdu_list.append(self.generate_single_beam_DMPD_hdu(beam=2, **common_args))
|
|
95
|
+
hdu_list.append(self.generate_DMPD_hdu(**common_args))
|
|
96
|
+
|
|
97
|
+
self.write_trend_output(hdu_list, arm_id, fit_options, inst_options)
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def beam_list(arm_id: str | int) -> list[int]:
|
|
101
|
+
"""Return the list of beams the need to be iterated over for a given arm id."""
|
|
102
|
+
if arm_id == "CI":
|
|
103
|
+
return [1]
|
|
104
|
+
return [1, 2]
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def convert_float64_to_float32(input_array: np.ndarray) -> np.ndarray:
|
|
108
|
+
"""
|
|
109
|
+
Convert an input `np.float64` array to `np.float32`.
|
|
110
|
+
|
|
111
|
+
Any other array type is returned unchanged.
|
|
112
|
+
"""
|
|
113
|
+
if np.issubdtype(input_array.dtype, np.float64):
|
|
114
|
+
return input_array.astype(np.float32, casting="same_kind")
|
|
115
|
+
return input_array
|
|
116
|
+
|
|
117
|
+
def build_hdu(
|
|
118
|
+
self, header: fits.Header, data: np.ndarray | None = None
|
|
119
|
+
) -> fits.ImageHDU | fits.CompImageHDU:
|
|
120
|
+
"""
|
|
121
|
+
Construct an HDU from input header and data objects.
|
|
122
|
+
|
|
123
|
+
This method also formats the header to be nice and readable.
|
|
124
|
+
|
|
125
|
+
If ``data`` is not `None` then the returned HDU will be a `~astropy.io.fits.CompImageHDU`. Header-only HDUs are
|
|
126
|
+
returned as `~astropy.io.fits.ImageHDU` objects.
|
|
127
|
+
"""
|
|
128
|
+
if data is None:
|
|
129
|
+
hdu_cls = fits.ImageHDU
|
|
130
|
+
extra_args = dict()
|
|
131
|
+
else:
|
|
132
|
+
data = self.convert_float64_to_float32(data)
|
|
133
|
+
hdu_cls = fits.CompImageHDU
|
|
134
|
+
extra_args = {"tile_shape": self.compute_tile_size_for_array(data)}
|
|
135
|
+
|
|
136
|
+
hdu = hdu_cls(header=header, data=data, **extra_args)
|
|
137
|
+
formatted_header = reformat_trend_header(hdu.header)
|
|
138
|
+
hdu = hdu_cls(header=formatted_header, data=data, **extra_args)
|
|
139
|
+
|
|
140
|
+
return hdu
|
|
141
|
+
|
|
142
|
+
def trend_filename(
|
|
143
|
+
self, arm_id: int | str, fit_options: FitOptions, instrument_options: InstrumentOptionsBase
|
|
144
|
+
) -> None:
|
|
145
|
+
"""
|
|
146
|
+
Construct a single Trend output file name.
|
|
147
|
+
|
|
148
|
+
The format is:
|
|
149
|
+
|
|
150
|
+
trend_polcal_DATE_INSTRUMENT_ARM_FITOPTION-INSTRUMENTOPTION_DATASETID.fits
|
|
151
|
+
|
|
152
|
+
Example
|
|
153
|
+
-------
|
|
154
|
+
"trend_polcal_2020-03-14T12_34_56_DLNIRSP_JBand_tpol_free-no_smooth_ADJFSI.fits"
|
|
155
|
+
"""
|
|
156
|
+
instrument = self.constants.instrument
|
|
157
|
+
date_beg = (
|
|
158
|
+
self.constants.earliest_ip_start_time.replace("-", "_")
|
|
159
|
+
.replace(":", "_")
|
|
160
|
+
.replace(".", "_")
|
|
161
|
+
)
|
|
162
|
+
options_str = self.compute_full_fit_opt_id(
|
|
163
|
+
fit_options=fit_options, instrument_options=instrument_options
|
|
164
|
+
)
|
|
165
|
+
return f"trend_polcal_{date_beg}_{instrument}_{arm_id}_{options_str}_{self.constants.dataset_id}.fits"
|
|
166
|
+
|
|
167
|
+
@staticmethod
|
|
168
|
+
def compute_full_fit_opt_id(
|
|
169
|
+
fit_options: FitOptions, instrument_options: InstrumentOptionsBase
|
|
170
|
+
) -> str:
|
|
171
|
+
"""Concatenate the 'names' of both the pac-fit and instrument-processing options."""
|
|
172
|
+
return f"{fit_options.name}-{instrument_options.name}"
|
|
173
|
+
|
|
174
|
+
def update_framevol(self, relative_path: str) -> None:
|
|
175
|
+
"""
|
|
176
|
+
Update the second HDU's (Inventory) FRAMEVOL key to be exactly the size of the file on-disk.
|
|
177
|
+
|
|
178
|
+
This is different than the L1 `update_framevol` method because we only update a single HDU (the Inventory HDU).
|
|
179
|
+
"""
|
|
180
|
+
full_path = self.scratch.workflow_base_path / relative_path
|
|
181
|
+
compressed_size = full_path.stat().st_size / 1024 / 1024
|
|
182
|
+
hdul = fits.open(full_path, mode="update", disable_image_compression=True)
|
|
183
|
+
hdul[1].header["FRAMEVOL"] = compressed_size
|
|
184
|
+
hdul.flush()
|
|
185
|
+
del hdul
|
|
186
|
+
|
|
187
|
+
def write_trend_output(
|
|
188
|
+
self,
|
|
189
|
+
hdu_list: fits.HDUList,
|
|
190
|
+
arm_id: int | str,
|
|
191
|
+
fit_options: FitOptions,
|
|
192
|
+
instrument_options: InstrumentOptionsBase,
|
|
193
|
+
) -> None:
|
|
194
|
+
"""Write the given HDUList to scratch, tag with FRAME and OUTPUT, and update the FRAMEVOL header key."""
|
|
195
|
+
relative_path = self.trend_filename(
|
|
196
|
+
arm_id=arm_id, fit_options=fit_options, instrument_options=instrument_options
|
|
197
|
+
)
|
|
198
|
+
tags = [
|
|
199
|
+
TrendTag.output(),
|
|
200
|
+
TrendTag.frame(),
|
|
201
|
+
# These next tags are only for easing test writing and debugging
|
|
202
|
+
TrendTag.arm_id(arm_id),
|
|
203
|
+
TrendTag.pac_fit_options(fit_options.name),
|
|
204
|
+
TrendTag.instrument_processing_options(instrument_options.name),
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
self.write(
|
|
208
|
+
data=hdu_list,
|
|
209
|
+
tags=tags,
|
|
210
|
+
encoder=fits_hdulist_encoder,
|
|
211
|
+
relative_path=relative_path,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
self.update_framevol(relative_path)
|
|
215
|
+
|
|
216
|
+
# TODO: Should there be a different configuration variable for `validate_trend_on_write`?
|
|
217
|
+
if self.validate_l1_on_write:
|
|
218
|
+
# Replace this logic with the fits_header_decoder once FITS library keys have been removed from the SPEC
|
|
219
|
+
# TODO: Actually do something here once validator is ready
|
|
220
|
+
pass
|
|
221
|
+
# with fits.open(self.scratch.absolute_path(relative_path)) as hdul:
|
|
222
|
+
# header = hdul[1].header
|
|
223
|
+
# spec214_validator.validate(header, extra=False)
|
|
224
|
+
|
|
225
|
+
def generate_fit_info_hdu(self, fit_options: FitOptions) -> fits.ImageHDU:
|
|
226
|
+
"""
|
|
227
|
+
Generate the data-less "Fit info" HDU.
|
|
228
|
+
|
|
229
|
+
Header values are pulled from the fit mode definitions that live in `dkist-processing-pac`.
|
|
230
|
+
"""
|
|
231
|
+
switches, vary = PolcalDresserParameters.load_fit_mode(fit_mode=fit_options.fit_mode_name)
|
|
232
|
+
|
|
233
|
+
# If we use db M12 values then Q_in *never* varies. See `*-pac` for more info
|
|
234
|
+
q_in_free = vary["Q_in"] and not switches["use_M12"]
|
|
235
|
+
|
|
236
|
+
header = fits.Header()
|
|
237
|
+
header["EXTNAME"] = "Fit info"
|
|
238
|
+
header["QINFREE"] = q_in_free
|
|
239
|
+
header["TPOLFREE"] = vary["t_pol"]
|
|
240
|
+
header["TRETFREE"] = vary["t_ret"]
|
|
241
|
+
header["RMITREND"] = fit_options.remove_I_trend
|
|
242
|
+
header["ISYSPERS"] = switches["I_sys_per_CS_step"]
|
|
243
|
+
header["GLBLTRNS"] = switches["global_transmission"]
|
|
244
|
+
header["GLBLRET"] = switches["global_retardance"]
|
|
245
|
+
|
|
246
|
+
# These perturbation parameters are not currently used
|
|
247
|
+
header["X34PERT"] = 0
|
|
248
|
+
header["T34PERT"] = 0
|
|
249
|
+
header["X56PERT"] = 0
|
|
250
|
+
header["T56PERT"] = 0
|
|
251
|
+
header["UINPERT"] = 0
|
|
252
|
+
header["VINPERT"] = 0
|
|
253
|
+
|
|
254
|
+
return self.build_hdu(header=header)
|
|
255
|
+
|
|
256
|
+
def generate_inventory_hdu(
|
|
257
|
+
self, arm_id: str | int, fit_options: FitOptions, instrument_options: InstrumentOptionsBase
|
|
258
|
+
) -> fits.ImageHDU:
|
|
259
|
+
"""
|
|
260
|
+
Generate the data-less "Inventory" HDU.
|
|
261
|
+
|
|
262
|
+
This is also where instrument-specific headers are added.
|
|
263
|
+
"""
|
|
264
|
+
header = fits.Header()
|
|
265
|
+
header["EXTNAME"] = "Inventory"
|
|
266
|
+
header["DSETID"] = self.constants.dataset_id
|
|
267
|
+
header["PROCTYPE"] = "POLCAL"
|
|
268
|
+
# This is just a placeholder value, but it's needed so FRAMEVOL gets properly commented and placed during header formatting
|
|
269
|
+
header["FRAMEVOL"] = -1.0
|
|
270
|
+
header["RRUNID"] = self.recipe_run_id
|
|
271
|
+
header["RECIPEID"] = self.metadata_store_recipe_run.recipeInstance.recipeId
|
|
272
|
+
header["RINSTID"] = self.metadata_store_recipe_run.recipeInstanceId
|
|
273
|
+
header["FILENAME"] = self.trend_filename(
|
|
274
|
+
arm_id=arm_id, fit_options=fit_options, instrument_options=instrument_options
|
|
275
|
+
)
|
|
276
|
+
header["HEADVERS"] = spec_version
|
|
277
|
+
header["HEAD_URL"] = f"{self.docs_base_url}/projects/data-products/en/v{spec_version}"
|
|
278
|
+
header["INFO_URL"] = self.docs_base_url
|
|
279
|
+
calvers = self.version_from_module_name()
|
|
280
|
+
header["CALVERS"] = calvers
|
|
281
|
+
header["CAL_URL"] = (
|
|
282
|
+
f"{self.docs_base_url}/projects/trend/en/v{calvers}/{self.workflow_name}.html"
|
|
283
|
+
)
|
|
284
|
+
header["WKFLNAME"] = self.workflow_name
|
|
285
|
+
header["WKFLVERS"] = self.workflow_version
|
|
286
|
+
header["MANPROCD"] = self.workflow_had_manual_intervention
|
|
287
|
+
header["FILE_ID"] = uuid.uuid4().hex
|
|
288
|
+
header["OBSPR_ID"] = self.constants.observing_program_id_list[0]
|
|
289
|
+
header["EXTOBSID"] = ",".join(self.constants.observing_program_id_list[1:])
|
|
290
|
+
header["EXPER_ID"] = self.constants.experiment_id_list[0]
|
|
291
|
+
header["EXTEXPID"] = ",".join(self.constants.experiment_id_list[1:])
|
|
292
|
+
header["PROP_ID"] = self.constants.proposal_id_list[0]
|
|
293
|
+
header["EXTPRPID"] = ",".join(self.constants.proposal_id_list[1:])
|
|
294
|
+
header["HLSVERS"] = self.constants.hls_version_list[0]
|
|
295
|
+
header["EXTHLSV"] = ",".join(self.constants.hls_version_list[1:])
|
|
296
|
+
header["OPTDATE"] = self.constants.earliest_ip_start_time
|
|
297
|
+
header["LINEWAV"] = self.constants.wavelength_for_arm[arm_id]
|
|
298
|
+
header["INSTRUME"] = self.constants.instrument
|
|
299
|
+
header["FITOPTID"] = self.compute_full_fit_opt_id(
|
|
300
|
+
fit_options=fit_options, instrument_options=instrument_options
|
|
301
|
+
)
|
|
302
|
+
header["ORIGIN"] = "National Solar Observatory"
|
|
303
|
+
header["TELESCOP"] = "Daniel K. Inouye Solar Telescope"
|
|
304
|
+
header["OBSRVTRY"] = "Haleakala High Altitude Observatory Site"
|
|
305
|
+
header["NETWORK"] = "NSF-DKIST"
|
|
306
|
+
header["DATE"] = Time.now().fits
|
|
307
|
+
|
|
308
|
+
match self.constants.instrument:
|
|
309
|
+
case InstrumentName.visp:
|
|
310
|
+
self.add_visp_headers(
|
|
311
|
+
header=header, arm_id=arm_id, instrument_options=instrument_options
|
|
312
|
+
)
|
|
313
|
+
case InstrumentName.cryonirsp:
|
|
314
|
+
self.add_cryonirsp_headers(
|
|
315
|
+
header=header, arm_id=arm_id, instrument_options=instrument_options
|
|
316
|
+
)
|
|
317
|
+
case InstrumentName.dlnirsp:
|
|
318
|
+
self.add_dlnirsp_headers(
|
|
319
|
+
header=header, arm_id=arm_id, instrument_options=instrument_options
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
return self.build_hdu(header=header)
|
|
323
|
+
|
|
324
|
+
def add_visp_headers(
|
|
325
|
+
self, header: fits.Header, arm_id: int, instrument_options: VispInstrumentOptions
|
|
326
|
+
) -> None:
|
|
327
|
+
"""Add keys in the "visp" trend table to a header."""
|
|
328
|
+
header["VSPARMID"] = arm_id
|
|
329
|
+
header["VSPNUMST"] = self.constants.num_modstates
|
|
330
|
+
header["VSPGRTAN"] = self.constants.visp_grating_angle_for_arm[arm_id]
|
|
331
|
+
header["VSPARMPS"] = self.constants.visp_arm_position_for_arm[arm_id]
|
|
332
|
+
header["VSPGRTCN"] = self.constants.visp_grating_constant_for_arm[arm_id]
|
|
333
|
+
header["VSPNSPIX"] = instrument_options.num_spatial_px
|
|
334
|
+
header["VSPNWPIX"] = instrument_options.num_spectral_px
|
|
335
|
+
header["VSPSFORD"] = instrument_options.spatial_smoothing_fit_order
|
|
336
|
+
|
|
337
|
+
def add_cryonirsp_headers(
|
|
338
|
+
self, header: fits.Header, arm_id: str, instrument_options: CryonirspInstrumentOptions
|
|
339
|
+
) -> None:
|
|
340
|
+
"""Add keys in the "cryonirsp" trend table to a header."""
|
|
341
|
+
header["CNARMID"] = arm_id
|
|
342
|
+
header["CNMODNST"] = self.constants.num_modstates
|
|
343
|
+
header["CNFILTNP"] = self.constants.cryonirsp_od_filter_name_for_arm[arm_id]
|
|
344
|
+
header["CNGRTPOS"] = self.constants.cryonirsp_grating_angle_for_arm[arm_id]
|
|
345
|
+
header["CNGRTLAT"] = self.constants.cryonirsp_grating_littrow_angle_for_arm[arm_id]
|
|
346
|
+
header["CNGRTCON"] = self.constants.cryonirsp_grating_constant_for_arm[arm_id]
|
|
347
|
+
header["CNSLITW"] = self.constants.cryonirsp_slit_width_for_arm[arm_id]
|
|
348
|
+
header["CNNSPIX"] = instrument_options.num_spatial_px
|
|
349
|
+
header["CNNWPIX"] = instrument_options.num_spectral_px
|
|
350
|
+
|
|
351
|
+
def add_dlnirsp_headers(
|
|
352
|
+
self, header: fits.Header, arm_id: str, instrument_options: DlnirspInstrumentOptions
|
|
353
|
+
) -> None:
|
|
354
|
+
"""Add keys in the "dlnirsp" trend table to a header."""
|
|
355
|
+
header["DLARMID"] = arm_id
|
|
356
|
+
header["DLNUMST"] = self.constants.num_modstates
|
|
357
|
+
header["DLGRTAN"] = self.constants.dlnirsp_grating_angle_for_arm[arm_id]
|
|
358
|
+
header["DLARMPS"] = self.constants.dlnirsp_arm_position_for_arm[arm_id]
|
|
359
|
+
header["DLGRTCN"] = self.constants.dlnirsp_grating_constant_for_arm[arm_id]
|
|
360
|
+
header["DLNSPIX"] = instrument_options.num_spatial_px
|
|
361
|
+
header["DLNWPIX"] = instrument_options.num_spectral_px
|
|
362
|
+
header["DLSFORD"] = instrument_options.spatial_smoothing_fit_order
|
|
363
|
+
|
|
364
|
+
def generate_calibration_sequence_hdu(self, arm_id: str | int) -> fits.ImageHDU:
|
|
365
|
+
"""Generate the "Calibration sequence" HDU that contains information about the full set of input GOS configurations."""
|
|
366
|
+
header = fits.Header()
|
|
367
|
+
header["EXTNAME"] = "Calibration sequence"
|
|
368
|
+
header["CSAX1"] = "CS_STEP"
|
|
369
|
+
header["CSAX2"] = "OPTIC"
|
|
370
|
+
header["CSAX3"] = "Calibration Sequence"
|
|
371
|
+
header["ONAME1"] = "POL_IN"
|
|
372
|
+
header["ONAME2"] = "POL_ANG_DEG"
|
|
373
|
+
header["ONAME3"] = "RET_IN"
|
|
374
|
+
header["ONAME4"] = "RET_ANG_DEG"
|
|
375
|
+
header["ONAME5"] = "DARK_IN"
|
|
376
|
+
header["ONAME6"] = "TAZIMUTH_DEG"
|
|
377
|
+
header["ONAME7"] = "ELEV_ANG_DEG"
|
|
378
|
+
header["ONAME8"] = "TTBLANGL_DEG"
|
|
379
|
+
|
|
380
|
+
data = next(
|
|
381
|
+
self.read(
|
|
382
|
+
tags=[
|
|
383
|
+
TrendTag.intermediate(),
|
|
384
|
+
TrendTag.arm_id(arm_id),
|
|
385
|
+
TrendTag.task_calibration_sequence(),
|
|
386
|
+
],
|
|
387
|
+
decoder=fits_array_decoder,
|
|
388
|
+
auto_squeeze=False,
|
|
389
|
+
)
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
return self.build_hdu(header=header, data=data)
|
|
393
|
+
|
|
394
|
+
def generate_fit_parameters_hdu(
|
|
395
|
+
self, arm_id: str | int, fit_options: FitOptions, instrument_options: InstrumentOptionsBase
|
|
396
|
+
) -> fits.CompImageHDU:
|
|
397
|
+
"""
|
|
398
|
+
Generate the "Fit parameters" HDU that contains the full set of best-fit parameters.
|
|
399
|
+
|
|
400
|
+
The actual array of fit parameters is constructed in `~dkist_processing_trend.tasks.run_pac_fitter.RunPacFitter.write_best_fit_parameters`.
|
|
401
|
+
|
|
402
|
+
This HDU is compressed because the FOV size can be large.
|
|
403
|
+
"""
|
|
404
|
+
header = fits.Header()
|
|
405
|
+
header["EXTNAME"] = "Fit parameters"
|
|
406
|
+
header["NCSSTEPS"] = self.constants.num_cs_steps
|
|
407
|
+
header["FPAX1"] = "[value, init_value, free_in_fit]"
|
|
408
|
+
header["FPAX2"] = "Parameter"
|
|
409
|
+
header["FPAX3"] = "Calibration Sequence"
|
|
410
|
+
header["FPAX4"] = "Detector FITS axis 1"
|
|
411
|
+
header["FPAX5"] = "Detector FITS axis 2"
|
|
412
|
+
header["FPAX6"] = "Beam"
|
|
413
|
+
header["PNAME001"] = "X12"
|
|
414
|
+
header["PNAME002"] = "Tau 12"
|
|
415
|
+
header["PNAME003"] = "X34"
|
|
416
|
+
header["PNAME004"] = "Tau 34"
|
|
417
|
+
header["PNAME005"] = "X56"
|
|
418
|
+
header["PNAME006"] = "Tau 56"
|
|
419
|
+
header["PNAME007"] = "Q_in"
|
|
420
|
+
header["PNAME008"] = "U_in"
|
|
421
|
+
header["PNAME009"] = "V_in"
|
|
422
|
+
header["PNAME010"] = "t_pol"
|
|
423
|
+
header["PNAME011"] = "t_ret"
|
|
424
|
+
header["PNAME012"] = "ret_r"
|
|
425
|
+
header["PNAME013"] = "ret_45"
|
|
426
|
+
header["PNAME014"] = "ret_h"
|
|
427
|
+
header["PNAME015"] = "py"
|
|
428
|
+
for s in range(self.constants.num_cs_steps):
|
|
429
|
+
# TODO: Account for multiple polcal OPs by having "I_sys_CSXX_stepNN"
|
|
430
|
+
header[f"PNAME{s + 16:03d}"] = f"I_sys_step{s:03d}"
|
|
431
|
+
|
|
432
|
+
all_beam_data_list = []
|
|
433
|
+
for beam in self.beam_list(arm_id):
|
|
434
|
+
all_beam_data_list.append(
|
|
435
|
+
next(
|
|
436
|
+
self.read(
|
|
437
|
+
tags=[
|
|
438
|
+
TrendTag.intermediate(),
|
|
439
|
+
TrendTag.arm_id(arm_id),
|
|
440
|
+
TrendTag.pac_fit_options(fit_options.name),
|
|
441
|
+
TrendTag.instrument_processing_options(instrument_options.name),
|
|
442
|
+
TrendTag.beam(beam),
|
|
443
|
+
TrendTag.task_best_fit_parameters(),
|
|
444
|
+
],
|
|
445
|
+
decoder=fits_array_decoder,
|
|
446
|
+
auto_squeeze=False,
|
|
447
|
+
)
|
|
448
|
+
)
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
# Will add the correct dimension even for a single beam
|
|
452
|
+
all_beam_data = np.stack(all_beam_data_list, axis=0)
|
|
453
|
+
|
|
454
|
+
return self.build_hdu(header=header, data=all_beam_data)
|
|
455
|
+
|
|
456
|
+
def generate_modulation_matrices_hdu(
|
|
457
|
+
self, arm_id: str | int, fit_options: FitOptions, instrument_options: InstrumentOptionsBase
|
|
458
|
+
) -> fits.CompImageHDU:
|
|
459
|
+
"""
|
|
460
|
+
Generate the "Modulation matrices" HDU that contains all modulation matrices for the given instrument FOV.
|
|
461
|
+
|
|
462
|
+
This is where we convert the DEmodulation matrices produced by `dkist_processing_pac` and
|
|
463
|
+
`~dkist_processing_trend.tasks.run_pac_fitter.RunPacFitter.write_best_fit_demodulation_matrices` into modulation matrices.
|
|
464
|
+
|
|
465
|
+
This HDU is compressed because the FOV size can be large.
|
|
466
|
+
"""
|
|
467
|
+
header = fits.Header()
|
|
468
|
+
header["EXTNAME"] = "Modulation matrices"
|
|
469
|
+
header["MMAX1"] = "Stokes parameter"
|
|
470
|
+
header["MMAX2"] = "Modulator state"
|
|
471
|
+
header["MMAX3"] = "Detector FITS axis 1"
|
|
472
|
+
header["MMAX4"] = "Detector FITS axis 2"
|
|
473
|
+
header["MMAX5"] = "Beam"
|
|
474
|
+
|
|
475
|
+
all_beam_data_list = []
|
|
476
|
+
for beam in self.beam_list(arm_id):
|
|
477
|
+
beam_demod = next(
|
|
478
|
+
self.read(
|
|
479
|
+
tags=[
|
|
480
|
+
TrendTag.intermediate(),
|
|
481
|
+
TrendTag.arm_id(arm_id),
|
|
482
|
+
TrendTag.pac_fit_options(fit_options.name),
|
|
483
|
+
TrendTag.instrument_processing_options(instrument_options.name),
|
|
484
|
+
TrendTag.beam(beam),
|
|
485
|
+
TrendTag.task_best_fit_demodulation_matrices(),
|
|
486
|
+
],
|
|
487
|
+
decoder=fits_array_decoder,
|
|
488
|
+
auto_squeeze=False,
|
|
489
|
+
)
|
|
490
|
+
)
|
|
491
|
+
fov_shape = beam_demod.shape[:-2]
|
|
492
|
+
num_points = np.prod(fov_shape)
|
|
493
|
+
beam_mod_mat = np.empty(fov_shape + (self.constants.num_modstates, 4))
|
|
494
|
+
|
|
495
|
+
for point in range(num_points):
|
|
496
|
+
idx = np.unravel_index(point, fov_shape)
|
|
497
|
+
point_demod = beam_demod[idx]
|
|
498
|
+
if np.sum(np.isnan(point_demod)) > 0:
|
|
499
|
+
point_mod = np.full((self.constants.num_modstates, 4), np.nan)
|
|
500
|
+
else:
|
|
501
|
+
point_mod = np.linalg.pinv(point_demod)
|
|
502
|
+
|
|
503
|
+
beam_mod_mat[idx] = point_mod
|
|
504
|
+
|
|
505
|
+
all_beam_data_list.append(beam_mod_mat)
|
|
506
|
+
|
|
507
|
+
# Will add the correct dimension even for a single beam
|
|
508
|
+
all_beam_data = np.stack(all_beam_data_list, axis=0)
|
|
509
|
+
|
|
510
|
+
return self.build_hdu(header=header, data=all_beam_data)
|
|
511
|
+
|
|
512
|
+
def generate_best_fit_flux_hdu(
|
|
513
|
+
self, arm_id: str | int, fit_options: FitOptions, instrument_options: InstrumentOptionsBase
|
|
514
|
+
) -> fits.CompImageHDU:
|
|
515
|
+
"""
|
|
516
|
+
Generate the "Best fit flux" HDU that contains the full set of best-fit model fluxes.
|
|
517
|
+
|
|
518
|
+
The actual flux array is constructed in `~dkist_processing_trend.tasks.run_pac_fitter.RunPacFitter.write_best_fit_flux_and_residuals`.
|
|
519
|
+
|
|
520
|
+
This HDU is compressed because the FOV size can be large.
|
|
521
|
+
"""
|
|
522
|
+
header = fits.Header()
|
|
523
|
+
header["EXTNAME"] = "Best fit flux"
|
|
524
|
+
header["BFFAX1"] = "CS step"
|
|
525
|
+
header["BFFAX2"] = "Modulator state"
|
|
526
|
+
header["BFFAX3"] = "Detector FITS axis 1"
|
|
527
|
+
header["BFFAX4"] = "Detector FITS axis 2"
|
|
528
|
+
header["BFFAX5"] = "Beam"
|
|
529
|
+
|
|
530
|
+
all_beam_data_list = []
|
|
531
|
+
for beam in self.beam_list(arm_id):
|
|
532
|
+
all_beam_data_list.append(
|
|
533
|
+
next(
|
|
534
|
+
self.read(
|
|
535
|
+
tags=[
|
|
536
|
+
TrendTag.intermediate(),
|
|
537
|
+
TrendTag.arm_id(arm_id),
|
|
538
|
+
TrendTag.pac_fit_options(fit_options.name),
|
|
539
|
+
TrendTag.instrument_processing_options(instrument_options.name),
|
|
540
|
+
TrendTag.beam(beam),
|
|
541
|
+
TrendTag.task_best_fit_flux(),
|
|
542
|
+
],
|
|
543
|
+
decoder=fits_array_decoder,
|
|
544
|
+
auto_squeeze=False,
|
|
545
|
+
)
|
|
546
|
+
)
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
# Will add the correct dimension even for a single beam
|
|
550
|
+
all_beam_data = np.stack(all_beam_data_list, axis=0)
|
|
551
|
+
|
|
552
|
+
return self.build_hdu(header=header, data=all_beam_data)
|
|
553
|
+
|
|
554
|
+
def generate_fit_residuals_hdu(
|
|
555
|
+
self, arm_id: str | int, fit_options: FitOptions, instrument_options: InstrumentOptionsBase
|
|
556
|
+
) -> fits.CompImageHDU:
|
|
557
|
+
"""
|
|
558
|
+
Generate the "Best fit residuals" HDU that contains the fitting residuals of the best-fit solution.
|
|
559
|
+
|
|
560
|
+
The actual flux array is constructed in `~dkist_processing_trend.tasks.run_pac_fitter.RunPacFitter.write_best_fit_flux_and_residuals`.
|
|
561
|
+
|
|
562
|
+
This HDU is compressed because the FOV size can be large.
|
|
563
|
+
"""
|
|
564
|
+
header = fits.Header()
|
|
565
|
+
header["EXTNAME"] = "Best fit residuals"
|
|
566
|
+
header["BFRAX1"] = "CS step"
|
|
567
|
+
header["BFRAX2"] = "Modulator state"
|
|
568
|
+
header["BFRAX3"] = "Detector FITS axis 1"
|
|
569
|
+
header["BFRAX4"] = "Detector FITS axis 2"
|
|
570
|
+
header["BFRAX5"] = "Beam"
|
|
571
|
+
|
|
572
|
+
all_beam_data_list = []
|
|
573
|
+
for beam in self.beam_list(arm_id):
|
|
574
|
+
all_beam_data_list.append(
|
|
575
|
+
next(
|
|
576
|
+
self.read(
|
|
577
|
+
tags=[
|
|
578
|
+
TrendTag.intermediate(),
|
|
579
|
+
TrendTag.arm_id(arm_id),
|
|
580
|
+
TrendTag.pac_fit_options(fit_options.name),
|
|
581
|
+
TrendTag.instrument_processing_options(instrument_options.name),
|
|
582
|
+
TrendTag.beam(beam),
|
|
583
|
+
TrendTag.task_fit_residuals(),
|
|
584
|
+
],
|
|
585
|
+
decoder=fits_array_decoder,
|
|
586
|
+
auto_squeeze=False,
|
|
587
|
+
)
|
|
588
|
+
)
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
# Will add the correct dimension even for a single beam
|
|
592
|
+
all_beam_data = np.stack(all_beam_data_list, axis=0)
|
|
593
|
+
|
|
594
|
+
return self.build_hdu(header=header, data=all_beam_data)
|
|
595
|
+
|
|
596
|
+
def generate_single_beam_DMPD_hdu(
|
|
597
|
+
self,
|
|
598
|
+
beam: int,
|
|
599
|
+
arm_id: str | int,
|
|
600
|
+
fit_options: FitOptions,
|
|
601
|
+
instrument_options: InstrumentOptionsBase,
|
|
602
|
+
) -> fits.CompImageHDU:
|
|
603
|
+
"""
|
|
604
|
+
Generate the "Demodulated polcal data" HDU that contains the "processed" and demodulated input data for a single beam.
|
|
605
|
+
|
|
606
|
+
This HDU is compressed because this HDU is huge.
|
|
607
|
+
"""
|
|
608
|
+
header = fits.Header()
|
|
609
|
+
header["EXTNAME"] = f"Beam {beam} demodulated polcal data"
|
|
610
|
+
header["DMPDAX1"] = "Input data axis 1"
|
|
611
|
+
header["DMPDAX2"] = "Input data axis 2"
|
|
612
|
+
header["DMPDAX3"] = "Stokes"
|
|
613
|
+
header["DMPDAX4"] = "CS Step"
|
|
614
|
+
header["DMPDAX5"] = "Calibration Sequence"
|
|
615
|
+
|
|
616
|
+
data = next(
|
|
617
|
+
self.read(
|
|
618
|
+
tags=[
|
|
619
|
+
TrendTag.intermediate(),
|
|
620
|
+
TrendTag.arm_id(arm_id),
|
|
621
|
+
TrendTag.beam(beam),
|
|
622
|
+
TrendTag.pac_fit_options(fit_options.name),
|
|
623
|
+
TrendTag.instrument_processing_options(instrument_options.name),
|
|
624
|
+
TrendTag.task_single_beam_demodulated_polcal_data(),
|
|
625
|
+
],
|
|
626
|
+
decoder=fits_array_decoder,
|
|
627
|
+
auto_squeeze=False,
|
|
628
|
+
)
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
return self.build_hdu(header=header, data=data)
|
|
632
|
+
|
|
633
|
+
def generate_DMPD_hdu(
|
|
634
|
+
self, arm_id: str | int, fit_options: FitOptions, instrument_options: InstrumentOptionsBase
|
|
635
|
+
) -> fits.CompImageHDU:
|
|
636
|
+
"""
|
|
637
|
+
Generate the "Demodulated polcal data" HDU that contains the "processed" and demodulated input data.
|
|
638
|
+
|
|
639
|
+
This HDU is compressed because this HDU is huge.
|
|
640
|
+
"""
|
|
641
|
+
header = fits.Header()
|
|
642
|
+
header["EXTNAME"] = "Demodulated polcal data"
|
|
643
|
+
header["DMPDAX1"] = "Input data axis 1"
|
|
644
|
+
header["DMPDAX2"] = "Input data axis 2"
|
|
645
|
+
header["DMPDAX3"] = "Stokes"
|
|
646
|
+
header["DMPDAX4"] = "CS Step"
|
|
647
|
+
header["DMPDAX5"] = "Calibration Sequence"
|
|
648
|
+
|
|
649
|
+
data = next(
|
|
650
|
+
self.read(
|
|
651
|
+
tags=[
|
|
652
|
+
TrendTag.intermediate(),
|
|
653
|
+
TrendTag.arm_id(arm_id),
|
|
654
|
+
TrendTag.pac_fit_options(fit_options.name),
|
|
655
|
+
TrendTag.instrument_processing_options(instrument_options.name),
|
|
656
|
+
TrendTag.task_demodulated_polcal_data(),
|
|
657
|
+
],
|
|
658
|
+
decoder=fits_array_decoder,
|
|
659
|
+
auto_squeeze=False,
|
|
660
|
+
)
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
return self.build_hdu(header=header, data=data)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tests for testing."""
|