dkist-processing-visp 3.3.0__py3-none-any.whl → 5.1.1__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 (71) hide show
  1. dkist_processing_visp/__init__.py +1 -0
  2. dkist_processing_visp/config.py +1 -0
  3. dkist_processing_visp/models/constants.py +52 -21
  4. dkist_processing_visp/models/fits_access.py +20 -0
  5. dkist_processing_visp/models/metric_code.py +10 -0
  6. dkist_processing_visp/models/parameters.py +129 -19
  7. dkist_processing_visp/models/tags.py +1 -0
  8. dkist_processing_visp/models/task_name.py +1 -0
  9. dkist_processing_visp/parsers/map_repeats.py +1 -0
  10. dkist_processing_visp/parsers/modulator_states.py +1 -0
  11. dkist_processing_visp/parsers/polarimeter_mode.py +3 -1
  12. dkist_processing_visp/parsers/raster_step.py +4 -1
  13. dkist_processing_visp/parsers/spectrograph_configuration.py +75 -0
  14. dkist_processing_visp/parsers/time.py +15 -7
  15. dkist_processing_visp/parsers/visp_l0_fits_access.py +19 -8
  16. dkist_processing_visp/parsers/visp_l1_fits_access.py +1 -0
  17. dkist_processing_visp/tasks/__init__.py +1 -0
  18. dkist_processing_visp/tasks/assemble_movie.py +1 -0
  19. dkist_processing_visp/tasks/background_light.py +2 -1
  20. dkist_processing_visp/tasks/dark.py +5 -4
  21. dkist_processing_visp/tasks/geometric.py +132 -20
  22. dkist_processing_visp/tasks/instrument_polarization.py +13 -12
  23. dkist_processing_visp/tasks/l1_output_data.py +203 -0
  24. dkist_processing_visp/tasks/lamp.py +53 -93
  25. dkist_processing_visp/tasks/make_movie_frames.py +8 -6
  26. dkist_processing_visp/tasks/mixin/beam_access.py +1 -0
  27. dkist_processing_visp/tasks/mixin/corrections.py +54 -4
  28. dkist_processing_visp/tasks/mixin/downsample.py +1 -0
  29. dkist_processing_visp/tasks/parse.py +34 -4
  30. dkist_processing_visp/tasks/quality_metrics.py +5 -4
  31. dkist_processing_visp/tasks/science.py +126 -46
  32. dkist_processing_visp/tasks/solar.py +896 -456
  33. dkist_processing_visp/tasks/visp_base.py +2 -0
  34. dkist_processing_visp/tasks/write_l1.py +25 -5
  35. dkist_processing_visp/tests/conftest.py +99 -35
  36. dkist_processing_visp/tests/header_models.py +92 -20
  37. dkist_processing_visp/tests/local_trial_workflows/l0_cals_only.py +4 -23
  38. dkist_processing_visp/tests/local_trial_workflows/l0_polcals_as_science.py +421 -0
  39. dkist_processing_visp/tests/local_trial_workflows/l0_solar_gain_as_science.py +10 -29
  40. dkist_processing_visp/tests/local_trial_workflows/l0_to_l1.py +1 -21
  41. dkist_processing_visp/tests/local_trial_workflows/local_trial_helpers.py +98 -14
  42. dkist_processing_visp/tests/test_assemble_movie.py +2 -3
  43. dkist_processing_visp/tests/test_assemble_quality.py +89 -4
  44. dkist_processing_visp/tests/test_background_light.py +8 -5
  45. dkist_processing_visp/tests/test_dark.py +4 -3
  46. dkist_processing_visp/tests/test_fits_access.py +43 -0
  47. dkist_processing_visp/tests/test_geometric.py +45 -4
  48. dkist_processing_visp/tests/test_instrument_polarization.py +4 -3
  49. dkist_processing_visp/tests/test_lamp.py +22 -26
  50. dkist_processing_visp/tests/test_make_movie_frames.py +4 -4
  51. dkist_processing_visp/tests/test_map_repeats.py +3 -1
  52. dkist_processing_visp/tests/test_parameters.py +122 -21
  53. dkist_processing_visp/tests/test_parse.py +98 -14
  54. dkist_processing_visp/tests/test_quality.py +2 -3
  55. dkist_processing_visp/tests/test_science.py +113 -15
  56. dkist_processing_visp/tests/test_solar.py +318 -99
  57. dkist_processing_visp/tests/test_visp_constants.py +36 -8
  58. dkist_processing_visp/tests/test_workflows.py +1 -0
  59. dkist_processing_visp/tests/test_write_l1.py +17 -3
  60. dkist_processing_visp/workflows/__init__.py +1 -0
  61. dkist_processing_visp/workflows/l0_processing.py +8 -2
  62. dkist_processing_visp/workflows/trial_workflows.py +8 -2
  63. dkist_processing_visp-5.1.1.dist-info/METADATA +552 -0
  64. dkist_processing_visp-5.1.1.dist-info/RECORD +94 -0
  65. docs/conf.py +5 -1
  66. docs/gain_correction.rst +50 -42
  67. dkist_processing_visp/tasks/mixin/line_zones.py +0 -115
  68. dkist_processing_visp-3.3.0.dist-info/METADATA +0 -459
  69. dkist_processing_visp-3.3.0.dist-info/RECORD +0 -90
  70. {dkist_processing_visp-3.3.0.dist-info → dkist_processing_visp-5.1.1.dist-info}/WHEEL +0 -0
  71. {dkist_processing_visp-3.3.0.dist-info → dkist_processing_visp-5.1.1.dist-info}/top_level.txt +0 -0
