symcad.parts.endcaps.ConicalFrustrum

  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 Part
 19from typing import Dict, Optional, Tuple, Union
 20from sympy import Expr, Symbol, sqrt, atan2, sin, tan
 21from . import EndcapShape
 22import math
 23
 24class ConicalFrustrum(EndcapShape):
 25   """Model representing a hollow, parametric, conical frustrum.
 26
 27   By default, the frustrum is oriented such that its base is perpendicular to the z-axis:
 28
 29   ![ConicalFrustrum](https://symbench.github.io/SymCAD/images/ConicalFrustrum.png)
 30
 31   The `geometry` of this shape includes the following parameters:
 32
 33   - `bottom_radius`: Radius (in `m`) of the base of the ConicalFrustrum
 34                      (must be larger than `top_radius`)
 35   - `top_radius`: Radius (in `m`) of the tip of the ConicalFrustrum
 36   - `height`: Height (in `m`) of the ConicalFrustrum from base to tip
 37   - `thickness`: Thickness (in `m`) of the shell of the ConicalFrustrum
 38
 39   Note that the above dimensions should be interpreted as if the ConicalFrustrum is unrotated.
 40   In other words, any shape rotation takes place *after* the ConicalFrustrum dimensions have been
 41   specified.
 42   """
 43
 44   # Constructor ----------------------------------------------------------------------------------
 45
 46   def __init__(self, identifier: str, material_density_kg_m3: Optional[float] = 1.0) -> None:
 47      """Initializes a hollow, parametric, conical frustrum object.
 48
 49      Parameters
 50      ----------
 51      identifier : `str`
 52         Unique identifying name for the object.
 53      material_density_kg_m3 : `float`, optional, default=1.0
 54         Uniform material density in `kg/m^3` to be used in mass property calculations.
 55      """
 56      super().__init__(identifier, self.__create_cad__, None, material_density_kg_m3)
 57      setattr(self.geometry, 'bottom_radius', Symbol(self.name + '_bottom_radius'))
 58      setattr(self.geometry, 'top_radius', Symbol(self.name + '_top_radius'))
 59      setattr(self.geometry, 'thickness', Symbol(self.name + '_thickness'))
 60      setattr(self.geometry, 'height', Symbol(self.name + '_height'))
 61
 62
 63   # CAD generation function ----------------------------------------------------------------------
 64
 65   @staticmethod
 66   def __create_cad__(params: Dict[str, float], fully_displace: bool) -> Part.Solid:
 67      """Scripted CAD generation method for a `ConicalFrustrum`."""
 68      height_angle = math.atan2(params['height'], params['bottom_radius'] - params['top_radius'])
 69      thickness_mm = 1000.0 * params['thickness']
 70      outer_height_mm = 1000.0 * params['height']
 71      outer_bottom_radius_mm = 1000.0 * params['bottom_radius']
 72      outer_top_radius_mm = 1000.0 * params['top_radius']
 73      inner_height_mm = outer_height_mm - thickness_mm
 74      inner_bottom_radius_mm = outer_bottom_radius_mm - (thickness_mm / math.sin(height_angle))
 75      inner_top_radius_mm = inner_bottom_radius_mm - (inner_height_mm / math.tan(height_angle))
 76      outer = Part.makeCone(outer_bottom_radius_mm, outer_top_radius_mm, outer_height_mm)
 77      if not fully_displace:
 78         inner = Part.makeCone(inner_bottom_radius_mm, inner_top_radius_mm, inner_height_mm)
 79         return outer.cut(inner)
 80      else:
 81         return outer
 82
 83
 84   # Geometry setter ------------------------------------------------------------------------------
 85
 86   def set_geometry(self, *, bottom_radius_m: Union[float, None],
 87                             top_radius_m: Union[float, None],
 88                             height_m: Union[float, None],
 89                             thickness_m: Union[float, None]) -> ConicalFrustrum:
 90      """Sets the physical geometry of the current `ConicalFrustrum` object.
 91
 92      See the `ConicalFrustrum` class documentation for a description of each geometric parameter.
 93      """
 94      self.geometry.set(bottom_radius=bottom_radius_m,
 95                        top_radius=top_radius_m,
 96                        height=height_m,
 97                        thickness=thickness_m)
 98      return self
 99
