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
@@ -1,4 +1,5 @@
1
1
  """init."""
2
+
2
3
  from importlib.metadata import PackageNotFoundError
3
4
  from importlib.metadata import version
4
5
 
@@ -1,4 +1,5 @@
1
1
  """Configuration for the dkist-processing-visp package and the logging thereof."""
2
+
2
3
  from dkist_processing_common.config import DKISTProcessingCommonConfiguration
3
4
 
4
5
 
@@ -1,13 +1,16 @@
1
1
  """Visp additions to common constants."""
2
+
2
3
  from enum import Enum
3
4
 
4
- from dkist_processing_common.models.constants import BudName
5
+ import astropy.units as u
6
+ from astropy.units import Quantity
5
7
  from dkist_processing_common.models.constants import ConstantsBase
6
8
 
7
9
 
8
10
  class VispBudName(Enum):
9
11
  """Names to be used in Visp buds."""
10
12
 
13
+ arm_id = "ARM_ID"
11
14
  num_raster_steps = "NUM_RASTER_STEPS"
12
15
  polarimeter_mode = "POLARIMETER_MODE"
13
16
  wavelength = "WAVELENGTH"
@@ -22,6 +25,10 @@ class VispBudName(Enum):
22
25
  polcal_readout_exp_times = "POLCAL_READOUT_EXP_TIMES"
23
26
  non_dark_task_readout_exp_times = "NON_DARK_TASK_READOUT_EXP_TIMES"
24
27
  num_map_scans = "NUM_MAP_SCANS"
28
+ incident_light_angle_deg = "INCIDENT_LIGHT_ANGLE_DEG"
29
+ reflected_light_angle_deg = "REFLECTED_LIGHT_ANGLE_DEG"
30
+ grating_constant_inverse_mm = "GRATING_CONSTANT_INVERSE_MM"
31
+ solar_gain_ip_start_time = "SOLAR_GAIN_IP_START_TIME"
25
32
  axis_1_type = "AXIS_1_TYPE"
26
33
  axis_2_type = "AXIS_2_TYPE"
27
34
  axis_3_type = "AXIS_3_TYPE"
@@ -31,6 +38,16 @@ class VispBudName(Enum):
31
38
  class VispConstants(ConstantsBase):
32
39
  """Visp specific constants to add to the common constants."""
33
40
 
41
+ @property
42
+ def arm_id(self) -> str:
43
+ """
44
+ Return the current ViSP arm ID.
45
+
46
+ Arm IDs are ints in the headers, but we convert them to str here because that's what downstream machinery expects
47
+ the type to be.
48
+ """
49
+ return str(self._db_dict[VispBudName.arm_id])
50
+
34
51
  @property
35
52
  def wavelength(self) -> float:
36
53
  """Wavelength."""
@@ -41,11 +58,6 @@ class VispConstants(ConstantsBase):
41
58
  """Return the start time of the observe IP."""
42
59
  return self._db_dict[VispBudName.obs_ip_start_time.value]
43
60
 
44
- @property
45
- def num_modstates(self):
46
- """Find the number of modulation states."""
47
- return self._db_dict[BudName.num_modstates.value]
48
-
49
61
  @property
50
62
  def num_beams(self):
51
63
  """
@@ -55,11 +67,6 @@ class VispConstants(ConstantsBase):
55
67
  """
56
68
  return 2
57
69
 
58
- @property
59
- def num_cs_steps(self):
60
- """Find the number of calibration sequence steps."""
61
- return self._db_dict[BudName.num_cs_steps.value]
62
-
63
70
  @property
64
71
  def num_raster_steps(self):
65
72
  """Find the number of raster steps."""
@@ -78,7 +85,7 @@ class VispConstants(ConstantsBase):
78
85
  @property
79
86
  def pac_init_set(self):
80
87
  """Return the label for the initial set of parameter values used when fitting demodulation matrices."""
81
- retarder_name = self._db_dict[BudName.retarder_name.value]
88
+ retarder_name = self.retarder_name
82
89
  match retarder_name:
83
90
  case "SiO2 OC":
84
91
  return "OCCal_VIS"
