fitsbolt 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.
- fitsbolt/__init__.py +43 -0
- fitsbolt/cfg/__init__.py +22 -0
- fitsbolt/cfg/create_config.py +486 -0
- fitsbolt/image_loader.py +223 -0
- fitsbolt/normalisation/NormalisationMethod.py +41 -0
- fitsbolt/normalisation/__init__.py +23 -0
- fitsbolt/normalisation/normalisation.py +500 -0
- fitsbolt/read.py +617 -0
- fitsbolt/resize.py +145 -0
- fitsbolt-0.1.0.dist-info/METADATA +395 -0
- fitsbolt-0.1.0.dist-info/RECORD +15 -0
- fitsbolt-0.1.0.dist-info/WHEEL +5 -0
- fitsbolt-0.1.0.dist-info/entry_points.txt +2 -0
- fitsbolt-0.1.0.dist-info/licenses/LICENSE +674 -0
- fitsbolt-0.1.0.dist-info/top_level.txt +1 -0
fitsbolt/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# fitsbolt - A Python package for image loading and processing
|
|
2
|
+
# Copyright (C) <2025> <Ruhberg>
|
|
3
|
+
#
|
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
# Core imports
|
|
18
|
+
from .image_loader import load_and_process_images
|
|
19
|
+
from .read import read_images
|
|
20
|
+
from .resize import resize_images
|
|
21
|
+
|
|
22
|
+
# Import from submodules
|
|
23
|
+
from .normalisation.NormalisationMethod import NormalisationMethod
|
|
24
|
+
from .normalisation.normalisation import normalise_images
|
|
25
|
+
from .cfg.create_config import create_config, validate_config, SUPPORTED_IMAGE_EXTENSIONS
|
|
26
|
+
|
|
27
|
+
__version__ = "0.1.0"
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
# Main functionality
|
|
31
|
+
"load_and_process_images",
|
|
32
|
+
"SUPPORTED_IMAGE_EXTENSIONS",
|
|
33
|
+
# Individual processing functions
|
|
34
|
+
"read_images",
|
|
35
|
+
"normalise_images",
|
|
36
|
+
"resize_images",
|
|
37
|
+
# Normalisation module
|
|
38
|
+
"NormalisationMethod",
|
|
39
|
+
"normalise_image",
|
|
40
|
+
# Configuration module
|
|
41
|
+
"create_config",
|
|
42
|
+
"validate_config",
|
|
43
|
+
]
|
fitsbolt/cfg/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# fitsbolt - A Python package for image loading and processing
|
|
2
|
+
# Copyright (C) <2025> <Ruhberg>
|
|
3
|
+
#
|
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
from .create_config import create_config, validate_config
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"create_config",
|
|
21
|
+
"validate_config",
|
|
22
|
+
]
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
# fitsbolt - A Python package for image loading and processing
|
|
2
|
+
# Copyright (C) <2025> <Ruhberg>
|
|
3
|
+
#
|
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
import numbers
|
|
18
|
+
import numpy as np
|
|
19
|
+
from dotmap import DotMap
|
|
20
|
+
from loguru import logger
|
|
21
|
+
|
|
22
|
+
from fitsbolt.normalisation.NormalisationMethod import NormalisationMethod
|
|
23
|
+
|
|
24
|
+
SUPPORTED_IMAGE_EXTENSIONS = {".fits", ".jpg", ".jpeg", ".png", ".tiff"}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def create_config(
|
|
28
|
+
output_dtype=np.uint8,
|
|
29
|
+
size=[224, 224],
|
|
30
|
+
fits_extension=None,
|
|
31
|
+
interpolation_order=1,
|
|
32
|
+
n_output_channels=3,
|
|
33
|
+
normalisation_method=NormalisationMethod.CONVERSION_ONLY,
|
|
34
|
+
channel_combination=None,
|
|
35
|
+
num_workers=4,
|
|
36
|
+
norm_maximum_value=None,
|
|
37
|
+
norm_minimum_value=None,
|
|
38
|
+
norm_log_calculate_minimum_value=False,
|
|
39
|
+
norm_crop_for_maximum_value=None,
|
|
40
|
+
norm_asinh_scale=[0.7, 0.7, 0.7],
|
|
41
|
+
norm_asinh_clip=[99.8, 99.8, 99.8],
|
|
42
|
+
log_level="INFO",
|
|
43
|
+
force_dtype=True,
|
|
44
|
+
):
|
|
45
|
+
"""Create a configuration object for loading and processing astronomical data.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
output_dtype (type, optional): Data type for output images. Defaults to np.uint8.
|
|
49
|
+
size (list, optional): Target size for image resizing. Defaults to [224, 224]. If None, no resizing.
|
|
50
|
+
fits_extension (list, optional): Extension(s) to use when loading FITS files. Defaults to None.
|
|
51
|
+
interpolation_order (int, optional): Order of interpolation for resizing with skimage, 0-5. Defaults to 1.
|
|
52
|
+
n_output_channels (int,optional): number of output channels. Defaults to 3.
|
|
53
|
+
normalisation_method (NormalisationMethod, optional): Method for normalising images.
|
|
54
|
+
Defaults to NormalisationMethod.CONVERSION_ONLY.
|
|
55
|
+
channel_combination (np.ndarray, optional): n_output x fits_extension sized np array for channel mapping& lerp.
|
|
56
|
+
Defaults to None, which will map extensions to channels either
|
|
57
|
+
1:1 if applicable or 1:n_output if only 1 input is provided.
|
|
58
|
+
num_workers (int, optional): Number of worker threads for data loading. Defaults to 4.
|
|
59
|
+
|
|
60
|
+
norm_maximum_value (type, optional): Maximum value for normalisation. Defaults to None implying dynamic.
|
|
61
|
+
norm_minimum_value (type, optional): Minimum value for normalisation. Defaults to None implying dynamic.
|
|
62
|
+
norm_log_calculate_minimum_value (bool, optional): If True, calculates the minimum value for log scaling.
|
|
63
|
+
Defaults to False.
|
|
64
|
+
norm_crop_for_maximum_value (type, optional): Crops the image for maximum value. Defaults to None.
|
|
65
|
+
norm_asinh_scale (list, optional): Scale factors for asinh normalisation. Defaults to [0.7, 0.7, 0.7].
|
|
66
|
+
norm_asinh_clip (list, optional): Clip values for asinh normalisation. Defaults to [99.8, 99.8, 99.8].
|
|
67
|
+
log_level (str, optional): Logging level. Defaults to "INFO".
|
|
68
|
+
force_dtype (bool, optional): If True, forces the output to maintain the original dtype after tensor operations
|
|
69
|
+
like channel combination. Defaults to True.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
_type_: _description_
|
|
73
|
+
"""
|
|
74
|
+
cfg = DotMap(_dynamic=False)
|
|
75
|
+
# Settings
|
|
76
|
+
cfg.log_level = log_level
|
|
77
|
+
cfg.output_dtype = output_dtype
|
|
78
|
+
cfg.size = size # tuple of (height, width)
|
|
79
|
+
cfg.n_output_channels = n_output_channels # int, normally 3 for R,G,B
|
|
80
|
+
cfg.num_workers = num_workers
|
|
81
|
+
cfg.force_dtype = force_dtype # Force output to maintain original dtype after tensor operations
|
|
82
|
+
# order of interpolation for resizing with skimage, 0-5
|
|
83
|
+
cfg.interpolation_order = interpolation_order
|
|
84
|
+
# Normalisation settings
|
|
85
|
+
cfg.normalisation_method = normalisation_method
|
|
86
|
+
# Optional normalisation settings
|
|
87
|
+
cfg.normalisation = DotMap()
|
|
88
|
+
cfg.normalisation.maximum_value = norm_maximum_value # None or float
|
|
89
|
+
cfg.normalisation.minimum_value = norm_minimum_value # None or float
|
|
90
|
+
cfg.normalisation.crop_for_maximum_value = (
|
|
91
|
+
norm_crop_for_maximum_value # None or integer tuple (height, width)
|
|
92
|
+
)
|
|
93
|
+
# Bool, if False assumes min value to be 0 or cfg.normalisation.minimum_value if not None
|
|
94
|
+
cfg.normalisation.log_calculate_minimum_value = norm_log_calculate_minimum_value
|
|
95
|
+
# only used if cfg.normalisation_method == NormalisationMethod.ASINH:
|
|
96
|
+
# asinh_scale list of 3 floats > 0, defining the scale for each channel (lower = higher stretch):
|
|
97
|
+
cfg.normalisation.asinh_scale = norm_asinh_scale
|
|
98
|
+
# asinh_clip list of 3 floats in ]0.,100.], defining the clip for each channel:
|
|
99
|
+
cfg.normalisation.asinh_clip = norm_asinh_clip
|
|
100
|
+
|
|
101
|
+
# FITS file handling settings
|
|
102
|
+
# Extension(s) to use when loading FITS files (can be int, string, or list of int/string)
|
|
103
|
+
cfg.fits_extension = fits_extension
|
|
104
|
+
# Dictionary defining how to combine FITS extensions into RGB channels, should contain lists of the same length
|
|
105
|
+
# as cfg.fits_extension, or empty lists - then the first three extensions will be used for RGB
|
|
106
|
+
if fits_extension is not None:
|
|
107
|
+
# Convert single extension to list format
|
|
108
|
+
if not isinstance(fits_extension, (list, tuple, np.ndarray)):
|
|
109
|
+
fits_extension = [fits_extension] # Convert to list for consistent handling
|
|
110
|
+
cfg.fits_extension = fits_extension
|
|
111
|
+
|
|
112
|
+
if channel_combination is None:
|
|
113
|
+
if not (len(fits_extension) in [1, n_output_channels]):
|
|
114
|
+
# fits extension does not match 1 channel (castable to greyscale) or n_output
|
|
115
|
+
|
|
116
|
+
raise ValueError(
|
|
117
|
+
"Length of fits_extensions does not match the specified number of output channels and"
|
|
118
|
+
+ "no mapping via channelcombination is provided."
|
|
119
|
+
+ f"Length fits_extension: {np.array(fits_extension).size}"
|
|
120
|
+
+ f"Specified output channels: {n_output_channels}"
|
|
121
|
+
+ f"-> set channel_combination to be a {n_output_channels}x{np.array(fits_extension).size}"
|
|
122
|
+
)
|
|
123
|
+
else:
|
|
124
|
+
# selecting n fits extensions but no combination will lead to a 1-1 mapping
|
|
125
|
+
extension_size = len(fits_extension)
|
|
126
|
+
combination_array = np.zeros((n_output_channels, extension_size))
|
|
127
|
+
if combination_array.shape[0] == combination_array.shape[1]:
|
|
128
|
+
# Identity matrix mapping
|
|
129
|
+
for i in range(0, combination_array.shape[0]):
|
|
130
|
+
combination_array[i, i] = 1
|
|
131
|
+
else:
|
|
132
|
+
# For the case where fits_extension has only 1 element
|
|
133
|
+
if extension_size == 1:
|
|
134
|
+
# Map all output channels to the single input channel
|
|
135
|
+
for i in range(0, combination_array.shape[0]):
|
|
136
|
+
combination_array[i, 0] = 1
|
|
137
|
+
|
|
138
|
+
# Debug output
|
|
139
|
+
print(f"Created channel_combination array with shape {combination_array.shape}")
|
|
140
|
+
print(f"Array contents: {combination_array}")
|
|
141
|
+
|
|
142
|
+
cfg.channel_combination = combination_array
|
|
143
|
+
else:
|
|
144
|
+
|
|
145
|
+
cfg.channel_combination = (
|
|
146
|
+
channel_combination # n_output x fits_extension sizes np array
|
|
147
|
+
)
|
|
148
|
+
validate_config(cfg)
|
|
149
|
+
return cfg
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _return_required_and_optional_keys():
|
|
153
|
+
"""
|
|
154
|
+
Returns the configuration parameters in a unified format.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
dict: Dictionary with parameter_name as key and [dtype, min, max, optional, allowed_values] as value
|
|
158
|
+
- dtype: expected data type (str, int, float, bool, list, tuple, 'directory', 'file', 'special')
|
|
159
|
+
- min: minimum value (None if not applicable)
|
|
160
|
+
- max: maximum value (None if not applicable)
|
|
161
|
+
- optional: True if parameter is optional, False if required
|
|
162
|
+
- allowed_values: list of allowed values (None if not applicable)
|
|
163
|
+
"""
|
|
164
|
+
config_spec = {
|
|
165
|
+
# Required positive integers
|
|
166
|
+
"num_workers": [int, 1, None, False, None],
|
|
167
|
+
"n_output_channels": [int, 1, None, False, None],
|
|
168
|
+
# Required boolean parameters
|
|
169
|
+
"normalisation.log_calculate_minimum_value": [bool, None, None, False, None],
|
|
170
|
+
"force_dtype": [bool, None, None, False, None],
|
|
171
|
+
# Required parameters with allowed values
|
|
172
|
+
"log_level": [
|
|
173
|
+
str,
|
|
174
|
+
None,
|
|
175
|
+
None,
|
|
176
|
+
False,
|
|
177
|
+
["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "TRACE"],
|
|
178
|
+
],
|
|
179
|
+
# Required special parameters
|
|
180
|
+
"size": ["special_size", None, None, False, None],
|
|
181
|
+
"normalisation_method": ["special_normalisation_method", None, None, False, None],
|
|
182
|
+
"normalisation": ["special_normalisation", None, None, False, None],
|
|
183
|
+
"normalisation.asinh_scale": ["special_asinh_scale", None, None, False, None],
|
|
184
|
+
"normalisation.asinh_clip": ["special_asinh_clip", None, None, False, None],
|
|
185
|
+
"interpolation_order": [int, 0, 5, False, None], # 0-5 for skimage interpolation"
|
|
186
|
+
"output_dtype": [type, None, None, False, None],
|
|
187
|
+
# Optional numeric parameters
|
|
188
|
+
"normalisation.maximum_value": [float, None, None, True, None],
|
|
189
|
+
"normalisation.minimum_value": [float, None, None, True, None],
|
|
190
|
+
# Optional special parameters
|
|
191
|
+
"normalisation.crop_for_maximum_value": ["special_crop", None, None, True, None],
|
|
192
|
+
"fits_extension": ["special_fits_extension", None, None, True, None],
|
|
193
|
+
"channel_combination": ["special_channel_combination", None, None, True, None],
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return config_spec
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _get_nested_value(cfg: DotMap, key: str):
|
|
200
|
+
"""Get a nested value from the config using dot notation.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
cfg (DotMap): Configuration object
|
|
204
|
+
key (str): Key in dot notation (e.g., 'normalisation.maximum_value')
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Any: Value from the config
|
|
208
|
+
"""
|
|
209
|
+
current = cfg
|
|
210
|
+
for part in key.split("."):
|
|
211
|
+
try:
|
|
212
|
+
current = current[part]
|
|
213
|
+
except (KeyError, TypeError):
|
|
214
|
+
raise ValueError(f"Missing key in config: {key}")
|
|
215
|
+
return current
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _get_all_keys(cfg: DotMap, parent_key: str = ""):
|
|
219
|
+
"""Get keys of the configuration in dot notation.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
cfg (DotMap): Configuration dotmap
|
|
223
|
+
parent_key (str, optional): Parent key for nested values."".
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
set: Set of all keys in dot notation
|
|
227
|
+
"""
|
|
228
|
+
keys = set()
|
|
229
|
+
for key, value in cfg.items():
|
|
230
|
+
current_key = f"{parent_key}.{key}" if parent_key else key
|
|
231
|
+
keys.add(current_key)
|
|
232
|
+
if isinstance(value, DotMap):
|
|
233
|
+
keys.update(_get_all_keys(value, current_key))
|
|
234
|
+
return keys
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def validate_config(cfg: DotMap, check_paths: bool = True) -> None:
|
|
238
|
+
"""Validate configuration against required and optional keys specification.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
cfg (DotMap): Configuration to validate
|
|
242
|
+
check_paths (bool): Whether to check if file and directory paths exist
|
|
243
|
+
|
|
244
|
+
Raises:
|
|
245
|
+
ValueError: If configuration is invalid
|
|
246
|
+
"""
|
|
247
|
+
# Get configuration specification
|
|
248
|
+
config_spec = _return_required_and_optional_keys()
|
|
249
|
+
|
|
250
|
+
# Keep track of checked keys
|
|
251
|
+
expected_keys = set()
|
|
252
|
+
|
|
253
|
+
# Validate each parameter
|
|
254
|
+
for param_name, (dtype, min_val, max_val, optional, allowed_values) in config_spec.items():
|
|
255
|
+
expected_keys.add(param_name)
|
|
256
|
+
|
|
257
|
+
# Try to get the value, handle missing optional parameters
|
|
258
|
+
try:
|
|
259
|
+
value = _get_nested_value(cfg, param_name)
|
|
260
|
+
except ValueError:
|
|
261
|
+
if optional:
|
|
262
|
+
continue # Skip missing optional parameters
|
|
263
|
+
else:
|
|
264
|
+
raise ValueError(
|
|
265
|
+
f"Missing required parameter: {param_name}"
|
|
266
|
+
+ f"(type: {dtype.__name__ if hasattr(dtype, '__name__') else dtype})"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# Skip validation for None values on optional parameters
|
|
270
|
+
if value is None and optional:
|
|
271
|
+
continue
|
|
272
|
+
|
|
273
|
+
# Helper function to format constraint info
|
|
274
|
+
def _format_constraints():
|
|
275
|
+
constraints = []
|
|
276
|
+
if min_val is not None:
|
|
277
|
+
constraints.append(f"min: {min_val}")
|
|
278
|
+
if max_val is not None:
|
|
279
|
+
constraints.append(f"max: {max_val}")
|
|
280
|
+
if allowed_values is not None:
|
|
281
|
+
constraints.append(f"allowed: {allowed_values}")
|
|
282
|
+
return f" ({', '.join(constraints)})" if constraints else ""
|
|
283
|
+
|
|
284
|
+
# Validate based on data type
|
|
285
|
+
if dtype == str:
|
|
286
|
+
if not isinstance(value, str):
|
|
287
|
+
raise ValueError(
|
|
288
|
+
f"{param_name} must be a string, got {type(value).__name__}{_format_constraints()}"
|
|
289
|
+
)
|
|
290
|
+
# Check allowed values for string types
|
|
291
|
+
if allowed_values is not None and value not in allowed_values:
|
|
292
|
+
raise ValueError(f"{param_name} must be one of {allowed_values}, got '{value}'")
|
|
293
|
+
|
|
294
|
+
elif dtype == int:
|
|
295
|
+
if not isinstance(value, int):
|
|
296
|
+
raise ValueError(
|
|
297
|
+
f"{param_name} must be an integer, got {type(value).__name__}{_format_constraints()}"
|
|
298
|
+
)
|
|
299
|
+
if min_val is not None and value < min_val:
|
|
300
|
+
raise ValueError(
|
|
301
|
+
f"{param_name} must be >= {min_val}, got {value}{_format_constraints()}"
|
|
302
|
+
)
|
|
303
|
+
if max_val is not None and value > max_val:
|
|
304
|
+
raise ValueError(
|
|
305
|
+
f"{param_name} must be <= {max_val}, got {value}{_format_constraints()}"
|
|
306
|
+
)
|
|
307
|
+
if allowed_values is not None and value not in allowed_values:
|
|
308
|
+
raise ValueError(f"{param_name} must be one of {allowed_values}, got {value}")
|
|
309
|
+
|
|
310
|
+
elif dtype == float:
|
|
311
|
+
if not isinstance(value, (int, float)):
|
|
312
|
+
raise ValueError(
|
|
313
|
+
f"{param_name} must be a number, got {type(value).__name__}{_format_constraints()}"
|
|
314
|
+
)
|
|
315
|
+
if min_val is not None and value < min_val:
|
|
316
|
+
raise ValueError(
|
|
317
|
+
f"{param_name} must be >= {min_val}, got {value}{_format_constraints()}"
|
|
318
|
+
)
|
|
319
|
+
if max_val is not None and value > max_val:
|
|
320
|
+
raise ValueError(
|
|
321
|
+
f"{param_name} must be <= {max_val}, got {value}{_format_constraints()}"
|
|
322
|
+
)
|
|
323
|
+
if allowed_values is not None and value not in allowed_values:
|
|
324
|
+
raise ValueError(f"{param_name} must be one of {allowed_values}, got {value}")
|
|
325
|
+
|
|
326
|
+
elif dtype == bool:
|
|
327
|
+
if not isinstance(value, bool):
|
|
328
|
+
raise ValueError(f"{param_name} must be a boolean, got {type(value).__name__}")
|
|
329
|
+
|
|
330
|
+
elif dtype == type:
|
|
331
|
+
if not (isinstance(value, type) or isinstance(value, np.dtype)):
|
|
332
|
+
raise ValueError(
|
|
333
|
+
f"{param_name} must be a (numpy) dtype, got {type(value).__name__}{_format_constraints()}"
|
|
334
|
+
)
|
|
335
|
+
if not issubclass(value, numbers.Number):
|
|
336
|
+
raise ValueError(
|
|
337
|
+
f"{param_name} must be a (numpy) dtype, got {type(value).__name__}{_format_constraints()}"
|
|
338
|
+
)
|
|
339
|
+
# No min/max/allowed values for dtypes
|
|
340
|
+
# Handle special validation cases
|
|
341
|
+
elif dtype == "special_size":
|
|
342
|
+
if value is not None:
|
|
343
|
+
if not isinstance(value, (list, tuple)) or len(value) != 2:
|
|
344
|
+
raise ValueError(
|
|
345
|
+
f"{param_name} must be a list or tuple of length 2, got {type(value).__name__}"
|
|
346
|
+
+ f"with length {len(value) if hasattr(value, '__len__') else 'unknown'}"
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
elif dtype == "special_normalisation_method":
|
|
350
|
+
if not isinstance(value, NormalisationMethod):
|
|
351
|
+
raise ValueError(
|
|
352
|
+
f"{param_name} must be a NormalisationMethod enum value, got {type(value).__name__}"
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
elif dtype == "special_normalisation":
|
|
356
|
+
if not isinstance(value, DotMap):
|
|
357
|
+
raise ValueError(f"{param_name} must be a DotMap, got {type(value).__name__}")
|
|
358
|
+
|
|
359
|
+
elif dtype == "special_asinh_scale":
|
|
360
|
+
if not isinstance(value, (list, tuple, int, float)):
|
|
361
|
+
raise ValueError(
|
|
362
|
+
f"{param_name} must be a number or list/tuple of 3 numbers > 0, got {type(value).__name__}"
|
|
363
|
+
)
|
|
364
|
+
if isinstance(value, (list, tuple)):
|
|
365
|
+
if len(value) != 3:
|
|
366
|
+
raise ValueError(
|
|
367
|
+
f"{param_name} if list/tuple, must have length 3, got length {len(value)}"
|
|
368
|
+
)
|
|
369
|
+
if not all(isinstance(x, (int, float)) for x in value):
|
|
370
|
+
raise ValueError(
|
|
371
|
+
f"{param_name} values must be numbers, got types: {[type(x).__name__ for x in value]}"
|
|
372
|
+
)
|
|
373
|
+
if not all(0 < x for x in value):
|
|
374
|
+
raise ValueError(f"{param_name} values must be > 0, got: {value}")
|
|
375
|
+
else:
|
|
376
|
+
# Single value
|
|
377
|
+
if not isinstance(value, (int, float)):
|
|
378
|
+
raise ValueError(
|
|
379
|
+
f"{param_name} must be a number > 0, got {type(value).__name__}"
|
|
380
|
+
)
|
|
381
|
+
if not (0 < value):
|
|
382
|
+
raise ValueError(f"{param_name} must > 0, got: {value}")
|
|
383
|
+
|
|
384
|
+
elif dtype == "special_asinh_clip":
|
|
385
|
+
if not isinstance(value, (list, tuple, int, float)):
|
|
386
|
+
raise ValueError(
|
|
387
|
+
f"{param_name} must be a number or list/tuple of 3 numbers in ]0,100.], got {type(value).__name__}"
|
|
388
|
+
)
|
|
389
|
+
if isinstance(value, (list, tuple)):
|
|
390
|
+
if len(value) != 3:
|
|
391
|
+
raise ValueError(
|
|
392
|
+
f"{param_name} if list/tuple, must have length 3, got length {len(value)}"
|
|
393
|
+
)
|
|
394
|
+
if not all(isinstance(x, (int, float)) for x in value):
|
|
395
|
+
raise ValueError(
|
|
396
|
+
f"{param_name} values must be numbers, got types: {[type(x).__name__ for x in value]}"
|
|
397
|
+
)
|
|
398
|
+
if not all(0 < x <= 100 for x in value):
|
|
399
|
+
raise ValueError(f"{param_name} values must be in range ]0,100.], got: {value}")
|
|
400
|
+
else:
|
|
401
|
+
# Single value
|
|
402
|
+
if not isinstance(value, (int, float)):
|
|
403
|
+
raise ValueError(
|
|
404
|
+
f"{param_name} must be a number in ]0,100.], got {type(value).__name__}"
|
|
405
|
+
)
|
|
406
|
+
if not (0 < value <= 100):
|
|
407
|
+
raise ValueError(f"{param_name} must be in range ]0,100.], got: {value}")
|
|
408
|
+
|
|
409
|
+
elif dtype == "special_crop":
|
|
410
|
+
if value is not None:
|
|
411
|
+
if not isinstance(value, (tuple, list)) or len(value) != 2:
|
|
412
|
+
raise ValueError(
|
|
413
|
+
f"{param_name} if set, must be a tuple of two integers, got {type(value).__name__}"
|
|
414
|
+
)
|
|
415
|
+
if not all(isinstance(x, int) for x in value):
|
|
416
|
+
raise ValueError(
|
|
417
|
+
f"{param_name} values must be integers, got types: {[type(x).__name__ for x in value]}"
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
elif dtype == "special_fits_extension":
|
|
421
|
+
if value is not None:
|
|
422
|
+
if isinstance(value, list):
|
|
423
|
+
# if no combination parameters are set
|
|
424
|
+
if len(value) != cfg.channel_combination.shape[1]:
|
|
425
|
+
raise ValueError(
|
|
426
|
+
f"{param_name} must be a str/int or list of strings/ints of length 1, n_output ="
|
|
427
|
+
+ f"{cfg.n_output_channels}, or match channel_combination.shape[1] = "
|
|
428
|
+
+ f"{cfg.channel_combination.shape[1]}, got list of length {len(value)}"
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
for v in value:
|
|
432
|
+
if not isinstance(v, (str, int)):
|
|
433
|
+
raise ValueError(
|
|
434
|
+
f"{param_name} list elements must be str or int, got {type(v).__name__}"
|
|
435
|
+
)
|
|
436
|
+
elif not isinstance(value, (str, int)):
|
|
437
|
+
raise ValueError(
|
|
438
|
+
f"{param_name} must be a str/int or list of strings/ints, got {type(value).__name__}"
|
|
439
|
+
)
|
|
440
|
+
elif dtype == "special_channel_combination":
|
|
441
|
+
|
|
442
|
+
if not isinstance(value, np.ndarray):
|
|
443
|
+
raise ValueError(f"{param_name} must be a numpyarray")
|
|
444
|
+
if np.any(value < 0):
|
|
445
|
+
raise ValueError(f"{param_name} must not have negative values")
|
|
446
|
+
# TODO check 2dimensionality
|
|
447
|
+
for i in range(0, value.shape[0]):
|
|
448
|
+
if np.any(np.allclose(np.sum(value[i, :]), 0)):
|
|
449
|
+
raise ValueError(
|
|
450
|
+
f"{param_name} values for channel '{i}' must not sum to zero, got {value[i, :]}"
|
|
451
|
+
)
|
|
452
|
+
if not value.shape[0] == cfg.n_output_channels and not value.shape[1] == len(
|
|
453
|
+
cfg.fits_extension
|
|
454
|
+
):
|
|
455
|
+
raise ValueError(
|
|
456
|
+
f"{param_name} channel mapping shape must reflect input and output shapes:"
|
|
457
|
+
+ f" expected n output {cfg.n_output_channels} & n input {len(cfg.fits_extension)}"
|
|
458
|
+
+ f" got {value.shape[0]}x {value.shape[1]}"
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
else:
|
|
462
|
+
raise ValueError(f"Unknown data type for {param_name}: {dtype}")
|
|
463
|
+
|
|
464
|
+
# Custom cross-parameter validation
|
|
465
|
+
if "normalisation" in cfg:
|
|
466
|
+
if (
|
|
467
|
+
hasattr(cfg.normalisation, "maximum_value")
|
|
468
|
+
and hasattr(cfg.normalisation, "minimum_value")
|
|
469
|
+
and isinstance(cfg.normalisation.maximum_value, (int, float))
|
|
470
|
+
and isinstance(cfg.normalisation.minimum_value, (int, float))
|
|
471
|
+
):
|
|
472
|
+
if cfg.normalisation.maximum_value <= cfg.normalisation.minimum_value:
|
|
473
|
+
raise ValueError(
|
|
474
|
+
f"normalisation.maximum_value {cfg.normalisation.maximum_value} must be larger than "
|
|
475
|
+
f"normalisation.minimum_value {cfg.normalisation.minimum_value}"
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
# Check for unexpected keys
|
|
479
|
+
actual_keys = _get_all_keys(cfg)
|
|
480
|
+
unexpected_keys = actual_keys - expected_keys
|
|
481
|
+
|
|
482
|
+
if unexpected_keys:
|
|
483
|
+
logger.warning(f"Found unexpected keys in config: {sorted(unexpected_keys)}")
|
|
484
|
+
logger.info("Config: validation partially successful")
|
|
485
|
+
else:
|
|
486
|
+
logger.info("Config: validation successful")
|