dkist-processing-visp 4.0.0__py3-none-any.whl → 5.0.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 (32) hide show
  1. dkist_processing_visp/models/constants.py +50 -9
  2. dkist_processing_visp/models/fits_access.py +5 -1
  3. dkist_processing_visp/models/metric_code.py +10 -0
  4. dkist_processing_visp/models/parameters.py +128 -19
  5. dkist_processing_visp/parsers/spectrograph_configuration.py +75 -0
  6. dkist_processing_visp/parsers/visp_l0_fits_access.py +6 -0
  7. dkist_processing_visp/tasks/geometric.py +115 -7
  8. dkist_processing_visp/tasks/l1_output_data.py +202 -0
  9. dkist_processing_visp/tasks/lamp.py +50 -91
  10. dkist_processing_visp/tasks/parse.py +19 -0
  11. dkist_processing_visp/tasks/science.py +14 -14
  12. dkist_processing_visp/tasks/solar.py +894 -451
  13. dkist_processing_visp/tasks/visp_base.py +1 -0
  14. dkist_processing_visp/tests/conftest.py +98 -35
  15. dkist_processing_visp/tests/header_models.py +71 -20
  16. dkist_processing_visp/tests/local_trial_workflows/local_trial_helpers.py +25 -1
  17. dkist_processing_visp/tests/test_assemble_quality.py +89 -4
  18. dkist_processing_visp/tests/test_geometric.py +40 -0
  19. dkist_processing_visp/tests/test_instrument_polarization.py +2 -1
  20. dkist_processing_visp/tests/test_lamp.py +17 -22
  21. dkist_processing_visp/tests/test_parameters.py +120 -18
  22. dkist_processing_visp/tests/test_parse.py +73 -1
  23. dkist_processing_visp/tests/test_science.py +5 -6
  24. dkist_processing_visp/tests/test_solar.py +319 -102
  25. dkist_processing_visp/tests/test_visp_constants.py +35 -6
  26. {dkist_processing_visp-4.0.0.dist-info → dkist_processing_visp-5.0.0.dist-info}/METADATA +40 -37
  27. {dkist_processing_visp-4.0.0.dist-info → dkist_processing_visp-5.0.0.dist-info}/RECORD +31 -30
  28. docs/conf.py +4 -1
  29. docs/gain_correction.rst +50 -42
  30. dkist_processing_visp/tasks/mixin/line_zones.py +0 -116
  31. {dkist_processing_visp-4.0.0.dist-info → dkist_processing_visp-5.0.0.dist-info}/WHEEL +0 -0
  32. {dkist_processing_visp-4.0.0.dist-info → dkist_processing_visp-5.0.0.dist-info}/top_level.txt +0 -0
@@ -45,4 +45,5 @@ class VispTaskBase(WorkflowTaskBase, ABC):
45
45
  scratch=self.scratch,
46
46
  obs_ip_start_time=self.constants.obs_ip_start_time,
47
47
  wavelength=self.constants.wavelength,
48
+ arm_id=self.constants.arm_id,
48
49
  )
@@ -1,8 +1,10 @@
1
1
  import json
2
2
  from dataclasses import asdict
3
3
  from dataclasses import dataclass
4
+ from dataclasses import field
4
5
  from dataclasses import is_dataclass
5
6
  from random import randint
7
+ from typing import Any
6
8
  from typing import Callable
7
9
  from typing import Type
8
10
 
@@ -17,6 +19,8 @@ from dkist_processing_common.codecs.fits import fits_array_encoder
17
19
  from dkist_processing_common.codecs.fits import fits_hdulist_encoder
18
20
  from dkist_processing_common.models.input_dataset import InputDatasetPartDocumentList
19
21
  from dkist_processing_common.tasks import WorkflowTaskBase
22
+
23
+ # Don't remove this; tests will break
20
24
  from dkist_processing_common.tests.mock_metadata_store import fake_gql_client
21
25
 
22
26
  from dkist_processing_visp.models.constants import VispConstants
@@ -41,6 +45,7 @@ def init_visp_constants_db():
41
45
 
42
46
  @dataclass
43
47
  class VispConstantsDb:
48
+ ARM_ID: int = 1
44
49
  POLARIMETER_MODE: str = "observe_polarimetric"
45
50
  OBS_IP_START_TIME: str = "2022-11-28T13:54:00"
46
51
  NUM_MODSTATES: int = 10
@@ -66,6 +71,10 @@ class VispConstantsDb:
66
71
  OBSERVE_READOUT_EXP_TIMES: tuple[float] = (0.02,)
67
72
  POLCAL_READOUT_EXP_TIMES: tuple[float] = (0.02,)
68
73
  SPECTRAL_LINE: str = "VISP Ca II H"
