symcad.parts.composite.HemisphericalCapsule

  1#!/usr/bin/env python3
  2# Copyright (C) 2022, Will Hedgecock
  3#
  4# This program is free software: you can redistribute it and/or modify
  5# it under the terms of the GNU General Public License as published by
  6# the Free Software Foundation, either version 3 of the License, or
  7# (at your option) any later version.
  8#
  9# This program is distributed in the hope that it will be useful,
 10# but WITHOUT ANY WARRANTY; without even the implied warranty of
 11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 12# GNU General Public License for more details.
 13#
 14# You should have received a copy of the GNU General Public License
 15# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 16
 17from __future__ import annotations
 18from PyFreeCAD.FreeCAD import FreeCAD, Part
 19from typing import Dict, Optional, Tuple, Union
 20from sympy import Expr, Symbol
 21from . import CompositeShape
 22import math
 23
 24class HemisphericalCapsule(CompositeShape):
 25   """Model representing a parameteric capsule with hemispherical endcaps.
 26
 27   By default, the capsule is oriented such that the endcaps are aligned with the x-axis:
 28
 29   ![HemisphericalCapsule](https://symbench.github.io/SymCAD/images/HemisphericalCapsule.png)
 30
 31   The `geometry` of this shape includes the following parameters:
 32
 33   - `cylinder_radius`: Outer radius (in `m`) of the center cylindrical part of the Capsule
 34   - `cylinder_length`: Length (in `m`) of the center cylindrical part of the Capsule
 35   - `cylinder_thickness`: Thickness (in `m`) of the cylindrical shell of the Capsule
 36   - `endcap_thickness`: Thickness (in `m`) of the hemispherical endcaps of the Capsule
 37
 38   Note that the above dimensions should be interpreted as if the capsule is unrotated. In other
 39   words, any shape rotation takes place *after* the capsule dimensions have been specified.
 40   """
 41
 42   # Constructor ----------------------------------------------------------------------------------
 43
 44   def __init__(self, identifier: str, material_density_kg_m3: Optional[float] = 1.0) -> None:
 45      """Initializes a parametric capsule object with hemispheric endcaps.
 46
 47      Parameters
 48      ----------
 49      identifier : `str`
 50         Unique identifying name for the object.
 51      material_density_kg_m3 : `float`, optional, default=1.0
 52         Uniform material density in `kg/m^3` to be used in mass property calculations.
 53      """
 54      super().__init__(identifier, self.__create_cad__, None, material_density_kg_m3)
 55      setattr(self.geometry, 'cylinder_radius', Symbol(self.name + '_cylinder_radius'))
 56      setattr(self.geometry, 'cylinder_length', Symbol(self.name + '_cylinder_length'))
 57      setattr(self.geometry, 'cylinder_thickness', Symbol(self.name + '_cylinder_thickness'))
 58      setattr(self.geometry, 'endcap_thickness', Symbol(self.name + '_endcap_thickness'))
 59
 60
 61   # CAD generation function ----------------------------------------------------------------------
 62
 63   @staticmethod
 64   def __create_cad__(params: Dict[str, float], fully_displace: bool) -> Part.Solid:
 65      """Scripted CAD generation method for `HemisphericalCapsule`."""
 66      cylinder_length_mm = 1000.0 * params['cylinder_length']
 67      outer_cylinder_radius_mm = 1000.0 * params['cylinder_radius']
 68      inner_cylinder_radius_mm = 1000.0 * (params['cylinder_radius'] - params['cylinder_thickness'])
 69      inner_endcap_radius_mm = 1000.0 * (params['cylinder_radius'] - params['endcap_thickness'])
 70      front = Part.makeSphere(outer_cylinder_radius_mm,
 71                              FreeCAD.Vector(0, 0, 0),
 72                              FreeCAD.Vector(-1, 0, 0), 0, 90, 360)
 73      rear = Part.makeSphere(outer_cylinder_radius_mm,
 74                             FreeCAD.Vector(0, 0, 0),
 75                             FreeCAD.Vector(1, 0, 0), 0, 90, 360)
 76      if fully_displace:
 77         pipe = Part.makeCylinder(outer_cylinder_radius_mm, cylinder_length_mm)
 78      else:
 79         pipe2d = Part.makeRuledSurface(Part.makeCircle(outer_cylinder_radius_mm),
 80                                        Part.makeCircle(inner_cylinder_radius_mm))
 81         pipe = pipe2d.extrude(FreeCAD.Vector(0, 0, cylinder_length_mm))
 82         inner_front = Part.makeSphere(inner_endcap_radius_mm,
 83                                       FreeCAD.Vector(0, 0, 0),
 84                                       FreeCAD.Vector(-1, 0, 0), 0, 90, 360)
 85         inner_rear = Part.makeSphere(inner_endcap_radius_mm,
 86                                      FreeCAD.Vector(0, 0, 0),
 87                                      FreeCAD.Vector(1, 0, 0), 0, 90, 360)
 88         front = front.cut(inner_front)
 89         rear = rear.cut(inner_rear)
 90      pipe.Placement = FreeCAD.Placement(FreeCAD.Vector(0, 0, 0),
 91                                         FreeCAD.Rotation(0, 90, 0))
 92      rear.Placement = \
 93         FreeCAD.Placement(FreeCAD.Vector(cylinder_length_mm, 0, 0),
 94                           FreeCAD.Rotation(0, 0, 0))
 95      return front.generalFuse([pipe, rear])[0]
 96
 97
 98   # Geometry setter ------------------------------------------------------------------------------
 99
