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
@@ -6,13 +6,12 @@ import pytest
6
6
  from astropy.io import fits
7
7
  from dkist_processing_common._util.scratch import WorkflowFileSystem
8
8
  from dkist_processing_common.models.tags import Tag
9
- from dkist_processing_common.tests.conftest import FakeGQLClient
10
9
 
11
10
  from dkist_processing_visp.models.tags import VispTag
12
11
  from dkist_processing_visp.tasks.lamp import LampCalibration
13
- from dkist_processing_visp.tests.conftest import tag_on_modstate
14
12
  from dkist_processing_visp.tests.conftest import VispConstantsDb
15
13
  from dkist_processing_visp.tests.conftest import VispInputDatasetParameterValues
14
+ from dkist_processing_visp.tests.conftest import tag_on_modstate
16
15
  from dkist_processing_visp.tests.conftest import write_frames_to_task
17
16
  from dkist_processing_visp.tests.conftest import write_intermediate_darks_to_task
18
17
  from dkist_processing_visp.tests.header_models import VispHeadersInputLampGainFrames
@@ -24,9 +23,10 @@ def make_lamp_array_data(
24
23
  frame: VispHeadersInputLampGainFrames, dark_signal: float, beam_border: int
25
24
  ):
26
25
  num_raw_frames_per_fpa = frame.header()["CAM__014"]
26
+ modstate = frame.current_modstate("") # Weird signature due to @key_function
27
27
  data = np.zeros(frame.array_shape)
28
- data[0, :beam_border, :] = (1.1 + dark_signal) * num_raw_frames_per_fpa
29
- 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
30
30
 
31
31
  return data
32
32
 
@@ -91,14 +91,16 @@ def lamp_calibration_task(
91
91
  task._purge()
92
92
 
93
93
 
94
- def test_lamp_calibration_task(lamp_calibration_task, assign_input_dataset_doc_to_task, mocker):
94
+ def test_lamp_calibration_task(
95
+ lamp_calibration_task, assign_input_dataset_doc_to_task, mocker, fake_gql_client
96
+ ):
95
97
  """
96
98
  Given: A LampCalibration task
97
99
  When: Calling the task instance
98
100
  Then: The correct number of output lamp gain frames exists, and are tagged correctly
99
101
  """
100
102
  mocker.patch(
101
- "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=FakeGQLClient
103
+ "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=fake_gql_client
102
104
  )
103
105
  # Given
104
106
  task, num_modstates, readout_exp_time = lamp_calibration_task
@@ -131,27 +133,21 @@ def test_lamp_calibration_task(lamp_calibration_task, assign_input_dataset_doc_t
131
133
  VispTag.task_lamp_gain(),
132
134
  VispTag.intermediate(),
133
135
  ]
134
- assert len(list(task.read(tags=tags))) == num_modstates * 2 # 2 from beams
135
-
136
- for i in range(num_modstates):
137
- for beam in [1, 2]:
138
- tags = [
139
- VispTag.task_lamp_gain(),
140
- VispTag.intermediate(),
141
- VispTag.modstate(i + 1),
142
- VispTag.beam(beam),
143
- ]
144
- files = list(task.read(tags=tags))
145
- assert len(files) == 1
146
- hdu = fits.open(files[0])[0]
147
- 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
148
137
 
149
- tags = [
150
- VispTag.task_lamp_gain(),
151
- VispTag.intermediate(),
152
- ]
153
- for filepath in task.read(tags=tags):
154
- 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)
155
151
 
156
152
  quality_files = task.read(tags=[Tag.quality("TASK_TYPES")])
157
153
  for file in quality_files:
@@ -1,7 +1,7 @@
1
1
  import pytest
2
2
  from astropy.io import fits
3
3
  from dkist_processing_common._util.scratch import WorkflowFileSystem
4
- from dkist_processing_common.tests.conftest import FakeGQLClient
4
+ from dkist_processing_common.models.fits_access import MetadataKey
5
5
 
6
6
  from dkist_processing_visp.models.tags import VispTag
7
7
  from dkist_processing_visp.tasks.make_movie_frames import MakeVispMovieFrames
@@ -30,14 +30,14 @@ def movie_frames_task(tmp_path, recipe_run_id, init_visp_constants_db):
30
30
 
31
31
 
32
32
  @pytest.mark.parametrize("pol_mode", ["observe_polarimetric", "observe_intensity"])
33
- def test_make_movie_frames(movie_frames_task, pol_mode, mocker):
33
+ def test_make_movie_frames(movie_frames_task, pol_mode, mocker, fake_gql_client):
34
34
  """
35
35
  Given: A MakeVispMovieFrames task
36
36
  When: Calling the task instance
37
37
  Then: a fits file is made for each raster scan containing the movie frame for that scan
38
38
  """
39
39
  mocker.patch(
40
- "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=FakeGQLClient
40
+ "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=fake_gql_client
41
41
  )
42
42
  task, num_maps, num_steps = movie_frames_task
43
43
  spectral_size = 3
@@ -57,5 +57,5 @@ def test_make_movie_frames(movie_frames_task, pol_mode, mocker):
57
57
  for filepath in task.read(tags=[VispTag.movie_frame()]):
58
58
  assert filepath.exists()
