dkist-processing-visp 5.1.2rc1__py3-none-any.whl → 5.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dkist_processing_visp/models/parameters.py +43 -1
- dkist_processing_visp/models/tags.py +11 -0
- dkist_processing_visp/models/task_name.py +2 -0
- dkist_processing_visp/tasks/geometric.py +1 -1
- dkist_processing_visp/tasks/science.py +88 -11
- dkist_processing_visp/tasks/solar.py +12 -205
- dkist_processing_visp/tasks/wavelength_calibration.py +430 -0
- dkist_processing_visp/tasks/write_l1.py +2 -0
- dkist_processing_visp/tests/conftest.py +11 -0
- dkist_processing_visp/tests/header_models.py +22 -6
- dkist_processing_visp/tests/local_trial_workflows/l0_cals_only.py +21 -0
- dkist_processing_visp/tests/local_trial_workflows/l0_polcals_as_science.py +21 -0
- dkist_processing_visp/tests/local_trial_workflows/l0_solar_gain_as_science.py +20 -0
- dkist_processing_visp/tests/local_trial_workflows/l0_to_l1.py +21 -0
- dkist_processing_visp/tests/local_trial_workflows/local_trial_helpers.py +27 -0
- dkist_processing_visp/tests/test_parameters.py +11 -5
- dkist_processing_visp/tests/test_science.py +60 -5
- dkist_processing_visp/tests/test_solar.py +0 -1
- dkist_processing_visp/tests/test_wavelength_calibration.py +297 -0
- dkist_processing_visp/tests/test_write_l1.py +0 -2
- dkist_processing_visp/workflows/l0_processing.py +4 -1
- dkist_processing_visp/workflows/trial_workflows.py +7 -2
- {dkist_processing_visp-5.1.2rc1.dist-info → dkist_processing_visp-5.2.0.dist-info}/METADATA +37 -37
- {dkist_processing_visp-5.1.2rc1.dist-info → dkist_processing_visp-5.2.0.dist-info}/RECORD +29 -27
- docs/gain_correction.rst +3 -0
- docs/index.rst +1 -0
- docs/wavelength_calibration.rst +64 -0
- changelog/251.feature.rst +0 -1
- {dkist_processing_visp-5.1.2rc1.dist-info → dkist_processing_visp-5.2.0.dist-info}/WHEEL +0 -0
- {dkist_processing_visp-5.1.2rc1.dist-info → dkist_processing_visp-5.2.0.dist-info}/top_level.txt +0 -0
|
@@ -5,6 +5,7 @@ from random import randint
|
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
7
|
import astropy.units as u
|
|
8
|
+
from astropy.units import Quantity
|
|
8
9
|
from dkist_processing_common.models.parameters import ParameterArmIdMixin
|
|
9
10
|
from dkist_processing_common.models.parameters import ParameterBase
|
|
10
11
|
from dkist_processing_common.models.parameters import ParameterWavelengthMixin
|
|
@@ -200,7 +201,7 @@ class VispParameters(ParameterBase, ParameterWavelengthMixin, ParameterArmIdMixi
|
|
|
200
201
|
@property
|
|
201
202
|
def solar_vignette_dispersion_bounds_fraction(self) -> float:
|
|
202
203
|
"""
|
|
203
|
-
Define the ± fraction
|
|
204
|
+
Define the ± fraction from the initial value for bounds on dispersion when fitting the initial vignette signal.
|
|
204
205
|
|
|
205
206
|
This value should be between 0 and 1. For example, the minimum bound is `init_value * (1 - solar_vignette_dispersion_bounds_fraction)`.
|
|
206
207
|
"""
|
|
@@ -230,6 +231,33 @@ class VispParameters(ParameterBase, ParameterWavelengthMixin, ParameterArmIdMixi
|
|
|
230
231
|
"""Return fractional number of samples required for the RANSAC regressor used to fit the 2D vignette signal."""
|
|
231
232
|
return self._find_most_recent_past_value("visp_solar_vignette_min_samples")
|
|
232
233
|
|
|
234
|
+
@property
|
|
235
|
+
def wavecal_crval_bounds_px(self) -> Quantity:
|
|
236
|
+
"""
|
|
237
|
+
Define the bounds (in *pix*) on crval when performing wavecal fitting.
|
|
238
|
+
|
|
239
|
+
The actual bounds on the value of crval are equal to ± the initial dispersion times this number. Note that the
|
|
240
|
+
total range searched by the fitting algorithm will be twice this number (in pixels).
|
|
241
|
+
"""
|
|
242
|
+
return self._find_most_recent_past_value("visp_wavecal_crval_bounds_px") * u.pix
|
|
243
|
+
|
|
244
|
+
@property
|
|
245
|
+
def wavecal_dispersion_bounds_fraction(self) -> Quantity:
|
|
246
|
+
"""
|
|
247
|
+
Define the ± fraction from the initial value for bounds on dispersion when performing wavecal fitting.
|
|
248
|
+
|
|
249
|
+
This value should be between 0 and 1. For example, the minimum bound is `init_value * (1 - wavecal_dispersion_bounds_fraction)`.
|
|
250
|
+
"""
|
|
251
|
+
return self._find_most_recent_past_value("visp_wavecal_dispersion_bounds_fraction")
|
|
252
|
+
|
|
253
|
+
@property
|
|
254
|
+
def wavecal_incident_light_angle_bounds_deg(self) -> Quantity:
|
|
255
|
+
"""Define the bounds (in *deg*) on incident_light_angle when performing wavecal fitting."""
|
|
256
|
+
return (
|
|
257
|
+
self._find_most_recent_past_value("visp_wavecal_incident_light_angle_bounds_deg")
|
|
258
|
+
* u.deg
|
|
259
|
+
)
|
|
260
|
+
|
|
233
261
|
@property
|
|
234
262
|
def wavecal_camera_lens_parameters(self) -> list[u.Quantity]:
|
|
235
263
|
r"""
|
|
@@ -279,6 +307,20 @@ class VispParameters(ParameterBase, ParameterWavelengthMixin, ParameterArmIdMixi
|
|
|
279
307
|
"""Define the initial guess for opacity factor in wavecal fits."""
|
|
280
308
|
return self._find_most_recent_past_value("visp_wavecal_init_opacity_factor")
|
|
281
309
|
|
|
310
|
+
@property
|
|
311
|
+
def wavecal_fit_kwargs(self) -> dict[str, Any]:
|
|
312
|
+
"""Define extra keyword arguments to pass to the wavelength calibration fitter."""
|
|
313
|
+
doc_dict = self._find_most_recent_past_value("visp_wavecal_fit_kwargs")
|
|
314
|
+
rng_kwarg = dict()
|
|
315
|
+
fitting_method = doc_dict.get("method", False)
|
|
316
|
+
if fitting_method in ["basinhopping", "differential_evolution", "dual_annealing"]:
|
|
317
|
+
rng = randint(1, 1_000_000)
|
|
318
|
+
rng_kwarg["rng"] = rng
|
|
319
|
+
|
|
320
|
+
# The order here allows us to override `rng` in a parameter value
|
|
321
|
+
fit_kwargs = rng_kwarg | doc_dict
|
|
322
|
+
return fit_kwargs
|
|
323
|
+
|
|
282
324
|
@property
|
|
283
325
|
def polcal_spatial_median_filter_width_px(self) -> int:
|
|
284
326
|
"""Return the size of the median filter to apply in the spatial dimension to polcal data."""
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from enum import Enum
|
|
4
4
|
|
|
5
|
+
from dkist_processing_common.models.tags import StemName
|
|
5
6
|
from dkist_processing_common.models.tags import Tag
|
|
6
7
|
|
|
7
8
|
from dkist_processing_visp.models.task_name import VispTaskName
|
|
@@ -63,6 +64,16 @@ class VispTag(Tag):
|
|
|
63
64
|
"""
|
|
64
65
|
return cls.format_tag(VispStemName.map_scan, map_scan_num)
|
|
65
66
|
|
|
67
|
+
@classmethod
|
|
68
|
+
def task_characteristic_spectra(cls) -> str:
|
|
69
|
+
"""Tags intermediate characteristic spectra."""
|
|
70
|
+
return cls.format_tag(StemName.task, VispTaskName.solar_char_spec.value)
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def task_wavelength_calibration(cls) -> str:
|
|
74
|
+
"""Tags wavelength calibration."""
|
|
75
|
+
return cls.format_tag(StemName.task, VispTaskName.wavelength_calibration.value)
|
|
76
|
+
|
|
66
77
|
##################
|
|
67
78
|
# Composite tags #
|
|
68
79
|
##################
|
|
@@ -1086,7 +1086,7 @@ class GeometricCalibration(
|
|
|
1086
1086
|
"rel_height": self.parameters.geo_zone_rel_height,
|
|
1087
1087
|
}
|
|
1088
1088
|
zones = self.compute_line_zones(array, **zone_kwargs)
|
|
1089
|
-
logger.info(f"Found
|
|
1089
|
+
logger.info(f"Found zones = {[tuple(int(i) for i in z) for z in zones]}")
|
|
1090
1090
|
mask = np.zeros(array.shape).astype(bool)
|
|
1091
1091
|
for z in zones:
|
|
1092
1092
|
mask[z[0] : z[1], :] = True
|
|
@@ -12,6 +12,7 @@ from astropy.time import TimeDelta
|
|
|
12
12
|
from dkist_processing_common.codecs.fits import fits_access_decoder
|
|
13
13
|
from dkist_processing_common.codecs.fits import fits_array_decoder
|
|
14
14
|
from dkist_processing_common.codecs.fits import fits_hdulist_encoder
|
|
15
|
+
from dkist_processing_common.codecs.json import json_decoder
|
|
15
16
|
from dkist_processing_common.models.fits_access import MetadataKey
|
|
16
17
|
from dkist_processing_common.models.task_name import TaskName
|
|
17
18
|
from dkist_processing_common.tasks.mixin.quality import QualityMixin
|
|
@@ -42,6 +43,7 @@ class CalibrationCollection:
|
|
|
42
43
|
angle: dict
|
|
43
44
|
state_offset: dict
|
|
44
45
|
spec_shift: dict
|
|
46
|
+
wavelength_calibration_header: dict
|
|
45
47
|
demod_matrices: dict | None
|
|
46
48
|
|
|
47
49
|
@cached_property
|
|
@@ -176,6 +178,15 @@ class ScienceCalibration(
|
|
|
176
178
|
spec_shift_dict = dict()
|
|
177
179
|
demod_dict = dict() if self.constants.correct_for_polarization else None
|
|
178
180
|
|
|
181
|
+
# WaveCal
|
|
182
|
+
#########
|
|
183
|
+
wavecal_header = next(
|
|
184
|
+
self.read(
|
|
185
|
+
tags=[VispTag.intermediate(), VispTag.task_wavelength_calibration()],
|
|
186
|
+
decoder=json_decoder,
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
|
|
179
190
|
for beam in range(1, self.constants.num_beams + 1):
|
|
180
191
|
for readout_exp_time in self.constants.observe_readout_exp_times:
|
|
181
192
|
# Dark
|
|
@@ -270,6 +281,7 @@ class ScienceCalibration(
|
|
|
270
281
|
angle=angle_dict,
|
|
271
282
|
state_offset=state_offset_dict,
|
|
272
283
|
spec_shift=spec_shift_dict,
|
|
284
|
+
wavelength_calibration_header=wavecal_header,
|
|
273
285
|
demod_matrices=demod_dict,
|
|
274
286
|
)
|
|
275
287
|
|
|
@@ -761,22 +773,31 @@ class ScienceCalibration(
|
|
|
761
773
|
"""
|
|
762
774
|
Update calibrated headers with any information gleaned during science calibration.
|
|
763
775
|
|
|
764
|
-
|
|
776
|
+
#. Apply the wavelength calibration header values
|
|
777
|
+
#. Add map scan keywords
|
|
778
|
+
#. Adjust CRPIX values based on any chopping needed to return only regions where the beams overlap
|
|
765
779
|
|
|
766
780
|
Parameters
|
|
767
781
|
----------
|
|
768
|
-
header
|
|
782
|
+
header
|
|
769
783
|
The header to update
|
|
770
784
|
|
|
771
|
-
map_scan
|
|
785
|
+
map_scan
|
|
772
786
|
Current map scan
|
|
773
787
|
|
|
788
|
+
calibrations
|
|
789
|
+
Container of intermediate calibration objects. Used to figure out how much to adjust CRPIX values.
|
|
790
|
+
|
|
774
791
|
Returns
|
|
775
792
|
-------
|
|
776
793
|
fits.Header
|
|
777
794
|
Updated header
|
|
778
795
|
|
|
779
796
|
"""
|
|
797
|
+
# Apply the wavelength calibration
|
|
798
|
+
# This needs to be done prior to adjusting CRPIX values below
|
|
799
|
+
header.update(calibrations.wavelength_calibration_header)
|
|
800
|
+
|
|
780
801
|
# Update the map scan number
|
|
781
802
|
header["VSPNMAPS"] = self.constants.num_map_scans
|
|
782
803
|
header["VSPMAP"] = map_scan
|
|
@@ -784,27 +805,51 @@ class ScienceCalibration(
|
|
|
784
805
|
# Adjust the CRPIX values if the beam overlap slicing chopped from the start of the array
|
|
785
806
|
x_slice, y_slice = calibrations.beams_overlap_slice
|
|
786
807
|
|
|
808
|
+
# Note: We KNOW that `x_slice` and `y_slice` correspond to the *array* dimensions corresponding to spectral and
|
|
809
|
+
# spatial directions, respectively. Some early ViSP data swap the *WCS* dimensions so that CRPIX1 contains
|
|
810
|
+
# information about the spectral axis, even though the spectral axis is always in the 0th array axis (and thus
|
|
811
|
+
# the second WCS axis because FITS and numpy are backwards; are we confused yet?).
|
|
812
|
+
#
|
|
813
|
+
# We want the adjustment of the CRPIX values to produce accurate WCS information, even if they're associated
|
|
814
|
+
# with the wrong array axes. For this reason we dynamically associate the WCS axis number with `x_slice` and
|
|
815
|
+
# `y_slice` via the `*_wcs_axis_num` properties.
|
|
816
|
+
#
|
|
817
|
+
# To say it differently, we KNOW that `x_slice` always refers to chopping in the spectral dimension, so we need
|
|
818
|
+
# to update the CRPIX associate with the spectral WCS, no matter which WCS axis that is.
|
|
819
|
+
|
|
787
820
|
# This if catches 0's and Nones
|
|
788
821
|
if x_slice.start:
|
|
789
822
|
# .start will only be non-None or 0 if the slice is from the start. In this case we need to update the WCS
|
|
790
823
|
logger.info(
|
|
791
|
-
f"Adjusting
|
|
824
|
+
f"Adjusting spectral CRPIX{self.spectral_wcs_axis_num} from "
|
|
825
|
+
f"{header[f'CRPIX{self.spectral_wcs_axis_num}']} to {header[f'CRPIX{self.spectral_wcs_axis_num}'] - x_slice.start}"
|
|
826
|
+
)
|
|
827
|
+
header[f"CRPIX{self.spectral_wcs_axis_num}"] = (
|
|
828
|
+
header[f"CRPIX{self.spectral_wcs_axis_num}"] - x_slice.start
|
|
792
829
|
)
|
|
793
|
-
header["CRPIX2"] = header["CRPIX2"] - x_slice.start
|
|
794
830
|
logger.info(
|
|
795
|
-
f"Adjusting
|
|
831
|
+
f"Adjusting spectral CRPIX{self.spectral_wcs_axis_num}A from "
|
|
832
|
+
f"{header[f'CRPIX{self.spectral_wcs_axis_num}A']} to {header[f'CRPIX{self.spectral_wcs_axis_num}A'] - x_slice.start}"
|
|
833
|
+
)
|
|
834
|
+
header[f"CRPIX{self.spectral_wcs_axis_num}A"] = (
|
|
835
|
+
header[f"CRPIX{self.spectral_wcs_axis_num}A"] - x_slice.start
|
|
796
836
|
)
|
|
797
|
-
header["CRPIX2A"] = header["CRPIX2A"] - x_slice.start
|
|
798
837
|
|
|
799
838
|
if y_slice.start:
|
|
800
839
|
logger.info(
|
|
801
|
-
f"Adjusting
|
|
840
|
+
f"Adjusting spatial CRPIX{self.spatial_wcs_axis_num} from "
|
|
841
|
+
f"{header[f'CRPIX{self.spatial_wcs_axis_num}']} to {header[f'CRPIX{self.spatial_wcs_axis_num}'] - y_slice.start}"
|
|
842
|
+
)
|
|
843
|
+
header[f"CRPIX{self.spatial_wcs_axis_num}"] = (
|
|
844
|
+
header[f"CRPIX{self.spatial_wcs_axis_num}"] - y_slice.start
|
|
802
845
|
)
|
|
803
|
-
header["CRPIX1"] = header["CRPIX1"] - y_slice.start
|
|
804
846
|
logger.info(
|
|
805
|
-
f"Adjusting
|
|
847
|
+
f"Adjusting spatial CRPIX{self.spatial_wcs_axis_num}A from "
|
|
848
|
+
f"{header[f'CRPIX{self.spatial_wcs_axis_num}A']} to {header[f'CRPIX{self.spatial_wcs_axis_num}A'] - y_slice.start}"
|
|
849
|
+
)
|
|
850
|
+
header[f"CRPIX{self.spatial_wcs_axis_num}A"] = (
|
|
851
|
+
header[f"CRPIX{self.spatial_wcs_axis_num}A"] - y_slice.start
|
|
806
852
|
)
|
|
807
|
-
header["CRPIX1A"] = header["CRPIX1A"] - y_slice.start
|
|
808
853
|
|
|
809
854
|
return header
|
|
810
855
|
|
|
@@ -900,3 +945,35 @@ class ScienceCalibration(
|
|
|
900
945
|
|
|
901
946
|
filename = next(self.read(tags=tags))
|
|
902
947
|
logger.info(f"Wrote intermediate file for {tags = } to {filename}")
|
|
948
|
+
|
|
949
|
+
@cached_property
|
|
950
|
+
def spectral_wcs_axis_num(self) -> int:
|
|
951
|
+
"""
|
|
952
|
+
Return the WCS axis number corresponding to wavelength.
|
|
953
|
+
|
|
954
|
+
We need to check this dynamically because some early ViSP data got the WCS backward w.r.t. the array dimensions.
|
|
955
|
+
"""
|
|
956
|
+
try:
|
|
957
|
+
spectral_axis_num = next(
|
|
958
|
+
(i for i in range(1, 4) if getattr(self.constants, f"axis_{i}_type") == "AWAV")
|
|
959
|
+
)
|
|
960
|
+
except StopIteration as e:
|
|
961
|
+
raise ValueError("Could not find WCS axis with type AWAV") from e
|
|
962
|
+
|
|
963
|
+
return spectral_axis_num
|
|
964
|
+
|
|
965
|
+
@cached_property
|
|
966
|
+
def spatial_wcs_axis_num(self) -> int:
|
|
967
|
+
"""
|
|
968
|
+
Return the WCS axis number corresponding to Helioprojective latitude.
|
|
969
|
+
|
|
970
|
+
We need to check this dynamically because some early ViSP data got the WCS backward w.r.t. the array dimensions.
|
|
971
|
+
"""
|
|
972
|
+
try:
|
|
973
|
+
spatial_axis_num = next(
|
|
974
|
+
(i for i in range(1, 4) if getattr(self.constants, f"axis_{i}_type") == "HPLT-TAN")
|
|
975
|
+
)
|
|
976
|
+
except StopIteration as e:
|
|
977
|
+
raise ValueError("Cound not find WCS axis with type HPLT-TAN") from e
|
|
978
|
+
|
|
979
|
+
return spatial_axis_num
|
|
@@ -8,14 +8,11 @@ from typing import Callable
|
|
|
8
8
|
import astropy.units as u
|
|
9
9
|
import numpy as np
|
|
10
10
|
import scipy.ndimage as spnd
|
|
11
|
-
from astropy.time import Time
|
|
12
11
|
from astropy.units import Quantity
|
|
13
|
-
from astropy.wcs import WCS
|
|
14
12
|
from dkist_processing_common.codecs.asdf import asdf_encoder
|
|
15
13
|
from dkist_processing_common.codecs.fits import fits_access_decoder
|
|
16
14
|
from dkist_processing_common.codecs.fits import fits_array_decoder
|
|
17
15
|
from dkist_processing_common.codecs.fits import fits_array_encoder
|
|
18
|
-
from dkist_processing_common.models.dkist_location import location_of_dkist
|
|
19
16
|
from dkist_processing_common.models.task_name import TaskName
|
|
20
17
|
from dkist_processing_common.tasks.mixin.quality import QualityMixin
|
|
21
18
|
from dkist_processing_math.arithmetic import divide_arrays_by_array
|
|
@@ -38,9 +35,7 @@ from solar_wavelength_calibration import UnitlessBoundRange
|
|
|
38
35
|
from solar_wavelength_calibration import WavelengthCalibrationFitter
|
|
39
36
|
from solar_wavelength_calibration import WavelengthCalibrationParameters
|
|
40
37
|
from solar_wavelength_calibration.fitter.wavelength_fitter import FitResult
|
|
41
|
-
from solar_wavelength_calibration.fitter.wavelength_fitter import WavelengthParameters
|
|
42
38
|
from solar_wavelength_calibration.fitter.wavelength_fitter import calculate_initial_crval_guess
|
|
43
|
-
from sunpy.coordinates import HeliocentricInertial
|
|
44
39
|
|
|
45
40
|
from dkist_processing_visp.models.metric_code import VispMetricCode
|
|
46
41
|
from dkist_processing_visp.models.tags import VispTag
|
|
@@ -48,15 +43,15 @@ from dkist_processing_visp.parsers.visp_l0_fits_access import VispL0FitsAccess
|
|
|
48
43
|
from dkist_processing_visp.tasks.mixin.beam_access import BeamAccessMixin
|
|
49
44
|
from dkist_processing_visp.tasks.mixin.corrections import CorrectionsMixin
|
|
50
45
|
from dkist_processing_visp.tasks.visp_base import VispTaskBase
|
|
46
|
+
from dkist_processing_visp.tasks.wavelength_calibration import compute_initial_dispersion
|
|
47
|
+
from dkist_processing_visp.tasks.wavelength_calibration import compute_input_wavelength_vector
|
|
48
|
+
from dkist_processing_visp.tasks.wavelength_calibration import compute_order
|
|
49
|
+
from dkist_processing_visp.tasks.wavelength_calibration import get_doppler_velocity
|
|
51
50
|
|
|
52
51
|
__all__ = [
|
|
53
52
|
"SolarCalibration",
|
|
54
53
|
"WavelengthCalibrationParametersWithContinuum",
|
|
55
54
|
"polynomial_continuum_model",
|
|
56
|
-
"compute_order",
|
|
57
|
-
"compute_initial_dispersion",
|
|
58
|
-
"compute_doppler_velocity",
|
|
59
|
-
"compute_input_wavelength_vector",
|
|
60
55
|
]
|
|
61
56
|
|
|
62
57
|
|
|
@@ -128,7 +123,7 @@ class SolarCalibration(
|
|
|
128
123
|
QualityMixin,
|
|
129
124
|
):
|
|
130
125
|
"""
|
|
131
|
-
Task class for generating Solar Gain images for each beam
|
|
126
|
+
Task class for generating Solar Gain images for each beam.
|
|
132
127
|
|
|
133
128
|
Parameters
|
|
134
129
|
----------
|
|
@@ -190,8 +185,8 @@ class SolarCalibration(
|
|
|
190
185
|
pixel_pitch=self.parameters.wavecal_pixel_pitch_micron_per_pix,
|
|
191
186
|
)
|
|
192
187
|
|
|
193
|
-
doppler_velocity =
|
|
194
|
-
|
|
188
|
+
doppler_velocity = get_doppler_velocity(
|
|
189
|
+
solar_gain_ip_start_time=self.constants.solar_gain_ip_start_time
|
|
195
190
|
)
|
|
196
191
|
|
|
197
192
|
self._log_wavecal_parameters(
|
|
@@ -247,8 +242,10 @@ class SolarCalibration(
|
|
|
247
242
|
self.write(
|
|
248
243
|
data=char_spec,
|
|
249
244
|
encoder=fits_array_encoder,
|
|
250
|
-
tags=[
|
|
251
|
-
|
|
245
|
+
tags=[
|
|
246
|
+
VispTag.intermediate_frame(beam=beam),
|
|
247
|
+
VispTag.task_characteristic_spectra(),
|
|
248
|
+
],
|
|
252
249
|
overwrite=True,
|
|
253
250
|
)
|
|
254
251
|
|
|
@@ -830,9 +827,6 @@ class SolarCalibration(
|
|
|
830
827
|
beam
|
|
831
828
|
The beam number for this array
|
|
832
829
|
|
|
833
|
-
modstate
|
|
834
|
-
The modulator state for this array
|
|
835
|
-
|
|
836
830
|
|
|
837
831
|
Returns
|
|
838
832
|
-------
|
|
@@ -958,7 +952,7 @@ class SolarCalibration(
|
|
|
958
952
|
self, dispersion: Quantity, order: int, doppler_velocity: Quantity
|
|
959
953
|
) -> None:
|
|
960
954
|
"""Log initial guess and instrument-derived wavecal parameters."""
|
|
961
|
-
logger.info(f"
|
|
955
|
+
logger.info(f"central_wavelength = {self.constants.wavelength * u.nm !s}")
|
|
962
956
|
logger.info(f"{dispersion = !s}")
|
|
963
957
|
logger.info(f"{order = }")
|
|
964
958
|
logger.info(f"grating constant = {self.constants.grating_constant_inverse_mm !s}")
|
|
@@ -996,190 +990,3 @@ def polynomial_continuum_model(
|
|
|
996
990
|
"""
|
|
997
991
|
coeffs = [fit_parameters[f"poly_coeff_{i:02n}"].value for i in range(fit_order + 1)]
|
|
998
992
|
return np.polyval(coeffs, abscissa)
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
##################################################################################
|
|
1002
|
-
# TODO: The following definitions should go in the wavecal module once it exists #
|
|
1003
|
-
##################################################################################
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
def compute_order(
|
|
1007
|
-
central_wavelength: Quantity,
|
|
1008
|
-
incident_light_angle: Quantity,
|
|
1009
|
-
reflected_light_angle: Quantity,
|
|
1010
|
-
grating_constant: Quantity,
|
|
1011
|
-
) -> int:
|
|
1012
|
-
r"""
|
|
1013
|
-
Compute the spectral order from the spectrograph setup.
|
|
1014
|
-
|
|
1015
|
-
From the grating equation, the spectral order, :math:`m`:, is
|
|
1016
|
-
|
|
1017
|
-
.. math::
|
|
1018
|
-
m = \frac{\sin\alpha + \sin\beta}{G \lambda}
|
|
1019
|
-
|
|
1020
|
-
where :math:`\alpha` and :math:`\beta` are the incident and reflected light angles, respectively, :math:`G` is the
|
|
1021
|
-
grating constant (lines per mm), and :math:`\lambda` is the central wavelength. All of these values come from the
|
|
1022
|
-
input headers.
|
|
1023
|
-
|
|
1024
|
-
Parameters
|
|
1025
|
-
----------
|
|
1026
|
-
central_wavelength
|
|
1027
|
-
Wavelength of the center of the spectral window.
|
|
1028
|
-
|
|
1029
|
-
incident_light_angle
|
|
1030
|
-
Angle of light incident to the spectrograph grating. Often called :math:`\alpha`.
|
|
1031
|
-
|
|
1032
|
-
reflected_light_angle
|
|
1033
|
-
Angle of light reflected from spectrograph grating. Often called :math:`\beta`.
|
|
1034
|
-
|
|
1035
|
-
grating_constant
|
|
1036
|
-
Grating constant of the spectrograph grating [lines per mm]
|
|
1037
|
-
|
|
1038
|
-
Returns
|
|
1039
|
-
-------
|
|
1040
|
-
spectral_order
|
|
1041
|
-
The order of the given spectrograph configuration
|
|
1042
|
-
"""
|
|
1043
|
-
return int(
|
|
1044
|
-
(np.sin(incident_light_angle) + np.sin(reflected_light_angle))
|
|
1045
|
-
/ (grating_constant * central_wavelength)
|
|
1046
|
-
)
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
def compute_initial_dispersion(
|
|
1050
|
-
central_wavelength: Quantity,
|
|
1051
|
-
incident_light_angle: Quantity,
|
|
1052
|
-
reflected_light_angle: Quantity,
|
|
1053
|
-
lens_parameters: list[Quantity],
|
|
1054
|
-
pixel_pitch: Quantity,
|
|
1055
|
-
) -> Quantity:
|
|
1056
|
-
r"""
|
|
1057
|
-
Compute the dispersion (:math:`d\,\lambda/d\, px`) given the spectrograph setup.
|
|
1058
|
-
|
|
1059
|
-
The dispersion is given via
|
|
1060
|
-
|
|
1061
|
-
.. math::
|
|
1062
|
-
d\,\lambda / d\, px = \frac{p \lambda_0 \cos\beta}{f (\sin\alpha + \sin\beta)}
|
|
1063
|
-
|
|
1064
|
-
where :math:`p` is the pixel pitch (microns per pix), :math:`\lambda_0` is the central wavelength, :math:`f` is the
|
|
1065
|
-
camera focal length, and :math:`\alpha` and :math:`\beta` are the incident and reflected light angles, respectively.
|
|
1066
|
-
:math:`\lambda_0`, :math:`\alpha`, and :math:`\beta` are taken from input headers, while :math:`f` and :math:`p` are
|
|
1067
|
-
pipeline parameters.
|
|
1068
|
-
|
|
1069
|
-
Parameters
|
|
1070
|
-
----------
|
|
1071
|
-
central_wavelength
|
|
1072
|
-
Wavelength of the center of the spectral window.
|
|
1073
|
-
|
|
1074
|
-
incident_light_angle
|
|
1075
|
-
Angle of light incident to the spectrograph grating. Often called :math:`\alpha`.
|
|
1076
|
-
|
|
1077
|
-
reflected_light_angle
|
|
1078
|
-
Angle of light reflected from spectrograph grating. Often called :math:`\beta`.
|
|
1079
|
-
|
|
1080
|
-
lens_parameters
|
|
1081
|
-
Parameterization of lense focal length as zero, first, and second orders of wavelength. If the total focal
|
|
1082
|
-
length of the lens is :math:`f = a_0 + a_1\lambda + a_2\lambda^2` then this list is :math:`[a_0, a_1, a_2]`.
|
|
1083
|
-
|
|
1084
|
-
pixel_pitch
|
|
1085
|
-
The physical size of a single pixel
|
|
1086
|
-
|
|
1087
|
-
Returns
|
|
1088
|
-
-------
|
|
1089
|
-
dispersion
|
|
1090
|
-
The computed dispersion in units of nm / px
|
|
1091
|
-
"""
|
|
1092
|
-
camera_focal_length = lens_parameters[0] + central_wavelength * (
|
|
1093
|
-
lens_parameters[1] + central_wavelength * lens_parameters[2]
|
|
1094
|
-
)
|
|
1095
|
-
logger.info(f"{camera_focal_length = !s}")
|
|
1096
|
-
|
|
1097
|
-
linear_dispersion = (
|
|
1098
|
-
camera_focal_length
|
|
1099
|
-
* (np.sin(incident_light_angle) + np.sin(reflected_light_angle))
|
|
1100
|
-
/ (np.cos(reflected_light_angle) * central_wavelength)
|
|
1101
|
-
)
|
|
1102
|
-
|
|
1103
|
-
dispersion = pixel_pitch / linear_dispersion
|
|
1104
|
-
|
|
1105
|
-
return dispersion.to(u.nm / u.pix)
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
def compute_doppler_velocity(time_of_observation: str) -> Quantity:
|
|
1109
|
-
"""Find the speed at which DKIST is moving relative to the Sun's center.
|
|
1110
|
-
|
|
1111
|
-
Positive values refer to when DKIST is moving away from the sun.
|
|
1112
|
-
|
|
1113
|
-
Parameters
|
|
1114
|
-
----------
|
|
1115
|
-
time_of_observation
|
|
1116
|
-
Time at which to compute the relative Dopper velocity. Any string that can be parsed by `astropy.time.Time` is
|
|
1117
|
-
acceptable.
|
|
1118
|
-
|
|
1119
|
-
Returns
|
|
1120
|
-
-------
|
|
1121
|
-
doppler_velocity
|
|
1122
|
-
The relative velocity between observer and the Sun with units of km / s
|
|
1123
|
-
"""
|
|
1124
|
-
coord = location_of_dkist.get_gcrs(obstime=Time(time_of_observation))
|
|
1125
|
-
heliocentric_coord = coord.transform_to(HeliocentricInertial(obstime=Time(time_of_observation)))
|
|
1126
|
-
obs_vr_kms = heliocentric_coord.d_distance
|
|
1127
|
-
return obs_vr_kms
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
def compute_input_wavelength_vector(
|
|
1131
|
-
*,
|
|
1132
|
-
central_wavelength: Quantity,
|
|
1133
|
-
dispersion: Quantity,
|
|
1134
|
-
grating_constant: Quantity,
|
|
1135
|
-
order: int,
|
|
1136
|
-
incident_light_angle: Quantity,
|
|
1137
|
-
num_spec_px: int,
|
|
1138
|
-
) -> u.Quantity:
|
|
1139
|
-
r"""
|
|
1140
|
-
Compute a wavelength vector based on information about the spectrograph setup.
|
|
1141
|
-
|
|
1142
|
-
The parameterization of the grating equation is via `astropy.wcs.WCS`, which follows section 5 of
|
|
1143
|
-
`Greisen et al (2006) <https://ui.adsabs.harvard.edu/abs/2006A%26A...446..747G/abstract>`_.
|
|
1144
|
-
|
|
1145
|
-
Parameters
|
|
1146
|
-
----------
|
|
1147
|
-
central_wavelength
|
|
1148
|
-
Wavelength at the center of the spectral window. This function forces the value of the output vector to be
|
|
1149
|
-
``central_wavelength`` at index ``num_spec // 2 + 1``.
|
|
1150
|
-
|
|
1151
|
-
dispersion
|
|
1152
|
-
Spectrograph dispersion [nm / px]
|
|
1153
|
-
|
|
1154
|
-
grating_constant
|
|
1155
|
-
Grating constant of the spectrograph grating [lines per mm]
|
|
1156
|
-
|
|
1157
|
-
order
|
|
1158
|
-
Spectrograph order
|
|
1159
|
-
|
|
1160
|
-
incident_light_angle
|
|
1161
|
-
Angle of light incident to the spectrograph grating. Often called :math:`\alpha`.
|
|
1162
|
-
|
|
1163
|
-
num_spec_px
|
|
1164
|
-
The length of the output wavelength vector. Defines size and physical limits of the output.
|
|
1165
|
-
|
|
1166
|
-
Returns
|
|
1167
|
-
-------
|
|
1168
|
-
wave_vec
|
|
1169
|
-
1D array of length ``num_spec`` containing the wavelength values described by the input WCS parameterization.
|
|
1170
|
-
The units of this array will be nanometers.
|
|
1171
|
-
"""
|
|
1172
|
-
wavelength_parameters = WavelengthParameters(
|
|
1173
|
-
crpix=num_spec_px // 2 + 1,
|
|
1174
|
-
crval=central_wavelength.to_value(u.nm),
|
|
1175
|
-
dispersion=dispersion.to_value(u.nm / u.pix),
|
|
1176
|
-
grating_constant=grating_constant.to_value(1 / u.mm),
|
|
1177
|
-
order=order,
|
|
1178
|
-
incident_light_angle=incident_light_angle.to_value(u.deg),
|
|
1179
|
-
cunit="nm",
|
|
1180
|
-
)
|
|
1181
|
-
header = wavelength_parameters.to_header(axis_num=1)
|
|
1182
|
-
wcs = WCS(header)
|
|
1183
|
-
input_wavelength_vector = wcs.spectral.pixel_to_world(np.arange(num_spec_px)).to(u.nm)
|
|
1184
|
-
|
|
1185
|
-
return input_wavelength_vector
|