100   def set_geometry(self, *, cylinder_radius_m: Union[float, None],
101                             cylinder_length_m: Union[float, None],
102                             cylinder_thickness_m: Union[float, None],
103                             endcap_thickness_m: Union[float, None]) -> HemisphericalCapsule:
104      """Sets the physical geometry of the current `HemisphericalCapsule` object.
105
106      See the `HemisphericalCapsule` class documentation for a description of each geometric
107      parameter.
108      """
109      self.geometry.set(cylinder_radius=cylinder_radius_m,
110                        cylinder_length=cylinder_length_m,
111                        cylinder_thickness=cylinder_thickness_m,
112                        endcap_thickness=endcap_thickness_m)
113      return self
114
115   def get_geometric_parameter_bounds(self, parameter: str) -> Tuple[float, float]:
116      parameter_bounds = {
117         'cylinder_radius': (0.01, 2.0),
118         'cylinder_length': (0.01, 2.0),
119         'cylinder_thickness': (0.001, 0.05),
120         'endcap_thickness': (0.001, 0.05)
121      }
122      return parameter_bounds.get(parameter, (0.0, 0.0))
123
124
125   # Geometric properties -------------------------------------------------------------------------
126
127   @property
128   def material_volume(self) -> Union[float, Expr]:
129      volume = self.displaced_volume
130      volume -= (math.pi
131                 * (self.geometry.cylinder_radius - self.geometry.cylinder_thickness)**2
132                 * self.geometry.cylinder_length)
133      volume -= ((4.0 * math.pi / 3.0) * (self.geometry.cylinder_radius
134                 - self.geometry.endcap_thickness)**3)
135      return volume
136
137   @property
138   def displaced_volume(self) -> Union[float, Expr]:
139      return (math.pi * self.geometry.cylinder_radius**2 * self.geometry.cylinder_length) + \
140             ((4.0 * math.pi / 3.0) * self.geometry.cylinder_radius**3)
141
142   @property
143   def surface_area(self) -> Union[float, Expr]:
144      return (2.0 * math.pi * self.geometry.cylinder_radius * self.geometry.cylinder_length) + \
145             (4.0 * math.pi * self.geometry.cylinder_radius**2)
146
147   @property
148   def unoriented_center_of_gravity(self) -> Tuple[Union[float, Expr],
149                                                   Union[float, Expr],
150                                                   Union[float, Expr]]:
151      return (0.5 * self.unoriented_length,
152              0.5 * self.unoriented_width,
153              0.5 * self.unoriented_height)
154
155   @property
156   def unoriented_center_of_buoyancy(self) -> Tuple[Union[float, Expr],
157                                                    Union[float, Expr],
158                                                    Union[float, Expr]]:
159      return self.unoriented_center_of_gravity
160
161   @property
162   def unoriented_length(self) -> Union[float, Expr]:
163      return (2.0 * self.geometry.cylinder_radius) + self.geometry.cylinder_length
164
165   @property
166   def unoriented_width(self) -> Union[float, Expr]:
167      return 2.0 * self.geometry.cylinder_radius
168
169   @property
170   def unoriented_height(self) -> Union[float, Expr]:
171      return self.unoriented_width
172
173   @property
174   def oriented_length(self) -> Union[float, Expr]:
175      # TODO: Implement this
176      return 0
177
178   @property
179   def oriented_width(self) -> Union[float, Expr]:
180      # TODO: Implement this
181      return 0
182
183   @property
184   def oriented_height(self) -> Union[float, Expr]:
185      # TODO: Implement this
186      return 0
class HemisphericalCapsule(symcad.parts.composite.CompositeShape):
 25class HemisphericalCapsule(CompositeShape):
 26   """Model representing a parameteric capsule with hemispherical endcaps.
 27
 28   By default, the capsule is oriented such that the endcaps are aligned with the x-axis:
 29
 30   ![HemisphericalCapsule](https://symbench.github.io/SymCAD/images/HemisphericalCapsule.png)
 31
 32   The `geometry` of this shape includes the following parameters:
 33
 34   - `cylinder_radius`: Outer radius (in `m`) of the center cylindrical part of the Capsule
 35   - `cylinder_length`: Length (in `m`) of the center cylindrical part of the Capsule
 36   - `cylinder_thickness`: Thickness (in `m`) of the cylindrical shell of the Capsule
 37   - `endcap_thickness`: Thickness (in `m`) of the hemispherical endcaps of the Capsule
 38
 39   Note that the above dimensions should be interpreted as if the capsule is unrotated. In other
 40   words, any shape rotation takes place *after* the capsule dimensions have been specified.
 41   """
 42
 43   # Constructor ----------------------------------------------------------------------------------
 44
 45   def __init__(self, identifier: str, material_density_kg_m3: Optional[float] = 1.0) -> None:
 46      """Initializes a parametric capsule object with hemispheric endcaps.
 47
 48      Parameters
 49      ----------
 50      identifier : `str`
 51         Unique identifying name for the object.
 52      material_density_kg_m3 : `float`, optional, default=1.0
 53         Uniform material density in `kg/m^3` to be used in mass property calculations.
 54      """
 55      super().__init__(identifier, self.__create_cad__, None, material_density_kg_m3)
 56      setattr(self.geometry, 'cylinder_radius', Symbol(self.name + '_cylinder_radius'))
 57      setattr(self.geometry, 'cylinder_length', Symbol(self.name + '_cylinder_length'))
 58      setattr(self.geometry, 'cylinder_thickness', Symbol(self.name + '_cylinder_thickness'))
 59      setattr(self.geometry, 'endcap_thickness', Symbol(self.name + '_endcap_thickness'))
 60
 61
 62   # CAD generation function ----------------------------------------------------------------------
 63
 64   @staticmethod
 65   def __create_cad__(params: Dict[str, float], fully_displace: bool) -> Part.Solid:
 66      """Scripted CAD generation method for `HemisphericalCapsule`."""
 67      cylinder_length_mm = 1000.0 * params['cylinder_length']
 68      outer_cylinder_radius_mm = 1000.0 * params['cylinder_radius']
 69      inner_cylinder_radius_mm = 1000.0 * (params['cylinder_radius'] - params['cylinder_thickness'])
 70      inner_endcap_radius_mm = 1000.0 * (params['cylinder_radius'] - params['endcap_thickness'])
 71      front = Part.makeSphere(outer_cylinder_radius_mm,
 72                              FreeCAD.Vector(0, 0, 0),
 73                              FreeCAD.Vector(-1, 0, 0), 0, 90, 360)
 74      rear = Part.makeSphere(outer_cylinder_radius_mm,
 75                             FreeCAD.Vector(0, 0, 0),
 76                             FreeCAD.Vector(1, 0, 0), 0, 90, 360)
 77      if fully_displace:
 78         pipe = Part.makeCylinder(outer_cylinder_radius_mm, cylinder_length_mm)
 79      else:
 80         pipe2d = Part.makeRuledSurface(Part.makeCircle(outer_cylinder_radius_mm),
 81                                        Part.makeCircle(inner_cylinder_radius_mm))
 82         pipe = pipe2d.extrude(FreeCAD.Vector(0, 0, cylinder_length_mm))
 83         inner_front = Part.makeSphere(inner_endcap_radius_mm,
 84                                       FreeCAD.Vector(0, 0, 0),
 85                                       FreeCAD.Vector(-1, 0, 0), 0, 90, 360)
 86         inner_rear = Part.makeSphere(inner_endcap_radius_mm,
 87                                      FreeCAD.Vector(0, 0, 0),
 88                                      FreeCAD.Vector(1, 0, 0), 0, 90, 360)
 89         front = front.cut(inner_front)
 90         rear = rear.cut(inner_rear)
 91      pipe.Placement = FreeCAD.Placement(FreeCAD.Vector(0, 0, 0),
 92                                         FreeCAD.Rotation(0, 90, 0))
 93      rear.Placement = \
 94         FreeCAD.Placement(FreeCAD.Vector(cylinder_length_mm, 0, 0),
 95                           FreeCAD.Rotation(0, 0, 0))
 96      return front.generalFuse([pipe, rear])[0]
 97
 98
 99   # Geometry setter ------------------------------------------------------------------------------
