gammasimtools 0.26.0__py3-none-any.whl → 0.27.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.
- {gammasimtools-0.26.0.dist-info → gammasimtools-0.27.0.dist-info}/METADATA +5 -1
- {gammasimtools-0.26.0.dist-info → gammasimtools-0.27.0.dist-info}/RECORD +70 -66
- {gammasimtools-0.26.0.dist-info → gammasimtools-0.27.0.dist-info}/WHEEL +1 -1
- {gammasimtools-0.26.0.dist-info → gammasimtools-0.27.0.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 +51 -3
- 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.0.dist-info}/licenses/LICENSE +0 -0
- {gammasimtools-0.26.0.dist-info → gammasimtools-0.27.0.dist-info}/top_level.txt +0 -0
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
"""Extract Cherenkov photons from a CORSIKA IACT file and fill histograms."""
|
|
2
2
|
|
|
3
|
-
import functools
|
|
4
3
|
import logging
|
|
5
|
-
import operator
|
|
6
4
|
from pathlib import Path
|
|
7
5
|
|
|
8
6
|
import boost_histogram as bh
|
|
@@ -21,6 +19,9 @@ class CorsikaHistograms:
|
|
|
21
19
|
----------
|
|
22
20
|
input_file: str or Path
|
|
23
21
|
CORSIKA IACT file.
|
|
22
|
+
axis_distance: astropy.units.Quantity or float
|
|
23
|
+
Distance from the axis to consider when calculating the lateral density profiles
|
|
24
|
+
along x and y axes. If a float is given, it is assumed to be in meters.
|
|
24
25
|
|
|
25
26
|
Raises
|
|
26
27
|
------
|
|
@@ -28,30 +29,23 @@ class CorsikaHistograms:
|
|
|
28
29
|
if the input file given does not exist.
|
|
29
30
|
"""
|
|
30
31
|
|
|
31
|
-
def __init__(self, input_file):
|
|
32
|
+
def __init__(self, input_file, normalization_method="per-telescope", axis_distance=1000 * u.m):
|
|
32
33
|
self._logger = logging.getLogger(__name__)
|
|
33
|
-
self._logger.debug("Init CorsikaHistograms")
|
|
34
34
|
self.input_file = Path(input_file)
|
|
35
35
|
if not self.input_file.exists():
|
|
36
36
|
raise FileNotFoundError(f"File {self.input_file} does not exist.")
|
|
37
37
|
|
|
38
|
+
self.axis_distance = (
|
|
39
|
+
axis_distance.to(u.m).value if isinstance(axis_distance, u.Quantity) else axis_distance
|
|
40
|
+
)
|
|
38
41
|
self.events = None
|
|
39
42
|
self.hist = self._set_2d_distributions()
|
|
40
43
|
self.hist.update(self._set_1d_distributions())
|
|
44
|
+
self._density_samples = []
|
|
45
|
+
self.normalization_method = normalization_method
|
|
41
46
|
|
|
42
47
|
def fill(self):
|
|
43
|
-
"""
|
|
44
|
-
Fill Cherenkov photons histograms.
|
|
45
|
-
|
|
46
|
-
Returns
|
|
47
|
-
-------
|
|
48
|
-
list: list of boost_histogram.Histogram instances.
|
|
49
|
-
|
|
50
|
-
Raises
|
|
51
|
-
------
|
|
52
|
-
AttributeError:
|
|
53
|
-
if event has not photon saved.
|
|
54
|
-
"""
|
|
48
|
+
"""Fill Cherenkov photons histograms."""
|
|
55
49
|
self._read_event_headers()
|
|
56
50
|
|
|
57
51
|
with IACTFile(self.input_file) as f:
|
|
@@ -59,7 +53,7 @@ class CorsikaHistograms:
|
|
|
59
53
|
for event_counter, event in enumerate(f):
|
|
60
54
|
if hasattr(event, "photon_bunches"):
|
|
61
55
|
photons = list(event.photon_bunches.values())
|
|
62
|
-
self._fill_histograms(photons, event_counter, telescope_positions,
|
|
56
|
+
self._fill_histograms(photons, event_counter, telescope_positions, False)
|
|
63
57
|
|
|
64
58
|
self._update_distributions()
|
|
65
59
|
|
|
@@ -155,7 +149,10 @@ class CorsikaHistograms:
|
|
|
155
149
|
incoming direction of the primary particle.
|
|
156
150
|
"""
|
|
157
151
|
hist_str = "histogram"
|
|
158
|
-
|
|
152
|
+
photons_per_telescope = np.zeros(len(telescope_positions))
|
|
153
|
+
zenith_rad = np.deg2rad(self.events["zenith_deg"][event_counter])
|
|
154
|
+
|
|
155
|
+
for tel_idx, (photon, telescope) in enumerate(zip(photons, telescope_positions)):
|
|
159
156
|
if rotate_photons:
|
|
160
157
|
px, py = rotate(
|
|
161
158
|
photon["x"],
|
|
@@ -166,16 +163,16 @@ class CorsikaHistograms:
|
|
|
166
163
|
else:
|
|
167
164
|
px, py = photon["x"], photon["y"]
|
|
168
165
|
|
|
169
|
-
px
|
|
170
|
-
py
|
|
166
|
+
px = px - telescope["x"]
|
|
167
|
+
py = py - telescope["y"]
|
|
171
168
|
w = photon["photons"]
|
|
172
169
|
|
|
173
170
|
pxm = px * u.cm.to(u.m)
|
|
174
171
|
pym = py * u.cm.to(u.m)
|
|
175
172
|
zem = (photon["zem"] * u.cm).to(u.km)
|
|
173
|
+
photons_per_telescope[tel_idx] += np.sum(w)
|
|
176
174
|
|
|
177
175
|
self.hist["counts_xy"][hist_str].fill(pxm, pym, weight=w)
|
|
178
|
-
self.hist["density_xy"][hist_str].fill(pxm, pym, weight=w)
|
|
179
176
|
self.hist["direction_xy"][hist_str].fill(photon["cx"], photon["cy"], weight=w)
|
|
180
177
|
self.hist["time_altitude"][hist_str].fill(photon["time"] * u.ns, zem, weight=w)
|
|
181
178
|
self.hist["wavelength_altitude"][hist_str].fill(
|
|
@@ -184,10 +181,23 @@ class CorsikaHistograms:
|
|
|
184
181
|
|
|
185
182
|
r = np.hypot(px, py) * u.cm.to(u.m)
|
|
186
183
|
self.hist["counts_r"][hist_str].fill(r, weight=w)
|
|
187
|
-
self.hist["density_r"][hist_str].fill(r, weight=w)
|
|
188
184
|
|
|
189
185
|
self.events["num_photons"][event_counter] += np.sum(w)
|
|
190
186
|
|
|
187
|
+
for tel_idx, telescope in enumerate(telescope_positions):
|
|
188
|
+
area = np.pi * (telescope["r"] ** 2) / np.cos(zenith_rad) / 1.0e4 # in m^2
|
|
189
|
+
n_photons = photons_per_telescope[tel_idx]
|
|
190
|
+
density = n_photons / area if area > 0 else 0.0
|
|
191
|
+
density_error = np.sqrt(n_photons) / area if area > 0 else 0.0
|
|
192
|
+
self._density_samples.append(
|
|
193
|
+
{
|
|
194
|
+
"x": telescope["x"] * u.cm.to(u.m),
|
|
195
|
+
"y": telescope["y"] * u.cm.to(u.m),
|
|
196
|
+
"density": density,
|
|
197
|
+
"density_error": density_error,
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
|
|
191
201
|
def get_hist_2d_projection(self, hist):
|
|
192
202
|
"""
|
|
193
203
|
Get 2D distributions.
|
|
@@ -318,6 +328,9 @@ class CorsikaHistograms:
|
|
|
318
328
|
x_axis_title = "x_axis_title"
|
|
319
329
|
y_axis_unit = "y_axis_unit"
|
|
320
330
|
y_axis_title = "y_axis_title"
|
|
331
|
+
log_y = "log_y"
|
|
332
|
+
photon_density = "Photon density"
|
|
333
|
+
distance_to_center = "Distance to center"
|
|
321
334
|
hist_1d = {
|
|
322
335
|
"wavelength": {
|
|
323
336
|
file_name: "hist_1d_photon_wavelength_distr",
|
|
@@ -328,27 +341,42 @@ class CorsikaHistograms:
|
|
|
328
341
|
file_name: "hist_1d_photon_radial_distr",
|
|
329
342
|
title: "Photon lateral distribution (ground level)",
|
|
330
343
|
x_bins: [bins, 0 * u.m, r_max, "linear"],
|
|
331
|
-
x_axis_title:
|
|
344
|
+
x_axis_title: distance_to_center,
|
|
332
345
|
x_axis_unit: u.m,
|
|
333
346
|
},
|
|
334
347
|
"density_r": {
|
|
335
348
|
file_name: "hist_1d_photon_density_distr",
|
|
336
349
|
title: "Photon lateral density distribution (ground level)",
|
|
337
350
|
x_bins: [bins, 0 * u.m, r_max, "linear"],
|
|
338
|
-
x_axis_title:
|
|
351
|
+
x_axis_title: distance_to_center,
|
|
352
|
+
x_axis_unit: u.m,
|
|
353
|
+
y_axis_title: photon_density,
|
|
354
|
+
y_axis_unit: u.m**-2,
|
|
355
|
+
},
|
|
356
|
+
"density_r_from_counts": {
|
|
357
|
+
file_name: "hist_1d_photon_density_from_counts_distr",
|
|
358
|
+
title: "Photon lateral density from counts distribution (ground level)",
|
|
359
|
+
x_bins: [bins, 0 * u.m, r_max, "linear"],
|
|
360
|
+
x_axis_title: distance_to_center,
|
|
339
361
|
x_axis_unit: u.m,
|
|
340
|
-
y_axis_title:
|
|
362
|
+
y_axis_title: photon_density,
|
|
341
363
|
y_axis_unit: u.m**-2,
|
|
342
364
|
},
|
|
343
365
|
"density_x": {
|
|
344
366
|
file_name: "hist_1d_photon_density_x_distr",
|
|
345
367
|
title: "Photon lateral density x distribution (ground level)",
|
|
346
|
-
projection: ["
|
|
368
|
+
projection: ["counts_xy", "x"], # projection requires counts_xy histogram
|
|
369
|
+
x_axis_title: distance_to_center,
|
|
370
|
+
x_axis_unit: u.m,
|
|
371
|
+
y_axis_title: photon_density,
|
|
372
|
+
y_axis_unit: u.m**-2,
|
|
347
373
|
},
|
|
348
374
|
"density_y": {
|
|
349
375
|
file_name: "hist_1d_photon_density_y_distr",
|
|
350
376
|
title: "Photon lateral density y distribution (ground level)",
|
|
351
|
-
projection: ["
|
|
377
|
+
projection: ["counts_xy", "y"], # projection requires counts_xy histogram
|
|
378
|
+
y_axis_title: photon_density,
|
|
379
|
+
y_axis_unit: u.m**-2,
|
|
352
380
|
},
|
|
353
381
|
"time": {
|
|
354
382
|
file_name: "hist_1d_photon_time_distr",
|
|
@@ -377,12 +405,13 @@ class CorsikaHistograms:
|
|
|
377
405
|
x_bins: [100, 0, None, "log"],
|
|
378
406
|
x_axis_title: "Cherenkov photons per event",
|
|
379
407
|
x_axis_unit: u.dimensionless_unscaled,
|
|
408
|
+
log_y: False,
|
|
380
409
|
},
|
|
381
410
|
}
|
|
382
411
|
|
|
383
412
|
for value in hist_1d.values():
|
|
384
413
|
value["is_1d"] = True
|
|
385
|
-
value["log_y"] = True
|
|
414
|
+
value["log_y"] = value.get("log_y", True)
|
|
386
415
|
value[y_axis_title] = (
|
|
387
416
|
"Counts" if value.get(y_axis_title) is None else value[y_axis_title]
|
|
388
417
|
)
|
|
@@ -419,6 +448,9 @@ class CorsikaHistograms:
|
|
|
419
448
|
x_axis_title, x_axis_unit = "x_axis_title", "x_axis_unit"
|
|
420
449
|
y_axis_title, y_axis_unit = "y_axis_title", "y_axis_unit"
|
|
421
450
|
z_axis_title, z_axis_unit = "z_axis_title", "z_axis_unit"
|
|
451
|
+
photon_density = "Photon density"
|
|
452
|
+
x_pos = "x position on the ground"
|
|
453
|
+
y_pos = "y position on the ground"
|
|
422
454
|
|
|
423
455
|
hist_2d = {
|
|
424
456
|
"counts_xy": {
|
|
@@ -426,21 +458,33 @@ class CorsikaHistograms:
|
|
|
426
458
|
title: "Photon count distribution (ground level)",
|
|
427
459
|
x_bins: [xy_bin, -xy_maximum, xy_maximum, "linear"],
|
|
428
460
|
y_bins: [xy_bin, -xy_maximum, xy_maximum],
|
|
429
|
-
x_axis_title:
|
|
461
|
+
x_axis_title: x_pos,
|
|
430
462
|
x_axis_unit: xy_maximum.unit,
|
|
431
|
-
y_axis_title:
|
|
463
|
+
y_axis_title: y_pos,
|
|
432
464
|
y_axis_unit: xy_maximum.unit,
|
|
433
465
|
},
|
|
434
466
|
"density_xy": {
|
|
435
467
|
file_name: "hist_2d_photon_density_distr",
|
|
436
|
-
title: "Photon
|
|
468
|
+
title: "Photon density distribution (ground level)",
|
|
469
|
+
x_bins: [xy_bin, -xy_maximum, xy_maximum, "linear"],
|
|
470
|
+
y_bins: [xy_bin, -xy_maximum, xy_maximum, "linear"],
|
|
471
|
+
x_axis_title: x_pos,
|
|
472
|
+
x_axis_unit: xy_maximum.unit,
|
|
473
|
+
y_axis_title: y_pos,
|
|
474
|
+
y_axis_unit: xy_maximum.unit,
|
|
475
|
+
z_axis_title: photon_density,
|
|
476
|
+
z_axis_unit: u.m**-2,
|
|
477
|
+
},
|
|
478
|
+
"density_xy_from_counts": {
|
|
479
|
+
file_name: "hist_2d_photon_density_from_counts_distr",
|
|
480
|
+
title: "Photon density from counts distribution (ground level)",
|
|
437
481
|
x_bins: [xy_bin, -xy_maximum, xy_maximum, "linear"],
|
|
438
482
|
y_bins: [xy_bin, -xy_maximum, xy_maximum, "linear"],
|
|
439
|
-
x_axis_title:
|
|
483
|
+
x_axis_title: x_pos,
|
|
440
484
|
x_axis_unit: xy_maximum.unit,
|
|
441
|
-
y_axis_title:
|
|
485
|
+
y_axis_title: y_pos,
|
|
442
486
|
y_axis_unit: xy_maximum.unit,
|
|
443
|
-
z_axis_title:
|
|
487
|
+
z_axis_title: photon_density,
|
|
444
488
|
z_axis_unit: u.m**-2,
|
|
445
489
|
},
|
|
446
490
|
"direction_xy": {
|
|
@@ -493,50 +537,183 @@ class CorsikaHistograms:
|
|
|
493
537
|
|
|
494
538
|
def _update_distributions(self):
|
|
495
539
|
"""Update the distributions dictionary with the histogram values and bin edges."""
|
|
496
|
-
self.
|
|
540
|
+
self._populate_density_from_probes()
|
|
541
|
+
self._populate_density_from_counts()
|
|
542
|
+
self._filter_density_histograms()
|
|
497
543
|
|
|
498
544
|
for key, value in self.hist.items():
|
|
499
545
|
value["input_file_name"] = str(self.input_file)
|
|
500
|
-
if
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
value
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
546
|
+
if "hist_values" not in value:
|
|
547
|
+
if value["is_1d"]:
|
|
548
|
+
(
|
|
549
|
+
value["hist_values"],
|
|
550
|
+
value["x_bin_edges"],
|
|
551
|
+
value["uncertainties"],
|
|
552
|
+
) = self.get_hist_1d_projection(key, value)
|
|
553
|
+
else:
|
|
554
|
+
(
|
|
555
|
+
value["hist_values"],
|
|
556
|
+
value["x_bin_edges"],
|
|
557
|
+
value["y_bin_edges"],
|
|
558
|
+
value["uncertainties"],
|
|
559
|
+
) = self.get_hist_2d_projection(value["histogram"])
|
|
560
|
+
|
|
561
|
+
def _filter_density_histograms(self):
|
|
562
|
+
"""Filter density histograms based on the normalization method."""
|
|
563
|
+
if self.normalization_method == "per-telescope":
|
|
564
|
+
keys_to_remove = ["density_xy_from_counts", "density_r_from_counts"]
|
|
565
|
+
elif self.normalization_method == "per-bin":
|
|
566
|
+
keys_to_remove = ["density_xy", "density_x", "density_y", "density_r"]
|
|
567
|
+
else:
|
|
568
|
+
raise ValueError(
|
|
569
|
+
f"Unknown normalization_method: {self.normalization_method}. "
|
|
570
|
+
"Must be 'per-telescope' or 'per-bin'."
|
|
571
|
+
)
|
|
511
572
|
|
|
512
|
-
|
|
513
|
-
|
|
573
|
+
for key in keys_to_remove:
|
|
574
|
+
if key in self.hist:
|
|
575
|
+
del self.hist[key]
|
|
514
576
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
else:
|
|
521
|
-
hist /= bin_areas
|
|
577
|
+
def _fill_projected_density_values(self, value):
|
|
578
|
+
"""Extract 1D density by using projection Counts and normalizing by area."""
|
|
579
|
+
histo_2d = value["projection"][0]
|
|
580
|
+
source_h = self.hist[histo_2d]["histogram"]
|
|
581
|
+
project_axis = value["projection"][1]
|
|
522
582
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
583
|
+
if project_axis == "x":
|
|
584
|
+
h_1d = source_h[:, sum]
|
|
585
|
+
total_ortho_width = source_h.axes[1].edges[-1] - source_h.axes[1].edges[0]
|
|
586
|
+
else:
|
|
587
|
+
h_1d = source_h[sum, :]
|
|
588
|
+
total_ortho_width = source_h.axes[0].edges[-1] - source_h.axes[0].edges[0]
|
|
589
|
+
|
|
590
|
+
areas_1d = h_1d.axes[0].widths * total_ortho_width
|
|
591
|
+
|
|
592
|
+
view = h_1d.view()
|
|
593
|
+
if self._check_for_all_attributes(view):
|
|
594
|
+
vals = view["value"] / areas_1d
|
|
595
|
+
uncs = np.sqrt(view["variance"]) / areas_1d
|
|
596
|
+
else:
|
|
597
|
+
vals = view / areas_1d
|
|
598
|
+
uncs = np.sqrt(vals) # Fallback if no weights
|
|
599
|
+
|
|
600
|
+
value["hist_values"] = np.asarray([vals.T])
|
|
601
|
+
value["x_bin_edges"] = np.asarray([h_1d.axes.edges[0]])
|
|
602
|
+
value["uncertainties"] = np.asarray([uncs.T])
|
|
603
|
+
|
|
604
|
+
def _populate_density_from_probes(self):
|
|
605
|
+
"""Build density distributions from per-telescope sampling."""
|
|
606
|
+
if not self._density_samples:
|
|
607
|
+
return
|
|
608
|
+
|
|
609
|
+
s = self._density_samples
|
|
610
|
+
xs, ys = (np.array([p[k] for p in s]) for k in ("x", "y"))
|
|
611
|
+
dens = np.array([p["density"] for p in s])
|
|
612
|
+
errs = np.array([p["density_error"] for p in s])
|
|
613
|
+
|
|
614
|
+
hxy = self.hist["counts_xy"]["histogram"]
|
|
615
|
+
x_edges, y_edges = hxy.axes[0].edges, hxy.axes[1].edges
|
|
616
|
+
r_edges = self.hist["density_r"]["histogram"].axes[0].edges
|
|
617
|
+
|
|
618
|
+
def avg_unc_nd(coords, edges, values, errors):
|
|
619
|
+
num = np.histogramdd(coords, bins=edges, weights=values)[0]
|
|
620
|
+
den = np.histogramdd(coords, bins=edges)[0]
|
|
621
|
+
var = np.histogramdd(coords, bins=edges, weights=errors**2)[0]
|
|
622
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
623
|
+
avg = np.divide(num, den, out=np.zeros_like(num), where=den > 0)
|
|
624
|
+
unc = np.sqrt(np.divide(var, den**2, out=np.zeros_like(var), where=den > 0))
|
|
625
|
+
return avg, unc
|
|
626
|
+
|
|
627
|
+
# 2D
|
|
628
|
+
avg_xy, unc_xy = avg_unc_nd(np.column_stack((xs, ys)), (x_edges, y_edges), dens, errs)
|
|
629
|
+
|
|
630
|
+
self.hist["density_xy"].update(
|
|
631
|
+
{
|
|
632
|
+
"hist_values": np.asarray([avg_xy.T]),
|
|
633
|
+
"x_bin_edges": np.asarray([x_edges]),
|
|
634
|
+
"y_bin_edges": np.asarray([y_edges]),
|
|
635
|
+
"uncertainties": np.asarray([unc_xy.T]),
|
|
636
|
+
}
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
# 1D helpers
|
|
640
|
+
def avg_unc_1d(x, e, v, err):
|
|
641
|
+
return avg_unc_nd(x[:, None], (e,), v, err)
|
|
642
|
+
|
|
643
|
+
ax = self.axis_distance
|
|
530
644
|
|
|
531
|
-
|
|
532
|
-
|
|
645
|
+
avg_x, unc_x = (
|
|
646
|
+
avg_unc_1d(xs[np.abs(ys) < ax], x_edges, dens[np.abs(ys) < ax], errs[np.abs(ys) < ax])
|
|
647
|
+
if np.any(np.abs(ys) < ax)
|
|
648
|
+
else (np.zeros(len(x_edges) - 1),) * 2
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
avg_y, unc_y = (
|
|
652
|
+
avg_unc_1d(ys[np.abs(xs) < ax], y_edges, dens[np.abs(xs) < ax], errs[np.abs(xs) < ax])
|
|
653
|
+
if np.any(np.abs(xs) < ax)
|
|
654
|
+
else (np.zeros(len(y_edges) - 1),) * 2
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
# Radial density
|
|
658
|
+
rs = np.hypot(xs, ys)
|
|
659
|
+
avg_r, unc_r = avg_unc_1d(rs, r_edges, dens, errs)
|
|
660
|
+
|
|
661
|
+
for k, avg, unc, edges in (
|
|
662
|
+
("density_x", avg_x, unc_x, x_edges),
|
|
663
|
+
("density_y", avg_y, unc_y, y_edges),
|
|
664
|
+
("density_r", avg_r, unc_r, r_edges),
|
|
665
|
+
):
|
|
666
|
+
self.hist[k].update(
|
|
667
|
+
{
|
|
668
|
+
"hist_values": np.asarray([avg]),
|
|
669
|
+
"x_bin_edges": np.asarray([edges]),
|
|
670
|
+
"uncertainties": np.asarray([unc]),
|
|
671
|
+
}
|
|
672
|
+
)
|
|
533
673
|
|
|
534
|
-
|
|
535
|
-
|
|
674
|
+
def _populate_density_from_counts(self):
|
|
675
|
+
"""Build density distributions by dividing counts histograms by bin area."""
|
|
676
|
+
# --- 2D ---
|
|
677
|
+
hxy = self.hist["counts_xy"]["histogram"]
|
|
678
|
+
xw = np.diff(hxy.axes[0].edges)
|
|
679
|
+
yw = np.diff(hxy.axes[1].edges)
|
|
680
|
+
areas2d = np.outer(xw, yw)
|
|
681
|
+
|
|
682
|
+
dens_xy, unc_xy = self._density_and_unc(hxy.view(), areas2d)
|
|
683
|
+
|
|
684
|
+
self.hist["density_xy_from_counts"].update(
|
|
685
|
+
{
|
|
686
|
+
"hist_values": np.asarray([dens_xy.T]),
|
|
687
|
+
"x_bin_edges": np.asarray([hxy.axes[0].edges]),
|
|
688
|
+
"y_bin_edges": np.asarray([hxy.axes[1].edges]),
|
|
689
|
+
"uncertainties": np.asarray([unc_xy.T]),
|
|
690
|
+
}
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
# --- 1D ---
|
|
694
|
+
hr = self.hist["counts_r"]["histogram"]
|
|
695
|
+
r = hr.axes[0].edges
|
|
696
|
+
areas1d = np.pi * (r[1:] ** 2 - r[:-1] ** 2)
|
|
536
697
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
698
|
+
dens_r, unc_r = self._density_and_unc(hr.view(), areas1d)
|
|
699
|
+
|
|
700
|
+
self.hist["density_r_from_counts"].update(
|
|
701
|
+
{
|
|
702
|
+
"hist_values": np.asarray([dens_r]),
|
|
703
|
+
"x_bin_edges": np.asarray([r]),
|
|
704
|
+
"uncertainties": np.asarray([unc_r]),
|
|
705
|
+
}
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
def _density_and_unc(self, view, areas):
|
|
709
|
+
"""Calculate density and uncertainty by dividing histogram values by areas."""
|
|
710
|
+
if self._check_for_all_attributes(view):
|
|
711
|
+
values = view["value"]
|
|
712
|
+
unc = np.sqrt(view["variance"])
|
|
713
|
+
else:
|
|
714
|
+
values = view
|
|
715
|
+
unc = np.sqrt(view)
|
|
716
|
+
return values / areas, unc / areas
|
|
540
717
|
|
|
541
718
|
def _check_for_all_attributes(self, view):
|
|
542
719
|
"""Check if view has dtype fields ('value', 'variance')."""
|
|
@@ -7,6 +7,7 @@ import packaging.version
|
|
|
7
7
|
from astropy.io.registry.base import IORegistryError
|
|
8
8
|
|
|
9
9
|
import simtools.utils.general as gen
|
|
10
|
+
from simtools import settings
|
|
10
11
|
from simtools.data_model import schema, validate_data
|
|
11
12
|
from simtools.data_model.metadata_collector import MetadataCollector
|
|
12
13
|
from simtools.db import db_handler
|
|
@@ -20,69 +21,62 @@ class ModelDataWriter:
|
|
|
20
21
|
|
|
21
22
|
Parameters
|
|
22
23
|
----------
|
|
23
|
-
|
|
24
|
+
output_file: str
|
|
24
25
|
Name of output file.
|
|
25
|
-
|
|
26
|
+
output_file_format: str
|
|
26
27
|
Format of output file.
|
|
27
|
-
args_dict: Dictionary
|
|
28
|
-
Dictionary with configuration parameters.
|
|
29
28
|
output_path: str or Path
|
|
30
29
|
Path to output file.
|
|
31
|
-
args_dict: dict
|
|
32
|
-
Dictionary with configuration parameters.
|
|
33
|
-
|
|
34
30
|
"""
|
|
35
31
|
|
|
36
|
-
def __init__(
|
|
37
|
-
self,
|
|
38
|
-
product_data_file=None,
|
|
39
|
-
product_data_format=None,
|
|
40
|
-
output_path=None,
|
|
41
|
-
args_dict=None,
|
|
42
|
-
):
|
|
32
|
+
def __init__(self, output_file=None, output_file_format=None, output_path=None):
|
|
43
33
|
"""Initialize model data writer."""
|
|
44
34
|
self._logger = logging.getLogger(__name__)
|
|
45
35
|
self.io_handler = io_handler.IOHandler()
|
|
46
36
|
self.schema_dict = {}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
self.
|
|
37
|
+
self.output_label = "model_data_writer"
|
|
38
|
+
self.io_handler.set_paths(
|
|
39
|
+
output_path=output_path or settings.config.args.get("output_path"),
|
|
40
|
+
output_path_label=self.output_label,
|
|
41
|
+
)
|
|
51
42
|
try:
|
|
52
|
-
self.
|
|
43
|
+
self.output_file = self.io_handler.get_output_file(
|
|
44
|
+
file_name=output_file, output_path_label=self.output_label
|
|
45
|
+
)
|
|
53
46
|
except TypeError:
|
|
54
|
-
self.
|
|
55
|
-
self.
|
|
47
|
+
self.output_file = None
|
|
48
|
+
self.output_file_format = self._derive_data_format(output_file_format, self.output_file)
|
|
56
49
|
|
|
57
50
|
@staticmethod
|
|
58
51
|
def dump(
|
|
59
|
-
|
|
52
|
+
output_file=None,
|
|
53
|
+
metadata=None,
|
|
54
|
+
product_data=None,
|
|
55
|
+
output_file_format="ascii.ecsv",
|
|
56
|
+
validate_schema_file=None,
|
|
60
57
|
):
|
|
61
58
|
"""
|
|
62
59
|
Write model data and metadata (as static method).
|
|
63
60
|
|
|
64
61
|
Parameters
|
|
65
62
|
----------
|
|
66
|
-
args_dict: dict
|
|
67
|
-
Dictionary with configuration parameters (including output file name and path).
|
|
68
63
|
output_file: string or Path
|
|
69
64
|
Name of output file (args["output_file"] is used if this parameter is not set).
|
|
70
65
|
metadata: MetadataCollector object
|
|
71
66
|
Metadata to be written.
|
|
72
67
|
product_data: astropy Table
|
|
73
68
|
Model data to be written
|
|
69
|
+
output_file_format: str
|
|
70
|
+
Format of output file.
|
|
74
71
|
validate_schema_file: str
|
|
75
72
|
Schema file used in validation of output data.
|
|
76
|
-
|
|
77
73
|
"""
|
|
78
74
|
writer = ModelDataWriter(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
),
|
|
82
|
-
product_data_format=args_dict.get("output_file_format", "ascii.ecsv"),
|
|
83
|
-
args_dict=args_dict,
|
|
75
|
+
output_file=output_file,
|
|
76
|
+
output_file_format=output_file_format,
|
|
84
77
|
)
|
|
85
|
-
|
|
78
|
+
skip_output_validation = settings.config.args.get("skip_output_validation", True)
|
|
79
|
+
if validate_schema_file is not None and not skip_output_validation:
|
|
86
80
|
product_data = writer.validate_and_transform(
|
|
87
81
|
product_data_table=product_data,
|
|
88
82
|
validate_schema_file=validate_schema_file,
|
|
@@ -137,9 +131,8 @@ class ModelDataWriter:
|
|
|
137
131
|
Validated parameter dictionary.
|
|
138
132
|
"""
|
|
139
133
|
writer = ModelDataWriter(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
args_dict=None,
|
|
134
|
+
output_file=output_file,
|
|
135
|
+
output_file_format="json",
|
|
143
136
|
output_path=output_path,
|
|
144
137
|
)
|
|
145
138
|
if check_db_for_existing_parameter:
|
|
@@ -434,19 +427,17 @@ class ModelDataWriter:
|
|
|
434
427
|
gen.change_dict_keys_case(metadata.get_top_level_metadata(), True)
|
|
435
428
|
)
|
|
436
429
|
|
|
437
|
-
self._logger.info(f"Writing data to {self.
|
|
438
|
-
if isinstance(product_data, dict) and Path(self.
|
|
439
|
-
self.write_dict_to_model_parameter_json(self.
|
|
430
|
+
self._logger.info(f"Writing data to {self.output_file}")
|
|
431
|
+
if isinstance(product_data, dict) and Path(self.output_file).suffix == ".json":
|
|
432
|
+
self.write_dict_to_model_parameter_json(self.output_file, product_data)
|
|
440
433
|
return
|
|
441
434
|
try:
|
|
442
|
-
product_data.write(
|
|
443
|
-
self.product_data_file, format=self.product_data_format, overwrite=True
|
|
444
|
-
)
|
|
435
|
+
product_data.write(self.output_file, format=self.output_file_format, overwrite=True)
|
|
445
436
|
except IORegistryError:
|
|
446
|
-
self._logger.error(f"Error writing model data to {self.
|
|
437
|
+
self._logger.error(f"Error writing model data to {self.output_file}.")
|
|
447
438
|
raise
|
|
448
439
|
if metadata is not None:
|
|
449
|
-
metadata.write(self.
|
|
440
|
+
metadata.write(self.output_file, add_activity_name=True)
|
|
450
441
|
|
|
451
442
|
def write_dict_to_model_parameter_json(self, file_name, data_dict):
|
|
452
443
|
"""
|
|
@@ -465,10 +456,13 @@ class ModelDataWriter:
|
|
|
465
456
|
if data writing was not successful.
|
|
466
457
|
"""
|
|
467
458
|
data_dict = ModelDataWriter.prepare_data_dict_for_writing(data_dict)
|
|
468
|
-
|
|
459
|
+
output_file = self.io_handler.get_output_file(
|
|
460
|
+
file_name, output_path_label=self.output_label
|
|
461
|
+
)
|
|
462
|
+
self._logger.info(f"Writing data to {output_file}")
|
|
469
463
|
ascii_handler.write_data_to_file(
|
|
470
464
|
data=data_dict,
|
|
471
|
-
output_file=
|
|
465
|
+
output_file=output_file,
|
|
472
466
|
sort_keys=True,
|
|
473
467
|
numpy_types=True,
|
|
474
468
|
)
|
|
@@ -508,9 +502,12 @@ class ModelDataWriter:
|
|
|
508
502
|
return data_dict
|
|
509
503
|
|
|
510
504
|
@staticmethod
|
|
511
|
-
def
|
|
505
|
+
def _derive_data_format(product_data_format, output_file=None):
|
|
512
506
|
"""
|
|
513
|
-
|
|
507
|
+
Derive data format and ensure conformance with astropy data format naming.
|
|
508
|
+
|
|
509
|
+
If product_data_format is None and output_file is given, derive format
|
|
510
|
+
from output_file suffix.
|
|
514
511
|
|
|
515
512
|
Parameters
|
|
516
513
|
----------
|
|
@@ -518,6 +515,6 @@ class ModelDataWriter:
|
|
|
518
515
|
format identifier
|
|
519
516
|
|
|
520
517
|
"""
|
|
521
|
-
if product_data_format
|
|
522
|
-
product_data_format = "
|
|
523
|
-
return product_data_format
|
|
518
|
+
if product_data_format is None and output_file is not None:
|
|
519
|
+
product_data_format = Path(output_file).suffix.lstrip(".")
|
|
520
|
+
return "ascii.ecsv" if product_data_format == "ecsv" else product_data_format
|
simtools/data_model/schema.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Module providing functionality to read and validate dictionaries using schema."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
from functools import lru_cache
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
import jsonschema
|
|
@@ -153,6 +154,7 @@ def _validate_meta_schema_url(data):
|
|
|
153
154
|
raise FileNotFoundError(f"Meta schema URL does not exist: {data['meta_schema_url']}")
|
|
154
155
|
|
|
155
156
|
|
|
157
|
+
@lru_cache
|
|
156
158
|
def _retrieve_yaml_schema_from_uri(uri):
|
|
157
159
|
"""Load schema from a file URI."""
|
|
158
160
|
path = SCHEMA_PATH / Path(uri.removeprefix("file:/"))
|