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,718 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from dataclasses import asdict
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from dataclasses import field
|
|
5
|
+
from dataclasses import is_dataclass
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from functools import partial
|
|
8
|
+
from itertools import product
|
|
9
|
+
from random import randint
|
|
10
|
+
from typing import Type
|
|
11
|
+
|
|
12
|
+
import astropy.units as u
|
|
13
|
+
import numpy as np
|
|
14
|
+
import pytest
|
|
15
|
+
from astropy import coordinates
|
|
16
|
+
from astropy.io import fits
|
|
17
|
+
from astropy.time import Time
|
|
18
|
+
from dkist_data_simulator.dataset import key_function
|
|
19
|
+
from dkist_data_simulator.spec122 import Spec122Dataset
|
|
20
|
+
from dkist_processing_common.codecs.basemodel import basemodel_encoder
|
|
21
|
+
from dkist_processing_common.codecs.fits import fits_array_encoder
|
|
22
|
+
from dkist_processing_common.codecs.fits import fits_hdulist_encoder
|
|
23
|
+
from dkist_processing_common.models.input_dataset import InputDatasetPartDocumentList
|
|
24
|
+
from dkist_processing_common.tasks.base import WorkflowTaskBase
|
|
25
|
+
|
|
26
|
+
# Despite what an IDE might think, the following import is used by most of the tests. Don't delete it!
|
|
27
|
+
from dkist_processing_common.tests.mock_metadata_store import fake_gql_client
|
|
28
|
+
|
|
29
|
+
from dkist_processing_trend.models.constants import TrendConstants
|
|
30
|
+
from dkist_processing_trend.models.instrument import InstrumentName
|
|
31
|
+
from dkist_processing_trend.models.tags import TrendTag
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.fixture()
|
|
35
|
+
def recipe_run_id():
|
|
36
|
+
return randint(0, 99999)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class TrendInputDatasetParameterValues:
|
|
41
|
+
trend_max_cs_step_time_sec: float = 180.0
|
|
42
|
+
trend_fit_options_list: tuple[dict] = (
|
|
43
|
+
{
|
|
44
|
+
"name": "option1",
|
|
45
|
+
"remove_I_trend": True,
|
|
46
|
+
"fit_mode_name": "fit_mode1",
|
|
47
|
+
"init_set_name": "init_set1",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name": "option2",
|
|
51
|
+
"remove_I_trend": False,
|
|
52
|
+
"fit_mode_name": "fit_mode1",
|
|
53
|
+
"init_set_name": "init_set2",
|
|
54
|
+
},
|
|
55
|
+
)
|
|
56
|
+
trend_instrument_processing_options_visp: tuple[dict] = (
|
|
57
|
+
{
|
|
58
|
+
"name": "visp_option1",
|
|
59
|
+
"num_spatial_px": 2560,
|
|
60
|
+
"num_spectral_px": 1,
|
|
61
|
+
"spatial_smoothing_fit_order": 3,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"name": "visp_option2",
|
|
65
|
+
"num_spatial_px": 1,
|
|
66
|
+
"num_spectral_px": 1,
|
|
67
|
+
"spatial_smoothing_fit_order": 1,
|
|
68
|
+
},
|
|
69
|
+
)
|
|
70
|
+
trend_instrument_processing_options_cryonirsp: tuple[dict] = (
|
|
71
|
+
{"name": "cryonirsp_option1", "num_spatial_px": 1, "num_spectral_px": 1},
|
|
72
|
+
{"name": "cryonirsp_option2", "num_spatial_px": 2, "num_spectral_px": 1},
|
|
73
|
+
)
|
|
74
|
+
trend_instrument_processing_options_dlnirsp: tuple[dict] = (
|
|
75
|
+
{
|
|
76
|
+
"name": "dlnirsp_option1",
|
|
77
|
+
"num_spatial_px": 1000,
|
|
78
|
+
"num_spectral_px": 1000,
|
|
79
|
+
"spatial_smoothing_fit_order": 3,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"name": "dlnirsp_option2",
|
|
83
|
+
"num_spatial_px": 1,
|
|
84
|
+
"num_spectral_px": 1,
|
|
85
|
+
"spatial_smoothing_fit_order": 1,
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
trend_visp_beam_border: int = 1000
|
|
89
|
+
trend_visp_geo_upsample_factor: int = 100
|
|
90
|
+
trend_visp_hairline_median_spatial_smoothing_width_px: int = 30
|
|
91
|
+
trend_visp_hairline_fraction: float = 0.11
|
|
92
|
+
trend_visp_hairline_mask_spatial_smoothing_width_px: float = 1.0
|
|
93
|
+
trend_visp_hairline_mask_gaussian_peak_cutoff_fraction: float = 0.02
|
|
94
|
+
trend_visp_polcal_spatial_median_filter_width_px: int = 10
|
|
95
|
+
trend_visp_polcal_demod_spatial_smooth_min_samples: float = 0.9
|
|
96
|
+
trend_visp_polcal_demod_upsample_order: int = 3
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
WAVELENGTH_FOR_ARM = {
|
|
100
|
+
# Visp
|
|
101
|
+
1: 854.2,
|
|
102
|
+
2: 589.6,
|
|
103
|
+
3: 630.2,
|
|
104
|
+
# Cryo
|
|
105
|
+
"CI": 1083.0,
|
|
106
|
+
"SP": 1074.7,
|
|
107
|
+
# DL
|
|
108
|
+
"VIS": 854.2,
|
|
109
|
+
"JBand": 1083.0,
|
|
110
|
+
"HBand": 1565.0,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
GRATING_ANGLE_FOR_ARM = { # Visp
|
|
115
|
+
1: 9.871,
|
|
116
|
+
2: 9.872,
|
|
117
|
+
3: 9.873,
|
|
118
|
+
# Cryo
|
|
119
|
+
"CI": 123.4,
|
|
120
|
+
"SP": 123.45,
|
|
121
|
+
# DL
|
|
122
|
+
"VIS": 2.22,
|
|
123
|
+
"JBand": 2.222,
|
|
124
|
+
"HBand": 2.2222,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
ARM_ANGLE_FOR_ARM = {
|
|
128
|
+
# Visp
|
|
129
|
+
1: 8.761,
|
|
130
|
+
2: 8.762,
|
|
131
|
+
3: 8.763,
|
|
132
|
+
# Cryo
|
|
133
|
+
"CI": 12.34,
|
|
134
|
+
"SP": 12.345,
|
|
135
|
+
# DL
|
|
136
|
+
"VIS": 3.33,
|
|
137
|
+
"JBand": 3.333,
|
|
138
|
+
"HBand": 3.3333,
|
|
139
|
+
}
|
|
140
|
+
GRATING_CONSTANT_FOR_ARM = {
|
|
141
|
+
# Visp
|
|
142
|
+
1: 9871,
|
|
143
|
+
2: 9872,
|
|
144
|
+
3: 9873,
|
|
145
|
+
# Cryo
|
|
146
|
+
"CI": 1234,
|
|
147
|
+
"SP": 12345,
|
|
148
|
+
# DL
|
|
149
|
+
"VIS": 4444,
|
|
150
|
+
"JBand": 4444.4,
|
|
151
|
+
"HBand": 4444.44,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
CRYO_OD_FILTER_FOR_ARM = {
|
|
155
|
+
"CI": "CIFILT",
|
|
156
|
+
"SP": "SPFILT",
|
|
157
|
+
# Need dummy values for other instrument's arm so tests can slice into this dict with any arm
|
|
158
|
+
1: None,
|
|
159
|
+
2: None,
|
|
160
|
+
3: None,
|
|
161
|
+
"VIS": None,
|
|
162
|
+
"JBand": None,
|
|
163
|
+
"HBand": None,
|
|
164
|
+
}
|
|
165
|
+
CRYO_SLIT_WIDTH_FOR_ARM = {
|
|
166
|
+
"CI": 0.1234,
|
|
167
|
+
"SP": 0.12345,
|
|
168
|
+
# Need dummy values for other instrument's arm so tests can slice into this dict with any arm
|
|
169
|
+
1: None,
|
|
170
|
+
2: None,
|
|
171
|
+
3: None,
|
|
172
|
+
"VIS": None,
|
|
173
|
+
"JBand": None,
|
|
174
|
+
"HBand": None,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@dataclass
|
|
179
|
+
class TrendTestingConstantsDatabase:
|
|
180
|
+
INSTRUMENT: str = "VISP"
|
|
181
|
+
EARLIEST_IP_START_TIME: str = "2026-05-01"
|
|
182
|
+
ARM_ID_LIST: tuple[str | int] = (1, 2, 3)
|
|
183
|
+
WAVELENGTH_FOR_ARM: dict[str, float] = field(default_factory=lambda: {"1": 123.4, "2": 567.8})
|
|
184
|
+
NUM_CS_STEPS: int = 7 # Number of steps in `small_calibration_sequence`
|
|
185
|
+
NUM_MODSTATES: int = 4
|
|
186
|
+
OBSERVING_PROGRAM_ID_LIST: tuple[str] = ("TOP1", "TOP2")
|
|
187
|
+
EXPERIMENT_ID_LIST: tuple[str] = "EX1"
|
|
188
|
+
PROPOSAL_ID_LIST: tuple[str] = "PROP5"
|
|
189
|
+
HLS_VERSION_LIST: tuple[str] = ("Tau 6.2.8", "Pi 3.14")
|
|
190
|
+
VISP_GRATING_ANGLE_FOR_ARM: dict[str, float] = field(
|
|
191
|
+
default_factory=lambda: {"1": 9.87, "2": 9.872}
|
|
192
|
+
)
|
|
193
|
+
VISP_ARM_POSITION_FOR_ARM: dict[str, float] = field(
|
|
194
|
+
default_factory=lambda: {"1": 8.76, "2": 8.762}
|
|
195
|
+
)
|
|
196
|
+
VISP_GRATING_CONSTANT_FOR_ARM: dict[str, float] = field(
|
|
197
|
+
default_factory=lambda: {"1": 9870, "2": 9872}
|
|
198
|
+
)
|
|
199
|
+
CRYONIRSP_OD_FILTER_NAME_FOR_ARM: dict[str, str] = field(
|
|
200
|
+
default_factory=lambda: {'"CI"': "CRYOFILT"}
|
|
201
|
+
)
|
|
202
|
+
CRYONIRSP_GRATING_ANGLE_FOR_ARM: dict[str, float] = field(
|
|
203
|
+
default_factory=lambda: {'"CI"': 123.4, '"SP"': 123.5}
|
|
204
|
+
)
|
|
205
|
+
CRYONIRSP_GRATING_LITTROW_ANGLE_FOR_ARM: dict[str, float] = field(
|
|
206
|
+
default_factory=lambda: {'"CI"': 12.34, '"SP"': 12.35}
|
|
207
|
+
)
|
|
208
|
+
CRYONIRSP_GRATING_CONSTANT_FOR_ARM: dict[str, float] = field(
|
|
209
|
+
default_factory=lambda: {'"SP"': 1234}
|
|
210
|
+
)
|
|
211
|
+
CRYONIRSP_SLIT_WIDTH_FOR_ARM: dict[str, float] = field(default_factory=lambda: {'"CI"': 0.1234})
|
|
212
|
+
DLNIRSP_GRATING_ANGLE_FOR_ARM: dict[str, float] = field(
|
|
213
|
+
default_factory=lambda: {'"VIS"': 2.22, '"JBand"': 2.222, '"HBand"': 2.2222}
|
|
214
|
+
)
|
|
215
|
+
DLNIRSP_ARM_POSITION_FOR_ARM: dict[str, float] = field(
|
|
216
|
+
default_factory=lambda: {'"JBand"': 3.33}
|
|
217
|
+
)
|
|
218
|
+
DLNIRSP_GRATING_CONSTANT_FOR_ARM: dict[str, float] = field(
|
|
219
|
+
default_factory=lambda: {'"HBand"': 4444}
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@pytest.fixture(scope="session")
|
|
224
|
+
def visp_grating_angle() -> float:
|
|
225
|
+
return 1.23
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@pytest.fixture(scope="session")
|
|
229
|
+
def visp_arm_position() -> float:
|
|
230
|
+
return 3.45
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@pytest.fixture(scope="session")
|
|
234
|
+
def visp_grating_constant() -> float:
|
|
235
|
+
return 6789
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@pytest.fixture(scope="session")
|
|
239
|
+
def cryonirsp_od_filter_name() -> str:
|
|
240
|
+
return "FILTER"
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@pytest.fixture(scope="session")
|
|
244
|
+
def cryonirsp_grating_angle() -> float:
|
|
245
|
+
return 4.56
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@pytest.fixture(scope="session")
|
|
249
|
+
def cryonirsp_grating_littrow_angle() -> float:
|
|
250
|
+
return 5.67
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@pytest.fixture(scope="session")
|
|
254
|
+
def cryonirsp_grating_constant() -> float:
|
|
255
|
+
return 7.89
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@pytest.fixture(scope="session")
|
|
259
|
+
def cryonirsp_slit_width() -> float:
|
|
260
|
+
return 0.3
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@pytest.fixture(scope="session")
|
|
264
|
+
def dlnirsp_grating_angle() -> float:
|
|
265
|
+
return 3.21
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@pytest.fixture(scope="session")
|
|
269
|
+
def dlnirsp_arm_position() -> float:
|
|
270
|
+
return 5.43
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@pytest.fixture(scope="session")
|
|
274
|
+
def dlnirsp_grating_constant() -> float:
|
|
275
|
+
return 9876
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@pytest.fixture(scope="session")
|
|
279
|
+
def input_dataset_document_simple_parameters_part():
|
|
280
|
+
def get_input_dataset_parameters_part(parameters: TrendInputDatasetParameterValues):
|
|
281
|
+
parameters_list = []
|
|
282
|
+
value_id = randint(1000, 2000)
|
|
283
|
+
for pn, pv in asdict(parameters).items():
|
|
284
|
+
values = [
|
|
285
|
+
{
|
|
286
|
+
"parameterValueId": value_id,
|
|
287
|
+
"parameterValue": json.dumps(pv),
|
|
288
|
+
"parameterValueStartDate": "1946-11-20", # Remember Duane Allman
|
|
289
|
+
}
|
|
290
|
+
]
|
|
291
|
+
parameter = {"parameterName": pn, "parameterValues": values}
|
|
292
|
+
parameters_list.append(parameter)
|
|
293
|
+
return parameters_list
|
|
294
|
+
|
|
295
|
+
return get_input_dataset_parameters_part
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@pytest.fixture(scope="session")
|
|
299
|
+
def assign_input_dataset_doc_to_task(
|
|
300
|
+
input_dataset_document_simple_parameters_part,
|
|
301
|
+
):
|
|
302
|
+
# NOTE: Unlike the other instruments, this fixture does NOT set the `task.parameters` attribute; it only writes the
|
|
303
|
+
# doc to scratch and tags it.
|
|
304
|
+
# This means you'll need to add `task.parameters = ParameterClass(...)` after using this fixture
|
|
305
|
+
def update_task(
|
|
306
|
+
task: WorkflowTaskBase,
|
|
307
|
+
parameter_values,
|
|
308
|
+
):
|
|
309
|
+
task.write(
|
|
310
|
+
data=InputDatasetPartDocumentList(
|
|
311
|
+
doc_list=input_dataset_document_simple_parameters_part(parameter_values)
|
|
312
|
+
),
|
|
313
|
+
tags=TrendTag.input_dataset_parameters(),
|
|
314
|
+
encoder=basemodel_encoder,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
return update_task
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
@pytest.fixture
|
|
321
|
+
def link_constants_db():
|
|
322
|
+
def constants_linker(recipe_run_id: int, constants_obj):
|
|
323
|
+
"""Take a dataclass (or dict) containing a constants DB and link it to a specific recipe run id."""
|
|
324
|
+
if is_dataclass(constants_obj):
|
|
325
|
+
constants_obj = asdict(constants_obj)
|
|
326
|
+
constants = TrendConstants(recipe_run_id=recipe_run_id, task_name="test")
|
|
327
|
+
constants._purge()
|
|
328
|
+
constants._update(constants_obj)
|
|
329
|
+
|
|
330
|
+
return constants_linker
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class ModulatedCSStepHeaders(Spec122Dataset):
|
|
334
|
+
def __init__(
|
|
335
|
+
self,
|
|
336
|
+
instrument: InstrumentName,
|
|
337
|
+
arm_id: int | str,
|
|
338
|
+
wavelength: float,
|
|
339
|
+
num_modstates: int,
|
|
340
|
+
pol_status: str,
|
|
341
|
+
pol_theta: float,
|
|
342
|
+
ret_status: str,
|
|
343
|
+
ret_theta: float,
|
|
344
|
+
dark_status: str,
|
|
345
|
+
cs_step_num: int,
|
|
346
|
+
array_shape: tuple[int, int],
|
|
347
|
+
exp_time_ms: float,
|
|
348
|
+
start_date: str = "2023-01-01T01:23:45",
|
|
349
|
+
modstate_length_sec: float = 0.5,
|
|
350
|
+
num_frames_per_cs_step: int = 1,
|
|
351
|
+
op_id: str = "Cool_OP",
|
|
352
|
+
experiment_id: str = "exp1",
|
|
353
|
+
proposal_id: str = "prop_1",
|
|
354
|
+
hls_version: str = "hls_v1",
|
|
355
|
+
visp_grating_angle: float = 6.28,
|
|
356
|
+
visp_arm_position: float = 6.2831,
|
|
357
|
+
visp_grating_constant: float = 62831,
|
|
358
|
+
cryonirsp_od_filter_name: str = "cryonirsp_filter",
|
|
359
|
+
cryonirsp_grating_angle: float = 8.67,
|
|
360
|
+
cryonirsp_grating_littrow_angle: float = 6.78,
|
|
361
|
+
cryonirsp_grating_constant: float = 7890,
|
|
362
|
+
cryonirsp_slit_width: float = 0.123,
|
|
363
|
+
dlnirsp_grating_angle: float = 11.22,
|
|
364
|
+
dlnirsp_arm_position: float = 22.11,
|
|
365
|
+
dlnirsp_grating_constant: float = 1231,
|
|
366
|
+
):
|
|
367
|
+
indices = list(product(range(1, num_modstates + 1), range(num_frames_per_cs_step)))
|
|
368
|
+
|
|
369
|
+
self.index_to_modstate = [ms for (ms, _) in indices]
|
|
370
|
+
self.index_to_cs_step_frame = [csf for (_, csf) in indices]
|
|
371
|
+
|
|
372
|
+
# This doesn't become a header key, but helps with making fake data
|
|
373
|
+
self.cs_step_num = cs_step_num
|
|
374
|
+
self.pol_status = pol_status
|
|
375
|
+
self.pol_theta = pol_theta
|
|
376
|
+
self.ret_status = ret_status
|
|
377
|
+
self.ret_theta = ret_theta
|
|
378
|
+
self.dark_status = dark_status
|
|
379
|
+
self.start_date = Time(start_date)
|
|
380
|
+
self.modstate_length_sec = modstate_length_sec
|
|
381
|
+
self.num_frames_per_cs_step = num_frames_per_cs_step
|
|
382
|
+
|
|
383
|
+
if len(array_shape) == 3:
|
|
384
|
+
if array_shape[0] != 1:
|
|
385
|
+
raise ValueError(f"{array_shape = } is weird")
|
|
386
|
+
elif len(array_shape) == 2:
|
|
387
|
+
array_shape = (1, *array_shape)
|
|
388
|
+
|
|
389
|
+
dataset_shape = (num_modstates * self.num_frames_per_cs_step, *array_shape)
|
|
390
|
+
inst_schema_table_name = InstrumentName(instrument).name
|
|
391
|
+
super().__init__(
|
|
392
|
+
instrument=inst_schema_table_name,
|
|
393
|
+
dataset_shape=dataset_shape,
|
|
394
|
+
array_shape=array_shape,
|
|
395
|
+
start_time=datetime.fromisoformat(start_date),
|
|
396
|
+
time_delta=modstate_length_sec,
|
|
397
|
+
file_schema="level0_spec214",
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
self.add_constant_key("WAVELNTH", wavelength)
|
|
401
|
+
self.add_constant_key("DKIST004", "polcal")
|
|
402
|
+
self.add_constant_key("CAM__004", exp_time_ms)
|
|
403
|
+
self.add_constant_key("ID___008", op_id)
|
|
404
|
+
self.add_constant_key("ID___012", experiment_id)
|
|
405
|
+
self.add_constant_key("ID___013", proposal_id)
|
|
406
|
+
self.add_constant_key("ID___014", hls_version)
|
|
407
|
+
|
|
408
|
+
# These should all be 122 keys
|
|
409
|
+
if instrument == InstrumentName.visp:
|
|
410
|
+
self.add_constant_key("VISP_001", arm_id)
|
|
411
|
+
self.add_constant_key("VISP_010", num_modstates)
|
|
412
|
+
self.add_generator_function(key="VISP_011", function=modstate_key_function)
|
|
413
|
+
self.add_constant_key("VISP_002", visp_arm_position)
|
|
414
|
+
self.add_constant_key("VISP_013", visp_grating_constant)
|
|
415
|
+
self.add_constant_key("VISP_015", visp_grating_angle)
|
|
416
|
+
|
|
417
|
+
if instrument == InstrumentName.cryonirsp:
|
|
418
|
+
self.add_constant_key("CRSP_001", arm_id)
|
|
419
|
+
self.add_constant_key("CRSP_041", num_modstates)
|
|
420
|
+
self.add_generator_function(key="CRSP_042", function=modstate_key_function)
|
|
421
|
+
self.add_constant_key("CRSP_048", cryonirsp_od_filter_name)
|
|
422
|
+
self.add_constant_key("CRSP_074", cryonirsp_grating_angle)
|
|
423
|
+
self.add_constant_key("CRSP_077", cryonirsp_grating_constant)
|
|
424
|
+
self.add_constant_key("CRSP_079", cryonirsp_grating_littrow_angle)
|
|
425
|
+
self.add_constant_key("CRSP_082", cryonirsp_slit_width)
|
|
426
|
+
|
|
427
|
+
if instrument == InstrumentName.dlnirsp:
|
|
428
|
+
self.add_constant_key("DLN__001", arm_id)
|
|
429
|
+
self.add_constant_key("DLN__014", num_modstates)
|
|
430
|
+
self.add_generator_function(key="DLN__015", function=modstate_key_function)
|
|
431
|
+
self.add_constant_key("DLN__002", dlnirsp_arm_position)
|
|
432
|
+
self.add_constant_key("DLN__017", dlnirsp_grating_constant)
|
|
433
|
+
self.add_constant_key("DLN__019", dlnirsp_grating_angle)
|
|
434
|
+
|
|
435
|
+
@key_function("PAC__004")
|
|
436
|
+
def polarizer_status(self, key: str) -> str:
|
|
437
|
+
return self.pol_status
|
|
438
|
+
|
|
439
|
+
@key_function("PAC__005")
|
|
440
|
+
def polarizer_angle(self, key: str) -> float | str:
|
|
441
|
+
return "none" if self.pol_status == "clear" else self.pol_theta
|
|
442
|
+
|
|
443
|
+
@key_function("PAC__006")
|
|
444
|
+
def retarder_status(self, key: str) -> str:
|
|
445
|
+
return self.ret_status
|
|
446
|
+
|
|
447
|
+
@key_function("PAC__007")
|
|
448
|
+
def retarder_angle(self, key: str) -> float | str:
|
|
449
|
+
return "none" if self.ret_status == "clear" else self.ret_theta
|
|
450
|
+
|
|
451
|
+
@key_function("PAC__008")
|
|
452
|
+
def gos_level3_status(self, key: str) -> str:
|
|
453
|
+
return self.dark_status
|
|
454
|
+
|
|
455
|
+
@key_function("TAZIMUTH", "TELEVATN", "TTBLANGL")
|
|
456
|
+
def telescope_geometry(self, key: str):
|
|
457
|
+
return compute_telgeom(Time(self.date_obs(key), format="fits"))[key]
|
|
458
|
+
|
|
459
|
+
@property
|
|
460
|
+
def current_cs_step_frame(self) -> int:
|
|
461
|
+
# There is no header value for CS step frame; used to help data generator fixtures with tagging
|
|
462
|
+
return self.index_to_cs_step_frame[self.index]
|
|
463
|
+
|
|
464
|
+
@property
|
|
465
|
+
def current_modstate(self) -> int:
|
|
466
|
+
# This is a property because the actual key generator function is added in __init__ because the associated
|
|
467
|
+
# key is not static but based on the instrument.
|
|
468
|
+
# This property is here for convenience in tests and fixtures
|
|
469
|
+
return self.index_to_modstate[self.index]
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def modstate_key_function(dataset: ModulatedCSStepHeaders, key: str) -> int:
|
|
473
|
+
return dataset.current_modstate
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def compute_telgeom(time_hst: Time):
|
|
477
|
+
dkist_lon = (156 + 15 / 60.0 + 21.7 / 3600.0) * (-1)
|
|
478
|
+
dkist_lat = 20 + 42 / 60.0 + 27.0 / 3600.0
|
|
479
|
+
hel = 3040.4
|
|
480
|
+
hloc = coordinates.EarthLocation.from_geodetic(dkist_lon, dkist_lat, hel)
|
|
481
|
+
sun_body = coordinates.get_body("sun", time_hst, hloc) # get the solar ephemeris
|
|
482
|
+
azel_frame = coordinates.AltAz(obstime=time_hst, location=hloc) # Horizon coords
|
|
483
|
+
sun_altaz = sun_body.transform_to(azel_frame) # Sun in horizon coords
|
|
484
|
+
alt = sun_altaz.alt.value # Extract altitude
|
|
485
|
+
azi = sun_altaz.az.value # Extract azimuth
|
|
486
|
+
|
|
487
|
+
tableang = alt - azi
|
|
488
|
+
|
|
489
|
+
return {"TELEVATN": alt, "TAZIMUTH": azi, "TTBLANGL": tableang}
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def make_random_data(frame: Spec122Dataset) -> np.ndarray:
|
|
493
|
+
shape = frame.array_shape[1:]
|
|
494
|
+
data = np.random.random(shape)
|
|
495
|
+
|
|
496
|
+
return data
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def make_cs_data(
|
|
500
|
+
frame: ModulatedCSStepHeaders, dark_signal: float, clear_signal: float
|
|
501
|
+
) -> np.ndarray:
|
|
502
|
+
|
|
503
|
+
shape = frame.array_shape[1:]
|
|
504
|
+
clear_signal += frame.current_modstate
|
|
505
|
+
if frame.pol_status == "clear" and frame.ret_status == "clear":
|
|
506
|
+
if frame.dark_status == "DarkShutter":
|
|
507
|
+
value = dark_signal
|
|
508
|
+
else:
|
|
509
|
+
value = clear_signal + dark_signal
|
|
510
|
+
else:
|
|
511
|
+
value = (
|
|
512
|
+
frame.cs_step_num * 10000.0 + frame.current_modstate * 100.0
|
|
513
|
+
) * clear_signal + dark_signal
|
|
514
|
+
|
|
515
|
+
data = np.full(shape, value)
|
|
516
|
+
|
|
517
|
+
return data
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def tag_on_modstate(frame: ModulatedCSStepHeaders) -> list[str]:
|
|
521
|
+
modstate = frame.current_modstate
|
|
522
|
+
return [TrendTag.modstate(modstate)]
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def write_frames_to_task(
|
|
526
|
+
task: WorkflowTaskBase,
|
|
527
|
+
frame_generator: Spec122Dataset,
|
|
528
|
+
data_func: callable = make_random_data,
|
|
529
|
+
extra_tags: list[str] | None = None,
|
|
530
|
+
tag_func: callable = lambda x: [],
|
|
531
|
+
compress_files: bool = False,
|
|
532
|
+
):
|
|
533
|
+
if not extra_tags:
|
|
534
|
+
extra_tags = []
|
|
535
|
+
tags = [TrendTag.frame()] + extra_tags
|
|
536
|
+
|
|
537
|
+
num_frames = 0
|
|
538
|
+
for frame in frame_generator:
|
|
539
|
+
header = frame.header()
|
|
540
|
+
data = data_func(frame)
|
|
541
|
+
frame_tags = tags + tag_func(frame)
|
|
542
|
+
if compress_files:
|
|
543
|
+
hdul = fits.HDUList([fits.PrimaryHDU(), fits.CompImageHDU(data=data, header=header)])
|
|
544
|
+
task.write(data=hdul, tags=frame_tags, encoder=fits_hdulist_encoder)
|
|
545
|
+
else:
|
|
546
|
+
task.write(data=data, header=header, tags=frame_tags, encoder=fits_array_encoder)
|
|
547
|
+
num_frames += 1
|
|
548
|
+
|
|
549
|
+
return num_frames
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def write_calibration_sequence_frames_to_task(
|
|
553
|
+
task: Type[WorkflowTaskBase],
|
|
554
|
+
instrument: InstrumentName,
|
|
555
|
+
arm_id: int | str,
|
|
556
|
+
wavelength: float,
|
|
557
|
+
pol_status: list[str],
|
|
558
|
+
pol_theta: list[float],
|
|
559
|
+
ret_status: list[str],
|
|
560
|
+
ret_theta: list[float],
|
|
561
|
+
dark_status: list[str],
|
|
562
|
+
num_modstates: int = 4,
|
|
563
|
+
exp_time_ms: float = 7.0,
|
|
564
|
+
dark_signal: float = 5.0,
|
|
565
|
+
clear_signal: float = 10.0,
|
|
566
|
+
array_shape: tuple[int, ...] = (10, 10),
|
|
567
|
+
num_frames_per_cs_step: int = 1,
|
|
568
|
+
input_tags: list[str] | None = None,
|
|
569
|
+
data_func: callable = None,
|
|
570
|
+
tags: list[str] | None = None,
|
|
571
|
+
op_start_time: str = "2023-01-01T01:23:45",
|
|
572
|
+
op_id: str = "Cool_OPID",
|
|
573
|
+
experiment_id: str = "cool exp",
|
|
574
|
+
proposal_id: str = "very cool proposal",
|
|
575
|
+
hls_version: str = "HLS_VERSION_x.x",
|
|
576
|
+
visp_grating_angle: float = 3.14,
|
|
577
|
+
visp_arm_position: float = 3.14159,
|
|
578
|
+
visp_grating_constant: float = 31415,
|
|
579
|
+
cryonirsp_od_filter_name: str = "cryonirsp_filter",
|
|
580
|
+
cryonirsp_grating_angle: float = 2.34,
|
|
581
|
+
cryonirsp_grating_littrow_angle: float = 3.45,
|
|
582
|
+
cryonirsp_grating_constant: float = 4.56,
|
|
583
|
+
cryonirsp_slit_width: float = 0.45,
|
|
584
|
+
dlnirsp_grating_angle: float = 4.55,
|
|
585
|
+
dlnirsp_arm_position: float = 4.5678,
|
|
586
|
+
dlnirsp_grating_constant: float = 5678,
|
|
587
|
+
) -> int:
|
|
588
|
+
"""
|
|
589
|
+
Write a full Calibration Sequence to a task's scratch.
|
|
590
|
+
|
|
591
|
+
If `input_tags` is specified then the files will be written with ONLY those tags. Otherwise they will always
|
|
592
|
+
have CS_STEP, MODSTATE, TASK_POLCAL, and (maybe) TASK_POLCAL_DARK or TASK_POLCAL_GAIN tags along with whatever is in
|
|
593
|
+
`extra_tags`. The FRAME tag is *always* included.
|
|
594
|
+
"""
|
|
595
|
+
if data_func is None:
|
|
596
|
+
data_func = partial(make_cs_data, dark_signal=dark_signal, clear_signal=clear_signal)
|
|
597
|
+
|
|
598
|
+
num_frames = 0
|
|
599
|
+
base_time = Time(op_start_time)
|
|
600
|
+
|
|
601
|
+
for step, (pol_s, pol_t, ret_s, ret_t, dark_s) in enumerate(
|
|
602
|
+
zip(pol_status, pol_theta, ret_status, ret_theta, dark_status)
|
|
603
|
+
):
|
|
604
|
+
step_start = (base_time + (step * 60) * u.s).fits
|
|
605
|
+
|
|
606
|
+
dataset = ModulatedCSStepHeaders(
|
|
607
|
+
instrument=instrument,
|
|
608
|
+
arm_id=arm_id,
|
|
609
|
+
wavelength=wavelength,
|
|
610
|
+
num_modstates=num_modstates,
|
|
611
|
+
pol_status=pol_s,
|
|
612
|
+
pol_theta=pol_t,
|
|
613
|
+
ret_status=ret_s,
|
|
614
|
+
ret_theta=ret_t,
|
|
615
|
+
dark_status=dark_s,
|
|
616
|
+
cs_step_num=step,
|
|
617
|
+
array_shape=array_shape,
|
|
618
|
+
exp_time_ms=exp_time_ms,
|
|
619
|
+
num_frames_per_cs_step=num_frames_per_cs_step,
|
|
620
|
+
start_date=step_start,
|
|
621
|
+
op_id=op_id,
|
|
622
|
+
experiment_id=experiment_id,
|
|
623
|
+
proposal_id=proposal_id,
|
|
624
|
+
hls_version=hls_version,
|
|
625
|
+
visp_grating_angle=visp_grating_angle,
|
|
626
|
+
visp_arm_position=visp_arm_position,
|
|
627
|
+
visp_grating_constant=visp_grating_constant,
|
|
628
|
+
cryonirsp_od_filter_name=cryonirsp_od_filter_name,
|
|
629
|
+
cryonirsp_grating_angle=cryonirsp_grating_angle,
|
|
630
|
+
cryonirsp_grating_littrow_angle=cryonirsp_grating_littrow_angle,
|
|
631
|
+
cryonirsp_grating_constant=cryonirsp_grating_constant,
|
|
632
|
+
cryonirsp_slit_width=cryonirsp_slit_width,
|
|
633
|
+
dlnirsp_grating_angle=dlnirsp_grating_angle,
|
|
634
|
+
dlnirsp_arm_position=dlnirsp_arm_position,
|
|
635
|
+
dlnirsp_grating_constant=dlnirsp_grating_constant,
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
if tags is None:
|
|
639
|
+
tags = []
|
|
640
|
+
if input_tags:
|
|
641
|
+
tag_func = lambda x: []
|
|
642
|
+
extra_tags = input_tags
|
|
643
|
+
else:
|
|
644
|
+
tag_func = tag_on_modstate
|
|
645
|
+
extra_tags = list(
|
|
646
|
+
set(
|
|
647
|
+
tags
|
|
648
|
+
+ [
|
|
649
|
+
TrendTag.cs_step(step),
|
|
650
|
+
TrendTag.task_polcal(),
|
|
651
|
+
]
|
|
652
|
+
)
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
if pol_s == "clear" and ret_s == "clear":
|
|
656
|
+
if dark_s == "DarkShutter":
|
|
657
|
+
extra_tags += [TrendTag.task_polcal_dark()]
|
|
658
|
+
else:
|
|
659
|
+
extra_tags += [TrendTag.task_polcal_gain()]
|
|
660
|
+
|
|
661
|
+
num_frames += write_frames_to_task(
|
|
662
|
+
task=task,
|
|
663
|
+
frame_generator=dataset,
|
|
664
|
+
data_func=data_func,
|
|
665
|
+
extra_tags=extra_tags,
|
|
666
|
+
tag_func=tag_func,
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
return num_frames
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
@pytest.fixture(scope="session")
|
|
673
|
+
def small_calibration_sequence() -> tuple[list, list, list, list, list]:
|
|
674
|
+
# Make up a Calibration sequence. Mostly random except for two clears and two darks at start and end
|
|
675
|
+
pol_status = [
|
|
676
|
+
"clear",
|
|
677
|
+
"clear",
|
|
678
|
+
"Sapphire Polarizer",
|
|
679
|
+
"Sapphire Polarizer",
|
|
680
|
+
"Sapphire Polarizer",
|
|
681
|
+
"clear",
|
|
682
|
+
"clear",
|
|
683
|
+
]
|
|
684
|
+
pol_theta = [0.0, 0.0, 60.0, 0.0, 120.0, 0.0, 0.0]
|
|
685
|
+
ret_status = ["clear", "clear", "clear", "SiO2 SAR", "clear", "clear", "clear"]
|
|
686
|
+
ret_theta = [0.0, 0.0, 0.0, 45.0, 0.0, 0.0, 0.0]
|
|
687
|
+
dark_status = [
|
|
688
|
+
"DarkShutter",
|
|
689
|
+
"FieldStop (5arcmin)",
|
|
690
|
+
"FieldStop (5arcmin)",
|
|
691
|
+
"FieldStop (5arcmin)",
|
|
692
|
+
"FieldStop (5arcmin)",
|
|
693
|
+
"FieldStop (5arcmin)",
|
|
694
|
+
"DarkShutter",
|
|
695
|
+
]
|
|
696
|
+
|
|
697
|
+
return pol_status, pol_theta, ret_status, ret_theta, dark_status
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
@pytest.fixture(scope="session")
|
|
701
|
+
def small_num_cs_steps(small_calibration_sequence) -> int:
|
|
702
|
+
return len(small_calibration_sequence[0])
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
@pytest.fixture(scope="session")
|
|
706
|
+
def small_num_gain_steps(small_calibration_sequence) -> int:
|
|
707
|
+
num_gains = 0
|
|
708
|
+
for ps, pt, rs, rt, ds in zip(*small_calibration_sequence):
|
|
709
|
+
if ps == "clear" and rs == "clear" and ds != "DarkShutter":
|
|
710
|
+
num_gains += 1
|
|
711
|
+
|
|
712
|
+
return num_gains
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
@pytest.fixture(scope="session")
|
|
716
|
+
def small_num_dark_steps(small_calibration_sequence) -> int:
|
|
717
|
+
dark_status = small_calibration_sequence[-1]
|
|
718
|
+
return dark_status.count("DarkShutter")
|