100   def get_geometric_parameter_bounds(self, parameter: str) -> Tuple[float, float]:
101      parameter_bounds = {
102         'bottom_radius': (0.0, 2.0),
103         'top_radius': (0.0, 2.0),
104         'thickness': (0.0, 0.05),
105         'height': (0.0, 2.0)
106      }
107      return parameter_bounds.get(parameter, (0.0, 0.0))
108
109
110   # Geometric properties -------------------------------------------------------------------------
111
112   @property
113   def material_volume(self) -> Union[float, Expr]:
114      volume = self.displaced_volume
115      height_angle = atan2(self.geometry.height,
116                           self.geometry.bottom_radius - self.geometry.top_radius)
117      inner_height = self.geometry.height - self.geometry.thickness
118      inner_bottom_radius = self.geometry.bottom_radius - \
119                            (self.geometry.thickness / sin(height_angle))
120      inner_top_radius = inner_bottom_radius - (inner_height / tan(height_angle))
121      volume -= ((inner_height * math.pi / 3.0) * \
122                 (inner_bottom_radius**2 + inner_top_radius**2 +
123                    (inner_bottom_radius * inner_top_radius)))
124      return volume
125
126   @property
127   def displaced_volume(self) -> Union[float, Expr]:
128      return (self.geometry.height * math.pi / 3.0) * \
129             (self.geometry.bottom_radius**2 + self.geometry.top_radius**2 +
130                (self.geometry.bottom_radius * self.geometry.top_radius))
131
132   @property
133   def surface_area(self) -> Union[float, Expr]:
134      return (math.pi * self.geometry.top_radius**2) + \
135             (math.pi * (self.geometry.bottom_radius + self.geometry.top_radius) *
136              sqrt(self.geometry.height**2 +
137                         (self.geometry.bottom_radius - self.geometry.top_radius)**2))
138
139   @property
140   def unoriented_center_of_gravity(self) -> Tuple[Union[float, Expr],
141                                                   Union[float, Expr],
142                                                   Union[float, Expr]]:
143      return (self.geometry.bottom_radius,
144              self.geometry.bottom_radius,
145              (self.geometry.height *
146                 (self.geometry.bottom_radius +
147                    (0.5 * self.geometry.top_radius**2) +
148                    (2.0 * self.geometry.top_radius))) /
149              (3.0 * (self.geometry.bottom_radius + self.geometry.top_radius)))
150
151   @property
152   def unoriented_center_of_buoyancy(self) -> Tuple[Union[float, Expr],
153                                                    Union[float, Expr],
154                                                    Union[float, Expr]]:
155      return (self.geometry.bottom_radius,
156              self.geometry.bottom_radius,
157              (self.geometry.height * (self.geometry.bottom_radius**2 +
158                 (2.0 * self.geometry.bottom_radius * self.geometry.top_radius) +
159                 (3.0 * self.geometry.top_radius**2))) /
160              (4.0 * (self.geometry.bottom_radius**2 +
161                 (self.geometry.bottom_radius * self.geometry.top_radius) +
162                 self.geometry.top_radius**2)))
163
164   @property
165   def unoriented_length(self) -> Union[float, Expr]:
166      return 2.0 * self.geometry.bottom_radius
167
168   @property
169   def unoriented_width(self) -> Union[float, Expr]:
170      return self.unoriented_length
171
172   @property
173   def unoriented_height(self) -> Union[float, Expr]:
174      return self.geometry.height
175
176   @property
177   def oriented_length(self) -> Union[float, Expr]:
178      # TODO: Implement this
179      return 0
180
181   @property
182   def oriented_width(self) -> Union[float, Expr]:
183      # TODO: Implement this
184      return 0
185
186   @property
187   def oriented_height(self) -> Union[float, Expr]:
188      # TODO: Implement this
189      return 0
class ConicalFrustrum(symcad.parts.endcaps.EndcapShape):
 25class ConicalFrustrum(EndcapShape):
 26   """Model representing a hollow, parametric, conical frustrum.
 27
 28   By default, the frustrum is oriented such that its base is perpendicular to the z-axis:
 29
 30   ![ConicalFrustrum](https://symbench.github.io/SymCAD/images/ConicalFrustrum.png)
 31
 32   The `geometry` of this shape includes the following parameters:
 33
 34   - `bottom_radius`: Radius (in `m`) of the base of the ConicalFrustrum
 35                      (must be larger than `top_radius`)
 36   - `top_radius`: Radius (in `m`) of the tip of the ConicalFrustrum
 37   - `height`: Height (in `m`) of the ConicalFrustrum from base to tip
 38   - `thickness`: Thickness (in `m`) of the shell of the ConicalFrustrum
 39
 40   Note that the above dimensions should be interpreted as if the ConicalFrustrum is unrotated.
 41   In other words, any shape rotation takes place *after* the ConicalFrustrum dimensions have been
 42   specified.
 43   """
 44
 45   # Constructor ----------------------------------------------------------------------------------
 46
 47   def __init__(self, identifier: str, material_density_kg_m3: Optional[float] = 1.0) -> None:
 48      """Initializes a hollow, parametric, conical frustrum object.
 49
 50      Parameters
 51      ----------
 52      identifier : `str`
 53         Unique identifying name for the object.
 54      material_density_kg_m3 : `float`, optional, default=1.0
 55         Uniform material density in `kg/m^3` to be used in mass property calculations.
 56      """
 57      super().__init__(identifier, self.__create_cad__, None, material_density_kg_m3)
 58      setattr(self.geometry, 'bottom_radius', Symbol(self.name + '_bottom_radius'))
 59      setattr(self.geometry, 'top_radius', Symbol(self.name + '_top_radius'))
 60      setattr(self.geometry, 'thickness', Symbol(self.name + '_thickness'))
 61      setattr(self.geometry, 'height', Symbol(self.name + '_height'))
 62
 63
 64   # CAD generation function ----------------------------------------------------------------------
 65
 66   @staticmethod
 67   def __create_cad__(params: Dict[str, float], fully_displace: bool) -> Part.Solid:
 68      """Scripted CAD generation method for a `ConicalFrustrum`."""
 69      height_angle = math.atan2(params['height'], params['bottom_radius'] - params['top_radius'])
 70      thickness_mm = 1000.0 * params['thickness']
 71      outer_height_mm = 1000.0 * params['height']
 72      outer_bottom_radius_mm = 1000.0 * params['bottom_radius']
 73      outer_top_radius_mm = 1000.0 * params['top_radius']
 74      inner_height_mm = outer_height_mm - thickness_mm
 75      inner_bottom_radius_mm = outer_bottom_radius_mm - (thickness_mm / math.sin(height_angle))
 76      inner_top_radius_mm = inner_bottom_radius_mm - (inner_height_mm / math.tan(height_angle))
 77      outer = Part.makeCone(outer_bottom_radius_mm, outer_top_radius_mm, outer_height_mm)
 78      if not fully_displace:
 79         inner = Part.makeCone(inner_bottom_radius_mm, inner_top_radius_mm, inner_height_mm)
 80         return outer.cut(inner)
 81      else:
 82         return outer
 83
 84
 85   # Geometry setter ------------------------------------------------------------------------------
 86
 87   def set_geometry(self, *, bottom_radius_m: Union[float, None],
 88                             top_radius_m: Union[float, None],
 89                             height_m: Union[float, None],
 90                             thickness_m: Union[float, None]) -> ConicalFrustrum:
 91      """Sets the physical geometry of the current `ConicalFrustrum` object.
 92
 93      See the `ConicalFrustrum` class documentation for a description of each geometric parameter.
 94      """
 95      self.geometry.set(bottom_radius=bottom_radius_m,
 96                        top_radius=top_radius_m,
 97                        height=height_m,
 98                        thickness=thickness_m)
 99      return self