74
+ INCIDENT_LIGHT_ANGLE_DEG: float = 73.22
75
+ REFLECTED_LIGHT_ANGLE_DEG: float = 64.92
76
+ GRATING_CONSTANT_INVERSE_MM: float = 316.0
77
+ SOLAR_GAIN_IP_START_TIME: str = "2025-09-24T20:00:00"
69
78
  STOKES_PARAMS: tuple[str] = (
70
79
  "I",
71
80
  "Q",
@@ -128,19 +137,59 @@ class VispInputDatasetParameterValues:
128
137
  visp_geo_upsample_factor: float = 10.0
129
138
  visp_geo_max_shift: float = 40.0
130
139
  visp_geo_poly_fit_order: int = 3
131
- visp_solar_spectral_avg_window: WavelengthParameter = WavelengthParameter(
132
- values=(800, 800, 800, 800)
140
+ visp_geo_zone_prominence: WavelengthParameter = WavelengthParameter(values=(0.2, 0.2, 0.3, 0.2))
141
+ visp_geo_zone_width: WavelengthParameter = WavelengthParameter(values=(7, 2, 3, 2))
142
+ visp_geo_zone_bg_order: WavelengthParameter = WavelengthParameter(values=(21, 22, 11, 22))
143
+ visp_geo_zone_normalization_percentile: WavelengthParameter = WavelengthParameter(
144
+ values=(90, 99, 90, 90)
145
+ )
146
+ visp_geo_zone_rel_height: float = 0.97
147
+ visp_solar_spatial_median_filter_width_px: WavelengthParameter = WavelengthParameter(
148
+ values=(250, 250, 250, 250)
133
149
  )
134
150
  visp_solar_characteristic_spatial_normalization_percentile: float = 90.0
135
- visp_solar_zone_prominence: WavelengthParameter = WavelengthParameter(
136
- values=(0.2, 0.2, 0.3, 0.2)
151
+ visp_solar_vignette_initial_continuum_poly_fit_order: int = 6
152
+ visp_solar_vignette_crval_bounds_px: float = 7
153
+ visp_solar_vignette_dispersion_bounds_fraction: float = 0.02
154
+ visp_solar_vignette_wavecal_fit_kwargs: dict[str, Any] = field(
155
+ default_factory=lambda: {
156
+ "method": "differential_evolution",
157
+ "init": "halton",
158
+ "popsize": 1,
159
+ "tol": 1e-10,
160
+ }
137
161
  )
138
- visp_solar_zone_width: WavelengthParameter = WavelengthParameter(values=(7, 2, 3, 2))
139
- visp_solar_zone_bg_order: WavelengthParameter = WavelengthParameter(values=(21, 22, 11, 22))
140
- visp_solar_zone_normalization_percentile: WavelengthParameter = WavelengthParameter(
141
- values=(90, 99, 90, 90)
162
+ visp_solar_vignette_spectral_poly_fit_order: int = 12
163
+ visp_solar_vignette_min_samples: float = 0.9
164
+ visp_wavecal_camera_lens_parameters_1: tuple[float, float, float] = (
165
+ 0.7613,
166
+ 1.720e-4,
167
+ -8.139e-8,
168
+ )
169
+ visp_wavecal_camera_lens_parameters_2: tuple[float, float, float] = (
170
+ 0.9512,
171
+ 2.141e-4,
172
+ -1.014e-7,
142
173
  )
143
- visp_solar_zone_rel_height: float = 0.97
174
+ visp_wavecal_camera_lens_parameters_3: tuple[float, float, float] = (
175
+ 0.1153e1,
176
+ 2.595e-4,
177
+ -1.230e-7,
178
+ )
179
+ visp_wavecal_pixel_pitch_micron_per_pix: float = 6.5
180
+ visp_wavecal_atlas_download_config: dict[str, str] = field(
181
+ default_factory=lambda: {
182
+ "base_url": "doi:10.5281/zenodo.14646787/",
183
+ "telluric_reference_atlas_file_name": "telluric_reference_atlas.npy",
184
+ "telluric_reference_atlas_hash_id": "md5:8db5e12508b293bca3495d81a0747447",
185
+ "solar_reference_atlas_file_name": "solar_reference_atlas.npy",
186
+ "solar_reference_atlas_hash_id": "md5:84ab4c50689ef235fe5ed4f7ee905ca0",
187
+ }
188
+ )
189
+ visp_wavecal_init_crval_guess_normalization_percentile: float = 95
190
+ visp_wavecal_init_resolving_power: int = 150000
191
+ visp_wavecal_init_straylight_fraction: float = 0.2
192
+ visp_wavecal_init_opacity_factor: float = 5.0
144
193
  visp_polcal_spatial_median_filter_width_px: int = 10
145
194
  visp_polcal_num_spatial_bins: int = 10
146
195
  visp_polcal_demod_spatial_smooth_fit_order: int = 17
@@ -160,6 +209,32 @@ def testing_obs_ip_start_time() -> str:
160
209
  return "1946-11-20T12:34:56"
161
210
 
162
211
 
212
+ @pytest.fixture(scope="session")
213
+ def testing_grating_constant() -> float:
214
+ # Just make it different than the defaults in header_models.py
215
+ return 317.2
216
+
217
+
218
+ @pytest.fixture(scope="session")
219
+ def testing_grating_angle() -> float:
220
+ return -43.2
221
+
222
+
223
+ @pytest.fixture(scope="session")
224
+ def testing_arm_position() -> float:
225
+ return -5.3
226
+
227
+
228
+ @pytest.fixture(scope="session")
229
+ def testing_solar_ip_start_time() -> str:
230
+ return "1946-11-21T12:34:56"
231
+
232
+
233
+ @pytest.fixture(scope="session")
234
+ def testing_arm_id() -> int:
235
+ return 2
236
+
237
+
163
238
  @pytest.fixture(scope="session")
164
239
  def input_dataset_document_simple_parameters_part():
165
240
  """Convert a dataclass of parameterValues into an actual input dataset parameters part."""
@@ -186,13 +261,17 @@ def input_dataset_document_simple_parameters_part():
186
261
 
187
262
  @pytest.fixture(scope="session")
188
263
  def assign_input_dataset_doc_to_task(
189
- input_dataset_document_simple_parameters_part, testing_obs_ip_start_time, testing_wavelength
264
+ input_dataset_document_simple_parameters_part,
265
+ testing_obs_ip_start_time,
266
+ testing_wavelength,
267
+ testing_arm_id,
190
268
  ):
191
269
  def update_task(
192
270
  task: WorkflowTaskBase,
193
271
  parameter_values,
194
272
  parameter_class=VispParameters,
195
273
  obs_ip_start_time=testing_obs_ip_start_time,
274
+ arm_id=testing_arm_id,
196
275
  ):
197
276
  task.write(
198
277
  data=InputDatasetPartDocumentList(
@@ -205,6 +284,7 @@ def assign_input_dataset_doc_to_task(
205
284
  scratch=task.scratch,
206
285
  obs_ip_start_time=obs_ip_start_time,
207
286
  wavelength=testing_wavelength,
287
+ arm_id=str(arm_id),
208
288
  )
209
289
 
210
290
  return update_task
@@ -343,28 +423,14 @@ def write_intermediate_background_to_task(
343
423
  )
344
424
 
345
425
 
346
- def write_intermediate_lamp_to_task(
347
- task, *, lamp_signal: float, beam: int, modstate: int, data_shape: tuple[int, int]
348
- ):
349
- lamp_array = np.ones(data_shape) * lamp_signal
350
- task.write(
351
- data=lamp_array,
352
- tags=[
353
- VispTag.intermediate_frame(beam=beam, modstate=modstate),
354
- VispTag.task_lamp_gain(),
355
- ],
356
- encoder=fits_array_encoder,
357
- )
358
-
359
-
360
426
  def write_intermediate_solar_to_task(
361
- task, *, solar_signal: float, beam: int, modstate: int, data_shape: tuple[int, int]
427
+ task, *, solar_signal: float, beam: int, data_shape: tuple[int, int]
362
428
  ):
363
429
  solar_array = np.ones(data_shape) * solar_signal
364
430
  task.write(
365
431
  data=solar_array,
366
432
  tags=[
367
- VispTag.intermediate_frame(beam=beam, modstate=modstate),
433
+ VispTag.intermediate_frame(beam=beam),
368
434
  VispTag.task_solar_gain(),
369
435
  ],
370
436
  encoder=fits_array_encoder,
@@ -414,19 +480,16 @@ def write_intermediate_geometric_to_task(
414
480
  def write_dummy_intermediate_solar_cals_to_task(
415
481
  task,
416
482
  *,
417
- num_modstates: int,
418
483
  data_shape: tuple[int, int],
419
484
  ):
420
485
  solar_signal = 1.0
421
486
  for beam in [1, 2]:
422
- for modstate in range(1, num_modstates + 1):
423
- write_intermediate_solar_to_task(
424
- task=task,
425
- solar_signal=solar_signal,
426
- beam=beam,
427
- modstate=modstate,
428
- data_shape=data_shape,
429
- )
487
+ write_intermediate_solar_to_task(
488
+ task=task,
489
+ solar_signal=solar_signal,
490
+ beam=beam,
491
+ data_shape=data_shape,
492
+ )
430
493
 
431
494
 
432
495
  def write_intermediate_polcal_darks_to_task(
@@ -31,6 +31,12 @@ class VispHeaders(Spec122Dataset):
31
31
  num_modstates_header_value: int = 2,
32
32
  instrument: str = "visp",
33
33
  polarimeter_mode: str = "observe_polarimetric",
34
+ arm_id: int = 1,
35
+ ip_start_time: str = "2022-11-28T13:00:00",
36
+ ip_end_time: str = "2022-11-28T13:00:00",
37
+ grating_constant: float = 316.0,
38
+ grating_angle: float = -69.9,
39
+ arm_position: float = -4.0,
34
40
  **kwargs,
35
41
  ):
36
42
  super().__init__(
@@ -40,13 +46,21 @@ class VispHeaders(Spec122Dataset):
40
46
  instrument=instrument,
41
47
  **kwargs,
42
48
  )
49
+ self.add_constant_key("VISP_001", arm_id)
43
50
  self.add_constant_key("WAVELNTH", 656.30)
44
51
  self.add_constant_key("VISP_010", num_modstates_header_value)
45
52
  self.add_constant_key("ID___013", "TEST_PROPOSAL_ID")
46
53
  self.add_constant_key("VISP_006", polarimeter_mode)
47
54
  self.add_constant_key("PAC__005", "0")
48
55
  self.add_constant_key("PAC__007", "10")
56
+ self.add_constant_key("DKIST011", ip_start_time)
57
+ self.add_constant_key("DKIST012", ip_end_time)
49
58
  self.add_constant_key("FILE_ID", uuid.uuid4().hex)
59
+
60
+ self.add_constant_key("VISP_002", arm_position)
61
+ self.add_constant_key("VISP_013", grating_constant)
62
+ self.add_constant_key("VISP_015", grating_angle)
63
+
50
64
  self.num_modstates_header_value = num_modstates_header_value
51
65
  self.add_constant_key("CAM__001", "camera_id")
52
66
  self.add_constant_key("CAM__002", "camera_name")
@@ -83,6 +97,8 @@ class VispHeadersInputDarkFrames(VispHeaders):
83
97
  num_modstates: int,
84
98
  exp_time: float = 1.0,
85
99
  readout_exp_time: float = 2.0,
100
+ ip_start_time="2022-11-28T13:44:00",
101
+ ip_end_time="2022-11-28T13:45:00",
86
102
  **kwargs,
87
103
  ):
88
104
  ################################################
@@ -95,13 +111,13 @@ class VispHeadersInputDarkFrames(VispHeaders):
95
111
  array_shape,
96
112
  time_delta,
97
113
  num_modstates_header_value=num_modstates,
114
+ ip_start_time=ip_start_time,
115
+ ip_end_time=ip_end_time,
98
116
  **kwargs,
99
117
  )
100
118
  self.add_constant_key("DKIST004", TaskName.dark.value.lower())
101
119
  self.add_constant_key("DKIST008", 1)
102
120
  self.add_constant_key("DKIST009", 1)
103
- self.add_constant_key("DKIST011", "2022-11-28T13:44:00")
104
- self.add_constant_key("DKIST012", "2022-11-28T13:45:00")
105
121
  self.add_constant_key("VISP_019", 1) # Num raster steps
106
122
  self.add_constant_key("VISP_020", 1) # Current raster step
107
123
  self.add_constant_key("ID___004")
@@ -131,6 +147,8 @@ class VispHeadersInputLampGainFrames(VispHeaders):
131
147
  num_modstates: int,
132
148
  exp_time: float = 10.0,
133
149
  readout_exp_time: float = 20.0,
150
+ ip_start_time="2022-11-28T13:46:00",
151
+ ip_end_time="2022-11-28T13:47:00",
134
152
  **kwargs,
135
153
  ):
136
154
  ################################################
@@ -143,14 +161,14 @@ class VispHeadersInputLampGainFrames(VispHeaders):
143
161
  array_shape,
144
162
  time_delta,
145
163
  num_modstates_header_value=num_modstates,
164
+ ip_start_time=ip_start_time,
165
+ ip_end_time=ip_end_time,
146
166
  **kwargs,
147
167
  )
148
168
  self.add_constant_key("DKIST004", TaskName.gain.value.lower())
149
169
  self.add_constant_key("PAC__002", "lamp")
150
170
  self.add_constant_key("DKIST008", 1)
151
171
  self.add_constant_key("DKIST009", 1)
152
- self.add_constant_key("DKIST011", "2022-11-28T13:46:00")
153
- self.add_constant_key("DKIST012", "2022-11-28T13:47:00")
154
172
  self.add_constant_key("VISP_019", 1)
155
173
  self.add_constant_key("VISP_020", 1)
156
174
  self.add_constant_key("PAC__003", "on")
@@ -172,6 +190,8 @@ class VispHeadersInputSolarGainFrames(VispHeaders):
172
190
  num_modstates: int,
173
191
  exp_time: float = 20.0,
174
192
  readout_exp_time: float = 40.0,
193
+ ip_start_time="2022-11-28T13:48:00",
194
+ ip_end_time="2022-11-28T13:49:00",
175
195
  **kwargs,
176
196
  ):
177
197
  ################################################
@@ -184,13 +204,13 @@ class VispHeadersInputSolarGainFrames(VispHeaders):
184
204
  array_shape,
185
205
  time_delta,
186
206
  num_modstates_header_value=num_modstates,
207
+ ip_start_time=ip_start_time,
208
+ ip_end_time=ip_end_time,
187
209
  **kwargs,
188
210
  )
189
211
  self.add_constant_key("DKIST004", TaskName.gain.value.lower())
190
212
  self.add_constant_key("DKIST008", 1)
191
213
  self.add_constant_key("DKIST009", 1)
192
- self.add_constant_key("DKIST011", "2022-11-28T13:48:00")
193
- self.add_constant_key("DKIST012", "2022-11-28T13:49:00")
194
214
  self.add_constant_key("VISP_019", 1)
195
215
  self.add_constant_key("VISP_020", 1)
196
216
  self.add_constant_key("PAC__002", "clear")
@@ -218,6 +238,8 @@ class VispHeadersInputPolcalFrames(VispHeaders):
218
238
  num_cs_steps: int = 1,
219
239
  exp_time: float = 0.01,
220
240
  readout_exp_time: float = 0.02,
241
+ ip_start_time="2022-11-28T13:50:00",
242
+ ip_end_time="2022-11-28T13:51:00",
221
243
  **kwargs,
222
244
  ):
223
245
  ################################################
@@ -230,6 +252,8 @@ class VispHeadersInputPolcalFrames(VispHeaders):
230
252
  array_shape,
231
253
  time_delta,
232
254
  num_modstates_header_value=num_modstates,
255
+ ip_start_time=ip_start_time,
256
+ ip_end_time=ip_end_time,
233
257
  **kwargs,
234
258
  )