@@ -86,17 +93,17 @@ class VispConstants(ConstantsBase):
86
93
  raise ValueError(f"No init set known for {retarder_name = }")
87
94
 
88
95
  @property
89
- def lamp_exposure_times(self) -> [float]:
96
+ def lamp_exposure_times(self) -> list[float]:
90
97
  """Find the lamp exposure time."""
91
98
  return self._db_dict[VispBudName.lamp_exposure_times.value]
92
99
 
93
100
  @property
94
- def solar_exposure_times(self) -> [float]:
101
+ def solar_exposure_times(self) -> list[float]:
95
102
  """Find the solar exposure time."""
96
103
  return self._db_dict[VispBudName.solar_exposure_times.value]
97
104
 
98
105
  @property
99
- def polcal_exposure_times(self) -> [float]:
106
+ def polcal_exposure_times(self) -> list[float]:
100
107
  """Find the polarization calibration exposure time."""
101
108
  if self.correct_for_polarization:
102
109
  return self._db_dict[VispBudName.polcal_exposure_times.value]
@@ -104,22 +111,22 @@ class VispConstants(ConstantsBase):
104
111
  return []
105
112
 
106
113
  @property
107
- def observe_exposure_times(self) -> [float]:
114
+ def observe_exposure_times(self) -> list[float]:
108
115
  """Find the observation exposure time."""
109
116
  return self._db_dict[VispBudName.observe_exposure_times.value]
110
117
 
111
118
  @property
112
- def lamp_readout_exp_times(self) -> [float]:
119
+ def lamp_readout_exp_times(self) -> list[float]:
113
120
  """Find the lamp readout exposure time."""
114
121
  return self._db_dict[VispBudName.lamp_readout_exp_times.value]
115
122
 
116
123
  @property
117
- def solar_readout_exp_times(self) -> [float]:
124
+ def solar_readout_exp_times(self) -> list[float]:
118
125
  """Find the solar readout exposure time."""
119
126
  return self._db_dict[VispBudName.solar_readout_exp_times.value]
120
127
 
121
128
  @property
122
- def polcal_readout_exp_times(self) -> [float]:
129
+ def polcal_readout_exp_times(self) -> list[float]:
123
130
  """Find the polarization calibration readout exposure time."""
124
131
  if self.correct_for_polarization:
125
132
  return self._db_dict[VispBudName.polcal_readout_exp_times.value]
@@ -127,7 +134,7 @@ class VispConstants(ConstantsBase):
127
134
  return []
128
135
 
129
136
  @property
