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.
- dkist_processing_visp/__init__.py +1 -0
- dkist_processing_visp/config.py +1 -0
- dkist_processing_visp/models/constants.py +52 -21
- dkist_processing_visp/models/fits_access.py +20 -0
- dkist_processing_visp/models/metric_code.py +10 -0
- dkist_processing_visp/models/parameters.py +129 -19
- dkist_processing_visp/models/tags.py +1 -0
- dkist_processing_visp/models/task_name.py +1 -0
- dkist_processing_visp/parsers/map_repeats.py +1 -0
- dkist_processing_visp/parsers/modulator_states.py +1 -0
- dkist_processing_visp/parsers/polarimeter_mode.py +3 -1
- dkist_processing_visp/parsers/raster_step.py +4 -1
- dkist_processing_visp/parsers/spectrograph_configuration.py +75 -0
- dkist_processing_visp/parsers/time.py +15 -7
- dkist_processing_visp/parsers/visp_l0_fits_access.py +19 -8
- dkist_processing_visp/parsers/visp_l1_fits_access.py +1 -0
- dkist_processing_visp/tasks/__init__.py +1 -0
- dkist_processing_visp/tasks/assemble_movie.py +1 -0
- dkist_processing_visp/tasks/background_light.py +2 -1
- dkist_processing_visp/tasks/dark.py +5 -4
- dkist_processing_visp/tasks/geometric.py +132 -20
- dkist_processing_visp/tasks/instrument_polarization.py +13 -12
- dkist_processing_visp/tasks/l1_output_data.py +203 -0
- dkist_processing_visp/tasks/lamp.py +53 -93
- dkist_processing_visp/tasks/make_movie_frames.py +8 -6
- dkist_processing_visp/tasks/mixin/beam_access.py +1 -0
- dkist_processing_visp/tasks/mixin/corrections.py +54 -4
- dkist_processing_visp/tasks/mixin/downsample.py +1 -0
- dkist_processing_visp/tasks/parse.py +34 -4
- dkist_processing_visp/tasks/quality_metrics.py +5 -4
- dkist_processing_visp/tasks/science.py +126 -46
- dkist_processing_visp/tasks/solar.py +896 -456
- dkist_processing_visp/tasks/visp_base.py +2 -0
- dkist_processing_visp/tasks/write_l1.py +25 -5
- dkist_processing_visp/tests/conftest.py +99 -35
- dkist_processing_visp/tests/header_models.py +92 -20
- dkist_processing_visp/tests/local_trial_workflows/l0_cals_only.py +4 -23
- dkist_processing_visp/tests/local_trial_workflows/l0_polcals_as_science.py +421 -0
- dkist_processing_visp/tests/local_trial_workflows/l0_solar_gain_as_science.py +10 -29
- dkist_processing_visp/tests/local_trial_workflows/l0_to_l1.py +1 -21
- dkist_processing_visp/tests/local_trial_workflows/local_trial_helpers.py +98 -14
- dkist_processing_visp/tests/test_assemble_movie.py +2 -3
- dkist_processing_visp/tests/test_assemble_quality.py +89 -4
- dkist_processing_visp/tests/test_background_light.py +8 -5
- dkist_processing_visp/tests/test_dark.py +4 -3
- dkist_processing_visp/tests/test_fits_access.py +43 -0
- dkist_processing_visp/tests/test_geometric.py +45 -4
- dkist_processing_visp/tests/test_instrument_polarization.py +4 -3
- dkist_processing_visp/tests/test_lamp.py +22 -26
- dkist_processing_visp/tests/test_make_movie_frames.py +4 -4
- dkist_processing_visp/tests/test_map_repeats.py +3 -1
- dkist_processing_visp/tests/test_parameters.py +122 -21
- dkist_processing_visp/tests/test_parse.py +98 -14
- dkist_processing_visp/tests/test_quality.py +2 -3
- dkist_processing_visp/tests/test_science.py +113 -15
- dkist_processing_visp/tests/test_solar.py +318 -99
- dkist_processing_visp/tests/test_visp_constants.py +36 -8
- dkist_processing_visp/tests/test_workflows.py +1 -0
- dkist_processing_visp/tests/test_write_l1.py +17 -3
- dkist_processing_visp/workflows/__init__.py +1 -0
- dkist_processing_visp/workflows/l0_processing.py +8 -2
- dkist_processing_visp/workflows/trial_workflows.py +8 -2
- dkist_processing_visp-5.1.1.dist-info/METADATA +552 -0
- dkist_processing_visp-5.1.1.dist-info/RECORD +94 -0
- docs/conf.py +5 -1
- docs/gain_correction.rst +50 -42
- dkist_processing_visp/tasks/mixin/line_zones.py +0 -115
- dkist_processing_visp-3.3.0.dist-info/METADATA +0 -459
- dkist_processing_visp-3.3.0.dist-info/RECORD +0 -90
- {dkist_processing_visp-3.3.0.dist-info → dkist_processing_visp-5.1.1.dist-info}/WHEEL +0 -0
- {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
|
|
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.
|
|
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
|
|
26
|
-
return 10 * beam
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
beam=beam,
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
226
|
+
lamp_signal = lamp_signal_func(beam)
|
|
63
227
|
raw_beam = (true_solar_gain * lamp_signal) + dark_signal
|
|
64
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
[
|
|
314
|
+
[
|
|
315
|
+
pytest.param(True, id="background_on"),
|
|
316
|
+
pytest.param(False, id="background_off"),
|
|
317
|
+
],
|
|
130
318
|
)
|
|
131
|
-
def test_solar_gain_task(
|
|
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=
|
|
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 = (
|
|
151
|
-
intermediate_shape = (
|
|
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,
|
|
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,
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
|
446
|
+
def test_continuum_wavecal_parameters():
|
|
255
447
|
"""
|
|
256
|
-
Given: A
|
|
257
|
-
When:
|
|
258
|
-
Then: The
|
|
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
|
-
|
|
261
|
-
|
|
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
|
-
|
|
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
|
-
|
|
266
|
-
assert
|
|
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(
|
|
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
|
|
58
|
-
|
|
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
|
-
|
|
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"])
|
|
@@ -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,
|
|
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=
|
|
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
|