235
259
  self.index_to_modstate = list(range(1, num_modstates + 1)) * num_cs_steps
@@ -240,8 +264,6 @@ class VispHeadersInputPolcalFrames(VispHeaders):
240
264
  self.add_constant_key("DKIST004", TaskName.polcal.value.lower())
241
265
  self.add_constant_key("DKIST008", 1)
242
266
  self.add_constant_key("DKIST009", 1)
243
- self.add_constant_key("DKIST011", "2022-11-28T13:50:00")
244
- self.add_constant_key("DKIST012", "2022-11-28T13:51:00")
245
267
  self.add_constant_key("VISP_019", 1)
246
268
  self.add_constant_key("VISP_020", 1)
247
269
  self.add_constant_key("TELSCAN", "Raster")
@@ -290,16 +312,23 @@ class VispHeadersInputPolcalDarkFrames(VispHeaders):
290
312
  num_modstates: int,
291
313
  exp_time: float = 0.01,
292
314
  readout_exp_time: float = 0.02,
315
+ ip_start_time="2022-11-28T13:50:00",
316
+ ip_end_time="2022-11-28T13:51:00",
293
317
  **kwargs,
294
318
  ):
295
319
  num_frames = num_modstates
296
320
  dataset_shape = (num_frames, *array_shape[-2:])