130
- def non_dark_task_readout_exp_times(self) -> [float]:
137
+ def non_dark_task_readout_exp_times(self) -> list[float]:
131
138
  """
132
139
  Find all readout exposure times that *need* to exist in a dark IP.
133
140
 
@@ -137,10 +144,34 @@ class VispConstants(ConstantsBase):
137
144
  return self._db_dict[VispBudName.non_dark_task_readout_exp_times.value]
138
145
 
139
146
  @property
140
- def observe_readout_exp_times(self) -> [float]:
147
+ def observe_readout_exp_times(self) -> list[float]:
141
148
  """Find the observation readout exposure time."""
142
149
  return self._db_dict[VispBudName.observe_readout_exp_times.value]
143
150
 
151
+ @property
152
+ def incident_light_angle_deg(self) -> Quantity:
153
+ """Return the spectrograph incident light angle [deg]."""
154
+ return self._db_dict[VispBudName.incident_light_angle_deg] * u.deg
155
+
156
+ @property
157
+ def reflected_light_angle_deg(self) -> Quantity:
158
+ """
159
+ Return the spectrograph reflected light angle [deg].
160
+
161
+ This angle is the incident light angle plus the angular position of the ViSP arm.
162
+ """
163
+ return self._db_dict[VispBudName.reflected_light_angle_deg] * u.deg
164
+
165
+ @property
166
+ def grating_constant_inverse_mm(self) -> Quantity:
167
+ """Return the spectrograph grating constant [1/mm]."""
168
+ return self._db_dict[VispBudName.grating_constant_inverse_mm] / u.mm
169
+
170
+ @property
171
+ def solar_gain_ip_start_time(self) -> str:
172
+ """Return the start time of the SOLAR GAIN Instrument Program."""
173
+ return self._db_dict[VispBudName.solar_gain_ip_start_time]
174
+
144
175
  @property
145
176
  def axis_1_type(self) -> str:
146
177
  """Find the type of the first array axis."""
@@ -0,0 +1,20 @@
1
+ """ViSP control of FITS key names and values."""
2
+
3
+ from enum import StrEnum
4
+
5
+
6
+ class VispMetadataKey(StrEnum):
7
+ """Controlled list of names for FITS metadata header keys."""
8
+
9
+ arm_id = "VSPARMID"
10
+ number_of_modulator_states = "VSPNUMST"
11
+ raster_scan_step = "VSPSTP"
12
+ total_raster_steps = "VSPNSTP"
13
+ modulator_state = "VSPSTNUM"
14
+ polarimeter_mode = "VISP_006"
15
+ grating_angle_deg = "VSPGRTAN"
16
+ arm_position_deg = "VSPARMPS"
17
+ grating_constant_inverse_mm = "VSPGRTCN"
18
+ axis_1_type = "CTYPE1"
19
+ axis_2_type = "CTYPE2"
20
+ axis_3_type = "CTYPE3"
@@ -0,0 +1,10 @@
1
+ """Controlled list of quality metric codes."""
2
+
3
+ from enum import StrEnum
4
+
5
+
6
+ class VispMetricCode(StrEnum):
7
+ """Controlled list of quality metric codes."""
8
+
9
+ solar_first_vignette = "SOLAR_CAL_FIRST_VIGNETTE"
10
+ solar_final_vignette = "SOLAR_CAL_FINAL_VIGNETTE"
@@ -1,8 +1,14 @@
1
1
  """Visp calibration pipeline parameters."""
2
+
2
3
  from datetime import datetime
4
+ from random import randint
5
+ from typing import Any
3
6
 
7
+ import astropy.units as u
8
+ from dkist_processing_common.models.parameters import ParameterArmIdMixin
4
9
  from dkist_processing_common.models.parameters import ParameterBase
5
10
  from dkist_processing_common.models.parameters import ParameterWavelengthMixin
11
+ from solar_wavelength_calibration import DownloadConfig
6
12
 
7
13
 
8
14
  class VispParsingParameters(ParameterBase):
@@ -21,7 +27,7 @@ class VispParsingParameters(ParameterBase):
21
27
  )
22
28
 
23
29
 
24
- class VispParameters(ParameterBase, ParameterWavelengthMixin):
30
+ class VispParameters(ParameterBase, ParameterWavelengthMixin, ParameterArmIdMixin):
25
31
  """Put all Visp parameters parsed from the input dataset document in a single property."""
26
32
 
27
33
  @property
@@ -68,7 +74,7 @@ class VispParameters(ParameterBase, ParameterWavelengthMixin):
68
74
 
69
75
  @property
70
76
  def hairline_mask_spatial_smoothing_width_px(self) -> float:
71
- """Amount to smooth the hairling mask in the spatial direction.
77
+ """Amount to smooth the hairline mask in the spatial direction.
72
78
 
73
79
  This helps capture the higher-flux wings of the hairlines that would otherwise require a `hairline_fraction`
74
80
  that was so low it captures other optical features.
@@ -133,9 +139,34 @@ class VispParameters(ParameterBase, ParameterWavelengthMixin):
133
139
  return self._find_most_recent_past_value("visp_geo_poly_fit_order")
134
140
 
135
141
  @property
136
- def solar_spectral_avg_window(self):
142
+ def geo_zone_prominence(self):
143
+ """Relative peak prominence threshold used to identify strong spectral features."""
144
+ return self._find_parameter_closest_wavelength("visp_geo_zone_prominence")
145
+
146
+ @property
147
+ def geo_zone_width(self):
148
+ """Pixel width used to search for strong spectral features."""
149
+ return self._find_parameter_closest_wavelength("visp_geo_zone_width")
150
+
151
+ @property
152
+ def geo_zone_bg_order(self):
153
+ """Order of polynomial fit used to remove continuum when identifying strong spectral features."""
154
+ return self._find_parameter_closest_wavelength("visp_geo_zone_bg_order")
155
+
156
+ @property
157
+ def geo_zone_normalization_percentile(self):
158
+ """Fraction of CDF to use for normalizing spectrum when search for strong features."""
159
+ return self._find_parameter_closest_wavelength("visp_geo_zone_normalization_percentile")
160
+
161
+ @property
162
+ def geo_zone_rel_height(self):
163
+ """Relative height at which to compute the width of strong spectral features."""
164
+ return self._find_most_recent_past_value("visp_geo_zone_rel_height")
165
+
166
+ @property
167
+ def solar_spatial_median_filter_width_px(self):
137
168
  """Pixel width of spatial median filter used to compute characteristic solar spectra."""
138
- return self._find_parameter_closest_wavelength("visp_solar_spectral_avg_window")
169
+ return self._find_parameter_closest_wavelength("visp_solar_spatial_median_filter_width_px")
139
170
 
140
171
  @property
141
172
  def solar_characteristic_spatial_normalization_percentile(self) -> float:
@@ -145,29 +176,108 @@ class VispParameters(ParameterBase, ParameterWavelengthMixin):
145
176
  )
146
177
 
147
178
  @property
148
- def solar_zone_prominence(self):
149
- """Relative peak prominence threshold used to identify strong spectral features."""
150
- return self._find_parameter_closest_wavelength("visp_solar_zone_prominence")
179
+ def solar_vignette_initial_continuum_poly_fit_order(self) -> int:
180
+ """
181
+ Define the order of polynomial to use when fitting the initial continuum function.
182
+
183
+ Note that "initial" in this context does not refer to an initial guess in the wavecal fitter, but rather the
184
+ fact that this represents the initial estimate of the vignette signal.
185
+ """
186
+ return self._find_most_recent_past_value(
187
+ "visp_solar_vignette_initial_continuum_poly_fit_order"
188
+ )
151
189
 
152
190
  @property
153
- def solar_zone_width(self):
154
- """Pixel width used to search for strong spectral features."""
155
- return self._find_parameter_closest_wavelength("visp_solar_zone_width")
191
+ def solar_vignette_crval_bounds_px(self) -> float:
192
+ """
193
+ Define the bounds (in *pixels*) on crval when fitting the initial vignette signal.
194
+
195
+ The actual bounds on the value of crval are equal to ± the initial dispersion times this number. Note that the
196
+ total range searched by the fitting algorithm will be twice this number (in pixels).
197
+ """
198
+ return self._find_most_recent_past_value("visp_solar_vignette_crval_bounds_px") * u.pix
156
199
 
157
200
  @property
158
- def solar_zone_bg_order(self):
159
- """Order of polynomial fit used to remove continuum when identifying strong spectral features."""
160
- return self._find_parameter_closest_wavelength("visp_solar_zone_bg_order")
201
+ def solar_vignette_dispersion_bounds_fraction(self) -> float:
202
+ """
203
+ Define the ± fraction away from the initial value for bounds on dispersion when fitting the initial vignette signal.
204
+
205
+ This value should be between 0 and 1. For example, the minimum bound is `init_value * (1 - solar_vignette_dispersion_bounds_fraction)`.
206
+ """
207
+ return self._find_most_recent_past_value("visp_solar_vignette_dispersion_bounds_fraction")
161
208
 
162
209
  @property
163
- def solar_zone_normalization_percentile(self):
164
- """Fraction of CDF to use for normalzing spectrum when search for strong features."""
165
- return self._find_parameter_closest_wavelength("visp_solar_zone_normalization_percentile")
210
+ def solar_vignette_wavecal_fit_kwargs(self) -> dict[str, Any]:
211
+ """Define extra keyword arguments to pass to the wavelength calibration fitter."""
212
+ doc_dict = self._find_most_recent_past_value("visp_solar_vignette_wavecal_fit_kwargs")
213
+ rng_kwarg = dict()
214
+ fitting_method = doc_dict.get("method", False)
215
+ if fitting_method in ["basinhopping", "differential_evolution", "dual_annealing"]:
216
+ rng = randint(1, 1_000_000)
217
+ rng_kwarg["rng"] = rng
218
+
219
+ # The order here allows us to override `rng` in a parameter value
220
+ fit_kwargs = rng_kwarg | doc_dict
221
+ return fit_kwargs
166
222
 
167
223
  @property
168
- def solar_zone_rel_height(self):
169
- """Relative height at which to compute the width of strong spectral features."""
170
- return self._find_most_recent_past_value("visp_solar_zone_rel_height")
224
+ def solar_vignette_spectral_poly_fit_order(self) -> int:
225
+ """Define the order of spectral polynomial used when computing the full, 2D vignette signal."""
226
+ return self._find_most_recent_past_value("visp_solar_vignette_spectral_poly_fit_order")
227
+
228
+ @property
229
+ def solar_vignette_min_samples(self) -> float:
230
+ """Return fractional number of samples required for the RANSAC regressor used to fit the 2D vignette signal."""
231
+ return self._find_most_recent_past_value("visp_solar_vignette_min_samples")
232
+
233
+ @property
234
+ def wavecal_camera_lens_parameters(self) -> list[u.Quantity]:
235
+ r"""
236
+ Define the 2nd order polynomial coefficients for computing the total camera focal length as a function of wavelength.
237
+
238
+ The total focal length of the lens is :math:`f = a_0 + a_1\lambda + a_2\lambda^2` where this property is
239
+ :math:`[a_0, a_1, a_2]`
240
+ """
241
+ value_list = self._find_parameter_for_arm("visp_wavecal_camera_lens_parameters")
242
+ unit_list = [u.m, u.m / u.nm, u.m / u.nm**2]
243
+ return [v * u for v, u in zip(value_list, unit_list)]
244
+
245
+ @property
246
+ def wavecal_pixel_pitch_micron_per_pix(self) -> u.Quantity:
247
+ """Define the physical size of ViSP detector pixels."""
248
+ return (
249
+ self._find_most_recent_past_value("visp_wavecal_pixel_pitch_micron_per_pix")
250
+ * u.micron
251
+ / u.pix
252
+ )
253
+
254
+ @property
255
+ def wavecal_atlas_download_config(self) -> DownloadConfig:
256
+ """Define the `~solar_wavelength_calibration.DownloadConfig` used to grab the Solar atlas used for wavelength calibration."""
257
+ config_dict = self._find_most_recent_past_value("visp_wavecal_atlas_download_config")
258
+ return DownloadConfig.model_validate(config_dict)
259
+
260
+ @property
261
+ def wavecal_init_crval_guess_normalization_percentile(self) -> float | None:
262
+ """Define the CDF percentage used to normalize the Atlas to the input spectrum level when computing an initial CRVAL guess."""
263
+ return self._find_most_recent_past_value(
264
+ "visp_wavecal_init_crval_guess_normalization_percentile"
265
+ )
266
+
267
+ @property
268
+ def wavecal_init_resolving_power(self) -> int:
269
+ """Define the initial guess for ViSP resolving power in wavecal fits."""
270
+ return self._find_most_recent_past_value("visp_wavecal_init_resolving_power")
271
+
272
+ @property
273
+ def wavecal_init_straylight_fraction(self) -> float:
274
+ """Define the initial guess for straylight fraction in wavecal fits."""
275
+ return self._find_most_recent_past_value("visp_wavecal_init_straylight_fraction")
276
+
277
+ @property
278
+ def wavecal_init_opacity_factor(self) -> float:
279
+ """Define the initial guess for opacity factor in wavecal fits."""
280
+ return self._find_most_recent_past_value("visp_wavecal_init_opacity_factor")
171
281
 
172
282
  @property
173
283
  def polcal_spatial_median_filter_width_px(self) -> int:
@@ -1,4 +1,5 @@
1
1
  """ViSP tags."""
2
+
2
3
  from enum import Enum
3
4
 
4
5
  from dkist_processing_common.models.tags import Tag
@@ -1,4 +1,5 @@
1
1
  """Controlled list of visp-specific task tag names."""
2
+
2
3
  from enum import Enum
3
4
 
4
5
 
@@ -1,4 +1,5 @@
1
1
  """Stems for organizing files into separate map scans."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  from abc import ABC
