dkist-processing-vbi 1.17.4__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_vbi/__init__.py +9 -0
  3. dkist_processing_vbi/config.py +12 -0
  4. dkist_processing_vbi/models/__init__.py +1 -0
  5. dkist_processing_vbi/models/constants.py +88 -0
  6. dkist_processing_vbi/models/filter.py +47 -0
  7. dkist_processing_vbi/models/tags.py +32 -0
  8. dkist_processing_vbi/parsers/__init__.py +1 -0
  9. dkist_processing_vbi/parsers/mosaic_repeats.py +167 -0
  10. dkist_processing_vbi/parsers/spatial_step_pattern.py +43 -0
  11. dkist_processing_vbi/parsers/vbi_l0_fits_access.py +36 -0
  12. dkist_processing_vbi/parsers/vbi_l1_fits_access.py +33 -0
  13. dkist_processing_vbi/tasks/__init__.py +10 -0
  14. dkist_processing_vbi/tasks/assemble_movie.py +87 -0
  15. dkist_processing_vbi/tasks/dark.py +105 -0
  16. dkist_processing_vbi/tasks/gain.py +161 -0
  17. dkist_processing_vbi/tasks/make_movie_frames.py +368 -0
  18. dkist_processing_vbi/tasks/mixin/__init__.py +1 -0
  19. dkist_processing_vbi/tasks/mixin/intermediate_loaders.py +74 -0
  20. dkist_processing_vbi/tasks/parse.py +78 -0
  21. dkist_processing_vbi/tasks/process_summit_processed.py +62 -0
  22. dkist_processing_vbi/tasks/quality_metrics.py +70 -0
  23. dkist_processing_vbi/tasks/science.py +131 -0
  24. dkist_processing_vbi/tasks/trial_output_data.py +51 -0
  25. dkist_processing_vbi/tasks/vbi_base.py +30 -0
  26. dkist_processing_vbi/tasks/write_l1.py +155 -0
  27. dkist_processing_vbi/tests/__init__.py +0 -0
  28. dkist_processing_vbi/tests/conftest.py +277 -0
  29. dkist_processing_vbi/tests/local_trial_workflows/__init__.py +0 -0
  30. dkist_processing_vbi/tests/local_trial_workflows/l0_to_l1.py +245 -0
  31. dkist_processing_vbi/tests/test_assemble_movie.py +59 -0
  32. dkist_processing_vbi/tests/test_dark.py +123 -0
  33. dkist_processing_vbi/tests/test_gain.py +109 -0
  34. dkist_processing_vbi/tests/test_intermediate_loaders.py +105 -0
  35. dkist_processing_vbi/tests/test_make_movie_frames.py +263 -0
  36. dkist_processing_vbi/tests/test_parse_l0.py +338 -0
  37. dkist_processing_vbi/tests/test_parse_summit.py +141 -0
  38. dkist_processing_vbi/tests/test_process_summit.py +210 -0
  39. dkist_processing_vbi/tests/test_quality_metrics.py +139 -0
  40. dkist_processing_vbi/tests/test_science.py +167 -0
  41. dkist_processing_vbi/tests/test_spatial_step_pattern.py +74 -0
  42. dkist_processing_vbi/tests/test_vbi_base.py +37 -0
  43. dkist_processing_vbi/tests/test_vbi_constants.py +145 -0
  44. dkist_processing_vbi/tests/test_workflows.py +9 -0
  45. dkist_processing_vbi/tests/test_write_l1.py +191 -0
  46. dkist_processing_vbi/workflows/__init__.py +2 -0
  47. dkist_processing_vbi/workflows/l0_processing.py +69 -0
  48. dkist_processing_vbi/workflows/summit_data_processing.py +64 -0
  49. dkist_processing_vbi/workflows/trial_workflows.py +128 -0
  50. dkist_processing_vbi-1.17.4.dist-info/METADATA +186 -0
  51. dkist_processing_vbi-1.17.4.dist-info/RECORD +66 -0
  52. dkist_processing_vbi-1.17.4.dist-info/WHEEL +5 -0
  53. dkist_processing_vbi-1.17.4.dist-info/top_level.txt +4 -0
  54. docs/Makefile +134 -0
  55. docs/changelog.rst +11 -0
  56. docs/conf.py +52 -0
  57. docs/index.rst +15 -0
  58. docs/l0_to_l1_vbi_no-speckle-full-trial.rst +11 -0
  59. docs/l0_to_l1_vbi_no-speckle.rst +11 -0
  60. docs/l0_to_l1_vbi_summit-calibrated-full-trial.rst +11 -0
  61. docs/l0_to_l1_vbi_summit-calibrated.rst +10 -0
  62. docs/make.bat +170 -0
  63. docs/requirements.txt +1 -0
  64. docs/requirements_table.rst +8 -0
  65. docs/scientific_changelog.rst +10 -0
  66. licenses/LICENSE.rst +11 -0