297
- super().__init__(dataset_shape, array_shape, time_delta, **kwargs)
321
+ super().__init__(
322
+ dataset_shape,
323
+ array_shape,
324
+ time_delta,
325
+ ip_start_time=ip_start_time,
326
+ ip_end_time=ip_end_time,
327
+ **kwargs,
328
+ )
298
329
  self.add_constant_key("DKIST004", TaskName.polcal.value.lower())
299
330
  self.add_constant_key("DKIST008", 1)
300
331
  self.add_constant_key("DKIST009", 1)
301
- self.add_constant_key("DKIST011", "2022-11-28T13:50:00")
302
- self.add_constant_key("DKIST012", "2022-11-28T13:51:00")
303
332
  self.add_constant_key("VISP_019", 1)
304
333
  self.add_constant_key("VISP_020", 1)
305
334
  self.add_constant_key("TELSCAN", "Raster")
@@ -322,16 +351,23 @@ class VispHeadersInputPolcalGainFrames(VispHeaders):
322
351
  num_modstates: int,
323
352
  exp_time: float = 0.01,
324
353
  readout_exp_time: float = 0.02,
354
+ ip_start_time="2022-11-28T13:50:00",
355
+ ip_end_time="2022-11-28T13:51:00",
325
356
  **kwargs,