59
59
  hdul = fits.open(filepath)
60
- assert hdul[0].header["INSTRUME"] == "VISP"
60
+ assert hdul[0].header[MetadataKey.instrument] == "VISP"
61
61
  assert hdul[0].data.shape == expected_movie_fram_shape
@@ -14,6 +14,7 @@ from dkist_processing_common.parsers.single_value_single_key_flower import (
14
14
  )
15
15
 
16
16
  from dkist_processing_visp.models.constants import VispBudName
17
+ from dkist_processing_visp.models.fits_access import VispMetadataKey
17
18
  from dkist_processing_visp.models.tags import VispStemName
18
19
  from dkist_processing_visp.models.tags import VispTag
19
20
  from dkist_processing_visp.parsers.map_repeats import MapScanFlower
@@ -90,7 +91,8 @@ class ParseTaskJustMapStuff(ParseL0VispInputData):
90
91
  MapScanFlower(),
91
92
  RasterScanStepFlower(),
92
93
  SingleValueSingleKeyFlower(
93
- tag_stem_name=VispStemName.modstate.value, metadata_key="modulator_state"
94
+ tag_stem_name=VispStemName.modstate.value,
95
+ metadata_key=VispMetadataKey.modulator_state,
94
96
  ),
95
97
  ]
96
98
 
@@ -1,13 +1,15 @@
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
7
+ from hypothesis import HealthCheck
6
8
  from hypothesis import example
7
9
  from hypothesis import given
8
- from hypothesis import HealthCheck
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,
@@ -34,8 +54,7 @@ def basic_science_task_with_parameter_mixin(
34
54
  obs_ip_start_time=testing_obs_ip_start_time,
35
55
  ):
36
56
  class Task(VispTaskBase):
37
- def run(self):
38
- ...
57
+ def run(self): ...
39
58
 
40
59
  init_visp_constants_db(recipe_run_id, VispConstantsDb())
41
60
  task = Task(
@@ -49,8 +68,9 @@ def basic_science_task_with_parameter_mixin(
49
68
  parameters_part,
50
69
  parameter_class=parameter_class,
51
70
  obs_ip_start_time=obs_ip_start_time,
71
+ arm_id=arm_id,
52
72
  )
53
- yield task, parameters_part
73
+ yield task, asdict(parameters_part)
54
74
  except:
55
75
  raise
56
76
  finally:
@@ -59,7 +79,14 @@ def basic_science_task_with_parameter_mixin(
59
79
  return make_task
60
80
 
61
81
 
62
- 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
+ ):
63
90
  """
64
91
  Given: A Science task with the parameter mixin
65
92
  When: Accessing properties for parameters that do not depend on wavelength
@@ -69,15 +96,41 @@ def test_non_wave_parameters(basic_science_task_with_parameter_mixin):
69
96
  basic_science_task_with_parameter_mixin(VispInputDatasetParameterValues())
70
97
  )
71
98
  task_param_attr = task.parameters
72
- for pn, pv in asdict(expected).items():
73
- 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
74
106
  if (
75
- type(pv) is not dict and property_name not in PARSE_PARAMETER_NAMES
76
- ): # Don't test wavelength dependent parameters
77
- 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)
78
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]
79
121
 
80
- def test_parse_parameters(basic_science_task_with_parameter_mixin):
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
130
+
131
+
132
+ @pytest.mark.parametrize("arm_id", ["1"])
133
+ def test_parse_parameters(basic_science_task_with_parameter_mixin, parse_parameter_names):
81
134
  """
82
135
  Given: A Science task with Parsing parameters
83
136
  When: Accessing properties for Parse parameters
@@ -91,16 +144,19 @@ def test_parse_parameters(basic_science_task_with_parameter_mixin):
91
144
  )
92
145
  )
93
146
  task_param_attr = task.parameters
94
- for pn, pv in asdict(expected).items():
147
+ for pn, pv in expected.items():
95
148
  property_name = pn.removeprefix("visp_")
96
- 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:
97
150
  assert getattr(task_param_attr, property_name) == pv
98
151
 
99
152
 
100
153
  @given(wave=st.floats(min_value=500.0, max_value=2000.0))
101
154
  @settings(suppress_health_check=[HealthCheck.function_scoped_fixture])
102
155
  @example(wave=492.5)
103
- 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
+ ):
104
160
  """
105
161
  Given: A Science task with the paramter mixin
106
162
  When: Accessing properties for parameters that depend on wavelength
@@ -111,10 +167,55 @@ def test_wave_parameters(basic_science_task_with_parameter_mixin, wave):
111
167
  )
112
168
  task_param_attr = task.parameters
113
169
  task_param_attr._wavelength = wave
114
- pwaves = np.array(expected.visp_solar_zone_normalization_percentile.wavelength)
170
+ pwaves = np.array(expected["visp_geo_zone_normalization_percentile"]["wavelength"])
115
171
  midpoints = 0.5 * (pwaves[1:] + pwaves[:-1])
116
172
  idx = np.sum(midpoints < wave)
117
- for pn, pv in asdict(expected).items():
173
+ for pn, pv in expected.items():
118
174
  property_name = pn.removeprefix("visp_")
119
- 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:
120
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]