100
101   def get_geometric_parameter_bounds(self, parameter: str) -> Tuple[float, float]:
102      parameter_bounds = {
103         'bottom_radius': (0.0, 2.0),
104         'top_radius': (0.0, 2.0),
105         'thickness': (0.0, 0.05),
106         'height': (0.0, 2.0)
107      }
108      return parameter_bounds.get(parameter, (0.0, 0.0))
109
110
111   # Geometric properties -------------------------------------------------------------------------
112
113   @property
114   def material_volume(self) -> Union[float, Expr]:
115      volume = self.displaced_volume
116      height_angle = atan2(self.geometry.height,
117                           self.geometry.bottom_radius - self.geometry.top_radius)
118      inner_height = self.geometry.height - self.geometry.thickness
119      inner_bottom_radius = self.geometry.bottom_radius - \
120                            (self.geometry.thickness / sin(height_angle))
121      inner_top_radius = inner_bottom_radius - (inner_height / tan(height_angle))
122      volume -= ((inner_height * math.pi / 3.0) * \
123                 (inner_bottom_radius**2 + inner_top_radius**2 +
124                    (inner_bottom_radius * inner_top_radius)))
125      return volume
126
127   @property
128   def displaced_volume(self) -> Union[float, Expr]:
129      return (self.geometry.height * math.pi / 3.0) * \
130             (self.geometry.bottom_radius**2 + self.geometry.top_radius**2 +
131                (self.geometry.bottom_radius * self.geometry.top_radius))
132
133   @property
134   def surface_area(self) -> Union[float, Expr]:
135      return (math.pi * self.geometry.top_radius**2) + \
136             (math.pi * (self.geometry.bottom_radius + self.geometry.top_radius) *
137              sqrt(self.geometry.height**2 +
138                         (self.geometry.bottom_radius - self.geometry.top_radius)**2))
139
140   @property
141   def unoriented_center_of_gravity(self) -> Tuple[Union[float, Expr],
142                                                   Union[float, Expr],
143                                                   Union[float, Expr]]:
144      return (self.geometry.bottom_radius,
145              self.geometry.bottom_radius,
146              (self.geometry.height *
147                 (self.geometry.bottom_radius +
148                    (0.5 * self.geometry.top_radius**2) +
149                    (2.0 * self.geometry.top_radius))) /
150              (3.0 * (self.geometry.bottom_radius + self.geometry.top_radius)))
151
152   @property
153   def unoriented_center_of_buoyancy(self) -> Tuple[Union[float, Expr],
154                                                    Union[float, Expr],
155                                                    Union[float, Expr]]:
156      return (self.geometry.bottom_radius,
157              self.geometry.bottom_radius,
158              (self.geometry.height * (self.geometry.bottom_radius**2 +
159                 (2.0 * self.geometry.bottom_radius * self.geometry.top_radius) +
160                 (3.0 * self.geometry.top_radius**2))) /
161              (4.0 * (self.geometry.bottom_radius**2 +
162                 (self.geometry.bottom_radius * self.geometry.top_radius) +
163                 self.geometry.top_radius**2)))
164
165   @property
166   def unoriented_length(self) -> Union[float, Expr]:
167      return 2.0 * self.geometry.bottom_radius
168
169   @property
170   def unoriented_width(self) -> Union[float, Expr]:
171      return self.unoriented_length
172
173   @property
174   def unoriented_height(self) -> Union[float, Expr]:
175      return self.geometry.height
176
177   @property
178   def oriented_length(self) -> Union[float, Expr]:
179      # TODO: Implement this
180      return 0
181
182   @property
183   def oriented_width(self) -> Union[float, Expr]:
184      # TODO: Implement this
185      return 0
186
187   @property
188   def oriented_height(self) -> Union[float, Expr]:
189      # TODO: Implement this
190      return 0

