diff --git a/converter/fluka/cards/assignmat_card.py b/converter/fluka/cards/assignmat_card.py new file mode 100644 index 00000000..dae3f29c --- /dev/null +++ b/converter/fluka/cards/assignmat_card.py @@ -0,0 +1,22 @@ +from dataclasses import dataclass, field +from converter.fluka.cards.card import Card + +@dataclass +class AssignMatCard(Card): + """ + Class representing description of material assignment in FLUKA input. + Card consists of: + - codewd (str): 'ASSIGNMA' + - what (list[str]): list of parameters (see FLUKA manual for details) + sdum is not used. + """ + + codewd: str = "ASSIGNMA" + region_name: str = "" + material_name: str = "BLCKHOLE" + + def __post_init__(self) -> None: + """Set material and region on appropriate positions in what list.""" + super().__post_init__() + self.what[0] = self.material_name + self.what[1] = self.region_name diff --git a/converter/fluka/cards/card.py b/converter/fluka/cards/card.py index cd5665d5..dc7be6b3 100644 --- a/converter/fluka/cards/card.py +++ b/converter/fluka/cards/card.py @@ -17,7 +17,7 @@ def __post_init__(self): def __str__(self) -> str: """Return card as string.""" - return self._format_line() + return self._format_line().rstrip() def _format_line(self) -> str: """Return formatted line from given parameters.""" diff --git a/converter/fluka/helper_parsers/region_parser.py b/converter/fluka/helper_parsers/region_parser.py index ffb0ace2..228dbcca 100644 --- a/converter/fluka/helper_parsers/region_parser.py +++ b/converter/fluka/helper_parsers/region_parser.py @@ -21,29 +21,27 @@ class FlukaRegion: figures_operators: list[list[tuple[BoolOperation, str]]] = field(default_factory=lambda: []) -def parse_regions(zones_json: dict, figures: list[FlukaFigure]) -> (list[FlukaRegion], list[FlukaFigure]): +def parse_regions(zones_json: dict, figures: list[FlukaFigure]) -> (dict[str, FlukaRegion], FlukaRegion, list[FlukaFigure]): """ Parse zones from JSON to Fluka regions. Returns list of regions and list of additional figures generated by parsing world zone. """ # Naming is different in Fluka - Fluka zones consist of figures joined by subtractions and intersections # Fluka regions consist of zones joined by unions - regions = [] + # uuid -> FlukaRegion + regions = {} zone_name = "region{}" for idx, zone in enumerate(zones_json["zones"]): - regions.append( - FlukaRegion( - name=zone_name.format(idx), - figures_operators=parse_csg_operations(zone["unionOperations"], figures), - ) + regions[zone["uuid"]] = FlukaRegion( + name=zone_name.format(idx), + figures_operators=parse_csg_operations(zone["unionOperations"], figures) ) if "worldZone" in zones_json: world_region, boundary_region, world_figure, world_boundary = parse_world_zone(zones_json, figures) - regions.append(world_region) - regions.append(boundary_region) + regions[zones_json["worldZone"]["uuid"]] = world_region - return regions, [world_figure, world_boundary] + return regions, boundary_region, [world_figure, world_boundary] def parse_world_zone(zones_json: dict, figures: list[FlukaFigure]) -> (FlukaRegion, FlukaFigure, FlukaFigure): diff --git a/converter/fluka/input.py b/converter/fluka/input.py index 28401a22..fd3b2f2b 100644 --- a/converter/fluka/input.py +++ b/converter/fluka/input.py @@ -16,6 +16,7 @@ class Input: compounds: list[Card] = field(default_factory=lambda: []) figures: list[SolidFigure] = field(default_factory=lambda: []) regions: list = field(default_factory=lambda: []) + assignmats: list[Card] = field(default_factory=lambda: []) template: str = """TITLE proton beam simulation @@ -35,9 +36,7 @@ class Input: GEOEND {MATERIALS} {COMPOUNDS} -ASSIGNMA BLCKHOLE Z_BBODY -ASSIGNMA AIR Z_AIR -ASSIGNMA WATER Z_TARGET +{ASSIGNMATS} * scoring NEUTRON on mesh z USRBIN 0.0 NEUTRON -21 0.5 0.5 5.0n_z USRBIN -0.5 -0.5 0.0 1 1 500& @@ -73,6 +72,7 @@ def __str__(self): """Return fluka input file as string""" materials_str = "\n".join([str(material) for material in self.materials]).strip() compounds_str = "\n".join([str(compound) for compound in self.compounds]).strip() + assignmats_str = "\n".join([str(assignmat) for assignmat in self.assignmats]).strip() return self.template.format( BEAM=Card(codewd="BEAM", what=[str(-self.energy_GeV)], sdum="PROTON"), START=Card(codewd="START", what=[str(self.number_of_particles)]), @@ -80,4 +80,5 @@ def __str__(self): REGIONS=RegionsCard(data=self.regions), MATERIALS=materials_str, COMPOUNDS=compounds_str, + ASSIGNMATS=assignmats_str, ) diff --git a/converter/fluka/material.py b/converter/fluka/material.py index b7f4ec84..1d05845c 100644 --- a/converter/fluka/material.py +++ b/converter/fluka/material.py @@ -31,7 +31,7 @@ def parse_materials_compounds(self, materials: list, zones: list) -> None: density = material["density"] self.__add_new_material_compound(uuid, icru, density) for zone in zones: - if "density" in zone["materialPropertiesOverrides"]: + if "density" in zone.get("materialPropertiesOverrides", []): uuid = zone["materialUuid"] density = zone["materialPropertiesOverrides"]["density"] icru = zone["customMaterial"]["icru"] @@ -71,17 +71,12 @@ def __add_new_material_compound(self, uuid, icru, density) -> None: else: self.compounds[uuid] = self.predefined_compounds[icru] else: - raise NotImplementedError( - "Only predefined materials and compounds are supported." - ) + raise NotImplementedError("Only predefined materials and compounds are supported.") def __load_predifined_materials(self) -> None: """Load predefined materials and compounds from file""" with open( - Path("__file__").resolve().parent - / "converter" - / "fluka" - / "predefined_materials.json", + Path("__file__").resolve().parent / "converter" / "fluka" / "predefined_materials.json", "r", ) as file: data = json.load(file) @@ -115,19 +110,15 @@ def __generate_name(self, length: int = 8) -> str: def get_custom_materials(self) -> list: """Return list of modified materials.""" - return [ - material - for material in self.materials.values() - if isinstance(material, CustomMaterial) - ] + return [material for material in self.materials.values() if isinstance(material, CustomMaterial)] def get_custom_compounds(self) -> list: """Return list of modified compounds.""" - return [ - compound - for compound in self.compounds.values() - if isinstance(compound, CustomCompound) - ] + return [compound for compound in self.compounds.values() if isinstance(compound, CustomCompound)] + + def get_parsed_materials_and_compounds(self) -> dict: + """Return dict of parsed materials and compounds.""" + return self.materials | self.compounds @dataclass diff --git a/converter/fluka/parser.py b/converter/fluka/parser.py index 5a172530..54c816fb 100644 --- a/converter/fluka/parser.py +++ b/converter/fluka/parser.py @@ -1,8 +1,9 @@ from converter.common import Parser from converter.fluka.helper_parsers.figure_parser import parse_figures -from converter.fluka.helper_parsers.region_parser import parse_regions +from converter.fluka.helper_parsers.region_parser import parse_regions, FlukaRegion from converter.fluka.input import Input from converter.fluka.material import MaterialsCompoundsConfig +from converter.fluka.cards.assignmat_card import AssignMatCard class FlukaParser(Parser): @@ -25,16 +26,20 @@ def parse_configs(self, json: dict) -> None: self.input.number_of_particles = json["beam"]["numberOfParticles"] self.materials_compounds_config.parse_materials_compounds( - materials=json["materialManager"].get("materials"), - zones=json["zoneManager"].get("zones"), + materials=json["materialManager"]["materials"], + zones=json["zoneManager"]["zones"] + [json["zoneManager"]["worldZone"]], ) self.input.materials = self.materials_compounds_config.get_custom_materials() self.input.compounds = self.materials_compounds_config.get_custom_compounds() self.input.figures = parse_figures(json["figureManager"].get("figures")) - self.input.regions, world_figures = parse_regions( - json["zoneManager"], self.input.figures - ) + regions, boundary_region, world_figures = parse_regions(json["zoneManager"], self.input.figures) + self.input.regions = list(regions.values()) + [boundary_region] self.input.figures.extend(world_figures) + self.__assign_materials_and_compounds( + regions=regions, + zones=json["zoneManager"]["zones"] + [json["zoneManager"]["worldZone"]], + boundary=boundary_region, + ) def get_configs_json(self) -> dict: """ @@ -45,3 +50,16 @@ def get_configs_json(self) -> dict: configs_json["fl_sim.inp"] = str(self.input) return configs_json + + def __assign_materials_and_compounds(self, regions: dict, zones: list, boundary: FlukaRegion) -> None: + """Assign materials and compounds to regions.""" + assignmat_cards = [] + materials = self.materials_compounds_config.get_parsed_materials_and_compounds() + for zone in zones: + assignmat_cards.append( + AssignMatCard( + region_name=regions[zone["uuid"]].name, material_name=materials[zone["materialUuid"]].get_name() + ) + ) + assignmat_cards.append(AssignMatCard(region_name=boundary.name)) + self.input.assignmats = assignmat_cards diff --git a/tests/fluka/expected_fluka_output/fl_sim.py b/tests/fluka/expected_fluka_output/fl_sim.py index c10f1433..50d676db 100644 --- a/tests/fluka/expected_fluka_output/fl_sim.py +++ b/tests/fluka/expected_fluka_output/fl_sim.py @@ -3,7 +3,7 @@ * default physics settings for hadron therapy DEFAULTS HADROTHE * beam source -BEAM -0.07 PROTON +BEAM -0.07 PROTON * beam source position BEAMPOS 0.0 0.0 -100.0 * geometry description starts here @@ -59,9 +59,12 @@ GEOEND MATERIAL 82.0 11.36 207.0KAFNAFQO COMPOUND -1.01 WATER QAHFTRXC -ASSIGNMA BLCKHOLE Z_BBODY -ASSIGNMA AIR Z_AIR -ASSIGNMA WATER Z_TARGET +ASSIGNMA AIR region0 +ASSIGNMA QAHFTRXC region1 +ASSIGNMA KAFNAFQO region2 +ASSIGNMA AIR region3 +ASSIGNMA WATER world +ASSIGNMA BLCKHOLE boundary * scoring NEUTRON on mesh z USRBIN 0.0 NEUTRON -21 0.5 0.5 5.0n_z USRBIN -0.5 -0.5 0.0 1 1 500& @@ -89,6 +92,6 @@ * random number generator settings RANDOMIZ 137 * number of particles to simulate -START 10000.0 +START 10000.0 STOP """ \ No newline at end of file diff --git a/tests/fluka/test_fluka_card.py b/tests/fluka/test_fluka_card.py index 0dcf548d..d3ead0db 100644 --- a/tests/fluka/test_fluka_card.py +++ b/tests/fluka/test_fluka_card.py @@ -3,18 +3,18 @@ def test_beam_line() -> None: """Test if BEAM line is formatted properly.""" - assert str(Card("BEAM", ["-0.15"], "PROTON")) == "BEAM -0.15 PROTON " - assert str(Card("BEAM", ["200.0", "0.2", "1.5", "1.2", "0.7", "1.0"], "PROTON")) == "BEAM 200.0 0.2 1.5 1.2 0.7 1.0PROTON " - assert str(Card("BEAM", ["10.0", "0.2", "0.0", "-2.36", "-1.18", "1.0"], "PROTON")) == "BEAM 10.0 0.2 0.0 -2.36 -1.18 1.0PROTON " - assert str(Card("BEAM", ["-661.7E-06", "0.0", "1.0E4", "0.0", "0.0", "1.0"], "PHOTON")) == "BEAM -0.0006617 0.0 10000.0 0.0 0.0 1.0PHOTON " - assert str(Card("BEAM", ["-2.0", "0.0", "3.0", "0.0", "0.0", "1.0"], "MUON-")) == "BEAM -2.0 0.0 3.0 0.0 0.0 1.0MUON- " - assert str(Card("BEAM", ["120000000000000.0", "3.02", "-30.4234"], "PROTON")) == "BEAM 1.200E+14 3.02 -30.4234 PROTON " + assert str(Card("BEAM", ["-0.15"], "PROTON")) == "BEAM -0.15 PROTON" + assert str(Card("BEAM", ["200.0", "0.2", "1.5", "1.2", "0.7", "1.0"], "PROTON")) == "BEAM 200.0 0.2 1.5 1.2 0.7 1.0PROTON" + assert str(Card("BEAM", ["10.0", "0.2", "0.0", "-2.36", "-1.18", "1.0"], "PROTON")) == "BEAM 10.0 0.2 0.0 -2.36 -1.18 1.0PROTON" + assert str(Card("BEAM", ["-661.7E-06", "0.0", "1.0E4", "0.0", "0.0", "1.0"], "PHOTON")) == "BEAM -0.0006617 0.0 10000.0 0.0 0.0 1.0PHOTON" + assert str(Card("BEAM", ["-2.0", "0.0", "3.0", "0.0", "0.0", "1.0"], "MUON-")) == "BEAM -2.0 0.0 3.0 0.0 0.0 1.0MUON-" + assert str(Card("BEAM", ["120000000000000.0", "3.02", "-30.4234"], "PROTON")) == "BEAM 1.200E+14 3.02 -30.4234 PROTON" def test_start_line() -> None: """Test if START line is formatted properly.""" - assert str(Card("START", ["10000"])) == "START 10000.0 " - assert str(Card("START", ["5000"])) == "START 5000.0 " - assert str(Card("START", ["200000"])) == "START 200000.0 " - assert str(Card("START", ["1234567"])) == "START 1234567.0 " + assert str(Card("START", ["10000"])) == "START 10000.0" + assert str(Card("START", ["5000"])) == "START 5000.0" + assert str(Card("START", ["200000"])) == "START 200000.0" + assert str(Card("START", ["1234567"])) == "START 1234567.0" diff --git a/tests/fluka/test_region_parser.py b/tests/fluka/test_region_parser.py index 9bcd5060..3cb37a5b 100644 --- a/tests/fluka/test_region_parser.py +++ b/tests/fluka/test_region_parser.py @@ -11,7 +11,8 @@ def test_parse_regions(zones_json, project_fluka_json): """Test if regions are parsed correctly""" figures = parse_figures(project_fluka_json["figureManager"].get('figures')) - regions, _ = parse_regions(zones_json, figures) + regions, boundary_region, _ = parse_regions(zones_json, figures) + regions = list(regions.values()) + [boundary_region] assert regions[0].name == "region0" assert regions[0].figures_operators == [[(BoolOperation.INTERSECTION, "fig0"), (BoolOperation.SUBTRACTION, "fig1"),