100
101   def set_geometry(self, *, cylinder_radius_m: Union[float, None],
102                             cylinder_length_m: Union[float, None],
103                             cylinder_thickness_m: Union[float, None],
104                             endcap_thickness_m: Union[float, None]) -> HemisphericalCapsule:
105      """Sets the physical geometry of the current `HemisphericalCapsule` object.
106
107      See the `HemisphericalCapsule` class documentation for a description of each geometric
108      parameter.
109      """
110      self.geometry.set(cylinder_radius=cylinder_radius_m,
111                        cylinder_length=cylinder_length_m,
112                        cylinder_thickness=cylinder_thickness_m,
113                        endcap_thickness=endcap_thickness_m)
114      return self
115
116   def get_geometric_parameter_bounds(self, parameter: str) -> Tuple[float, float]:
117      parameter_bounds = {
118         'cylinder_radius': (0.01, 2.0),
119         'cylinder_length': (0.01, 2.0),
120         'cylinder_thickness': (0.001, 0.05),
121         'endcap_thickness': (0.001, 0.05)
122      }
123      return parameter_bounds.get(parameter, (0.0, 0.0))
124
125
126   # Geometric properties -------------------------------------------------------------------------
127
128   @property
129   def material_volume(self) -> Union[float, Expr]:
130      volume = self.displaced_volume
131      volume -= (math.pi
132                 * (self.geometry.cylinder_radius - self.geometry.cylinder_thickness)**2
133                 * self.geometry.cylinder_length)
134      volume -= ((4.0 * math.pi / 3.0) * (self.geometry.cylinder_radius
135                 - self.geometry.endcap_thickness)**3)
136      return volume
137
138   @property
139   def displaced_volume(self) -> Union[float, Expr]:
140      return (math.pi * self.geometry.cylinder_radius**2 * self.geometry.cylinder_length) + \
141             ((4.0 * math.pi / 3.0) * self.geometry.cylinder_radius**3)
142
143   @property
144   def surface_area(self) -> Union[float, Expr]:
145      return (2.0 * math.pi * self.geometry.cylinder_radius * self.geometry.cylinder_length) + \
146             (4.0 * math.pi * self.geometry.cylinder_radius**2)
147
148   @property
149   def unoriented_center_of_gravity(self) -> Tuple[Union[float, Expr],
150                                                   Union[float, Expr],
151                                                   Union[float, Expr]]:
152      return (0.5 * self.unoriented_length,
153              0.5 * self.unoriented_width,
154              0.5 * self.unoriented_height)
155
156   @property
157   def unoriented_center_of_buoyancy(self) -> Tuple[Union[float, Expr],
158                                                    Union[float, Expr],
159                                                    Union[float, Expr]]:
160      return self.unoriented_center_of_gravity
161
162   @property
163   def unoriented_length(self) -> Union[float, Expr]:
164      return (2.0 * self.geometry.cylinder_radius) + self.geometry.cylinder_length
165
166   @property
167   def unoriented_width(self) -> Union[float, Expr]:
168      return 2.0 * self.geometry.cylinder_radius
169
170   @property
171   def unoriented_height(self) -> Union[float, Expr]:
172      return self.unoriented_width
173
174   @property
175   def oriented_length(self) -> Union[float, Expr]:
176      # TODO: Implement this
177      return 0
178
179   @property
180   def oriented_width(self) -> Union[float, Expr]:
181      # TODO: Implement this
182      return 0
183
184   @property
185   def oriented_height(self) -> Union[float, Expr]:
186      # TODO: Implement this
187      return 0

