gammasimtools 0.26.0__py3-none-any.whl → 0.27.1__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.
- {gammasimtools-0.26.0.dist-info → gammasimtools-0.27.1.dist-info}/METADATA +5 -1
- {gammasimtools-0.26.0.dist-info → gammasimtools-0.27.1.dist-info}/RECORD +70 -66
- {gammasimtools-0.26.0.dist-info → gammasimtools-0.27.1.dist-info}/WHEEL +1 -1
- {gammasimtools-0.26.0.dist-info → gammasimtools-0.27.1.dist-info}/entry_points.txt +1 -1
- simtools/_version.py +2 -2
- simtools/applications/convert_geo_coordinates_of_array_elements.py +2 -1
- simtools/applications/db_get_array_layouts_from_db.py +1 -1
- simtools/applications/{calculate_incident_angles.py → derive_incident_angle.py} +16 -16
- simtools/applications/derive_mirror_rnda.py +111 -177
- simtools/applications/generate_corsika_histograms.py +38 -1
- simtools/applications/generate_regular_arrays.py +73 -36
- simtools/applications/simulate_flasher.py +3 -13
- simtools/applications/simulate_illuminator.py +2 -10
- simtools/applications/simulate_pedestals.py +1 -1
- simtools/applications/simulate_prod.py +8 -7
- simtools/applications/submit_data_from_external.py +2 -1
- simtools/applications/validate_camera_efficiency.py +28 -27
- simtools/applications/validate_cumulative_psf.py +1 -3
- simtools/applications/validate_optics.py +2 -1
- simtools/atmosphere.py +83 -0
- simtools/camera/camera_efficiency.py +171 -48
- simtools/camera/single_photon_electron_spectrum.py +6 -6
- simtools/configuration/commandline_parser.py +47 -9
- simtools/constants.py +5 -0
- simtools/corsika/corsika_config.py +88 -185
- simtools/corsika/corsika_histograms.py +246 -69
- simtools/data_model/model_data_writer.py +46 -49
- simtools/data_model/schema.py +2 -0
- simtools/db/db_handler.py +4 -2
- simtools/db/mongo_db.py +2 -2
- simtools/io/ascii_handler.py +52 -4
- simtools/io/io_handler.py +23 -12
- simtools/job_execution/job_manager.py +154 -79
- simtools/job_execution/process_pool.py +137 -0
- simtools/layout/array_layout.py +0 -1
- simtools/layout/array_layout_utils.py +143 -21
- simtools/model/array_model.py +22 -50
- simtools/model/calibration_model.py +4 -4
- simtools/model/model_parameter.py +123 -73
- simtools/model/model_utils.py +40 -1
- simtools/model/site_model.py +4 -4
- simtools/model/telescope_model.py +4 -5
- simtools/ray_tracing/incident_angles.py +87 -6
- simtools/ray_tracing/mirror_panel_psf.py +337 -217
- simtools/ray_tracing/psf_analysis.py +57 -42
- simtools/ray_tracing/psf_parameter_optimisation.py +3 -2
- simtools/ray_tracing/ray_tracing.py +37 -10
- simtools/runners/corsika_runner.py +52 -191
- simtools/runners/corsika_simtel_runner.py +74 -100
- simtools/runners/runner_services.py +214 -213
- simtools/runners/simtel_runner.py +27 -155
- simtools/runners/simtools_runner.py +9 -69
- simtools/schemas/application_workflow.metaschema.yml +8 -0
- simtools/settings.py +19 -0
- simtools/simtel/simtel_config_writer.py +0 -55
- simtools/simtel/simtel_seeds.py +184 -0
- simtools/simtel/simulator_array.py +115 -103
- simtools/simtel/simulator_camera_efficiency.py +66 -42
- simtools/simtel/simulator_light_emission.py +110 -123
- simtools/simtel/simulator_ray_tracing.py +78 -63
- simtools/simulator.py +135 -346
- simtools/testing/sim_telarray_metadata.py +13 -11
- simtools/testing/validate_output.py +87 -19
- simtools/utils/general.py +6 -17
- simtools/utils/random.py +36 -0
- simtools/visualization/plot_corsika_histograms.py +2 -0
- simtools/visualization/plot_incident_angles.py +48 -1
- simtools/visualization/plot_psf.py +160 -18
- {gammasimtools-0.26.0.dist-info → gammasimtools-0.27.1.dist-info}/licenses/LICENSE +0 -0
- {gammasimtools-0.26.0.dist-info → gammasimtools-0.27.1.dist-info}/top_level.txt +0 -0
|
@@ -6,11 +6,11 @@ import numpy as np
|
|
|
6
6
|
|
|
7
7
|
from simtools.sim_events.file_info import get_corsika_run_number
|
|
8
8
|
from simtools.simtel.simtel_config_reader import SimtelConfigReader
|
|
9
|
-
from simtools.simtel.simtel_config_writer import sim_telarray_random_seeds
|
|
10
9
|
from simtools.simtel.simtel_io_metadata import (
|
|
11
10
|
get_sim_telarray_telescope_id,
|
|
12
11
|
read_sim_telarray_metadata,
|
|
13
12
|
)
|
|
13
|
+
from simtools.utils import random
|
|
14
14
|
|
|
15
15
|
_logger = logging.getLogger(__name__)
|
|
16
16
|
|
|
@@ -30,7 +30,7 @@ def assert_sim_telarray_metadata(file, array_model):
|
|
|
30
30
|
_logger.info(f"Found metadata in sim_telarray file for {len(telescope_meta)} telescopes")
|
|
31
31
|
site_parameter_mismatch = _assert_model_parameters(global_meta, array_model.site_model)
|
|
32
32
|
sim_telarray_seed_mismatch = _assert_sim_telarray_seed(
|
|
33
|
-
global_meta, array_model.
|
|
33
|
+
global_meta, array_model.sim_telarray_seed, file
|
|
34
34
|
)
|
|
35
35
|
if sim_telarray_seed_mismatch:
|
|
36
36
|
site_parameter_mismatch.append(sim_telarray_seed_mismatch)
|
|
@@ -101,7 +101,7 @@ def _assert_model_parameters(metadata, model):
|
|
|
101
101
|
return invalid_parameter_list
|
|
102
102
|
|
|
103
103
|
|
|
104
|
-
def _assert_sim_telarray_seed(metadata,
|
|
104
|
+
def _assert_sim_telarray_seed(metadata, sim_telarray_seed, file=None):
|
|
105
105
|
"""
|
|
106
106
|
Assert that sim_telarray seed matches the values in the sim_telarray metadata.
|
|
107
107
|
|
|
@@ -111,8 +111,8 @@ def _assert_sim_telarray_seed(metadata, sim_telarray_seeds, file=None):
|
|
|
111
111
|
----------
|
|
112
112
|
metadata: dict
|
|
113
113
|
Metadata dictionary.
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
sim_telarray_seed: SimtelSeeds
|
|
115
|
+
sim_telarray seed.
|
|
116
116
|
file : Path
|
|
117
117
|
Path to the sim_telarray file.
|
|
118
118
|
|
|
@@ -122,23 +122,25 @@ def _assert_sim_telarray_seed(metadata, sim_telarray_seeds, file=None):
|
|
|
122
122
|
Error message if sim_telarray seeds do not match.
|
|
123
123
|
|
|
124
124
|
"""
|
|
125
|
-
if
|
|
125
|
+
if sim_telarray_seed is None:
|
|
126
126
|
return None
|
|
127
127
|
|
|
128
128
|
if "instrument_seed" in metadata.keys() and "instrument_instances" in metadata.keys():
|
|
129
|
-
if str(metadata.get("instrument_seed")) != str(
|
|
129
|
+
if str(metadata.get("instrument_seed")) != str(sim_telarray_seed.instrument_seed):
|
|
130
130
|
return (
|
|
131
131
|
"Parameter instrument_seed mismatch between sim_telarray file: "
|
|
132
|
-
f"{metadata['instrument_seed']}, and model: {
|
|
132
|
+
f"{metadata['instrument_seed']}, and model: {sim_telarray_seed.instrument_seed}"
|
|
133
133
|
)
|
|
134
134
|
_logger.info(
|
|
135
135
|
f"sim_telarray_seed in sim_telarray file: {metadata['instrument_seed']}, "
|
|
136
|
-
f"and model: {
|
|
136
|
+
f"and model: {sim_telarray_seed.instrument_seed}"
|
|
137
137
|
)
|
|
138
138
|
if file:
|
|
139
139
|
run_number_modified = get_corsika_run_number(file) - 1
|
|
140
|
-
test_seeds =
|
|
141
|
-
int(metadata["
|
|
140
|
+
test_seeds = random.seeds(
|
|
141
|
+
n_seeds=int(metadata["instrument_instances"]),
|
|
142
|
+
max_seed=np.iinfo(np.int32).max,
|
|
143
|
+
fixed_seed=int(metadata["instrument_seed"]),
|
|
142
144
|
)
|
|
143
145
|
# no +1 as in sim_telarray (as we count from 0)
|
|
144
146
|
seed_used = run_number_modified % int(metadata["instrument_instances"])
|
|
@@ -46,7 +46,7 @@ def validate_application_output(config, from_command_line=None, from_config_file
|
|
|
46
46
|
"""
|
|
47
47
|
Validate application output against expected output.
|
|
48
48
|
|
|
49
|
-
Expected output is defined in configuration file.
|
|
49
|
+
Expected output is defined in the test configuration file.
|
|
50
50
|
Some tests run only if the model version from the command line
|
|
51
51
|
equals the model version from the configuration file.
|
|
52
52
|
|
|
@@ -88,6 +88,8 @@ def _validate_output_files(config, integration_test):
|
|
|
88
88
|
if "reference_output_file" in integration_test:
|
|
89
89
|
_validate_reference_output_file(config, integration_test)
|
|
90
90
|
if "test_output_files" in integration_test:
|
|
91
|
+
if isinstance(integration_test["test_output_files"], dict):
|
|
92
|
+
integration_test["test_output_files"] = [integration_test["test_output_files"]]
|
|
91
93
|
_validate_output_path_and_file(config, integration_test["test_output_files"])
|
|
92
94
|
if "output_file" in integration_test:
|
|
93
95
|
_validate_output_path_and_file(
|
|
@@ -116,11 +118,10 @@ def _test_simtel_cfg_files(config, integration_test, from_command_line, from_con
|
|
|
116
118
|
|
|
117
119
|
def _validate_reference_output_file(config, integration_test):
|
|
118
120
|
"""Compare with reference output file."""
|
|
121
|
+
test_file = integration_test.get("test_output_file") or config["configuration"]["output_file"]
|
|
119
122
|
assert compare_files(
|
|
120
123
|
integration_test["reference_output_file"],
|
|
121
|
-
Path(config["configuration"]["output_path"]).joinpath(
|
|
122
|
-
config["configuration"]["output_file"]
|
|
123
|
-
),
|
|
124
|
+
Path(config["configuration"]["output_path"]).joinpath(test_file),
|
|
124
125
|
integration_test.get("tolerance", 1.0e-5),
|
|
125
126
|
integration_test.get("test_columns", None),
|
|
126
127
|
)
|
|
@@ -189,6 +190,7 @@ def _validate_model_parameter_json_file(config, model_parameter_validation):
|
|
|
189
190
|
model_parameter["value"],
|
|
190
191
|
reference_model_parameter[reference_parameter_name]["value"],
|
|
191
192
|
model_parameter_validation["tolerance"],
|
|
193
|
+
model_parameter_validation.get("scaling", 1.0),
|
|
192
194
|
)
|
|
193
195
|
|
|
194
196
|
|
|
@@ -227,12 +229,56 @@ def compare_files(file1, file2, tolerance=1.0e-5, test_columns=None):
|
|
|
227
229
|
return False
|
|
228
230
|
|
|
229
231
|
|
|
232
|
+
def _compare_nested_dicts_with_tolerance(data1, data2, tolerance, is_value_field=False):
|
|
233
|
+
"""
|
|
234
|
+
Recursively compare nested dictionaries, applying allclose to "value" fields.
|
|
235
|
+
|
|
236
|
+
Parameters
|
|
237
|
+
----------
|
|
238
|
+
data1 : dict, list, or scalar
|
|
239
|
+
First data to compare
|
|
240
|
+
data2 : dict, list, or scalar
|
|
241
|
+
Second data to compare
|
|
242
|
+
tolerance : float
|
|
243
|
+
Tolerance for comparing numerical values
|
|
244
|
+
is_value_field : bool
|
|
245
|
+
Whether this data is from a "value" key (applies tolerance to scalars)
|
|
246
|
+
|
|
247
|
+
Returns
|
|
248
|
+
-------
|
|
249
|
+
bool
|
|
250
|
+
True if the data are equal within tolerance
|
|
251
|
+
"""
|
|
252
|
+
if isinstance(data1, dict) and isinstance(data2, dict):
|
|
253
|
+
return data1.keys() == data2.keys() and all(
|
|
254
|
+
_compare_nested_dicts_with_tolerance(
|
|
255
|
+
data1[k], data2[k], tolerance, is_value_field=(k == "value")
|
|
256
|
+
)
|
|
257
|
+
for k in data1
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
if isinstance(data1, (list, tuple)) and isinstance(data2, (list, tuple)):
|
|
261
|
+
return len(data1) == len(data2) and all(
|
|
262
|
+
_compare_nested_dicts_with_tolerance(v1, v2, tolerance, is_value_field)
|
|
263
|
+
for v1, v2 in zip(data1, data2)
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# Apply tolerance if this is a "value" field, otherwise use exact equality
|
|
267
|
+
if is_value_field:
|
|
268
|
+
try:
|
|
269
|
+
return _compare_value_from_parameter_dict(data1, data2, tolerance)
|
|
270
|
+
except (TypeError, ValueError):
|
|
271
|
+
return data1 == data2
|
|
272
|
+
return data1 == data2
|
|
273
|
+
|
|
274
|
+
|
|
230
275
|
def compare_json_or_yaml_files(file1, file2, tolerance=1.0e-2):
|
|
231
276
|
"""
|
|
232
277
|
Compare two json or yaml files.
|
|
233
278
|
|
|
234
279
|
Take into account float comparison for sim_telarray string-embedded floats.
|
|
235
280
|
Allow differences in 'schema_version' field.
|
|
281
|
+
Works recursively for nested dicts with "value" fields on any level.
|
|
236
282
|
|
|
237
283
|
Parameters
|
|
238
284
|
----------
|
|
@@ -259,24 +305,45 @@ def compare_json_or_yaml_files(file1, file2, tolerance=1.0e-2):
|
|
|
259
305
|
if data1 == data2:
|
|
260
306
|
return True
|
|
261
307
|
|
|
262
|
-
if data1
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
else data1[k] == data2[k]
|
|
270
|
-
)
|
|
271
|
-
for k in data1
|
|
308
|
+
if isinstance(data1, dict) and isinstance(data2, dict):
|
|
309
|
+
if data1.keys() != data2.keys():
|
|
310
|
+
_logger.error(f"Keys do not match: {data1.keys()} and {data2.keys()}")
|
|
311
|
+
return False
|
|
312
|
+
|
|
313
|
+
_comparison = _compare_nested_dicts_with_tolerance(
|
|
314
|
+
data1, data2, tolerance, is_value_field=False
|
|
272
315
|
)
|
|
273
316
|
if not _comparison:
|
|
274
317
|
_logger.error(f"Values do not match: {data1} and {data2} (tolerance: {tolerance})")
|
|
275
318
|
return _comparison
|
|
276
319
|
|
|
277
320
|
|
|
278
|
-
def _compare_value_from_parameter_dict(
|
|
279
|
-
"""
|
|
321
|
+
def _compare_value_from_parameter_dict(data_1, data_2, tolerance=1.0e-5, factor_1=1.0):
|
|
322
|
+
"""
|
|
323
|
+
Compare value fields given in different formats.
|
|
324
|
+
|
|
325
|
+
Parameters
|
|
326
|
+
----------
|
|
327
|
+
data_1 : float, int, str, list, numpy.ndarray
|
|
328
|
+
First value or collection of values to compare. May be a scalar,
|
|
329
|
+
a sequence, a numpy array, or a string representation of a list.
|
|
330
|
+
data_2 : float, int, str, list, numpy.ndarray
|
|
331
|
+
Second value or collection of values to compare, with the same
|
|
332
|
+
allowed formats as ``data_2``.
|
|
333
|
+
tolerance : float, optional
|
|
334
|
+
Relative tolerance used when comparing numerical values via
|
|
335
|
+
``numpy.allclose``.
|
|
336
|
+
factor1 : float, optional
|
|
337
|
+
Multiplicative factor applied to ``data_1`` before comparison. This
|
|
338
|
+
can be used to account for unit conversions or normalisation
|
|
339
|
+
differences between ``data_1`` and ``data_2``.
|
|
340
|
+
|
|
341
|
+
Returns
|
|
342
|
+
-------
|
|
343
|
+
bool
|
|
344
|
+
True if the two values are considered equal within the given
|
|
345
|
+
tolerance, False otherwise.
|
|
346
|
+
"""
|
|
280
347
|
|
|
281
348
|
def _as_list(value):
|
|
282
349
|
if isinstance(value, str):
|
|
@@ -285,12 +352,13 @@ def _compare_value_from_parameter_dict(data1, data2, tolerance=1.0e-5):
|
|
|
285
352
|
return value
|
|
286
353
|
return [value]
|
|
287
354
|
|
|
288
|
-
_logger.info(f"Comparing values: {
|
|
355
|
+
_logger.info(f"Comparing values: {data_1} and {data_2} (tolerance: {tolerance})")
|
|
289
356
|
|
|
290
|
-
_as_list_1 = _as_list(
|
|
291
|
-
_as_list_2 = _as_list(
|
|
357
|
+
_as_list_1 = _as_list(data_1)
|
|
358
|
+
_as_list_2 = _as_list(data_2)
|
|
292
359
|
if isinstance(_as_list_1, str):
|
|
293
360
|
return _as_list_1 == _as_list_2
|
|
361
|
+
_as_list_1 = np.array(_as_list_1) * factor_1
|
|
294
362
|
return np.allclose(_as_list_1, _as_list_2, rtol=tolerance)
|
|
295
363
|
|
|
296
364
|
|
simtools/utils/general.py
CHANGED
|
@@ -582,6 +582,12 @@ def validate_data_type(reference_dtype, value=None, dtype=None, allow_subtypes=T
|
|
|
582
582
|
if reference_dtype in ("boolean", "bool"):
|
|
583
583
|
return _is_valid_boolean_type(dtype, value)
|
|
584
584
|
|
|
585
|
+
if reference_dtype == "dict":
|
|
586
|
+
return isinstance(value, dict)
|
|
587
|
+
|
|
588
|
+
if reference_dtype == "list":
|
|
589
|
+
return isinstance(value, list)
|
|
590
|
+
|
|
585
591
|
return _is_valid_numeric_type(dtype, reference_dtype)
|
|
586
592
|
|
|
587
593
|
|
|
@@ -796,23 +802,6 @@ def find_differences_in_json_objects(obj1, obj2, path=""):
|
|
|
796
802
|
return diffs
|
|
797
803
|
|
|
798
804
|
|
|
799
|
-
def clear_default_sim_telarray_cfg_directories(command):
|
|
800
|
-
"""Prefix the command to clear default sim_telarray configuration directories.
|
|
801
|
-
|
|
802
|
-
Parameters
|
|
803
|
-
----------
|
|
804
|
-
command: str
|
|
805
|
-
Command to be prefixed.
|
|
806
|
-
|
|
807
|
-
Returns
|
|
808
|
-
-------
|
|
809
|
-
str
|
|
810
|
-
Prefixed command.
|
|
811
|
-
|
|
812
|
-
"""
|
|
813
|
-
return f"SIM_TELARRAY_CONFIG_PATH='' {command}"
|
|
814
|
-
|
|
815
|
-
|
|
816
805
|
def get_list_of_files_from_command_line(file_names, suffix_list):
|
|
817
806
|
"""
|
|
818
807
|
Get a list of files from the command line.
|
simtools/utils/random.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Random numbers utilities."""
|
|
2
|
+
|
|
3
|
+
import secrets
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def seeds(n_seeds=1, min_seed=1, max_seed=2_000_000_000, fixed_seed=None):
|
|
9
|
+
"""
|
|
10
|
+
Generate independent random seeds.
|
|
11
|
+
|
|
12
|
+
Parameters
|
|
13
|
+
----------
|
|
14
|
+
n_seeds : int
|
|
15
|
+
Number of seeds to generate.
|
|
16
|
+
min_seed : int
|
|
17
|
+
Lower limit for the seed (inclusive).
|
|
18
|
+
max_seed : int
|
|
19
|
+
Upper limit for the seed (exclusive).
|
|
20
|
+
fixed_seed : int or None
|
|
21
|
+
If provided, use this fixed seed.
|
|
22
|
+
|
|
23
|
+
Returns
|
|
24
|
+
-------
|
|
25
|
+
int or list of int:
|
|
26
|
+
A single seed if n_seeds is 1, otherwise a list of seeds.
|
|
27
|
+
"""
|
|
28
|
+
entropy = fixed_seed if fixed_seed is not None else secrets.randbits(128)
|
|
29
|
+
ss = np.random.SeedSequence(entropy)
|
|
30
|
+
rng = np.random.default_rng(ss)
|
|
31
|
+
|
|
32
|
+
seed_list = rng.integers(low=min_seed, high=max_seed, size=n_seeds)
|
|
33
|
+
|
|
34
|
+
if n_seeds == 1:
|
|
35
|
+
return int(seed_list[0])
|
|
36
|
+
return [int(x) for x in seed_list]
|
|
@@ -149,6 +149,8 @@ def _plot_1d(hist_list, labels=None):
|
|
|
149
149
|
ax.set_xlabel(_get_axis_label(hist["x_axis_title"], hist["x_axis_unit"]))
|
|
150
150
|
ax.set_ylabel(_get_axis_label(hist["y_axis_title"], hist["y_axis_unit"]))
|
|
151
151
|
_configure_plot_scales(ax, hist)
|
|
152
|
+
if "y_axis_min" in hist and hist["y_axis_min"] is not None:
|
|
153
|
+
ax.set_ylim(bottom=float(hist["y_axis_min"]))
|
|
152
154
|
ax.set_title(f"{hist['title']}")
|
|
153
155
|
ax.legend()
|
|
154
156
|
ax.grid(True, alpha=0.3)
|
|
@@ -351,6 +351,7 @@ def _plot_component_angles(
|
|
|
351
351
|
out_path,
|
|
352
352
|
bin_width_deg,
|
|
353
353
|
log,
|
|
354
|
+
model_version=None,
|
|
354
355
|
):
|
|
355
356
|
arrays = _gather_angle_arrays(results_by_offset, column, log)
|
|
356
357
|
if not arrays:
|
|
@@ -365,6 +366,17 @@ def _plot_component_angles(
|
|
|
365
366
|
ax.set_title(f"Incident angle {title_suffix} vs off-axis angle")
|
|
366
367
|
ax.grid(True, alpha=0.3)
|
|
367
368
|
ax.legend()
|
|
369
|
+
if model_version:
|
|
370
|
+
ax.text(
|
|
371
|
+
0.03,
|
|
372
|
+
0.97,
|
|
373
|
+
f"Model version: {model_version}",
|
|
374
|
+
transform=ax.transAxes,
|
|
375
|
+
fontsize=8,
|
|
376
|
+
verticalalignment="top",
|
|
377
|
+
horizontalalignment="left",
|
|
378
|
+
bbox={"boxstyle": "round", "facecolor": "white", "alpha": 0.5},
|
|
379
|
+
)
|
|
368
380
|
plt.tight_layout()
|
|
369
381
|
plt.savefig(out_path, dpi=300)
|
|
370
382
|
plt.close(fig)
|
|
@@ -378,8 +390,30 @@ def plot_incident_angles(
|
|
|
378
390
|
radius_bin_width_m=0.01,
|
|
379
391
|
debug_plots=False,
|
|
380
392
|
logger=None,
|
|
393
|
+
model_version=None,
|
|
381
394
|
):
|
|
382
|
-
"""Plot overlaid histograms of focal, primary, secondary angles, and primary hit radius.
|
|
395
|
+
"""Plot overlaid histograms of focal, primary, secondary angles, and primary hit radius.
|
|
396
|
+
|
|
397
|
+
Parameters
|
|
398
|
+
----------
|
|
399
|
+
results_by_offset : dict
|
|
400
|
+
Mapping from off-axis angle to result tables containing angle and radius columns.
|
|
401
|
+
output_dir : path-like
|
|
402
|
+
Base output directory where the ``plots`` subdirectory will be created.
|
|
403
|
+
label : str
|
|
404
|
+
Label used to distinguish this set of plots in the output filenames.
|
|
405
|
+
bin_width_deg : float, optional
|
|
406
|
+
Bin width in degrees for the angle-of-incidence histograms.
|
|
407
|
+
radius_bin_width_m : float, optional
|
|
408
|
+
Bin width in meters for the primary mirror hit-radius histograms.
|
|
409
|
+
debug_plots : bool, optional
|
|
410
|
+
If True, generate additional diagnostic plots.
|
|
411
|
+
logger : logging.Logger or None, optional
|
|
412
|
+
Logger instance to use for messages. If None, a module-level logger is used.
|
|
413
|
+
model_version : str or None, optional
|
|
414
|
+
Semantic model version identifier to annotate the generated plots. If None,
|
|
415
|
+
no model version text is added to the figures.
|
|
416
|
+
"""
|
|
383
417
|
log = logger or logging.getLogger(__name__)
|
|
384
418
|
if not results_by_offset:
|
|
385
419
|
log.warning("No results provided for multi-offset plot")
|
|
@@ -402,6 +436,17 @@ def plot_incident_angles(
|
|
|
402
436
|
ax.set_title("Incident angle distribution vs off-axis angle")
|
|
403
437
|
ax.grid(True, alpha=0.3)
|
|
404
438
|
ax.legend()
|
|
439
|
+
if model_version:
|
|
440
|
+
ax.text(
|
|
441
|
+
0.03,
|
|
442
|
+
0.97,
|
|
443
|
+
f"Model version: {model_version}",
|
|
444
|
+
transform=ax.transAxes,
|
|
445
|
+
fontsize=8,
|
|
446
|
+
verticalalignment="top",
|
|
447
|
+
horizontalalignment="left",
|
|
448
|
+
bbox={"boxstyle": "round", "facecolor": "white", "alpha": 0.5},
|
|
449
|
+
)
|
|
405
450
|
plt.tight_layout()
|
|
406
451
|
plt.savefig(out_dir / f"incident_angles_multi_{label}.png", dpi=300)
|
|
407
452
|
plt.close(fig)
|
|
@@ -414,6 +459,7 @@ def plot_incident_angles(
|
|
|
414
459
|
out_path=out_dir / f"incident_angles_primary_multi_{label}.png",
|
|
415
460
|
bin_width_deg=bin_width_deg,
|
|
416
461
|
log=log,
|
|
462
|
+
model_version=model_version,
|
|
417
463
|
)
|
|
418
464
|
_plot_component_angles(
|
|
419
465
|
results_by_offset=results_by_offset,
|
|
@@ -422,6 +468,7 @@ def plot_incident_angles(
|
|
|
422
468
|
out_path=out_dir / f"incident_angles_secondary_multi_{label}.png",
|
|
423
469
|
bin_width_deg=bin_width_deg,
|
|
424
470
|
log=log,
|
|
471
|
+
model_version=model_version,
|
|
425
472
|
)
|
|
426
473
|
|
|
427
474
|
# Debug plots
|
|
@@ -6,6 +6,7 @@ including parameter comparison plots, convergence plots, and PSF diameter vs off
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import logging
|
|
9
|
+
from pathlib import Path
|
|
9
10
|
|
|
10
11
|
import astropy.units as u
|
|
11
12
|
import matplotlib.pyplot as plt
|
|
@@ -141,22 +142,27 @@ def _create_base_plot_figure(data_to_plot, simulated_data=None):
|
|
|
141
142
|
|
|
142
143
|
|
|
143
144
|
def _build_parameter_title(pars, is_best):
|
|
144
|
-
"""Build parameter title string for plots."""
|
|
145
|
+
"""Build parameter title string for plots, handling optional parameter groups."""
|
|
145
146
|
title_prefix = "* " if is_best else ""
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
f"{
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
147
|
+
title_lines = []
|
|
148
|
+
|
|
149
|
+
if "mirror_reflection_random_angle" in pars:
|
|
150
|
+
refl = pars["mirror_reflection_random_angle"]
|
|
151
|
+
title_lines.append(f"reflection = {refl[0]:.5f}, {refl[1]:.5f}, {refl[2]:.5f}")
|
|
152
|
+
|
|
153
|
+
if "mirror_align_random_vertical" in pars:
|
|
154
|
+
vert = pars["mirror_align_random_vertical"]
|
|
155
|
+
title_lines.append(
|
|
156
|
+
f"align_vertical = {vert[0]:.5f}, {vert[1]:.5f}, {vert[2]:.5f}, {vert[3]:.5f}"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if "mirror_align_random_horizontal" in pars:
|
|
160
|
+
horiz = pars["mirror_align_random_horizontal"]
|
|
161
|
+
title_lines.append(
|
|
162
|
+
f"align_horizontal = {horiz[0]:.5f}, {horiz[1]:.5f}, {horiz[2]:.5f}, {horiz[3]:.5f}"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
return title_prefix + "\n".join(title_lines)
|
|
160
166
|
|
|
161
167
|
|
|
162
168
|
def _add_metric_text_box(ax, metrics_text, is_best):
|
|
@@ -640,7 +646,7 @@ def create_psf_vs_offaxis_plot(tel_model, site_model, args_dict, best_pars, outp
|
|
|
640
646
|
logger.info(f"Creating {psf_label_cm} vs off-axis angle plot with best parameters...")
|
|
641
647
|
|
|
642
648
|
# Apply best parameters to telescope model
|
|
643
|
-
tel_model.overwrite_parameters(best_pars)
|
|
649
|
+
tel_model.overwrite_parameters(best_pars, flat_dict=True)
|
|
644
650
|
|
|
645
651
|
# Create off-axis angle array
|
|
646
652
|
max_offset = args_dict.get("max_offset", MAX_OFFSET_DEFAULT)
|
|
@@ -654,6 +660,7 @@ def create_psf_vs_offaxis_plot(tel_model, site_model, args_dict, best_pars, outp
|
|
|
654
660
|
ray = RayTracing(
|
|
655
661
|
telescope_model=tel_model,
|
|
656
662
|
site_model=site_model,
|
|
663
|
+
label=args_dict.get("label") or getattr(tel_model, "label", None),
|
|
657
664
|
zenith_angle=args_dict["zenith"] * u.deg,
|
|
658
665
|
source_distance=args_dict["src_distance"] * u.km,
|
|
659
666
|
off_axis_angle=off_axis_angles * u.deg,
|
|
@@ -661,9 +668,9 @@ def create_psf_vs_offaxis_plot(tel_model, site_model, args_dict, best_pars, outp
|
|
|
661
668
|
|
|
662
669
|
logger.info(f"Running ray tracing for {len(off_axis_angles)} off-axis angles...")
|
|
663
670
|
ray.simulate(test=args_dict.get("test", False), force=True)
|
|
664
|
-
ray.analyze(force=True)
|
|
671
|
+
ray.analyze(force=True, containment_fraction=fraction)
|
|
665
672
|
|
|
666
|
-
for key in ["
|
|
673
|
+
for key in ["psf_cm", "psf_deg"]:
|
|
667
674
|
plt.figure(figsize=(10, 6), tight_layout=True)
|
|
668
675
|
|
|
669
676
|
ray.plot(key, marker="o", linestyle="-", color="blue", linewidth=2, markersize=6)
|
|
@@ -772,3 +779,138 @@ def create_optimization_plots(args_dict, gd_results, tel_model, data_to_plot, ou
|
|
|
772
779
|
use_ks_statistic=False,
|
|
773
780
|
)
|
|
774
781
|
pdf_pages.close()
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
def create_summary_psf_comparison_plot(
|
|
785
|
+
tel_model, optimized_params, data_to_plot, output_dir, final_rmsd, simulated_data
|
|
786
|
+
):
|
|
787
|
+
"""
|
|
788
|
+
Create a standalone plot comparing measured vs simulated PSF with final optimized parameters.
|
|
789
|
+
|
|
790
|
+
This creates a single plot showing the cumulative PSF comparison
|
|
791
|
+
before and after the optimization.
|
|
792
|
+
|
|
793
|
+
Parameters
|
|
794
|
+
----------
|
|
795
|
+
tel_model : TelescopeModel
|
|
796
|
+
Telescope model with optimized parameters
|
|
797
|
+
optimized_params : dict
|
|
798
|
+
Dictionary of optimized parameter values
|
|
799
|
+
data_to_plot : dict
|
|
800
|
+
Measured PSF data
|
|
801
|
+
output_dir : Path
|
|
802
|
+
Directory for output files
|
|
803
|
+
final_rmsd : float
|
|
804
|
+
Final RMSD value at the end of optimization
|
|
805
|
+
simulated_data : dict
|
|
806
|
+
Final simulated PSF data with optimized parameters
|
|
807
|
+
|
|
808
|
+
Returns
|
|
809
|
+
-------
|
|
810
|
+
Path
|
|
811
|
+
Path to the created plot file
|
|
812
|
+
"""
|
|
813
|
+
fig, ax = _create_base_plot_figure(data_to_plot, simulated_data)
|
|
814
|
+
|
|
815
|
+
title_lines = ["Final Optimized Parameters:"]
|
|
816
|
+
if "mirror_reflection_random_angle" in optimized_params:
|
|
817
|
+
refl = optimized_params["mirror_reflection_random_angle"]
|
|
818
|
+
title_lines.append(
|
|
819
|
+
f"mirror_reflection_random_angle = [{refl[0]:.6f}, {refl[1]:.6f}, {refl[2]:.6f}]"
|
|
820
|
+
)
|
|
821
|
+
|
|
822
|
+
if "mirror_align_random_vertical" in optimized_params:
|
|
823
|
+
vert = optimized_params["mirror_align_random_vertical"]
|
|
824
|
+
title_lines.append(
|
|
825
|
+
f"mirror_align_random_vertical = "
|
|
826
|
+
f"[{vert[0]:.6f}, {vert[1]:.6f}, {vert[2]:.6f}, {vert[3]:.6f}]"
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
if "mirror_align_random_horizontal" in optimized_params:
|
|
830
|
+
horiz = optimized_params["mirror_align_random_horizontal"]
|
|
831
|
+
title_lines.append(
|
|
832
|
+
f"mirror_align_random_horizontal = "
|
|
833
|
+
f"[{horiz[0]:.6f}, {horiz[1]:.6f}, {horiz[2]:.6f}, {horiz[3]:.6f}]"
|
|
834
|
+
)
|
|
835
|
+
|
|
836
|
+
ax.set_title("\n".join(title_lines), fontsize=9, loc="left")
|
|
837
|
+
|
|
838
|
+
rmsd_text = f"RMSD = {final_rmsd:.4f} ({final_rmsd * 100:.2f}%)"
|
|
839
|
+
ax.text(
|
|
840
|
+
0.98,
|
|
841
|
+
0.02,
|
|
842
|
+
rmsd_text,
|
|
843
|
+
transform=ax.transAxes,
|
|
844
|
+
fontsize=12,
|
|
845
|
+
verticalalignment="bottom",
|
|
846
|
+
horizontalalignment="right",
|
|
847
|
+
bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.8},
|
|
848
|
+
)
|
|
849
|
+
|
|
850
|
+
output_file = Path(output_dir) / f"{tel_model.name}_final_psf_comparison.png"
|
|
851
|
+
fig.savefig(output_file, dpi=150, bbox_inches="tight")
|
|
852
|
+
plt.close(fig)
|
|
853
|
+
|
|
854
|
+
logger.info(f"Final PSF comparison plot saved to {output_file}")
|
|
855
|
+
return output_file
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
def plot_psf_histogram(measured, simulated, args_dict):
|
|
859
|
+
"""Write histogram comparing measured vs simulated PSF diameter distributions."""
|
|
860
|
+
output_dir = Path(args_dict.get("output_path", "."))
|
|
861
|
+
out_name = args_dict.get("psf_hist")
|
|
862
|
+
if not out_name:
|
|
863
|
+
return None
|
|
864
|
+
out_path = Path(out_name)
|
|
865
|
+
if not out_path.is_absolute():
|
|
866
|
+
out_path = output_dir / out_path
|
|
867
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
868
|
+
measured = np.asarray(measured, dtype=float)
|
|
869
|
+
simulated = np.asarray(simulated, dtype=float)
|
|
870
|
+
measured = measured[np.isfinite(measured)]
|
|
871
|
+
simulated = simulated[np.isfinite(simulated)]
|
|
872
|
+
if measured.size == 0 or simulated.size == 0:
|
|
873
|
+
return None
|
|
874
|
+
bins = 25
|
|
875
|
+
all_vals = np.concatenate([measured, simulated])
|
|
876
|
+
x_min = float(np.nanmin(all_vals))
|
|
877
|
+
x_max = float(np.nanmax(all_vals))
|
|
878
|
+
if not np.isfinite(x_min) or not np.isfinite(x_max) or x_max <= x_min:
|
|
879
|
+
return None
|
|
880
|
+
bin_edges = np.linspace(x_min, x_max, bins + 1)
|
|
881
|
+
meas_mean = float(np.mean(measured))
|
|
882
|
+
meas_rms = float(np.std(measured, ddof=0))
|
|
883
|
+
sim_mean = float(np.mean(simulated))
|
|
884
|
+
sim_rms = float(np.std(simulated, ddof=0))
|
|
885
|
+
fig, ax = plt.subplots(figsize=(7.5, 4.5), constrained_layout=True)
|
|
886
|
+
ax.hist(
|
|
887
|
+
measured,
|
|
888
|
+
bins=bin_edges,
|
|
889
|
+
alpha=0.55,
|
|
890
|
+
color="tab:red",
|
|
891
|
+
edgecolor="white",
|
|
892
|
+
label=f"Measured (mean={meas_mean:.2f} mm, rms={meas_rms:.2f} mm)",
|
|
893
|
+
)
|
|
894
|
+
ax.hist(
|
|
895
|
+
simulated,
|
|
896
|
+
bins=bin_edges,
|
|
897
|
+
alpha=0.55,
|
|
898
|
+
color="tab:blue",
|
|
899
|
+
edgecolor="white",
|
|
900
|
+
label=f"Simulated (mean={sim_mean:.2f} mm, rms={sim_rms:.2f} mm)",
|
|
901
|
+
)
|
|
902
|
+
ax.axvline(meas_mean, color="tab:red", linestyle="--", linewidth=1)
|
|
903
|
+
ax.axvline(sim_mean, color="tab:blue", linestyle="--", linewidth=1)
|
|
904
|
+
tel = args_dict.get("telescope", "")
|
|
905
|
+
model_version = args_dict.get("model_version", "")
|
|
906
|
+
fraction = args_dict.get("fraction")
|
|
907
|
+
label = get_psf_diameter_label(fraction, unit="mm") if fraction is not None else "PSF"
|
|
908
|
+
suffix = " ".join([s for s in (tel, model_version) if s])
|
|
909
|
+
ax.set_xlabel(label)
|
|
910
|
+
ax.set_ylabel("Count")
|
|
911
|
+
ax.set_title(f"{label} ({suffix})" if suffix else label)
|
|
912
|
+
ax.legend(loc="best", fontsize=9, frameon=True)
|
|
913
|
+
fig.savefig(out_path)
|
|
914
|
+
plt.close(fig)
|
|
915
|
+
logger.info("PSF histogram written to %s", str(out_path))
|
|
916
|
+
return str(out_path)
|
|
File without changes
|
|
File without changes
|