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.
Files changed (66) hide show
  1. changelog/.gitempty +0 -0
  2. dkist_processing_trend/__init__.py +10 -0
  3. dkist_processing_trend/config.py +11 -0
  4. dkist_processing_trend/models/__init__.py +1 -0
  5. dkist_processing_trend/models/constants.py +143 -0
  6. dkist_processing_trend/models/fit_options.py +15 -0
  7. dkist_processing_trend/models/fits_access.py +65 -0
  8. dkist_processing_trend/models/instrument.py +35 -0
  9. dkist_processing_trend/models/instrument_options.py +35 -0
  10. dkist_processing_trend/models/parameters.py +134 -0
  11. dkist_processing_trend/models/tags.py +117 -0
  12. dkist_processing_trend/models/task_name.py +20 -0
  13. dkist_processing_trend/parsers/__init__.py +1 -0
  14. dkist_processing_trend/parsers/arm_id.py +103 -0
  15. dkist_processing_trend/parsers/instrument_unique_bud.py +40 -0
  16. dkist_processing_trend/parsers/time.py +27 -0
  17. dkist_processing_trend/parsers/trend_l0_fits_access.py +123 -0
  18. dkist_processing_trend/tasks/__init__.py +27 -0
  19. dkist_processing_trend/tasks/arm_task_factory.py +57 -0
  20. dkist_processing_trend/tasks/dark.py +56 -0
  21. dkist_processing_trend/tasks/gain.py +74 -0
  22. dkist_processing_trend/tasks/initialize_arm_tasks.py +50 -0
  23. dkist_processing_trend/tasks/parse.py +169 -0
  24. dkist_processing_trend/tasks/prepare_fit_data_base.py +257 -0
  25. dkist_processing_trend/tasks/run_pac_fitter.py +392 -0
  26. dkist_processing_trend/tasks/trend_base.py +97 -0
  27. dkist_processing_trend/tasks/trend_output_data.py +167 -0
  28. dkist_processing_trend/tasks/visp/__init__.py +6 -0
  29. dkist_processing_trend/tasks/visp/visp_dmpd.py +410 -0
  30. dkist_processing_trend/tasks/visp/visp_extract_beam.py +14 -0
  31. dkist_processing_trend/tasks/visp/visp_geometric.py +260 -0
  32. dkist_processing_trend/tasks/visp/visp_prep_fit_data.py +162 -0
  33. dkist_processing_trend/tasks/visp/visp_process_demod.py +236 -0
  34. dkist_processing_trend/tasks/write_trend.py +663 -0
  35. dkist_processing_trend/tests/__init__.py +1 -0
  36. dkist_processing_trend/tests/conftest.py +718 -0
  37. dkist_processing_trend/tests/local_trial_workflows/__init__.py +0 -0
  38. dkist_processing_trend/tests/local_trial_workflows/l0_to_trend_visp_polcal.py +294 -0
  39. dkist_processing_trend/tests/local_trial_workflows/local_trial_helpers.py +488 -0
  40. dkist_processing_trend/tests/test_arm_task_factory.py +82 -0
  41. dkist_processing_trend/tests/test_base_tasks.py +86 -0
  42. dkist_processing_trend/tests/test_constants.py +120 -0
  43. dkist_processing_trend/tests/test_dark.py +97 -0
  44. dkist_processing_trend/tests/test_gain.py +135 -0
  45. dkist_processing_trend/tests/test_parameters.py +149 -0
  46. dkist_processing_trend/tests/test_parse.py +276 -0
  47. dkist_processing_trend/tests/test_prep_fit_data_base.py +233 -0
  48. dkist_processing_trend/tests/test_publish_catalog_messages.py +45 -0
  49. dkist_processing_trend/tests/test_run_pac_fitter.py +371 -0
  50. dkist_processing_trend/tests/test_stems.py +75 -0
  51. dkist_processing_trend/tests/test_transfer_output_data.py +76 -0
  52. dkist_processing_trend/tests/test_trend_fits_access.py +173 -0
  53. dkist_processing_trend/tests/test_visp.py +874 -0
  54. dkist_processing_trend/tests/test_workflows.py +10 -0
  55. dkist_processing_trend/tests/test_write_trend.py +460 -0
  56. dkist_processing_trend/workflows/__init__.py +3 -0
  57. dkist_processing_trend/workflows/visp.py +58 -0
  58. dkist_processing_trend-0.1.0.dist-info/METADATA +549 -0
  59. dkist_processing_trend-0.1.0.dist-info/RECORD +66 -0
  60. dkist_processing_trend-0.1.0.dist-info/WHEEL +5 -0
  61. dkist_processing_trend-0.1.0.dist-info/top_level.txt +3 -0
  62. docs/conf.py +57 -0
  63. docs/index.rst +10 -0
  64. docs/l0_to_trend_visp_polcal.rst +4 -0
  65. docs/landing_page.rst +11 -0
  66. 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)