diff --git a/examples/example_parametric_components/make_all_parametric_components.py b/examples/example_parametric_components/make_all_parametric_components.py index bcee900e0..4160bc5eb 100644 --- a/examples/example_parametric_components/make_all_parametric_components.py +++ b/examples/example_parametric_components/make_all_parametric_components.py @@ -334,7 +334,7 @@ def main(): component = paramak.PortCutterRectangular( distance=3, - z_pos=0, + center_point=(0, 0), height=0.2, width=0.4, fillet_radius=0.02, @@ -344,7 +344,7 @@ def main(): component = paramak.PortCutterCircular( distance=3, - z_pos=0.25, + center_point=(0.25, 0), radius=0.1, # azimuth_placement_angle=[0, 45, 90, 180], # TODO: fix issue #548 azimuth_placement_angle=[0, 45, 90], diff --git a/examples/example_parametric_components/make_vacuum_vessel_with_ports.py b/examples/example_parametric_components/make_vacuum_vessel_with_ports.py index 9085eb41a..16b3d2b61 100644 --- a/examples/example_parametric_components/make_vacuum_vessel_with_ports.py +++ b/examples/example_parametric_components/make_vacuum_vessel_with_ports.py @@ -25,7 +25,7 @@ def main(): # makes the middle row of ports circular_ports = paramak.PortCutterCircular( distance=5, - z_pos=0, + center_point=(0, 0), radius=0.2, azimuth_placement_angle=angles_for_ports ) @@ -33,7 +33,7 @@ def main(): # makes the lower row of ports rectangular_ports = paramak.PortCutterRectangular( distance=5, - z_pos=-1, + center_point=(-1, 0), height=0.3, width=0.4, fillet_radius=0.08, diff --git a/paramak/__init__.py b/paramak/__init__.py index 52c7e4490..0229848c5 100644 --- a/paramak/__init__.py +++ b/paramak/__init__.py @@ -77,6 +77,7 @@ from .parametric_components.shell_fs import ShellFS from .parametric_reactors.ball_reactor import BallReactor +from .parametric_reactors.ball_reactor_with_ports import BallReactorWithPorts from .parametric_reactors.submersion_reactor import SubmersionTokamak from .parametric_reactors.single_null_submersion_reactor import SingleNullSubmersionTokamak from .parametric_reactors.single_null_ball_reactor import SingleNullBallReactor diff --git a/paramak/parametric_components/port_cutters_circular.py b/paramak/parametric_components/port_cutters_circular.py index b6aa508d0..bf3290590 100644 --- a/paramak/parametric_components/port_cutters_circular.py +++ b/paramak/parametric_components/port_cutters_circular.py @@ -1,36 +1,37 @@ from paramak import ExtrudeCircleShape +from typing import Optional + class PortCutterCircular(ExtrudeCircleShape): """Creates an extruded shape with a circular section that is used to cut other components (eg. blanket, vessel,..) in order to create ports. Args: - z_pos (float): Z position (cm) of the port - height (float): height (cm) of the port - width (float): width (cm) of the port - distance (float): extruded distance (cm) of the cutter - stp_filename (str, optional): defaults to "PortCutterCircular.stp". - stl_filename (str, optional): defaults to "PortCutterCircular.stl". - name (str, optional): defaults to "circular_port_cutter". - material_tag (str, optional): defaults to "circular_port_cutter_mat". - extrusion_start_offset (float, optional): the distance between 0 and + center_point: center point of the port cutter. Defaults to (0, 0). + radius: radius (cm) of port cutter. + distance: extruded distance (cm) of the port cutter. + stp_filename: defaults to "PortCutterCircular.stp". + stl_filename: defaults to "PortCutterCircular.stl". + name: defaults to "circular_port_cutter". + material_tag: defaults to "circular_port_cutter_mat". + extrusion_start_offset: the distance between 0 and the start of the extrusion. Defaults to 1.. """ def __init__( self, - z_pos, - radius, - distance, - workplane="ZY", - rotation_axis="Z", - extrusion_start_offset=1., - stp_filename="PortCutterCircular.stp", - stl_filename="PortCutterCircular.stl", - name="circular_port_cutter", - material_tag="circular_port_cutter_mat", + radius: float, + distance: float, + center_point: Optional[tuple] = (0, 0), + workplane: Optional[str] = "ZY", + rotation_axis: Optional[str] = "Z", + extrusion_start_offset: Optional[float] = 1., + stp_filename: Optional[str] = "PortCutterCircular.stp", + stl_filename: Optional[str] = "PortCutterCircular.stl", + name: Optional[str] = "circular_port_cutter", + material_tag: Optional[str] = "circular_port_cutter_mat", **kwargs ): super().__init__( @@ -47,8 +48,8 @@ def __init__( **kwargs ) - self.z_pos = z_pos + self.center_point = center_point self.radius = radius def find_points(self): - self.points = [(0, self.z_pos)] + self.points = [self.center_point] diff --git a/paramak/parametric_components/port_cutters_rectangular.py b/paramak/parametric_components/port_cutters_rectangular.py index 4ac5fc9b4..55ce4a741 100644 --- a/paramak/parametric_components/port_cutters_rectangular.py +++ b/paramak/parametric_components/port_cutters_rectangular.py @@ -1,16 +1,18 @@ from paramak import ExtrudeStraightShape +from typing import Optional + class PortCutterRectangular(ExtrudeStraightShape): """Creates an extruded shape with a rectangular section that is used to cut other components (eg. blanket, vessel,..) in order to create ports. Args: - z_pos (float): Z position (cm) of the port - height (float): height (cm) of the port - width (float): width (cm) of the port - distance (float): extruded distance (cm) of the cutter + center_point: Center point of the port cutter. Defaults to (0, 0). + height: height (cm) of the port cutter. + width: width (cm) of the port cutter. + distance: extruded distance (cm) of the port cutter. fillet_radius (float, optional): If not None, radius (cm) of fillets added to edges orthogonal to the Z direction. Defaults to None. stp_filename (str, optional): defaults to "PortCutterRectangular.stp". @@ -24,18 +26,18 @@ class PortCutterRectangular(ExtrudeStraightShape): def __init__( self, - z_pos, - height, - width, - distance, - workplane="ZY", - rotation_axis="Z", - extrusion_start_offset=1., - fillet_radius=None, - stp_filename="PortCutterRectangular.stp", - stl_filename="PortCutterRectangular.stl", - name="rectangular_port_cutter", - material_tag="rectangular_port_cutter_mat", + height: float, + width: float, + distance: float, + center_point: Optional[tuple] = (0, 0), + workplane: Optional[str] = "ZY", + rotation_axis: Optional[str] = "Z", + extrusion_start_offset: Optional[float] = 1., + fillet_radius: Optional[float] = None, + stp_filename: Optional[str] = "PortCutterRectangular.stp", + stl_filename: Optional[str] = "PortCutterRectangular.stl", + name: Optional[str] = "rectangular_port_cutter", + material_tag: Optional[str] = "rectangular_port_cutter_mat", **kwargs ): @@ -52,11 +54,11 @@ def __init__( **kwargs ) - self.z_pos = z_pos + self.center_point = center_point self.height = height self.width = width self.fillet_radius = fillet_radius - self.add_fillet() + # self.add_fillet() def find_points(self): points = [ @@ -65,9 +67,19 @@ def find_points(self): (self.width / 2, self.height / 2), (-self.width / 2, self.height / 2), ] - points = [(e[0], e[1] + self.z_pos) for e in points] + points = [(e[0] + self.center_point[0], e[1] + + self.center_point[1]) for e in points] self.points = points - def add_fillet(self): + def add_fillet(self, solid): + if "X" not in self.workplane: + filleting_edge = "|X" + if "Y" not in self.workplane: + filleting_edge = "|Y" + if "Z" not in self.workplane: + filleting_edge = "|Z" + if self.fillet_radius is not None and self.fillet_radius != 0: - self.solid = self.solid.edges('#Z').fillet(self.fillet_radius) + solid = solid.edges(filleting_edge).fillet(self.fillet_radius) + + return solid diff --git a/paramak/parametric_reactors/ball_reactor.py b/paramak/parametric_reactors/ball_reactor.py index 6cae0fb92..f157e5511 100644 --- a/paramak/parametric_reactors/ball_reactor.py +++ b/paramak/parametric_reactors/ball_reactor.py @@ -2,6 +2,7 @@ import warnings import paramak +from paramak.utils import perform_port_cutting class BallReactor(paramak.Reactor): @@ -423,6 +424,9 @@ def _make_blankets_layers(self): cut=[self._center_column_cutter], ) + self._firstwall, self._blanket, self._blanket_rear_wall = perform_port_cutting( + self, self._firstwall, self._blanket, self._blanket_rear_wall) + return [self._firstwall, self._blanket, self._blanket_rear_wall] def _make_divertor(self): diff --git a/paramak/parametric_reactors/ball_reactor_with_ports.py b/paramak/parametric_reactors/ball_reactor_with_ports.py new file mode 100644 index 000000000..151f91ab6 --- /dev/null +++ b/paramak/parametric_reactors/ball_reactor_with_ports.py @@ -0,0 +1,69 @@ + +import warnings +from typing import Optional + +import numpy as np +import paramak +from paramak.utils import perform_port_cutting + + +class BallReactorWithPorts(paramak.BallReactor): + """Creates geometry for a simple ball reactor including a plasma, + cylindrical center column shielding, square toroidal field coils + and ports. There is no inboard breeder blanket on this ball reactor + like most spherical reactors. + + Arguments: + port_type: type of port to be cut. Defaults to None. + port_center_point: position of port center point in the workplane + given. Defaults to (0, 0). + port_radius: radius of circular ports. Defaults to None. + port_height: height of rectangular ports. Defaults to None. + port_width: width of rectangular ports. Defaults to None. + port_distance: extrusion distance of port cutter. Defaults to None. + port_azimuth_placement_angle: azimuthal placement angle of each port. + Defaults to None if no ports are created. Defaults to list of + equally spaced floats between 0 and 360 of length equal to + number_of_ports if number_of_ports is provided but + port_azimuth_placement_angle is not. + port_start_radius: extrusion start point of port cutter. Defaults + to major_radius. + port_fillet_radius: fillet radius of rectangular ports. Defaults to 0. + """ + + def __init__( + self, + port_type: Optional[str] = None, + port_center_point: Optional[tuple] = (0, 0), + port_radius: Optional[float] = None, + port_height: Optional[float] = None, + port_width: Optional[float] = None, + port_distance: Optional[float] = None, + port_azimuth_placement_angle: Optional[list] = None, + port_start_radius: Optional[float] = None, + port_fillet_radius: Optional[float] = 0, + **kwargs + ): + + super().__init__(**kwargs) + + self.port_type = port_type + self.port_center_point = port_center_point + self.port_radius = port_radius + self.port_height = port_height + self.port_width = port_width + self.port_distance = port_distance + self.port_azimuth_placement_angle = port_azimuth_placement_angle + self.port_start_radius = port_start_radius + self.port_fillet_radius = port_fillet_radius + + @property + def port_start_radius(self): + return self._port_start_radius + + @port_start_radius.setter + def port_start_radius(self, value): + if value is None: + self._port_start_radius = self.major_radius + else: + self._port_start_radius = value diff --git a/paramak/parametric_shapes/extruded_mixed_shape.py b/paramak/parametric_shapes/extruded_mixed_shape.py index 0e19acc2c..027d5d362 100644 --- a/paramak/parametric_shapes/extruded_mixed_shape.py +++ b/paramak/parametric_shapes/extruded_mixed_shape.py @@ -89,6 +89,11 @@ def create_solid(self): distance=extrusion_distance, both=self.extrude_both) + # filleting rectangular port cutter edges + # must be done before azimuthal placement + if hasattr(self, "add_fillet"): + solid = self.add_fillet(solid) + solid = self.rotate_solid(solid) cutting_wedge = calculate_wedge_cut(self) solid = self.perform_boolean_operations(solid, wedge_cut=cutting_wedge) diff --git a/paramak/utils.py b/paramak/utils.py index 7ea02bbdd..17e202c87 100644 --- a/paramak/utils.py +++ b/paramak/utils.py @@ -620,6 +620,64 @@ def export_wire_to_html( return fig +def perform_port_cutting(self, *args): + + components = [] + + if self.port_type is None: + for component in args: + components.append(component) + if len(args) == 1: + return component + return components + + else: + if self.port_type == "circular": + if self.port_height is not None or self.port_width is not None: + raise ValueError('only port_radius should be specified') + if self.port_radius is None: + raise ValueError('port_radius must be specified') + + port_cutter = paramak.PortCutterCircular( + center_point=self.port_center_point, + radius=self.port_radius, + distance=self.port_distance, + extrusion_start_offset=self.port_start_radius, + azimuth_placement_angle=self.port_azimuth_placement_angle + ) + + elif self.port_type == "rectangular": + if self.port_radius is not None: + raise ValueError( + 'only port_height and port_width should be specified') + if self.port_height is None or self.port_width is None: + raise ValueError( + 'port_height and port_width must be specified') + + port_cutter = paramak.PortCutterRectangular( + center_point=self.port_center_point, + height=self.port_height, + width=self.port_width, + distance=self.port_distance, + extrusion_start_offset=self.port_start_radius, + fillet_radius=self.port_fillet_radius, + azimuth_placement_angle=self.port_azimuth_placement_angle + ) + + else: + raise ValueError('invalid port type') + + for component in args: + if component.cut is None: + component.cut = [port_cutter] + else: + component.cut.insert(0, port_cutter) + components.append(component) + if len(args) == 1: + return component + return components + + class FaceAreaSelector(cq.Selector): """A custom CadQuery selector the selects faces based on their area with a tolerance. The following useage example will fillet the faces of an extrude diff --git a/tests/test_parametric_components/test_PortCutterCircular.py b/tests/test_parametric_components/test_PortCutterCircular.py index a6d5e7197..a4d25af4a 100644 --- a/tests/test_parametric_components/test_PortCutterCircular.py +++ b/tests/test_parametric_components/test_PortCutterCircular.py @@ -1,19 +1,56 @@ - +import math import unittest import paramak +import pytest + + +class test_component(unittest.TestCase): + + def setUp(self): + self.test_shape = paramak.PortCutterCircular( + distance=300, radius=20 + ) + + def test_default_parameters(self): + """Checks that the default parameters of a PortCutterCircular are correct.""" + + assert self.test_shape.center_point == (0, 0) + assert self.test_shape.workplane == "ZY" + assert self.test_shape.rotation_axis == "Z" + assert self.test_shape.extrusion_start_offset == 1 + assert self.test_shape.stp_filename == "PortCutterCircular.stp" + assert self.test_shape.stl_filename == "PortCutterCircular.stl" + assert self.test_shape.name == "circular_port_cutter" + assert self.test_shape.material_tag == "circular_port_cutter_mat" + + def test_creation(self): + """Creates a circular port cutter using the PortCutterCircular parametric + component and checks that a cadquery solid is created.""" + + assert self.test_shape.solid is not None + assert self.test_shape.volume > 1000 + + def test_relative_volume(self): + """Creates PortCutterCircular shapes and checks that their relative volumes + are correct.""" + + test_volume = self.test_shape.volume + + self.test_shape.extrusion_start_offset = 20 + self.test_shape.azimuth_placement_angle = [0, 90, 180, 270] + + assert self.test_shape.volume == pytest.approx(test_volume * 4) + + def test_absolute_volume(self): + """Creates a PortCutterCircular shape and checks that its volume is correct.""" -# class test_component(unittest.TestCase): -# TODO: fix issue 548 -# def test_creation(self): -# """Checks a PortCutterCircular creation.""" + assert self.test_shape.volume == pytest.approx(math.pi * (20**2) * 300) -# test_component = paramak.PortCutterCircular( -# distance=3, -# z_pos=0.25, -# radius=0.1, -# azimuth_placement_angle=[0, 45, 90, 180] -# ) + self.test_shape.extrusion_start_offset = 20 + self.test_shape.azimuth_placement_angle = [0, 90, 180, 270] + self.test_shape.radius = 10 -# assert test_component.solid is not None + assert self.test_shape.volume == pytest.approx( + math.pi * (10**2) * 300 * 4) diff --git a/tests/test_parametric_components/test_PortCutterRectangular.py b/tests/test_parametric_components/test_PortCutterRectangular.py index 0f927ccc3..9b21016af 100644 --- a/tests/test_parametric_components/test_PortCutterRectangular.py +++ b/tests/test_parametric_components/test_PortCutterRectangular.py @@ -3,19 +3,55 @@ import paramak +import pytest + class TestPortCutterRectangular(unittest.TestCase): - def test_creation(self): - """Checks a PortCutterRectangular creation.""" - - test_component = paramak.PortCutterRectangular( - distance=3, - z_pos=0, - height=0.2, - width=0.4, - fillet_radius=0.02, - azimuth_placement_angle=[0, 45, 90, 180] + def setUp(self): + self.test_shape = paramak.PortCutterRectangular( + width=40, height=40, distance=300 ) - assert test_component.solid is not None + def test_default_parameters(self): + """Checks that the default parameters of a PortCutterRectangular are correct.""" + + assert self.test_shape.center_point == (0, 0) + assert self.test_shape.workplane == "ZY" + assert self.test_shape.rotation_axis == "Z" + assert self.test_shape.extrusion_start_offset == 1 + assert self.test_shape.fillet_radius is None + assert self.test_shape.stp_filename == "PortCutterRectangular.stp" + assert self.test_shape.stl_filename == "PortCutterRectangular.stl" + assert self.test_shape.name == "rectangular_port_cutter" + assert self.test_shape.material_tag == "rectangular_port_cutter_mat" + + def test_creation(self): + """Creates a rectangular port cutter using the PortCutterRectangular parametric + component and checks that a cadquery solid is created.""" + + assert self.test_shape.solid is not None + assert self.test_shape.volume > 1000 + + def test_relative_volume(self): + """Creates PortCutterRectangular shapes and checks that their relative volumes + are correct.""" + + test_volume = self.test_shape.volume + + self.test_shape.extrusion_start_offset = 20 + self.test_shape.azimuth_placement_angle = [0, 90, 180, 270] + + assert self.test_shape.volume == pytest.approx(test_volume * 4) + + def test_absolute_volume(self): + """Creates a PortCutterRectangular shape and checks that its volume is correct.""" + + assert self.test_shape.volume == pytest.approx(40 * 40 * 300) + + self.test_shape.extrusion_start_offset = 20 + self.test_shape.azimuth_placement_angle = [0, 90, 180, 270] + self.test_shape.width = 20 + self.test_shape.height = 20 + + assert self.test_shape.volume == pytest.approx(20 * 20 * 300 * 4)