dkist-processing-trend 0.1.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.
- changelog/.gitempty +0 -0
- dkist_processing_trend/__init__.py +10 -0
- dkist_processing_trend/config.py +11 -0
- dkist_processing_trend/models/__init__.py +1 -0
- dkist_processing_trend/models/constants.py +143 -0
- dkist_processing_trend/models/fit_options.py +15 -0
- dkist_processing_trend/models/fits_access.py +65 -0
- dkist_processing_trend/models/instrument.py +35 -0
- dkist_processing_trend/models/instrument_options.py +35 -0
- dkist_processing_trend/models/parameters.py +134 -0
- dkist_processing_trend/models/tags.py +117 -0
- dkist_processing_trend/models/task_name.py +20 -0
- dkist_processing_trend/parsers/__init__.py +1 -0
- dkist_processing_trend/parsers/arm_id.py +103 -0
- dkist_processing_trend/parsers/instrument_unique_bud.py +40 -0
- dkist_processing_trend/parsers/time.py +27 -0
- dkist_processing_trend/parsers/trend_l0_fits_access.py +123 -0
- dkist_processing_trend/tasks/__init__.py +27 -0
- dkist_processing_trend/tasks/arm_task_factory.py +57 -0
- dkist_processing_trend/tasks/dark.py +56 -0
- dkist_processing_trend/tasks/gain.py +74 -0
- dkist_processing_trend/tasks/initialize_arm_tasks.py +50 -0
- dkist_processing_trend/tasks/parse.py +169 -0
- dkist_processing_trend/tasks/prepare_fit_data_base.py +257 -0
- dkist_processing_trend/tasks/run_pac_fitter.py +392 -0
- dkist_processing_trend/tasks/trend_base.py +97 -0
- dkist_processing_trend/tasks/trend_output_data.py +167 -0
- dkist_processing_trend/tasks/visp/__init__.py +6 -0
- dkist_processing_trend/tasks/visp/visp_dmpd.py +410 -0
- dkist_processing_trend/tasks/visp/visp_extract_beam.py +14 -0
- dkist_processing_trend/tasks/visp/visp_geometric.py +260 -0
- dkist_processing_trend/tasks/visp/visp_prep_fit_data.py +162 -0
- dkist_processing_trend/tasks/visp/visp_process_demod.py +236 -0
- dkist_processing_trend/tasks/write_trend.py +663 -0
- dkist_processing_trend/tests/__init__.py +1 -0
- dkist_processing_trend/tests/conftest.py +718 -0
- dkist_processing_trend/tests/local_trial_workflows/__init__.py +0 -0
- dkist_processing_trend/tests/local_trial_workflows/l0_to_trend_visp_polcal.py +294 -0
- dkist_processing_trend/tests/local_trial_workflows/local_trial_helpers.py +488 -0
- dkist_processing_trend/tests/test_arm_task_factory.py +82 -0
- dkist_processing_trend/tests/test_base_tasks.py +86 -0
- dkist_processing_trend/tests/test_constants.py +120 -0
- dkist_processing_trend/tests/test_dark.py +97 -0
- dkist_processing_trend/tests/test_gain.py +135 -0
- dkist_processing_trend/tests/test_parameters.py +149 -0
- dkist_processing_trend/tests/test_parse.py +276 -0
- dkist_processing_trend/tests/test_prep_fit_data_base.py +233 -0
- dkist_processing_trend/tests/test_publish_catalog_messages.py +45 -0
- dkist_processing_trend/tests/test_run_pac_fitter.py +371 -0
- dkist_processing_trend/tests/test_stems.py +75 -0
- dkist_processing_trend/tests/test_transfer_output_data.py +76 -0
- dkist_processing_trend/tests/test_trend_fits_access.py +173 -0
- dkist_processing_trend/tests/test_visp.py +874 -0
- dkist_processing_trend/tests/test_workflows.py +10 -0
- dkist_processing_trend/tests/test_write_trend.py +460 -0
- dkist_processing_trend/workflows/__init__.py +3 -0
- dkist_processing_trend/workflows/visp.py +58 -0
- dkist_processing_trend-0.1.0.dist-info/METADATA +549 -0
- dkist_processing_trend-0.1.0.dist-info/RECORD +66 -0
- dkist_processing_trend-0.1.0.dist-info/WHEEL +5 -0
- dkist_processing_trend-0.1.0.dist-info/top_level.txt +3 -0
- docs/conf.py +57 -0
- docs/index.rst +10 -0
- docs/l0_to_trend_visp_polcal.rst +4 -0
- docs/landing_page.rst +11 -0
- docs/requirements_table.rst +8 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Bud for parsing the full set of input ARM IDs."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
from typing import DefaultDict
|
|
7
|
+
|
|
8
|
+
from dkist_processing_common.models.flower_pot import SetStem
|
|
9
|
+
from dkist_processing_common.models.flower_pot import SpilledDirt
|
|
10
|
+
|
|
11
|
+
from dkist_processing_trend.models.constants import TrendBudName
|
|
12
|
+
from dkist_processing_trend.models.fits_access import TrendMetadataKey
|
|
13
|
+
from dkist_processing_trend.parsers.trend_l0_fits_access import TrendL0FitsAccess
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TrendArmIdListBud(SetStem):
|
|
17
|
+
"""
|
|
18
|
+
Bud for parsing the set of ARM IDs present in the input data.
|
|
19
|
+
|
|
20
|
+
The actual value stored in the db is a `list` only because `set` isn't serializable with JSON.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
super().__init__(stem_name=TrendBudName.arm_id_list)
|
|
25
|
+
self.metadata_key = TrendMetadataKey.arm_id.name
|
|
26
|
+
|
|
27
|
+
def setter(self, fits_obj: TrendL0FitsAccess) -> int | str:
|
|
28
|
+
"""Return the arm_id property of the given FitsAccess object."""
|
|
29
|
+
return getattr(fits_obj, self.metadata_key)
|
|
30
|
+
|
|
31
|
+
def getter(self) -> list[int | str]:
|
|
32
|
+
"""Return the set of values as a list for database storage."""
|
|
33
|
+
return list(self.value_set)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ArmIdLookupBud(SetStem):
|
|
37
|
+
"""
|
|
38
|
+
Bud that stores a header key as a function of arm_id.
|
|
39
|
+
|
|
40
|
+
Each arm is allowed to only have a single value of the given key.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, constant_name: str, metadata_key: StrEnum):
|
|
44
|
+
super().__init__(stem_name=constant_name)
|
|
45
|
+
self.value_metadata_key = metadata_key.name
|
|
46
|
+
self.mapping: DefaultDict[float, set[float]] = defaultdict(set)
|
|
47
|
+
|
|
48
|
+
def setter(self, fits_obj: TrendL0FitsAccess) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Update the mapping dictionary.
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
fits_obj
|
|
55
|
+
The input fits object
|
|
56
|
+
|
|
57
|
+
Returns
|
|
58
|
+
-------
|
|
59
|
+
Updates the dictionary and returns None
|
|
60
|
+
"""
|
|
61
|
+
arm_id = fits_obj.arm_id
|
|
62
|
+
value = getattr(fits_obj, self.value_metadata_key)
|
|
63
|
+
self.mapping[arm_id].add(value)
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
def getter(self) -> dict[str, float | str]:
|
|
67
|
+
"""
|
|
68
|
+
Check that each arm only contains a single value and convert each arm's `set` to a single value.
|
|
69
|
+
|
|
70
|
+
The output dict has its keys wrapped in `json.loads` to simplify the extraction of both `int` and `str` arm IDs
|
|
71
|
+
in `~dkist_processing_trend.models.constants.TrendConstants`.
|
|
72
|
+
"""
|
|
73
|
+
output_mapping = dict()
|
|
74
|
+
error_list = []
|
|
75
|
+
for arm_id, value_set in self.mapping.items():
|
|
76
|
+
if len(value_set) > 1:
|
|
77
|
+
error_list.append(f"{arm_id = }: {value_set}")
|
|
78
|
+
|
|
79
|
+
output_mapping[json.dumps(arm_id)] = value_set.pop()
|
|
80
|
+
|
|
81
|
+
if len(error_list) > 0:
|
|
82
|
+
error_str = f"Some arms have multiple values for {self.value_metadata_key}: "
|
|
83
|
+
for arm_err in error_list:
|
|
84
|
+
error_str += f"{arm_err} "
|
|
85
|
+
raise ValueError(error_str)
|
|
86
|
+
|
|
87
|
+
return output_mapping
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class InstrumentArmIdLookupBud(ArmIdLookupBud):
|
|
91
|
+
"""Subclass of `ArmIdLookupBud` that only ingests data for a given instrument."""
|
|
92
|
+
|
|
93
|
+
def __init__(self, constant_name: str, metadata_key: StrEnum, instruments: str | list[str]):
|
|
94
|
+
super().__init__(constant_name=constant_name, metadata_key=metadata_key)
|
|
95
|
+
if isinstance(instruments, str):
|
|
96
|
+
instruments = [instruments]
|
|
97
|
+
self.instruments = [i.casefold() for i in instruments]
|
|
98
|
+
|
|
99
|
+
def setter(self, fits_obj: TrendL0FitsAccess) -> None:
|
|
100
|
+
"""Update the mapping dict only if the input object's instrument attribute matches the instrument associated with this instance."""
|
|
101
|
+
if fits_obj.instrument.casefold() not in self.instruments:
|
|
102
|
+
return SpilledDirt
|
|
103
|
+
return super().setter(fits_obj)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Version of UniqueBud that only parses frame from a specific instrument."""
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
|
|
5
|
+
from dkist_processing_common.models.flower_pot import SpilledDirt
|
|
6
|
+
from dkist_processing_common.parsers.unique_bud import UniqueBud
|
|
7
|
+
|
|
8
|
+
from dkist_processing_trend.parsers.trend_l0_fits_access import TrendL0FitsAccess
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class InstrumentUniqueBud(UniqueBud):
|
|
12
|
+
"""
|
|
13
|
+
Subclass of `UniqueBud` that only considers objects that have specific task types.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
constant_name
|
|
18
|
+
The name for the constant to be defined
|
|
19
|
+
|
|
20
|
+
metadata_key
|
|
21
|
+
The metadata key associated with the constant
|
|
22
|
+
|
|
23
|
+
instruments
|
|
24
|
+
Only consider frames whose header INSTRUME value is in this list
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self, constant_name: str, metadata_key: str | StrEnum, instruments: str | list[str]
|
|
29
|
+
):
|
|
30
|
+
super().__init__(constant_name=constant_name, metadata_key=metadata_key)
|
|
31
|
+
if isinstance(instruments, str):
|
|
32
|
+
instruments = [instruments]
|
|
33
|
+
self.instruments = [i.casefold() for i in instruments]
|
|
34
|
+
|
|
35
|
+
def setter(self, fits_obj: TrendL0FitsAccess) -> int | float | bool | str:
|
|
36
|
+
"""Ingest an object's metadata key only if its instrument property is in the allowed list."""
|
|
37
|
+
if fits_obj.instrument.casefold() in self.instruments:
|
|
38
|
+
return super().setter(fits_obj)
|
|
39
|
+
|
|
40
|
+
return SpilledDirt
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Stems that parse time-related constants and tags."""
|
|
2
|
+
|
|
3
|
+
from dkist_processing_common.models.flower_pot import SetStem
|
|
4
|
+
|
|
5
|
+
from dkist_processing_trend.models.constants import TrendBudName
|
|
6
|
+
from dkist_processing_trend.parsers.trend_l0_fits_access import TrendL0FitsAccess
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EarliestIpStartTime(SetStem):
|
|
10
|
+
"""
|
|
11
|
+
Bud for parsing the earliest IP start time in the input data.
|
|
12
|
+
|
|
13
|
+
This is akin to the concept of "OBSERVE IP start time" in the instrument pipelines. The wrinkle is that a POLCAL OP
|
|
14
|
+
consists of multiple IPs, so with this Bud we consider the "time of observation" to be the earliest IP we have. This
|
|
15
|
+
time is used for choosing parameter values, so high precision is not needed.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
super().__init__(stem_name=TrendBudName.earliest_ip_start_time)
|
|
20
|
+
|
|
21
|
+
def setter(self, fits_obj: TrendL0FitsAccess) -> str:
|
|
22
|
+
"""Return the IP start time of the given FitsAccess object."""
|
|
23
|
+
return fits_obj.ip_start_time
|
|
24
|
+
|
|
25
|
+
def getter(self) -> str:
|
|
26
|
+
"""Return the earliest IP start time in the set of all start times."""
|
|
27
|
+
return min(self.value_set)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Trend FitsAccess classes for accessing fits header values as properties."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from astropy.io import fits
|
|
5
|
+
from dkist_processing_common.parsers.l0_fits_access import L0FitsAccess
|
|
6
|
+
|
|
7
|
+
from dkist_processing_trend.models.fits_access import CryonirspMetadataKey
|
|
8
|
+
from dkist_processing_trend.models.fits_access import DlnirspMetadataKey
|
|
9
|
+
from dkist_processing_trend.models.fits_access import TrendMetadataKey
|
|
10
|
+
from dkist_processing_trend.models.fits_access import VispMetadataKey
|
|
11
|
+
from dkist_processing_trend.models.instrument import InstrumentName
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class IncorrectInstrumentError(Exception):
|
|
15
|
+
"""Error to indicate we're trying to access a property that only exists for a different instrument."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class InstrumentSpecificProperty:
|
|
19
|
+
"""
|
|
20
|
+
A class for defining properties that will produce a custom (and useful) error when accessed.
|
|
21
|
+
|
|
22
|
+
The error is an indication that the user is trying to access an instrument-specific property for a `TrendL0FitsAccess`
|
|
23
|
+
object that doesn't correspond to that particular instrument.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, instrument: str):
|
|
27
|
+
self.instrument = instrument
|
|
28
|
+
|
|
29
|
+
# Inspired by https://stackoverflow.com/questions/56997102/python-notimplementederror-for-instance-attributes
|
|
30
|
+
def __get__(self, *args, **kwargs):
|
|
31
|
+
raise IncorrectInstrumentError(
|
|
32
|
+
f"This property is only defined for data from {self.instrument}, which this frame is not."
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TrendL0FitsAccess(L0FitsAccess):
|
|
37
|
+
"""
|
|
38
|
+
Class to provide easy access to L0 headers for input/linearized trend data.
|
|
39
|
+
|
|
40
|
+
This class contains logic that allows the same property to look at different header keys depending on instrument.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
header
|
|
45
|
+
The header from which to parse higher-level properties
|
|
46
|
+
|
|
47
|
+
data
|
|
48
|
+
A data array to associate with the current instance
|
|
49
|
+
|
|
50
|
+
name
|
|
51
|
+
The name of the file that was loaded into this FitsAccess object
|
|
52
|
+
|
|
53
|
+
auto_squeeze
|
|
54
|
+
When set to True, dimensions of length 1 will be removed from the array
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
visp_grating_angle: float = InstrumentSpecificProperty(InstrumentName.visp)
|
|
58
|
+
visp_arm_position: float = InstrumentSpecificProperty(InstrumentName.visp)
|
|
59
|
+
visp_grating_constant: float = InstrumentSpecificProperty(InstrumentName.visp)
|
|
60
|
+
cryonirsp_od_filter_name: str = InstrumentSpecificProperty(InstrumentName.cryonirsp)
|
|
61
|
+
cryonirsp_grating_angle: float = InstrumentSpecificProperty(InstrumentName.cryonirsp)
|
|
62
|
+
cryonirsp_grating_littrow_angle: float = InstrumentSpecificProperty(InstrumentName.cryonirsp)
|
|
63
|
+
cryonirsp_grating_constant: float = InstrumentSpecificProperty(InstrumentName.cryonirsp)
|
|
64
|
+
cryonirsp_slit_width: float = InstrumentSpecificProperty(InstrumentName.cryonirsp)
|
|
65
|
+
dlnirsp_grating_angle: float = InstrumentSpecificProperty(InstrumentName.dlnirsp)
|
|
66
|
+
dlnirsp_arm_position: float = InstrumentSpecificProperty(InstrumentName.dlnirsp)
|
|
67
|
+
dlnirsp_grating_constant: float = InstrumentSpecificProperty(InstrumentName.dlnirsp)
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
*,
|
|
72
|
+
header: fits.Header,
|
|
73
|
+
data: np.ndarray | None = None,
|
|
74
|
+
name: str | None = None,
|
|
75
|
+
auto_squeeze: bool = True,
|
|
76
|
+
):
|
|
77
|
+
super().__init__(header=header, data=data, name=name, auto_squeeze=auto_squeeze)
|
|
78
|
+
|
|
79
|
+
match self.instrument:
|
|
80
|
+
case InstrumentName.visp:
|
|
81
|
+
self.number_of_modulator_states: int = header[
|
|
82
|
+
VispMetadataKey.number_of_modulator_states
|
|
83
|
+
]
|
|
84
|
+
self.modulator_state: int = header[VispMetadataKey.modulator_state]
|
|
85
|
+
self.arm_id: int = header[VispMetadataKey.arm_id]
|
|
86
|
+
self.visp_grating_angle: float = header[TrendMetadataKey.visp_grating_angle]
|
|
87
|
+
self.visp_arm_position: float = header[TrendMetadataKey.visp_arm_position]
|
|
88
|
+
self.visp_grating_constant: float = header[TrendMetadataKey.visp_grating_constant]
|
|
89
|
+
|
|
90
|
+
case InstrumentName.cryonirsp:
|
|
91
|
+
self.number_of_modulator_states: int = header[
|
|
92
|
+
CryonirspMetadataKey.number_of_modulator_states
|
|
93
|
+
]
|
|
94
|
+
self.modulator_state: int = header[CryonirspMetadataKey.modulator_state]
|
|
95
|
+
self.arm_id: str = header[CryonirspMetadataKey.arm_id]
|
|
96
|
+
self.cryonirsp_od_filter_name: str = header[
|
|
97
|
+
TrendMetadataKey.cryonirsp_od_filter_name
|
|
98
|
+
]
|
|
99
|
+
self.cryonirsp_grating_angle: float = header[
|
|
100
|
+
TrendMetadataKey.cryonirsp_grating_angle
|
|
101
|
+
]
|
|
102
|
+
self.cryonirsp_grating_littrow_angle: float = header[
|
|
103
|
+
TrendMetadataKey.cryonirsp_grating_littrow_angle
|
|
104
|
+
]
|
|
105
|
+
self.cryonirsp_grating_constant: float = header[
|
|
106
|
+
TrendMetadataKey.cryonirsp_grating_constant
|
|
107
|
+
]
|
|
108
|
+
self.cryonirsp_slit_width: float = header[TrendMetadataKey.cryonirsp_slit_width]
|
|
109
|
+
|
|
110
|
+
case InstrumentName.dlnirsp:
|
|
111
|
+
self.number_of_modulator_states: int = header[
|
|
112
|
+
DlnirspMetadataKey.number_of_modulator_states
|
|
113
|
+
]
|
|
114
|
+
self.modulator_state: int = header[DlnirspMetadataKey.modulator_state]
|
|
115
|
+
self.arm_id: int = header[DlnirspMetadataKey.arm_id]
|
|
116
|
+
self.dlnirsp_grating_angle: float = header[TrendMetadataKey.dlnirsp_grating_angle]
|
|
117
|
+
self.dlnirsp_arm_position: float = header[TrendMetadataKey.dlnirsp_arm_position]
|
|
118
|
+
self.dlnirsp_grating_constant: float = header[
|
|
119
|
+
TrendMetadataKey.dlnirsp_grating_constant
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
case _:
|
|
123
|
+
raise ValueError(f"Did not expect instrument = {self.instrument}")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Task modules and classes that are used to build up a workflow."""
|
|
2
|
+
|
|
3
|
+
from dkist_processing_trend.tasks.dark import *
|
|
4
|
+
from dkist_processing_trend.tasks.gain import *
|
|
5
|
+
from dkist_processing_trend.tasks.parse import *
|
|
6
|
+
from dkist_processing_trend.tasks.run_pac_fitter import *
|
|
7
|
+
from dkist_processing_trend.tasks.trend_output_data import *
|
|
8
|
+
from dkist_processing_trend.tasks.write_trend import *
|
|
9
|
+
|
|
10
|
+
# These imports need to be after all the task imports to avoid circularity
|
|
11
|
+
from dkist_processing_trend.tasks.initialize_arm_tasks import ( # isort: skip
|
|
12
|
+
initialize_cryonirsp_arm_tasks,
|
|
13
|
+
)
|
|
14
|
+
from dkist_processing_trend.tasks.initialize_arm_tasks import ( # isort: skip
|
|
15
|
+
initialize_dlnirsp_arm_tasks,
|
|
16
|
+
)
|
|
17
|
+
from dkist_processing_trend.tasks.initialize_arm_tasks import ( # isort: skip
|
|
18
|
+
initialize_visp_arm_tasks,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Not used directly, but ensures that the common_configurations are logged early in a task run
|
|
22
|
+
from dkist_processing_trend.config import dkist_processing_trend_configurations # isort: skip
|
|
23
|
+
|
|
24
|
+
# Initialize the arm task classes for each instrument
|
|
25
|
+
initialize_visp_arm_tasks()
|
|
26
|
+
initialize_cryonirsp_arm_tasks()
|
|
27
|
+
initialize_dlnirsp_arm_tasks()
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Functions for defining and retreiving 'partial' classes with their `__init__` methods pre-hydrated with an ``arm_id``.
|
|
3
|
+
|
|
4
|
+
This is used to ease the definition of `~dkist_processing_trend.tasks.trend_base.TrendArmTaskBase` with a list of ``arm_ids``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# Inspired by https://stackoverflow.com/questions/38911146/python-equivalent-of-functools-partial-for-a-class-constructor
|
|
8
|
+
|
|
9
|
+
from functools import partialmethod
|
|
10
|
+
from importlib import import_module
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _arm_task_name(cls_name: str, arm_id: str | int) -> str:
|
|
14
|
+
"""Define common logic for smashing a class name and arm_id together."""
|
|
15
|
+
return f"{cls_name}{str(arm_id).capitalize()}"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def initialize_arm_task(cls, arm_id: str | int) -> None:
|
|
19
|
+
"""
|
|
20
|
+
Define a new class that subclasses ``cls`` with its `__init__` pre-populated with the `arm_id` argument.
|
|
21
|
+
|
|
22
|
+
This function also updates the attrs of the module of ``cls`` so that the new class appears to be a member of that
|
|
23
|
+
same module.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
class ArmTaskClass(cls):
|
|
27
|
+
__init__ = partialmethod(cls.__init__, arm_id=arm_id)
|
|
28
|
+
__doc__ = cls.__doc__
|
|
29
|
+
|
|
30
|
+
# Give the class a new name based on the arm_id. This is required for the DAG to keep the same task with different
|
|
31
|
+
# arm_ids separate.
|
|
32
|
+
ArmTaskClass.__name__ = _arm_task_name(cls.__name__, arm_id)
|
|
33
|
+
ArmTaskClass.__qualname__ = _arm_task_name(cls.__qualname__, arm_id)
|
|
34
|
+
|
|
35
|
+
# Modify the import path so the new class can be imported from its parent's module.
|
|
36
|
+
ArmTaskClass.__module__ = cls.__module__
|
|
37
|
+
mod = import_module(cls.__module__)
|
|
38
|
+
setattr(mod, ArmTaskClass.__name__, ArmTaskClass)
|
|
39
|
+
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def retreive_arm_task(cls, arm_id: str | int):
|
|
44
|
+
"""
|
|
45
|
+
Return the "arm task" version of a given ``cls`` and ``arm_id``.
|
|
46
|
+
|
|
47
|
+
In other words, return the arm task class that was created by `initialize_arm_task`.
|
|
48
|
+
"""
|
|
49
|
+
parent_module = import_module(cls.__module__)
|
|
50
|
+
arm_class_name = _arm_task_name(cls.__name__, arm_id)
|
|
51
|
+
try:
|
|
52
|
+
arm_class = getattr(parent_module, arm_class_name)
|
|
53
|
+
except AttributeError as e:
|
|
54
|
+
raise AttributeError(
|
|
55
|
+
f"Can't find {arm_class_name} in {parent_module}. Was it initialized with `initialize_arm_task`?"
|
|
56
|
+
) from e
|
|
57
|
+
return arm_class
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Task for computing the average of the DARK CS steps."""
|
|
2
|
+
|
|
3
|
+
from dkist_processing_common.codecs.fits import fits_array_decoder
|
|
4
|
+
from dkist_processing_common.codecs.fits import fits_array_encoder
|
|
5
|
+
from dkist_processing_math.statistics import average_numpy_arrays
|
|
6
|
+
|
|
7
|
+
from dkist_processing_trend.models.tags import TrendTag
|
|
8
|
+
from dkist_processing_trend.tasks.trend_base import TrendArmTaskBase
|
|
9
|
+
|
|
10
|
+
__all__ = ["DarkCalibration"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DarkCalibration(TrendArmTaskBase):
|
|
14
|
+
"""
|
|
15
|
+
Generate an INTERMEDIATE dark frame by averaging all data from the "dark" CS steps.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
arm_id
|
|
20
|
+
id of the instrument arm to operate on
|
|
21
|
+
|
|
22
|
+
recipe_run_id
|
|
23
|
+
id of the recipe run used to identify the workflow run this task is part of
|
|
24
|
+
|
|
25
|
+
workflow_name
|
|
26
|
+
name of the workflow to which this instance of the task belongs
|
|
27
|
+
|
|
28
|
+
workflow_version
|
|
29
|
+
version of the workflow to which this instance of the task belongs
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
record_provenance = True
|
|
33
|
+
|
|
34
|
+
def run(self):
|
|
35
|
+
"""
|
|
36
|
+
Produce a single, average dark frame from all of the dark CS steps.
|
|
37
|
+
|
|
38
|
+
In other words, collect all frames tagged as POLCAL_DARK, average them together, and write the result to disk.
|
|
39
|
+
"""
|
|
40
|
+
with self.telemetry_span(f"Producing average dark for arm {self.arm_id}"):
|
|
41
|
+
input_dark_arrays = self.read(
|
|
42
|
+
tags=[TrendTag.task_polcal_dark(), TrendTag.arm_id(self.arm_id)],
|
|
43
|
+
decoder=fits_array_decoder,
|
|
44
|
+
)
|
|
45
|
+
averaged_dark_array = average_numpy_arrays(input_dark_arrays)
|
|
46
|
+
|
|
47
|
+
self.write(
|
|
48
|
+
data=averaged_dark_array,
|
|
49
|
+
tags=[
|
|
50
|
+
TrendTag.intermediate(),
|
|
51
|
+
TrendTag.frame(),
|
|
52
|
+
TrendTag.arm_id(self.arm_id),
|
|
53
|
+
TrendTag.task_dark(),
|
|
54
|
+
],
|
|
55
|
+
encoder=fits_array_encoder,
|
|
56
|
+
)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Task for computing the average of the CLEAR CS steps."""
|
|
2
|
+
|
|
3
|
+
from dkist_processing_common.codecs.fits import fits_array_decoder
|
|
4
|
+
from dkist_processing_common.codecs.fits import fits_array_encoder
|
|
5
|
+
from dkist_processing_math.arithmetic import subtract_array_from_arrays
|
|
6
|
+
from dkist_processing_math.statistics import average_numpy_arrays
|
|
7
|
+
|
|
8
|
+
from dkist_processing_trend.models.tags import TrendTag
|
|
9
|
+
from dkist_processing_trend.tasks.trend_base import TrendArmTaskBase
|
|
10
|
+
|
|
11
|
+
__all__ = ["GainCalibration"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GainCalibration(TrendArmTaskBase):
|
|
15
|
+
"""
|
|
16
|
+
Generate an INTERMEDIATE gain frame by averaging all data from the "clear" CS steps.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
arm_id
|
|
21
|
+
id of the instrument arm to operate on
|
|
22
|
+
|
|
23
|
+
recipe_run_id
|
|
24
|
+
id of the recipe run used to identify the workflow run this task is part of
|
|
25
|
+
|
|
26
|
+
workflow_name
|
|
27
|
+
name of the workflow to which this instance of the task belongs
|
|
28
|
+
|
|
29
|
+
workflow_version
|
|
30
|
+
version of the workflow to which this instance of the task belongs
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
record_provenance = True
|
|
34
|
+
|
|
35
|
+
def run(self):
|
|
36
|
+
"""
|
|
37
|
+
Produce a single, average gain frame from all of the clear CS steps.
|
|
38
|
+
|
|
39
|
+
In other words, collect all frames tagged as POLCAL_GAIN, average them together, subtract the INTERMEDIATE DARK
|
|
40
|
+
signal, and write the result to disk.
|
|
41
|
+
"""
|
|
42
|
+
with self.telemetry_span(f"Producing average gain for arm {self.arm_id}"):
|
|
43
|
+
input_gain_arrays = self.read(
|
|
44
|
+
tags=[TrendTag.task_polcal_gain(), TrendTag.arm_id(self.arm_id)],
|
|
45
|
+
decoder=fits_array_decoder,
|
|
46
|
+
)
|
|
47
|
+
averaged_gain_array = average_numpy_arrays(input_gain_arrays)
|
|
48
|
+
|
|
49
|
+
dark_array = next(
|
|
50
|
+
self.read(
|
|
51
|
+
tags=[
|
|
52
|
+
TrendTag.intermediate(),
|
|
53
|
+
TrendTag.frame(),
|
|
54
|
+
TrendTag.task_dark(),
|
|
55
|
+
TrendTag.arm_id(self.arm_id),
|
|
56
|
+
],
|
|
57
|
+
decoder=fits_array_decoder,
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
final_gain = next(
|
|
62
|
+
subtract_array_from_arrays(arrays=averaged_gain_array, array_to_subtract=dark_array)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
self.write(
|
|
66
|
+
data=final_gain,
|
|
67
|
+
tags=[
|
|
68
|
+
TrendTag.intermediate(),
|
|
69
|
+
TrendTag.frame(),
|
|
70
|
+
TrendTag.arm_id(self.arm_id),
|
|
71
|
+
TrendTag.task_gain(),
|
|
72
|
+
],
|
|
73
|
+
encoder=fits_array_encoder,
|
|
74
|
+
)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Initialize "arm class" versions of all the arm-specific tasks needed by each instrument.
|
|
3
|
+
|
|
4
|
+
The functions in this module need to be called prior to importing the respective instrument's workflow.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dkist_processing_trend.models.instrument import CryonirspArms
|
|
8
|
+
from dkist_processing_trend.models.instrument import DlnirspArms
|
|
9
|
+
from dkist_processing_trend.models.instrument import VispArms
|
|
10
|
+
from dkist_processing_trend.tasks.arm_task_factory import initialize_arm_task
|
|
11
|
+
from dkist_processing_trend.tasks.dark import DarkCalibration
|
|
12
|
+
from dkist_processing_trend.tasks.gain import GainCalibration
|
|
13
|
+
from dkist_processing_trend.tasks.run_pac_fitter import RunPacFitter
|
|
14
|
+
from dkist_processing_trend.tasks.visp.visp_dmpd import VispDemodulatedPolcalData
|
|
15
|
+
from dkist_processing_trend.tasks.visp.visp_geometric import VispGeometricCalibration
|
|
16
|
+
from dkist_processing_trend.tasks.visp.visp_prep_fit_data import VispPrepareFitData
|
|
17
|
+
from dkist_processing_trend.tasks.visp.visp_process_demod import VispProcessDemodulationMatrices
|
|
18
|
+
|
|
19
|
+
VISP_ARM_TASKS = [
|
|
20
|
+
DarkCalibration,
|
|
21
|
+
GainCalibration,
|
|
22
|
+
VispGeometricCalibration,
|
|
23
|
+
VispPrepareFitData,
|
|
24
|
+
RunPacFitter,
|
|
25
|
+
VispProcessDemodulationMatrices,
|
|
26
|
+
VispDemodulatedPolcalData,
|
|
27
|
+
]
|
|
28
|
+
CRYONIRSP_ARM_TASKS = [DarkCalibration, GainCalibration, RunPacFitter]
|
|
29
|
+
DLNIRSP_ARM_TASKS = [DarkCalibration, GainCalibration, RunPacFitter]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def initialize_visp_arm_tasks():
|
|
33
|
+
"""Create arm versions of all arm-specific ViSP tasks."""
|
|
34
|
+
for arm_id in VispArms:
|
|
35
|
+
for task_class in VISP_ARM_TASKS:
|
|
36
|
+
initialize_arm_task(task_class, arm_id=arm_id)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def initialize_cryonirsp_arm_tasks():
|
|
40
|
+
"""Create arm versions of all arm-specific CryoNIRSP tasks."""
|
|
41
|
+
for arm_id in CryonirspArms:
|
|
42
|
+
for task_class in CRYONIRSP_ARM_TASKS:
|
|
43
|
+
initialize_arm_task(task_class, arm_id=arm_id)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def initialize_dlnirsp_arm_tasks():
|
|
47
|
+
"""Create arm versions of all arm-specific DLNIRSP tasks."""
|
|
48
|
+
for arm_id in DlnirspArms:
|
|
49
|
+
for task_class in DLNIRSP_ARM_TASKS:
|
|
50
|
+
initialize_arm_task(task_class, arm_id=arm_id)
|