Model representing a hollow, parametric, conical frustrum.

By default, the frustrum is oriented such that its base is perpendicular to the z-axis:

ConicalFrustrum

The geometry of this shape includes the following parameters:

  • bottom_radius: Radius (in m) of the base of the ConicalFrustrum (must be larger than top_radius)
  • top_radius: Radius (in m) of the tip of the ConicalFrustrum
  • height: Height (in m) of the ConicalFrustrum from base to tip
  • thickness: Thickness (in m) of the shell of the ConicalFrustrum

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

ConicalFrustrum(identifier: str, material_density_kg_m3: Optional[float] = 1.0)
47   def __init__(self, identifier: str, material_density_kg_m3: Optional[float] = 1.0) -> None:
48      """Initializes a hollow, parametric, conical frustrum object.
49
50      Parameters
51      ----------
52      identifier : `str`
53         Unique identifying name for the object.
54      material_density_kg_m3 : `float`, optional, default=1.0
55         Uniform material density in `kg/m^3` to be used in mass property calculations.
56      """
57      super().__init__(identifier, self.__create_cad__, None, material_density_kg_m3)
58      setattr(self.geometry, 'bottom_radius', Symbol(self.name + '_bottom_radius'))
59      setattr(self.geometry, 'top_radius', Symbol(self.name + '_top_radius'))
60      setattr(self.geometry, 'thickness', Symbol(self.name + '_thickness'))
61      setattr(self.geometry, 'height', Symbol(self.name + '_height'))

Initializes a hollow, parametric, conical frustrum object.

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, *, bottom_radius_m: Optional[float], top_radius_m: Optional[float], height_m: Optional[float], thickness_m: Optional[float]) -> ConicalFrustrum:
87   def set_geometry(self, *, bottom_radius_m: Union[float, None],
88                             top_radius_m: Union[float, None],
89                             height_m: Union[float, None],
90                             thickness_m: Union[float, None]) -> ConicalFrustrum:
91      """Sets the physical geometry of the current `ConicalFrustrum` object.
92
93      See the `ConicalFrustrum` class documentation for a description of each geometric parameter.
94      """
95      self.geometry.set(bottom_radius=bottom_radius_m,
96                        top_radius=top_radius_m,
97                        height=height_m,
98                        thickness=thickness_m)
99      return self

Sets the physical geometry of the current ConicalFrustrum object.

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

def get_geometric_parameter_bounds(self, parameter: str) -> Tuple[float, float]:
101   def get_geometric_parameter_bounds(self, parameter: str) -> Tuple[float, float]:
102      parameter_bounds = {
103         'bottom_radius': (0.0, 2.0),
104         'top_radius': (0.0, 2.0),
105         'thickness': (0.0, 0.05),
106         'height': (0.0, 2.0)
107      }
108      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).