@@ -2,52 +2,217 @@ import json
2
2
  from functools import partial
3
3
  from typing import Callable
4
4
 
5
+ import asdf
6
+ import astropy.units as u
5
7
  import numpy as np
6
8
  import pytest
9
+ from asdf.tags.core import NDArrayType
10
+ from astropy import constants
11
+ from astropy.coordinates import EarthLocation
12
+ from astropy.io import fits
13
+ from astropy.stats import gaussian_fwhm_to_sigma
14
+ from astropy.time import Time
7
15
  from dkist_processing_common._util.scratch import WorkflowFileSystem
8
16
  from dkist_processing_common.codecs.fits import fits_array_decoder
17
+ from dkist_processing_common.codecs.fits import fits_array_encoder
9
18
  from dkist_processing_common.models.tags import Tag
10
- from dkist_processing_common.tests.conftest import FakeGQLClient
19
+ from scipy.ndimage import gaussian_filter1d
20
+ from solar_wavelength_calibration import Atlas
21
+ from sunpy.coordinates import HeliocentricInertial
11
22
 
12
23
  from dkist_processing_visp.models.tags import VispTag
13
24
  from dkist_processing_visp.tasks.solar import SolarCalibration
14
- from dkist_processing_visp.tests.conftest import tag_on_modstate
25
+ from dkist_processing_visp.tasks.solar import WavelengthCalibrationParametersWithContinuum
26
+ from dkist_processing_visp.tasks.solar import compute_initial_dispersion
15
27
  from dkist_processing_visp.tests.conftest import VispConstantsDb
16
28
  from dkist_processing_visp.tests.conftest import VispInputDatasetParameterValues
29
+ from dkist_processing_visp.tests.conftest import tag_on_modstate
17
30
  from dkist_processing_visp.tests.conftest import write_frames_to_task
18
31
  from dkist_processing_visp.tests.conftest import write_intermediate_background_to_task
19
32
  from dkist_processing_visp.tests.conftest import write_intermediate_darks_to_task
20
33
  from dkist_processing_visp.tests.conftest import write_intermediate_geometric_to_task
21
- from dkist_processing_visp.tests.conftest import write_intermediate_lamp_to_task
22
34
  from dkist_processing_visp.tests.header_models import VispHeadersInputSolarGainFrames
23
35
 
24
36
 
