diff --git a/.github/workflows/auto_format_pep8.yml b/.github/workflows/auto_format_pep8.yml index a329f2ee5..88ee8807c 100644 --- a/.github/workflows/auto_format_pep8.yml +++ b/.github/workflows/auto_format_pep8.yml @@ -1,4 +1,4 @@ -name: black +name: auto_format_pep8 on: push: @@ -23,7 +23,7 @@ jobs: - name: Install black run: | python -m pip install --upgrade pip - pip install black==22.1.0 + pip install black==22.3.0 - name: Run black run: | black . diff --git a/.github/workflows/auto_lint.yml b/.github/workflows/auto_lint.yml index 41ce6764e..63a74f7d1 100644 --- a/.github/workflows/auto_lint.yml +++ b/.github/workflows/auto_lint.yml @@ -1,4 +1,4 @@ -name: black +name: auto_lint on: push: diff --git a/.github/workflows/test_demos.yml b/.github/workflows/test_demos.yml index dfe7e15ad..efa86952b 100644 --- a/.github/workflows/test_demos.yml +++ b/.github/workflows/test_demos.yml @@ -7,6 +7,13 @@ on: branches: - main - develop + paths: + - "**.py" + - "**.ipynb" + - "**.yml" + - "**.cfg" + - "**.toml" + - "**.sh" # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: diff --git a/README.md b/README.md index b9ce3e659..43949ce60 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ [![codecov](https://codecov.io/gh/fusion-energy/paramak/branch/main/graph/badge.svg)](https://codecov.io/gh/fusion-energy/paramak) -[![Code Grade](https://www.code-inspector.com/project/25342/score/svg)](https://frontend.code-inspector.com/public/project/25342/paramak/dashboard) -[![Code Grade](https://www.code-inspector.com/project/25342/status/svg)](https://frontend.code-inspector.com/public/project/25342/paramak/dashboard) +[![Code Grade](https://api.codiga.io/project/25342/score/svg)](https://app.codiga.io/public/project/25342/paramak/dashboard) +[![Code Grade](https://api.codiga.io/project/25342/status/svg)](https://app.codiga.io/public/project/25342/paramak/dashboard) [![Documentation Status](https://readthedocs.org/projects/paramak/badge/?version=main)](https://paramak.readthedocs.io/en/main/?badge=main) @@ -17,16 +17,19 @@ [![Upload Python Package](https://github.com/fusion-energy/paramak/actions/workflows/python-publish.yml/badge.svg)](https://github.com/fusion-energy/paramak/actions/workflows/python-publish.yml) [![PyPI](https://img.shields.io/pypi/v/paramak?color=brightgreen&label=pypi&logo=grebrightgreenen&logoColor=green)](https://pypi.org/project/paramak/) +[![anaconda-publish](https://github.com/fusion-energy/paramak/actions/workflows/anaconda-publish.yml/badge.svg)](https://github.com/fusion-energy/paramak/actions/workflows/anaconda-publish.yml) +[![anaconda.org](https://anaconda.org/fusion-energy/paramak/badges/version.svg)](https://anaconda.org/fusion-energy/paramak) + [![docker-publish-release](https://github.com/fusion-energy/paramak/actions/workflows/docker_publish.yml/badge.svg)](https://github.com/fusion-energy/paramak/actions/workflows/docker_publish.yml) [![DOI](https://zenodo.org/badge/269635577.svg)](https://zenodo.org/badge/latestdoi/269635577) # Paramak -The Paramak python package allows rapid production of 3D CAD models of fusion -reactors. The purpose of the Paramak is to provide geometry for parametric -studies. The paramak can create geometry in standard CAD formats such as STP, -STL and Brep. +Paramak python package allows rapid production of 3D CAD models and neutronics +models of fusion reactors. The purpose of Paramak is to provide geometry for +parametric studies. Paramak can create geometry in standard CAD formats such as +STP, STL, BRep, HTML and DAGMC h5m. :point_right: [Documentation](https://paramak.readthedocs.io) diff --git a/conda/meta.yaml b/conda/meta.yaml index 45d7c4b04..35887f271 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -19,10 +19,10 @@ requirements: - python {{ python }} - cadquery {{ cadquery }} - mpmath - - plasmaboundaries + - plasmaboundaries >=0.1.8 - plotly - - brep_part_finder # [not win] - - brep_to_h5m # [not win] + - brep_part_finder >=0.4.1 # [not win] + - brep_to_h5m >=0.3.1 # [not win] # - jupyter-cadquery not available on conda test: diff --git a/docs/source/API-Reference.rst b/docs/source/API-Reference.rst index dedbf4cfa..aa38f3b5d 100644 --- a/docs/source/API-Reference.rst +++ b/docs/source/API-Reference.rst @@ -1516,6 +1516,43 @@ CapsuleVacuumVessel() :show-inheritance: +DishedVacuumVessel() +^^^^^^^^^^^^^^^^^^^^ +.. cadquery:: + :select: cadquery_object + :gridsize: 0 + + import paramak + my_component = paramak.DishedVacuumVessel(rotation_angle=180) + + cadquery_object = my_component.solid + +.. image:: https://user-images.githubusercontent.com/8583900/160503281-3aabf145-22b0-4953-bc4f-3f75a6696b5e.png + +.. automodule:: paramak.parametric_components.dished_vacuum_vessel + :members: + :show-inheritance: + +ConstantThicknessDome() +^^^^^^^^^^^^^^^^^^^^^^^ +.. cadquery:: + :select: cadquery_object + :gridsize: 0 + + import paramak + my_component = paramak.ConstantThicknessDome(rotation_angle=180) + + cadquery_object = my_component.solid + +.. image:: https://user-images.githubusercontent.com/8583900/160503286-71a6d771-1d47-476e-a0cf-85fb94c9389c.png + +.. automodule:: paramak.parametric_components.constant_thickness_dome + :members: + :show-inheritance: + + + + Other components ---------------- diff --git a/examples/example_parametric_reactors/render_of_random_reactor_wireframe_with_text_gif.py b/examples/example_parametric_reactors/render_of_random_reactor_wireframe_with_text_gif.py index 4640e028c..92c70f2bb 100644 --- a/examples/example_parametric_reactors/render_of_random_reactor_wireframe_with_text_gif.py +++ b/examples/example_parametric_reactors/render_of_random_reactor_wireframe_with_text_gif.py @@ -3,11 +3,9 @@ # animation. Two animations are made, of of a 3D render and one of a wireframe # line drawing. -import math import os # to run this example you will need all of the following packages installed -import matplotlib.pyplot as plt import numpy as np import paramak diff --git a/paramak/__init__.py b/paramak/__init__.py index 7f8a70a5b..8c222acda 100644 --- a/paramak/__init__.py +++ b/paramak/__init__.py @@ -13,7 +13,18 @@ from .shape import Shape from .reactor import Reactor -from .utils import rotate, extend, distance_between_two_points, diff_between_angles +from .utils import ( + rotate, + extend, + distance_between_two_points, + diff_between_angles, + find_center_point_of_circle, + angle_between_two_points_on_circle, + find_radius_of_circle, + export_solids_to_brep, + export_solids_to_dagmc_h5m, + get_center_of_bounding_box, +) from .utils import EdgeLengthSelector, FaceAreaSelector from .parametric_shapes.extruded_mixed_shape import ExtrudeMixedShape @@ -113,7 +124,10 @@ ToroidalFieldCoilRectangleRoundCorners, ) +from .parametric_components.constant_thickness_dome import ConstantThicknessDome from .parametric_components.vacuum_vessel import VacuumVessel + +from .parametric_components.dished_vacuum_vessel import DishedVacuumVessel from .parametric_components.vacuum_vessel_inner_leg import VacuumVesselInnerLeg from .parametric_components.capsule_vacuum_vessel import CapsuleVacuumVessel from .parametric_components.hollow_cube import HollowCube diff --git a/paramak/parametric_components/capsule_vacuum_vessel.py b/paramak/parametric_components/capsule_vacuum_vessel.py index bc77bfa2a..26e61432c 100644 --- a/paramak/parametric_components/capsule_vacuum_vessel.py +++ b/paramak/parametric_components/capsule_vacuum_vessel.py @@ -6,7 +6,7 @@ class CapsuleVacuumVessel(RotateMixedShape): """A cylindrical vessel volume with constant thickness that has addition - spherical edges. + hemispherical head. Arguments: outer_start_point: the x,z coordinates of the outer bottom of the diff --git a/paramak/parametric_components/constant_thickness_dome.py b/paramak/parametric_components/constant_thickness_dome.py new file mode 100644 index 000000000..45ee92e32 --- /dev/null +++ b/paramak/parametric_components/constant_thickness_dome.py @@ -0,0 +1,210 @@ +import math +from paramak import RotateMixedShape, RotateStraightShape, Shape, CuttingWedge +import cadquery as cq + + +class ConstantThicknessDome(RotateMixedShape): + """A cylindrical vessel volume with constant thickness with a simple dished + head. This style of tank head has no knuckle radius or straight flange. The + dished shape is made from a chord of a circle. + + Arguments: + thickness: the radial thickness of the dome. + chord_center_height: the vertical position of the chord center + chord_width: the width of the chord base + chord_height: the height of the chord which is also distance between + the chord_center_height and the inner surface of the dome + upper_or_lower: Curves the dish with a positive or negative direction + to allow the upper section or lower section of vacuum vessel + domes to be made. + name: the name of the shape, used in the graph legend and as a + filename prefix when exporting. + """ + + def __init__( + self, + thickness: float = 10, + chord_center_height: float = 0, + chord_width: float = 100, + chord_height: float = 20, + upper_or_lower: str = "upper", + name: str = "constant_thickness_dome", + **kwargs, + ): + + self.thickness = thickness + self.chord_center_height = chord_center_height + self.chord_width = chord_width + self.chord_height = chord_height + self.upper_or_lower = upper_or_lower + self.name = name + + super().__init__(name=name, **kwargs) + + @property + def chord_width(self): + return self._chord_width + + @chord_width.setter + def chord_width(self, value): + if not isinstance(value, (float, int)): + raise ValueError("ConstantThicknessDome.chord_width must be a number. Not", value) + if value <= 0: + msg = f"ConstantThicknessDome.chord_width must be a positive number above 0. Not {value}" + raise ValueError(msg) + self._chord_width = value + + @property + def chord_height(self): + return self._chord_height + + @chord_height.setter + def chord_height(self, value): + if not isinstance(value, (float, int)): + raise ValueError("ConstantThicknessDome.chord_height must be a number. Not", value) + if value <= 0: + msg = f"ConstantThicknessDome.chord_height must be a positive number above 0. Not {value}" + raise ValueError(msg) + self._chord_height = value + + @property + def thickness(self): + return self._thickness + + @thickness.setter + def thickness(self, value): + if not isinstance(value, (float, int)): + msg = f"VacuumVessel.thickness must be a number. Not {value}" + raise ValueError(msg) + if value <= 0: + msg = f"VacuumVessel.thickness must be a positive number above 0. Not {value}" + raise ValueError(msg) + self._thickness = value + + def find_points(self): + """ + Finds the XZ points joined by straight and circle connections that + describe the 2D profile of the vessel shape. + """ + + # Note these points are not used in the normal way when constructing + # the solid + # + # 6 - + # | - + # 7 - - + # - - + # - 3 + # - | + # cc 1 -- 2 + # chord center + # + # + # cp + # center point + # + # + # + # + # cc 1 -- 2 + # - | + # - 3 + # - - + # 7 - - + # | - + # 6 - + # far side + + if self.chord_height * 2 >= self.chord_width: + msg = "ConstantThicknessDome requires that the self.chord_width is at least 2 times as large as the chord height" + raise ValueError(msg) + + radius_of_sphere = ((math.pow(self.chord_width, 2)) + (4.0 * math.pow(self.chord_height, 2))) / ( + 8 * self.chord_height + ) + + # TODO set to 0 for now, add ability to shift the center of the chord left and right + chord_center = (0, self.chord_center_height) + + point_1 = (chord_center[0] + (self.chord_width / 2), chord_center[1], "straight") + + if self.upper_or_lower == "upper": + center_point = (chord_center[0], chord_center[1] + self.chord_height - radius_of_sphere) + inner_tri_angle = math.atan((center_point[1] - chord_center[1]) / (self.chord_width / 2)) + outer_tri_adj = math.cos(inner_tri_angle) * self.thickness + point_2 = (point_1[0] + self.thickness, point_1[1], "straight") + outer_tri_opp = math.sqrt(math.pow(self.thickness, 2) - math.pow(outer_tri_adj, 2)) + point_7 = (chord_center[0], chord_center[1] + radius_of_sphere, "straight") + point_6 = (chord_center[0], chord_center[1] + radius_of_sphere + self.thickness, "straight") + self.far_side = (center_point[0], center_point[1] - (radius_of_sphere + self.thickness)) + point_3 = (point_2[0], point_2[1] + outer_tri_opp, "straight") + elif self.upper_or_lower == "lower": + center_point = (chord_center[0], chord_center[1] - self.chord_height + radius_of_sphere) + inner_tri_angle = math.atan((center_point[1] - chord_center[1]) / (self.chord_width / 2)) + outer_tri_adj = math.cos(inner_tri_angle) * self.thickness + point_2 = (point_1[0] + self.thickness, point_1[1], "straight") + outer_tri_opp = math.sqrt(math.pow(self.thickness, 2) - math.pow(outer_tri_adj, 2)) + point_7 = (chord_center[0], chord_center[1] - radius_of_sphere, "straight") + point_6 = (chord_center[0], chord_center[1] - (radius_of_sphere + self.thickness), "straight") + self.far_side = (center_point[0], center_point[1] + radius_of_sphere + self.thickness) + point_3 = (point_2[0], point_2[1] - outer_tri_opp, "straight") + else: + msg = f'upper_or_lower should be either "upper" or "lower". Not {self.upper_or_lower}' + raise ValueError(msg) + + self.points = [point_1, point_2, point_3, point_6, point_7] + + def create_solid(self): + """Creates a rotated 3d solid using points with circular edges. + + Returns: + A CadQuery solid: A 3D solid volume + """ + + radius_of_sphere = ((math.pow(self.chord_width, 2)) + (4.0 * math.pow(self.chord_height, 2))) / ( + 8 * self.chord_height + ) + + # TODO set to 0 for now, add ability to shift the center of the chord left and right + chord_center = (0, self.chord_center_height) + + if self.upper_or_lower == "upper": + center_point = (chord_center[0], chord_center[1] + self.chord_height - radius_of_sphere) + far_side = (center_point[0], center_point[1] - (radius_of_sphere + self.thickness)) + elif self.upper_or_lower == "lower": + center_point = (chord_center[0], chord_center[1] - self.chord_height + radius_of_sphere) + far_side = (center_point[0], center_point[1] + radius_of_sphere + self.thickness) + else: + raise ValueError("self.upper_or_lower") + + big_sphere = ( + cq.Workplane(self.workplane) + .moveTo(center_point[0], center_point[1]) + .sphere(radius_of_sphere + self.thickness) + ) + small_sphere = cq.Workplane(self.workplane).moveTo(center_point[0], center_point[1]).sphere(radius_of_sphere) + + outer_cylinder_cutter = RotateStraightShape( + workplane=self.workplane, + points=( + (chord_center[0], chord_center[1]), # cc + (self.points[1][0], self.points[1][1]), # point 2 + (self.points[2][0], self.points[2][1]), # point 3 + (self.points[2][0] + radius_of_sphere, self.points[2][1]), # point 3 wider + (self.points[2][0] + radius_of_sphere, far_side[1]), + far_side, + ), + rotation_angle=360, + ) + + cap = Shape() + cap.solid = big_sphere.cut(small_sphere) + + height = 2 * (radius_of_sphere + abs(center_point[1]) + self.thickness) + radius = 2 * (radius_of_sphere + abs(center_point[0]) + self.thickness) + cutter = CuttingWedge(height=height, radius=radius, rotation_angle=self.rotation_angle) + + cap.solid = cap.solid.intersect(cutter.solid) + cap.solid = cap.solid.cut(outer_cylinder_cutter.solid) + + self.solid = cap.solid diff --git a/paramak/parametric_components/cutting_wedge_fs.py b/paramak/parametric_components/cutting_wedge_fs.py index f732e8850..18f592e45 100644 --- a/paramak/parametric_components/cutting_wedge_fs.py +++ b/paramak/parametric_components/cutting_wedge_fs.py @@ -94,7 +94,10 @@ def azimuth_placement_angle(self, value): def find_radius_height(self): shape = self.shape if shape.rotation_angle == 360: - msg = "cutting_wedge cannot be created, rotation_angle must be " "less than 360 degrees" + msg = ( + "cutting_wedge cannot be created, the shapes rotation_angle " + f"must be less than 360 degrees. shape.rotation_angle is {shape.rotation_angle}" + ) raise ValueError(msg) shape_points = shape.points if hasattr(shape, "radius") and len(shape_points) == 1: diff --git a/paramak/parametric_components/dished_vacuum_vessel.py b/paramak/parametric_components/dished_vacuum_vessel.py new file mode 100644 index 000000000..20f53800d --- /dev/null +++ b/paramak/parametric_components/dished_vacuum_vessel.py @@ -0,0 +1,121 @@ +from paramak import RotateMixedShape, CenterColumnShieldCylinder, ConstantThicknessDome + + +class DishedVacuumVessel(RotateMixedShape): + """A cylindrical vessel volume with constant thickness with a simple dished + head. This style of tank head has no knuckle radius or straight flange. + + Arguments: + radius: the radius from which the centres of the vessel meets the outer + circumference. + center_point: the x,z coordinates of the center of the vessel + dish_height: the height of the dish section. This is also the chord + heigh of the circle used to make the dish. + cylinder_height: the height of the cylindrical section of the vacuum + vessel. + thickness: the radial thickness of the vessel in cm. + """ + + def __init__( + self, + radius: float = 300, + center_point: float = 0, + dish_height: float = 50, + cylinder_height: float = 400, + thickness: float = 15, + name: str = "dished_vacuum_vessel", + **kwargs, + ): + self.radius = radius + self.center_point = center_point + self.dish_height = dish_height + self.cylinder_height = cylinder_height + self.thickness = thickness + self.name = name + + super().__init__(name=name, **kwargs) + + @property + def radius(self): + return self._radius + + @radius.setter + def radius(self, value): + if not isinstance(value, (float, int)): + raise ValueError("VacuumVessel.radius must be a number. Not", value) + if value <= 0: + msg = "VacuumVessel.radius must be a positive number above 0. " f"Not {value}" + raise ValueError(msg) + self._radius = value + + @property + def thickness(self): + return self._thickness + + @thickness.setter + def thickness(self, value): + if not isinstance(value, (float, int)): + msg = f"VacuumVessel.thickness must be a number. Not {value}" + raise ValueError(msg) + if value <= 0: + msg = f"VacuumVessel.thickness must be a positive number above 0. Not {value}" + raise ValueError(msg) + self._thickness = value + + def create_solid(self): + """Creates a rotated 3d solid using points with circular edges. + + Returns: + A CadQuery solid: A 3D solid volume + """ + + # + # - - + # - + # - - - + # - - + # - - + # - | + # | | + # | | + # | | + # c,p | | + # | | + # | | + # | | + # - | + # - - + # - - + # - - - + # - + # - - + # + + cylinder_section = CenterColumnShieldCylinder( + height=self.cylinder_height, + inner_radius=self.radius - self.thickness, + outer_radius=self.radius, + center_height=self.center_point, + rotation_angle=self.rotation_angle, + ) + + upper_dome_section = ConstantThicknessDome( + thickness=self.thickness, + chord_center_height=self.center_point + 0.5 * self.cylinder_height, + chord_width=(self.radius - self.thickness) * 2, + chord_height=self.dish_height, + upper_or_lower="upper", + rotation_angle=self.rotation_angle, + ) + + lower_dome_section = ConstantThicknessDome( + thickness=self.thickness, + chord_center_height=self.center_point - 0.5 * self.cylinder_height, + chord_width=(self.radius - self.thickness) * 2, + chord_height=self.dish_height, + upper_or_lower="lower", + rotation_angle=self.rotation_angle, + ) + + upper_dome_section.solid = upper_dome_section.solid.union(cylinder_section.solid) + self.solid = lower_dome_section.solid.union(upper_dome_section.solid) diff --git a/paramak/parametric_components/extrude_hollow_rectangle.py b/paramak/parametric_components/extrude_hollow_rectangle.py index 24611800e..3f84b3f0b 100644 --- a/paramak/parametric_components/extrude_hollow_rectangle.py +++ b/paramak/parametric_components/extrude_hollow_rectangle.py @@ -8,9 +8,10 @@ class ExtrudeHollowRectangle(ExtrudeStraightShape): """Creates a rectangular with a hollow section extrusion. Args: - height: the vertical (z axis) height of the rectangle (cm). - width: the horizontal (x axis) width of the rectangle (cm). - casing_thickness: the thickness of the casing (cm). + height: the height of the internal hollow section. + width: the width of the internal hollow section. + distance: the depth of the internal hollow section. + casing_thickness: the thickness of the casing around the hollow section. center_point: the center of the rectangle (x,z) values (cm). name: defaults to "extrude_rectangle". """ @@ -82,7 +83,7 @@ def find_points(self): # 9-------------6 # | 4 -------5,1| # | | | | - # | | (0,0) | | + # | | cp | | # | | | | # | 3 ------- 2 | # 8-------------7 diff --git a/paramak/parametric_components/hollow_cube.py b/paramak/parametric_components/hollow_cube.py index 02f5d1e12..caa727127 100644 --- a/paramak/parametric_components/hollow_cube.py +++ b/paramak/parametric_components/hollow_cube.py @@ -1,5 +1,6 @@ -import cadquery as cq +from typing import Tuple +import cadquery as cq from paramak import Shape @@ -8,15 +9,24 @@ class HollowCube(Shape): Graveyard. Arguments: - length (float): The length to use for the height, width, depth of the + length: The length to use for the height, width, depth of the inner dimensions of the cube. - thickness (float, optional): thickness of the vessel. Defaults to 10.0. + thickness: thickness of the vessel. + center_coordinate: the location the center of the cube. """ - def __init__(self, length, thickness=10.0, **kwargs): + def __init__( + self, + length: float, + thickness: float = 10.0, + center_coordinate: Tuple[float, float, float] = (0.0, 0.0, 0.0), + name="hollow_cube", + **kwargs + ): + super().__init__(name=name, **kwargs) self.length = length self.thickness = thickness - super().__init__(**kwargs) + self.center_coordinate = center_coordinate @property def thickness(self): @@ -37,13 +47,17 @@ def length(self, value): def create_solid(self): # creates a small box that surrounds the geometry - inner_box = cq.Workplane("front").box(self.length, self.length, self.length) + inner_box = cq.Workplane("front").box(self.length, self.length, self.length).translate(self.center_coordinate) # creates a large box that surrounds the smaller box - outer_box = cq.Workplane("front").box( - self.length + self.thickness, - self.length + self.thickness, - self.length + self.thickness, + outer_box = ( + cq.Workplane("front") + .box( + self.length + self.thickness, + self.length + self.thickness, + self.length + self.thickness, + ) + .translate(self.center_coordinate) ) # subtracts the two boxes to leave a hollow box diff --git a/paramak/parametric_reactors/flf_system_code_reactor.py b/paramak/parametric_reactors/flf_system_code_reactor.py index 54e146910..f6158508a 100644 --- a/paramak/parametric_reactors/flf_system_code_reactor.py +++ b/paramak/parametric_reactors/flf_system_code_reactor.py @@ -77,19 +77,16 @@ def create_solids(self): inner_wall = self.inner_blanket_radius + self.blanket_thickness + self.blanket_vv_gap lower_vv = paramak.RotateStraightShape( points=[ + (inner_wall, 0), ( inner_wall, - (-self.blanket_height / 2.0) - self.lower_blanket_thickness, - ), - ( - inner_wall, - (-self.blanket_height / 2.0) - (self.lower_blanket_thickness + self.lower_vv_thickness), + self.lower_vv_thickness, ), ( 0, - (-self.blanket_height / 2.0) - (self.lower_blanket_thickness + self.lower_vv_thickness), + self.lower_vv_thickness, ), - (0, (-self.blanket_height / 2.0) - self.lower_blanket_thickness), + (0, 0), ], rotation_angle=self.rotation_angle, color=(0.5, 0.5, 0.5), @@ -98,13 +95,10 @@ def create_solids(self): lower_blanket = paramak.RotateStraightShape( points=[ - (inner_wall, -self.blanket_height / 2.0), - ( - inner_wall, - (-self.blanket_height / 2.0) - self.lower_blanket_thickness, - ), - (0, (-self.blanket_height / 2.0) - self.lower_blanket_thickness), - (0, -self.blanket_height / 2.0), + (inner_wall, self.lower_vv_thickness), + (inner_wall, self.lower_vv_thickness + self.lower_blanket_thickness), + (0, self.lower_vv_thickness + self.lower_blanket_thickness), + (0, self.lower_vv_thickness), ], rotation_angle=self.rotation_angle, color=(0.0, 1.0, 0.498), @@ -113,6 +107,7 @@ def create_solids(self): blanket = paramak.CenterColumnShieldCylinder( height=self.blanket_height, + center_height=self.lower_vv_thickness + self.lower_blanket_thickness + 0.5 * self.blanket_height, inner_radius=self.inner_blanket_radius, outer_radius=self.blanket_thickness + self.inner_blanket_radius, rotation_angle=self.rotation_angle, @@ -121,54 +116,88 @@ def create_solids(self): name="blanket", ) - upper_blanket = paramak.RotateStraightShape( + upper_vv = paramak.RotateStraightShape( points=[ - (inner_wall, (self.blanket_height / 2.0) + self.upper_vv_thickness), + (inner_wall, self.lower_vv_thickness + self.lower_blanket_thickness + self.blanket_height), ( inner_wall, - (self.blanket_height / 2.0) + self.upper_vv_thickness + self.upper_blanket_thickness, + self.lower_vv_thickness + + self.lower_blanket_thickness + + self.blanket_height + + self.upper_vv_thickness, ), ( 0, - (self.blanket_height / 2.0) + self.upper_vv_thickness + self.upper_blanket_thickness, + self.lower_vv_thickness + + self.lower_blanket_thickness + + self.blanket_height + + self.upper_vv_thickness, ), - (0, (self.blanket_height / 2.0) + self.upper_vv_thickness), - ], - rotation_angle=self.rotation_angle, - color=(0.0, 1.0, 0.498), - name="upper_blanket", - ) - - upper_vv = paramak.RotateStraightShape( - points=[ - (inner_wall, self.blanket_height / 2.0), - (inner_wall, (self.blanket_height / 2.0) + self.upper_vv_thickness), - (0, (self.blanket_height / 2.0) + self.upper_vv_thickness), - (0, self.blanket_height / 2.0), + (0, self.lower_vv_thickness + self.lower_blanket_thickness + self.blanket_height), ], rotation_angle=self.rotation_angle, color=(0.5, 0.5, 0.5), name="upper_vacuum_vessel", ) - vac_ves = paramak.RotateStraightShape( + upper_blanket = paramak.RotateStraightShape( points=[ ( - inner_wall + self.vv_thickness, - (self.blanket_height / 2.0) + self.upper_vv_thickness + self.upper_blanket_thickness, + inner_wall, + self.lower_vv_thickness + + self.lower_blanket_thickness + + self.blanket_height + + self.upper_vv_thickness, ), ( inner_wall, - (self.blanket_height / 2.0) + self.upper_vv_thickness + self.upper_blanket_thickness, + self.lower_vv_thickness + + self.lower_blanket_thickness + + self.blanket_height + + self.upper_vv_thickness + + self.upper_blanket_thickness, + ), + ( + 0, + self.lower_vv_thickness + + self.lower_blanket_thickness + + self.blanket_height + + self.upper_vv_thickness + + self.upper_blanket_thickness, + ), + ( + 0, + self.lower_vv_thickness + + self.lower_blanket_thickness + + self.blanket_height + + self.upper_vv_thickness, ), + ], + rotation_angle=self.rotation_angle, + color=(0.0, 1.0, 0.498), + name="upper_blanket", + ) + + vac_ves = paramak.RotateStraightShape( + points=[ + (inner_wall, 0), ( inner_wall, - -(self.blanket_height / 2.0) - self.lower_blanket_thickness - self.lower_vv_thickness, + self.lower_vv_thickness + + self.lower_blanket_thickness + + self.blanket_height + + self.upper_vv_thickness + + self.upper_blanket_thickness, ), ( inner_wall + self.vv_thickness, - -(self.blanket_height / 2.0) - self.lower_blanket_thickness - self.lower_vv_thickness, + self.lower_vv_thickness + + self.lower_blanket_thickness + + self.blanket_height + + self.upper_vv_thickness + + self.upper_blanket_thickness, ), + (inner_wall + self.vv_thickness, 0), ], rotation_angle=self.rotation_angle, color=(0.5, 0.5, 0.5), diff --git a/paramak/reactor.py b/paramak/reactor.py index 50e90876b..e805cfa1a 100644 --- a/paramak/reactor.py +++ b/paramak/reactor.py @@ -1,6 +1,5 @@ -import os -import tempfile from collections.abc import Iterable +from logging import warning from pathlib import Path from typing import List, Optional, Tuple, Union @@ -9,7 +8,15 @@ from cadquery import exporters import paramak -from paramak.utils import _replace, get_hash +from paramak.utils import ( + _replace, + get_hash, + get_bounding_box, + get_largest_dimension, + export_solids_to_brep, + export_solids_to_dagmc_h5m, + get_center_of_bounding_box, +) class Reactor: @@ -20,37 +27,17 @@ class Reactor: Args: shapes_and_components: list of paramak.Shape objects - graveyard_size: The dimension of cube shaped the graveyard region used - by DAGMC. This attribute is used preferentially over - graveyard_offset. - graveyard_offset: The distance between the graveyard and the largest - shape. If graveyard_size is set the this is ignored. - largest_shapes: Identifying the shape(s) with the largest size in each - dimension (x,y,z) can speed up the production of the graveyard. - Defaults to None which finds the largest shapes by looping through - all the shapes and creating bounding boxes. This can be slow and - that is why the user is able to provide a subsection of shapes to - use when calculating the graveyard dimensions. """ def __init__( self, shapes_and_components: List[paramak.Shape] = [], - graveyard_size: float = 20_000.0, - graveyard_offset: Optional[float] = None, - largest_shapes: Optional[List[paramak.Shape]] = None, ): self.shapes_and_components = shapes_and_components - self.graveyard_offset = graveyard_offset - self.graveyard_size = graveyard_size - self.largest_shapes = largest_shapes self.input_variable_names: List[str] = [ # 'shapes_and_components', commented out to avoid calculating solids - "graveyard_size", - "graveyard_offset", - "largest_shapes", ] self.stp_filenames: List[str] = [] @@ -99,16 +86,9 @@ def graveyard_offset(self, value): def largest_dimension(self): """Calculates a bounding box for the Reactor and returns the largest absolute value of the largest dimension of the bounding box""" - largest_dimension = 0 - if self.largest_shapes is None: - shapes_to_bound = self.shapes_and_components - else: - shapes_to_bound = self.largest_shapes + largest_dimension = get_largest_dimension(self.solid) - for component in shapes_to_bound: - largest_dimension = max(largest_dimension, component.largest_dimension) - # self._largest_dimension = largest_dimension return largest_dimension @largest_dimension.setter @@ -116,14 +96,16 @@ def largest_dimension(self, value): self._largest_dimension = value @property - def largest_shapes(self): - return self._largest_shapes + def bounding_box(self): + """Calculates a bounding box for the Shape and returns the coordinates of + the corners lower-left and upper-right. This function is useful when + creating OpenMC mesh tallies as the bounding box is required in this form""" + + return get_bounding_box(self.solid) - @largest_shapes.setter - def largest_shapes(self, value): - if not isinstance(value, (list, tuple, type(None))): - raise ValueError("paramak.Reactor.largest_shapes should be a " "list of paramak.Shapes") - self._largest_shapes = value + @bounding_box.setter + def bounding_box(self, value): + self._bounding_box = value @property def shapes_and_components(self): @@ -235,10 +217,12 @@ def export_dagmc_h5m( min_mesh_size: float = 5, max_mesh_size: float = 20, exclude: List[str] = None, - verbose=False, - volume_atol=0.000001, - center_atol=0.000001, - bounding_box_atol=0.000001, + verbose: bool = False, + volume_atol: float = 0.000001, + center_atol: float = 0.000001, + bounding_box_atol: float = 0.000001, + tags: Optional[List[str]] = None, + include_graveyard: Optional[dict] = None, ) -> str: """Export a DAGMC compatible h5m file for use in neutronics simulations. This method makes use of Gmsh to create a surface mesh of the geometry. @@ -265,82 +249,55 @@ def export_dagmc_h5m( bounding_box_atol: the absolute volume tolerance to allow when matching parts in the intermediate brep file with the cadquery parts + tags: the dagmc tag to use in when naming the shape in the h5m file. + If left as None then the Shape.name will be used. This allows + the DAGMC geometry created to be compatible with a wider range + of neutronics codes that have specific DAGMC tag requirements. + include_graveyard: specify if the graveyard box will be included or + not and how it will be sized. Leave as None if a graveyard is + not included. If a graveyard is required then set + include_graveyard to a dictionary with a key and value. + Acceptable keys are 'offset' and 'size'. Each key must have a + float value associated. For example {'size': 1000} or + {'offset': 10}. The size simple sets the height, width, depth + of the graveyard while the offset adds to the geometry to get + the graveyard box size. """ - # a local import is used here as these packages need CQ master to work - from brep_to_h5m import brep_to_h5m - import brep_part_finder as bpf + shapes_to_convert = [] - tmp_brep_filename = tempfile.mkstemp(suffix=".brep", prefix="paramak_")[1] - - # saves the reactor as a Brep file with merged surfaces - self.export_brep(tmp_brep_filename) + for shape in self.shapes_and_components: + # allows components like the plasma to be removed + if exclude: + if shape.name not in exclude: + shapes_to_convert.append(shape) + else: + shapes_to_convert.append(shape) - # brep file is imported - brep_file_part_properties = bpf.get_brep_part_properties(tmp_brep_filename) + if include_graveyard: + graveyard = self.make_graveyard(**include_graveyard) + shapes_to_convert.append(graveyard) - if verbose: - print("brep_file_part_properties", brep_file_part_properties) + if tags is None: + tags = [] + for shape in shapes_to_convert: + tags.append(shape.name) - shape_properties = {} - for shape_or_compound in self.shapes_and_components: - sub_solid_descriptions = [] + print(tags) - # checks if the solid is a cq.Compound or not - if isinstance(shape_or_compound.solid, cq.occ_impl.shapes.Compound): - iterable_solids = shape_or_compound.solid.Solids() - else: - iterable_solids = shape_or_compound.solid.val().Solids() - - for sub_solid in iterable_solids: - part_bb = sub_solid.BoundingBox() - part_center = sub_solid.Center() - sub_solid_description = { - "volume": sub_solid.Volume(), - "center": (part_center.x, part_center.y, part_center.z), - "bounding_box": ( - (part_bb.xmin, part_bb.ymin, part_bb.zmin), - (part_bb.xmax, part_bb.ymax, part_bb.zmax), - ), - } - sub_solid_descriptions.append(sub_solid_description) - shape_properties[shape_or_compound.name] = sub_solid_descriptions - - if verbose: - print("shape_properties", shape_properties) - - # request to find part ids that are mixed up in the Brep file - # using the volume, center, bounding box that we know about when creating the - # CAD geometry in the first place - key_and_part_id = bpf.get_dict_of_part_ids( - brep_part_properties=brep_file_part_properties, - shape_properties=shape_properties, + output_filename = export_solids_to_dagmc_h5m( + solids=[shape.solid for shape in shapes_to_convert], + filename=filename, + min_mesh_size=min_mesh_size, + max_mesh_size=max_mesh_size, + verbose=verbose, volume_atol=volume_atol, center_atol=center_atol, bounding_box_atol=bounding_box_atol, + tags=tags, ) - if verbose: - print(f"key_and_part_id={key_and_part_id}") - - # allows components like the plasma to be removed - if isinstance(exclude, Iterable): - for name_to_remove in exclude: - key_and_part_id = {key: val for key, val in key_and_part_id.items() if val != name_to_remove} - - brep_to_h5m( - brep_filename=tmp_brep_filename, - volumes_with_tags=key_and_part_id, - h5m_filename=filename, - min_mesh_size=min_mesh_size, - max_mesh_size=max_mesh_size, - delete_intermediate_stl_files=True, - ) - - # temporary brep is deleted - os.remove(tmp_brep_filename) - - return filename + return output_filename def export_stp( self, @@ -417,50 +374,41 @@ def export_stp( return filename - def export_brep(self, filename: str, merge: bool = True): - """Exports a brep file for the Reactor.solid. + def export_brep( + self, + filename: str = "reactor.brep", + include_graveyard: Optional[dict] = None, + ) -> str: + """Exports a brep file for the Reactor. Optionally including a DAGMC + graveyard. Args: filename: the filename of exported the brep file. - merged: if the surfaces should be merged (True) or not (False). + include_graveyard: specify if the graveyard box will be included or + not and how it will be sized. Leave as None if a graveyard is + not included. If a graveyard is required then set + include_graveyard to a dictionary with a key and value. + Acceptable keys are 'offset' and 'size'. Each key must have a + float value associated. For example {'size': 1000} or + {'offset': 10}. The size simple sets the height, width, depth + of the graveyard while the offset adds to the geometry to get + the graveyard box size. Returns: filename of the brep created """ - path_filename = Path(filename) - - if path_filename.suffix != ".brep": - msg = "When exporting a brep file the filename must end with .brep" - raise ValueError(msg) - - path_filename.parents[0].mkdir(parents=True, exist_ok=True) - - if not merge: - self.solid.exportBrep(str(path_filename)) - else: - import OCP - - bldr = OCP.BOPAlgo.BOPAlgo_Splitter() + geometry_to_save = [shape.solid for shape in self.shapes_and_components] + if include_graveyard: + graveyard = self.make_graveyard(**include_graveyard) + geometry_to_save.append(graveyard.solid) - for shape in self.shapes_and_components: - # checks if solid is a compound as .val() is not needed for compunds - if isinstance(shape.solid, cq.occ_impl.shapes.Compound): - bldr.AddArgument(shape.solid.wrapped) - else: - bldr.AddArgument(shape.solid.val().wrapped) - - bldr.SetNonDestructive(True) - - bldr.Perform() - - bldr.Images() - - merged = cq.Compound(bldr.Shape()) - - merged.exportBrep(str(path_filename)) + output_filename = export_solids_to_brep( + solids=geometry_to_save, + filename=filename, + ) - return str(path_filename) + return output_filename def export_stl( self, @@ -573,6 +521,8 @@ def make_sector_wedge( print("No sector wedge made as rotation angle is 360") return None + # todo this should be cetered around the center point + if height is None: height = self.largest_dimension * 2 @@ -666,83 +616,48 @@ def export_svg( return str(path_filename) - def export_stp_graveyard( - self, - filename: Optional[str] = "graveyard.stp", - graveyard_size: Optional[float] = None, - graveyard_offset: Optional[float] = None, - ) -> str: - """Writes a stp file (CAD geometry) for the reactor graveyard. This - is needed for DAGMC simulations. This method also calls - Reactor.make_graveyard() with the graveyard_size and graveyard_size - values. - - Args: - filename (str): the filename for saving the stp file. Appends - .stp to the filename if it is missing. - graveyard_size: directly sets the size of the graveyard. Defaults - to None which then uses the Reactor.graveyard_size attribute. - graveyard_offset: the offset between the largest edge of the - geometry and inner bounding shell created. Defaults to None - which then uses Reactor.graveyard_offset attribute. - - Returns: - str: the stp filename created - """ - - graveyard = self.make_graveyard( - graveyard_offset=graveyard_offset, - graveyard_size=graveyard_size, - ) - - path_filename = Path(filename) - - if path_filename.suffix != ".stp": - path_filename = path_filename.with_suffix(".stp") - - graveyard.export_stp(filename=str(path_filename)) - - return str(path_filename) - def make_graveyard( self, - graveyard_size: Optional[float] = None, - graveyard_offset: Optional[float] = None, + size: Optional[float] = None, + offset: Optional[float] = None, ) -> paramak.Shape: """Creates a graveyard volume (bounding box) that encapsulates all volumes. This is required by DAGMC when performing neutronics simulations. The graveyard size can be ascertained in two ways. Either - the size can be set directly using the graveyard_size which is the + the size can be set directly using the size which is the quickest method. Alternativley the graveyard can be automatically sized - to the geometry by setting a graveyard_offset value. If both options - are set then the method will default to using the graveyard_size + to the geometry by setting a offset value. If both options + are set then the method will default to using the size preferentially. Args: - graveyard_size: directly sets the size of the graveyard. Defaults - to None which then uses the Reactor.graveyard_size attribute. - graveyard_offset: the offset between the largest edge of the - geometry and inner bounding shell created. Defaults to None - which then uses Reactor.graveyard_offset attribute. + size: directly sets the size of the graveyard. + offset: the offset between the largest edge of the geometry and + inner surface of the graveyard Returns: CadQuery solid: a shell volume that bounds the geometry, referred to as a graveyard in DAGMC """ - if graveyard_size is not None: - graveyard_size_to_use = graveyard_size + solid = self.solid - elif self.graveyard_size is not None: - graveyard_size_to_use = self.graveyard_size + # makes the graveyard around the center of the geometry + center = get_center_of_bounding_box(solid) - elif graveyard_offset is not None: - self.solid - graveyard_size_to_use = self.largest_dimension * 2 + graveyard_offset * 2 + if size is not None: + graveyard_size_to_use = size + if size <= 0: + raise ValueError("Graveyard size should be larger than 0") + largest_dim = get_largest_dimension(solid) + if size < largest_dim: + msg = f"Graveyard size should be larger than the largest shape in the Reactor. Which is {largest_dim}" + raise ValueError(msg) - elif self.graveyard_offset is not None: - self.solid - graveyard_size_to_use = self.largest_dimension * 2 + self.graveyard_offset * 2 + elif offset is not None: + graveyard_size_to_use = get_largest_dimension(solid) * 2 + offset * 2 + if offset <= 0: + raise ValueError("Graveyard size should be larger than 0") else: raise ValueError( @@ -751,10 +666,7 @@ def make_graveyard( Please specify at least one of these attributes or arguments" ) - graveyard_shape = paramak.HollowCube( - length=graveyard_size_to_use, - name="graveyard", - ) + graveyard_shape = paramak.HollowCube(length=graveyard_size_to_use, name="graveyard", center_coordinate=center) self.graveyard = graveyard_shape diff --git a/paramak/shape.py b/paramak/shape.py index ec9647b82..0cbc57f9a 100644 --- a/paramak/shape.py +++ b/paramak/shape.py @@ -1,6 +1,4 @@ import numbers -import os -import tempfile from collections.abc import Iterable from pathlib import Path from typing import List, Optional, Tuple, Union @@ -18,8 +16,11 @@ facet_wire, get_hash, intersect_solid, - plotly_trace, union_solid, + get_largest_dimension, + get_bounding_box, + export_solids_to_brep, + export_solids_to_dagmc_h5m, ) @@ -214,39 +215,28 @@ def union(self, value): self._union = value @property - def largest_dimension(self): - """Calculates a bounding box for the Shape and returns the largest + def largest_dimension(self) -> float: + """Calculates a bounding box for the Reactor and returns the largest absolute value of the largest dimension of the bounding box""" - largest_dimension = 0 - if isinstance(self.solid, (Compound, shapes.Solid)): - for solid in self.solid.Solids(): - bound_box = solid.BoundingBox() - largest_dimension = max( - abs(bound_box.xmax), - abs(bound_box.xmin), - abs(bound_box.ymax), - abs(bound_box.ymin), - abs(bound_box.zmax), - abs(bound_box.zmin), - largest_dimension, - ) - else: - bound_box = self.solid.val().BoundingBox() - largest_dimension = max( - abs(bound_box.xmax), - abs(bound_box.xmin), - abs(bound_box.ymax), - abs(bound_box.ymin), - abs(bound_box.zmax), - abs(bound_box.zmin), - ) - self.largest_dimension = largest_dimension - return largest_dimension + + return get_largest_dimension(self.solid) @largest_dimension.setter def largest_dimension(self, value): self._largest_dimension = value + @property + def bounding_box(self): + """Calculates a bounding box for the Shape and returns the coordinates of + the corners lower-left and upper-right. This function is useful when + creating OpenMC mesh tallies as the bounding box is required in this form""" + + return get_bounding_box(self.solid) + + @bounding_box.setter + def bounding_box(self, value): + self._bounding_box = value + @property def workplane(self): return self._workplane @@ -280,19 +270,22 @@ def rotation_axis(self, value): if len(value) != 2: raise ValueError(msg) for point in value: - if not isinstance(point, tuple): + if not isinstance(point, Iterable): + msg = f"Shape.rotation_axis must be an iterable of iterables, not {type(point)}" raise ValueError(msg) if len(point) != 3: + msg = f"Shape.rotation_axis must be an iterable of iterables with 3 entries, not {len(point)}" raise ValueError(msg) for val in point: if not isinstance(val, (int, float)): + msg = f"Shape.rotation_axis should be an iterable of iterables where the nested iterables are numerical, not {type(val)}" raise ValueError(msg) if value[0] == value[1]: - msg = "The two points must be different" + msg = "The two coordinates points for rotation_axis must be different" raise ValueError(msg) elif value is not None: - msg = "Shape.rotation_axis must be a list or a string or None" + msg = "Shape.rotation_axis must be an iterable or a string or None" raise ValueError(msg) self._rotation_axis = value @@ -785,40 +778,53 @@ def export_stl( return str(path_filename) - def export_brep(self, filename): - """Exports a brep file for the Shape.solid. + def export_brep(self, filename="shape.brep", include_graveyard=False) -> str: + """Exports a brep file for the Shape. Optionally including a DAGMC + graveyard. Args: filename: the filename of exported the brep file. - """ + include_graveyard: specify if the graveyard will be included or + not. If True the the Shape.make_graveyard will be called + using Shape.graveyard_size and Shape.graveyard_offset + attribute values. - path_filename = Path(filename) + Returns: + filename of the brep created + """ - if path_filename.suffix != ".brep": - msg = "When exporting a brep file the filename must end with .brep" - raise ValueError(msg) + geometry_to_save = [self.solid] - path_filename.parents[0].mkdir(parents=True, exist_ok=True) + if include_graveyard: + self.make_graveyard() + geometry_to_save.append(self.graveyard.solid) - self.solid.val().exportBrep(str(path_filename)) - # alternative method is to use BRepTools that might support imprinting - # and merging https://github.com/CadQuery/cadquery/issues/449 - # from OCP.BRepTools import BRepTools - # BRepTools.Write_s(self.solid.toOCC(), str(path_filename)) + output_filename = export_solids_to_brep( + solids=geometry_to_save, + filename=filename, + ) - return str(path_filename) + return output_filename def export_dagmc_h5m( self, filename: str = "dagmc.h5m", - min_mesh_size: float = 10, + min_mesh_size: float = 5, max_mesh_size: float = 20, + verbose: bool = False, + volume_atol: float = 0.000001, + center_atol: float = 0.000001, + bounding_box_atol: float = 0.000001, + tags: Optional[List[str]] = None, + include_graveyard: bool = False, ) -> str: """Export a DAGMC compatible h5m file for use in neutronics simulations. This method makes use of Gmsh to create a surface mesh of the geometry. MOAB is used to convert the meshed geometry into a h5m with parts tagged by using the reactor.shape_and_components.name properties. You will need - Gmsh installed and MOAB installed to use this function. + Gmsh installed and MOAB installed to use this function. Acceptable + tolerances may need increasing to match reactor parts with the parts + in the intermediate Brep file produced during the process Args: filename: the filename of the DAGMC h5m file to write @@ -826,31 +832,48 @@ def export_dagmc_h5m( into gmsh.option.setNumber("Mesh.MeshSizeMin", min_mesh_size) max_mesh_size: the maximum mesh element size to use in Gmsh. Passed into gmsh.option.setNumber("Mesh.MeshSizeMax", max_mesh_size) + volume_atol: the absolute volume tolerance to allow when matching + parts in the intermediate brep file with the cadquery parts + center_atol: the absolute center coordinates tolerance to allow + when matching parts in the intermediate brep file with the + cadquery parts + bounding_box_atol: the absolute volume tolerance to allow when + matching parts in the intermediate brep file with the cadquery + parts + tags: the dagmc tag to use in when naming the shape in the h5m file. + If left as None then the Shape.name will be used. This allows + the DAGMC geometry created to be compatible with a wider range + of neutronics codes that have specific DAGMC tag requirements. + include_graveyard: specify if the graveyard will be included or + not. If True the the Reactor.make_graveyard will be called + using Reactor.graveyard_size and Reactor.graveyard_offset + attribute values. """ - from brep_to_h5m import brep_to_h5m - - tmp_brep_filename = tempfile.mkstemp(suffix=".brep", prefix="paramak_")[1] + shapes_to_convert = [self.solid] - # saves the reactor as a Brep file with merged surfaces - self.export_brep(tmp_brep_filename) + if include_graveyard: + self.make_graveyard() + shapes_to_convert.append(self.graveyard.solid) - volumes_with_tags = {} - for counter, _ in enumerate(self.solid.val().Solids(), 1): - volumes_with_tags[counter] = f"mat_{self.name}" + if tags is None: + tags = [self.name] + if include_graveyard: + tags.append(self.graveyard.name) - brep_to_h5m( - brep_filename=tmp_brep_filename, - volumes_with_tags=volumes_with_tags, - h5m_filename=filename, + output_filename = export_solids_to_dagmc_h5m( + solids=shapes_to_convert, + filename=filename, min_mesh_size=min_mesh_size, max_mesh_size=max_mesh_size, + verbose=verbose, + volume_atol=volume_atol, + center_atol=center_atol, + bounding_box_atol=bounding_box_atol, + tags=tags, ) - # temporary brep is deleted - os.remove(tmp_brep_filename) - - return filename + return output_filename def export_stp( self, @@ -1061,16 +1084,9 @@ def export_html( facet_splines=facet_splines, facet_circles=facet_circles, tolerance=tolerance, - title=(f"coordinates of {self.__class__.__name__} shape, viewed " "from the {view_plane} plane"), + title=f"coordinates of {self.__class__.__name__} shape, viewed from the {view_plane} plane", ) - if self.points is not None: - fig.add_trace(plotly_trace(points=self.points, mode="markers", name="Shape.points")) - - # sweep shapes have .path_points but not .points attribute - if self.path_points: - fig.add_trace(plotly_trace(points=self.path_points, mode="markers", name="Shape.path_points")) - if filename is not None: Path(filename).parents[0].mkdir(parents=True, exist_ok=True) diff --git a/paramak/utils.py b/paramak/utils.py index a79d52de8..8bacf582f 100644 --- a/paramak/utils.py +++ b/paramak/utils.py @@ -7,13 +7,230 @@ from tempfile import mkstemp from typing import List, Optional, Tuple, Union +import tempfile import cadquery as cq import numpy as np import plotly.graph_objects as go from cadquery import importers from OCP.GCPnts import GCPnts_QuasiUniformDeflection +from cadquery.occ_impl import shapes +import OCP -import paramak + +def export_solids_to_brep( + solids: Iterable, + filename: str = "reactor.brep", +): + """Exports a brep file for the Reactor.solid. + + Args: + solids: a list of cadquery solids + filename: the filename of exported the brep file. + + Returns: + filename of the brep created + """ + + path_filename = Path(filename) + + if path_filename.suffix != ".brep": + msg = "When exporting a brep file the filename must end with .brep" + raise ValueError(msg) + + path_filename.parents[0].mkdir(parents=True, exist_ok=True) + + # TODO bring non merge capability back + # if not merge: + # geometry_to_save = cq.Compound.makeCompound([self.solid, self.graveyard.solid.val()]) + # geometry_to_save.exportBrep(str(path_filename)) + + bldr = OCP.BOPAlgo.BOPAlgo_Splitter() + + if len(solids) == 1: + solids[0].val().exportBrep(str(path_filename)) + return str(path_filename) + + for solid in solids: + # checks if solid is a compound as .val() is not needed for compounds + if isinstance(solid, cq.occ_impl.shapes.Compound): + bldr.AddArgument(solid.wrapped) + else: + bldr.AddArgument(solid.val().wrapped) + + bldr.SetNonDestructive(True) + + bldr.Perform() + + bldr.Images() + + merged_solid = cq.Compound(bldr.Shape()) + + merged_solid.exportBrep(str(path_filename)) + + return str(path_filename) + + +def export_solids_to_dagmc_h5m( + solids: List, + filename: str = "dagmc.h5m", + min_mesh_size: float = 5, + max_mesh_size: float = 20, + verbose: bool = False, + volume_atol: float = 0.000001, + center_atol: float = 0.000001, + bounding_box_atol: float = 0.000001, + tags: List[str] = None, +): + if len(tags) != len(solids): + msg = ( + "When specifying tags then there must be one tag for " + f"every shape. Currently there are {len(tags)} tags " + f"provided and {len(solids)} shapes" + ) + raise ValueError(msg) + + # a local import is used here as these packages need Moab to work + from brep_to_h5m import brep_to_h5m + import brep_part_finder as bpf + + tmp_brep_filename = tempfile.mkstemp(suffix=".brep", prefix="paramak_")[1] + + # saves the reactor as a Brep file with merged surfaces + export_solids_to_brep(solids=solids, filename=tmp_brep_filename) + + # brep file is imported + brep_file_part_properties = bpf.get_brep_part_properties(tmp_brep_filename) + + if verbose: + print("brep_file_part_properties", brep_file_part_properties) + + shape_properties = {} + for counter, solid in enumerate(solids): + sub_solid_descriptions = [] + + # checks if the solid is a cq.Compound or not + if isinstance(solid, cq.occ_impl.shapes.Compound): + iterable_solids = solid.Solids() + else: + iterable_solids = solid.val().Solids() + + for sub_solid in iterable_solids: + part_bb = sub_solid.BoundingBox() + part_center = sub_solid.Center() + sub_solid_description = { + "volume": sub_solid.Volume(), + "center": (part_center.x, part_center.y, part_center.z), + "bounding_box": ( + (part_bb.xmin, part_bb.ymin, part_bb.zmin), + (part_bb.xmax, part_bb.ymax, part_bb.zmax), + ), + } + sub_solid_descriptions.append(sub_solid_description) + + shape_properties[tags[counter]] = sub_solid_descriptions + + if verbose: + print("shape_properties", shape_properties) + + # request to find part ids that are mixed up in the Brep file + # using the volume, center, bounding box that we know about when creating the + # CAD geometry in the first place + + key_and_part_id = bpf.get_dict_of_part_ids( + brep_part_properties=brep_file_part_properties, + shape_properties=shape_properties, + volume_atol=volume_atol, + center_atol=center_atol, + bounding_box_atol=bounding_box_atol, + ) + + if verbose: + print(f"key_and_part_id={key_and_part_id}") + + brep_to_h5m( + brep_filename=tmp_brep_filename, + volumes_with_tags=key_and_part_id, + h5m_filename=filename, + min_mesh_size=min_mesh_size, + max_mesh_size=max_mesh_size, + delete_intermediate_stl_files=True, + ) + + # temporary brep is deleted using os.remove + remove(tmp_brep_filename) + + return filename + + +def get_bounding_box(solid) -> Tuple[Tuple[float, float, float], Tuple[float, float, float]]: + """Calculates a bounding box for the Shape and returns the coordinates of + the corners lower-left and upper-right. This function is useful when + creating OpenMC mesh tallies as the bounding box is required in this form""" + + if isinstance(solid, (cq.Compound, shapes.Solid)): + + bound_box = solid.BoundingBox() + # previous method lopped though solids but this is not needed + # for single_solid in solid.Solids(): + # bound_box = single_solid.BoundingBox() + + else: + bound_box = solid.val().BoundingBox() + + lower_left = (bound_box.xmin, bound_box.ymin, bound_box.zmin) + + upper_right = (bound_box.xmax, bound_box.ymax, bound_box.zmax) + + return (lower_left, upper_right) + + +def get_center_of_bounding_box(solid): + """Calculates the geometric center of the solids bounding box""" + + bounding_box = get_bounding_box(solid) + + center = ( + (bounding_box[0][0] + bounding_box[1][0]) / 2, + (bounding_box[0][1] + bounding_box[1][1]) / 2, + (bounding_box[0][2] + bounding_box[1][2]) / 2, + ) + + return center + + +def get_largest_dimension(solid): + """Calculates the extent of the geometry in the x,y and z axis and returns + the largest of the three.""" + + bounding_box = get_bounding_box(solid) + + largest_dimension = max( + abs(bounding_box[0][0] - bounding_box[0][1]), + abs(bounding_box[0][2] - bounding_box[1][0]), + abs(bounding_box[1][1] - bounding_box[1][2]), + ) + + return largest_dimension + + +def get_largest_distance_from_origin(solid): + """Calculates the distance from (0, 0, 0) to the furthest part of + the geometry. This distance is returned as an positive value.""" + + bounding_box = get_bounding_box(solid) + + largest_dimension = max( + ( + abs(bounding_box[0][0]), + abs(bounding_box[0][1]), + abs(bounding_box[0][2]), + abs(bounding_box[1][0]), + abs(bounding_box[1][1]), + abs(bounding_box[1][2]), + ) + ) + + return largest_dimension def transform_curve(edge, tolerance: Optional[float] = 1e-3): @@ -146,6 +363,16 @@ def diff_between_angles(angle_a: float, angle_b: float) -> float: return delta_mod +def angle_between_two_points_on_circle( + point_1: Tuple[float, float], point_2: Tuple[float, float], radius_of_circle: float +): + + separation = distance_between_two_points(point_1, point_2) + isos_tri_term = (2 * math.pow(radius_of_circle, 2) - math.pow(separation, 2)) / (2 * math.pow(radius_of_circle, 2)) + angle = math.acos(isos_tri_term) + return angle + + def distance_between_two_points(point_a: Tuple[float, float], point_b: Tuple[float, float]) -> float: """Computes the distance between two points. @@ -157,8 +384,8 @@ def distance_between_two_points(point_a: Tuple[float, float], point_b: Tuple[flo float: distance between A and B """ - xa, ya = point_a - xb, yb = point_b + xa, ya = point_a[0], point_a[1] + xb, yb = point_b[0], point_b[1] u_vec = [xb - xa, yb - ya] return np.linalg.norm(u_vec) @@ -174,8 +401,8 @@ def extend(point_a: Tuple[float, float], point_b: Tuple[float, float], L: float) float, float: point C coordinates """ - xa, ya = point_a - xb, yb = point_b + xa, ya = point_a[0], point_a[1] + xb, yb = point_b[0], point_b[1] u_vec = [xb - xa, yb - ya] u_vec /= np.linalg.norm(u_vec) @@ -270,7 +497,7 @@ def rotate(origin: Tuple[float, float], point: Tuple[float, float], angle: float """ ox, oy = origin - px, py = point + px, py = point[0], point[1] qx = ox + math.cos(angle) * (px - ox) - math.sin(angle) * (py - oy) qy = oy + math.sin(angle) * (px - ox) + math.cos(angle) * (py - oy) @@ -304,7 +531,9 @@ def calculate_wedge_cut(self): if self.rotation_angle == 360: return None - cutting_wedge = paramak.CuttingWedgeFS(self) + from paramak import CuttingWedgeFS + + cutting_wedge = CuttingWedgeFS(self) return cutting_wedge @@ -448,9 +677,9 @@ def plotly_trace( text_values = [] for i, point in enumerate(points): - text = "point number= {i}
x={point[0]}
y= {point[1]}" + text = f"point number= {i}
x={point[0]}
y= {point[1]}" if len(point) == 3: - text = text + "
z= {point[2]}
" + text = text + f"
z= {point[2]}
" text_values.append(text) @@ -622,7 +851,7 @@ def export_wire_to_html( tolerance=tolerance, ) - points = paramak.utils.extract_points_from_edges(edges=edges, view_plane=view_plane) + points = extract_points_from_edges(edges=edges, view_plane=view_plane) fig.add_trace(plotly_trace(points=points, mode=mode, name="edge " + str(counter))) @@ -638,7 +867,7 @@ def export_wire_to_html( # this is for cadquery generated solids edges = wire.val().Edges() - points = paramak.utils.extract_points_from_edges(edges=edges, view_plane=view_plane) + points = extract_points_from_edges(edges=edges, view_plane=view_plane) fig.add_trace(plotly_trace(points=points, mode="markers", name="points on wire " + str(counter))) @@ -684,9 +913,9 @@ def convert_circle_to_spline( solid = solid.moveTo(p_0[0], p_0[1]).threePointArc(p_1, p_2) edge = solid.vals()[0] - new_edge = paramak.utils.transform_curve(edge, tolerance=tolerance) + new_edge = transform_curve(edge, tolerance=tolerance) - points = paramak.utils.extract_points_from_edges(edges=new_edge, view_plane="XZ") + points = extract_points_from_edges(edges=new_edge, view_plane="XZ") return points diff --git a/setup.cfg b/setup.cfg index ee8296996..2ef248554 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,7 +26,6 @@ project_urls = packages = find: python_requires= >=3.6 install_requires= - pyparsing ~= 2.4.7 plotly >= 5.1.0 scipy >= 1.7.0 sympy >= 1.8 @@ -36,7 +35,7 @@ install_requires= jupyter-client < 7 jupyter-cadquery >= 3.0.0 brep_part_finder >= 0.4.1 - brep_to_h5m >= 0.3.0 + brep_to_h5m >= 0.3.1 setuptools_scm [options.extras_require] diff --git a/tests/test_parametric_components/test_constant_thickness_dome.py b/tests/test_parametric_components/test_constant_thickness_dome.py new file mode 100644 index 000000000..d2ea6c544 --- /dev/null +++ b/tests/test_parametric_components/test_constant_thickness_dome.py @@ -0,0 +1,50 @@ +import unittest +import pytest + +import paramak + + +class TestConstantThicknessDome(unittest.TestCase): + """tests for the ConstantThicknessDome class""" + + def test_volume_increases_with_rotation_angle(self): + """Tests that the volume doubles when rotation angle doubles""" + + test_shape_1 = paramak.ConstantThicknessDome(rotation_angle=180) + test_shape_2 = paramak.ConstantThicknessDome(rotation_angle=360) + + assert test_shape_1.volume() * 2 == pytest.approx(test_shape_2.volume()) + + def test_upper_lower_flips_points(self): + """Checks that the coords of the flips version are the same for p1 and p2 + and negative for part of p3""" + test_shape_1 = paramak.ConstantThicknessDome(upper_or_lower="upper") + test_shape_2 = paramak.ConstantThicknessDome(upper_or_lower="lower") + assert test_shape_1.points[0] == test_shape_2.points[0] + assert test_shape_1.points[1] == test_shape_2.points[1] + assert test_shape_1.points[2][0] == test_shape_2.points[2][0] + assert test_shape_1.points[2][1] == -test_shape_2.points[2][1] + + assert test_shape_1.volume() == pytest.approx(test_shape_2.volume()) + + def test_invalid_parameters_errors(self): + """Checks that the correct errors are raised when invalid arguments are + input as shape parameters.""" + + def incorrect_shape_height_width_ratio(): + my_shape = paramak.ConstantThicknessDome(chord_width=10, chord_height=40) + my_shape.solid + + def incorrect_thickness(): + paramak.ConstantThicknessDome(thickness=-1) + + def incorrect_chord_height(): + paramak.ConstantThicknessDome(chord_height=-1) + + def incorrect_chord_width(): + paramak.ConstantThicknessDome(chord_width=-1) + + self.assertRaises(ValueError, incorrect_shape_height_width_ratio) + self.assertRaises(ValueError, incorrect_thickness) + self.assertRaises(ValueError, incorrect_chord_height) + self.assertRaises(ValueError, incorrect_chord_width) diff --git a/tests/test_parametric_components/test_dished_vacuum_vessel.py b/tests/test_parametric_components/test_dished_vacuum_vessel.py new file mode 100644 index 000000000..4ff3bdd10 --- /dev/null +++ b/tests/test_parametric_components/test_dished_vacuum_vessel.py @@ -0,0 +1,9 @@ +import pytest + +import paramak + + +def test_volume_increases_with_rotation_angle(): + test_shape_1 = paramak.DishedVacuumVessel(rotation_angle=180) + test_shape_2 = paramak.DishedVacuumVessel(rotation_angle=360) + assert test_shape_1.volume() * 2 == pytest.approx(test_shape_2.volume()) diff --git a/tests/test_parametric_components/test_extrude_hollow_cube.py b/tests/test_parametric_components/test_extrude_hollow_cube.py new file mode 100644 index 000000000..fb21dc75a --- /dev/null +++ b/tests/test_parametric_components/test_extrude_hollow_cube.py @@ -0,0 +1,36 @@ +import unittest + +import paramak + + +class TestHollowCube(unittest.TestCase): + """tests the hoolw cube shape that is used as a graveyard""" + + def setUp(self): + self.test_shape = paramak.HollowCube(length=10, thickness=2) + + def test_default_parameters(self): + """Checks that the default parameters of a HollowCube are + correct.""" + + assert self.test_shape.center_coordinate == (0.0, 0.0, 0.0) + + def test_center_point_changes_bounding_box(self): + """Checks that moving the center results in the bounding box move as well""" + + default_shape_bb = ((-(10 + 2) / 2, -(10 + 2) / 2, -(10 + 2) / 2), ((10 + 2) / 2, (10 + 2) / 2, (10 + 2) / 2)) + assert self.test_shape.bounding_box == default_shape_bb + + self.test_shape.center_coordinate = (1, 1, 1) + + assert self.test_shape.bounding_box == ( + (default_shape_bb[0][0] + 1, default_shape_bb[0][1] + 1, default_shape_bb[0][2] + 1), + (default_shape_bb[1][0] + 1, default_shape_bb[1][1] + 1, default_shape_bb[1][2] + 1), + ) + + self.test_shape.center_coordinate = (-2, 3, 14) + + assert self.test_shape.bounding_box == ( + (default_shape_bb[0][0] - 2, default_shape_bb[0][1] + 3, default_shape_bb[0][2] + 14), + (default_shape_bb[1][0] - 2, default_shape_bb[1][1] + 3, default_shape_bb[1][2] + 14), + ) diff --git a/tests/test_parametric_components/test_extrude_hollow_rectangle.py b/tests/test_parametric_components/test_extrude_hollow_rectangle.py index 92274cf0b..28f645aae 100644 --- a/tests/test_parametric_components/test_extrude_hollow_rectangle.py +++ b/tests/test_parametric_components/test_extrude_hollow_rectangle.py @@ -37,7 +37,6 @@ def test_points_calculation(self): """Checks that the points used to construct the ExtrudeHollowRectangle are calculated correctly from the parameters given.""" - print(self.test_shape.points) assert self.test_shape.points == [ (7.5, 5.0), (7.5, -5.0), @@ -72,3 +71,15 @@ def test_absolute_areas(self): assert len(set([round(i) for i in self.test_shape.areas])) == 5 assert self.test_shape.areas.count(pytest.approx(15 * 2)) == 2 assert self.test_shape.areas.count(pytest.approx(10 * 2)) == 2 + + def test_center_point_changes_bounding_box(self): + + default_shape_bb = ((-(15 + 2) / 2, -1.0, -(10 + 2) / 2), ((15 + 2) / 2, 1.0, (10 + 2) / 2)) + assert self.test_shape.bounding_box == default_shape_bb + + self.test_shape.center_point = (1, 1) + + assert self.test_shape.bounding_box == ( + (default_shape_bb[0][0] + 1, default_shape_bb[0][1], default_shape_bb[0][2] + 1), + (default_shape_bb[1][0] + 1, default_shape_bb[1][1], default_shape_bb[1][2] + 1), + ) diff --git a/tests/test_parametric_reactors/test_ball_reactor.py b/tests/test_parametric_reactors/test_ball_reactor.py index f5e3e9ded..54a09f5c3 100644 --- a/tests/test_parametric_reactors/test_ball_reactor.py +++ b/tests/test_parametric_reactors/test_ball_reactor.py @@ -32,8 +32,8 @@ def setUp(self): def test_input_variable_names(self): """tests that the number of inputs variables is correct""" - assert len(self.test_reactor.input_variables.keys()) == 28 - assert len(self.test_reactor.input_variable_names) == 28 + assert len(self.test_reactor.input_variables.keys()) == 25 + assert len(self.test_reactor.input_variable_names) == 25 def test_creation_with_narrow_divertor(self): """Creates a BallReactor with a narrow divertor and checks that the correct diff --git a/tests/test_parametric_reactors/test_center_column_study_reactor.py b/tests/test_parametric_reactors/test_center_column_study_reactor.py index 65ee8e844..dc6c48086 100644 --- a/tests/test_parametric_reactors/test_center_column_study_reactor.py +++ b/tests/test_parametric_reactors/test_center_column_study_reactor.py @@ -32,8 +32,8 @@ def setUp(self): def test_input_variable_names(self): """tests that the number of inputs variables is correct""" - assert len(self.test_reactor.input_variables.keys()) == 17 - assert len(self.test_reactor.input_variable_names) == 17 + assert len(self.test_reactor.input_variables.keys()) == 14 + assert len(self.test_reactor.input_variable_names) == 14 def test_creation(self): """Creates a ball reactor using the CenterColumnStudyReactor parametric_reactor and checks @@ -92,19 +92,13 @@ def warning_trigger(): def test_export_brep(self): """Exports a brep file and checks that the output exist""" - os.system("rm test_reactor.brep") + os.system("rm merged.brep") - self.test_reactor.export_brep(filename="merged.brep", merge=True) - self.test_reactor.export_brep(filename="not_merged.brep", merge=False) + self.test_reactor.export_brep(filename="merged.brep") assert Path("merged.brep").exists() is True - assert Path("not_merged.brep").exists() is True - # not always true - # assert Path("not_merged.brep").stat().st_size > Path( - # "merged.brep").stat().st_size os.system("rm merged.brep") - os.system("rm not_merged.brep") def test_export_brep_without_extention(self): """Exports a brep file without the extention and checks that the diff --git a/tests/test_parametric_reactors/test_eu_demo_2015_reactor.py b/tests/test_parametric_reactors/test_eu_demo_2015_reactor.py index af6f01714..54c8166bc 100644 --- a/tests/test_parametric_reactors/test_eu_demo_2015_reactor.py +++ b/tests/test_parametric_reactors/test_eu_demo_2015_reactor.py @@ -12,8 +12,8 @@ def test_input_variable_names(self): """tests that the number of inputs variables is correct""" my_reactor = paramak.EuDemoFrom2015PaperDiagram(number_of_tf_coils=1) - assert len(my_reactor.input_variables.keys()) == 5 - assert len(my_reactor.input_variable_names) == 5 + assert len(my_reactor.input_variables.keys()) == 2 + assert len(my_reactor.input_variable_names) == 2 def test_plasma_construction(self): """Creates the plasma part of the EuDemoFrom2015PaperDiagram and checks diff --git a/tests/test_parametric_reactors/test_flf_system_code_reactor.py b/tests/test_parametric_reactors/test_flf_system_code_reactor.py index b1986c7d4..bcdd4835b 100644 --- a/tests/test_parametric_reactors/test_flf_system_code_reactor.py +++ b/tests/test_parametric_reactors/test_flf_system_code_reactor.py @@ -2,7 +2,7 @@ import os import unittest from pathlib import Path - +from cadquery.occ_impl.shapes import Shape import pytest import paramak @@ -28,8 +28,8 @@ def setUp(self): def test_input_variable_names(self): """tests that the number of inputs variables is correct""" - assert len(self.test_reactor.input_variables.keys()) == 13 - assert len(self.test_reactor.input_variable_names) == 13 + assert len(self.test_reactor.input_variables.keys()) == 10 + assert len(self.test_reactor.input_variable_names) == 10 def test_stp_file_creation(self): """Exports a step file and checks that it was saved successfully""" @@ -51,6 +51,29 @@ def test_multiple_stp_file_creation(self): assert Path("upper_vacuum_vessel.stp").is_file() assert Path("vacuum_vessel.stp").is_file() + def test_graveyard_volume_in_brep_export(self): + """Exports the reactor as a brep file and checks the number of volumes + with and without the optional graveyard""" + + my_reactor = paramak.FlfSystemCodeReactor() + + my_reactor.export_brep(filename="without_graveyard.brep", include_graveyard=None) + brep_shapes = Shape.importBrep("without_graveyard.brep").Solids() + assert len(brep_shapes) == 6 + + my_reactor.export_brep(filename="with_graveyard.brep", include_graveyard={"size": 2000}) + brep_shapes = Shape.importBrep("with_graveyard.brep").Solids() + assert len(brep_shapes) == 7 + + # TODO uncomment if ability to not merge surfaces is brought back + # my_reactor.export_brep(filename="without_graveyard.brep", include_graveyard=False, merge=False) + # brep_shapes = Shape.importBrep("without_graveyard.brep").Solids() + # assert len(brep_shapes) == 6 + + # my_reactor.export_brep(filename="with_graveyard.brep", include_graveyard=True, merge=False) + # brep_shapes = Shape.importBrep("with_graveyard.brep").Solids() + # assert len(brep_shapes) == 7 + def test_order_of_names_in_reactor(self): """tests the order of Shapes in the reactor is as expected""" diff --git a/tests/test_parametric_reactors/test_iter_reactor.py b/tests/test_parametric_reactors/test_iter_reactor.py index 9fb0abd02..dfe5d42c6 100644 --- a/tests/test_parametric_reactors/test_iter_reactor.py +++ b/tests/test_parametric_reactors/test_iter_reactor.py @@ -12,8 +12,8 @@ def test_input_variable_names(self): """tests that the number of inputs variables is correct""" my_reactor = paramak.IterFrom2020PaperDiagram(number_of_tf_coils=1) - assert len(my_reactor.input_variables.keys()) == 5 - assert len(my_reactor.input_variable_names) == 5 + assert len(my_reactor.input_variables.keys()) == 2 + assert len(my_reactor.input_variable_names) == 2 def test_plasma_construction(self): """Creates the plasma part of the ITERTokamak and checks diff --git a/tests/test_parametric_reactors/test_segmented_blanket_ball_reactor.py b/tests/test_parametric_reactors/test_segmented_blanket_ball_reactor.py index 8c7eda029..e1a631582 100644 --- a/tests/test_parametric_reactors/test_segmented_blanket_ball_reactor.py +++ b/tests/test_parametric_reactors/test_segmented_blanket_ball_reactor.py @@ -37,8 +37,8 @@ def setUp(self): def test_input_variable_names(self): """tests that the number of inputs variable is correct""" - assert len(self.test_reactor.input_variables.keys()) == 31 - assert len(self.test_reactor.input_variable_names) == 31 + assert len(self.test_reactor.input_variables.keys()) == 28 + assert len(self.test_reactor.input_variable_names) == 28 def test_gap_between_blankets_impacts_volume(self): """Creates a SegmentedBlanketBallReactor with different diff --git a/tests/test_parametric_reactors/test_single_null_ball_reactor.py b/tests/test_parametric_reactors/test_single_null_ball_reactor.py index 14211de4d..a5434b736 100644 --- a/tests/test_parametric_reactors/test_single_null_ball_reactor.py +++ b/tests/test_parametric_reactors/test_single_null_ball_reactor.py @@ -39,8 +39,8 @@ def setUp(self): def test_input_variable_names(self): """tests that the number of inputs variables is correct""" - assert len(self.test_reactor.input_variables.keys()) == 28 - assert len(self.test_reactor.input_variable_names) == 28 + assert len(self.test_reactor.input_variables.keys()) == 25 + assert len(self.test_reactor.input_variable_names) == 25 def test_single_null_ball_reactor_with_pf_and_tf_coils(self): """Checks that a SingleNullBallReactor with optional pf and tf coils can diff --git a/tests/test_parametric_reactors/test_single_null_submersion_tokamak.py b/tests/test_parametric_reactors/test_single_null_submersion_tokamak.py index adb622717..1c396fea0 100644 --- a/tests/test_parametric_reactors/test_single_null_submersion_tokamak.py +++ b/tests/test_parametric_reactors/test_single_null_submersion_tokamak.py @@ -40,8 +40,8 @@ def setUp(self): def test_input_variable_names(self): """tests that the number of inputs variables is correct""" - assert len(self.test_reactor.input_variables.keys()) == 29 - assert len(self.test_reactor.input_variable_names) == 29 + assert len(self.test_reactor.input_variables.keys()) == 26 + assert len(self.test_reactor.input_variable_names) == 26 def test_single_null_submersion_tokamak_with_pf_and_tf_coils(self): """Creates a SingleNullSubmersionTokamak with pf and tf coils and checks diff --git a/tests/test_parametric_reactors/test_sparc_2020_reactor.py b/tests/test_parametric_reactors/test_sparc_2020_reactor.py index 7d0733d09..183d30957 100644 --- a/tests/test_parametric_reactors/test_sparc_2020_reactor.py +++ b/tests/test_parametric_reactors/test_sparc_2020_reactor.py @@ -64,8 +64,8 @@ def setUp(self): def test_input_variables_names(self): """tests that the number of inputs variables is correct""" - assert len(self.test_reactor.input_variables.keys()) == 28 - assert len(self.test_reactor.input_variable_names) == 28 + assert len(self.test_reactor.input_variables.keys()) == 25 + assert len(self.test_reactor.input_variable_names) == 25 def test_make_sparc_2020_reactor(self): """Runs the example to check the output files are produced""" diff --git a/tests/test_parametric_reactors/test_submersion_tokamak.py b/tests/test_parametric_reactors/test_submersion_tokamak.py index e86787a30..2e81b69d0 100644 --- a/tests/test_parametric_reactors/test_submersion_tokamak.py +++ b/tests/test_parametric_reactors/test_submersion_tokamak.py @@ -32,8 +32,8 @@ def setUp(self): def test_input_variable_names(self): """tests that the number of inputs variables is correct""" - assert len(self.test_reactor.input_variables.keys()) == 29 - assert len(self.test_reactor.input_variable_names) == 29 + assert len(self.test_reactor.input_variables.keys()) == 26 + assert len(self.test_reactor.input_variable_names) == 26 def test_svg_creation(self): """Creates a SubmersionTokamak and checks that an svg file of the diff --git a/tests/test_parametric_shapes/test_extrude_straight_shape.py b/tests/test_parametric_shapes/test_extrude_straight_shape.py index eb283e389..2b01b1e76 100644 --- a/tests/test_parametric_shapes/test_extrude_straight_shape.py +++ b/tests/test_parametric_shapes/test_extrude_straight_shape.py @@ -12,15 +12,28 @@ class TestExtrudeStraightShape(unittest.TestCase): def setUp(self): self.test_shape = ExtrudeStraightShape(points=[(10, 10), (10, 30), (30, 30), (30, 10)], distance=30) + def test_bounding_box(self): + """checks the bounding box value""" + + assert self.test_shape.bounding_box == ( + (10.0, -15.0, 10.0), + (30.0, 15.0, 30.0), + ) + + def test_largest_dimension(self): + """checks the largest dimension value""" + + assert self.test_shape.largest_dimension == 25.0 + def test_translate(self): """Checks the shape extends to the bounding box and then translates the shape and checks it is extended to the new bounding box""" assert self.test_shape.solid.val().BoundingBox().xmax == 30 - assert self.test_shape.solid.val().BoundingBox().xmin == 10 assert self.test_shape.solid.val().BoundingBox().ymax == 15 - assert self.test_shape.solid.val().BoundingBox().ymin == -15 assert self.test_shape.solid.val().BoundingBox().zmax == 30 + assert self.test_shape.solid.val().BoundingBox().xmin == 10 + assert self.test_shape.solid.val().BoundingBox().ymin == -15 assert self.test_shape.solid.val().BoundingBox().zmin == 10 self.test_shape.translate = (1, 2, 3) diff --git a/tests/test_parametric_shapes/test_rotate_straight_shape.py b/tests/test_parametric_shapes/test_rotate_straight_shape.py index d576285c9..14aa90d43 100644 --- a/tests/test_parametric_shapes/test_rotate_straight_shape.py +++ b/tests/test_parametric_shapes/test_rotate_straight_shape.py @@ -2,7 +2,7 @@ import os import unittest from pathlib import Path - +from cadquery.occ_impl.shapes import Shape import pytest from paramak import RotateStraightShape @@ -305,6 +305,34 @@ def incorrect_points_definition(): self.assertRaises(ValueError, incorrect_points_definition) + def test_graveyard_volume_in_brep_export(self): + """Exports the reactor as a brep file and checks the number of volumes + with and without the optional graveyard""" + + my_shape = RotateStraightShape(rotation_angle=20, points=[(10, 0), (10, 20), (20, 20), (20, 0)]) + + my_shape.export_brep(filename="without_graveyard.brep", include_graveyard=True) + brep_shapes = Shape.importBrep("without_graveyard.brep").Solids() + assert len(brep_shapes) == 2 + + my_shape.export_brep(filename="without_graveyard.brep", include_graveyard=False) + brep_shapes = Shape.importBrep("without_graveyard.brep").Solids() + assert len(brep_shapes) == 1 + + my_shape.azimuth_placement_angle = [0, 90, 180] + + my_shape.export_brep(filename="with_graveyard.brep", include_graveyard=True) + brep_shapes = Shape.importBrep("with_graveyard.brep").Solids() + assert len(brep_shapes) == 4 + + my_shape.export_brep(filename="without_graveyard.brep", include_graveyard=False) + brep_shapes = Shape.importBrep("without_graveyard.brep").Solids() + assert len(brep_shapes) == 3 + + my_shape.export_brep(filename="with_graveyard.brep", include_graveyard=True) + brep_shapes = Shape.importBrep("with_graveyard.brep").Solids() + assert len(brep_shapes) == 4 + if __name__ == "__main__": unittest.main() diff --git a/tests/test_reactor.py b/tests/test_reactor.py index 0d0393aa2..d1e2da001 100644 --- a/tests/test_reactor.py +++ b/tests/test_reactor.py @@ -27,6 +27,18 @@ def setUp(self): # this reactor has a compound shape in the geometry self.test_reactor_3 = paramak.Reactor([self.test_shape, test_shape_3]) + def test_bounding_box(self): + """checks the bounding box value""" + + bounding_box = self.test_reactor_2.bounding_box + + assert bounding_box[0][0] == pytest.approx(-20.0) + assert bounding_box[0][1] == pytest.approx(-20.0) + assert bounding_box[0][2] == pytest.approx(0.0) + assert bounding_box[1][0] == pytest.approx(100.0) + assert bounding_box[1][1] == pytest.approx(20.0) + assert bounding_box[1][2] == pytest.approx(100.0) + def test_reactor_export_stp_with_name_set_to_none(self): """Exports the reactor as separate files and as a single file""" @@ -50,17 +62,16 @@ def test_reactor_export_stp(self): def test_incorrect_graveyard_offset_too_small(self): def incorrect_graveyard_offset_too_small(): - """Set graveyard_offset as a negative number which should raise an error""" + """Set graveyard offset as a negative number which should raise an error""" - self.test_reactor.graveyard_offset = -3 + self.test_reactor.make_graveyard(offset=-3) self.assertRaises(ValueError, incorrect_graveyard_offset_too_small) def test_incorrect_graveyard_offset_wrong_type(self): def incorrect_graveyard_offset_wrong_type(): - """Set graveyard_offset as a string which should raise an error""" - - self.test_reactor.graveyard_offset = "coucou" + """Set graveyard offset as a string which should raise an error""" + self.test_reactor.make_graveyard(offset="coucou") self.assertRaises(TypeError, incorrect_graveyard_offset_wrong_type) @@ -68,16 +79,14 @@ def test_largest_dimension_setting_and_getting_using_largest_shapes(self): """Makes a neutronics model and checks the default largest_dimension and that largest_dimension changes with largest_shapes""" - assert self.test_reactor.largest_dimension == 20.0 + assert pytest.approx(self.test_reactor.largest_dimension) == 20.0 + assert self.test_reactor_2.largest_dimension == 100.0 test_shape = paramak.RotateStraightShape(points=[(0, 0), (0, 20), (20, 20)]) test_shape2 = paramak.RotateStraightShape(points=[(0, 0), (0, 40), (40, 40)]) test_reactor = paramak.Reactor([test_shape, test_shape2]) - assert test_reactor.largest_dimension == 40 - - test_reactor.largest_shapes = [test_shape] - assert test_reactor.largest_dimension == 20 + assert pytest.approx(test_reactor.largest_dimension) == 40 def test_make_sector_wedge(self): """Checks that the wedge is not made when rotation angle is 360""" @@ -94,7 +103,7 @@ def test_stl_filename_list_length(): def test_make_graveyard_accepts_offset_from_graveyard(self): """Creates a graveyard for a reactor and sets the graveyard_offset. - Checks that the Reactor.graveyard_offset property is set""" + Checks that the Reactor.graveyard property is set""" test_shape = paramak.RotateStraightShape( points=[(0, 0), (0, 20), (20, 20)], @@ -104,9 +113,10 @@ def test_make_graveyard_accepts_offset_from_graveyard(self): ) test_shape.rotation_angle = 360 test_reactor = paramak.Reactor([test_shape, test_shape2]) - test_reactor.graveyard_offset == 101 - graveyard = test_reactor.make_graveyard() + + graveyard = test_reactor.make_graveyard(offset=101) assert graveyard.volume() > 0 + assert test_reactor.graveyard.volume() > 0 def test_reactor_creation_with_default_properties(self): """creates a Reactor object and checks that it has no default properties""" @@ -134,7 +144,7 @@ def test_graveyard_exists(self): test_shape.rotation_angle = 360 test_shape.create_solid() test_reactor = paramak.Reactor([test_shape]) - test_reactor.make_graveyard() + test_reactor.make_graveyard(size=100) assert isinstance(test_reactor.graveyard, paramak.Shape) @@ -148,7 +158,7 @@ def test_graveyard_exists_solid_is_none(self): test_shape.create_solid() test_reactor = paramak.Reactor([test_shape]) test_reactor.shapes_and_components[0].solid = None - test_reactor.make_graveyard() + test_reactor.make_graveyard(size=100) assert isinstance(test_reactor.graveyard, paramak.Shape) @@ -162,14 +172,15 @@ def test_export_graveyard(self): os.system("rm graveyard.stp") test_reactor = paramak.Reactor([test_shape]) - test_reactor.export_stp_graveyard() - test_reactor.export_stp_graveyard(filename="my_graveyard.stp") - test_reactor.export_stp_graveyard(filename="my_graveyard_without_ext") + test_reactor.make_graveyard(size=100) + test_reactor.graveyard.export_stp(filename="graveyard.stp") + test_reactor.graveyard.export_stp(filename="my_graveyard.stp") + test_reactor.graveyard.export_stp(filename="my_graveyard_without_ext.step") for filepath in [ "graveyard.stp", "my_graveyard.stp", - "my_graveyard_without_ext.stp", + "my_graveyard_without_ext.step", ]: assert Path(filepath).exists() is True os.system("rm " + filepath) @@ -183,16 +194,16 @@ def test_make_graveyard_offset(self): test_shape = paramak.RotateStraightShape(points=[(0, 0), (0, 20), (20, 20)]) os.system("rm graveyard.stp") - test_reactor = paramak.Reactor([test_shape], graveyard_size=None, graveyard_offset=100) - test_reactor.make_graveyard() + test_reactor = paramak.Reactor([test_shape]) + test_reactor.make_graveyard(offset=100) graveyard_volume_1 = test_reactor.graveyard.volume() - test_reactor.make_graveyard(graveyard_offset=50) + test_reactor.make_graveyard(offset=50) assert test_reactor.graveyard.volume() < graveyard_volume_1 graveyard_volume_2 = test_reactor.graveyard.volume() - test_reactor.make_graveyard(graveyard_offset=200) + test_reactor.make_graveyard(offset=200) assert test_reactor.graveyard.volume() > graveyard_volume_1 assert test_reactor.graveyard.volume() > graveyard_volume_2 @@ -375,27 +386,30 @@ def test_graveyard_size_setting_magnitude_checking(self): def incorrect_graveyard_size_size(): test_shape = paramak.RotateStraightShape(points=[(0, 0), (0, 20), (20, 20)]) - paramak.Reactor([test_shape], graveyard_size=-10) + test_reactor = paramak.Reactor([test_shape]) + test_reactor.make_graveyard(size=-10) self.assertRaises(ValueError, incorrect_graveyard_size_size) def test_graveyard_offset_setting_type_checking(self): - """Attempts to make a reactor with a graveyard_offset that is an float + """Attempts to make a reactor with a graveyard offset that is an float which should raise a ValueError""" def incorrect_graveyard_offset_type(): test_shape = paramak.RotateStraightShape(points=[(0, 0), (0, 20), (20, 20)]) - paramak.Reactor([test_shape], graveyard_offset="coucou") + test_reactor = paramak.Reactor([test_shape]) + test_reactor.make_graveyard(offset="coucou") self.assertRaises(TypeError, incorrect_graveyard_offset_type) def test_graveyard_offset_setting_magnitude_checking(self): - """Attempts to make a reactor with a graveyard_offset that is an int + """Attempts to make a reactor with a graveyard offset that is an int which should raise a ValueError""" def incorrect_graveyard_offset_size(): test_shape = paramak.RotateStraightShape(points=[(0, 0), (0, 20), (20, 20)]) - paramak.Reactor([test_shape], graveyard_offset=-10) + test_reactor = paramak.Reactor([test_shape]) + test_reactor.make_graveyard(size=-10) self.assertRaises(ValueError, incorrect_graveyard_offset_size) @@ -404,17 +418,17 @@ def test_graveyard_error(self): test_reactor = paramak.Reactor([test_shape]) def str_graveyard_offset(): - test_reactor.graveyard_offset = "coucou" + test_reactor.make_graveyard(offset="coucou") self.assertRaises(TypeError, str_graveyard_offset) def negative_graveyard_offset(): - test_reactor.graveyard_offset = -2 + test_reactor.make_graveyard(offset=-2) self.assertRaises(ValueError, negative_graveyard_offset) def list_graveyard_offset(): - test_reactor.graveyard_offset = [1.2] + test_reactor.make_graveyard(offset=[1.2]) self.assertRaises(TypeError, list_graveyard_offset) @@ -427,7 +441,7 @@ def test_compound_in_shapes(self): assert test_reactor.solid is not None def test_sector_wedge_with_360_returns_none(self): - """Trys to make a sector wedge with full 360 degree rotation and checks + """Tries to make a sector wedge with full 360 degree rotation and checks that None is returned""" test_shape = paramak.RotateStraightShape(points=[(0, 0), (0, 20), (20, 20)]) diff --git a/tests/test_shape.py b/tests/test_shape.py index 4c6aefd1d..75c3df995 100644 --- a/tests/test_shape.py +++ b/tests/test_shape.py @@ -595,7 +595,7 @@ def test_rotation_axis_error(self): [(1, 1, 1), (1, 1, 1)], [(1, 1, 1), (1, 0, 1, 2)], [(1, 1, 1, 2), (1, 0, 2)], - [(1, 1, 2), [1, 0, 2]], + # [(1, 1, 2), [1, 0, 2]], lists are now acceptable [(1, 1, 1)], [(1, 1, 1), (1, "coucou", 1)], [(1, 1, 1), (1, 0, 1), (1, 2, 3)], diff --git a/tests/test_utils.py b/tests/test_utils.py index 753edac95..f42ed3fd0 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -12,10 +12,105 @@ find_center_point_of_circle, plotly_trace, find_radius_of_circle, + get_bounding_box, + get_largest_dimension, + get_largest_distance_from_origin, ) +import cadquery as cq class TestUtilityFunctions(unittest.TestCase): + """ "tests the utility functions""" + + def test_bounding_box_with_single_shape_at_origin(self): + """checks the type and values of the bounding box returned""" + + test_sphere = cq.Workplane("XY").moveTo(0, 0).sphere(10) + + bounding_box = get_bounding_box(test_sphere) + + assert len(bounding_box) == 2 + assert len(bounding_box[0]) == 3 + assert len(bounding_box[1]) == 3 + assert bounding_box[0][0] == -10 + assert bounding_box[0][1] == -10 + assert bounding_box[0][2] == -10 + assert bounding_box[1][0] == 10 + assert bounding_box[1][1] == 10 + assert bounding_box[1][2] == 10 + + def test_bounding_box_with_single_shape(self): + """checks the type and values of the bounding box returned""" + + test_sphere = cq.Workplane("XY").moveTo(100, 50).sphere(10) + + bounding_box = get_bounding_box(test_sphere) + + assert len(bounding_box) == 2 + assert len(bounding_box[0]) == 3 + assert len(bounding_box[1]) == 3 + assert bounding_box[0][0] == 90 + assert bounding_box[0][1] == 40 + assert bounding_box[0][2] == -10 + assert bounding_box[1][0] == 110 + assert bounding_box[1][1] == 60 + assert bounding_box[1][2] == 10 + + def test_bounding_box_with_compound(self): + """checks the type and values of the bounding box returned""" + + test_sphere_1 = cq.Workplane("XY").moveTo(100, 50).sphere(10) + test_sphere_2 = cq.Workplane("XY").moveTo(-100, -50).sphere(10) + + both_shapes = cq.Compound.makeCompound([test_sphere_1.val(), test_sphere_2.val()]) + + bounding_box = get_bounding_box(both_shapes) + + assert len(bounding_box) == 2 + assert len(bounding_box[0]) == 3 + assert len(bounding_box[1]) == 3 + assert bounding_box[0][0] == -110 + assert bounding_box[0][1] == -60 + assert bounding_box[0][2] == -10 + assert bounding_box[1][0] == 110 + assert bounding_box[1][1] == 60 + assert bounding_box[1][2] == 10 + + def test_largest_dimension_with_single_solid_at_origin(self): + + test_sphere = cq.Workplane("XY").moveTo(0, 0).sphere(10) + + largest_dimension = get_largest_dimension(test_sphere) + + assert largest_dimension == 20 + + def test_largest_dimension__from_origin_with_single_solid_at_origin(self): + + test_sphere = cq.Workplane("XY").moveTo(0, 0).sphere(10) + + largest_dimension = get_largest_distance_from_origin(test_sphere) + + assert largest_dimension == 10 + + def test_largest_dimension_with_single_solid(self): + + test_sphere = cq.Workplane("XY").moveTo(100, 0).sphere(10) + + largest_dimension = get_largest_distance_from_origin(test_sphere) + + assert largest_dimension == 110 + + def test_largest_dimension_with_compound(self): + + test_sphere_1 = cq.Workplane("XY").moveTo(100, 50).sphere(10) + test_sphere_2 = cq.Workplane("XY").moveTo(-200, -50).sphere(10) + + both_shapes = cq.Compound.makeCompound([test_sphere_1.val(), test_sphere_2.val()]) + + largest_dimension = get_largest_distance_from_origin(both_shapes) + + assert largest_dimension == 210 + def test_convert_circle_to_spline(self): """Tests the conversion of 3 points on a circle into points on a spline curve.""" diff --git a/tests_h5m/test_reactor_export_h5m.py b/tests_h5m/test_reactor_export_h5m.py index 97b61de03..60d5fb240 100644 --- a/tests_h5m/test_reactor_export_h5m.py +++ b/tests_h5m/test_reactor_export_h5m.py @@ -26,25 +26,95 @@ def setUp(self): # this reactor has a compound shape in the geometry self.test_reactor_3 = paramak.Reactor([self.test_shape, test_shape_3]) + def test_dagmc_h5m_custom_tags_export(self): + """Exports a reactor with two shapes checks that the tags are correctly + named in the resulting h5m file""" + + self.test_reactor_3.rotation_angle = 180 + self.test_reactor_3.export_dagmc_h5m("dagmc_reactor.h5m", tags=["1", "2"]) + + vols = di.get_volumes_from_h5m("dagmc_reactor.h5m") + assert vols == [1, 2, 3] # there are three volumes in test_reactor_3 + + mats = di.get_materials_from_h5m("dagmc_reactor.h5m") + print(mats) + assert mats == ["1", "2"] + + vols_and_mats = di.get_volumes_and_materials_from_h5m("dagmc_reactor.h5m") + assert vols_and_mats == { + 1: "1", + 2: "2", + 3: "2", + } + def test_dagmc_h5m_export(self): - """Exports a shape with a single volume and checks that it - exist (volume id and material tag) in the resulting h5m file""" + """Exports a reactor with two shapes checks that the tags are correctly + named in the resulting h5m file""" self.test_reactor_3.rotation_angle = 180 self.test_reactor_3.export_dagmc_h5m("dagmc_reactor.h5m") vols = di.get_volumes_from_h5m("dagmc_reactor.h5m") - assert vols == [1, 2, 3] # there are three volumes in test_reactor_3 + assert vols == [1, 2, 3] # there are two shapes three volumes in test_reactor_3 + + mats = di.get_materials_from_h5m("dagmc_reactor.h5m") + print(mats) + assert mats == ["pf_coil", "test_shape"] + + vols_and_mats = di.get_volumes_and_materials_from_h5m("dagmc_reactor.h5m") + assert vols_and_mats == { + 1: "test_shape", + 2: "pf_coil", + 3: "pf_coil", + } + + def test_dagmc_h5m_custom_tags_export_with_graveyard(self): + """Exports a reactor with two shapes checks that the tags are correctly + named in the resulting h5m file, includes the optional graveyard""" + + self.test_reactor_3.rotation_angle = 180 + self.test_reactor_3.export_dagmc_h5m( + "dagmc_reactor.h5m", tags=["1", "2", "grave"], include_graveyard={"size": 250} + ) + + vols = di.get_volumes_from_h5m("dagmc_reactor.h5m") + assert vols == [1, 2, 3, 4] + + mats = di.get_materials_from_h5m("dagmc_reactor.h5m") + print(mats) + assert mats == ["1", "2", "grave"] + + vols_and_mats = di.get_volumes_and_materials_from_h5m("dagmc_reactor.h5m") + assert vols_and_mats == { + 1: "1", + 2: "2", + 3: "2", + 4: "grave", + } + + def test_dagmc_h5m_export_with_graveyard(self): + """Exports a reactor with two shapes checks that the tags are correctly + named in the resulting h5m file, includes the optional graveyard""" + + self.test_reactor_3.rotation_angle = 180 + self.test_reactor_3.export_dagmc_h5m("dagmc_reactor.h5m", include_graveyard={"size": 250}) + + vols = di.get_volumes_from_h5m("dagmc_reactor.h5m") + assert vols == [1, 2, 3, 4] mats = di.get_materials_from_h5m("dagmc_reactor.h5m") print(mats) - assert mats == ["mat_pf_coil", "mat_test_shape"] + assert "test_shape" in mats + assert "pf_coil" in mats + assert "graveyard" in mats + assert len(mats) == 3 vols_and_mats = di.get_volumes_and_materials_from_h5m("dagmc_reactor.h5m") assert vols_and_mats == { - 1: "mat_test_shape", - 2: "mat_pf_coil", - 3: "mat_pf_coil", + 1: "test_shape", + 2: "pf_coil", + 3: "pf_coil", + 4: "graveyard", } def test_dagmc_h5m_export_mesh_size(self): @@ -56,6 +126,21 @@ def test_dagmc_h5m_export_mesh_size(self): assert Path("dagmc_bigger.h5m").stat().st_size > Path("dagmc_default.h5m").stat().st_size + def test_dagmc_h5m_export_error_handling(self): + """Exports a shape with the wrong amount of tags""" + + def too_few_tags(): + self.test_reactor_3.rotation_angle = 180 + self.test_reactor_3.export_dagmc_h5m("dagmc_reactor.h5m", tags=["1"]) + + self.assertRaises(ValueError, too_few_tags) + + def too_many_tags(): + self.test_reactor_3.rotation_angle = 180 + self.test_reactor_3.export_dagmc_h5m("dagmc_reactor.h5m", tags=["1", "2", "3"]) + + self.assertRaises(ValueError, too_many_tags) + if __name__ == "__main__": unittest.main() diff --git a/tests_h5m/test_rotate_straight_shape_export_h5m.py b/tests_h5m/test_rotate_straight_shape_export_h5m.py index 880b9a28f..9537aaa5d 100644 --- a/tests_h5m/test_rotate_straight_shape_export_h5m.py +++ b/tests_h5m/test_rotate_straight_shape_export_h5m.py @@ -10,6 +10,7 @@ class TestRotateStraightShape(unittest.TestCase): def setUp(self): self.test_shape = RotateStraightShape(points=[(0, 0), (0, 20), (20, 20), (20, 0)]) + self.test_shape.graveyard_size = 100 def test_dagmc_h5m_export_multi_volume(self): """Exports a shape with multiple volumes and checks that they all @@ -24,14 +25,61 @@ def test_dagmc_h5m_export_multi_volume(self): assert vols == [1, 2, 3, 4] mats = di.get_materials_from_h5m("dagmc_multi_volume.h5m") - assert mats == ["mat_my_material_name"] + assert mats == ["my_material_name"] vols_and_mats = di.get_volumes_and_materials_from_h5m("dagmc_multi_volume.h5m") assert vols_and_mats == { - 1: "mat_my_material_name", - 2: "mat_my_material_name", - 3: "mat_my_material_name", - 4: "mat_my_material_name", + 1: "my_material_name", + 2: "my_material_name", + 3: "my_material_name", + 4: "my_material_name", + } + + def test_dagmc_h5m_export_custom_tag_multi_volume(self): + """Exports a shape with multiple volumes and checks that they all + exist (volume ids and material tags) in the resulting h5m file""" + + self.test_shape.rotation_angle = 10 + self.test_shape.azimuth_placement_angle = [0, 90, 180, 270] + self.test_shape.name = "my_material_name" + self.test_shape.export_dagmc_h5m("dagmc_multi_volume.h5m", tags=["1"]) + + vols = di.get_volumes_from_h5m("dagmc_multi_volume.h5m") + assert vols == [1, 2, 3, 4] + + mats = di.get_materials_from_h5m("dagmc_multi_volume.h5m") + assert mats == ["1"] + + vols_and_mats = di.get_volumes_and_materials_from_h5m("dagmc_multi_volume.h5m") + assert vols_and_mats == { + 1: "1", + 2: "1", + 3: "1", + 4: "1", + } + + def test_dagmc_h5m_export_custom_tag_multi_volume_with_graveyard(self): + """Exports a shape with multiple volumes and checks that they all + exist (volume ids and material tags) in the resulting h5m file""" + + self.test_shape.rotation_angle = 10 + self.test_shape.azimuth_placement_angle = [0, 90, 180, 270] + self.test_shape.name = "my_material_name" + self.test_shape.export_dagmc_h5m("dagmc_multi_volume.h5m", tags=["1", "graveyard"], include_graveyard=True) + + vols = di.get_volumes_from_h5m("dagmc_multi_volume.h5m") + assert vols == [1, 2, 3, 4, 5] + + mats = di.get_materials_from_h5m("dagmc_multi_volume.h5m") + assert mats == ["1", "graveyard"] + + vols_and_mats = di.get_volumes_and_materials_from_h5m("dagmc_multi_volume.h5m") + assert vols_and_mats == { + 1: "1", + 2: "1", + 3: "1", + 4: "1", + 5: "graveyard", } def test_dagmc_h5m_export_single_volume(self): @@ -46,10 +94,45 @@ def test_dagmc_h5m_export_single_volume(self): assert vols == [1] mats = di.get_materials_from_h5m("dagmc_single_volume.h5m") - assert mats == ["mat_my_material_name_single"] + assert mats == ["my_material_name_single"] + + vols_and_mats = di.get_volumes_and_materials_from_h5m("dagmc_single_volume.h5m") + assert vols_and_mats == {1: "my_material_name_single"} + + def test_dagmc_h5m_export_single_volume_with_graveyard(self): + """Exports a shape with a single volume plus graveyard cell and checks + that it exist (volume id and material tag) in the resulting h5m file""" + + self.test_shape.rotation_angle = 180 + self.test_shape.name = "my_material_name_single" + self.test_shape.export_dagmc_h5m(filename="dagmc_single_volume.h5m", include_graveyard=True) + + vols = di.get_volumes_from_h5m("dagmc_single_volume.h5m") + assert vols == [1, 2] + + mats = di.get_materials_from_h5m("dagmc_single_volume.h5m") + assert "my_material_name_single" in mats + assert "graveyard" in mats + assert len(mats) == 2 + + vols_and_mats = di.get_volumes_and_materials_from_h5m("dagmc_single_volume.h5m") + assert vols_and_mats == {1: "my_material_name_single", 2: "graveyard"} + + def test_dagmc_h5m_export_single_volume_custom_tags(self): + """Exports a shape with a single volume and checks that it + exist (volume id and custom material tag) in the resulting h5m file""" + + self.test_shape.rotation_angle = 180 + self.test_shape.export_dagmc_h5m("dagmc_custom_tag_single_volume.h5m", tags=["1"]) + + vols = di.get_volumes_from_h5m("dagmc_custom_tag_single_volume.h5m") + assert vols == [1] + + mats = di.get_materials_from_h5m("dagmc_custom_tag_single_volume.h5m") + assert mats == ["1"] vols_and_mats = di.get_volumes_and_materials_from_h5m("dagmc_single_volume.h5m") - assert vols_and_mats == {1: "mat_my_material_name_single"} + assert vols_and_mats == {1: "my_material_name_single"} def test_dagmc_h5m_export_mesh_size(self): """Exports h5m file with higher resolution mesh and checks that the