@@ -1,4 +1,5 @@
1
1
  """ViSP modulator state parser."""
2
+
2
3
  from dkist_processing_common.models.constants import BudName
3
4
  from dkist_processing_common.models.flower_pot import Stem
4
5
  from dkist_processing_common.models.tags import StemName
@@ -1,8 +1,10 @@
1
1
  """ViSP polarimeter mode parser."""
2
+
2
3
  from dkist_processing_common.models.task_name import TaskName
3
4
  from dkist_processing_common.parsers.unique_bud import TaskUniqueBud
4
5
 
5
6
  from dkist_processing_visp.models.constants import VispBudName
7
+ from dkist_processing_visp.models.fits_access import VispMetadataKey
6
8
 
7
9
 
8
10
  class PolarimeterModeBud(TaskUniqueBud):
@@ -11,6 +13,6 @@ class PolarimeterModeBud(TaskUniqueBud):
11
13
  def __init__(self):
12
14
  super().__init__(
13
15
  constant_name=VispBudName.polarimeter_mode.value,
14
- metadata_key="polarimeter_mode",
16
+ metadata_key=VispMetadataKey.polarimeter_mode,
15
17
  ip_task_types=TaskName.observe.value,
16
18
  )
@@ -1,4 +1,5 @@
1
1
  """Copies of UniqueBud and SingleValueSingleKeyFlower from common that only activate if the frames are "observe" task."""
