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
@@ -358,3 +358,43 @@ def test_basic_corrections(geometric_calibration_task, assign_input_dataset_doc_
358
358
  lamp_array = task.basic_corrected_lamp_data(beam=beam, modstate=modstate)
359
359
  np.testing.assert_equal(expected, solar_array)
360
360
  np.testing.assert_equal(expected, lamp_array)
361
+
362
+
363
+ def test_line_zones(geometric_calibration_task):
364
+ """
365
+ Given: A spectrum with some absorption lines
366
+ When: Computing zones around the lines
367
+ Then: Correct results are returned
368
+ """
369
+
370
+ # NOTE that it does not test for removal of overlapping regions
371
+ def gaussian(x, amp, mu, sig):
372
+ return amp * np.exp(-np.power(x - mu, 2.0) / (2 * np.power(sig, 2.0)))
373
+
374
+ spec = np.ones(1000) * 100
375
+ x = np.arange(1000.0)
376
+ expected = []
377
+ for m, s in zip([100.0, 300.0, 700], [10.0, 20.0, 5.0]):
378
+ spec -= gaussian(x, 40, m, s)
379
+ hwhm = s * 2.355 / 2
380
+ expected.append((np.floor(m - hwhm).astype(int), np.ceil(m + hwhm).astype(int)))
381
+
382
+ task = geometric_calibration_task[0]
383
+
384
+ zones = task.compute_line_zones(spec[:, None], bg_order=0, rel_height=0.5)
385
+ assert zones == expected
386
+
387
+
388
+ def test_identify_overlapping_zones(geometric_calibration_task):
389
+ """
390
+ Given: A list of zone borders that contain overlapping zones
391
+ When: Identifying zones that overlap
392
+ Then: The smaller of the overlapping zones are identified for removal
393
+ """
394
+ rips = np.array([100, 110, 220, 200])
395
+ lips = np.array([150, 120, 230, 250])
396
+
397
+ task = geometric_calibration_task[0]
398
+
399
+ idx_to_remove = task.identify_overlapping_zones(rips, lips)
400
+ assert idx_to_remove == [1, 2]
@@ -255,7 +255,8 @@ def test_instrument_polarization_calibration_task(
255
255
  task=task, num_modstates=num_modstates, data_shape=intermediate_shape
256
256
  )
257
257
  write_dummy_intermediate_solar_cals_to_task(
258
- task=task, data_shape=intermediate_shape, num_modstates=num_modstates
258
+ task=task,
259
+ data_shape=intermediate_shape,
259
260
  )
260
261
  write_input_polcals_to_task(
261
262
  task=task,
@@ -23,9 +23,10 @@ def make_lamp_array_data(
23
23
  frame: VispHeadersInputLampGainFrames, dark_signal: float, beam_border: int
24
24
  ):
25
25
  num_raw_frames_per_fpa = frame.header()["CAM__014"]
26
+ modstate = frame.current_modstate("") # Weird signature due to @key_function
26
27
  data = np.zeros(frame.array_shape)
27
- data[0, :beam_border, :] = (1.1 + dark_signal) * num_raw_frames_per_fpa
28
- data[0, beam_border:, :] = (1.2 + dark_signal) * num_raw_frames_per_fpa
28
+ data[0, :beam_border, :] = (1.0 + 0.1 * modstate + dark_signal) * num_raw_frames_per_fpa
29
+ data[0, beam_border:, :] = (2.0 + 0.1 * modstate + dark_signal) * num_raw_frames_per_fpa
29
30
 
30
31
  return data
31
32
 
@@ -132,27 +133,21 @@ def test_lamp_calibration_task(
132
133
  VispTag.task_lamp_gain(),
133
134
  VispTag.intermediate(),
134
135
  ]
135
- assert len(list(task.read(tags=tags))) == num_modstates * 2 # 2 from beams
136
-
137
- for i in range(num_modstates):
138
- for beam in [1, 2]:
139
- tags = [
140
- VispTag.task_lamp_gain(),
141
- VispTag.intermediate(),
142
- VispTag.modstate(i + 1),
143
- VispTag.beam(beam),
144
- ]
145
- files = list(task.read(tags=tags))
146
- assert len(files) == 1
147
- hdu = fits.open(files[0])[0]
148
- np.testing.assert_allclose(hdu.data, np.ones((10, 10)) * (1 + (0.1 * beam)))
136
+ assert len(list(task.read(tags=tags))) == 2 # One per beam
149
137
 
150
- tags = [
151
- VispTag.task_lamp_gain(),
152
- VispTag.intermediate(),
153
- ]
154
- for filepath in task.read(tags=tags):
155
- assert filepath.exists()
138
+ for beam in [1, 2]:
139
+ tags = [
140
+ VispTag.task_lamp_gain(),
141
+ VispTag.intermediate(),
142
+ VispTag.beam(beam),
143
+ ]
144
+ files = list(task.read(tags=tags))
145
+ assert len(files) == 1
146
+
147
+ expected_signal = beam + np.mean(np.arange(1, num_modstates + 1)) * 0.1
148
+
149
+ hdu = fits.open(files[0])[0]
150
+ np.testing.assert_allclose(hdu.data, np.ones(intermediate_shape) * expected_signal)
156
151
 
157
152
  quality_files = task.read(tags=[Tag.quality("TASK_TYPES")])
158
153
  for file in quality_files:
@@ -1,6 +1,7 @@
1
1
  from dataclasses import asdict
2
2
  from dataclasses import dataclass
3
3
 
4
+ import astropy.units as u
4
5
  import numpy as np
5
6
  import pytest
6
7
  from hypothesis import HealthCheck
@@ -8,6 +9,7 @@ from hypothesis import example
8
9
  from hypothesis import given
9
10
  from hypothesis import settings
10
11
  from hypothesis import strategies as st
12
+ from pydantic import BaseModel
11
13
 
12
14
  from dkist_processing_visp.models.parameters import VispParameters
13
15
  from dkist_processing_visp.models.parameters import VispParsingParameters
@@ -15,10 +17,27 @@ from dkist_processing_visp.tasks.visp_base import VispTaskBase
15
17
  from dkist_processing_visp.tests.conftest import VispConstantsDb
16
18
  from dkist_processing_visp.tests.conftest import VispInputDatasetParameterValues
17
19
 
18
- # The property names of all parameters on `VispParsingParameters`
19
- PARSE_PARAMETER_NAMES = [
20
- k for k, v in vars(VispParsingParameters).items() if isinstance(v, property)
21
- ]
20
+
21
+ @pytest.fixture(scope="session")
22
+ def parse_parameter_names() -> list[str]:
23
+ # The property names of all parameters on `VispParsingParameters`
24
+ return [k for k, v in vars(VispParsingParameters).items() if isinstance(v, property)]
25
+
26
+
27
+ @pytest.fixture(scope="session")
28
+ def arm_parameter_names() -> list[str]:
29
+ return [
30
+ "wavecal_camera_lens_parameters",
31
+ ]
32
+
33
+
34
+ @pytest.fixture(scope="session")
35
+ def unit_parameter_names_and_units() -> dict[str, u.Unit | list[u.Unit]]:
36
+ return {
37
+ "solar_vignette_crval_bounds_px": u.pix,
38
+ "wavecal_camera_lens_parameters": [u.m, u.m / u.nm, u.m / u.nm**2],
39
+ "wavecal_pixel_pitch_micron_per_pix": u.um / u.pix,
40
+ }
22
41
 
23
42
 
24
43
  @pytest.fixture(scope="function")
@@ -27,6 +46,7 @@ def basic_science_task_with_parameter_mixin(
27
46
  assign_input_dataset_doc_to_task,
28
47
  init_visp_constants_db,
29
48
  testing_obs_ip_start_time,
49
+ arm_id,
30
50
  ):
31
51
  def make_task(
32
52
  parameters_part: dataclass,
@@ -48,8 +68,9 @@ def basic_science_task_with_parameter_mixin(
48
68
  parameters_part,
49
69
  parameter_class=parameter_class,
50
70
  obs_ip_start_time=obs_ip_start_time,
71
+ arm_id=arm_id,
51
72
  )
52
- yield task, parameters_part
73
+ yield task, asdict(parameters_part)
53
74
  except:
54
75
  raise
55
76
  finally:
@@ -58,7 +79,14 @@ def basic_science_task_with_parameter_mixin(
58
79
  return make_task
59
80
 
60
81
 
61
- def test_non_wave_parameters(basic_science_task_with_parameter_mixin):
82
+ @pytest.mark.parametrize("arm_id", [pytest.param("1"), pytest.param("2"), pytest.param("3")])
83
+ def test_non_wave_parameters(
84
+ basic_science_task_with_parameter_mixin,
85
+ parse_parameter_names,
86
+ arm_parameter_names,
87
+ unit_parameter_names_and_units,
88
+ arm_id,
89
+ ):
62
90
  """
63
91
  Given: A Science task with the parameter mixin
64
92
  When: Accessing properties for parameters that do not depend on wavelength
@@ -68,15 +96,41 @@ def test_non_wave_parameters(basic_science_task_with_parameter_mixin):
68
96
  basic_science_task_with_parameter_mixin(VispInputDatasetParameterValues())
69
97
  )
70
98
  task_param_attr = task.parameters
71
- for pn, pv in asdict(expected).items():
72
- property_name = pn.removeprefix("visp_")
99
+ parameter_properties = [k for k, v in vars(VispParameters).items() if isinstance(v, property)]
100
+ for parameter_name in parameter_properties:
101
+ pn = f"visp_{parameter_name}"
102
+ if parameter_name in arm_parameter_names:
103
+ pn = f"{pn}_{arm_id}"
104
+ pv = expected[pn]
105
+ is_wavelength_param = isinstance(pv, dict) and "wavelength" in pv
73
106
  if (
74
- type(pv) is not dict and property_name not in PARSE_PARAMETER_NAMES
75
- ): # Don't test wavelength dependent parameters
76
- assert getattr(task_param_attr, property_name) == pv
107
+ parameter_name not in parse_parameter_names
108
+ and not is_wavelength_param
109
+ and parameter_name != "solar_vignette_wavecal_fit_kwargs"
110
+ ):
111
+ param_obj_value = getattr(task_param_attr, parameter_name)
112
+ if isinstance(pv, tuple):
113
+ pv = list(pv)
114
+
115
+ if parameter_name in unit_parameter_names_and_units:
116
+ expected_units = unit_parameter_names_and_units[parameter_name]
117
+ if not isinstance(param_obj_value, list):
118
+ param_obj_value = [param_obj_value]
119
+ pv = [pv]
120
+ expected_units = [expected_units]
77
121
 
122
+ assert all([param_obj_value[i].value == pv[i] for i in range(len(pv))])
123
+ assert all(
124
+ [param_obj_value[i].unit == expected_units[i] for i in range(len(pv))]
125
+ ), f"Units {[v.unit for v in param_obj_value]} does not match expected {expected_units}"
126
+ elif isinstance(param_obj_value, BaseModel):
127
+ assert param_obj_value.model_dump() == pv
128
+ else:
129
+ assert getattr(task_param_attr, parameter_name) == pv
78
130
 
79
- def test_parse_parameters(basic_science_task_with_parameter_mixin):
131
+
132
+ @pytest.mark.parametrize("arm_id", ["1"])
133
+ def test_parse_parameters(basic_science_task_with_parameter_mixin, parse_parameter_names):
80
134
  """
81
135
  Given: A Science task with Parsing parameters
82
136
  When: Accessing properties for Parse parameters
@@ -90,16 +144,19 @@ def test_parse_parameters(basic_science_task_with_parameter_mixin):
90
144
  )
91
145
  )
92
146
  task_param_attr = task.parameters
93
- for pn, pv in asdict(expected).items():
147
+ for pn, pv in expected.items():
94
148
  property_name = pn.removeprefix("visp_")
95
- if property_name in PARSE_PARAMETER_NAMES and type(pv) is not dict:
149
+ if property_name in parse_parameter_names and type(pv) is not dict:
96
150
  assert getattr(task_param_attr, property_name) == pv
97
151
 
98
152
 
99
153
  @given(wave=st.floats(min_value=500.0, max_value=2000.0))
100
154
  @settings(suppress_health_check=[HealthCheck.function_scoped_fixture])
101
155
  @example(wave=492.5)
102
- def test_wave_parameters(basic_science_task_with_parameter_mixin, wave):
156
+ @pytest.mark.parametrize("arm_id", ["1"])
157
+ def test_wave_parameters(
158
+ basic_science_task_with_parameter_mixin, parse_parameter_names, arm_parameter_names, wave
159
+ ):
103
160
  """
104
161
  Given: A Science task with the paramter mixin
105
162
  When: Accessing properties for parameters that depend on wavelength
@@ -110,10 +167,55 @@ def test_wave_parameters(basic_science_task_with_parameter_mixin, wave):
110
167
  )
111
168
  task_param_attr = task.parameters
112
169
  task_param_attr._wavelength = wave
113
- pwaves = np.array(expected.visp_solar_zone_normalization_percentile.wavelength)
170
+ pwaves = np.array(expected["visp_geo_zone_normalization_percentile"]["wavelength"])
114
171
  midpoints = 0.5 * (pwaves[1:] + pwaves[:-1])
115
172
  idx = np.sum(midpoints < wave)
116
- for pn, pv in asdict(expected).items():
173
+ for pn, pv in expected.items():
117
174
  property_name = pn.removeprefix("visp_")
118
- if type(pv) is dict and property_name not in PARSE_PARAMETER_NAMES:
175
+ is_wavelength_param = isinstance(pv, dict) and "wavelength" in pv
176
+ if is_wavelength_param and property_name not in parse_parameter_names + arm_parameter_names:
119
177
  assert getattr(task_param_attr, property_name) == pv["values"][idx]
178
+
179
+
180
+ class AnyInt:
181
+ pass
182
+
183
+
184
+ @pytest.mark.parametrize("arm_id", [pytest.param("1")])
185
+ @pytest.mark.parametrize(
186
+ "db_value, expected",
187
+ [
188
+ pytest.param({"method": "nelder"}, {"method": "nelder"}, id="non_rng_method"),
189
+ pytest.param(
190
+ {"method": "basinhopping"}, {"method": "basinhopping", "rng": AnyInt}, id="random_rng"
191
+ ),
192
+ pytest.param(
193
+ {"method": "differential_evolution", "rng": 6.28},
194
+ {"method": "differential_evolution", "rng": 6.28},
195
+ id="override_rng",
196
+ ),
197
+ pytest.param(dict(), dict(), id="no_kwargs"),
198
+ ],
199
+ )
200
+ def test_fit_kwarg_parameters(
201
+ basic_science_task_with_parameter_mixin,
202
+ db_value,
203
+ expected,
204
+ ):
205
+ """
206
+ Given: A Science task with the parameter mixin
207
+ When: Accessing properties for parameters that do not depend on wavelength
208
+ Then: The correct value is returned
209
+ """
210
+ task, _ = next(
211
+ basic_science_task_with_parameter_mixin(
212
+ VispInputDatasetParameterValues(visp_solar_vignette_wavecal_fit_kwargs=db_value)
213
+ )
214
+ )
215
+ kwarg_dict = task.parameters.solar_vignette_wavecal_fit_kwargs
216
+ assert kwarg_dict.keys() == expected.keys()
217
+ for k in expected.keys():
218
+ if expected[k] is AnyInt:
219
+ assert type(kwarg_dict[k]) is int
220
+ else:
221
+ assert expected[k] == kwarg_dict[k]
@@ -86,6 +86,7 @@ def write_input_dark_frames_to_task(
86
86
  time_delta: float = 10.0,
87
87
  num_modstates: int = 2,
88
88
  data_shape: tuple[int, int] = (2, 2),
89
+ **kwargs,
89
90
  ):
90
91
  array_shape = (1, *data_shape)
91
92
  dataset = VispHeadersInputDarkFrames(
@@ -94,6 +95,7 @@ def write_input_dark_frames_to_task(
94
95
  exp_time=exp_time,
95
96
  readout_exp_time=readout_exp_time,
96
97
  num_modstates=num_modstates,
98
+ **kwargs,
97
99
  )
98
100
 
99
101
  num_written_frames = write_frames_to_task(
@@ -109,6 +111,7 @@ def write_input_lamp_frames_to_task(
109
111
  time_delta: float = 10.0,
110
112
  num_modstates: int = 2,
111
113
  data_shape: tuple[int, int] = (2, 2),
114
+ **kwargs,
112
115
  ):
113
116
  array_shape = (1, *data_shape)
114
117
  dataset = VispHeadersInputLampGainFrames(
@@ -117,6 +120,7 @@ def write_input_lamp_frames_to_task(
117
120
  exp_time=exp_time,
118
121
  readout_exp_time=readout_exp_time,
119
122
  num_modstates=num_modstates,
123
+ **kwargs,
120
124
  )
121
125
 
122
126
  num_written_frames = write_frames_to_task(
@@ -132,6 +136,7 @@ def write_input_solar_frames_to_task(
132
136
  time_delta: float = 10.0,
133
137
  num_modstates: int = 2,
134
138
  data_shape: tuple[int, int] = (2, 2),
139
+ **kwargs,
135
140
  ):
136
141
  array_shape = (1, *data_shape)
137
142
  dataset = VispHeadersInputSolarGainFrames(
@@ -140,6 +145,7 @@ def write_input_solar_frames_to_task(
140
145
  exp_time=exp_time,
141
146
  readout_exp_time=readout_exp_time,
142
147
  num_modstates=num_modstates,
148
+ **kwargs,
143
149
  )
144
150
 
145
151
  num_written_frames = write_frames_to_task(
@@ -155,6 +161,7 @@ def write_input_polcal_frames_to_task(
155
161
  time_delta: float = 30.0,
156
162
  num_modstates: int = 2,
157
163
  data_shape: tuple[int, int] = (2, 2),
164
+ **kwargs,
158
165
  ):
159
166
  array_shape = (1, *data_shape)
160
167
  dataset = VispHeadersInputPolcalFrames(
@@ -163,6 +170,7 @@ def write_input_polcal_frames_to_task(
163
170
  exp_time=exp_time,
164
171
  readout_exp_time=readout_exp_time,
165
172
  num_modstates=num_modstates,
173
+ **kwargs,
166
174
  )
167
175
 
168
176
  num_written_frames = write_frames_to_task(
@@ -178,6 +186,7 @@ def write_input_polcal_dark_frames_to_task(
178
186
  time_delta: float = 30.0,
179
187
  num_modstates: int = 2,
180
188
  data_shape: tuple[int, int] = (2, 2),
189
+ **kwargs,
181
190
  ):
182
191
  array_shape = (1, *data_shape)
183
192
  dataset = VispHeadersInputPolcalDarkFrames(
@@ -186,6 +195,7 @@ def write_input_polcal_dark_frames_to_task(
186
195
  exp_time=exp_time,
187
196
  readout_exp_time=readout_exp_time,
188
197
  num_modstates=num_modstates,
198
+ **kwargs,
189
199
  )
190
200
 
191
201
  num_written_frames = write_frames_to_task(
@@ -201,6 +211,7 @@ def write_input_polcal_gain_frames_to_task(
201
211
  time_delta: float = 30.0,
202
212
  num_modstates: int = 2,
203
213
  data_shape: tuple[int, int] = (2, 2),
214
+ **kwargs,
204
215
  ):
205
216
  array_shape = (1, *data_shape)
206
217
  dataset = VispHeadersInputPolcalGainFrames(
@@ -209,6 +220,7 @@ def write_input_polcal_gain_frames_to_task(
209
220
  exp_time=exp_time,
210
221
  readout_exp_time=readout_exp_time,
211
222
  num_modstates=num_modstates,
223
+ **kwargs,
212
224
  )
213
225
 
214
226
  num_written_frames = write_frames_to_task(
@@ -227,6 +239,7 @@ def write_input_observe_frames_to_task(
227
239
  time_delta: float = 10.0,
228
240
  data_shape: tuple[int, int] = (2, 2),
229
241
  obs_dataset_class=VispHeadersValidObserveFrames,
242
+ **kwargs,
230
243
  ):
231
244
  array_shape = (1, *data_shape)
232
245
  dataset = obs_dataset_class(
@@ -237,6 +250,7 @@ def write_input_observe_frames_to_task(
237
250
  num_modstates=num_modstates,
238
251
  exp_time=exp_time,
239
252
  readout_exp_time=readout_exp_time,
253
+ **kwargs,
240
254
  )
241
255
  num_written_frames = write_frames_to_task(
242
256
  task=task, frame_generator=dataset, extra_tags=[VispTag.input()]
@@ -272,6 +286,11 @@ def write_input_cal_frames_to_task(
272
286
  solar_exp_time,
273
287
  polcal_exp_time,
274
288
  num_modstates,
289
+ testing_arm_id,
290
+ testing_solar_ip_start_time,
291
+ testing_grating_constant,
292
+ testing_grating_angle,
293
+ testing_arm_position,
275
294
  ):
276
295
  def write_frames_to_task(task):
277
296
  for readout_exp_time in required_dark_readout_exp_times:
@@ -280,6 +299,7 @@ def write_input_cal_frames_to_task(
280
299
  readout_exp_time=readout_exp_time,
281
300
  exp_time=dark_exp_time,
282
301
  num_modstates=num_modstates,
302
+ arm_id=testing_arm_id,
283
303
  )
284
304
 
285
305
  write_input_lamp_frames_to_task(
@@ -287,30 +307,39 @@ def write_input_cal_frames_to_task(
287
307
  readout_exp_time=lamp_readout_exp_time,
288
308
  exp_time=lamp_exp_time,
289
309
  num_modstates=num_modstates,
310
+ arm_id=testing_arm_id,
290
311
  )
291
312
  write_input_solar_frames_to_task(
292
313
  task=task,
293
314
  readout_exp_time=solar_readout_exp_time,
294
315
  exp_time=solar_exp_time,
295
316
  num_modstates=num_modstates,
317
+ arm_id=testing_arm_id,
318
+ ip_start_time=testing_solar_ip_start_time,
319
+ grating_constant=testing_grating_constant,
320
+ grating_angle=testing_grating_angle,
321
+ arm_position=testing_arm_position,
296
322
  )
297
323
  write_input_polcal_frames_to_task(
298
324
  task=task,
299
325
  readout_exp_time=polcal_readout_exp_time,
300
326
  exp_time=polcal_exp_time,
301
327
  num_modstates=num_modstates,
328
+ arm_id=testing_arm_id,
302
329
  )
303
330
  write_input_polcal_dark_frames_to_task(
304
331
  task=task,
305
332
  readout_exp_time=polcal_readout_exp_time,
306
333
  exp_time=polcal_exp_time,
307
334
  num_modstates=num_modstates,
335
+ arm_id=testing_arm_id,
308
336
  )
309
337
  write_input_polcal_gain_frames_to_task(
310
338
  task,
311
339
  readout_exp_time=polcal_readout_exp_time,
312
340
  exp_time=polcal_exp_time,
313
341
  num_modstates=num_modstates,
342
+ arm_id=testing_arm_id,
314
343
  )
315
344
 
316
345
  return write_frames_to_task
@@ -350,6 +379,10 @@ def test_parse_visp_input_data(
350
379
  num_modstates,
351
380
  mocker,
352
381
  fake_gql_client,
382
+ testing_arm_id,
383
+ testing_grating_constant,
384
+ testing_grating_angle,
385
+ testing_arm_position,
353
386
  ):
354
387
  """
355
388
  Given: A ParseVispInputData task
@@ -369,6 +402,10 @@ def test_parse_visp_input_data(
369
402
  num_steps=3,
370
403
  readout_exp_time=obs_readout_exp_time,
371
404
  exp_time=obs_exp_time,
405
+ arm_id=testing_arm_id,
406
+ grating_constant=testing_grating_constant,
407
+ grating_angle=testing_grating_angle,
408
+ arm_position=testing_arm_position,
372
409
  )
373
410
 
374
411
  # When
@@ -404,6 +441,12 @@ def test_parse_visp_input_data_constants(
404
441
  polcal_exp_time,
405
442
  observe_exp_times,
406
443
  num_modstates,
444
+ testing_arm_id,
445
+ testing_obs_ip_start_time,
446
+ testing_solar_ip_start_time,
447
+ testing_grating_constant,
448
+ testing_grating_angle,
449
+ testing_arm_position,
407
450
  ):
408
451
  """
409
452
  Given: A ParseVispInputData task
@@ -426,16 +469,29 @@ def test_parse_visp_input_data_constants(
426
469
  num_steps=num_steps,
427
470
  readout_exp_time=obs_readout_exp_time,
428
471
  exp_time=obs_exp_time,
472
+ arm_id=testing_arm_id,
473
+ ip_start_time=testing_obs_ip_start_time,
474
+ grating_constant=testing_grating_constant,
475
+ grating_angle=testing_grating_angle,
476
+ arm_position=testing_arm_position,
429
477
  )
430
478
 
431
479
  # When
432
480
  task()
433
481
  # Then
482
+ assert task.constants._db_dict["ARM_ID"] == testing_arm_id
434
483
  expected_dark_readout_exp_times = [
435
484
  lamp_readout_exp_time,
436
485
  solar_readout_exp_time,
437
486
  ] + observe_readout_exp_times
438
- assert task.constants._db_dict["OBS_IP_START_TIME"] == "2022-11-28T13:55:00"
487
+ assert task.constants._db_dict["OBS_IP_START_TIME"] == testing_obs_ip_start_time
488
+ assert task.constants._db_dict["INCIDENT_LIGHT_ANGLE_DEG"] == -1 * testing_grating_angle
489
+ assert (
490
+ task.constants._db_dict["REFLECTED_LIGHT_ANGLE_DEG"]
491
+ == -1 * testing_grating_angle + testing_arm_position
492
+ )
493
+ assert task.constants._db_dict["GRATING_CONSTANT_INVERSE_MM"] == testing_grating_constant
494
+ assert task.constants._db_dict["SOLAR_GAIN_IP_START_TIME"] == testing_solar_ip_start_time
439
495
  assert task.constants._db_dict["NUM_MODSTATES"] == num_modstates
440
496
  assert task.constants._db_dict["NUM_MAP_SCANS"] == num_maps_per_readout_exp_time * len(
441
497
  observe_readout_exp_times
@@ -468,6 +524,10 @@ def test_parse_visp_values(
468
524
  num_modstates,
469
525
  mocker,
470
526
  fake_gql_client,
527
+ testing_arm_id,
528
+ testing_grating_constant,
529
+ testing_grating_angle,
530
+ testing_arm_position,
471
531
  ):
472
532
  """
473
533
  :Given: A valid parse input task
@@ -486,6 +546,10 @@ def test_parse_visp_values(
486
546
  num_maps=1,
487
547
  num_steps=1,
488
548
  num_modstates=num_modstates,
549
+ arm_id=testing_arm_id,
550
+ grating_constant=testing_grating_constant,
551
+ grating_angle=testing_grating_angle,
552
+ arm_position=testing_arm_position,
489
553
  )
490
554
 
491
555
  task()
@@ -585,6 +649,10 @@ def test_intensity_observes_and_polarimetric_cals(
585
649
  observe_exp_times,
586
650
  mocker,
587
651
  fake_gql_client,
652
+ testing_arm_id,
653
+ testing_grating_constant,
654
+ testing_grating_angle,
655
+ testing_arm_position,
588
656
  ):
589
657
  """
590
658
  :Given: Data where the observe frames are in intensity mode and the calibration frames are in polarimetric mode
@@ -604,6 +672,10 @@ def test_intensity_observes_and_polarimetric_cals(
604
672
  readout_exp_time=observe_readout_exp_times[0],
605
673
  exp_time=observe_exp_times[0],
606
674
  obs_dataset_class=VispHeadersIntensityObserveFrames,
675
+ arm_id=testing_arm_id,
676
+ grating_constant=testing_grating_constant,
677
+ grating_angle=testing_grating_angle,
678
+ arm_position=testing_arm_position,
607
679
  )
608
680
  task()
609
681
  assert task.constants._db_dict["NUM_MODSTATES"] == 1
@@ -144,7 +144,7 @@ def dummy_calibration_collection():
144
144
 
145
145
  dark_dict = {VispTag.beam(beam): {VispTag.readout_exp_time(0.04): np.zeros(intermediate_shape)}}
146
146
  background_dict = {VispTag.beam(beam): np.zeros(intermediate_shape)}
147
- solar_dict = {VispTag.beam(beam): {VispTag.modstate(modstate): np.ones(intermediate_shape)}}
147
+ solar_dict = {VispTag.beam(beam): np.ones(intermediate_shape)}
148
148
  angle_dict = {VispTag.beam(beam): 0.0}
149
149
  spec_dict = {VispTag.beam(beam): np.zeros(intermediate_shape[1])}
150
150
  offset_dict = {VispTag.beam(beam): {VispTag.modstate(modstate): np.zeros(2)}}
@@ -252,7 +252,7 @@ def calibration_collection_with_full_overlap_slice() -> CalibrationCollection:
252
252
 
253
253
  @pytest.mark.parametrize(
254
254
  "background_on",
255
- [pytest.param(True, id="Background on"), pytest.param(False, id="Background off")],
255
+ [pytest.param(True, id="background_on"), pytest.param(False, id="background_off")],
256
256
  )
257
257
  def test_science_calibration_task(
258
258
  science_calibration_task,
@@ -308,7 +308,8 @@ def test_science_calibration_task(
308
308
  task=task, num_modstates=num_modstates, data_shape=intermediate_shape, offsets=offsets
309
309
  )
310
310
  write_dummy_intermediate_solar_cals_to_task(
311
- task=task, data_shape=intermediate_shape, num_modstates=num_modstates
311
+ task=task,
312
+ data_shape=intermediate_shape,
312
313
  )
313
314
  write_demod_matrices_to_task(task=task, num_modstates=num_modstates)
314
315
  write_input_observe_frames_to_task(
@@ -658,9 +659,7 @@ def test_generate_nan_mask(science_calibration_task, dummy_calibration_collectio
658
659
  calibration_collection, _, _ = dummy_calibration_collection
659
660
  beam = 1
660
661
  modstate = 1
661
- solar_gain_array = calibration_collection.solar_gain[VispTag.beam(beam)][
662
- VispTag.modstate(modstate)
663
- ]
662
+ solar_gain_array = calibration_collection.solar_gain[VispTag.beam(beam)]
664
663
  angle = calibration_collection.angle[VispTag.beam(beam)]
665
664
  spec_shift = calibration_collection.spec_shift[VispTag.beam(beam)]
666
665
  state_offset = calibration_collection.state_offset[VispTag.beam(beam)][