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.
- changelog/.gitempty +0 -0
- dkist_processing_vbi/__init__.py +9 -0
- dkist_processing_vbi/config.py +12 -0
- dkist_processing_vbi/models/__init__.py +1 -0
- dkist_processing_vbi/models/constants.py +88 -0
- dkist_processing_vbi/models/filter.py +47 -0
- dkist_processing_vbi/models/tags.py +32 -0
- dkist_processing_vbi/parsers/__init__.py +1 -0
- dkist_processing_vbi/parsers/mosaic_repeats.py +167 -0
- dkist_processing_vbi/parsers/spatial_step_pattern.py +43 -0
- dkist_processing_vbi/parsers/vbi_l0_fits_access.py +36 -0
- dkist_processing_vbi/parsers/vbi_l1_fits_access.py +33 -0
- dkist_processing_vbi/tasks/__init__.py +10 -0
- dkist_processing_vbi/tasks/assemble_movie.py +87 -0
- dkist_processing_vbi/tasks/dark.py +105 -0
- dkist_processing_vbi/tasks/gain.py +161 -0
- dkist_processing_vbi/tasks/make_movie_frames.py +368 -0
- dkist_processing_vbi/tasks/mixin/__init__.py +1 -0
- dkist_processing_vbi/tasks/mixin/intermediate_loaders.py +74 -0
- dkist_processing_vbi/tasks/parse.py +78 -0
- dkist_processing_vbi/tasks/process_summit_processed.py +62 -0
- dkist_processing_vbi/tasks/quality_metrics.py +70 -0
- dkist_processing_vbi/tasks/science.py +131 -0
- dkist_processing_vbi/tasks/trial_output_data.py +51 -0
- dkist_processing_vbi/tasks/vbi_base.py +30 -0
- dkist_processing_vbi/tasks/write_l1.py +155 -0
- dkist_processing_vbi/tests/__init__.py +0 -0
- dkist_processing_vbi/tests/conftest.py +277 -0
- dkist_processing_vbi/tests/local_trial_workflows/__init__.py +0 -0
- dkist_processing_vbi/tests/local_trial_workflows/l0_to_l1.py +245 -0
- dkist_processing_vbi/tests/test_assemble_movie.py +59 -0
- dkist_processing_vbi/tests/test_dark.py +123 -0
- dkist_processing_vbi/tests/test_gain.py +109 -0
- dkist_processing_vbi/tests/test_intermediate_loaders.py +105 -0
- dkist_processing_vbi/tests/test_make_movie_frames.py +263 -0
- dkist_processing_vbi/tests/test_parse_l0.py +338 -0
- dkist_processing_vbi/tests/test_parse_summit.py +141 -0
- dkist_processing_vbi/tests/test_process_summit.py +210 -0
- dkist_processing_vbi/tests/test_quality_metrics.py +139 -0
- dkist_processing_vbi/tests/test_science.py +167 -0
- dkist_processing_vbi/tests/test_spatial_step_pattern.py +74 -0
- dkist_processing_vbi/tests/test_vbi_base.py +37 -0
- dkist_processing_vbi/tests/test_vbi_constants.py +145 -0
- dkist_processing_vbi/tests/test_workflows.py +9 -0
- dkist_processing_vbi/tests/test_write_l1.py +191 -0
- dkist_processing_vbi/workflows/__init__.py +2 -0
- dkist_processing_vbi/workflows/l0_processing.py +69 -0
- dkist_processing_vbi/workflows/summit_data_processing.py +64 -0
- dkist_processing_vbi/workflows/trial_workflows.py +128 -0
- dkist_processing_vbi-1.17.4.dist-info/METADATA +186 -0
- dkist_processing_vbi-1.17.4.dist-info/RECORD +66 -0
- dkist_processing_vbi-1.17.4.dist-info/WHEEL +5 -0
- dkist_processing_vbi-1.17.4.dist-info/top_level.txt +4 -0
- docs/Makefile +134 -0
- docs/changelog.rst +11 -0
- docs/conf.py +52 -0
- docs/index.rst +15 -0
- docs/l0_to_l1_vbi_no-speckle-full-trial.rst +11 -0
- docs/l0_to_l1_vbi_no-speckle.rst +11 -0
- docs/l0_to_l1_vbi_summit-calibrated-full-trial.rst +11 -0
- docs/l0_to_l1_vbi_summit-calibrated.rst +10 -0
- docs/make.bat +170 -0
- docs/requirements.txt +1 -0
- docs/requirements_table.rst +8 -0
- docs/scientific_changelog.rst +10 -0
- 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)
|