diff --git a/examples/1make_pin.py b/examples/1_make_pin.py similarity index 100% rename from examples/1make_pin.py rename to examples/1_make_pin.py diff --git a/examples/2parameter_demo.py b/examples/2_parameter_demo.py similarity index 100% rename from examples/2parameter_demo.py rename to examples/2_parameter_demo.py diff --git a/examples/3_custom_component.py b/examples/3_custom_component.py new file mode 100644 index 0000000..3a34c12 --- /dev/null +++ b/examples/3_custom_component.py @@ -0,0 +1,26 @@ +from hypnos.components import SimpleComponent +from hypnos.geometry import create_brick +from hypnos.geometry_maker import GeometryMaker + + +class CustomComponent(SimpleComponent): + def __init__(self, params): + super().__init__("custom", params) + + def check_sanity(self): + length = self.geometry["length"] + height = self.geometry["height"] + if length < 0 or height < 0: + raise ValueError("parameters must be positive") + + def make_geometry(self): + length = self.geometry["length"] + height = self.geometry["height"] + brick = create_brick({"dimensions": [length, length, height]}) + return brick + + +maker = GeometryMaker([CustomComponent]) +maker.file_to_tracked_geometry("3custom_component.json") +maker.tetmesh() +maker.export(rootname="custom_geometry") diff --git a/examples/3custom_component.json b/examples/3custom_component.json new file mode 100644 index 0000000..749c6d3 --- /dev/null +++ b/examples/3custom_component.json @@ -0,0 +1,8 @@ +{ + "class": "CustomComponent", + "material": "material_name", + "geometry": { + "length": 5, + "height": 7.931 + } +} \ No newline at end of file diff --git a/examples/sample_blanket.json b/examples/sample_blanket.json index 10e9233..618e200 100644 --- a/examples/sample_blanket.json +++ b/examples/sample_blanket.json @@ -1,5 +1,5 @@ { - "class": "HCPB_blanket", + "class": "HCPBBlanket", "geometry": { "pin spacing": 135, "pin vertical offset": 0, @@ -23,7 +23,7 @@ }, "components": { "pin": { - "class": "pin", + "class": "PinAssembly", "material": { "cladding": "EUROFER", "pressure tube": "EUROFER", @@ -57,7 +57,7 @@ } }, "first_wall": { - "class": "first_wall", + "class": "FirstWallComponent", "material": "Tungsten", "geometry": { "inner width": 1480, @@ -78,7 +78,7 @@ } }, "front_rib": { - "class": "front_rib", + "class": "FrontRib", "geometry": { "thickness": 30, "side channel width": 10, @@ -87,7 +87,7 @@ } }, "back_rib": { - "class": "back_rib", + "class": "BackRib", "geometry": { "thickness": 60, "side channel width": 10, @@ -96,7 +96,7 @@ } }, "coolant_outlet_plenum": { - "class": "coolant_outlet_plenum", + "class": "CoolantOutletPlenum", "geometry": { "length": 120, "width": 1000, diff --git a/examples/sample_blanket_ring.json b/examples/sample_blanket_ring.json index 2c3c6cf..d6f61bc 100644 --- a/examples/sample_blanket_ring.json +++ b/examples/sample_blanket_ring.json @@ -1,5 +1,5 @@ { - "class": "blanket_ring", + "class": "BlanketRingAssembly", "geometry": { "minimum radius": 580 }, diff --git a/examples/sample_blanket_shell.json b/examples/sample_blanket_shell.json index 4d64ab7..f5ef6d5 100644 --- a/examples/sample_blanket_shell.json +++ b/examples/sample_blanket_shell.json @@ -1,5 +1,5 @@ { - "class": "blanket_shell", + "class": "BlanketShellAssembly", "geometry": { "pin spacing": 135, "vertical offset": 0, diff --git a/examples/sample_first_wall.json b/examples/sample_first_wall.json index e6fe009..b9936cc 100644 --- a/examples/sample_first_wall.json +++ b/examples/sample_first_wall.json @@ -1,5 +1,5 @@ { - "class": "first_wall", + "class": "FirstWallComponent", "material": "Tungsten", "geometry": { "inner width": 1480, diff --git a/examples/sample_mini_blanket.json b/examples/sample_mini_blanket.json index 2982fc2..0070ce1 100644 --- a/examples/sample_mini_blanket.json +++ b/examples/sample_mini_blanket.json @@ -1,5 +1,5 @@ { - "class": "blanket_shell", + "class": "BlanketShellAssembly", "geometry": { "pin spacing": 135, "vertical offset": 0, @@ -7,7 +7,7 @@ }, "components": { "first_wall": { - "class": "first_wall", + "class": "FirstWallComponent", "material": "Tungsten", "geometry": { "inner width": 480, @@ -19,7 +19,7 @@ } }, "pin": { - "class": "pin", + "class": "PinAssembly", "material": { "cladding": "Steel", "pressure tube": "Steel", diff --git a/examples/sample_morphology.json b/examples/sample_morphology.json index 325702a..8dda380 100644 --- a/examples/sample_morphology.json +++ b/examples/sample_morphology.json @@ -1,10 +1,10 @@ [ { - "class": "neutron_test_facility", + "class": "NeutronTestFacility", "morphology": "overlap", "components": [ { - "class" : "source", + "class" : "SourceAssembly", "filepath": "./dummy_source.stp", "group": "neutron_source", "manufacturer": "blobcorp", @@ -15,10 +15,10 @@ }, { - "class": "room", + "class": "RoomAssembly", "components": [ { - "class": "surrounding_walls", + "class": "SurroundingWallsComponent", "material": "SS", "air": "some_gas", "geometry": { @@ -28,7 +28,7 @@ }, { - "class": "wall", + "class": "WallComponent", "material": "SS", "geometry": { "thickness": 5, diff --git a/examples/sample_pin.json b/examples/sample_pin.json index 3dc9f78..8ec4a84 100644 --- a/examples/sample_pin.json +++ b/examples/sample_pin.json @@ -1,5 +1,5 @@ { - "class": "pin", + "class": "PinAssembly", "material": { "cladding": "EUROFER", "pressure tube": "EUROFER", diff --git a/src/hypnos/assemblies.py b/src/hypnos/assemblies.py index 3dd7aab..4c24197 100644 --- a/src/hypnos/assemblies.py +++ b/src/hypnos/assemblies.py @@ -40,7 +40,6 @@ from hypnos.geometry import Vertex, arctan from hypnos.cubit_functions import to_bodies from hypnos.constants import ( - CLASS_MAPPING, NEUTRON_TEST_FACILITY_REQUIREMENTS, ROOM_REQUIREMENTS, BLANKET_REQUIREMENTS, @@ -412,7 +411,7 @@ def __init__(self, json_object): # Take out any walls from component list json_walls = [] for json_component in component_list: - if json_component["class"] == "wall": + if json_component["class"] == "WallComponent": json_walls.append(json_component) component_list.remove(json_component) json_object["components"] = component_list @@ -869,17 +868,17 @@ def setup_assembly(self): def __add_component_attributes(self): for component in self.component_list: - if component["class"] == "first_wall": + if component["class"] == "FirstWallComponent": self.first_wall_geometry = component["geometry"] self.first_wall_material = component["material"] - elif component["class"] == "pin": + elif component["class"] == "PinAssembly": self.breeder_materials = component["material"] self.breeder_geometry = component["geometry"] - elif component["class"] == "front_rib": + elif component["class"] == "FrontRib": self.front_ribs_geometry = component["geometry"] - elif component["class"] == "back_rib": + elif component["class"] == "BackRib": self.back_ribs_geometry = component["geometry"] - elif component["class"] == "coolant_outlet_plenum": + elif component["class"] == "CoolantOutletPlenum": self.cop_geometry = component["geometry"] def __dict_with_height(self): @@ -1129,10 +1128,8 @@ def get_all_geometries_from_components(component_list) -> list[CubitInstance]: for component in component_list: if isinstance(component, CubitInstance): instances.append(component) - elif isinstance(component, SimpleComponent): - instances += component.subcomponents - elif isinstance(component, GenericComponentAssembly): - instances += component.get_all_geometries() + elif isinstance(component, ComponentBase): + instances += component.get_geometries() return instances @@ -1187,8 +1184,24 @@ def construct(json_object: dict, *args): Returns ------- - SimpleComponent | GenericComponentAssembly + ComponentBase Instantiated python class ''' - constructor = globals()[CLASS_MAPPING[json_object["class"]]] + constructor = classdict(ComponentBase)[json_object["class"]] return constructor(json_object, *args) + + +# it take so long to run :( +def classdict(cls): + '''Find all subclasses recursively. + + Returns + ------- + dict + {"Class": Class} + ''' + return_dict = {} + for subcls in cls.__subclasses__(): + return_dict[subcls.__name__] = subcls + return_dict.update(classdict(subcls)) + return return_dict diff --git a/src/hypnos/components.py b/src/hypnos/components.py index 2b04a5b..7730035 100644 --- a/src/hypnos/components.py +++ b/src/hypnos/components.py @@ -68,13 +68,6 @@ def classname(self, new_classname): def classname(self): del self._classname - @classmethod - def from_classname(cls, classname, params): - for toplvl in cls.__subclasses__(): - for construct_lvl in toplvl.__subclasses__(): - if construct_lvl.classname == classname: - return construct_lvl(params) - def check_sanity(self): '''Check whether the parameters supplied to this instance are physical ''' @@ -188,6 +181,9 @@ def volume_id_string(self): self.as_volumes() return " ".join([str(cmp.cid) for cmp in self.get_geometries() if cmp.geometry_type == "volume"]) + def get_volume(self): + return sum([geom.get_volume() for geom in self.get_geometries()]) + class SurroundingWallsComponent(SimpleComponent): '''Surrounding walls, filled with air''' diff --git a/src/hypnos/constants.py b/src/hypnos/constants.py index 8ef78c6..760a84a 100644 --- a/src/hypnos/constants.py +++ b/src/hypnos/constants.py @@ -8,43 +8,17 @@ ''' # required components for assemblies to be generated -NEUTRON_TEST_FACILITY_REQUIREMENTS = ["room", "source"] -BLANKET_REQUIREMENTS = ["breeder", "structure"] -ROOM_REQUIREMENTS = ["blanket", "surrounding_walls"] -BLANKET_SHELL_REQUIREMENTS = ["first_wall", "pin"] +NEUTRON_TEST_FACILITY_REQUIREMENTS = ["RoomAssembly", "SourceAssembly"] +BLANKET_REQUIREMENTS = ["BreederComponent", "StructureComponent"] +ROOM_REQUIREMENTS = ["BlanketComponent", "SurroundingWallsComponent"] +BLANKET_SHELL_REQUIREMENTS = ["FirstWallComponent", "PinAssembly"] HCPB_BLANKET_REQUIREMENTS = [ - "first_wall", - "pin", - "front_rib", - "back_rib", - "coolant_outlet_plenum" + "FirstWallComponent", + "PinAssembly", + "FrontRib", + "BackRib", + "CoolantOutletPlenum" ] -# LEGACY - classes according to what make_geometry subfunction(?) needs to be called -BLOB_CLASSES = ["complex", "breeder", "structure", "air"] -ROOM_CLASSES = ["surrounding_walls"] -WALL_CLASSES = ["wall"] - # LEGACY - currently only supports exclusive, inclusive, and overlap FACILITY_MORPHOLOGIES = ["exclusive", "inclusive", "overlap", "wall"] - -# mapping from json class names to python class names -CLASS_MAPPING = { - "complex": "ComplexComponent", - "external": "ExternalComponentAssembly", - "source": "SourceAssembly", - "neutron_test_facility": "NeutronTestFacility", - "blanket": "BlanketAssembly", - "room": "RoomAssembly", - "surrounding_walls": "SurroundingWallsComponent", - "breeder": "BreederComponent", - "structure": "StructureComponent", - "pin": "PinAssembly", - "cladding": "CladdingComponent", - "pressure_tube": "PressureTubeComponent", - "multiplier": "MultiplierComponent", - "first_wall": "FirstWallComponent", - "blanket_shell": "BlanketShellAssembly", - "blanket_ring": "BlanketRingAssembly", - "HCPB_blanket": "HCPBBlanket" -} diff --git a/src/hypnos/default_params.py b/src/hypnos/default_params.py index 27090ee..6b235f1 100644 --- a/src/hypnos/default_params.py +++ b/src/hypnos/default_params.py @@ -8,7 +8,7 @@ ''' PIN = { - "class": "pin", + "class": "PinAssembly", "material": { "cladding": "EUROFER", "pressure tube": "EUROFER", @@ -43,7 +43,7 @@ } FIRST_WALL = { - "class": "first_wall", + "class": "FirstWallComponent", "material": "Tungsten", "geometry": { "inner width": 1480, @@ -65,34 +65,34 @@ } BLANKET_SHELL = { - "class": "blanket_shell", + "class": "BlanketShellAssembly", "geometry": { "pin spacing": 135, "vertical offset": 0, "horizontal offset": 0 }, "components": { - "pin": PIN, - "first_wall": FIRST_WALL + "PinAssembly": PIN, + "FirstWallComponent": FIRST_WALL } } BLANKET_RING = { - "class": "blanket_ring", + "class": "BlanketRingAssembly", "geometry": { "minimum radius": 580 }, "components": { - "blanket_shell": { - "class": "blanket_shell", + "BlanketShellAssembly": { + "class": "BlanketShellAssembly", "geometry": { "pin spacing": 135, "vertical offset": 0, "horizontal offset": 0 }, "components": { - "first_wall": { - "class": "first_wall", + "FirstWallComponent": { + "class": "FirstWallComponent", "material": "Tungsten", "geometry": { "inner width": 480, @@ -103,8 +103,8 @@ "height": 580 } }, - "pin": { - "class": "pin", + "PinAssembly": { + "class": "PinAssembly", "material": { "cladding": "EUROFER", "pressure tube": "EUROFER", @@ -137,7 +137,7 @@ } HCPB_BLANKET = { - "class": "HCPB_blanket", + "class": "HCPBBlanket", "geometry": { "pin spacing": 135, "pin vertical offset": 0, @@ -160,8 +160,8 @@ "FW backplate thickness": 100 }, "components": { - "pin": { - "class": "pin", + "PinAssembly": { + "class": "PinAssembly", "material": { "cladding": "EUROFER", "pressure tube": "EUROFER", @@ -194,8 +194,8 @@ "filter lid length": 300 } }, - "first_wall": { - "class": "first_wall", + "FirstWallComponent": { + "class": "FirstWallComponent", "material": "Tungsten", "geometry": { "inner width": 1480, @@ -215,8 +215,8 @@ "channel spacing": 10 } }, - "front_rib": { - "class": "front_rib", + "FrontRib": { + "class": "FrontRib", "geometry": { "thickness": 30, "side channel width": 10, @@ -224,8 +224,8 @@ "side channel horizontal offset": 0 } }, - "back_rib": { - "class": "back_rib", + "BackRib": { + "class": "BackRib", "geometry": { "thickness": 60, "side channel width": 10, @@ -233,8 +233,8 @@ "side channel horizontal offset": 70 } }, - "coolant_outlet_plenum": { - "class": "coolant_outlet_plenum", + "CoolantOutletPlenum": { + "class": "CoolantOutletPlenum", "geometry": { "length": 120, "width": 1000, diff --git a/src/hypnos/generic_classes.py b/src/hypnos/generic_classes.py index fd073ad..ab3098d 100644 --- a/src/hypnos/generic_classes.py +++ b/src/hypnos/generic_classes.py @@ -80,6 +80,18 @@ def update_reference(self, cid: int, geometry_type: str): self.geometry_type = geometry_type self.handle = get_cubit_geometry(cid, geometry_type) + def get_volume(self) -> float: + '''get volume of this geometry + + Returns + ------- + float + volume of geometry + ''' + if self.geometry_type in ["body", "volume"]: + return self.handle.volume() + return 0 + # make finding handles less annoying - used by CubitInstance def get_cubit_geometry(geometry_id: int, geometry_type: str): diff --git a/src/hypnos/geometry_maker.py b/src/hypnos/geometry_maker.py index e04b402..0f3202c 100644 --- a/src/hypnos/geometry_maker.py +++ b/src/hypnos/geometry_maker.py @@ -8,32 +8,14 @@ ''' from hypnos.tracking import Tracker -from hypnos.assemblies import construct +from hypnos.components import ComponentBase +from hypnos.assemblies import classdict from hypnos.generic_classes import CubismError, cmd from hypnos.cubit_functions import initialise_cubit, reset_cubit from hypnos.parsing import extract_data, ParameterFiller, get_format_extension import functools -def make_everything(json_object): - '''Construct all specified components - - Parameters - ---------- - json_object : dict or list - Description of geometry(ies) to construct in cubit - - Returns - ------- - Class corresponding to constructed geometry - ''' - if type(json_object) is list: - return [construct(json_component) for json_component in json_object] - elif type(json_object) is dict: - return [construct(json_object)] - raise CubismError("json object not recognised") - - def log_method(method_name: str): '''Decorator to print logs for class methods @@ -63,7 +45,7 @@ class GeometryMaker(): constructed geometry key_route_delimiter (str): delimiter for parameter paths ''' - def __init__(self) -> None: + def __init__(self, custom_classes=[]) -> None: initialise_cubit() self.parameter_filler = ParameterFiller() self.tracker = Tracker() @@ -72,6 +54,9 @@ def __init__(self) -> None: self.print_parameter_logs = False self.track_components = False self.key_route_delimiter = '/' + self.class_dict = classdict(ComponentBase) + for cls in custom_classes: + self.class_dict[cls.__name__] = cls def fill_design_tree(self): '''Process design_tree manually @@ -188,7 +173,7 @@ def __build_param_dict(self, key_route: list, param_dict: dict, updated_value): raise CubismError("Path given does not correspond to existing parameters") param_dict[key_route[0]] = self.__build_param_dict(key_route[1:], param_dict[key_route[0]], updated_value) return param_dict - + @log_method("Making geometry") def make_geometry(self): '''Build geometry corresponding to design tree in cubit @@ -197,7 +182,15 @@ def make_geometry(self): ------- Class corresponding to the constructed cubit geometry ''' - self.constructed_geometry = make_everything(self.design_tree) + params = self.design_tree + classname = params["class"] + + if type(self.design_tree) is list: + self.constructed_geometry = [self.class_dict[classname](param) for param in params] + elif type(self.design_tree) is dict: + self.constructed_geometry = [self.class_dict[classname](params)] + else: + raise CubismError("json object not recognised") return self.constructed_geometry @log_method("Imprint and merge") diff --git a/tests/geometry_maker_testing/parse_this.json b/tests/geometry_maker_testing/parse_this.json index e2be44d..cb79917 100644 --- a/tests/geometry_maker_testing/parse_this.json +++ b/tests/geometry_maker_testing/parse_this.json @@ -1 +1 @@ -{"class": "pin"} \ No newline at end of file +{"class": "PinAssembly"} \ No newline at end of file diff --git a/tests/sample_test.json b/tests/sample_test.json index 04a1682..769359b 100644 --- a/tests/sample_test.json +++ b/tests/sample_test.json @@ -1,5 +1,5 @@ { - "class": "HCPB_blanket", + "class": "HCPBBlanket", "geometry": { "pin spacing": 135, "pin vertical offset": 0, @@ -22,9 +22,9 @@ "FW backplate thickness": 100 }, "components": { - "pin": {"class": "pin"}, - "first_wall": { - "class": "first_wall", + "PinAssembly": {"class": "PinAssembly"}, + "FirstWallComponent": { + "class": "FirstWallComponent", "material": "Tungsten", "geometry": { "inner width": 1480, @@ -44,8 +44,8 @@ "channel spacing": 10 } }, - "front_rib": { - "class": "front_rib", + "FrontRib": { + "class": "FrontRib", "geometry": { "thickness": 30, "side channel width": 10, @@ -53,8 +53,8 @@ "side channel horizontal offset": 0 } }, - "back_rib": { - "class": "back_rib", + "BackRib": { + "class": "BackRib", "geometry": { "thickness": 60, "side channel width": 10, @@ -62,8 +62,8 @@ "side channel horizontal offset": 70 } }, - "coolant_outlet_plenum": { - "class": "coolant_outlet_plenum", + "CoolantOutletPlenum": { + "class": "CoolantOutletPlenum", "geometry": { "length": 120, "width": 1000, diff --git a/tests/test_components.py b/tests/test_components.py index c5b85d0..fd32c10 100644 --- a/tests/test_components.py +++ b/tests/test_components.py @@ -76,3 +76,7 @@ def test_extract_parameters(simple_component: SimpleComponent): def test_vol_id_string(simple_component: SimpleComponent): assert simple_component.volume_id_string() == "1" + + +def test_get_volume(simple_component: SimpleComponent): + assert simple_component.get_volume() == 125 diff --git a/tests/test_generic_classes.py b/tests/test_generic_classes.py index fa40858..723abfb 100644 --- a/tests/test_generic_classes.py +++ b/tests/test_generic_classes.py @@ -55,6 +55,12 @@ def test_move(brick): assert brick_vol.centroid() == (1, 1, 1) +def test_get_volume(brick): + assert brick.get_volume() == 1 + fake_brick = CubitInstance(1, "surface") + assert fake_brick.get_volume() == 0 + + def test_update_reference(brick): # here we want to check that 'updating' the reference to volume 1 will # make CubitInstance refer to the same volume diff --git a/tests/test_geometry_maker.py b/tests/test_geometry_maker.py index 3d41b16..7efff4a 100644 --- a/tests/test_geometry_maker.py +++ b/tests/test_geometry_maker.py @@ -1,8 +1,9 @@ -from hypnos.geometry_maker import GeometryMaker, make_everything +from hypnos.geometry_maker import GeometryMaker from hypnos.default_params import PIN from hypnos.generic_classes import CubismError from hypnos.assemblies import PinAssembly from hypnos.components import ( + SimpleComponent, MultiplierComponent, PressureTubeComponent, CladdingComponent, @@ -12,6 +13,7 @@ FilterDiskComponent, PinBreeder ) +from hypnos.geometry import create_brick import difflib import sys @@ -82,18 +84,14 @@ def compare_stp(filepath1: str, filepath2: str): return True -@pytest.fixture(scope='function') -def maker(): - return GeometryMaker() - - @pytest.fixture(scope="function") def dirpath(pytestconfig): return pytestconfig.rootpath / "tests" / "geometry_maker_testing" @pytest.fixture(scope='function') -def parsed(maker, dirpath): +def parsed(dirpath): + maker = GeometryMaker() parse_file = dirpath / PARSE_FILE maker.parse_json(parse_file) return maker @@ -104,27 +102,6 @@ def goldpath(pytestconfig): return pytestconfig.rootpath / "tests" / "gold" -def test_make_everything(): - # this should make a pin assembly - geom_list = make_everything(PIN) - geom = geom_list[0] - assert len(geom_list) == 1 - assert isinstance(geom, PinAssembly) - - # make sure it contains all the components we want - comp_classes = [type(comp) for comp in geom.components] - for pin_comp in PIN_COMPS: - assert pin_comp in comp_classes - - fake_json_obj = [PIN] - json_list = make_everything(fake_json_obj) - assert len(json_list) == 1 - assert isinstance(json_list[0], PinAssembly) - - with pytest.raises(CubismError): - make_everything(1) - - def test_parse_json(parsed): assert parsed.design_tree == PIN @@ -192,3 +169,38 @@ def test_export_existence(parsed, tmp_path): with pytest.raises(CubismError): parsed.export("not a file type", file_path) + + +class CustomComponent(SimpleComponent): + def __init__(self, params): + super().__init__("custom", params) + + def check_sanity(self): + length = self.geometry["length"] + height = self.geometry["height"] + if length < 0 or height < 0: + raise ValueError("parameters must be positive") + + def make_geometry(self): + length = self.geometry["length"] + height = self.geometry["height"] + brick = create_brick({"dimensions": [length, length, height]}) + return brick + + +def test_custom_components(): + maker = GeometryMaker(custom_classes=[CustomComponent]) + custom_dict = { + "class": "CustomComponent", + "material": "none", + "geometry": { + "length": 7, + "height": 3 + } + } + maker.design_tree = custom_dict + maker.make_geometry() + custom_comp = maker.constructed_geometry[0] + + assert isinstance(custom_comp, CustomComponent) + assert custom_comp.get_volume() == 147