2
+
2
3
  from typing import Type
3
4
 
4
5
  from dkist_processing_common.models.flower_pot import SpilledDirt
@@ -9,6 +10,7 @@ from dkist_processing_common.parsers.single_value_single_key_flower import (
9
10
  )
10
11
 
11
12
  from dkist_processing_visp.models.constants import VispBudName
13
+ from dkist_processing_visp.models.fits_access import VispMetadataKey
12
14
  from dkist_processing_visp.models.tags import VispStemName
13
15
  from dkist_processing_visp.parsers.visp_l0_fits_access import VispL0FitsAccess
14
16
 
@@ -68,7 +70,8 @@ class RasterScanStepFlower(SingleValueSingleKeyFlower):
68
70
 
69
71
  def __init__(self):
70
72
  super().__init__(
71
- tag_stem_name=VispStemName.raster_step.value, metadata_key="raster_scan_step"
73
+ tag_stem_name=VispStemName.raster_step.value,
74
+ metadata_key=VispMetadataKey.raster_scan_step,
72
75
  )
73
76
 
74
77
  def setter(self, fits_obj: VispL0FitsAccess):
@@ -0,0 +1,75 @@
1
+ """Buds for parsing the incident and reflected light angles of the ViSP spectrograph."""
2
+
3
+ from dkist_processing_common.models.flower_pot import SpilledDirt
4
+ from dkist_processing_common.models.flower_pot import Stem
5
+ from dkist_processing_common.models.task_name import TaskName
6
+ from dkist_processing_common.parsers.task import parse_header_ip_task_with_gains
7
+ from dkist_processing_common.parsers.unique_bud import TaskUniqueBud
8
+
9
+ from dkist_processing_visp.models.constants import VispBudName
10
+ from dkist_processing_visp.models.fits_access import VispMetadataKey
11
+ from dkist_processing_visp.parsers.visp_l0_fits_access import VispL0FitsAccess
12
+
13
+
14
+ def convert_grating_angle_to_incident_light_angle(grating_angle: float) -> float:
15
+ """Convert the raw header "grating angle" to the incident light angle expected by the solar wavecal library."""
16
+ return -1 * grating_angle
17
+
18
+
19
+ class IncidentLightAngleBud(TaskUniqueBud):
20
+ """Special case of `TaskUniqueBud` so we can apply the sign shift to the header incident light angle values."""
21
+
22
+ def __init__(self):
23
+ super().__init__(
24
+ constant_name=VispBudName.incident_light_angle_deg.value,
25
+ metadata_key=VispMetadataKey.grating_angle_deg,
26
+ ip_task_types=[TaskName.observe.value, TaskName.solar_gain.value],
27
+ task_type_parsing_function=parse_header_ip_task_with_gains,
28
+ )
29
+
30
+ def setter(self, fits_obj: VispL0FitsAccess) -> float | type[SpilledDirt]:
31
+ """Apply a sign flip to the raw header value for incident light angle."""
32
+ grating_angle = super().setter(fits_obj)
33
+
34
+ if grating_angle is SpilledDirt:
35
+ return grating_angle
36
+
37
+ return convert_grating_angle_to_incident_light_angle(grating_angle)
38
+
39
+
40
+ class ReflectedLightAngleBud(Stem):
41
+ """Bud that combines the incident light angle and arm position header values to compute the reflected light angle."""
42
+
43
+ key_to_petal_dict: dict[str, float]
44
+
45
+ def __init__(self):
46
+ super().__init__(stem_name=VispBudName.reflected_light_angle_deg.value)
47
+ self.ip_task_types = [
48
+ task.casefold() for task in [TaskName.observe.value, TaskName.solar_gain.value]
49
+ ]
50
+
51
+ def setter(self, fits_obj: VispL0FitsAccess) -> float | type[SpilledDirt]:
52
+ """
53
+ Compute the reflected light angle.
54
+
55
+ The reflected light angle is `-1 * fits_objs.grating_angle_deg + fits_obj.arm_position_deg`.
56
+ """
57
+ task = parse_header_ip_task_with_gains(fits_obj)
58
+
59
+ if task.casefold() in self.ip_task_types:
60
+ incident_light_angle = convert_grating_angle_to_incident_light_angle(
61
+ fits_obj.grating_angle_deg
62
+ )
63
+ arm_position = fits_obj.arm_position_deg
64
+ return incident_light_angle + arm_position
65
+
66
+ return SpilledDirt
67
+
68
+ def getter(self, key: str) -> float:
69
+ """Get the value for the reflected light angle and raise an error if it is not unique."""
70
+ value_set = set(self.key_to_petal_dict.values())
71
+ if len(value_set) > 1:
72
+ raise ValueError(
73
+ f"Multiple {self.stem_name} values found for key {key}. Values: {value_set}"
74
+ )
75
+ return value_set.pop()
@@ -1,7 +1,9 @@
1
1
  """Stems for parsing constants and tags related to time header keys."""
