Coverage for yaptide/converter/converter/shieldhit/parser.py: 69%
264 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-07-01 12:55 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2024-07-01 12:55 +0000
1import itertools
2from typing import Optional
3import re
5import converter.solid_figures as solid_figures
6from converter.common import Parser
7from converter.shieldhit.beam import (BeamConfig, BeamModulator, BeamSourceType, ModulatorInterpretationMode,
8 ModulatorSimulationMethod, MultipleScatteringMode, StragglingModel)
9from converter.shieldhit.detect import (DetectConfig, OutputQuantity, ScoringFilter, ScoringOutput, QuantitySettings)
10from converter.shieldhit.geo import (DefaultMaterial, GeoMatConfig, Material, Zone, StoppingPowerFile)
11from converter.shieldhit.detectors import (ScoringCylinder, ScoringDetector, ScoringGlobal, ScoringMesh, ScoringZone)
13_particle_dict: dict[int, dict] = {
14 1: {
15 'name': 'NEUTRON',
16 'filter': [
17 ('Z', '==', 0),
18 ('A', '==', 1),
19 ]
20 },
21 2: {
22 'name': 'PROTON',
23 'filter': [('Z', '==', 1), ('A', '==', 1)]
24 },
25 3: {
26 'name': 'PION-',
27 'filter': [('ID', '==', 3)]
28 },
29 4: {
30 'name': 'PION+',
31 'filter': [('ID', '==', 4)]
32 },
33 5: {
34 'name': 'PIZERO',
35 'filter': [('ID', '==', 5)]
36 },
37 # 6: {
38 # 'name': 'ANEUTRON',
39 # 'a': 1
40 # },
41 7: {
42 'name': 'APROTON',
43 'filter': [('Z', '==', -1), ('A', '==', 3)]
44 },
45 8: {
46 'name': 'KAON-',
47 'filter': [('ID', '==', 8)]
48 },
49 9: {
50 'name': 'KAON+',
51 'filter': [('ID', '==', 9)]
52 },
53 10: {
54 'name': 'KAONZERO',
55 'filter': [('ID', '==', 10)]
56 },
57 11: {
58 'name': 'KAONLONG',
59 'filter': [('ID', '==', 11)]
60 },
61 15: {
62 'name': 'MUON-',
63 'filter': [('ID', '==', 15)]
64 },
65 16: {
66 'name': 'MUON+',
67 'filter': [('ID', '==', 16)]
68 },
69 21: {
70 'name': 'DEUTERON',
71 'filter': [('Z', '==', 1), ('A', '==', 2)]
72 },
73 22: {
74 'name': 'TRITON',
75 'filter': [('Z', '==', 1), ('A', '==', 3)]
76 },
77 23: {
78 'name': '3-HELIUM',
79 'filter': [('Z', '==', 2), ('A', '==', 3)]
80 },
81 24: {
82 'name': '4-HELIUM',
83 'filter': [('Z', '==', 2), ('A', '==', 4)]
84 }
85}
88def parse_scoring_filter(scoring_filter: dict) -> ScoringFilter:
89 """Parse scoring filter from JSON
91 Generates a ScoringFilter object from a JSON dictionary.
92 """
93 if scoring_filter.get("particle"):
94 # If the filter is a particle filter, we want to map it to format used by SHIELD-HIT12A
95 return ScoringFilter(uuid=scoring_filter["uuid"],
96 name=scoring_filter["name"],
97 rules=_particle_dict[scoring_filter["particle"]["id"]]['filter'])
99 return ScoringFilter(uuid=scoring_filter["uuid"],
100 name=scoring_filter["name"],
101 rules=[(rule_dict["keyword"], rule_dict["operator"], rule_dict["value"])
102 for rule_dict in scoring_filter["rules"]])
105class ShieldhitParser(Parser):
106 """A SHIELD-HIT12A parser"""
108 def __init__(self) -> None:
109 super().__init__()
110 self.info['simulator'] = 'shieldhit'
111 self.beam_config = BeamConfig()
112 self.detect_config = DetectConfig()
113 self.geo_mat_config = GeoMatConfig()
115 def parse_configs(self, json: dict) -> None:
116 """Wrapper for all parse functions"""
117 self._parse_geo_mat(json)
118 self._parse_beam(json)
119 self._parse_detect(json)
121 def parse_modulator(self, json: dict) -> None:
122 """Parses data from the input json into the beam_config property"""
123 if json["specialComponentsManager"].get("modulator") is not None:
124 modulator = json["specialComponentsManager"].get("modulator")
125 parameters = modulator['geometryData'].get('parameters')
126 sourceFile = modulator.get('sourceFile')
127 zone_id = self._get_zone_index_by_uuid(parameters["zoneUuid"])
128 if sourceFile is not None and zone_id is not None:
129 if sourceFile.get('name') is None or sourceFile.get('value') is None:
130 raise ValueError("Modulator source file name or content is not defined")
131 self.beam_config.modulator = BeamModulator(
132 filename=sourceFile.get('name'),
133 file_content=sourceFile.get('value'),
134 zone_id=zone_id,
135 simulation=ModulatorSimulationMethod.from_str(modulator.get('simulationMethod', 'modulus')),
136 mode=ModulatorInterpretationMode.from_str(modulator.get('interpretationMode', 'material')))
138 def parse_physics(self, json: dict) -> None:
139 """Parses data from the input json into the beam_config property"""
140 if json.get("physic") is not None:
141 self.beam_config.delta_e = json["physic"].get("energyLoss", self.beam_config.delta_e)
142 self.beam_config.nuclear_reactions = json["physic"].get("enableNuclearReactions",
143 self.beam_config.nuclear_reactions)
144 self.beam_config.straggling = StragglingModel.from_str(json["physic"].get(
145 "energyModelStraggling", self.beam_config.straggling.value))
146 self.beam_config.multiple_scattering = MultipleScatteringMode.from_str(json["physic"].get(
147 "multipleScattering", self.beam_config.multiple_scattering.value))
149 def _parse_beam(self, json: dict) -> None:
150 """Parses data from the input json into the beam_config property"""
151 self.beam_config.particle = json["beam"]["particle"]["id"]
152 self.beam_config.particle_name = json["beam"]["particle"].get("name")
153 self.beam_config.heavy_ion_a = json["beam"]["particle"]["a"]
154 self.beam_config.heavy_ion_z = json["beam"]["particle"]["z"]
155 self.beam_config.energy = json["beam"]["energy"]
156 self.beam_config.energy_spread = json["beam"]["energySpread"]
157 # we use get here to avoid KeyError if the cutoffs are not defined
158 # in that case None will be inserted into the beam config
159 # which is well handled by the converter
160 self.beam_config.energy_low_cutoff = json["beam"].get("energyLowCutoff")
161 self.beam_config.energy_high_cutoff = json["beam"].get("energyHighCutoff")
162 self.beam_config.n_stat = json["beam"].get("numberOfParticles", self.beam_config.n_stat)
163 self.beam_config.beam_pos = tuple(json["beam"]["position"])
164 self.beam_config.beam_dir = tuple(json["beam"]["direction"])
166 if json["beam"].get("sigma") is not None:
167 beam_type = json["beam"]["sigma"]["type"]
169 if beam_type == "Gaussian":
170 self.beam_config.beam_ext_x = abs(json["beam"]["sigma"]["x"])
171 self.beam_config.beam_ext_y = abs(json["beam"]["sigma"]["y"])
172 elif beam_type == "Flat square":
173 self.beam_config.beam_ext_x = -abs(json["beam"]["sigma"]["x"])
174 self.beam_config.beam_ext_y = -abs(json["beam"]["sigma"]["y"])
175 elif beam_type == "Flat circular":
176 # To generate a circular beam x value must be greater than 0
177 self.beam_config.beam_ext_x = 1.0
178 self.beam_config.beam_ext_y = -abs(json["beam"]["sigma"]["y"])
180 if json["beam"].get("sad") is not None:
181 beam_type = json["beam"]["sad"]["type"]
183 if beam_type == "double":
184 self.beam_config.sad_x = json["beam"]["sad"]["x"]
185 self.beam_config.sad_y = json["beam"]["sad"]["y"]
186 elif beam_type == "single":
187 self.beam_config.sad_x = json["beam"]["sad"]["x"]
188 self.beam_config.sad_y = None
189 else:
190 self.beam_config.sad_x = None
191 self.beam_config.sad_y = None
193 if json["beam"].get("sourceType", "") == BeamSourceType.FILE.label:
194 self.beam_config.beam_source_type = BeamSourceType.FILE
195 if "sourceFile" in json["beam"]:
196 self.beam_config.beam_source_filename = json["beam"]["sourceFile"].get("name")
197 self.beam_config.beam_source_file_content = json["beam"]["sourceFile"].get("value")
199 self.parse_physics(json)
200 self.parse_modulator(json)
202 def _parse_detect(self, json: dict) -> None:
203 """Parses data from the input json into the detect_config property"""
204 self.detect_config.detectors = self._parse_detectors(json)
205 self.detect_config.filters = self._parse_filters(json)
206 self.detect_config.outputs = self._parse_outputs(json)
208 def _parse_detectors(self, json: dict) -> list[ScoringDetector]:
209 """Parses detectors from the input json."""
210 detectors = []
211 for detector_dict in json["detectorManager"].get("detectors"):
212 geometry_type = detector_dict['geometryData'].get('geometryType')
213 position = detector_dict['geometryData'].get('position')
214 parameters = detector_dict['geometryData'].get('parameters')
215 if geometry_type == "Cyl":
216 detectors.append(
217 ScoringCylinder(
218 uuid=detector_dict["uuid"],
219 name=detector_dict["name"],
220 r_min=parameters["innerRadius"],
221 r_max=parameters["radius"],
222 r_bins=parameters["radialSegments"],
223 h_min=position[2] - parameters["depth"] / 2,
224 h_max=position[2] + parameters["depth"] / 2,
225 h_bins=parameters["zSegments"],
226 ))
228 elif geometry_type == "Mesh":
229 detectors.append(
230 ScoringMesh(
231 uuid=detector_dict["uuid"],
232 name=detector_dict["name"],
233 x_min=position[0] - parameters["width"] / 2,
234 x_max=position[0] + parameters["width"] / 2,
235 x_bins=parameters["xSegments"],
236 y_min=position[1] - parameters["height"] / 2,
237 y_max=position[1] + parameters["height"] / 2,
238 y_bins=parameters["ySegments"],
239 z_min=position[2] - parameters["depth"] / 2,
240 z_max=position[2] + parameters["depth"] / 2,
241 z_bins=parameters["zSegments"],
242 ))
244 elif geometry_type == "Zone":
245 detectors.append(
246 ScoringZone(
247 uuid=detector_dict["uuid"],
248 name=detector_dict["name"],
249 first_zone_id=self._get_zone_index_by_uuid(parameters["zoneUuid"]),
250 ))
252 elif geometry_type == "All":
253 detectors.append(ScoringGlobal(
254 uuid=detector_dict["uuid"],
255 name=detector_dict["name"],
256 ))
257 else:
258 raise ValueError(f"Invalid ScoringGeometry type: {detector_dict['type']}")
260 return detectors
262 def _get_zone_index_by_uuid(self, zone_uuid: str) -> int:
263 """Finds zone in the geo_mat_config object by its uuid and returns its simulation index."""
264 for idx, zone in enumerate(self.geo_mat_config.zones):
265 if zone.uuid == zone_uuid:
266 return idx + 1
268 raise ValueError(f"No zone with uuid \"{zone_uuid}\".")
270 @staticmethod
271 def _parse_filters(json: dict) -> list[ScoringFilter]:
272 """Parses scoring filters from the input json."""
273 filters = [parse_scoring_filter(filter_dict) for filter_dict in json["scoringManager"]["filters"]]
275 return filters
277 def _parse_outputs(self, json: dict) -> list[ScoringOutput]:
278 """Parses scoring outputs from the input json."""
279 outputs = [
280 ScoringOutput(
281 filename=output_dict["name"] + ".bdo",
282 fileformat=output_dict["fileFormat"] if "fileFormat" in output_dict else "",
283 geometry=self._get_detector_by_uuid(output_dict["detectorUuid"])
284 if 'detectorUuid' in output_dict else None,
285 quantities=[self._parse_output_quantity(quantity) for quantity in output_dict.get("quantities", [])],
286 ) for output_dict in json["scoringManager"]["outputs"]
287 ]
289 return outputs
291 def _get_detector_by_uuid(self, detect_uuid: str) -> Optional[str]:
292 """Finds detector in the detect_config object by its uuid and returns its simulation name."""
293 for detector in self.detect_config.detectors:
294 if detector.uuid == detect_uuid:
295 return detector.name
297 raise ValueError(f"No detector with uuid {detect_uuid}")
299 def _parse_quantity_settings(self, quantity_dict: dict) -> dict or None:
300 """Parses settings from the input json into the quantity settings property"""
302 def create_name_from_settings() -> str:
303 """Create a name for the quantity from its settings."""
304 # If the quantity has generic name in format [Quantity_XYZ], we want to use more descriptive name
305 # New name will be in format [Absolute/Rescaled]_[Quantity_XYZ]_[QuantityKeyword]_[to_Medium/to_Material]
306 # Specific elements of the name will be added only if they are present in the settings
307 if re.search(r'^Quantity(_\d*)?$', quantity_dict['name']):
308 prefix = ''
309 suffix = ''
310 if 'primaries' in quantity_dict:
311 prefix = 'Absolute_'
312 elif 'rescale' in quantity_dict:
313 prefix = 'Rescaled_'
314 if 'medium' in quantity_dict:
315 suffix = f'_to_{quantity_dict["medium"]}'
316 elif 'materialUuid' in quantity_dict:
317 suffix = f'_to_{self._get_material_by_uuid(quantity_dict["materialUuid"]).sanitized_name}'
318 result = f"{prefix}{quantity_dict['keyword']}_{quantity_dict['name']}{suffix}"
319 return result
321 # If the quantity has a custom name, we want to remove all non-alphanumeric characters from it
322 return re.sub(r'\W+', '', quantity_dict['name'])
324 # We want to skip parsing settings if there are no parameters to put in the settings
325 if all(map(lambda el: el not in quantity_dict, ['medium', 'offset', 'primaries', 'materialUuid', 'rescale'])):
326 return None
328 return QuantitySettings(
329 name=create_name_from_settings(),
330 medium=quantity_dict.get("medium", None),
331 offset=quantity_dict.get("offset", None),
332 primaries=quantity_dict.get("primaries", None),
333 rescale=quantity_dict.get("rescale", None),
334 material=self._get_material_id(quantity_dict["materialUuid"]) if 'materialUuid' in quantity_dict else None)
336 def _parse_output_quantity(self, quantity_dict: dict) -> OutputQuantity:
337 """Parse a single output quantity."""
338 self._parse_custom_material(quantity_dict)
339 diff1 = None
340 diff1_t = None
341 diff2 = None
342 diff2_t = None
344 if len(quantity_dict["modifiers"]) >= 1:
345 diff1 = (
346 quantity_dict["modifiers"][0]["lowerLimit"],
347 quantity_dict["modifiers"][0]["upperLimit"],
348 quantity_dict["modifiers"][0]["binsNumber"],
349 quantity_dict["modifiers"][0]["isLog"],
350 )
351 diff1_t = quantity_dict["modifiers"][0]["diffType"]
353 if len(quantity_dict["modifiers"]) >= 2:
354 diff2 = (
355 quantity_dict["modifiers"][1]["lowerLimit"],
356 quantity_dict["modifiers"][1]["upperLimit"],
357 quantity_dict["modifiers"][1]["binsNumber"],
358 quantity_dict["modifiers"][1]["isLog"],
359 )
360 diff2_t = quantity_dict["modifiers"][1]["diffType"]
362 return OutputQuantity(
363 name=quantity_dict["name"],
364 detector_type=quantity_dict["keyword"],
365 filter_name=self._get_scoring_filter_by_uuid(quantity_dict["filter"]) if "filter" in quantity_dict else "",
366 diff1=diff1,
367 diff1_t=diff1_t,
368 diff2=diff2,
369 diff2_t=diff2_t,
370 settings=self._parse_quantity_settings(quantity_dict))
372 def _get_scoring_filter_by_uuid(self, filter_uuid: str) -> str:
373 """Finds scoring filter in the detect_config object by its uuid and returns its simulation name."""
374 for scoring_filter in self.detect_config.filters:
375 if scoring_filter.uuid == filter_uuid:
376 return scoring_filter.name
378 raise ValueError(f"No scoring filter with uuid {filter_uuid} in {self.detect_config.filters}.")
380 def _parse_geo_mat(self, json: dict) -> None:
381 """Parses data from the input json into the geo_mat_config property"""
382 self._parse_title(json)
383 self._parse_materials(json)
384 self._parse_figures(json)
385 self._parse_zones(json)
387 def _parse_title(self, json: dict) -> None:
388 """Parses data from the input json into the geo_mat_config property"""
389 if "title" in json["project"] and len(json["project"]["title"]) > 0:
390 self.geo_mat_config.title = json["project"]["title"]
392 def _parse_materials(self, json: dict) -> None:
393 """Parse materials from JSON"""
394 self.geo_mat_config.materials = [
395 Material(material["name"], material["sanitizedName"], material["uuid"], material["icru"])
396 for material in json["materialManager"].get("materials")
397 ]
399 if json.get("physic") is not None and json["physic"].get("availableStoppingPowerFiles", False):
400 for icru in json["physic"]["availableStoppingPowerFiles"]:
401 value = json["physic"]["availableStoppingPowerFiles"][icru]
402 self.geo_mat_config.available_custom_stopping_power_files[int(icru)] = StoppingPowerFile(
403 int(icru), value.get("name", ''), value.get("content", ''))
405 def _parse_figures(self, json: dict) -> None:
406 """Parse figures from JSON"""
407 self.geo_mat_config.figures = [
408 solid_figures.parse_figure(figure_dict) for figure_dict in json["figureManager"].get('figures')
409 ]
411 def _add_overridden_material(self, material: Material) -> None:
412 """Parse materials from JSON"""
413 self.geo_mat_config.materials.append(material)
415 def _get_material_by_uuid(self, material_uuid: str) -> Material:
416 """Finds first material in the geo_mat_config object with corresponding uuid and returns it."""
417 for material in self.geo_mat_config.materials:
418 if material.uuid == material_uuid:
419 return material
421 raise ValueError(f"No material with uuid {material_uuid}.")
423 def _get_material_id(self, material_uuid: str) -> int:
424 """Find material by uuid and return its id."""
425 offset = 0
426 for idx, material in enumerate(self.geo_mat_config.materials):
428 # If the material is a DefaultMaterial then we need the value not its index,
429 # the _value2member_map_ returns a map of values and members that allows us to check if
430 # a given value is defined within the DefaultMaterial enum.
431 if DefaultMaterial.is_default_material(material.icru):
433 if material.uuid == material_uuid:
434 return int(material.icru)
436 # We need to count all DefaultMaterials prior to the searched one.
437 offset += 1
439 elif material.uuid == material_uuid:
440 # Only materials defined in mat.dat file are indexed.
441 return idx + 1 - offset
443 raise ValueError(f"No material with uuid {material_uuid} in materials {self.geo_mat_config.materials}.")
445 def _parse_custom_material(self, json: dict) -> None:
446 """Parse custom material from JSON and add it to the list of materials"""
447 if ('customMaterial' not in json or json['customMaterial'] is None
448 or 'materialPropertiesOverrides' not in json):
449 return
451 icru = json['customMaterial']['icru']
452 available_files = self.geo_mat_config.available_custom_stopping_power_files
453 is_stopping_power_file_available = icru in available_files
454 custom_stopping_power = is_stopping_power_file_available and json['materialPropertiesOverrides'].get(
455 'customStoppingPower', False)
457 overridden_material = Material(name=f"Custom {json['customMaterial']['name']}",
458 sanitized_name=f"custom_{json['customMaterial']['sanitizedName']}",
459 uuid=json['customMaterial']['uuid'],
460 icru=json['customMaterial']['icru'],
461 density=json['materialPropertiesOverrides'].get(
462 'density', json['customMaterial']['density']),
463 custom_stopping_power=custom_stopping_power)
465 self._add_overridden_material(overridden_material)
467 def _parse_zones(self, json: dict) -> None:
468 """Parse zones from JSON"""
469 self.geo_mat_config.zones = []
471 for idx, zone in enumerate(json["zoneManager"]["zones"]):
472 self._parse_custom_material(zone)
473 self.geo_mat_config.zones.append(
474 Zone(
475 uuid=zone["uuid"],
476 # lists are numbered from 0, but shieldhit zones are numbered from 1
477 id=idx + 1,
478 figures_operators=self._parse_csg_operations(zone["unionOperations"]),
479 material=self._get_material_id(zone["materialUuid"]),
480 material_override=zone.get('materialPropertiesOverrides', None),
481 ))
483 if "worldZone" in json["zoneManager"]:
484 self._parse_world_zone(json)
486 def _parse_world_zone(self, json: dict) -> None:
487 """Parse the world zone and add it to the zone list"""
488 # Add bounding figure to figures
489 world_zone = json["zoneManager"]["worldZone"]
490 world_figure = solid_figures.parse_figure(world_zone)
491 self.geo_mat_config.figures.append(world_figure)
493 operations = self._calculate_world_zone_operations(len(self.geo_mat_config.figures))
494 material = self._get_material_id(world_zone["materialUuid"])
495 # add zone to zones for every operation in operations
496 for operation in operations:
497 self.geo_mat_config.zones.append(
498 Zone(
499 uuid='',
500 id=len(self.geo_mat_config.zones) + 1,
501 # world zone defined by bounding figure and contained zones
502 figures_operators=[operation],
503 # the material of the world zone is usually defined as vacuum
504 material=material))
506 # Adding Black Hole wrapper outside of the World Zone is redundant
507 # if the World Zone already is made of Black Hole
508 if material != DefaultMaterial.BLACK_HOLE.value:
510 # Add the figure that will serve as a black hole wrapper around the world zone
511 black_hole_figure = solid_figures.parse_figure(world_zone)
513 # Make the figure slightly bigger. It will form the black hole wrapper around the simulation.
514 black_hole_figure.expand(1.)
516 # Add the black hole figure to the figures list
517 self.geo_mat_config.figures.append(black_hole_figure)
519 # Add the black hole wrapper zone to the zones list
520 last_figure_idx = len(self.geo_mat_config.figures)
521 self.geo_mat_config.zones.append(
522 Zone(
523 uuid="",
524 id=len(self.geo_mat_config.zones) + 1,
525 # slightly larger world zone - world zone
526 figures_operators=[{last_figure_idx, -(last_figure_idx - 1)}],
527 # the last material is the black hole
528 material=DefaultMaterial.BLACK_HOLE))
530 def _parse_csg_operations(self, operations: list[list[dict]]) -> list[set[int]]:
531 """
532 Parse dict of csg operations to a list of sets. Sets contain a list of intersecting geometries.
533 The list contains a union of geometries from sets.
534 """
535 list_of_operations = [item for ops in operations for item in ops]
536 parsed_operations = []
537 for operation in list_of_operations:
538 # lists are numbered from 0, but SHIELD-HIT12A figures are numbered from 1
539 figure_id = self._get_figure_index_by_uuid(operation["objectUuid"]) + 1
540 if operation["mode"] == "union":
541 parsed_operations.append({figure_id})
542 elif operation["mode"] == "subtraction":
543 parsed_operations[-1].add(-figure_id)
544 elif operation["mode"] == "intersection":
545 parsed_operations[-1].add(figure_id)
546 else:
547 raise ValueError(f"Unexpected CSG operation: {operation['mode']}")
549 return parsed_operations
551 def _calculate_world_zone_operations(self, world_zone_figure: int) -> list[set[int]]:
552 """Calculate the world zone operations. Take the world zone figure and subtract all geometries."""
553 # Sum all zones
554 all_zones = [
555 figure_operators for zone in self.geo_mat_config.zones for figure_operators in zone.figures_operators
556 ]
558 world_zone = [{world_zone_figure}]
560 for figure_set in all_zones:
561 new_world_zone = []
562 for w_figure_set in world_zone:
563 for figure in figure_set:
564 new_world_zone.append({*w_figure_set, -figure})
565 world_zone = new_world_zone
567 # filter out sets containing opposite pairs of values
568 world_zone = filter(lambda x: not any(abs(i) == abs(j) for i, j in itertools.combinations(x, 2)), world_zone)
570 return list(world_zone)
572 def _get_figure_index_by_uuid(self, figure_uuid: str) -> int:
573 """Find the list index of a figure from geo_mat_config.figures by uuid. Useful when parsing CSG operations."""
574 for idx, figure in enumerate(self.geo_mat_config.figures):
575 if figure.uuid == figure_uuid:
576 return idx
578 raise ValueError(f"No figure with uuid \"{figure_uuid}\".")
580 def get_configs_json(self) -> dict:
581 """Get JSON data for configs"""
582 configs_json = super().get_configs_json()
583 configs_json.update({
584 "beam.dat": str(self.beam_config),
585 "mat.dat": self.geo_mat_config.get_mat_string(),
586 "detect.dat": str(self.detect_config),
587 "geo.dat": self.geo_mat_config.get_geo_string()
588 })
590 files = {}
591 for icru in self.geo_mat_config.available_custom_stopping_power_files:
592 file = self.geo_mat_config.available_custom_stopping_power_files[icru]
593 files[file.name] = file.content
595 configs_json.update(files)
597 if self.beam_config.beam_source_type == BeamSourceType.FILE:
598 filename_of_beam_source_file: str = 'sobp.dat'
599 if not self.beam_config.beam_source_filename:
600 filename_of_beam_source_file = str(self.beam_config.beam_source_filename)
601 configs_json[filename_of_beam_source_file] = str(self.beam_config.beam_source_file_content)
603 if self.beam_config.modulator is not None:
604 filename_of_modulator_source_file: str = self.beam_config.modulator.filename
605 configs_json[filename_of_modulator_source_file] = str(self.beam_config.modulator.file_content)
607 return configs_json