Model representing a parameteric capsule with hemispherical endcaps.

By default, the capsule is oriented such that the endcaps are aligned with the x-axis:

HemisphericalCapsule

The geometry of this shape includes the following parameters:

  • cylinder_radius: Outer radius (in m) of the center cylindrical part of the Capsule
  • cylinder_length: Length (in m) of the center cylindrical part of the Capsule
  • cylinder_thickness: Thickness (in m) of the cylindrical shell of the Capsule
  • endcap_thickness: Thickness (in m) of the hemispherical endcaps of the Capsule

Note that the above dimensions should be interpreted as if the capsule is unrotated. In other words, any shape rotation takes place after the capsule dimensions have been specified.

HemisphericalCapsule(identifier: str, material_density_kg_m3: Optional[float] = 1.0)
45   def __init__(self, identifier: str, material_density_kg_m3: Optional[float] = 1.0) -> None:
46      """Initializes a parametric capsule object with hemispheric endcaps.
47
48      Parameters
49      ----------
50      identifier : `str`
51         Unique identifying name for the object.
52      material_density_kg_m3 : `float`, optional, default=1.0
53         Uniform material density in `kg/m^3` to be used in mass property calculations.
54      """
55      super().__init__(identifier, self.__create_cad__, None, material_density_kg_m3)
56      setattr(self.geometry, 'cylinder_radius', Symbol(self.name + '_cylinder_radius'))
57      setattr(self.geometry, 'cylinder_length', Symbol(self.name + '_cylinder_length'))
58      setattr(self.geometry, 'cylinder_thickness', Symbol(self.name + '_cylinder_thickness'))
59      setattr(self.geometry, 'endcap_thickness', Symbol(self.name + '_endcap_thickness'))