326
357
  ):
327
358
  num_frames = num_modstates
328
359
  dataset_shape = (num_frames, *array_shape[-2:])
329
- super().__init__(dataset_shape, array_shape, time_delta, **kwargs)
360
+ super().__init__(
361
+ dataset_shape,
362
+ array_shape,
363
+ time_delta,
364
+ ip_start_time=ip_start_time,
365
+ ip_end_time=ip_end_time,
366
+ **kwargs,
367
+ )
330
368
  self.add_constant_key("DKIST004", TaskName.polcal.value.lower())
331
369
  self.add_constant_key("DKIST008", 1)
332
370
  self.add_constant_key("DKIST009", 1)
333
- self.add_constant_key("DKIST011", "2022-11-28T13:50:00")
334
- self.add_constant_key("DKIST012", "2022-11-28T13:51:00")
335
371
  self.add_constant_key("VISP_019", 1)
336
372
  self.add_constant_key("VISP_020", 1)
337
373
  self.add_constant_key("TELSCAN", "Raster")
@@ -357,6 +393,9 @@ class VispHeadersValidObserveFrames(VispHeaders):
357
393
  exp_time: float = 15.0,
358
394
  readout_exp_time: float = 30.0,
359
395
  abort_last_step: bool = False,
396
+ ip_start_time="2022-11-28T13:55:00",
397
+ ip_end_time="2022-11-28T13:56:00",
398
+ grating_constant=316,
360
399
  **kwargs,
