Coverage for src/susi/base/config/cam.py: 97%
58 statements
« prev ^ index » next coverage.py v7.5.0, created at 2025-08-11 10:03 +0000
« prev ^ index » next coverage.py v7.5.0, created at 2025-08-11 10:03 +0000
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3"""
4Module to specify cam specific calibration.
6@author: hoelken
7"""
8from __future__ import annotations
9from ..exceptions import MissConfigurationException
10from dataclasses import dataclass, field
13@dataclass
14class Cam:
15 C1 = "cam1"
16 C2 = "cam2"
17 C3 = "cam3"
19 #: Internal name of the camera
20 name: str = None
22 #: ID string of the camera
23 id: str = None
25 #: Names of the data shape entries. Supported: x, y, lambda and mod_state
26 shape_keys: list = None
28 #: Define the science ROI shape of the data within the full FOV
29 #: as a pair of slices in shape_keys order. Default (0, 2048) each.
30 data_shape: list = field(default_factory=lambda: [slice(0, 2048), slice(0, 2048)])
32 #: lid area of the camera
33 #: <pre>
34 #: ╔══════════════════════════════════════╗
35 #: ║ shielded area ║
36 #: ║ ╭──────────────────────────────╮ ║
37 #: ║ │ │ ║
38 #: ║ │ illuminated area │ ║
39 #: ║ │ │ ║
40 #: ║ │ │ ║
41 #: ║ ╰──────────────────────────────╯ ║
42 #: ╚══════════════════════════════════════╝
43 #: </pre>
44 lid_area: list = field(default_factory=lambda: [])
46 #: Borders that define thickness of usable area (no stray light) of the
47 #: shielded pixels in pixels distance from the frame border
48 #: in the order of [top, bottom, left, right]
49 #: <pre>
50 #: ╔════════════════════════════════════════╗
51 #: ║ shielded area ║
52 #: ║ ┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐ ║
53 #: ║ ┊╭──────────────────────────────╮┊ ║
54 #: ║ ┊│ │┊ ║
55 #: ║ ┊│ illuminated area │┊ ║
56 #: ║ ┊│ │┊ ║
57 #: ║ ┊│ │┊ ║
58 #: ║ ┊╰──────────────────────────────╯┊ ║
59 #: ║ └┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘ ║
60 #: ╚════════════════════════════════════════╝
61 #: </pre>
62 #: The area between `┊` and `║` will be used.
63 shielded_px: list = field(default_factory=lambda: [])
65 #: Mode for shielded px correction. Default is given in with_defaults below
66 #: Supported:
67 #: - 'N/A': Skip banding correction
68 #: - 'mean' for (frame) global mean correction
69 #: - 'linear_row' for a row-wise linear fit correction from left to right border
70 #: - 'median_col' for a column-wise median correction from top to bottom border
71 #: - 'linear_row_and_median_col' for both row and coll wise corrrction
72 shielded_px_mode: str = "linear_row_and_median_col"
74 #: Window size for the median filter used in cam_shielded_px_mode='mean' (must be an odd integer)
75 shielded_px_win: int = 9
77 #: Configuration of the modes used to filter hot pixels. Default is 'N/A' or not applied. See hot_pixels.py
78 hot_px_mode: str = "N/A"
80 # ====== Binning ======
81 #: Define the number of columns & rows to bin.
82 #: 1 (default) means no binning.
83 #: Negative values will bin everything to 1D in this direction.
84 #: (similar to the frames: keyword)
85 spacial_binning: list = field(default_factory=lambda: [1, 1])
87 #: Configures how many modulation cycles shall be averaged to a slice.
88 #: the number of frames thereby also defines the x-dimension
89 #: via (`= <num_files_in_folder>/<frames>`) of the resulting data cube.
90 #: The default `1` means no averaging, `2` would average over two frames of the same mod state, etc.
91 #: Set to `-1` to average over all frames in the input folder per mod state.
92 #: NOTE: This also controls the chunk size returned by the 'Chunker' even if block B is not used.
93 temporal_binning: int = 1
95 @staticmethod
96 def with_defaults(cam: str) -> Cam:
97 """
98 Load the class with the defaults suitable for the given camera
100 :param: cam: Cam identifier string ('cam1', 'cam2' or 'cam3')
101 """
102 if cam == Cam.C1:
103 lid_area = [slice(50, 1992), slice(70, 1992)]
104 shielded_px = [10, 20, 10, 10]
105 cam_id = "US550"
106 shielded_px_mode = "linear_row"
107 shape_keys = ["y", "lambda"]
108 elif cam == Cam.C2:
109 lid_area = [slice(50, 1992), slice(70, 1992)]
110 shielded_px = [10, 20, 10, 10]
111 cam_id = "US560"
112 shielded_px_mode = "linear_row"
113 shape_keys = ["y", "lambda"]
114 elif cam == Cam.C3:
115 lid_area = [slice(50, 1970), slice(100, 1840)]
116 shielded_px = [10, 20, 10, 10]
117 shielded_px_mode = "N/A"
118 cam_id = "US540"
119 shape_keys = ["y", "x"]
120 else:
121 raise MissConfigurationException(f'Cam id "cam" is unknown.')
123 return Cam(
124 name=cam,
125 id=cam_id,
126 lid_area=lid_area,
127 shielded_px=shielded_px,
128 shielded_px_mode=shielded_px_mode,
129 shape_keys=shape_keys,
130 )
132 def __repr__(self) -> str:
133 txt = f"== {self.__class__.__name__} ==\n"
134 txt += "\n".join(["{:<21} = {}".format(k, v) for k, v in self.__dict__.items()])
135 return txt
137 def amend_from_dict(self, data: dict):
138 """
139 Overwrites the parameters with the values from the given dictionary.
140 All instance variable names are supported as keywords.
141 All keywords are optional, if the keyword is not present the previous value will be kept.
143 There is a special key that must follow a specific syntax if given
145 - 'data_shape': The data shape must be given as a dictionary with keys 'x' and 'y'
146 for vertical and horizontal shape. The values of both keys must be lists
147 of integers of length 2, e.g. `{'x': [1, 2], 'y': [3, 4]}`
149 ### Params
150 - data: The dictionary to parse.
151 - c_name: The name of the camera to set defaults for.
153 ### Returns
154 the created Config
155 """
156 shape = data.pop("data_shape") if "data_shape" in data else None
157 self.shape_keys = shape["keys"] if shape and "keys" in shape else self.shape_keys
158 for k, v in data.items():
159 if not hasattr(self, k):
160 raise MissConfigurationException(f"{self.__class__.__name__} config has no attribute {k}")
161 setattr(self, k, v)
162 if shape:
163 self.data_shape = []
164 for key in self.shape_keys:
165 dim = slice(shape[key][0], shape[key][1]) if key in shape else slice(None, None)
166 self.data_shape.append(dim)