Initializes a parametric capsule object with hemispheric endcaps.

Parameters
  • identifier (str): Unique identifying name for the object.
  • material_density_kg_m3 (float, optional, default=1.0): Uniform material density in kg/m^3 to be used in mass property calculations.
def set_geometry( self, *, cylinder_radius_m: Optional[float], cylinder_length_m: Optional[float], cylinder_thickness_m: Optional[float], endcap_thickness_m: Optional[float]) -> HemisphericalCapsule:
101   def set_geometry(self, *, cylinder_radius_m: Union[float, None],
102                             cylinder_length_m: Union[float, None],
103                             cylinder_thickness_m: Union[float, None],
104                             endcap_thickness_m: Union[float, None]) -> HemisphericalCapsule:
105      """Sets the physical geometry of the current `HemisphericalCapsule` object.
106
107      See the `HemisphericalCapsule` class documentation for a description of each geometric
108      parameter.
109      """
110      self.geometry.set(cylinder_radius=cylinder_radius_m,
111                        cylinder_length=cylinder_length_m,
112                        cylinder_thickness=cylinder_thickness_m,
113                        endcap_thickness=endcap_thickness_m)
114      return self

Sets the physical geometry of the current HemisphericalCapsule object.

See the HemisphericalCapsule class documentation for a description of each geometric parameter.

def get_geometric_parameter_bounds(self, parameter: str) -> Tuple[float, float]:
116   def get_geometric_parameter_bounds(self, parameter: str) -> Tuple[float, float]:
117      parameter_bounds = {
118         'cylinder_radius': (0.01, 2.0),
119         'cylinder_length': (0.01, 2.0),
120         'cylinder_thickness': (0.001, 0.05),
121         'endcap_thickness': (0.001, 0.05)
122      }
123      return parameter_bounds.get(parameter, (0.0, 0.0))

Abstract method that must be overridden by a concrete SymPart class to return the minimum and maximum expected bounds for a given geometric parameter.

Parameters
  • parameter (str): Name of the geometric parameter for which to return the minimum and maximum bounds.
Returns
  • Tuple[float, float]: Minimum and maximum bounds for the specified geometric parameter.
material_volume: Union[float, sympy.core.expr.Expr]

Material volume (in m^3) of the SymPart (read-only).

displaced_volume: Union[float, sympy.core.expr.Expr]

Displaced volume (in m^3) of the SymPart (read-only).

surface_area: Union[float, sympy.core.expr.Expr]

Surface/wetted area (in m^2) of the SymPart (read-only).

unoriented_center_of_gravity: Tuple[Union[float, sympy.core.expr.Expr], Union[float, sympy.core.expr.Expr], Union[float, sympy.core.expr.Expr]]

Center of gravity (in m) of the unoriented SymPart (read-only).

unoriented_center_of_buoyancy: Tuple[Union[float, sympy.core.expr.Expr], Union[float, sympy.core.expr.Expr], Union[float, sympy.core.expr.Expr]]

Center of buoyancy (in m) of the unoriented SymPart (read-only).

unoriented_length: Union[float, sympy.core.expr.Expr]

X-axis length (in m) of the bounding box of the unoriented SymPart (read-only).

unoriented_width: Union[float, sympy.core.expr.Expr]

Y-axis width (in m) of the bounding box of the unoriented SymPart (read-only).

unoriented_height: Union[float, sympy.core.expr.Expr]

Z-axis height (in m) of the bounding box of the unoriented SymPart (read-only).

oriented_length: Union[float, sympy.core.expr.Expr]

X-axis length (in m) of the bounding box of the oriented SymPart (read-only).

oriented_width: Union[float, sympy.core.expr.Expr]

Y-axis length (in m) of the bounding box of the oriented SymPart (read-only).

oriented_height: Union[float, sympy.core.expr.Expr]

Z-axis length (in m) of the bounding box of the oriented SymPart (read-only).