gammasimtools 0.12.0__py3-none-any.whl → 0.13.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.
Files changed (81) hide show
  1. {gammasimtools-0.12.0.dist-info → gammasimtools-0.13.0.dist-info}/METADATA +1 -1
  2. {gammasimtools-0.12.0.dist-info → gammasimtools-0.13.0.dist-info}/RECORD +64 -77
  3. {gammasimtools-0.12.0.dist-info → gammasimtools-0.13.0.dist-info}/WHEEL +1 -1
  4. {gammasimtools-0.12.0.dist-info → gammasimtools-0.13.0.dist-info}/entry_points.txt +2 -1
  5. simtools/_version.py +2 -2
  6. simtools/applications/convert_all_model_parameters_from_simtel.py +77 -88
  7. simtools/applications/convert_geo_coordinates_of_array_elements.py +1 -1
  8. simtools/applications/db_get_parameter_from_db.py +52 -22
  9. simtools/applications/derive_photon_electron_spectrum.py +1 -1
  10. simtools/applications/docs_produce_array_element_report.py +1 -10
  11. simtools/applications/docs_produce_model_parameter_reports.py +4 -17
  12. simtools/applications/plot_tabular_data.py +14 -2
  13. simtools/applications/{production_derive_limits.py → production_derive_corsika_limits.py} +20 -8
  14. simtools/applications/production_extract_mc_event_data.py +125 -0
  15. simtools/applications/run_application.py +9 -10
  16. simtools/applications/submit_data_from_external.py +1 -1
  17. simtools/applications/submit_model_parameter_from_external.py +2 -1
  18. simtools/camera/single_photon_electron_spectrum.py +6 -2
  19. simtools/constants.py +7 -0
  20. simtools/data_model/metadata_collector.py +159 -61
  21. simtools/data_model/model_data_writer.py +11 -55
  22. simtools/data_model/schema.py +2 -1
  23. simtools/data_model/validate_data.py +5 -3
  24. simtools/db/db_handler.py +115 -31
  25. simtools/model/model_parameter.py +0 -31
  26. simtools/production_configuration/derive_corsika_limits.py +260 -0
  27. simtools/production_configuration/extract_mc_event_data.py +253 -0
  28. simtools/ray_tracing/mirror_panel_psf.py +1 -1
  29. simtools/reporting/docs_read_parameters.py +164 -91
  30. simtools/schemas/metadata.metaschema.yml +7 -6
  31. simtools/schemas/model_parameter.metaschema.yml +0 -4
  32. simtools/schemas/model_parameter_and_data_schema.metaschema.yml +13 -5
  33. simtools/schemas/model_parameters/array_coordinates.schema.yml +1 -1
  34. simtools/schemas/model_parameters/array_layouts.schema.yml +3 -0
  35. simtools/schemas/model_parameters/asum_shaping.schema.yml +1 -1
  36. simtools/schemas/model_parameters/atmospheric_profile.schema.yml +1 -1
  37. simtools/schemas/model_parameters/camera_config_file.schema.yml +1 -1
  38. simtools/schemas/model_parameters/camera_degraded_map.schema.yml +1 -1
  39. simtools/schemas/model_parameters/camera_filter.schema.yml +1 -1
  40. simtools/schemas/model_parameters/dsum_shaping.schema.yml +1 -1
  41. simtools/schemas/model_parameters/fadc_dev_pedestal.schema.yml +1 -1
  42. simtools/schemas/model_parameters/fadc_lg_dev_pedestal.schema.yml +1 -1
  43. simtools/schemas/model_parameters/fadc_lg_max_sum.schema.yml +3 -3
  44. simtools/schemas/model_parameters/fadc_max_sum.schema.yml +3 -3
  45. simtools/schemas/model_parameters/fake_mirror_list.schema.yml +1 -1
  46. simtools/schemas/model_parameters/lightguide_efficiency_vs_incidence_angle.schema.yml +1 -1
  47. simtools/schemas/model_parameters/lightguide_efficiency_vs_wavelength.schema.yml +1 -1
  48. simtools/schemas/model_parameters/mirror_list.schema.yml +1 -1
  49. simtools/schemas/model_parameters/nsb_reference_spectrum.schema.yml +1 -1
  50. simtools/schemas/model_parameters/nsb_skymap.schema.yml +1 -1
  51. simtools/schemas/model_parameters/primary_mirror_degraded_map.schema.yml +1 -1
  52. simtools/schemas/model_parameters/primary_mirror_segmentation.schema.yml +1 -1
  53. simtools/schemas/model_parameters/secondary_mirror_degraded_map.schema.yml +1 -1
  54. simtools/schemas/model_parameters/secondary_mirror_segmentation.schema.yml +1 -1
  55. simtools/schemas/plot_configuration.metaschema.yml +162 -0
  56. simtools/schemas/production_tables.schema.yml +1 -1
  57. simtools/simtel/simtel_config_reader.py +85 -34
  58. simtools/simtel/simtel_table_reader.py +4 -0
  59. simtools/utils/general.py +50 -9
  60. simtools/utils/names.py +7 -2
  61. simtools/visualization/plot_tables.py +25 -20
  62. simtools/visualization/visualize.py +71 -23
  63. simtools/_dev_version/__init__.py +0 -9
  64. simtools/applications/__init__.py +0 -0
  65. simtools/configuration/__init__.py +0 -0
  66. simtools/corsika/__init__.py +0 -0
  67. simtools/data_model/__init__.py +0 -0
  68. simtools/db/__init__.py +0 -0
  69. simtools/io_operations/__init__.py +0 -0
  70. simtools/job_execution/__init__.py +0 -0
  71. simtools/layout/__init__.py +0 -0
  72. simtools/model/__init__.py +0 -0
  73. simtools/production_configuration/limits_calculation.py +0 -202
  74. simtools/ray_tracing/__init__.py +0 -0
  75. simtools/runners/__init__.py +0 -0
  76. simtools/simtel/__init__.py +0 -0
  77. simtools/testing/__init__.py +0 -0
  78. simtools/utils/__init__.py +0 -0
  79. simtools/visualization/__init__.py +0 -0
  80. {gammasimtools-0.12.0.dist-info → gammasimtools-0.13.0.dist-info}/LICENSE +0 -0
  81. {gammasimtools-0.12.0.dist-info → gammasimtools-0.13.0.dist-info}/top_level.txt +0 -0