25
- def lamp_signal_func(beam: int, modstate: int):
26
- return 10 * beam * modstate
37
+ def lamp_signal_func(beam: int):
38
+ return 10 * beam
39
+
40
+
41
+ @pytest.fixture(scope="session")
42
+ def num_wave_pix() -> int:
43
+ return 1000
44
+
45
+
46
+ @pytest.fixture(scope="session")
47
+ def telluric_opacity_factor() -> float:
48
+ return 5.0
49
+
50
+
51
+ @pytest.fixture(scope="session")
52
+ def solar_ip_start_time() -> str:
53
+ return "1988-07-02T10:00:00"
54
+
55
+
56
+ @pytest.fixture(scope="session")
57
+ def doppler_velocity(solar_ip_start_time) -> u.Quantity:
58
+ _dkist_site_info = {
59
+ "aliases": ["DKIST", "ATST"],
60
+ "name": "Daniel K. Inouye Solar Telescope",
61
+ "elevation": 3067,
62
+ "elevation_unit": "meter",
63
+ "latitude": 20.7067,
64
+ "latitude_unit": "degree",
65
+ "longitude": 203.7436,
66
+ "longitude_unit": "degree",
67
+ "timezone": "US/Hawaii",
68
+ "source": "DKIST website: https://www.nso.edu/telescopes/dki-solar-telescope/",
69
+ }
70
+ location_of_dkist = EarthLocation.from_geodetic(
71
+ _dkist_site_info["longitude"] * u.Unit(_dkist_site_info["longitude_unit"]),
72
+ _dkist_site_info["latitude"] * u.Unit(_dkist_site_info["latitude_unit"]),
73
+ _dkist_site_info["elevation"] * u.Unit(_dkist_site_info["elevation_unit"]),
74
+ )
75
+
76
+ coord = location_of_dkist.get_gcrs(obstime=Time(solar_ip_start_time))
77
+ heliocentric_coord = coord.transform_to(HeliocentricInertial(obstime=Time(solar_ip_start_time)))
78
+ obs_vr_kms = heliocentric_coord.d_distance
79
+
80
+ return obs_vr_kms
81
+
82
+
83
+ @pytest.fixture(scope="session")
84
+ def central_wavelength() -> u.Quantity:
85
+ return 854.2 * u.nm
86
+
87
+
88
+ @pytest.fixture(scope="session")
89
+ def grating_constant() -> u.Quantity:
90
+ return 316.0 / u.mm
91
+
92
+
93
+ @pytest.fixture(scope="session")
94
+ def incident_light_angle() -> u.Quantity:
95
+ return 73.22 * u.deg
96
+
97
+
98
+ @pytest.fixture(scope="session")
99
+ def reflected_light_angle() -> u.Quantity:
100
+ return 68.76 * u.deg
101
+
102
+
103
+ @pytest.fixture(scope="session")
104
+ def pixel_pitch() -> u.Quantity:
105
+ return 6.5 * u.micron / u.pix
106
+
107
+
108
+ @pytest.fixture(scope="session")
109
+ def lens_parameters_no_units() -> list[float]:
110
+ return [0.7613, 1.720e-4, -8.139e-8]
111
+
112
+
113
+ @pytest.fixture(scope="session")
114
+ def lens_parameters(lens_parameters_no_units) -> list[u.Quantity]:
115
+ return [
116
+ lens_parameters_no_units[0] * u.m,
117
+ lens_parameters_no_units[1] * u.m / u.nm,
118
+ lens_parameters_no_units[2] * u.m / u.nm**2,
119
+ ]
120
+
121
+
122
+ @pytest.fixture(scope="session")
123
+ def dispersion(
124
+ central_wavelength, incident_light_angle, reflected_light_angle, lens_parameters, pixel_pitch
125
+ ) -> u.Quantity:
126
+ return compute_initial_dispersion(
127
+ central_wavelength=central_wavelength,
128
+ incident_light_angle=incident_light_angle,
129
+ reflected_light_angle=reflected_light_angle,
130
+ lens_parameters=lens_parameters,
131
+ pixel_pitch=pixel_pitch,
132
+ )
133
+
134
+
135
+ @pytest.fixture(scope="session")
136
+ def resolving_power() -> int:
137
+ return int(1.15e5)
138
+
139
+
140
+ @pytest.fixture(scope="session")
141
+ def solar_atlas() -> Atlas:
142
+ return Atlas()
143
+
144
+
145
+ @pytest.fixture(scope="session")
146
+ def spectral_vignette_function(num_wave_pix) -> np.ndarray:
147
+ return (np.arange(num_wave_pix) - num_wave_pix // 2) / (num_wave_pix * 10) + 1.0
148
+
149
+
150
+ @pytest.fixture
151
+ def observed_solar_spectrum(
152
+ central_wavelength,
153
+ dispersion,
154
+ num_wave_pix,
155
+ solar_atlas,
156
+ telluric_opacity_factor,
157
+ doppler_velocity,
158
+ resolving_power,
159
+ ) -> np.ndarray:
160
+ """Make an "observed" solar spectrum from a shifted and atmospherically corrected combination of solar and telluric atlases."""
161
+ # Making a simple solution with just y = mx + b (no higher order angle or order stuff)
162
+ wave_vec = (
163
+ np.arange(num_wave_pix) - num_wave_pix // 2
164
+ ) * u.pix * dispersion + central_wavelength
165
+
166
+ doppler_shift = doppler_velocity / constants.c * central_wavelength
167
+ solar_signal = np.interp(
168
+ wave_vec,
169
+ solar_atlas.solar_atlas_wavelength + doppler_shift,
170
+ solar_atlas.solar_atlas_transmission,
171
+ )
172
+ telluric_signal = (
173
+ np.interp(
174
+ wave_vec,
175
+ solar_atlas.telluric_atlas_wavelength,
176
+ solar_atlas.telluric_atlas_transmission,
177
+ )
178
+ ** telluric_opacity_factor
179
+ )
180
+
181
+ combined_spec = solar_signal * telluric_signal
182
+
183
+ sigma_wavelength = (
184
+ central_wavelength.to_value(u.nm) / resolving_power
185
+ ) * gaussian_fwhm_to_sigma
186
+ sigma_pix = sigma_wavelength / np.abs(dispersion.to_value(u.nm / u.pix))
187
+ observed_spec = gaussian_filter1d(combined_spec, np.abs(sigma_pix))
188
+
189
+ return observed_spec
27
190
 
28
191
 
29
192
  def write_full_set_of_intermediate_lamp_cals_to_task(
30
193
  task,
31
194
  data_shape: tuple[int, int],
32
- num_modstates: int,
33
- lamp_signal_func: Callable[[int, int], float] = lamp_signal_func,
195
+ lamp_signal_func: Callable[[int], float] = lamp_signal_func,
34
196
  ):
35
197
  for beam in [1, 2]:
36
- for modstate in range(1, num_modstates + 1):
37
- lamp_signal = lamp_signal_func(beam, modstate)
38
- write_intermediate_lamp_to_task(
39
- task=task,
40
- lamp_signal=lamp_signal,
41
- beam=beam,
42
- modstate=modstate,
43
- data_shape=data_shape,
44
- )
198
+ lamp_signal = lamp_signal_func(beam)
199
+ lamp_array = np.ones(data_shape) * lamp_signal
200
+ task.write(
201
+ data=lamp_array,
202
+ tags=[
203
+ VispTag.intermediate_frame(beam=beam),
204
+ VispTag.task_lamp_gain(),
205
+ ],
206
+ encoder=fits_array_encoder,
207
+ )
45
208
 
46
209
 
47
210
  def make_solar_input_array_data(
48
211
  frame: VispHeadersInputSolarGainFrames,
49
212
  dark_signal: float,
50
- lamp_signal_func: Callable[[int, int], float] = lamp_signal_func,
213
+ true_solar_signal: np.ndarray,
214
+ spectral_vignette_signal: np.ndarray,
215
+ lamp_signal_func: Callable[[int], float] = lamp_signal_func,
51
216
  ):
52
217
  data_shape = frame.array_shape[1:]
53
218
  beam_shape = (data_shape[0] // 2, data_shape[1])
@@ -56,12 +221,14 @@ def make_solar_input_array_data(
56
221
 
57
222
  beam_list = []
58
223
  for beam in [1, 2]:
59
- true_gain = np.ones(beam_shape) + modstate + beam
60
- true_solar_signal = np.arange(1, beam_shape[0] + 1) / 5
224
+ true_gain = np.ones(beam_shape) * spectral_vignette_signal[:, None]
61
225
  true_solar_gain = true_gain * true_solar_signal[:, None]
62
- lamp_signal = lamp_signal_func(beam, modstate)
226
+ lamp_signal = lamp_signal_func(beam)
63
227
  raw_beam = (true_solar_gain * lamp_signal) + dark_signal
64
- beam_list.append(raw_beam)
228
+ if beam == 2:
229
+ beam_list.append(raw_beam[::-1, :])
230
+ else:
231
+ beam_list.append(raw_beam)
65
232
 
66
233
  raw_solar = np.concatenate(beam_list) * num_raw_per_fpa
67
234
  return raw_solar
@@ -73,6 +240,8 @@ def write_input_solar_gains_to_task(
73
240
  dark_signal: float,
74
241
  readout_exp_time: float,
75
242
  num_modstates: int,
243
+ true_solar_signal: np.ndarray,
244
+ spectra_vignette_signal: np.ndarray,
76
245
  lamp_signal_func: Callable[[int, int], float] = lamp_signal_func,
77
246
  ):
78
247
  array_shape = (1, *data_shape)
@@ -82,7 +251,11 @@ def write_input_solar_gains_to_task(
82
251
  num_modstates=num_modstates,
83
252
  )
84
253
  data_func = partial(
85
- make_solar_input_array_data, dark_signal=dark_signal, lamp_signal_func=lamp_signal_func
254
+ make_solar_input_array_data,
255
+ dark_signal=dark_signal,
256
+ lamp_signal_func=lamp_signal_func,
257
+ true_solar_signal=true_solar_signal,
258
+ spectral_vignette_signal=spectra_vignette_signal,
86
259
  )
87
260
  write_frames_to_task(
88
261
  task=task,
@@ -102,11 +275,23 @@ def solar_gain_task(
102
275
  tmp_path,
103
276
  recipe_run_id,
104
277
  init_visp_constants_db,
278
+ central_wavelength,
279
+ incident_light_angle,
280
+ reflected_light_angle,
281
+ grating_constant,
282
+ solar_ip_start_time,
105
283
  ):
106
284
  number_of_modstates = 3
107
285
  readout_exp_time = 40.0
108
286
  constants_db = VispConstantsDb(
109
- NUM_MODSTATES=number_of_modstates, SOLAR_READOUT_EXP_TIMES=(readout_exp_time,)
287
+ ARM_ID=1,
288
+ WAVELENGTH=central_wavelength.to_value(u.nm),
289
+ NUM_MODSTATES=number_of_modstates,
290
+ SOLAR_READOUT_EXP_TIMES=(readout_exp_time,),
291
+ SOLAR_GAIN_IP_START_TIME=solar_ip_start_time,
292
+ INCIDENT_LIGHT_ANGLE_DEG=incident_light_angle.to_value(u.deg),
293
+ REFLECTED_LIGHT_ANGLE_DEG=reflected_light_angle.to_value(u.deg),
294
+ GRATING_CONSTANT_INVERSE_MM=grating_constant.to_value(1 / u.mm),
110
295
  )
111
296
  init_visp_constants_db(recipe_run_id, constants_db)
112
297
  with SolarCalibration(
@@ -126,35 +311,50 @@ def solar_gain_task(
126
311
 
127
312
  @pytest.mark.parametrize(
128
313
  "background_on",
129
- [pytest.param(True, id="Background on"), pytest.param(False, id="Background off")],
314
+ [
315
+ pytest.param(True, id="background_on"),
316
+ pytest.param(False, id="background_off"),
317
+ ],
130
318
  )
131
- def test_solar_gain_task(solar_gain_task, background_on, assign_input_dataset_doc_to_task, mocker):
319
+ def test_solar_gain_task(
320
+ solar_gain_task,
321
+ background_on,
322
+ num_wave_pix,
323
+ spectral_vignette_function,
324
+ observed_solar_spectrum,
325
+ solar_ip_start_time,
326
+ lens_parameters_no_units,
327
+ resolving_power,
328
+ telluric_opacity_factor,
329
+ assign_input_dataset_doc_to_task,
330
+ mocker,
331
+ fake_gql_client,
332
+ ):
132
333
  """
133
334
  Given: A set of raw solar gain images and necessary intermediate calibrations
134
335
  When: Running the solargain task
135
336
  Then: The task completes and the outputs are correct
136
337
  """
137
338
  mocker.patch(
138
- "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=FakeGQLClient
139
- )
140
-
141
- # It's way too hard to make data for a unit test that will get through the line zones calculation.
142
- # Leave that for grogu.
143
- mocker.patch(
144
- "dkist_processing_visp.tasks.solar.SolarCalibration.compute_line_zones",
145
- return_value=[(4, 7)],
339
+ "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=fake_gql_client
146
340
  )
147
341
 
148
342
  task, readout_exp_time, num_modstates = solar_gain_task
149
343
  dark_signal = 3.0
150
- input_shape = (20, 10)
151
- intermediate_shape = (10, 10)
344
+ input_shape = (num_wave_pix * 2, 100)
345
+ intermediate_shape = (num_wave_pix, 100)
152
346
  beam_border = input_shape[0] // 2
153
347
  assign_input_dataset_doc_to_task(
154
348
  task,
155
349
  VispInputDatasetParameterValues(
156
- visp_background_on=background_on, visp_beam_border=beam_border
350
+ visp_background_on=background_on,
351
+ visp_beam_border=beam_border,
352
+ visp_wavecal_init_opacity_factor=telluric_opacity_factor,
353
+ visp_wavecal_init_resolving_power=resolving_power,
354
+ visp_wavecal_camera_lens_parameters_1=lens_parameters_no_units,
355
+ visp_solar_vignette_initial_continuum_poly_fit_order=1,
157
356
  ),
357
+ arm_id=1,
158
358
  )
159
359
  write_intermediate_darks_to_task(
160
360
  task=task,
@@ -167,7 +367,8 @@ def test_solar_gain_task(solar_gain_task, background_on, assign_input_dataset_do
167
367
  task=task, background_signal=0.0, data_shape=intermediate_shape
168
368
  )
169
369
  write_full_set_of_intermediate_lamp_cals_to_task(
170
- task=task, data_shape=intermediate_shape, num_modstates=num_modstates
370
+ task=task,
371
+ data_shape=intermediate_shape,
171
372
  )
172
373
  write_intermediate_geometric_to_task(
173
374
  task=task, num_modstates=num_modstates, data_shape=intermediate_shape
@@ -178,43 +379,38 @@ def test_solar_gain_task(solar_gain_task, background_on, assign_input_dataset_do
178
379
  dark_signal=dark_signal,
179
380
  readout_exp_time=readout_exp_time,
180
381
  num_modstates=num_modstates,
382
+ true_solar_signal=observed_solar_spectrum,
383
+ spectra_vignette_signal=spectral_vignette_function,
181
384
  )
182
385
 
183
386
  task()
184
387
  for beam in range(1, task.constants.num_beams + 1):
185
- equalization_flux = np.nanmedian(
186
- [
187
- np.ones(intermediate_shape)
188
- * (1 + beam + m)
189
- * (10 * beam * m)
190
- * np.nanpercentile(
191
- np.arange(1, 11) / 5,
192
- task.parameters.solar_characteristic_spatial_normalization_percentile,
193
- )
194
- for m in range(1, task.constants.num_modstates + 1)
195
- ],
196
- axis=0,
388
+ # We need to multiply by the percentile normalization factor that is divided from the characteristic spectra.
389
+ # I.e., the `true_solar_signal` defined above is modified by this scalar prior to removal from the raw spectra
390
+ # so we need to undo that modification here.
391
+ expected_signal = (
392
+ np.ones(intermediate_shape)
393
+ * spectral_vignette_function[:, None]
394
+ * 10
395
+ * beam # Lamp signal
396
+ * np.nanpercentile(
397
+ observed_solar_spectrum,
398
+ task.parameters.solar_characteristic_spatial_normalization_percentile,
399
+ )
197
400
  )
198
401
 
199
- for modstate in range(1, task.constants.num_modstates + 1):
200
- # Gains aren't normalized so their expected value is weird. This expression comes from the math applied above. Sorry.
201
- raw = (
202
- np.ones(intermediate_shape)
203
- * (1 + beam + modstate)
204
- * (10 * beam * modstate)
205
- * np.mean(np.arange(1, 11) / 5)
402
+ solar_gain = next(
403
+ task.read(
404
+ tags=[
405
+ VispTag.intermediate_frame(beam=beam),
406
+ VispTag.task_solar_gain(),
407
+ ],
408
+ decoder=fits_array_decoder,
206
409
  )
207
- expected = raw * equalization_flux / np.nanmedian(raw)
208
- solar_gain = next(
209
- task.read(
210
- tags=[
211
- VispTag.intermediate_frame(beam=beam, modstate=modstate),
212
- VispTag.task_solar_gain(),
213
- ],
214
- decoder=fits_array_decoder,
215
- )
216
- )
217
- np.testing.assert_allclose(expected, solar_gain)
410
+ )
411
+
412
+ # Testing that the ratio is close to 1.0 give us a bit more leeway in the deviation
413
+ np.testing.assert_allclose(expected_signal / solar_gain, 1.0, atol=1e-2, rtol=1e-2)
218
414
 
219
415
  quality_files = task.read(tags=[Tag.quality("TASK_TYPES")])
220
416
  for file in quality_files:
@@ -225,42 +421,65 @@ def test_solar_gain_task(solar_gain_task, background_on, assign_input_dataset_do
225
421
  tags=[VispTag.input(), VispTag.frame(), VispTag.task_solar_gain()]
226
422
  )
227
423
 
424
+ first_vignette_quality_files = list(task.read(tags=[Tag.quality("SOLAR_CAL_FIRST_VIGNETTE")]))
425
+ assert len(first_vignette_quality_files) == 2
426
+ for beam_file in first_vignette_quality_files:
427
+ with asdf.open(beam_file, lazy_load=False, memmap=False) as f:
428
+ results = f.tree
429
+ assert isinstance(results["output_wave_vec"], NDArrayType)
430
+ assert isinstance(results["input_spectrum"], NDArrayType)
431
+ assert isinstance(results["best_fit_atlas"], NDArrayType)
432
+ assert isinstance(results["best_fit_continuum"], NDArrayType)
433
+ assert isinstance(results["residuals"], NDArrayType)
228
434
 
229
- def test_line_zones(solar_gain_task):
230
- """
231
- Given: A spectrum with some absorption lines
232
- When: Computing zones around the lines
233
- Then: Correct results are returned
234
- """
235
- # This is here because we mocked it out in the solar gain task test above
236
- # NOTE that it does not test for removal of overlapping regions
237
- def gaussian(x, amp, mu, sig):
238
- return amp * np.exp(-np.power(x - mu, 2.0) / (2 * np.power(sig, 2.0)))
239
-
240
- spec = np.ones(1000) * 100
241
- x = np.arange(1000.0)
242
- expected = []
243
- for m, s in zip([100.0, 300.0, 700], [10.0, 20.0, 5.0]):
244
- spec -= gaussian(x, 40, m, s)
245
- hwhm = s * 2.355 / 2
246
- expected.append((np.floor(m - hwhm).astype(int), np.ceil(m + hwhm).astype(int)))
247
-
248
- task = solar_gain_task[0]
249
-
250
- zones = task.compute_line_zones(spec[:, None], bg_order=0, rel_height=0.5)
251
- assert zones == expected
435
+ final_vignette_quality_files = list(task.read(tags=[Tag.quality("SOLAR_CAL_FINAL_VIGNETTE")]))
436
+ assert len(final_vignette_quality_files) == 2
437
+ for beam_file in final_vignette_quality_files:
438
+ with asdf.open(beam_file, lazy_load=False, memmap=False) as f:
439
+ results = f.tree
440
+ assert isinstance(results["output_wave_vec"], NDArrayType)
441
+ assert isinstance(results["median_spec"], NDArrayType)
442
+ assert isinstance(results["low_deviation"], NDArrayType)
443
+ assert isinstance(results["high_deviation"], NDArrayType)
252
444
 
253
445
 
254
- def test_identify_overlapping_zones(solar_gain_task):
446
+ def test_continuum_wavecal_parameters():
255
447
  """
256
- Given: A list of zone borders that contain overlapping zones
257
- When: Identifying zones that overlap
258
- Then: The smaller of the overlapping zones are identified for removal
448
+ Given: A `WavelengthCalibrationParametersWithContinuum` class instantiated with a poly fit order
449
+ When: Constructing the `lmfit_parameters` object
450
+ Then: The polynomial coefficients are present
259
451
  """
260
- rips = np.array([100, 110, 220, 200])
261
- lips = np.array([150, 120, 230, 250])
452
+ order = 3
453
+ zeroth_order = 6.28
454
+ param_object = WavelengthCalibrationParametersWithContinuum(
455
+ continuum_poly_fit_order=order,
456
+ zeroth_order_continuum_coefficient=zeroth_order,
457
+ crval=400 * u.nm,
458
+ dispersion=3 * u.nm / u.pix,
459
+ incident_light_angle=4 * u.deg,
460
+ grating_constant=3 / u.mm,
461
+ doppler_velocity=0 * u.km / u.s,
462
+ order=1,
463
+ normalized_abscissa=np.arange(10),
464
+ continuum_level=99999.9,
465
+ )
466
+ lmfit_params = param_object.lmfit_parameters
467
+
468
+ assert "poly_coeff_03" in lmfit_params
469
+ assert "poly_coeff_02" in lmfit_params
470
+ assert "poly_coeff_01" in lmfit_params
471
+ assert "poly_coeff_00" in lmfit_params
262
472
 
263
- task = solar_gain_task[0]
473
+ assert lmfit_params["poly_coeff_03"].init_value == zeroth_order
474
+ assert lmfit_params["poly_coeff_02"].init_value == 0
475
+ assert lmfit_params["poly_coeff_01"].init_value == 0
476
+ assert lmfit_params["poly_coeff_00"].init_value == 0
264
477
 
265
- idx_to_remove = task.identify_overlapping_zones(rips, lips)
266
- assert idx_to_remove == [1, 2]
478
+ assert lmfit_params["poly_coeff_03"].min == zeroth_order * 0.5
479
+ assert lmfit_params["poly_coeff_03"].max == zeroth_order * 1.5
480
+ assert lmfit_params["poly_coeff_02"].min == -1
481
+ assert lmfit_params["poly_coeff_02"].max == 1
482
+ assert lmfit_params["poly_coeff_01"].min == -1
483
+ assert lmfit_params["poly_coeff_01"].max == 1
484
+ assert lmfit_params["poly_coeff_00"].min == -1
485
+ assert lmfit_params["poly_coeff_00"].max == 1
@@ -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 pytest
5
6
 
6
7
  from dkist_processing_visp.tasks.visp_base import VispTaskBase
@@ -8,6 +9,7 @@ from dkist_processing_visp.tasks.visp_base import VispTaskBase
8
9
 
9
10
  @dataclass
10
11
  class testing_constants:
12
+ arm_id: int = 2
11
13
  obs_ip_start_time: str = "1999-12-31T23:59:59"
12
14
  num_modstates: int = 10
13
15
  num_beams: int = 2
@@ -22,10 +24,23 @@ class testing_constants:
22
24
  solar_readout_exp_times: tuple[float] = (2.0,)
23
25
  observe_readout_exp_times: tuple[float] = (0.02,)
24
26
  retarder_name: str = "SiO2 OC"
27
+ incident_light_angle_deg: float = 74.2
28
+ reflected_light_angle_deg: float = 71.3
29
+ grating_constant_inverse_mm: float = 316.0
30
+ solar_gain_ip_start_time: str = "2000-01-01T00:00:01"
25
31
  # We don't need all the common ones, but let's put one just to check
26
32
  instrument: str = "CHECK_OUT_THIS_INSTRUMENT"
27
33
 
28
34
 
35
+ @pytest.fixture(scope="session")
36
+ def unit_constant_names_and_units() -> dict[str, u.Unit]:
37
+ return {
38
+ "incident_light_angle_deg": u.deg,
39
+ "reflected_light_angle_deg": u.deg,
40
+ "grating_constant_inverse_mm": 1 / u.mm,
41
+ }
42
+
43
+
29
44
  @pytest.fixture(scope="session")
30
45
  def expected_constant_dict() -> dict:
31
46
  lower_dict = asdict(testing_constants())
@@ -35,8 +50,7 @@ def expected_constant_dict() -> dict:
35
50
  @pytest.fixture(scope="function")
36
51
  def visp_science_task_with_constants(recipe_run_id, expected_constant_dict, init_visp_constants_db):
37
52
  class Task(VispTaskBase):
38
- def run(self):
39
- ...
53
+ def run(self): ...
40
54
 
41
55
  init_visp_constants_db(recipe_run_id, expected_constant_dict)
42
56
  task = Task(
@@ -50,14 +64,28 @@ def visp_science_task_with_constants(recipe_run_id, expected_constant_dict, init
50
64
  task._purge()
51
65
 
52
66
 
53
- def test_visp_constants(visp_science_task_with_constants, expected_constant_dict):
54
-
67
+ def test_visp_constants(
68
+ visp_science_task_with_constants, unit_constant_names_and_units, expected_constant_dict
69
+ ):
70
+ """
71
+ Given: A task with an attached `VispConstants` object and a populated constants db
72
+ When: Accessing values through the `.constants.VALUE` machinery
73
+ Then: The correct values form the db are returned
74
+ """
55
75
  task = visp_science_task_with_constants
56
76
  for k, v in expected_constant_dict.items():
57
- if type(v) is tuple:
58
- v = list(v)
59
- if k in ["POLARIMETER_MODE", "RETARDER_NAME"]:
77
+ if k in ["ARM_ID", "POLARIMETER_MODE", "RETARDER_NAME"]:
78
+ # These have some extra logic after db retrieval
60
79
  continue
61
- assert getattr(task.constants, k.lower()) == v
80
+ if k.lower() in unit_constant_names_and_units:
81
+ united_value = getattr(task.constants, k.lower())
82
+ assert united_value.value == v
83
+ assert united_value.unit == unit_constant_names_and_units[k.lower()]
84
+ else:
85
+ if type(v) is tuple:
86
+ v = list(v)
87
+ assert getattr(task.constants, k.lower()) == v
88
+
62
89
  assert task.constants.correct_for_polarization == True
63
90
  assert task.constants.pac_init_set == "OCCal_VIS"
91
+ assert task.constants.arm_id == str(expected_constant_dict["ARM_ID"])
@@ -1,4 +1,5 @@
1
1
  """Test integrity of workflows."""
2
+
2
3
  from dkist_processing_core.build_utils import validate_workflows
3
4
 
4
5
  from dkist_processing_visp import workflows
@@ -4,9 +4,9 @@ from astropy.io import fits
4
4
  from astropy.time import Time
5
5
  from dkist_fits_specifications import __version__ as spec_version
6
6
  from dkist_header_validator import spec214_validator
7
+ from dkist_processing_common.models.fits_access import MetadataKey
7
8
  from dkist_processing_common.models.tags import Tag
8
9
  from dkist_processing_common.models.wavelength import WavelengthRange
9
- from dkist_processing_common.tests.conftest import FakeGQLClient
10
10
  from dkist_spectral_lines import get_closest_spectral_line
11
11
  from dkist_spectral_lines import get_spectral_lines
12
12
 
@@ -40,6 +40,7 @@ def write_l1_task(
40
40
  NUM_RASTER_STEPS=2,
41
41
  SPECTRAL_LINE="VISP Ca II H",
42
42
  POLARIMETER_MODE=pol_mode,
43
+ NUM_MODSTATES=1 if pol_mode == "observe_intensity" else 10,
43
44
  )
44
45
  init_visp_constants_db(recipe_run_id, constants_db)
45
46
  with VispWriteL1Frame(
@@ -68,7 +69,13 @@ def mocked_get_wavelength_range(wavelength_range):
68
69
 
69
70
  @pytest.mark.parametrize("pol_mode", ["observe_polarimetric", "observe_intensity"])
70
71
  def test_write_l1_frame(
71
- write_l1_task, wcs_axis_names, pol_mode, wavelength_range, mocked_get_wavelength_range, mocker
72
+ write_l1_task,
73
+ wcs_axis_names,
74
+ pol_mode,
75
+ wavelength_range,
76
+ mocked_get_wavelength_range,
77
+ mocker,
78
+ fake_gql_client,
72
79
  ):
73
80
  """
74
81
  :Given: a write L1 task
@@ -76,7 +83,7 @@ def test_write_l1_frame(
76
83
  :Then: no errors are raised
77
84
  """
78
85
  mocker.patch(
79
- "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=FakeGQLClient
86
+ "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=fake_gql_client
80
87
  )
81
88
  mocker.patch(
82
89
  "dkist_processing_visp.tasks.write_l1.VispWriteL1Frame.get_wavelength_range",
@@ -167,3 +174,10 @@ def test_write_l1_frame(
167
174
  with pytest.raises(KeyError):
168
175
  # Make sure no more lines were added
169
176
  header[f"SPECLN{i+1:02}"]
177
+
178
+ if pol_mode == "observe_polarimetric":
179
+ assert header["CADENCE"] == 100
180
+ assert header[MetadataKey.fpa_exposure_time_ms] == 150
181
+ else:
182
+ assert header["CADENCE"] == 10
183
+ assert header[MetadataKey.fpa_exposure_time_ms] == 15