361
400
  ):
362
401
  ################################################
@@ -370,7 +409,15 @@ class VispHeadersValidObserveFrames(VispHeaders):
370
409
  num_frames -= num_dropped_frames
371
410
 
372
411
  dataset_shape = (num_frames, *array_shape[-2:])
373
- super().__init__(dataset_shape, array_shape, time_delta, **kwargs)
412
+ super().__init__(
413
+ dataset_shape,
414
+ array_shape,
415
+ time_delta,
416
+ ip_start_time=ip_start_time,
417
+ ip_end_time=ip_end_time,
418
+ grating_constant=grating_constant,
419
+ **kwargs,
420
+ )
374
421
 
375
422
  self.index_to_map = sum(
376
423
  [[map_num + 1] * num_modstates * num_raster_steps for map_num in range(num_maps)], []
@@ -391,8 +438,6 @@ class VispHeadersValidObserveFrames(VispHeaders):
391
438
 
392
439
  self.num_raster_steps = num_raster_steps
393
440
  self.add_constant_key("DKIST004", TaskName.observe.value.lower())
394
- self.add_constant_key("DKIST011", "2022-11-28T13:55:00") # IP start time
395
- self.add_constant_key("DKIST012", "2022-11-28T13:56:00") # IP end time
396
441
  self.add_constant_key("ID___004")
397
442
  self.add_constant_key("WAVELNTH", 656.28)
398
443
  self.add_constant_key("EXPER_ID", "EXPERIMENT ID")
@@ -439,6 +484,8 @@ class VispHeadersValidCalibratedFrames(VispHeaders):
439
484
  exp_time: float = 15.0,
440
485
  readout_exp_time: float = 30.0,
441
486
  wcs_axis_names: tuple[str, str] | None = None,
487
+ ip_start_time="2022-11-28T13:55:00",
488
+ ip_end_time="2022-11-28T13:56:00",
442
489
  **kwargs,
443
490
  ):
444
491
  ################################################
@@ -452,7 +499,13 @@ class VispHeadersValidCalibratedFrames(VispHeaders):
452
499
  num_frames = num_maps * num_raster_steps * num_stokes
453
500
  dataset_shape = (num_frames, *array_shape[-2:])
454
501
  super().__init__(
455
- dataset_shape, array_shape, time_delta, polarimeter_mode=polarimeter_mode, **kwargs
502
+ dataset_shape,
503
+ array_shape,
504
+ time_delta,
505
+ polarimeter_mode=polarimeter_mode,
506
+ ip_start_time=ip_start_time,
507
+ ip_end_time=ip_end_time,
508
+ **kwargs,
456
509
  )
457
510
 
458
511
  stokes_list = ["I", "Q", "U", "V"][:num_stokes]
@@ -466,8 +519,6 @@ class VispHeadersValidCalibratedFrames(VispHeaders):
466
519
 
467
520
  self.num_raster_steps = num_raster_steps
468
521
  self.add_constant_key("DKIST004", TaskName.observe.value.lower())
469
- self.add_constant_key("DKIST011", "2022-11-28T13:55:00") # IP start time
470
- self.add_constant_key("DKIST012", "2022-11-28T13:56:00") # IP end time
471
522
  self.add_constant_key("ID___004")
472
523
  self.add_constant_key("WAVELNTH", 656.28)
473
524
  self.add_constant_key("EXPER_ID", "EXPERIMENT ID")
@@ -10,6 +10,7 @@ from dkist_header_validator import spec214_validator
10
10
  from dkist_processing_common.codecs.fits import fits_array_decoder
11
11
  from dkist_processing_common.codecs.fits import fits_hdulist_encoder
12
12
  from dkist_processing_common.models.constants import BudName
13
+ from dkist_processing_common.models.fits_access import MetadataKey
13
14
  from dkist_processing_common.models.task_name import TaskName
14
15
  from dkist_processing_common.parsers.cs_step import CSStepFlower
15
16
  from dkist_processing_common.parsers.cs_step import NumCSStepBud
@@ -21,6 +22,8 @@ from dkist_processing_common.parsers.time import ExposureTimeFlower
21
22
  from dkist_processing_common.parsers.time import ReadoutExpTimeFlower
22
23
  from dkist_processing_common.parsers.time import TaskExposureTimesBud
23
24
  from dkist_processing_common.parsers.time import TaskReadoutExpTimesBud
25
+ from dkist_processing_common.parsers.unique_bud import TaskUniqueBud
26
+ from dkist_processing_common.parsers.unique_bud import UniqueBud
24
27
  from dkist_processing_common.tasks import ParseL0InputDataBase