@@ -6,7 +6,6 @@ from pathlib import Path
6
6
 
7
7
  import astropy.units as u
8
8
  import numpy as np
9
- import yaml
10
9
  from astropy.io.registry.base import IORegistryError
11
10
 
12
11
  import simtools.utils.general as gen
@@ -95,7 +94,7 @@ class ModelDataWriter:
95
94
  Dictionary with configuration parameters (including output file name and path).
96
95
  output_file: string or Path
97
96
  Name of output file (args["output_file"] is used if this parameter is not set).
98
- metadata: dict
97
+ metadata: MetadataCollector object
99
98
  Metadata to be written.
100
99
  product_data: astropy Table
101
100
  Model data to be written
@@ -174,11 +173,11 @@ class ModelDataWriter:
174
173
  if metadata_input_dict is not None:
175
174
  metadata_input_dict["output_file"] = output_file
176
175
  metadata_input_dict["output_file_format"] = Path(output_file).suffix.lstrip(".")
177
- metadata = MetadataCollector(args_dict=metadata_input_dict).get_top_level_metadata()
178
- writer.write_metadata_to_yml(
179
- metadata=metadata, yml_file=output_path / f"{Path(output_file).stem}"
176
+ metadata = MetadataCollector(args_dict=metadata_input_dict)
177
+ metadata.write(output_path / Path(output_file))
178
+ unique_id = (
179
+ metadata.get_top_level_metadata().get("cta", {}).get("product", {}).get("id")
180
180
  )
181
- unique_id = metadata.get("cta", {}).get("product", {}).get("id")
182
181
 
183
182
  _json_dict = writer.get_validated_parameter_dict(
184
183
  parameter_name, value, instrument, parameter_version, unique_id
@@ -364,7 +363,7 @@ class ModelDataWriter:
364
363
  ----------
365
364
  product_data: astropy Table
366
365
  Model data to be written
367
- metadata: dict
366
+ metadata: MetadataCollector object
368
367
  Metadata to be written.
369
368
 
370
369
  Raises
@@ -377,7 +376,9 @@ class ModelDataWriter:
377
376
  return
378
377
 
379
378
  if metadata is not None:
380
- product_data.meta.update(gen.change_dict_keys_case(metadata, False))
379
+ product_data.meta.update(
380
+ gen.change_dict_keys_case(metadata.get_top_level_metadata(), False)
381
+ )
381
382
 
382
383
  self._logger.info(f"Writing data to {self.product_data_file}")
383
384
  if isinstance(product_data, dict) and Path(self.product_data_file).suffix == ".json":
@@ -390,6 +391,8 @@ class ModelDataWriter:
390
391
  except IORegistryError:
391
392
  self._logger.error(f"Error writing model data to {self.product_data_file}.")
392
393
  raise
394
+ if metadata is not None:
395
+ metadata.write(self.product_data_file, add_activity_name=True)
393
396
 
394
397
  def write_dict_to_model_parameter_json(self, file_name, data_dict):
395
398
  """
@@ -449,53 +452,6 @@ class ModelDataWriter:
449
452
  pass
450
453
  return data_dict
451
454
 
452
- def write_metadata_to_yml(self, metadata, yml_file=None, keys_lower_case=False):
453
- """
454
- Write model metadata file (yaml file format).
455
-
456
- Parameters
457
- ----------
458
- metadata: dict
459
- Metadata to be stored
460
- yml_file: str
461
- Name of output file.
462
- keys_lower_case: bool
463
- Write yaml keys in lower case.
464
-
465
- Returns
466
- -------
467
- str
468
- Name of output file
469
-
470
- Raises
471
- ------
472
- FileNotFoundError
473
- If yml_file not found.
474
- TypeError
475
- If yml_file is not defined.
476
- """
477
- try:
478
- yml_file = names.file_name_with_version(
479
- yml_file or self.product_data_file, ".metadata.yml"
480
- )
481
- with open(yml_file, "w", encoding="UTF-8") as file:
482
- yaml.safe_dump(
483
- gen.change_dict_keys_case(metadata, keys_lower_case),
484
- file,
485
- sort_keys=False,
486
- )
487
- self._logger.info(f"Writing metadata to {yml_file}")
488
- return yml_file
489
- except FileNotFoundError:
490
- self._logger.error(f"Error writing model data to {yml_file}")
491
- raise
492
- except AttributeError:
493
- self._logger.error("No metadata defined for writing")
494
- raise
495
- except TypeError:
496
- self._logger.error("No output file for metadata defined")
497
- raise
498
-
499
455
  @staticmethod
500
456
  def _astropy_data_format(product_data_format):
501
457
  """
@@ -108,7 +108,7 @@ def validate_dict_using_schema(data, schema_file=None, json_schema=None):
108
108
  """
109
109
  if json_schema is None and schema_file is None:
110
110
  _logger.warning(f"No schema provided for validation of {data}")
111
- return
111
+ return None
112
112
  if json_schema is None:
113
113
  json_schema = load_schema(
114
114
  schema_file,
@@ -131,6 +131,7 @@ def validate_dict_using_schema(data, schema_file=None, json_schema=None):
131
131
  raise FileNotFoundError(f"Meta schema URL does not exist: {data['meta_schema_url']}")
132
132
 
133
133
  _logger.debug(f"Successful validation of data using schema ({json_schema.get('name')})")
134
+ return data
134
135
 
135
136
 
136
137
  def load_schema(schema_file=None, schema_version=None):
@@ -220,7 +220,7 @@ class DataValidator:
220
220
  json_schema=self._get_data_description(index).get("json_schema"),
221
221
  )
222
222
  else:
223
- self._check_data_type(np.array(value).dtype, index)
223
+ self._check_data_type(np.array(value).dtype, index, value)
224
224
 
225
225
  if self.data_dict.get("type") not in ("string", "dict", "file"):
226
226
  self._check_for_not_a_number(value, index)
@@ -436,7 +436,7 @@ class DataValidator:
436
436
 
437
437
  return u.Unit(reference_unit)
438
438
 
439
- def _check_data_type(self, dtype, column_name):
439
+ def _check_data_type(self, dtype, column_name, value=None):
440
440
  """
441
441
  Check column data type.
442
442
 
@@ -446,6 +446,8 @@ class DataValidator:
446
446
  data type
447
447
  column_name: str
448
448
  column name
449
+ value: value
450
+ value to be tested (optional)
449
451
 
450
452
  Raises
451
453
  ------
@@ -456,7 +458,7 @@ class DataValidator:
456
458
  reference_dtype = self._get_data_description(column_name).get("type", None)
457
459
  if not gen.validate_data_type(
458
460
  reference_dtype=reference_dtype,
459
- value=None,
461
+ value=value,
460
462
  dtype=dtype,
461
463
  allow_subtypes=(not self.check_exact_data_type),
462
464
  ):
simtools/db/db_handler.py CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  import logging
4
4
  import re
5
+ from collections import defaultdict
5
6
  from pathlib import Path
6
7
  from threading import Lock
7
8
 
@@ -13,6 +14,7 @@ from pymongo import MongoClient
13
14
 
14
15
  from simtools.data_model import validate_data
15
16
  from simtools.io_operations import io_handler
17
+ from simtools.simtel import simtel_table_reader
16
18
  from simtools.utils import names, value_conversion
17
19
 
18
20
  __all__ = ["DatabaseHandler"]
@@ -113,6 +115,7 @@ class DatabaseHandler:
113
115
  direct_connection = self.mongo_db_config["db_server"] in (
114
116
  "localhost",
115
117
  "simtools-mongodb",
118
+ "mongodb",
116
119
  )
117
120
  return MongoClient(
118
121
  self.mongo_db_config["db_server"],
@@ -177,48 +180,62 @@ class DatabaseHandler:
177
180
  def get_model_parameter(
178
181
  self,
179
182
  parameter,
180
- parameter_version,
181
183
  site,
182
184
  array_element_name,
185
+ parameter_version=None,
186
+ model_version=None,
183
187
  ):
184
188
  """
185
- Get a model parameter using the parameter version.
189
+ Get a single model parameter using model or parameter version.
190
+
191
+ Note that this function should not be called in a loop for many parameters,
192
+ as it each call queries the database.
186
193
 
187
194
  Parameters
188
195
  ----------
189
196
  parameter: str
190
197
  Name of the parameter.
191
- parameter_version: str
192
- Version of the parameter.
193
198
  site: str
194
199
  Site name.
195
200
  array_element_name: str
196
- Name of the array element model (e.g. MSTN, SSTS).
201
+ Name of the array element model.
202
+ parameter_version: str
203
+ Version of the parameter.
204
+ model_version: str
205
+ Version of the model.
197
206
 
198
207
  Returns
199
208
  -------
200
209
  dict containing the parameter
201
210
 
202
211
  """
212
+ collection_name = names.get_collection_name_from_parameter_name(parameter)
213
+ if model_version:
214
+ production_table = self._read_production_table_from_mongo_db(
215
+ collection_name, model_version
216
+ )
217
+ array_element_list = self._get_array_element_list(
218
+ array_element_name, site, production_table, collection_name
219
+ )
220
+ for array_element in reversed(array_element_list):
221
+ parameter_version = (
222
+ production_table["parameters"].get(array_element, {}).get(parameter)
223
+ )
224
+ if parameter_version:
225
+ array_element_name = array_element
226
+ break
227
+
203
228
  query = {
204
229
  "parameter_version": parameter_version,
205
230
  "parameter": parameter,
206
231
  }
207
- if array_element_name is not None:
232
+ if array_element_name:
208
233
  query["instrument"] = array_element_name
209
- if site is not None:
234
+ if site:
210
235
  query["site"] = site
211
- return self._read_mongo_db(
212
- query=query, collection_name=names.get_collection_name_from_parameter_name(parameter)
213
- )
236
+ return self._read_mongo_db(query=query, collection_name=collection_name)
214
237
 
215
- def get_model_parameters(
216
- self,
217
- site,
218
- array_element_name,
219
- collection,
220
- model_version=None,
221
- ):
238
+ def get_model_parameters(self, site, array_element_name, collection, model_version):
222
239
  """
223
240
  Get model parameters using the model version.
224
241
 
@@ -239,22 +256,44 @@ class DatabaseHandler:
239
256
  -------
240
257
  dict containing the parameters
241
258
  """
242
- model_versions = (
243
- self.get_model_versions(collection) if model_version is None else [model_version]
259
+ pars = {}
260
+ production_table = self._read_production_table_from_mongo_db(collection, model_version)
261
+ array_element_list = self._get_array_element_list(
262
+ array_element_name, site, production_table, collection
244
263
  )
264
+ for array_element in array_element_list:
265
+ pars.update(
266
+ self._get_parameter_for_model_version(
267
+ array_element, model_version, site, collection, production_table
268
+ )
269
+ )
270
+ return pars
245
271
 
246
- pars = {}
247
- for _model_version in model_versions:
248
- production_table = self._read_production_table_from_mongo_db(collection, _model_version)
249
- array_element_list = self._get_array_element_list(
250
- array_element_name, site, production_table, collection
272
+ def get_model_parameters_for_all_model_versions(self, site, array_element_name, collection):
273
+ """
274
+ Get model parameters for all model versions.
275
+
276
+ Queries parameters for design and for the specified array element (if necessary).
277
+
278
+ Parameters
279
+ ----------
280
+ site: str
281
+ Site name.
282
+ array_element_name: str
283
+ Name of the array element model (e.g. LSTN-01, MSTx-FlashCam, ILLN-01).
284
+ collection: str
285
+ Collection of array element (e.g. telescopes, calibration_devices).
286
+
287
+ Returns
288
+ -------
289
+ dict containing the parameters with model version as first key
290
+ """
291
+ pars = defaultdict(dict)
292
+ for _model_version in self.get_model_versions(collection):
293
+ parameter_data = self.get_model_parameters(
294
+ site, array_element_name, collection, _model_version
251
295
  )
252
- for array_element in array_element_list:
253
- pars.update(
254
- self._get_parameter_for_model_version(
255
- array_element, _model_version, site, collection, production_table
256
- )
257
- )
296
+ pars[_model_version].update(parameter_data)
258
297
  return pars
259
298
 
260
299
  def _get_parameter_for_model_version(
@@ -331,9 +370,54 @@ class DatabaseHandler:
331
370
  return [collection for collection in collections if not collection.startswith("fs.")]
332
371
  return collections
333
372
 
373
+ def export_model_file(
374
+ self,
375
+ parameter,
376
+ site,
377
+ array_element_name,
378
+ model_version=None,
379
+ parameter_version=None,
380
+ export_file_as_table=False,
381
+ ):
382
+ """
383
+ Export single model file from the DB identified by the parameter name.
384
+
385
+ The parameter can be identified by model or parameter version.
386
+ Files can be exported as astropy tables (ecsv format).
387
+
388
+ Parameters
389
+ ----------
390
+ parameter: str
391
+ Name of the parameter.
392
+ site: str
393
+ Site name.
394
+ array_element_name: str
395
+ Name of the array element model (e.g. MSTN, SSTS).
396
+ parameter_version: str
397
+ Version of the parameter.
398
+ model_version: str
399
+ Version of the model.
400
+ export_file_as_table: bool
401
+ If True, export the file as an astropy table (ecsv format).
402
+ """
403
+ parameters = self.get_model_parameter(
404
+ parameter,
405
+ site,
406
+ array_element_name,
407
+ parameter_version=parameter_version,
408
+ model_version=model_version,
409
+ )
410
+ self.export_model_files(parameters=parameters, dest=self.io_handler.get_output_directory())
411
+ if export_file_as_table:
412
+ return simtel_table_reader.read_simtel_table(
413
+ parameter,
414
+ self.io_handler.get_output_directory().joinpath(parameters[parameter]["value"]),
415
+ )
416
+ return None
417
+
334
418
  def export_model_files(self, parameters=None, file_names=None, dest=None, db_name=None):
335
419
  """
336
- Export files from the DB to the model directory.
420
+ Export models files from the DB to given directory.
337
421
 
338
422
  The files to be exported can be specified by file_name or retrieved from the database
339
423
  using the parameters dictionary.
@@ -6,12 +6,10 @@ import shutil
6
6
  from copy import copy
7
7
 
8
8
  import astropy.units as u
9
- from astropy.table import Table
10
9
 
11
10
  import simtools.utils.general as gen
12
11
  from simtools.db import db_handler
13
12
  from simtools.io_operations import io_handler
14
- from simtools.simtel import simtel_table_reader
15
13
  from simtools.simtel.simtel_config_writer import SimtelConfigWriter
16
14
  from simtools.utils import names
17
15
 
@@ -517,35 +515,6 @@ class ModelParameter:
517
515
  self.db.export_model_files(parameters=pars_from_db, dest=self.config_file_directory)
518
516
  self._is_exported_model_files_up_to_date = True
519
517
 
520
- def get_model_file_as_table(self, par_name):
521
- """
522
- Return tabular data from file as astropy table.
523
-
524
- Parameters
525
- ----------
526
- par_name: str
527
- Name of the parameter.
528
-
529
- Returns
530
- -------
531
- Table
532
- Astropy table.
533
- """
534
- _par_entry = {}
535
- try:
536
- _par_entry[par_name] = self._parameters[par_name]
537
- except KeyError as exc:
538
- raise ValueError(f"Parameter {par_name} not found in the model.") from exc
539
- self.db.export_model_files(parameters=_par_entry, dest=self.config_file_directory)
540
- if _par_entry[par_name]["value"].endswith("ecsv"):
541
- return Table.read(
542
- self.config_file_directory.joinpath(_par_entry[par_name]["value"]),
543
- format="ascii.ecsv",
544
- )
545
- return simtel_table_reader.read_simtel_table(
546
- par_name, self.config_file_directory.joinpath(_par_entry[par_name]["value"])
547
- )
548
-
549
518
  def export_config_file(self):
550
519
  """Export the config file used by sim_telarray."""
551
520
  # Exporting model file
@@ -0,0 +1,260 @@
1
+ """Calculate the thresholds for energy, radial distance, and viewcone."""
2
+
3
+ import astropy.units as u
4
+ import matplotlib.pyplot as plt
5
+ import numpy as np
6
+ import tables
7
+ from astropy.coordinates import AltAz
8
+ from ctapipe.coordinates import GroundFrame, TiltedGroundFrame
9
+
10
+
11
+ class LimitCalculator:
12
+ """
13
+ Compute thresholds/limits for energy, radial distance, and viewcone.
14
+
15
+ Event data is read from the reduced MC event data file.
16
+
17
+ Parameters
18
+ ----------
19
+ event_data_file : str
20
+ Path to the HDF5 file containing the event data.
21
+ telescope_list : list, optional
22
+ List of telescope IDs to filter the events (default is None).
23
+ """
24
+
25
+ def __init__(self, event_data_file, telescope_list=None):
26
+ """
27
+ Initialize the LimitCalculator with the given event data file.
28
+
29
+ Parameters
30
+ ----------
31
+ event_data_file : str
32
+ Path to the reduced MC event data file.
33
+ telescope_list : list, optional
34
+ List of telescope IDs to filter the events (default is None).
35
+ """
36
+ self.event_data_file = event_data_file
37
+ self.telescope_list = telescope_list
38
+ self.event_x_core = None
39
+ self.event_y_core = None
40
+ self.simulated = None
41
+ self.shower_id_triggered = None
42
+ self.list_of_files = None
43
+ self.shower_sim_azimuth = None
44
+ self.shower_sim_altitude = None
45
+ self.array_azimuth = None
46
+ self.array_altitude = None
47
+ self.trigger_telescope_list_list = None
48
+ self.units = {}
49
+ self._read_event_data()
50
+
51
+ def _read_event_data(self):
52
+ """Read the event data from the reduced MC event data file."""
53
+ with tables.open_file(self.event_data_file, mode="r") as f:
54
+ reduced_data = f.root.data.reduced_data
55
+ triggered_data = f.root.data.triggered_data
56
+ file_names = f.root.data.file_names
57
+ trigger_telescope_list_list = f.root.data.trigger_telescope_list_list
58
+
59
+ self.event_x_core = reduced_data.col("core_x")
60
+ self.event_y_core = reduced_data.col("core_y")
61
+ self.simulated = reduced_data.col("simulated")
62
+ self.shower_id_triggered = triggered_data.col("shower_id_triggered")
63
+ self.list_of_files = file_names.col("file_names")
64
+ self.shower_sim_azimuth = reduced_data.col("shower_sim_azimuth")
65
+ self.shower_sim_altitude = reduced_data.col("shower_sim_altitude")
66
+ self.array_altitude = reduced_data.col("array_altitudes")
67
+ self.array_azimuth = reduced_data.col("array_azimuths")
68
+
69
+ self.trigger_telescope_list_list = [
70
+ [np.int16(tel) for tel in event] for event in trigger_telescope_list_list
71
+ ]
72
+
73
+ def _compute_limits(self, hist, bin_edges, loss_fraction, limit_type="lower"):
74
+ """
75
+ Compute the limits based on the loss fraction.
76
+
77
+ Parameters
78
+ ----------
79
+ hist : np.ndarray
80
+ 1D histogram array.
81
+ bin_edges : np.ndarray
82
+ Array of bin edges.
83
+ loss_fraction : float
84
+ Fraction of events to be lost.
85
+ limit_type : str, optional
86
+ Type of limit ('lower' or 'upper'). Default is 'lower'.
87
+
88
+ Returns
89
+ -------
90
+ float
91
+ Bin edge value corresponding to the threshold.
92
+ """
93
+ cumulative_sum = np.cumsum(hist) if limit_type == "upper" else np.cumsum(hist[::-1])
94
+ total_events = np.sum(hist)
95
+ threshold = (1 - loss_fraction) * total_events
96
+ bin_index = np.searchsorted(cumulative_sum, threshold)
97
+ return bin_edges[bin_index] if limit_type == "upper" else bin_edges[-bin_index]
98
+
99
+ def _prepare_data_for_limits(self):
100
+ """
101
+ Prepare the data required for computing limits.
102
+
103
+ Returns
104
+ -------
105
+ tuple
106
+ Tuple containing core distances, triggered energies, core bins, and energy bins.
107
+ """
108
+ shower_id_triggered_masked = self.shower_id_triggered
109
+ if self.telescope_list is not None:
110
+ mask = np.array(
111
+ [
112
+ all(tel in event for tel in self.telescope_list)
113
+ for event in self.trigger_telescope_list_list
114
+ ]
115
+ )
116
+ shower_id_triggered_masked = self.shower_id_triggered[mask]
117
+
118
+ triggered_energies = self.simulated[shower_id_triggered_masked]
119
+ energy_bins = np.logspace(
120
+ np.log10(triggered_energies.min()), np.log10(triggered_energies.max()), 1000
121
+ )
122
+ event_x_core_shower, event_y_core_shower = self._transform_to_shower_coordinates()
123
+ core_distances_all = np.sqrt(event_x_core_shower**2 + event_y_core_shower**2)
124
+ core_distances_triggered = core_distances_all[shower_id_triggered_masked]
125
+ core_bins = np.linspace(
126
+ core_distances_triggered.min(), core_distances_triggered.max(), 1000
127
+ )
128
+
129
+ return core_distances_triggered, triggered_energies, core_bins, energy_bins
130
+
131
+ def compute_lower_energy_limit(self, loss_fraction):
132
+ """
133
+ Compute the lower energy limit in TeV based on the event loss fraction.
134
+
135
+ Parameters
136
+ ----------
137
+ loss_fraction : float
138
+ Fraction of events to be lost.
139
+
140
+ Returns
141
+ -------
142
+ astropy.units.Quantity
143
+ Lower energy limit.
144
+ """
145
+ _, triggered_energies, _, energy_bins = self._prepare_data_for_limits()
146
+
147
+ hist, _ = np.histogram(triggered_energies, bins=energy_bins)
148
+ lower_bin_edge_value = self._compute_limits(
149
+ hist, energy_bins, loss_fraction, limit_type="lower"
150
+ )
151
+ return lower_bin_edge_value * u.TeV
152
+
153
+ def compute_upper_radial_distance(self, loss_fraction):
154
+ """
155
+ Compute the upper radial distance based on the event loss fraction.
156
+
157
+ Parameters
158
+ ----------
159
+ loss_fraction : float
160
+ Fraction of events to be lost.
161
+
162
+ Returns
163
+ -------
164
+ astropy.units.Quantity
165
+ Upper radial distance in m.
166
+ """
167
+ core_distances_triggered, _, core_bins, _ = self._prepare_data_for_limits()
168
+
169
+ hist, _ = np.histogram(core_distances_triggered, bins=core_bins)
170
+ upper_bin_edge_value = self._compute_limits(
171
+ hist, core_bins, loss_fraction, limit_type="upper"
172
+ )
173
+ return upper_bin_edge_value * u.m
174
+
175
+ def compute_viewcone(self, loss_fraction):
176
+ """
177
+ Compute the viewcone based on the event loss fraction.
178
+
179
+ Parameters
180
+ ----------
181
+ loss_fraction : float
182
+ Fraction of events to be lost.
183
+
184
+ Returns
185
+ -------
186
+ astropy.units.Quantity
187
+ Viewcone radius in degrees.
188
+ """
189
+ # already in radians
190
+ azimuth_diff = self.array_azimuth - self.shower_sim_azimuth # * (np.pi / 180.0)
191
+ sim_altitude_rad = self.shower_sim_altitude # * (np.pi / 180.0)
192
+ array_altitude_rad = self.array_altitude # * (np.pi / 180.0)
193
+ x_1 = np.cos(azimuth_diff) * np.cos(sim_altitude_rad)
194
+ y_1 = np.sin(azimuth_diff) * np.cos(sim_altitude_rad)
195
+ z_1 = np.sin(sim_altitude_rad)
196
+ x_2 = x_1 * np.sin(array_altitude_rad) - z_1 * np.cos(array_altitude_rad)
197
+ y_2 = y_1
198
+ z_2 = x_1 * np.cos(array_altitude_rad) + z_1 * np.sin(array_altitude_rad)
199
+ off_angles = np.arctan2(np.sqrt(x_2**2 + y_2**2), z_2) * (180.0 / np.pi)
200
+ angle_bins = np.linspace(off_angles.min(), off_angles.max(), 400)
201
+ hist, _ = np.histogram(off_angles, bins=angle_bins)
202
+
203
+ upper_bin_edge_value = self._compute_limits(
204
+ hist, angle_bins, loss_fraction, limit_type="upper"
205
+ )
206
+ return upper_bin_edge_value * u.deg
207
+
208
+ def _transform_to_shower_coordinates(self):
209
+ """
210
+ Transform core positions from ground coordinates to shower coordinates.
211
+
212
+ Returns
213
+ -------
214
+ tuple
215
+ Core positions in shower coordinates (x, y).
216
+ """
217
+ pointing_az = self.shower_sim_azimuth * u.rad
218
+ pointing_alt = self.shower_sim_altitude * u.rad
219
+
220
+ pointing = AltAz(az=pointing_az, alt=pointing_alt)
221
+ ground = GroundFrame(x=self.event_x_core * u.m, y=self.event_y_core * u.m, z=0 * u.m)
222
+ shower_frame = ground.transform_to(TiltedGroundFrame(pointing_direction=pointing))
223
+
224
+ return shower_frame.x.value, shower_frame.y.value
225
+
226
+ def plot_data(self):
227
+ """Plot the core distances and energies of triggered events."""
228
+ shower_id_triggered_masked = self.shower_id_triggered
229
+ if self.telescope_list is not None:
230
+ mask = np.array(
231
+ [
232
+ all(tel in event for tel in self.telescope_list)
233
+ for event in self.trigger_telescope_list_list
234
+ ]
235
+ )
236
+ shower_id_triggered_masked = self.shower_id_triggered[mask]
237
+
238
+ core_distances_all = np.sqrt(self.event_x_core**2 + self.event_y_core**2)
239
+ core_distances_triggered = core_distances_all[shower_id_triggered_masked]
240
+ triggered_energies = self.simulated[shower_id_triggered_masked]
241
+
242
+ core_bins = np.linspace(core_distances_triggered.min(), core_distances_triggered.max(), 400)
243
+ energy_bins = np.logspace(
244
+ np.log10(triggered_energies.min()), np.log10(triggered_energies.max()), 400
245
+ )
246
+ plt.figure(figsize=(8, 6))
247
+ plt.hist2d(
248
+ core_distances_triggered,
249
+ triggered_energies,
250
+ bins=[core_bins, energy_bins],
251
+ norm="log",
252
+ cmap="viridis",
253
+ )
254
+
255
+ plt.colorbar(label="Event Count")
256
+ plt.xlabel("Core Distance [m]")
257
+ plt.ylabel("Energy [TeV]")
258
+ plt.yscale("log")
259
+ plt.title("2D Histogram of Triggered Core Distance vs Energy")
260
+ plt.show()