diff --git a/examples/example_parametric_components/make_all_parametric_components.py b/examples/example_parametric_components/make_all_parametric_components.py index 5348f4fec..14fd31c91 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/parametric_components/port_cutters_circular.py b/paramak/parametric_components/port_cutters_circular.py index b6aa508d0..096f26181 100644 --- a/paramak/parametric_components/port_cutters_circular.py +++ b/paramak/parametric_components/port_cutters_circular.py @@ -7,10 +7,9 @@ class PortCutterCircular(ExtrudeCircleShape): 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 ((float, float)): Defaults to (0, 0). + radius (float): radius (cm) of 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". @@ -21,9 +20,9 @@ class PortCutterCircular(ExtrudeCircleShape): def __init__( self, - z_pos, radius, distance, + center_point=(0, 0), workplane="ZY", rotation_axis="Z", extrusion_start_offset=1., @@ -47,8 +46,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..8932d49b6 100644 --- a/paramak/parametric_components/port_cutters_rectangular.py +++ b/paramak/parametric_components/port_cutters_rectangular.py @@ -7,10 +7,10 @@ class PortCutterRectangular(ExtrudeStraightShape): 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 ((float, float)): Defaults to (0, 0). + height (float): height (cm) of the port. + width (float): width (cm) of the port. + distance (float): extruded distance (cm) of the 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,10 +24,10 @@ class PortCutterRectangular(ExtrudeStraightShape): def __init__( self, - z_pos, height, width, distance, + center_point=(0, 0), workplane="ZY", rotation_axis="Z", extrusion_start_offset=1., @@ -52,11 +52,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 +65,17 @@ 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..4469ffa80 100644 --- a/paramak/parametric_reactors/ball_reactor.py +++ b/paramak/parametric_reactors/ball_reactor.py @@ -1,8 +1,11 @@ +import numpy as np import warnings import paramak +from paramak.utils import perform_port_cutting + class BallReactor(paramak.Reactor): """Creates geometry for a simple ball reactor including a plasma, @@ -61,6 +64,26 @@ class BallReactor(paramak.Reactor): "upper", "lower" or "both". Defaults to "both". rotation_angle (float): the angle of the sector that is desired. Defaults to 360.0. + port_type (str, optional): type of port to be cut. Defaults to None. + port_center_point ((float, float), optional): position of port center + point in the workplane given. Defaults to (0, 0). + port_radius (float, optional): radius of circular ports. Defaults to + None. + port_height (float, optional): height of rectangular ports. Defaults + to None. + port_width (float, optional): width of rectangular ports. Defaults to + None. + port_distance (float, optional): extrusion distance of port cutter. + Defaults to None. + port_azimuth_placement_angle (float or list of floats, optional): + azimuthal placement of each port. Defualts 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 (float, optional): extrusion start point of port + cutter. Defaults to major_radius. + port_fillet_radius (float, optional): fillet radius of rectangular + ports. Defaults to 0. """ def __init__( @@ -88,6 +111,16 @@ def __init__( outboard_tf_coil_poloidal_thickness=None, divertor_position="both", rotation_angle=360.0, + port_type=None, + port_center_point=(0, 0), + port_radius=None, + port_height=None, + port_width=None, + port_distance=None, + port_azimuth_placement_angle=None, + port_start_radius=None, + port_fillet_radius=0, + **kwargs ): super().__init__([]) @@ -152,6 +185,27 @@ def __init__( self.plasma_gap_vertical_thickness, self.major_radius - self.minor_radius] + 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_start_radius = port_start_radius + self.port_azimuth_placement_angle = port_azimuth_placement_angle + 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 + @property def pf_coil_radial_thicknesses(self): return self._pf_coil_radial_thicknesses @@ -423,6 +477,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/submersion_reactor.py b/paramak/parametric_reactors/submersion_reactor.py index 4496f05fb..7a3875d96 100644 --- a/paramak/parametric_reactors/submersion_reactor.py +++ b/paramak/parametric_reactors/submersion_reactor.py @@ -4,6 +4,8 @@ import cadquery as cq import paramak +from paramak.utils import perform_port_cutting + class SubmersionTokamak(paramak.Reactor): """Creates geometry for a simple submersion reactor including a plasma, @@ -61,6 +63,27 @@ class SubmersionTokamak(paramak.Reactor): "upper", "lower" or "both". Defaults to "both". support_position (str, optional): the position of the supports, "upper", "lower" or "both". Defaults to "both". + port_type (str, optional): type of port to be cut. Defaults to None. + port_center_point ((float, float), optional): position of port center + point in the workplane given. Defaults to (0, 0). + port_radius (float, optional): radius of circular ports. Defaults to + None. + port_height (float, optional): height of rectangular ports. Defaults + to None. + port_width (float, optional): width of rectangular ports. Defaults to + None. + port_distance (float, optional): extrusion distance of port cutter. + Defaults to None. + port_azimuth_placement_angle (float or list of floats, optional): + azimuthal placement of each port. Defualts 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 (float, optional): extrusion start point of port + cutter. Defaults to None if no ports are created. Defaults to + major_radius otherwise. + port_fillet_radius (float, optional): fillet radius of rectangular + ports. Defaults to 0. """ def __init__( @@ -90,6 +113,16 @@ def __init__( pf_coil_case_thickness=10, divertor_position="both", support_position="both", + port_type=None, + port_center_point=(0, 0), + port_radius=None, + port_height=None, + port_width=None, + port_distance=None, + port_azimuth_placement_angle=None, + port_start_radius=None, + port_fillet_radius=0, + **kwargs ): super().__init__([]) @@ -147,6 +180,27 @@ def __init__( (outer_equatorial_point + inner_equatorial_point) / 2 self.minor_radius = self.major_radius - inner_equatorial_point + 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_start_radius = port_start_radius + self.port_azimuth_placement_angle = port_azimuth_placement_angle + 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 + @property def pf_coil_radial_thicknesses(self): return self._pf_coil_radial_thicknesses @@ -446,6 +500,7 @@ def _make_firstwall(self): material_tag="firstwall_mat", union=self._inboard_firstwall, ) + self._firstwall = perform_port_cutting(self, self._firstwall) return self._firstwall def _make_divertor(self): @@ -495,8 +550,10 @@ def _make_divertor(self): name="divertor", material_tag="divertor_mat" ) - - self._firstwall.cut = self._divertor + if self._firstwall.cut is None: + self._firstwall.cut = [self._divertor] + else: + self._firstwall.cut = self._firstwall.cut + [self._divertor] self._inboard_firstwall.cut = self._divertor return self._divertor @@ -531,6 +588,7 @@ def _make_blanket(self): material_tag="blanket_mat", union=self._inboard_blanket, ) + self._blanket = perform_port_cutting(self, self._blanket) return self._blanket def _make_supports(self): @@ -567,7 +625,10 @@ def _make_supports(self): material_tag="supports_mat", intersect=blanket_enveloppe, ) - self._blanket.cut = self._supports + if self._blanket.cut is None: + self._blanket.cut = [self._supports] + else: + self._blanket.cut = self._blanket.cut + [self._supports] return self._supports @@ -634,6 +695,9 @@ def _make_rear_blanket_wall(self): self._outboard_rear_blanket_wall_lower], ) + self._outboard_rear_blanket_wall = perform_port_cutting( + self, self._outboard_rear_blanket_wall) + return self._outboard_rear_blanket_wall def _make_coils(self): diff --git a/paramak/parametric_shapes/extruded_mixed_shape.py b/paramak/parametric_shapes/extruded_mixed_shape.py index fa1cb9a7d..b4aca24ad 100644 --- a/paramak/parametric_shapes/extruded_mixed_shape.py +++ b/paramak/parametric_shapes/extruded_mixed_shape.py @@ -84,6 +84,11 @@ def create_solid(self): distance=extrusion_distance, both=self.extrude_both) + # used for 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 4ba232328..17724fda3 100644 --- a/paramak/utils.py +++ b/paramak/utils.py @@ -208,6 +208,65 @@ def calculate_wedge_cut(self): return cutting_wedge +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 + + def add_thickness(x, y, thickness, dy_dx=None): """Computes outer curve points based on thickness diff --git a/tests/test_parametric_components/test_PortCutterCircular.py b/tests/test_parametric_components/test_PortCutterCircular.py index a6d5e7197..efc1ea607 100644 --- a/tests/test_parametric_components/test_PortCutterCircular.py +++ b/tests/test_parametric_components/test_PortCutterCircular.py @@ -1,19 +1,57 @@ +import math import unittest import paramak +import pytest -# class test_component(unittest.TestCase): -# TODO: fix issue 548 -# def test_creation(self): -# """Checks a PortCutterCircular creation.""" -# test_component = paramak.PortCutterCircular( -# distance=3, -# z_pos=0.25, -# radius=0.1, -# azimuth_placement_angle=[0, 45, 90, 180] -# ) +class test_component(unittest.TestCase): -# assert test_component.solid is not None + 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.""" + + assert self.test_shape.volume == pytest.approx(math.pi * (20**2) * 300) + + self.test_shape.extrusion_start_offset = 20 + self.test_shape.azimuth_placement_angle = [0, 90, 180, 270] + self.test_shape.radius = 10 + + 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) diff --git a/tests/test_parametric_components/test_VacuumVessel.py b/tests/test_parametric_components/test_VacuumVessel.py index 7026be32d..245d4ab4b 100644 --- a/tests/test_parametric_components/test_VacuumVessel.py +++ b/tests/test_parametric_components/test_VacuumVessel.py @@ -22,15 +22,33 @@ def test_ports(self): caquery solid is created.""" cutter1 = paramak.PortCutterRectangular( - distance=3, z_pos=0, height=0.2, width=0.4, fillet_radius=0.01) + center_point=( + 0, + 0), + distance=3, + extrusion_start_offset=0, + height=0.2, + width=0.4, + fillet_radius=0.01) cutter2 = paramak.PortCutterRectangular( - distance=3, z_pos=0.5, height=0.2, width=0.4, fillet_radius=0.00) + center_point=( + 0.5, + 0), + distance=3, + extrusion_start_offset=0, + height=0.2, + width=0.4, + fillet_radius=0.00) cutter3 = paramak.PortCutterRectangular( - distance=3, z_pos=-0.5, height=0.2, width=0.4, - physical_groups=None) + distance=3, center_point=(-0.5, 0), height=0.2, width=0.4) cutter4 = paramak.PortCutterCircular( - distance=3, z_pos=0.25, radius=0.1, azimuth_placement_angle=45, - physical_groups=None) + center_point=( + 0.25, + 0), + distance=3, + extrusion_start_offset=0, + radius=0.1, + azimuth_placement_angle=45) cutter5 = paramak.PortCutterRotated( (0, 0), azimuth_placement_angle=-90, rotation_angle=10, fillet_radius=0.01, physical_groups=None)