25
28
  from dkist_processing_common.tasks import WorkflowTaskBase
26
29
  from dkist_processing_common.tasks.mixin.globus import GlobusTransferItem
@@ -31,10 +34,13 @@ from loguru import logger
31
34
  from dkist_processing_visp.models.constants import VispBudName
32
35
  from dkist_processing_visp.models.constants import VispConstants
33
36
  from dkist_processing_visp.models.fits_access import VispMetadataKey
37
+ from dkist_processing_visp.models.metric_code import VispMetricCode
34
38
  from dkist_processing_visp.models.parameters import VispParsingParameters
35
39
  from dkist_processing_visp.models.tags import VispTag
36
40
  from dkist_processing_visp.models.task_name import VispTaskName
37
41
  from dkist_processing_visp.parsers.modulator_states import ModulatorStateFlower
42
+ from dkist_processing_visp.parsers.spectrograph_configuration import IncidentLightAngleBud
43
+ from dkist_processing_visp.parsers.spectrograph_configuration import ReflectedLightAngleBud
38
44
  from dkist_processing_visp.parsers.time import DarkReadoutExpTimePickyBud
39
45
  from dkist_processing_visp.parsers.time import NonDarkNonPolcalTaskReadoutExpTimesBud
40
46
  from dkist_processing_visp.parsers.visp_l0_fits_access import VispL0FitsAccess
@@ -264,7 +270,9 @@ class SaveSolarCal(SaveTaskTags):
264
270
  @property
265
271
  def tag_lists_to_save(self) -> list[list[str]]:
266
272
  return super().tag_lists_to_save + [
267
- [VispTag.quality("TASK_TYPES"), VispTag.workflow_task("SolarCalibration")]
273
+ [VispTag.quality("TASK_TYPES"), VispTag.workflow_task("SolarCalibration")],
274
+ [VispTag.quality(VispMetricCode.solar_first_vignette)],
275
+ [VispTag.quality(VispMetricCode.solar_final_vignette)],
268
276
  ]
269
277
 
270
278
  @property
@@ -416,11 +424,27 @@ class ParseCalOnlyL0InputData(ParseL0InputDataBase):
416
424
  @property
417
425
  def constant_buds(self):
418
426
  """Add ViSP specific constants to common constants."""