2
- from collections import namedtuple
2
+
3
3
  from pathlib import Path
4
+ from typing import NamedTuple
4
5
 
6
+ from dkist_processing_common.models.fits_access import MetadataKey
5
7
  from dkist_processing_common.models.flower_pot import SpilledDirt
6
8
  from dkist_processing_common.models.flower_pot import Stem
7
9
  from dkist_processing_common.models.flower_pot import Thorn
@@ -17,7 +19,7 @@ class NonDarkNonPolcalTaskReadoutExpTimesBud(Stem):
17
19
 
18
20
  def __init__(self):
19
21
  super().__init__(stem_name=VispBudName.non_dark_task_readout_exp_times.value)
20
- self.metadata_key = "sensor_readout_exposure_time_ms"
22
+ self.metadata_key = MetadataKey.sensor_readout_exposure_time_ms.name
21
23
 
22
24
  def setter(self, fits_obj: VispL0FitsAccess) -> float | SpilledDirt:
23
25
  """
@@ -56,17 +58,23 @@ class NonDarkNonPolcalTaskReadoutExpTimesBud(Stem):
56
58
  return exposure_times
57
59
 
58
60
 
61
+ class ReadoutExposureTimeContainer(NamedTuple):
62
+ """Named tuple to hold whether the task is dark and/or polcal along with the associated exposure time."""
63
+
64
+ is_dark: bool
65
+ is_polcal: bool
66
+ readout_exposure_time: float
67
+
68
+
59
69
  class DarkReadoutExpTimePickyBud(Stem):
60
70
  """Parse exposure times to ensure existence of the necessary DARK exposure times."""
61
71
 
62
- ReadoutExposureTime = namedtuple(
63
- "ReadoutExposureTime", ["is_dark", "is_polcal", "readout_exposure_time"]
64
- )
65
- key_to_petal_dict: dict[str | Path, ReadoutExposureTime] # For type hinting
72
+ ReadoutExposureTime: ReadoutExposureTimeContainer = ReadoutExposureTimeContainer
73
+ key_to_petal_dict: dict[str | Path, ReadoutExposureTimeContainer] # For type hinting
66
74
 
67
75
  def __init__(self):
68
76
  super().__init__(stem_name=VispBudName.dark_readout_exp_time_picky_bud.value)
69
- self.metadata_key = "sensor_readout_exposure_time_ms"
77
+ self.metadata_key = MetadataKey.sensor_readout_exposure_time_ms.name
70
78
 
71
79
  def setter(self, fits_obj: VispL0FitsAccess) -> tuple:
72
80
  """
