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.
Files changed (66) hide show
  1. changelog/.gitempty +0 -0
  2. dkist_processing_trend/__init__.py +10 -0
  3. dkist_processing_trend/config.py +11 -0
  4. dkist_processing_trend/models/__init__.py +1 -0
  5. dkist_processing_trend/models/constants.py +143 -0
  6. dkist_processing_trend/models/fit_options.py +15 -0
  7. dkist_processing_trend/models/fits_access.py +65 -0
  8. dkist_processing_trend/models/instrument.py +35 -0
  9. dkist_processing_trend/models/instrument_options.py +35 -0
  10. dkist_processing_trend/models/parameters.py +134 -0
  11. dkist_processing_trend/models/tags.py +117 -0
  12. dkist_processing_trend/models/task_name.py +20 -0
  13. dkist_processing_trend/parsers/__init__.py +1 -0
  14. dkist_processing_trend/parsers/arm_id.py +103 -0
  15. dkist_processing_trend/parsers/instrument_unique_bud.py +40 -0
  16. dkist_processing_trend/parsers/time.py +27 -0
  17. dkist_processing_trend/parsers/trend_l0_fits_access.py +123 -0
  18. dkist_processing_trend/tasks/__init__.py +27 -0
  19. dkist_processing_trend/tasks/arm_task_factory.py +57 -0
  20. dkist_processing_trend/tasks/dark.py +56 -0
  21. dkist_processing_trend/tasks/gain.py +74 -0
  22. dkist_processing_trend/tasks/initialize_arm_tasks.py +50 -0
  23. dkist_processing_trend/tasks/parse.py +169 -0
  24. dkist_processing_trend/tasks/prepare_fit_data_base.py +257 -0
  25. dkist_processing_trend/tasks/run_pac_fitter.py +392 -0
  26. dkist_processing_trend/tasks/trend_base.py +97 -0
  27. dkist_processing_trend/tasks/trend_output_data.py +167 -0
  28. dkist_processing_trend/tasks/visp/__init__.py +6 -0
  29. dkist_processing_trend/tasks/visp/visp_dmpd.py +410 -0
  30. dkist_processing_trend/tasks/visp/visp_extract_beam.py +14 -0
  31. dkist_processing_trend/tasks/visp/visp_geometric.py +260 -0
  32. dkist_processing_trend/tasks/visp/visp_prep_fit_data.py +162 -0
  33. dkist_processing_trend/tasks/visp/visp_process_demod.py +236 -0
  34. dkist_processing_trend/tasks/write_trend.py +663 -0
  35. dkist_processing_trend/tests/__init__.py +1 -0
  36. dkist_processing_trend/tests/conftest.py +718 -0
  37. dkist_processing_trend/tests/local_trial_workflows/__init__.py +0 -0
  38. dkist_processing_trend/tests/local_trial_workflows/l0_to_trend_visp_polcal.py +294 -0
  39. dkist_processing_trend/tests/local_trial_workflows/local_trial_helpers.py +488 -0
  40. dkist_processing_trend/tests/test_arm_task_factory.py +82 -0
  41. dkist_processing_trend/tests/test_base_tasks.py +86 -0
  42. dkist_processing_trend/tests/test_constants.py +120 -0
  43. dkist_processing_trend/tests/test_dark.py +97 -0
  44. dkist_processing_trend/tests/test_gain.py +135 -0
  45. dkist_processing_trend/tests/test_parameters.py +149 -0
  46. dkist_processing_trend/tests/test_parse.py +276 -0
  47. dkist_processing_trend/tests/test_prep_fit_data_base.py +233 -0
  48. dkist_processing_trend/tests/test_publish_catalog_messages.py +45 -0
  49. dkist_processing_trend/tests/test_run_pac_fitter.py +371 -0
  50. dkist_processing_trend/tests/test_stems.py +75 -0
  51. dkist_processing_trend/tests/test_transfer_output_data.py +76 -0
  52. dkist_processing_trend/tests/test_trend_fits_access.py +173 -0
  53. dkist_processing_trend/tests/test_visp.py +874 -0
  54. dkist_processing_trend/tests/test_workflows.py +10 -0
  55. dkist_processing_trend/tests/test_write_trend.py +460 -0
  56. dkist_processing_trend/workflows/__init__.py +3 -0
  57. dkist_processing_trend/workflows/visp.py +58 -0
  58. dkist_processing_trend-0.1.0.dist-info/METADATA +549 -0
  59. dkist_processing_trend-0.1.0.dist-info/RECORD +66 -0
  60. dkist_processing_trend-0.1.0.dist-info/WHEEL +5 -0
  61. dkist_processing_trend-0.1.0.dist-info/top_level.txt +3 -0
  62. docs/conf.py +57 -0
  63. docs/index.rst +10 -0
  64. docs/l0_to_trend_visp_polcal.rst +4 -0
  65. docs/landing_page.rst +11 -0
  66. 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")