changelog/.gitempty ADDED
File without changes
@@ -0,0 +1,9 @@
1
+ """Package providing support classes and methods used by all workflow tasks."""
2
+ from importlib.metadata import PackageNotFoundError
3
+ from importlib.metadata import version
4
+
5
+ try:
6
+ __version__ = version(distribution_name=__name__)
7
+ except PackageNotFoundError:
8
+ # package is not installed
9
+ __version__ = "unknown"
@@ -0,0 +1,12 @@
1
+ """Configuration for the dkist-processing-vbi package and the logging thereof."""
2
+ from dkist_processing_common.config import DKISTProcessingCommonConfiguration
3
+
4
+
5
+ class DKISTProcessingVBIConfigurations(DKISTProcessingCommonConfiguration):
6
+ """Configurations custom to the dkist-processing-vbi package."""
7
+
8
+ pass # nothing custom yet
9
+
10
+
11
+ dkist_processing_vbi_configurations = DKISTProcessingVBIConfigurations()
12
+ dkist_processing_vbi_configurations.log_configurations()
@@ -0,0 +1 @@
1
+ """init."""
@@ -0,0 +1,88 @@
1
+ """VBI additions to common constants."""
2
+ from enum import Enum
3
+
4
+ from dkist_processing_common.models.constants import ConstantsBase
5
+
6
+
7
+ class VbiBudName(Enum):
8
+ """Names to be used in VBI buds."""
9
+
10
+ num_mosaics_repeats = "NUM_MOSAIC_REPEATS"
11
+ spatial_step_pattern = "SPATIAL_STEP_PATTERN"
12
+ num_spatial_steps = "NUM_SPATIAL_STEPS"
13
+ gain_exposure_times = "GAIN_EXPOSURE_TIMES"
14
+ observe_exposure_times = "OBSERVE_EXPOSURE_TIMES"
15
+
16
+
17
+ class VbiConstants(ConstantsBase):
18
+ """VBI specific constants to add to the common constants."""
19
+
20
+ @property
21
+ def num_mosaic_repeats(self) -> int:
22
+ """Return the number of times the full mosaic is repeated."""
23
+ return self._db_dict[VbiBudName.num_mosaics_repeats.value]
24
+
25
+ @property
26
+ def mindices_of_mosaic_field_positions(self) -> list[tuple[int, int]]:
27
+ """
28
+ Define the mapping from mosaic field position to (MINDEX1, MINDEX2) tuple.
29
+
30
+ The mosaic fields are defined as:
31
+
32
+ BLUE RED SINGLE
33
+ 1 2 3 1 2
34
+ 4 5 6 5
35
+ 7 8 9 3 4
36
+
37
+ Thus, the Nth index of this list gives the (MINDEX1, MINDEX2) tuple for the Nth mosaic field (a dummy 0th
38
+ element is provided so that the 1-indexing of the mosaic fields is preserved).
39
+ """
40
+ # fmt: off
41
+ # A dummy value goes in the 0th position because the mosaic fields are 1-indexed. This means we can
42
+ # simply slice this list with the mosaic field number.
43
+ blue_mosiac = ["index_placeholder",
44
+ (1, 1), (1, 2), (1, 3),
45
+ (2, 1), (2, 2), (2, 3),
46
+ (3, 1), (3, 2), (3, 3),
47
+ ]
48
+ red_mosiac = ["index_placeholder",
49
+ (1, 1), (1, 2),
50
+ (2, 1), (2, 2),
51
+ ]
52
+ single = ["index_placeholder"] * 5 + [(1, 1)]
53
+ # fmt: on
54
+ match len(self.spatial_step_pattern):
55
+ case 1:
56
+ return single
57
+
58
+ case 4:
59
+ return red_mosiac
60
+
61
+ case 9:
62
+ return blue_mosiac
63
+
64
+ case _:
65
+ raise ValueError(
66
+ f"Spatial step pattern {self.spatial_step_pattern} describes an unknown mosaic. Parsing should have caught this."
67
+ )
68
+
69
+ @property
70
+ def spatial_step_pattern(self) -> list[int]:
71
+ """Return a parsed list of the spatial step pattern used to scan the mosaic FOV."""
72
+ raw_list = self._db_dict[VbiBudName.spatial_step_pattern.value]
73
+ return [int(val) for val in raw_list.split(",")]
74
+
75
+ @property
76
+ def num_spatial_steps(self) -> int:
77
+ """Spatial steps in a raster."""
78
+ return self._db_dict[VbiBudName.num_spatial_steps.value]
79
+
80
+ @property
81
+ def gain_exposure_times(self) -> [float]:
82
+ """Exposure times of gain frames."""
83
+ return self._db_dict[VbiBudName.gain_exposure_times.value]
84
+
85
+ @property
86
+ def observe_exposure_times(self) -> [float]:
87
+ """Exposure times of observe frames."""
88
+ return self._db_dict[VbiBudName.observe_exposure_times.value]
@@ -0,0 +1,47 @@
1
+ """VBI filter list and tooling."""
2
+ import astropy.units as u
3
+ from dkist_processing_common.models.wavelength import WavelengthRange
4
+
5
+
6
+ class Filter(WavelengthRange):
7
+ """
8
+ VBI filter data structure.
9
+
10
+ Parameters
11
+ ----------
12
+ name
13
+ The name of the filter
14
+ """
15
+
16
+ name: str
17
+
18
+
19
+ VBI_FILTERS = [
20
+ Filter(name="VBI-Blue Ca II K", min=393.276 * u.nm, max=393.378 * u.nm),
21
+ Filter(name="VBI-Blue G-Band", min=430.301 * u.nm, max=430.789 * u.nm),
22
+ Filter(name="VBI-Blue Continuum", min=450.084 * u.nm, max=450.490 * u.nm),
23
+ Filter(name="VBI-Blue H-Beta", min=486.116 * u.nm, max=486.162 * u.nm),
24
+ Filter(name="VBI-Red H-alpha", min=656.258 * u.nm, max=656.306 * u.nm),
25
+ Filter(name="VBI-Red Continuum", min=668.202 * u.nm, max=668.644 * u.nm),
26
+ Filter(name="VBI-Red Ti O", min=705.545 * u.nm, max=706.133 * u.nm),
27
+ Filter(name="VBI-Red Fe IX", min=789.168 * u.nm, max=789.204 * u.nm),
28
+ ]
29
+
30
+
31
+ def find_associated_filter(wavelength: u.Quantity) -> Filter:
32
+ """
33
+ Given a wavelength, find the Filter that contains that wavelength between its wavemin/wavemax.
34
+
35
+ Parameters
36
+ ----------
37
+ wavelength
38
+ The wavelength to use in the search
39
+
40
+ Returns
41
+ -------
42
+ A Filter object that contains the wavelength
43
+ """
44
+ matching_filters = [f for f in VBI_FILTERS if f.min <= wavelength <= f.max]
45
+ if len(matching_filters) == 1:
46
+ return matching_filters[0]
47
+ raise ValueError(f"Found {len(matching_filters)} matching filters when 1 was expected.")
@@ -0,0 +1,32 @@
1
+ """VBI tags."""
2
+ from enum import Enum
3
+
4
+ from dkist_processing_common.models.tags import Tag
5
+
6
+
7
+ class VbiStemName(str, Enum):
8
+ """VBI specific tag stems."""
9
+
10
+ current_spatial_step = "STEP"
11
+ current_mosaic = "MOSAIC"
12
+
13
+
14
+ class VbiTag(Tag):
15
+ """VBI specific tag formatting."""
16
+
17
+ @classmethod
18
+ def spatial_step(cls, step_num: int) -> str:
19
+ """
20
+ Tags by spatial step.
21
+
22
+ Parameters
23
+ ----------
24
+ step_num: int
25
+ The step number in the FOV
26
+ """
27
+ return cls.format_tag(VbiStemName.current_spatial_step, step_num)
28
+
29
+ @classmethod
30
+ def mosaic(cls, mosaic_num: int) -> str:
31
+ """Tags by mosaic number."""
32
+ return cls.format_tag(VbiStemName.current_mosaic, mosaic_num)
@@ -0,0 +1 @@
1
+ """init."""
@@ -0,0 +1,167 @@
1
+ """Stems for organizing files based on their Mosaic repeat number."""
2
+ from __future__ import annotations
3
+
4
+ from abc import ABC
5
+ from collections import defaultdict
6
+ from functools import cached_property
7
+ from typing import Type
8
+
9
+ from astropy.time import Time
10
+ from dkist_processing_common.models.flower_pot import SpilledDirt
11
+ from dkist_processing_common.models.flower_pot import Stem
12
+ from dkist_processing_common.models.task_name import TaskName
13
+
14
+ from dkist_processing_vbi.models.constants import VbiBudName
15
+ from dkist_processing_vbi.models.tags import VbiStemName
16
+ from dkist_processing_vbi.parsers.vbi_l0_fits_access import VbiL0FitsAccess
17
+
18
+
19
+ class SingleMosaicTile:
20
+ """
21
+ An object that uniquely defines a (mosaic_step, exp_num, time_obs) tuple from any number of mosaic repeats.
22
+
23
+ This is just a fancy tuple.
24
+
25
+ Basically, it just hashes the (mosaic_step, exp_num, time_obs) tuple so these objects can easily be compared.
26
+ Also uses the time_obs property so that multiple DSPS repeats of the same (mosaic_step, modstate) can be sorted.
27
+
28
+ This is just a fancy tuple.
29
+ """
30
+
31
+ def __init__(self, fits_obj: VbiL0FitsAccess):
32
+ """Read mosaic step, exp_num, and obs time information from a FitsAccess object."""
33
+ self.mosaic_step = fits_obj.current_spatial_step
34
+ self.exposure_num = fits_obj.current_mosaic_step_exp
35
+ self.date_obs = Time(fits_obj.time_obs)
36
+
37
+ def __repr__(self):
38
+ return f"SingleMosaicTile with {self.mosaic_step = }, {self.exposure_num = }, and {self.date_obs = }"
39
+
40
+ def __eq__(self, other: SingleMosaicTile) -> bool:
41
+ """Two frames are equal if they have the same (mosaic_step, exp_num, date_obs) tuple."""
42
+ if not isinstance(other, SingleMosaicTile):
43
+ raise TypeError(f"Cannot compare SingleMosaicTile with type {type(other)}")
44
+
45
+ for attr in ["mosaic_step", "exposure_num", "date_obs"]:
46
+ if getattr(self, attr) != getattr(other, attr):
47
+ return False
48
+
49
+ return True
50
+
51
+ def __lt__(self, other: SingleMosaicTile) -> bool:
52
+ """Only sort on date_obs."""
53
+ return self.date_obs < other.date_obs
54
+
55
+ def __hash__(self) -> int:
56
+ # Not strictly necessary, but does allow for using set() on these objects
57
+ return hash((self.mosaic_step, self.exposure_num, self.date_obs))
58
+
59
+
60
+ class MosaicBase(Stem, ABC):
61
+ """Base class for Stems that use a dict of [int, Dict[int, SingleMosaicTile]] to parse mosaic tiles."""
62
+
63
+ # This only here so type-hinting of this complex dictionary will work.
64
+ key_to_petal_dict: dict[str, SingleMosaicTile]
65
+
66
+ @cached_property
67
+ def mosaic_tile_dict(self) -> dict[int, dict[int, list[SingleMosaicTile]]]:
68
+ """Nested dictionary that contains a SingleMosaicTile for each ingested frame.
69
+
70
+ Dictionary structure is [mosaic_step (int), Dict[exp_num (int), List[SingleMosaicTile]]
71
+ """
72
+ scan_step_dict = defaultdict(lambda: defaultdict(list))
73
+ for scan_step_obj in self.key_to_petal_dict.values():
74
+ scan_step_dict[scan_step_obj.mosaic_step][scan_step_obj.exposure_num].append(
75
+ scan_step_obj
76
+ )
77
+
78
+ return scan_step_dict
79
+
80
+ def setter(self, fits_obj: VbiL0FitsAccess) -> SingleMosaicTile | Type[SpilledDirt]:
81
+ """Ingest observe frames as SingleMosaicTile objects."""
82
+ if fits_obj.ip_task_type.casefold() != TaskName.observe.value.casefold():
83
+ return SpilledDirt
84
+ return SingleMosaicTile(fits_obj=fits_obj)
85
+
86
+
87
+ class MosaicRepeatNumberFlower(MosaicBase):
88
+ """Flower for computing and assigning mosaic repeat numbers.
89
+
90
+ We can't use DKIST009 directly because subcycling in the instrument program task breaks the 1-to-1 mapping between
91
+ DSPS repeat and mosaic repeat
92
+ """
93
+
94
+ def __init__(self):
95
+ super().__init__(stem_name=VbiStemName.current_mosaic.value)
96
+
97
+ def getter(self, key: str) -> int:
98
+ """Compute the mosaic repeat number for a single frame.
99
+
100
+ A list of all the frames associated with a single (mosaic_step, exp_num) tuple is sorted by date and that list
101
+ is used to find the index of the frame in question.
102
+ """
103
+ mosaic_tile_obj = self.key_to_petal_dict[key]
104
+ all_obj_for_tile = sorted(
105
+ self.mosaic_tile_dict[mosaic_tile_obj.mosaic_step][mosaic_tile_obj.exposure_num]
106
+ )
107
+
108
+ num_files = all_obj_for_tile.count(mosaic_tile_obj)
109
+ if num_files > 1:
110
+ raise ValueError(
111
+ f"More than one file found for a single (mosaic_step, exp_num). Randomly selected example has {num_files} files."
112
+ )
113
+
114
+ # Here is where we decide that mosaic repeat numbers start at 1
115
+ return all_obj_for_tile.index(mosaic_tile_obj) + 1
116
+
117
+
118
+ class TotalMosaicRepeatsBud(MosaicBase):
119
+ """Compute the total number of *complete* VBI Mosaics.
120
+
121
+ Note that an IP with only one camera position is still considered a "mosaic".
122
+
123
+ We can't use DKIST008 directly for two reasons:
124
+
125
+ 1. Subcycling in the instrument program task breaks the 1-to-1 mapping between DSPS repeat and mosaic repeat
126
+
127
+ 2. It's possible that the last repeat has an aborted mosaic. Instead, we return the number of completed mosaics found.
128
+ """
129
+
130
+ def __init__(self):
131
+ super().__init__(stem_name=VbiBudName.num_mosaics_repeats.value)
132
+
133
+ def getter(self, key: str) -> int:
134
+ """Compute the total number of mosaic repeats.
135
+
136
+ The number of mosaic repeats for every camera position are calculated and if a mosaic is incomplete,
137
+ it will not be included.
138
+ Assumes the incomplete mosaic is always the last one due to summit abort or cancellation.
139
+ """
140
+ # HOW THIS WORKS
141
+ ################
142
+ # self.mosaic_tile_dict conceptually looks like this:
143
+ # {mosaic_1:
144
+ # {exp_1: [file1, file2],
145
+ # exp_2: [file3, file4]},
146
+ # mosaic_2:
147
+ # {exp_1: [file5, file6],
148
+ # exp_2: [file7, file7]}}
149
+ #
150
+ # We assume that each file for a (mosaic_step, exp_num) tuple is a different mosaic repeat
151
+ # (there are 2 repeats in the above example). So all we really need to do is find the lengths of all
152
+ # of the lists at the "exp_N" level.
153
+
154
+ # The k[0] assumes that a mosaic step has the same number of exposures for all DSPS repeats
155
+ repeats_per_mosaic_tile = [
156
+ k[0]
157
+ # The following list is the number of files found for each mosaic location for each exp_num
158
+ for k in [
159
+ # exp_dict is a dict of {exp_num: list(SingleMosaicTile)}
160
+ # so len(m) is the number of SingleMosaicTiles detected for each exp_num
161
+ [len(m) for m in exp_dict.values()]
162
+ for exp_dict in self.mosaic_tile_dict.values()
163
+ ]
164
+ ]
165
+ if min(repeats_per_mosaic_tile) + 1 < max(repeats_per_mosaic_tile):
166
+ raise ValueError("More than one incomplete mosaic exists in the data.")
167
+ return min(repeats_per_mosaic_tile)
@@ -0,0 +1,43 @@
1
+ """Bud for checking that the spatial step pattern (VBISTPAT) matches expectations."""
2
+ from dkist_processing_common.models.task_name import TaskName
3
+ from dkist_processing_common.parsers.unique_bud import TaskUniqueBud
4
+
5
+ from dkist_processing_vbi.models.constants import VbiBudName
6
+
7
+
8
+ class SpatialStepPatternBud(TaskUniqueBud):
9
+ """
10
+ Bud for checking and returning the VBI spatial step pattern.
11
+
12
+ This is just a `TaskUniqueBud` that performs the following checks on the unique value:
13
+
14
+ 1. The step pattern must describe either a 1x1, 2x2, or 3x3 grid. This means it must have 1, 4, or 9 elements.
15
+
16
+ 2. The step pattern cannot be empty.
17
+ """
18
+
19
+ def __init__(self):
20
+ super().__init__(
21
+ constant_name=VbiBudName.spatial_step_pattern.value,
22
+ metadata_key="spatial_step_pattern",
23
+ ip_task_type=TaskName.observe.value,
24
+ )
25
+
26
+ def getter(self, key):
27
+ """
28
+ Get a unique value and ensure it describes a valid step pattern.
29
+
30
+ See this Bud's class docstring for more information.
31
+ """
32
+ spatial_step_str = super().getter(key)
33
+ pos_list = spatial_step_str.split(",")
34
+ num_positions = len(pos_list)
35
+
36
+ # We need the check of pos_list[0] because `split` will always return a single element list
37
+ if num_positions not in [1, 4, 9] or pos_list[0] == "":
38
+ raise ValueError(
39
+ f'Spatial step pattern "{spatial_step_str}" does not represent either a 1x1, 2x2, or 3x3 mosaic. '
40
+ f"We don't know how to deal with this."
41
+ )
42
+
43
+ return spatial_step_str
@@ -0,0 +1,36 @@
1
+ """VBI FITS access for L0 data."""
2
+ from astropy.io import fits
3
+ from dkist_processing_common.parsers.l0_fits_access import L0FitsAccess
4
+
5
+
6
+ class VbiL0FitsAccess(L0FitsAccess):
7
+ """
8
+ Class to provide easy access to L0 headers.
9
+
10
+ i.e. instead of <VbiL0FitsAccess>.header['key'] this class lets us use <VbiL0FitsAccess>.key instead
11
+
12
+ Parameters
13
+ ----------
14
+ hdu :
15
+ Fits L0 header object
16
+
17
+ name : str
18
+ The name of the file that was loaded into this FitsAccess object
19
+
20
+ auto_squeeze : bool
21
+ When set to True, dimensions of length 1 will be removed from the array
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ hdu: fits.ImageHDU | fits.PrimaryHDU | fits.CompImageHDU,
27
+ name: str | None = None,
28
+ auto_squeeze: bool = True,
29
+ ):
30
+ super().__init__(hdu=hdu, name=name, auto_squeeze=auto_squeeze)
31
+
32
+ self.spatial_step_pattern: str = self.header["VBISTPAT"]
33
+ self.number_of_spatial_steps: int = self.header.get("VBINSTP")
34
+ self.current_spatial_step: int = self.header.get("VBISTP")
35
+ self.number_of_exp_per_mosaic_step: int = self.header.get("VBINFRAM")
36
+ self.current_mosaic_step_exp: int = self.header.get("VBICFRAM")
@@ -0,0 +1,33 @@
1
+ """VBI FITS access for L1 data."""
2
+ from astropy.io import fits
3
+ from dkist_processing_common.parsers.l1_fits_access import L1FitsAccess
4
+
5
+
6
+ class VbiL1FitsAccess(L1FitsAccess):
7
+ """
8
+ Class to provide easy access to L1 headers.
9
+
10
+ i.e. instead of <VbiL1FitsAccess>.header['key'] this class lets us use <VbiL1FitsAccess>.key instead
11
+
12
+ Parameters
13
+ ----------
14
+ hdu :
15
+ Fits L1 header object
16
+
17
+ name : str
18
+ The name of the file that was loaded into this FitsAccess object
19
+
20
+ auto_squeeze : bool
21
+ When set to True, dimensions of length 1 will be removed from the array
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ hdu: fits.ImageHDU | fits.PrimaryHDU | fits.CompImageHDU,
27
+ name: str | None = None,
28
+ auto_squeeze: bool = True,
29
+ ):
30
+ super().__init__(hdu=hdu, name=name, auto_squeeze=auto_squeeze)
31
+
32
+ self.number_of_spatial_steps: int = self.header.get("VBINSTP")
33
+ self.current_spatial_step: int = self.header.get("VBISTP")
@@ -0,0 +1,10 @@
1
+ """init."""
2
+ from dkist_processing_vbi.tasks.assemble_movie import *
3
+ from dkist_processing_vbi.tasks.dark import *
4
+ from dkist_processing_vbi.tasks.gain import *
5
+ from dkist_processing_vbi.tasks.make_movie_frames import *
6
+ from dkist_processing_vbi.tasks.parse import *
7
+ from dkist_processing_vbi.tasks.process_summit_processed import *
8
+ from dkist_processing_vbi.tasks.quality_metrics import *
9
+ from dkist_processing_vbi.tasks.science import *
10
+ from dkist_processing_vbi.tasks.write_l1 import *
@@ -0,0 +1,87 @@
1
+ """VBI-specific assemble movie task subclass."""
2
+ from typing import Type
3
+
4
+ import numpy as np
5
+ from astropy.visualization import ZScaleInterval
6
+ from dkist_processing_common.models.constants import ConstantsBase
7
+ from dkist_processing_common.tasks import AssembleMovie
8
+ from matplotlib import colormaps
9
+ from PIL import ImageDraw
10
+
11
+ from dkist_processing_vbi.models.constants import VbiConstants
12
+ from dkist_processing_vbi.models.tags import VbiTag
13
+ from dkist_processing_vbi.parsers.vbi_l1_fits_access import VbiL1FitsAccess
14
+
15
+ __all__ = ["AssembleVbiMovie"]
16
+
17
+
18
+ class AssembleVbiMovie(AssembleMovie):
19
+ """
20
+ Class for assembling pre-made movie frames (as FITS/numpy) into an mp4 movie file.
21
+
22
+ Subclassed from the AssembleMovie task in dkist_processing_common to add VBI specific text overlays.
23
+
24
+ Parameters
25
+ ----------
26
+ recipe_run_id : int
27
+ id of the recipe run used to identify the workflow run this task is part of
28
+ workflow_name : str
29
+ name of the workflow to which this instance of the task belongs
30
+ workflow_version : str
31
+ version of the workflow to which this instance of the task belongs
32
+ """
33
+
34
+ # So tab completion shows all the ViSP constants
35
+ constants: VbiConstants
36
+
37
+ @property
38
+ def constants_model_class(self) -> Type[ConstantsBase]:
39
+ """Class used to access constant database."""
40
+ return VbiConstants
41
+
42
+ @property
43
+ def fits_parsing_class(self):
44
+ """VBI specific subclass of L1FitsAccess to use for reading images."""
45
+ return VbiL1FitsAccess
46
+
47
+ @property
48
+ def num_images(self) -> int:
49
+ """Total number of images in final movie."""
50
+ return self.constants.num_mosaic_repeats
51
+
52
+ def tags_for_image_n(self, n: int) -> list[str]:
53
+ """Return the tags needed to find image n."""
54
+ return [VbiTag.mosaic(n + 1)]
55
+
56
+ def apply_colormap(self, array: np.ndarray) -> np.ndarray:
57
+ """
58
+ Convert floats to RGB colors using the ZScale normalization scheme.
59
+
60
+ Parameters
61
+ ----------
62
+ array : np.ndarray
63
+ data to convert
64
+ """
65
+ color_mapper = colormaps.get_cmap(self.MPL_COLOR_MAP)
66
+ scaled_array = ZScaleInterval()(array)
67
+ return color_mapper(scaled_array, bytes=True)[
68
+ :, :, :-1
69
+ ] # Drop the last (alpha) color dimension
70
+
71
+ def write_overlay(self, draw: ImageDraw, fits_obj: VbiL1FitsAccess) -> None:
72
+ """
73
+ Mark each image with it's instrument, observed wavelength, and observation time.
74
+
75
+ Parameters
76
+ ----------
77
+ draw
78
+ A simple 2D drawing function for PIL images
79
+
80
+ fits_obj
81
+ A single movie "image", i.e., a single array tagged with VBITag.movie_frame
82
+ """
83
+ self.write_line(
84
+ draw, f"INSTRUMENT: {self.constants.instrument}", 3, "right", font=self.font_18
85
+ )
86
+ self.write_line(draw, f"WAVELENGTH: {fits_obj.wavelength}", 2, "right", font=self.font_15)
87
+ self.write_line(draw, f"DATE OBS: {fits_obj.time_obs}", 1, "right", font=self.font_15)