@@ -1,7 +1,10 @@
1
1
  """ViSP FITS access for L0 data."""
2
+
2
3
  from astropy.io import fits
3
4
  from dkist_processing_common.parsers.l0_fits_access import L0FitsAccess
4
5
 
6
+ from dkist_processing_visp.models.fits_access import VispMetadataKey
7
+
5
8
 
6
9
  class VispL0FitsAccess(L0FitsAccess):
7
10
  """
@@ -29,11 +32,19 @@ class VispL0FitsAccess(L0FitsAccess):
29
32
  ):
30
33
  super().__init__(hdu=hdu, name=name, auto_squeeze=auto_squeeze)
31
34
 
32
- self.number_of_modulator_states: int = self.header["VSPNUMST"]
33
- self.raster_scan_step: int = self.header["VSPSTP"]
34
- self.total_raster_steps: int = self.header["VSPNSTP"]
35
- self.modulator_state: int = self.header["VSPSTNUM"]
36
- self.polarimeter_mode: str = self.header["VISP_006"]
37
- self.axis_1_type: str = self.header["CTYPE1"]
38
- self.axis_2_type: str = self.header["CTYPE2"]
39
- self.axis_3_type: str = self.header["CTYPE3"]
35
+ self.arm_id: int = self.header[VispMetadataKey.arm_id]
36
+ self.number_of_modulator_states: int = self.header[
37
+ VispMetadataKey.number_of_modulator_states
38
+ ]
39
+ self.raster_scan_step: int = self.header[VispMetadataKey.raster_scan_step]
40
+ self.total_raster_steps: int = self.header[VispMetadataKey.total_raster_steps]
41
+ self.modulator_state: int = self.header[VispMetadataKey.modulator_state]
42
+ self.polarimeter_mode: str = self.header[VispMetadataKey.polarimeter_mode]
43
+ self.grating_angle_deg: float = self.header[VispMetadataKey.grating_angle_deg]
44
+ self.arm_position_deg: float = self.header[VispMetadataKey.arm_position_deg]
45
+ self.grating_constant_inverse_mm: float = self.header[
46
+ VispMetadataKey.grating_constant_inverse_mm
47
+ ]
48
+ self.axis_1_type: str = self.header[VispMetadataKey.axis_1_type]
49
+ self.axis_2_type: str = self.header[VispMetadataKey.axis_2_type]
50
+ self.axis_3_type: str = self.header[VispMetadataKey.axis_3_type]
@@ -1,4 +1,5 @@
1
1
  """ViSP FITS access for L1 data."""
2
+
2
3
  from astropy.io import fits
3
4
  from dkist_processing_common.parsers.l1_fits_access import L1FitsAccess
4
5
 
@@ -1,4 +1,5 @@
1
1
  """init."""
2
+
2
3
  from dkist_processing_visp.tasks.assemble_movie import *
3
4
  from dkist_processing_visp.tasks.background_light import *
4
5
  from dkist_processing_visp.tasks.dark import *