427
+ # TODO: Subclass ViSP parse task and *remove* unneeded things from this list
419
428
  return super().constant_buds + [
429
+ UniqueBud(constant_name=VispBudName.arm_id.value, metadata_key=VispMetadataKey.arm_id),
420
430
  NumCSStepBud(self.parameters.max_cs_step_time_sec),
421
431
  NonDarkNonPolcalTaskReadoutExpTimesBud(),
422
432
  DarkReadoutExpTimePickyBud(),
423
433
  RetarderNameBud(),
434
+ IncidentLightAngleBud(),
435
+ ReflectedLightAngleBud(),
436
+ TaskUniqueBud(
437
+ constant_name=VispBudName.grating_constant_inverse_mm.value,
438
+ metadata_key=VispMetadataKey.grating_constant_inverse_mm,
439
+ ip_task_types=[TaskName.observe.value, TaskName.solar_gain.value],
440
+ task_type_parsing_function=parse_header_ip_task_with_gains,
441
+ ),
442
+ TaskUniqueBud(
443
+ constant_name=VispBudName.solar_gain_ip_start_time.value,
444
+ metadata_key=MetadataKey.ip_start_time,
445
+ ip_task_types=TaskName.solar_gain,
446
+ task_type_parsing_function=parse_header_ip_task_with_gains,
447
+ ),
424
448
  TaskExposureTimesBud(
425
449
  stem_name=VispBudName.lamp_exposure_times.value,
426
450
  ip_task_types=TaskName.lamp_gain.value,
@@ -1,27 +1,65 @@
1
+ from typing import Generator
1
2
  from unittest.mock import MagicMock
2
3
 
4
+ import numpy as np
3
5
  import pytest
6
+ from dkist_processing_common._util.scratch import WorkflowFileSystem
7
+ from dkist_processing_common.codecs.asdf import asdf_encoder
8
+ from dkist_processing_common.codecs.quality import quality_data_decoder
4
9
 
10
+ from dkist_processing_visp.models.metric_code import VispMetricCode
11
+ from dkist_processing_visp.models.tags import VispTag
5
12
  from dkist_processing_visp.tasks.l1_output_data import VispAssembleQualityData
6
13
 
7
14
 
8
15
  @pytest.fixture
9
- def visp_assemble_quality_data_task(tmp_path, recipe_run_id) -> VispAssembleQualityData:
16
+ def visp_assemble_quality_data_task(
17
+ tmp_path, recipe_run_id
18
+ ) -> Generator[VispAssembleQualityData, None, None]:
10
19
 
11
20
  with VispAssembleQualityData(
12
21
  recipe_run_id=recipe_run_id, workflow_name="visp_assemble_quality", workflow_version="VX.Y"
13
22
  ) as task:
23
+ task.scratch = WorkflowFileSystem(scratch_base_path=tmp_path, recipe_run_id=recipe_run_id)
14
24
  yield task
15
25
  task._purge()
16
26
 
17
27
 
28
+ def write_raw_vignette_metrics_to_task(task):
29
+ for beam in [1, 2]:
30
+ dummy_vec = np.arange(10)
31
+ first_vignette_quality_outputs = {
32
+ "output_wave_vec": dummy_vec,
33
+ "input_spectrum": dummy_vec,
34
+ "best_fit_atlas": dummy_vec,
35
+ "best_fit_continuum": dummy_vec,
36
+ "residuals": dummy_vec,
37
+ }
38
+ task.write(
39
+ data=first_vignette_quality_outputs,
40
+ tags=[VispTag.quality(VispMetricCode.solar_first_vignette), VispTag.beam(beam)],
41
+ encoder=asdf_encoder,
42
+ )
43
+ final_correction_quality_outputs = {
44
+ "output_wave_vec": dummy_vec,
45
+ "median_spec": dummy_vec,
46
+ "low_deviation": dummy_vec,
47
+ "high_deviation": dummy_vec,
48
+ }
49
+ task.write(
50
+ data=final_correction_quality_outputs,
51
+ tags=[VispTag.quality(VispMetricCode.solar_final_vignette), VispTag.beam(beam)],
52
+ encoder=asdf_encoder,
53
+ )
54
+
55
+
18
56
  @pytest.fixture
19
57
  def dummy_quality_data() -> list[dict]:
20
58
  return [{"dummy_key": "dummy_value"}]
21
59
 
22
60
 
23
61
  @pytest.fixture
24
- def quality_assemble_data_mock(mocker, dummy_quality_data) -> MagicMock:
62
+ def common_quality_assemble_data_mock(mocker, dummy_quality_data) -> MagicMock:
25
63
  yield mocker.patch(
26
64
  "dkist_processing_common.tasks.mixin.quality.QualityMixin.quality_assemble_data",
27
65
  return_value=dummy_quality_data,
@@ -29,13 +67,60 @@ def quality_assemble_data_mock(mocker, dummy_quality_data) -> MagicMock:
29
67
  )
30
68
 
31
69
 
32
- def test_correct_polcal_label_list(visp_assemble_quality_data_task, quality_assemble_data_mock):
70
+ def test_vignette_metrics_built(visp_assemble_quality_data_task):
71
+ """
72
+ Given: A `VispAssembleQualityData` task with raw vignette metrics in scratch
73
+ When: Building the quality report data
74
+ Then: The vignette metrics are included in the data
75
+ """
76
+ task = visp_assemble_quality_data_task
77
+ write_raw_vignette_metrics_to_task(task)
78
+
79
+ task()
80
+
81
+ final_report_list = list(task.read(tags=VispTag.quality_data(), decoder=quality_data_decoder))
82
+ assert len(final_report_list) == 1
83
+ final_report = final_report_list[0]
84
+
85
+ initial_vignette_metrics = list(
86
+ filter(lambda i: i["name"].startswith("Initial Vignette Estimation"), final_report)
87
+ )
88
+ assert len(initial_vignette_metrics) == 2
89
+ facet_set = set()
90
+ for m in initial_vignette_metrics:
91
+ assert m["metric_code"] == VispMetricCode.solar_first_vignette.value
92
+ assert m["description"]
93
+ assert m["multi_plot_data"]
94
+ facet_set.add(m["facet"])
95
+
96
+ assert facet_set == {"BEAM_1", "BEAM_2"}
97
+
98
+ final_vignette_metrics = list(
99
+ filter(lambda i: i["name"].startswith("Final Vignette Estimation"), final_report)
100
+ )
101
+ assert len(final_vignette_metrics) == 2
102
+ facet_set = set()
103
+ for m in final_vignette_metrics:
104
+ assert m["metric_code"] == VispMetricCode.solar_final_vignette.value
105
+ assert m["description"]
106
+ assert m["multi_plot_data"]
107
+ facet_set.add(m["facet"])
108
+
109
+ assert facet_set == {"BEAM_1", "BEAM_2"}
110
+
111
+
112
+ def test_correct_polcal_label_list(
113
+ visp_assemble_quality_data_task, common_quality_assemble_data_mock
114
+ ):
33
115
  """
34
116
  Given: A VispAssembleQualityData task
35
117
  When: Calling the task
36
118
  Then: The correct polcal_label_list property is passed to .quality_assemble_data
37
119
  """
38
120
  task = visp_assemble_quality_data_task
121
+ write_raw_vignette_metrics_to_task(task)
39
122
 
40
123
  task()
41
- quality_assemble_data_mock.assert_called_once_with(task, polcal_label_list=["Beam 1", "Beam 2"])
124
+ common_quality_assemble_data_mock.assert_called_once_with(
125
+ task, polcal_label_list=["Beam 1", "Beam 2"]
126
+ )