symcad.core.SymPart

  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 .ML import NeuralNet
 19from .CAD import ModeledCad, ScriptedCad
 20from .Coordinate import Coordinate
 21from .Geometry import Geometry
 22from .Rotation import Rotation
 23from typing import Callable, Dict, List, Literal
 24from typing import Optional, Tuple, TypeVar, Union
 25from copy import deepcopy
 26from sympy import Expr
 27import abc
 28
 29SymPartSub = TypeVar('SymPartSub', bound='SymPart')
 30
 31class SymPart(metaclass=abc.ABCMeta):
 32   """Symbolic part base class from which all SymParts inherit.
 33
 34   Defines the interface to a set of abstract properties that all SymParts possess, including
 35   mass, material volume, displaced volume, surface area, centroid, centers of gravity and
 36   buouyancy, length, width, and height, among others. These properties may be concrete or
 37   symbolic, and they represent the external interface that is expected to be used when
 38   accessing the physical properties of a given SymPart.
 39
 40   When creating a new SymPart, its geometry and global placement will be treated as symbolic
 41   parameters, and its global orientation will be assumed to be 0 such that the SymPart does not
 42   rotate in space. These assumptions can be overridden using the various `set_*` methods of the
 43   SymPart object. Attachment points can be defined to indicate areas on a SymPart that are able
 44   to rigidly attach to other SymParts, or connection ports can be defined to indicate areas that
 45   are able to flexibly and/or non-mechanically connect to other SymParts.
 46
 47   By default, a SymPart is assumed to be environmentally exposed and thus contribute to such
 48   geometric properties as the displaced volume. If a SymPart is not exposed, for example a dry
 49   component inside of a pressurized container, this should be specified by calling the
 50   `set_unexposed()` method on the SymPart object.
 51
 52   The local coordinate space of a SymPart is defined with its origin at the front, center,
 53   bottom of the part, where the x-axis extends positively from its front to its rear, the y-axis
 54   extends positively from the xz-plane to the right of the part when looking from the positive
 55   x-axis toward origin, and the z-axis extends positively from the bottom to the top of the part.
 56   """
 57
 58
 59   # Public attributes ----------------------------------------------------------------------------
 60
 61   name: str
 62   """Unique, identifying name of the `SymPart` instance."""
 63
 64   geometry: Geometry
 65   """Part-specific geometry parameters."""
 66
 67   attachment_points: List[Coordinate]
 68   """List of local points on the SymPart that can attach to other SymParts."""
 69
 70   attachments: Dict[str, str]
 71   """Dictionary of SymParts and attachment points that are rigidly attached to this SymPart."""
 72
 73   connection_ports: List[Coordinate]
 74   """List of local points on the SymPart that can connect flexibly to other SymParts."""
 75
 76   connections: Dict[str, str]
 77   """Dictionary of SymParts and connection ports that are flexibly connected to this SymPart."""
 78
 79   static_origin: Union[Coordinate, None]
 80   """Local point on the unoriented SymPart that is used for static placement and rotation."""
 81
 82   static_placement: Union[Coordinate, None]
 83   """Global static placement of the `static_origin` of the SymPart."""
 84
 85   orientation: Rotation
 86   """Global orientation of the SymPart (no rotation by default)."""
 87
 88   material_density: float
 89   """Uniform material density in `kg/m^3` to be used in mass property calculations."""
 90
 91   current_states: List[str]
 92   """List of geometric states for which the SymPart is currently configured."""
 93
 94   is_exposed: bool
 95   """Whether the SymPart is environmentally exposed versus contained in another element."""
 96
 97
 98   # Constructor ----------------------------------------------------------------------------------
 99
100   def __init__(self, identifier: str,
101                      cad_representation: Union[str, Callable],
102                      properties_model: Union[str, NeuralNet, None],
103                      material_density: float) -> None:
104      """Initializes an instance of a `SymPart`.
105
106      The underlying `cad_representation` may either be a predefined FreeCAD model or a reference
107      to a method that is able to create such a model. The `material_density` indicates the
108      uniform material density in `kg/m^3` that should be used in mass property calculations for
109      the given SymPart.
110
111      Parameters
112      ----------
113      identifier : `str`
114         Unique, identifying name for the SymPart.
115      cad_representation : `Union[str, Callable]`
116         Either the path to a representative CAD model for the given SymPart or a callable method
117         that can create such a model.
118      properties_model : `Union[str, NeuralNet, None]`
119         Path to or instance of a neural network that may be evaluated to obtain the underlying
120         geometric properties for the given SymPart.
121      material_density: `float`
122         Uniform material density in `kg/m^3` to be used in mass property calculations.
123      """
124      super().__init__()
125      self.name = identifier
126      self.geometry = Geometry(identifier)
127      self.attachment_points = []
128      self.attachments = {}
129      self.connection_ports = []
130      self.connections = {}
131      self.static_origin = None
132      self.static_placement = None
133      self.orientation = Rotation(identifier + '_orientation')
134      self.material_density = material_density
135      self.current_states = []
136      self.is_exposed = True
137      self.__cad__ = ModeledCad(cad_representation) if isinstance(cad_representation, str) else \
138                     ScriptedCad(cad_representation)
139      self.__neural_net__ = NeuralNet(identifier, properties_model) \
140                               if (properties_model and isinstance(properties_model, str)) else \
141                            properties_model
142
143
144   # Built-in method implementations --------------------------------------------------------------
145
146   def __repr__(self) -> str:
147      return str(type(self)).split('.')[-1].split('\'')[0] + ' (' + self.name + '): Geometry: [' \
148             + str(self.geometry) + '], Placement: [' + str(self.static_placement) \
149             + '], Orientation: [' + str(self.orientation) + '], Density: ' \
150             + str(self.material_density) + ' kg/m^3, Exposed: ' + str(self.is_exposed)
151
152   def __eq__(self, other: SymPart) -> bool:
153      for key, val in self.__dict__.items():
154         if key != 'name' and (key not in other.__dict__ or val != getattr(other, key)):
155            return False
156      return type(self) == type(other)
157
158   def __copy__(self) -> SymPartSub:
159      copy = self.__class__.__new__(self.__class__)
160      copy.__dict__.update(self.__dict__)
161      return copy
162
163   def __deepcopy__(self, memo) -> SymPartSub:
164      copy = self.__class__.__new__(self.__class__)
165      memo[id(self)] = copy
166      for key, val in self.__dict__.items():
167         setattr(copy, key, deepcopy(val, memo))
168      return copy
169
170   def __imul__(self: SymPartSub, value: float) -> SymPartSub:
171      self.geometry *= value
172      return self
173
174   def __itruediv__(self: SymPartSub, value: float) -> SymPartSub:
175      self.geometry /= value
176      return self
177
178
179   # Public methods -------------------------------------------------------------------------------
180
181   def clone(self: SymPartSub) -> SymPartSub:
182      """Returns an exact clone of this `SymPart` instance."""
183      return deepcopy(self)
184
185
186   def set_placement(self: SymPartSub, *, placement: Tuple[Union[float, Expr, None],
187                                                           Union[float, Expr, None],
188                                                           Union[float, Expr, None]],
189                                          local_origin: Tuple[float, float, float]) -> SymPartSub:
190      """Sets the global placement of the `local_origin` of this SymPart.
191
192      Parameters
193      ----------
194      placement : `Tuple` of `Union[float, sympy.Expr, None]`
195         Global XYZ placement of the `local_origin` of the SymPart in meters. If `None` is
196         specified for any given axis, placement on that axis will be treated as a symbol.
197      local_origin : `Tuple[float, float, float]`
198         Local XYZ point on the unoriented SymPart to be used for static placement and rotation.
199         Each coordinate of the origin point should fall in the range `[0.0, 1.0]` and be relative
200         to the *x-axis* length, *y-axis* width, and *z-axis* height of the SymPart with its
201         origin at the front, left, center of the part.
202
203      Returns
204      -------
205      self : `SymPart`
206         The current SymPart being manipulated.
207      """
208      if self.static_placement is None:
209         self.static_placement = Coordinate(self.name + '_placement')
210      self.static_placement.set(x=placement[0], y=placement[1], z=placement[2])
211      if self.static_origin is None:
212         self.static_origin = Coordinate(self.name + '_origin')
213      self.static_origin.set(x=local_origin[0], y=local_origin[1], z=local_origin[2])
214      return self
215
216
217   def set_orientation(self: SymPartSub, *, roll_deg: Union[float, Expr, None],
218                                            pitch_deg: Union[float, Expr, None],
219                                            yaw_deg: Union[float, Expr, None]) -> SymPartSub:
220      """Sets the global orientation of the SymPart when rotated about its `z-`, `y-`, then
221      `x-axis` (`yaw`, `pitch`, then `roll`) using a right-hand coordinate system.
222
223      A positive `roll_deg` will tilt the part to the left from the point of view of a location
224      inside the part. A positive `pitch_deg` will rotate the nose of the part downward, and a
225      positive `yaw_deg` will rotate the nose of the part toward the left from the point of view
226      of a location inside the part.
227
228      Parameters
229      ----------
230      roll_deg : `Union[float, sympy.Expr, None]`
231         Desired intrinsic roll angle in degrees. If `None` is specified, the angle will be
232         treated as a symbol.
233      pitch_deg : `Union[float, sympy.Expr, None]`
234         Desired intrinsic pitch angle in degrees. If `None` is specified, the angle will be
235         treated as a symbol.
236      yaw_deg : `Union[float, sympy.Expr, None]`
237         Desired intrinsic yaw angle in degress. If `None` is specified, the angle will be
238         treated as a symbol.
239
240      Returns
241      -------
242      self : `SymPart`
243         The current SymPart being manipulated.
244      """
245      self.orientation.set(roll_deg=roll_deg, pitch_deg=pitch_deg, yaw_deg=yaw_deg)
246      return self
247
248
249   def set_state(self: SymPartSub, state_names: Union[List[str], None]) -> SymPartSub:
250      """Sets the geometric configuration of the SymPart according to the indicated `state_names`.
251
252      The concrete, overriding `SymPart` class may use these `state_names` to alter its underlying
253      geometric properties.
254
255      Parameters
256      ----------
257      state_names : `Union[List[str], None]`
258         List of geometric states for which the part should be configured. If set to `None`,
259         the part will be configured in its default state.
260
261      Returns
262      -------
263      self : `SymPart`
264         The current SymPart being manipulated.
265      """
266      self.current_states = [] if not state_names else \
267                            [state for state in state_names if state in self.get_valid_states()]
268      return self
269
270
271   def set_unexposed(self: SymPartSub) -> SymPartSub:
272      """Specifies that the SymPart is environmentally unexposed.
273
274      This results in the part being excluded from certain geometric property calculations such
275      as `displaced_volume`.
276
277      Returns
278      -------
279      self : `SymPart`
280         The current SymPart being manipulated.
281      """
282      self.is_exposed = False
283      return self
284
285
286   def set_material_density(self: SymPartSub, material_density) -> SymPartSub:
287      """Sets the material density of the SymPart.
288
289      Parameters
290      ----------
291      material_density: `float`
292         Uniform material density in `kg/m^3` to be used in mass property calculations.
293
294      Returns
295      -------
296      self : `SymPart`
297         The current SymPart being manipulated.
298      """
299      self.material_density = material_density
300      return self
301
302
303   def add_attachment_point(self: SymPartSub, attachment_point_id: str, *,
304                                              x: Union[float, Expr],
305                                              y: Union[float, Expr],
306                                              z: Union[float, Expr]) -> SymPartSub:
307      """Adds a local attachment point to the SymPart.
308
309      Each coordinate of the attachment point should fall in the range `[0.0, 1.0]` and be
310      relative to the *x-axis* length, *y-axis* width, and *z-axis* height of the SymPart with
311      its origin at the front, left, center of the part.
312
313      Parameters
314      ----------
315      attachment_point_id : `str`
316         Unique identifier for the new attachment point.
317      x : `Union[float, sympy.Expr]`
318         Local x-axis placement of the attachment point on the SymPart relative to its length.
319      y : `Union[float, sympy.Expr]`
320         Local y-axis placement of the attachment point on the SymPart relative to its width.
321      z : `Union[float, sympy.Expr]`
322         Local z-axis placement of the attachment point on the SymPart relative to its height.
323
324      Returns
325      -------
326      self : `SymPart`
327         The current SymPart being manipulated.
328      """
329      if attachment_point_id in [point.name for point in self.attachment_points]:
330         raise ValueError('An attachment point with the ID "{}" already exists'
331                          .format(attachment_point_id))
332      self.attachment_points.append(Coordinate(attachment_point_id, x=x, y=y, z=z))
333      return self
334
335
336   def add_connection_port(self: SymPartSub, connection_port_id: str, *,
337                                              x: Union[float, Expr],
338                                              y: Union[float, Expr],
339                                              z: Union[float, Expr]) -> SymPartSub:
340      """Adds a local connection port to the SymPart.
341
342      Each coordinate of the connection port should fall in the range `[0.0, 1.0]` and be
343      relative to the *x-axis* length, *y-axis* width, and *z-axis* height of the SymPart with
344      its origin at the front, left, center of the part.
345
346      Parameters
347      ----------
348      connection_port_id : `str`
349         Unique identifier for the new connection port.
350      x : `Union[float, sympy.Expr]`
351         Local x-axis placement of the connection port on the SymPart relative to its length.
352      y : `Union[float, sympy.Expr]`
353         Local y-axis placement of the connection port on the SymPart relative to its width.
354      z : `Union[float, sympy.Expr]`
355         Local z-axis placement of the connection port on the SymPart relative to its height.
356
357      Returns
358      -------
359      self : `SymPart`
360         The current SymPart being manipulated.
361      """
362      if connection_port_id in [port.name for port in self.connection_ports]:
363         raise ValueError('A connection port with the ID "{}" already exists'
364                          .format(connection_port_id))
365      self.connection_ports.append(Coordinate(connection_port_id, x=x, y=y, z=z))
366      return self
367
368
369   def attach(self: SymPartSub, local_attachment_id: str,
370                                remote_part: SymPart,
371                                remote_attachment_id: str) -> SymPartSub:
372      """Creates a rigid attachment between a local and remote attachment point.
373
374      Parameters
375      ----------
376      local_attachment_id : `str`
377         Identifier of the local attachment point to which to attach.
378      remote_part : `SymPart`
379         The remote SymPart to which to make an attachment.
380      remote_attachment_id : `str`
381         Identifier of the remote attachment point to which to attach.
382
383      Returns
384      -------
385      self : `SymPart`
386         The current SymPart being manipulated.
387      """
388
389      # Ensure that the requested attachment is valid
390      if self.name == remote_part.name:
391         raise ValueError('The local and attached parts cannot both have the same name "{}"'
392                           .format(self.name))
393      if local_attachment_id not in [point.name for point in self.attachment_points]:
394         raise ValueError('The local attachment point identifier "{}" does not exist'
395                           .format(local_attachment_id))
396      if remote_attachment_id not in [point.name for point in remote_part.attachment_points]:
397         raise ValueError('The remote attachment point identifier "{}" does not exist'
398                           .format(remote_attachment_id))
399      if local_attachment_id in self.attachments:
400         raise ValueError('The local attachment point "{}" is already being used'
401                           .format(local_attachment_id))
402      if remote_attachment_id in remote_part.attachments:
403         raise ValueError('The remote attachment point "{}" is already being used'
404                           .format(remote_attachment_id))
405
406      # Make the rigid attachment in both directions
407      self.attachments[local_attachment_id] = remote_part.name + '#' + remote_attachment_id
408      remote_part.attachments[remote_attachment_id] = self.name + '#' + local_attachment_id
409      return self
410
411
412   def connect(self: SymPartSub, local_connection_id: str,
413                                 remote_part: SymPart,
414                                 remote_connection_id: str) -> SymPartSub:
415      """Creates a non-rigid connection between a local and remote connection port.
416
417      Parameters
418      ----------
419      local_connection_id : `str`
420         Identifier of the local connection port to which to connect.
421      remote_part : `SymPart`
422         The remote SymPart to which to make a connection.
423      remote_connection_id : `str`
424         Identifier of the remote connection port to which to connect.
425
426      Returns
427      -------
428      self : `SymPart`
429         The current SymPart being manipulated.
430      """
431
432      # Ensure that the requested connection is valid
433      if self.name == remote_part.name:
434         raise ValueError('The local and connected parts cannot both have the same name "{}"'
435                           .format(self.name))
436      if local_connection_id not in [port.name for port in self.connection_ports]:
437         raise ValueError('The local connection port identifier "{}" does not exist'
438                           .format(local_connection_id))
439      if remote_connection_id not in [port.name for port in remote_part.connection_ports]:
440         raise ValueError('The remote connection port identifier "{}" does not exist'
441                           .format(remote_connection_id))
442      if local_connection_id in self.connections:
443         raise ValueError('The local connection port "{}" is already being used'
444                           .format(local_connection_id))
445      if remote_connection_id in remote_part.connections:
446         raise ValueError('The remote connection port "{}" is already being used'
447                           .format(remote_connection_id))
448
449      # Make the flexible connection in both directions
450      self.connections[local_connection_id] = remote_part.name + '#' + remote_connection_id
451      remote_part.connections[remote_connection_id] = self.name + '#' + local_connection_id
452      return self
453
454
455   def get_cad_physical_properties(self,
456                                   normalize_origin: Optional[bool] = False) -> Dict[str, float]:
457      """Retrieves the set of physical properties of the SymPart as reported by the underlying
458      CAD model.
459
460      Parameters
461      ----------
462      normalize_origin : `bool`, optional, default=False
463         Return physical properties with respect to the front, left, bottom corner of the
464         underlying CAD model.
465
466      Returns
467      -------
468      `Dict[str, float]`
469         A list of physical properties as calculated from the underlying CAD model.
470      """
471      placement_center = self.static_origin.as_tuple() if \
472                            self.static_origin is not None else \
473                         (0.0, 0.0, 0.0)
474      return self.__cad__.get_physical_properties(self.geometry.__dict__,
475                                                  placement_center,
476                                                  self.orientation.as_tuple(),
477                                                  self.material_density,
478                                                  normalize_origin)
479
480
481   def export(self, save_path: str, export_type: Literal['freecad', 'step', 'stl']) -> None:
482      """Exports the SymPart to an external CAD representation.
483
484      Supported CAD model formats currently include FreeCAD, STEP, and STL.
485
486      Parameters
487      ----------
488      save_path : `str`
489         Output file path at which to store the the generated CAD model.
490      export_type : {'freecad', 'step', 'stl'}
491         Format of the CAD model to export.
492      """
493      placement_center = self.static_origin.as_tuple() if \
494                            self.static_origin is not None else \
495                         (0.0, 0.0, 0.0)
496      self.__cad__.export_model(save_path,
497                                export_type,
498                                self.geometry.__dict__,
499                                placement_center,
500                                self.orientation.as_tuple())
501
502
503   # Abstract methods to be overridden ------------------------------------------------------------
504
505   @abc.abstractmethod
506   def set_geometry(self: SymPartSub, **kwargs) -> SymPartSub:
507      """Abstract method that must be overridden by a concrete `SymPart` class to allow setting
508      its physical geometry.
509
510      Parameters
511      ----------
512      **kwargs : `Dict`
513         Set of named parameters that define the geometry of a SymPart.
514
515      Raises
516      -------
517      NotImplementedError
518         If the implementing `SymPart` class does not override this method.
519      """
520      raise NotImplementedError
521
522   @abc.abstractmethod
523   def get_geometric_parameter_bounds(self: SymPartSub, parameter: str) -> Tuple[float, float]:
524      """Abstract method that must be overridden by a concrete `SymPart` class to return the
525      minimum and maximum expected bounds for a given geometric parameter.
526
527      Parameters
528      ----------
529      parameter : `str`
530         Name of the geometric parameter for which to return the minimum and maximum bounds.
531
532      Returns
533      -------
534      `Tuple[float, float]`
535         Minimum and maximum bounds for the specified geometric `parameter`."""
536      raise NotImplementedError
537
538   def get_valid_states(self: SymPartSub) -> List[str]:
539      """Method that may be overridden by a concrete `SymPart` class to indicate the list of
540      geometric state names recognized by the part.
541
542      If this method is not overridden, it will return an empty list, indicating that the part
543      only has one valid geometric state.
544      """
545      return []
546
547
548   # Abstract properties that must be overridden --------------------------------------------------
549
550   @property
551   def mass(self) -> Union[float, Expr]:
552      """Mass (in `kg`) of the SymPart (read-only)."""
553      return self.material_volume * self.material_density
554
555   @property
556   @abc.abstractmethod
557   def material_volume(self) -> Union[float, Expr]:
558      """Material volume (in `m^3`) of the SymPart (read-only)."""
559      raise NotImplementedError
560
561   @property
562   @abc.abstractmethod
563   def displaced_volume(self) -> Union[float, Expr]:
564      """Displaced volume (in `m^3`) of the SymPart (read-only)."""
565      raise NotImplementedError
566
567   @property
568   @abc.abstractmethod
569   def surface_area(self) -> Union[float, Expr]:
570      """Surface/wetted area (in `m^2`) of the SymPart (read-only)."""
571      raise NotImplementedError
572
573   @property
574   @abc.abstractmethod
575   def unoriented_center_of_gravity(self) -> Tuple[Union[float, Expr],
576                                                   Union[float, Expr],
577                                                   Union[float, Expr]]:
578      """Center of gravity (in `m`) of the **unoriented** SymPart (read-only)."""
579      raise NotImplementedError
580
581   @property
582   def oriented_center_of_gravity(self) -> Tuple[Union[float, Expr],
583                                                 Union[float, Expr],
584                                                 Union[float, Expr]]:
585      """Center of gravity (in `m`) of the **oriented** SymPart (read-only)."""
586      origin = self.static_origin.clone() \
587                  if self.static_origin is not None else \
588               Coordinate(self.name + '_origin')
589      center_of_gravity = self.unoriented_center_of_gravity
590      center_of_gravity = [center_of_gravity[0] - (origin.x * self.unoriented_length),
591                           center_of_gravity[1] - (origin.y * self.unoriented_width),
592                           center_of_gravity[2] - (origin.z * self.unoriented_height)]
593      return self.orientation.rotate_point((0.0, 0.0, 0.0), center_of_gravity)
594
595   @property
596   @abc.abstractmethod
597   def unoriented_center_of_buoyancy(self) -> Tuple[Union[float, Expr],
598                                                    Union[float, Expr],
599                                                    Union[float, Expr]]:
600      """Center of buoyancy (in `m`) of the **unoriented** SymPart (read-only)."""
601      raise NotImplementedError
602
603   @property
604   def oriented_center_of_buoyancy(self) -> Tuple[Union[float, Expr],
605                                                  Union[float, Expr],
606                                                  Union[float, Expr]]:
607      """Center of buoyancy (in `m`) of the **oriented** SymPart (read-only)."""
608      origin = self.static_origin.clone() \
609                  if self.static_origin is not None else \
610               Coordinate(self.name + '_origin')
611      center_of_buoyancy = self.unoriented_center_of_buoyancy
612      center_of_buoyancy = [center_of_buoyancy[0] - (origin.x * self.unoriented_length),
613                            center_of_buoyancy[1] - (origin.y * self.unoriented_width),
614                            center_of_buoyancy[2] - (origin.z * self.unoriented_height)]
615      return self.orientation.rotate_point((0.0, 0.0, 0.0), center_of_buoyancy)
616
617   @property
618   @abc.abstractmethod
619   def unoriented_length(self) -> Union[float, Expr]:
620      """X-axis length (in `m`) of the bounding box of the **unoriented** SymPart (read-only)."""
621      raise NotImplementedError
622
623   @property
624   @abc.abstractmethod
625   def unoriented_width(self) -> Union[float, Expr]:
626      """Y-axis width (in `m`) of the bounding box of the **unoriented** SymPart (read-only)."""
627      raise NotImplementedError
628
629   @property
630   @abc.abstractmethod
631   def unoriented_height(self) -> Union[float, Expr]:
632      """Z-axis height (in `m`) of the bounding box of the **unoriented** SymPart (read-only)."""
633      raise NotImplementedError
634
635   @property
636   def oriented_length(self) -> Union[float, Expr]:
637      """X-axis length (in `m`) of the bounding box of the **oriented** SymPart (read-only)."""
638      R = self.orientation.get_rotation_matrix_row(0)
639      original_extents = [self.unoriented_length, self.unoriented_width, self.unoriented_height]
640      return sum([abs(R[i] * original_extents[i]) for i in range(3)])
641
642   @property
643   def oriented_width(self) -> Union[float, Expr]:
644      """Y-axis length (in `m`) of the bounding box of the **oriented** SymPart (read-only)."""
645      R = self.orientation.get_rotation_matrix_row(1)
646      original_extents = [self.unoriented_length, self.unoriented_width, self.unoriented_height]
647      return sum([abs(R[i] * original_extents[i]) for i in range(3)])
648
649   @property
650   def oriented_height(self) -> Union[float, Expr]:
651      """Z-axis length (in `m`) of the bounding box of the **oriented** SymPart (read-only)."""
652      R = self.orientation.get_rotation_matrix_row(2)
653      original_extents = [self.unoriented_length, self.unoriented_width, self.unoriented_height]
654      return sum([abs(R[i] * original_extents[i]) for i in range(3)])
class SymPart:
 32class SymPart(metaclass=abc.ABCMeta):
 33   """Symbolic part base class from which all SymParts inherit.
 34
 35   Defines the interface to a set of abstract properties that all SymParts possess, including
 36   mass, material volume, displaced volume, surface area, centroid, centers of gravity and
 37   buouyancy, length, width, and height, among others. These properties may be concrete or
 38   symbolic, and they represent the external interface that is expected to be used when
 39   accessing the physical properties of a given SymPart.
 40
 41   When creating a new SymPart, its geometry and global placement will be treated as symbolic
 42   parameters, and its global orientation will be assumed to be 0 such that the SymPart does not
 43   rotate in space. These assumptions can be overridden using the various `set_*` methods of the
 44   SymPart object. Attachment points can be defined to indicate areas on a SymPart that are able
 45   to rigidly attach to other SymParts, or connection ports can be defined to indicate areas that
 46   are able to flexibly and/or non-mechanically connect to other SymParts.
 47
 48   By default, a SymPart is assumed to be environmentally exposed and thus contribute to such
 49   geometric properties as the displaced volume. If a SymPart is not exposed, for example a dry
 50   component inside of a pressurized container, this should be specified by calling the
 51   `set_unexposed()` method on the SymPart object.
 52
 53   The local coordinate space of a SymPart is defined with its origin at the front, center,
 54   bottom of the part, where the x-axis extends positively from its front to its rear, the y-axis
 55   extends positively from the xz-plane to the right of the part when looking from the positive
 56   x-axis toward origin, and the z-axis extends positively from the bottom to the top of the part.
 57   """
 58
 59
 60   # Public attributes ----------------------------------------------------------------------------
 61
 62   name: str
 63   """Unique, identifying name of the `SymPart` instance."""
 64
 65   geometry: Geometry
 66   """Part-specific geometry parameters."""
 67
 68   attachment_points: List[Coordinate]
 69   """List of local points on the SymPart that can attach to other SymParts."""
 70
 71   attachments: Dict[str, str]
 72   """Dictionary of SymParts and attachment points that are rigidly attached to this SymPart."""
 73
 74   connection_ports: List[Coordinate]
 75   """List of local points on the SymPart that can connect flexibly to other SymParts."""
 76
 77   connections: Dict[str, str]
 78   """Dictionary of SymParts and connection ports that are flexibly connected to this SymPart."""
 79
 80   static_origin: Union[Coordinate, None]
 81   """Local point on the unoriented SymPart that is used for static placement and rotation."""
 82
 83   static_placement: Union[Coordinate, None]
 84   """Global static placement of the `static_origin` of the SymPart."""
 85
 86   orientation: Rotation
 87   """Global orientation of the SymPart (no rotation by default)."""
 88
 89   material_density: float
 90   """Uniform material density in `kg/m^3` to be used in mass property calculations."""
 91
 92   current_states: List[str]
 93   """List of geometric states for which the SymPart is currently configured."""
 94
 95   is_exposed: bool
 96   """Whether the SymPart is environmentally exposed versus contained in another element."""
 97
 98
 99   # Constructor ----------------------------------------------------------------------------------
100
101   def __init__(self, identifier: str,
102                      cad_representation: Union[str, Callable],
103                      properties_model: Union[str, NeuralNet, None],
104                      material_density: float) -> None:
105      """Initializes an instance of a `SymPart`.
106
107      The underlying `cad_representation` may either be a predefined FreeCAD model or a reference
108      to a method that is able to create such a model. The `material_density` indicates the
109      uniform material density in `kg/m^3` that should be used in mass property calculations for
110      the given SymPart.
111
112      Parameters
113      ----------
114      identifier : `str`
115         Unique, identifying name for the SymPart.
116      cad_representation : `Union[str, Callable]`
117         Either the path to a representative CAD model for the given SymPart or a callable method
118         that can create such a model.
119      properties_model : `Union[str, NeuralNet, None]`
120         Path to or instance of a neural network that may be evaluated to obtain the underlying
121         geometric properties for the given SymPart.
122      material_density: `float`
123         Uniform material density in `kg/m^3` to be used in mass property calculations.
124      """
125      super().__init__()
126      self.name = identifier
127      self.geometry = Geometry(identifier)
128      self.attachment_points = []
129      self.attachments = {}
130      self.connection_ports = []
131      self.connections = {}
132      self.static_origin = None
133      self.static_placement = None
134      self.orientation = Rotation(identifier + '_orientation')
135      self.material_density = material_density
136      self.current_states = []
137      self.is_exposed = True
138      self.__cad__ = ModeledCad(cad_representation) if isinstance(cad_representation, str) else \
139                     ScriptedCad(cad_representation)
140      self.__neural_net__ = NeuralNet(identifier, properties_model) \
141                               if (properties_model and isinstance(properties_model, str)) else \
142                            properties_model
143
144
145   # Built-in method implementations --------------------------------------------------------------
146
147   def __repr__(self) -> str:
148      return str(type(self)).split('.')[-1].split('\'')[0] + ' (' + self.name + '): Geometry: [' \
149             + str(self.geometry) + '], Placement: [' + str(self.static_placement) \
150             + '], Orientation: [' + str(self.orientation) + '], Density: ' \
151             + str(self.material_density) + ' kg/m^3, Exposed: ' + str(self.is_exposed)
152
153   def __eq__(self, other: SymPart) -> bool:
154      for key, val in self.__dict__.items():
155         if key != 'name' and (key not in other.__dict__ or val != getattr(other, key)):
156            return False
157      return type(self) == type(other)
158
159   def __copy__(self) -> SymPartSub:
160      copy = self.__class__.__new__(self.__class__)
161      copy.__dict__.update(self.__dict__)
162      return copy
163
164   def __deepcopy__(self, memo) -> SymPartSub:
165      copy = self.__class__.__new__(self.__class__)
166      memo[id(self)] = copy
167      for key, val in self.__dict__.items():
168         setattr(copy, key, deepcopy(val, memo))
169      return copy
170
171   def __imul__(self: SymPartSub, value: float) -> SymPartSub:
172      self.geometry *= value
173      return self
174
175   def __itruediv__(self: SymPartSub, value: float) -> SymPartSub:
176      self.geometry /= value
177      return self
178
179
180   # Public methods -------------------------------------------------------------------------------
181
182   def clone(self: SymPartSub) -> SymPartSub:
183      """Returns an exact clone of this `SymPart` instance."""
184      return deepcopy(self)
185
186
187   def set_placement(self: SymPartSub, *, placement: Tuple[Union[float, Expr, None],
188                                                           Union[float, Expr, None],
189                                                           Union[float, Expr, None]],
190                                          local_origin: Tuple[float, float, float]) -> SymPartSub:
191      """Sets the global placement of the `local_origin` of this SymPart.
192
193      Parameters
194      ----------
195      placement : `Tuple` of `Union[float, sympy.Expr, None]`
196         Global XYZ placement of the `local_origin` of the SymPart in meters. If `None` is
197         specified for any given axis, placement on that axis will be treated as a symbol.
198      local_origin : `Tuple[float, float, float]`
199         Local XYZ point on the unoriented SymPart to be used for static placement and rotation.
200         Each coordinate of the origin point should fall in the range `[0.0, 1.0]` and be relative
201         to the *x-axis* length, *y-axis* width, and *z-axis* height of the SymPart with its
202         origin at the front, left, center of the part.
203
204      Returns
205      -------
206      self : `SymPart`
207         The current SymPart being manipulated.
208      """
209      if self.static_placement is None:
210         self.static_placement = Coordinate(self.name + '_placement')
211      self.static_placement.set(x=placement[0], y=placement[1], z=placement[2])
212      if self.static_origin is None:
213         self.static_origin = Coordinate(self.name + '_origin')
214      self.static_origin.set(x=local_origin[0], y=local_origin[1], z=local_origin[2])
215      return self
216
217
218   def set_orientation(self: SymPartSub, *, roll_deg: Union[float, Expr, None],
219                                            pitch_deg: Union[float, Expr, None],
220                                            yaw_deg: Union[float, Expr, None]) -> SymPartSub:
221      """Sets the global orientation of the SymPart when rotated about its `z-`, `y-`, then
222      `x-axis` (`yaw`, `pitch`, then `roll`) using a right-hand coordinate system.
223
224      A positive `roll_deg` will tilt the part to the left from the point of view of a location
225      inside the part. A positive `pitch_deg` will rotate the nose of the part downward, and a
226      positive `yaw_deg` will rotate the nose of the part toward the left from the point of view
227      of a location inside the part.
228
229      Parameters
230      ----------
231      roll_deg : `Union[float, sympy.Expr, None]`
232         Desired intrinsic roll angle in degrees. If `None` is specified, the angle will be
233         treated as a symbol.
234      pitch_deg : `Union[float, sympy.Expr, None]`
235         Desired intrinsic pitch angle in degrees. If `None` is specified, the angle will be
236         treated as a symbol.
237      yaw_deg : `Union[float, sympy.Expr, None]`
238         Desired intrinsic yaw angle in degress. If `None` is specified, the angle will be
239         treated as a symbol.
240
241      Returns
242      -------
243      self : `SymPart`
244         The current SymPart being manipulated.
245      """
246      self.orientation.set(roll_deg=roll_deg, pitch_deg=pitch_deg, yaw_deg=yaw_deg)
247      return self
248
249
250   def set_state(self: SymPartSub, state_names: Union[List[str], None]) -> SymPartSub:
251      """Sets the geometric configuration of the SymPart according to the indicated `state_names`.
252
253      The concrete, overriding `SymPart` class may use these `state_names` to alter its underlying
254      geometric properties.
255
256      Parameters
257      ----------
258      state_names : `Union[List[str], None]`
259         List of geometric states for which the part should be configured. If set to `None`,
260         the part will be configured in its default state.
261
262      Returns
263      -------
264      self : `SymPart`
265         The current SymPart being manipulated.
266      """
267      self.current_states = [] if not state_names else \
268                            [state for state in state_names if state in self.get_valid_states()]
269      return self
270
271
272   def set_unexposed(self: SymPartSub) -> SymPartSub:
273      """Specifies that the SymPart is environmentally unexposed.
274
275      This results in the part being excluded from certain geometric property calculations such
276      as `displaced_volume`.
277
278      Returns
279      -------
280      self : `SymPart`
281         The current SymPart being manipulated.
282      """
283      self.is_exposed = False
284      return self
285
286
287   def set_material_density(self: SymPartSub, material_density) -> SymPartSub:
288      """Sets the material density of the SymPart.
289
290      Parameters
291      ----------
292      material_density: `float`
293         Uniform material density in `kg/m^3` to be used in mass property calculations.
294
295      Returns
296      -------
297      self : `SymPart`
298         The current SymPart being manipulated.
299      """
300      self.material_density = material_density
301      return self
302
303
304   def add_attachment_point(self: SymPartSub, attachment_point_id: str, *,
305                                              x: Union[float, Expr],
306                                              y: Union[float, Expr],
307                                              z: Union[float, Expr]) -> SymPartSub:
308      """Adds a local attachment point to the SymPart.
309
310      Each coordinate of the attachment point should fall in the range `[0.0, 1.0]` and be
311      relative to the *x-axis* length, *y-axis* width, and *z-axis* height of the SymPart with
312      its origin at the front, left, center of the part.
313
314      Parameters
315      ----------
316      attachment_point_id : `str`
317         Unique identifier for the new attachment point.
318      x : `Union[float, sympy.Expr]`
319         Local x-axis placement of the attachment point on the SymPart relative to its length.
320      y : `Union[float, sympy.Expr]`
321         Local y-axis placement of the attachment point on the SymPart relative to its width.
322      z : `Union[float, sympy.Expr]`
323         Local z-axis placement of the attachment point on the SymPart relative to its height.
324
325      Returns
326      -------
327      self : `SymPart`
328         The current SymPart being manipulated.
329      """
330      if attachment_point_id in [point.name for point in self.attachment_points]:
331         raise ValueError('An attachment point with the ID "{}" already exists'
332                          .format(attachment_point_id))
333      self.attachment_points.append(Coordinate(attachment_point_id, x=x, y=y, z=z))
334      return self
335
336
337   def add_connection_port(self: SymPartSub, connection_port_id: str, *,
338                                              x: Union[float, Expr],
339                                              y: Union[float, Expr],
340                                              z: Union[float, Expr]) -> SymPartSub:
341      """Adds a local connection port to the SymPart.
342
343      Each coordinate of the connection port should fall in the range `[0.0, 1.0]` and be
344      relative to the *x-axis* length, *y-axis* width, and *z-axis* height of the SymPart with
345      its origin at the front, left, center of the part.
346
347      Parameters
348      ----------
349      connection_port_id : `str`
350         Unique identifier for the new connection port.
351      x : `Union[float, sympy.Expr]`
352         Local x-axis placement of the connection port on the SymPart relative to its length.
353      y : `Union[float, sympy.Expr]`
354         Local y-axis placement of the connection port on the SymPart relative to its width.
355      z : `Union[float, sympy.Expr]`
356         Local z-axis placement of the connection port on the SymPart relative to its height.
357
358      Returns
359      -------
360      self : `SymPart`
361         The current SymPart being manipulated.
362      """
363      if connection_port_id in [port.name for port in self.connection_ports]:
364         raise ValueError('A connection port with the ID "{}" already exists'
365                          .format(connection_port_id))
366      self.connection_ports.append(Coordinate(connection_port_id, x=x, y=y, z=z))
367      return self
368
369
370   def attach(self: SymPartSub, local_attachment_id: str,
371                                remote_part: SymPart,
372                                remote_attachment_id: str) -> SymPartSub:
373      """Creates a rigid attachment between a local and remote attachment point.
374
375      Parameters
376      ----------
377      local_attachment_id : `str`
378         Identifier of the local attachment point to which to attach.
379      remote_part : `SymPart`
380         The remote SymPart to which to make an attachment.
381      remote_attachment_id : `str`
382         Identifier of the remote attachment point to which to attach.
383
384      Returns
385      -------
386      self : `SymPart`
387         The current SymPart being manipulated.
388      """
389
390      # Ensure that the requested attachment is valid
391      if self.name == remote_part.name:
392         raise ValueError('The local and attached parts cannot both have the same name "{}"'
393                           .format(self.name))
394      if local_attachment_id not in [point.name for point in self.attachment_points]:
395         raise ValueError('The local attachment point identifier "{}" does not exist'
396                           .format(local_attachment_id))
397      if remote_attachment_id not in [point.name for point in remote_part.attachment_points]:
398         raise ValueError('The remote attachment point identifier "{}" does not exist'
399                           .format(remote_attachment_id))
400      if local_attachment_id in self.attachments:
401         raise ValueError('The local attachment point "{}" is already being used'
402                           .format(local_attachment_id))
403      if remote_attachment_id in remote_part.attachments:
404         raise ValueError('The remote attachment point "{}" is already being used'
405                           .format(remote_attachment_id))
406
407      # Make the rigid attachment in both directions
408      self.attachments[local_attachment_id] = remote_part.name + '#' + remote_attachment_id
409      remote_part.attachments[remote_attachment_id] = self.name + '#' + local_attachment_id
410      return self
411
412
413   def connect(self: SymPartSub, local_connection_id: str,
414                                 remote_part: SymPart,
415                                 remote_connection_id: str) -> SymPartSub:
416      """Creates a non-rigid connection between a local and remote connection port.
417
418      Parameters
419      ----------
420      local_connection_id : `str`
421         Identifier of the local connection port to which to connect.
422      remote_part : `SymPart`
423         The remote SymPart to which to make a connection.
424      remote_connection_id : `str`
425         Identifier of the remote connection port to which to connect.
426
427      Returns
428      -------
429      self : `SymPart`
430         The current SymPart being manipulated.
431      """
432
433      # Ensure that the requested connection is valid
434      if self.name == remote_part.name:
435         raise ValueError('The local and connected parts cannot both have the same name "{}"'
436                           .format(self.name))
437      if local_connection_id not in [port.name for port in self.connection_ports]:
438         raise ValueError('The local connection port identifier "{}" does not exist'
439                           .format(local_connection_id))
440      if remote_connection_id not in [port.name for port in remote_part.connection_ports]:
441         raise ValueError('The remote connection port identifier "{}" does not exist'
442                           .format(remote_connection_id))
443      if local_connection_id in self.connections:
444         raise ValueError('The local connection port "{}" is already being used'
445                           .format(local_connection_id))
446      if remote_connection_id in remote_part.connections:
447         raise ValueError('The remote connection port "{}" is already being used'
448                           .format(remote_connection_id))
449
450      # Make the flexible connection in both directions
451      self.connections[local_connection_id] = remote_part.name + '#' + remote_connection_id
452      remote_part.connections[remote_connection_id] = self.name + '#' + local_connection_id
453      return self
454
455
456   def get_cad_physical_properties(self,
457                                   normalize_origin: Optional[bool] = False) -> Dict[str, float]:
458      """Retrieves the set of physical properties of the SymPart as reported by the underlying
459      CAD model.
460
461      Parameters
462      ----------
463      normalize_origin : `bool`, optional, default=False
464         Return physical properties with respect to the front, left, bottom corner of the
465         underlying CAD model.
466
467      Returns
468      -------
469      `Dict[str, float]`
470         A list of physical properties as calculated from the underlying CAD model.
471      """
472      placement_center = self.static_origin.as_tuple() if \
473                            self.static_origin is not None else \
474                         (0.0, 0.0, 0.0)
475      return self.__cad__.get_physical_properties(self.geometry.__dict__,
476                                                  placement_center,
477                                                  self.orientation.as_tuple(),
478                                                  self.material_density,
479                                                  normalize_origin)
480
481
482   def export(self, save_path: str, export_type: Literal['freecad', 'step', 'stl']) -> None:
483      """Exports the SymPart to an external CAD representation.
484
485      Supported CAD model formats currently include FreeCAD, STEP, and STL.
486
487      Parameters
488      ----------
489      save_path : `str`
490         Output file path at which to store the the generated CAD model.
491      export_type : {'freecad', 'step', 'stl'}
492         Format of the CAD model to export.
493      """
494      placement_center = self.static_origin.as_tuple() if \
495                            self.static_origin is not None else \
496                         (0.0, 0.0, 0.0)
497      self.__cad__.export_model(save_path,
498                                export_type,
499                                self.geometry.__dict__,
500                                placement_center,
501                                self.orientation.as_tuple())
502
503
504   # Abstract methods to be overridden ------------------------------------------------------------
505
506   @abc.abstractmethod
507   def set_geometry(self: SymPartSub, **kwargs) -> SymPartSub:
508      """Abstract method that must be overridden by a concrete `SymPart` class to allow setting
509      its physical geometry.
510
511      Parameters
512      ----------
513      **kwargs : `Dict`
514         Set of named parameters that define the geometry of a SymPart.
515
516      Raises
517      -------
518      NotImplementedError
519         If the implementing `SymPart` class does not override this method.
520      """
521      raise NotImplementedError
522
523   @abc.abstractmethod
524   def get_geometric_parameter_bounds(self: SymPartSub, parameter: str) -> Tuple[float, float]:
525      """Abstract method that must be overridden by a concrete `SymPart` class to return the
526      minimum and maximum expected bounds for a given geometric parameter.
527
528      Parameters
529      ----------
530      parameter : `str`
531         Name of the geometric parameter for which to return the minimum and maximum bounds.
532
533      Returns
534      -------
535      `Tuple[float, float]`
536         Minimum and maximum bounds for the specified geometric `parameter`."""
537      raise NotImplementedError
538
539   def get_valid_states(self: SymPartSub) -> List[str]:
540      """Method that may be overridden by a concrete `SymPart` class to indicate the list of
541      geometric state names recognized by the part.
542
543      If this method is not overridden, it will return an empty list, indicating that the part
544      only has one valid geometric state.
545      """
546      return []
547
548
549   # Abstract properties that must be overridden --------------------------------------------------
550
551   @property
552   def mass(self) -> Union[float, Expr]:
553      """Mass (in `kg`) of the SymPart (read-only)."""
554      return self.material_volume * self.material_density
555
556   @property
557   @abc.abstractmethod
558   def material_volume(self) -> Union[float, Expr]:
559      """Material volume (in `m^3`) of the SymPart (read-only)."""
560      raise NotImplementedError
561
562   @property
563   @abc.abstractmethod
564   def displaced_volume(self) -> Union[float, Expr]:
565      """Displaced volume (in `m^3`) of the SymPart (read-only)."""
566      raise NotImplementedError
567
568   @property
569   @abc.abstractmethod
570   def surface_area(self) -> Union[float, Expr]:
571      """Surface/wetted area (in `m^2`) of the SymPart (read-only)."""
572      raise NotImplementedError
573
574   @property
575   @abc.abstractmethod
576   def unoriented_center_of_gravity(self) -> Tuple[Union[float, Expr],
577                                                   Union[float, Expr],
578                                                   Union[float, Expr]]:
579      """Center of gravity (in `m`) of the **unoriented** SymPart (read-only)."""
580      raise NotImplementedError
581
582   @property
583   def oriented_center_of_gravity(self) -> Tuple[Union[float, Expr],
584                                                 Union[float, Expr],
585                                                 Union[float, Expr]]:
586      """Center of gravity (in `m`) of the **oriented** SymPart (read-only)."""
587      origin = self.static_origin.clone() \
588                  if self.static_origin is not None else \
589               Coordinate(self.name + '_origin')
590      center_of_gravity = self.unoriented_center_of_gravity
591      center_of_gravity = [center_of_gravity[0] - (origin.x * self.unoriented_length),
592                           center_of_gravity[1] - (origin.y * self.unoriented_width),
593                           center_of_gravity[2] - (origin.z * self.unoriented_height)]
594      return self.orientation.rotate_point((0.0, 0.0, 0.0), center_of_gravity)
595
596   @property
597   @abc.abstractmethod
598   def unoriented_center_of_buoyancy(self) -> Tuple[Union[float, Expr],
599                                                    Union[float, Expr],
600                                                    Union[float, Expr]]:
601      """Center of buoyancy (in `m`) of the **unoriented** SymPart (read-only)."""
602      raise NotImplementedError
603
604   @property
605   def oriented_center_of_buoyancy(self) -> Tuple[Union[float, Expr],
606                                                  Union[float, Expr],
607                                                  Union[float, Expr]]:
608      """Center of buoyancy (in `m`) of the **oriented** SymPart (read-only)."""
609      origin = self.static_origin.clone() \
610                  if self.static_origin is not None else \
611               Coordinate(self.name + '_origin')
612      center_of_buoyancy = self.unoriented_center_of_buoyancy
613      center_of_buoyancy = [center_of_buoyancy[0] - (origin.x * self.unoriented_length),
614                            center_of_buoyancy[1] - (origin.y * self.unoriented_width),
615                            center_of_buoyancy[2] - (origin.z * self.unoriented_height)]
616      return self.orientation.rotate_point((0.0, 0.0, 0.0), center_of_buoyancy)
617
618   @property
619   @abc.abstractmethod
620   def unoriented_length(self) -> Union[float, Expr]:
621      """X-axis length (in `m`) of the bounding box of the **unoriented** SymPart (read-only)."""
622      raise NotImplementedError
623
624   @property
625   @abc.abstractmethod
626   def unoriented_width(self) -> Union[float, Expr]:
627      """Y-axis width (in `m`) of the bounding box of the **unoriented** SymPart (read-only)."""
628      raise NotImplementedError
629
630   @property
631   @abc.abstractmethod
632   def unoriented_height(self) -> Union[float, Expr]:
633      """Z-axis height (in `m`) of the bounding box of the **unoriented** SymPart (read-only)."""
634      raise NotImplementedError
635
636   @property
637   def oriented_length(self) -> Union[float, Expr]:
638      """X-axis length (in `m`) of the bounding box of the **oriented** SymPart (read-only)."""
639      R = self.orientation.get_rotation_matrix_row(0)
640      original_extents = [self.unoriented_length, self.unoriented_width, self.unoriented_height]
641      return sum([abs(R[i] * original_extents[i]) for i in range(3)])
642
643   @property
644   def oriented_width(self) -> Union[float, Expr]:
645      """Y-axis length (in `m`) of the bounding box of the **oriented** SymPart (read-only)."""
646      R = self.orientation.get_rotation_matrix_row(1)
647      original_extents = [self.unoriented_length, self.unoriented_width, self.unoriented_height]
648      return sum([abs(R[i] * original_extents[i]) for i in range(3)])
649
650   @property
651   def oriented_height(self) -> Union[float, Expr]:
652      """Z-axis length (in `m`) of the bounding box of the **oriented** SymPart (read-only)."""
653      R = self.orientation.get_rotation_matrix_row(2)
654      original_extents = [self.unoriented_length, self.unoriented_width, self.unoriented_height]
655      return sum([abs(R[i] * original_extents[i]) for i in range(3)])

Symbolic part base class from which all SymParts inherit.

Defines the interface to a set of abstract properties that all SymParts possess, including mass, material volume, displaced volume, surface area, centroid, centers of gravity and buouyancy, length, width, and height, among others. These properties may be concrete or symbolic, and they represent the external interface that is expected to be used when accessing the physical properties of a given SymPart.

When creating a new SymPart, its geometry and global placement will be treated as symbolic parameters, and its global orientation will be assumed to be 0 such that the SymPart does not rotate in space. These assumptions can be overridden using the various set_* methods of the SymPart object. Attachment points can be defined to indicate areas on a SymPart that are able to rigidly attach to other SymParts, or connection ports can be defined to indicate areas that are able to flexibly and/or non-mechanically connect to other SymParts.

By default, a SymPart is assumed to be environmentally exposed and thus contribute to such geometric properties as the displaced volume. If a SymPart is not exposed, for example a dry component inside of a pressurized container, this should be specified by calling the set_unexposed() method on the SymPart object.

The local coordinate space of a SymPart is defined with its origin at the front, center, bottom of the part, where the x-axis extends positively from its front to its rear, the y-axis extends positively from the xz-plane to the right of the part when looking from the positive x-axis toward origin, and the z-axis extends positively from the bottom to the top of the part.

SymPart( identifier: str, cad_representation: Union[str, Callable], properties_model: Union[str, symcad.core.ML.NeuralNet.NeuralNet, NoneType], material_density: float)
101   def __init__(self, identifier: str,
102                      cad_representation: Union[str, Callable],
103                      properties_model: Union[str, NeuralNet, None],
104                      material_density: float) -> None:
105      """Initializes an instance of a `SymPart`.
106
107      The underlying `cad_representation` may either be a predefined FreeCAD model or a reference
108      to a method that is able to create such a model. The `material_density` indicates the
109      uniform material density in `kg/m^3` that should be used in mass property calculations for
110      the given SymPart.
111
112      Parameters
113      ----------
114      identifier : `str`
115         Unique, identifying name for the SymPart.
116      cad_representation : `Union[str, Callable]`
117         Either the path to a representative CAD model for the given SymPart or a callable method
118         that can create such a model.
119      properties_model : `Union[str, NeuralNet, None]`
120         Path to or instance of a neural network that may be evaluated to obtain the underlying
121         geometric properties for the given SymPart.
122      material_density: `float`
123         Uniform material density in `kg/m^3` to be used in mass property calculations.
124      """
125      super().__init__()
126      self.name = identifier
127      self.geometry = Geometry(identifier)
128      self.attachment_points = []
129      self.attachments = {}
130      self.connection_ports = []
131      self.connections = {}
132      self.static_origin = None
133      self.static_placement = None
134      self.orientation = Rotation(identifier + '_orientation')
135      self.material_density = material_density
136      self.current_states = []
137      self.is_exposed = True
138      self.__cad__ = ModeledCad(cad_representation) if isinstance(cad_representation, str) else \
139                     ScriptedCad(cad_representation)
140      self.__neural_net__ = NeuralNet(identifier, properties_model) \
141                               if (properties_model and isinstance(properties_model, str)) else \
142                            properties_model

Initializes an instance of a SymPart.

The underlying cad_representation may either be a predefined FreeCAD model or a reference to a method that is able to create such a model. The material_density indicates the uniform material density in kg/m^3 that should be used in mass property calculations for the given SymPart.

Parameters
  • identifier (str): Unique, identifying name for the SymPart.
  • cad_representation (Union[str, Callable]): Either the path to a representative CAD model for the given SymPart or a callable method that can create such a model.
  • properties_model (Union[str, NeuralNet, None]): Path to or instance of a neural network that may be evaluated to obtain the underlying geometric properties for the given SymPart.
  • material_density (float): Uniform material density in kg/m^3 to be used in mass property calculations.
name: str

Unique, identifying name of the SymPart instance.

Part-specific geometry parameters.

attachment_points: List[symcad.core.Coordinate.Coordinate]

List of local points on the SymPart that can attach to other SymParts.

attachments: Dict[str, str]

Dictionary of SymParts and attachment points that are rigidly attached to this SymPart.

connection_ports: List[symcad.core.Coordinate.Coordinate]

List of local points on the SymPart that can connect flexibly to other SymParts.

connections: Dict[str, str]

Dictionary of SymParts and connection ports that are flexibly connected to this SymPart.

static_origin: Optional[symcad.core.Coordinate.Coordinate]

Local point on the unoriented SymPart that is used for static placement and rotation.

static_placement: Optional[symcad.core.Coordinate.Coordinate]

Global static placement of the static_origin of the SymPart.

Global orientation of the SymPart (no rotation by default).

material_density: float

Uniform material density in kg/m^3 to be used in mass property calculations.

current_states: List[str]

List of geometric states for which the SymPart is currently configured.

is_exposed: bool

Whether the SymPart is environmentally exposed versus contained in another element.

def clone(self: ~SymPartSub) -> ~SymPartSub:
182   def clone(self: SymPartSub) -> SymPartSub:
183      """Returns an exact clone of this `SymPart` instance."""
184      return deepcopy(self)

Returns an exact clone of this SymPart instance.

def set_placement( self: ~SymPartSub, *, placement: Tuple[Union[float, sympy.core.expr.Expr, NoneType], Union[float, sympy.core.expr.Expr, NoneType], Union[float, sympy.core.expr.Expr, NoneType]], local_origin: Tuple[float, float, float]) -> ~SymPartSub:
187   def set_placement(self: SymPartSub, *, placement: Tuple[Union[float, Expr, None],
188                                                           Union[float, Expr, None],
189                                                           Union[float, Expr, None]],
190                                          local_origin: Tuple[float, float, float]) -> SymPartSub:
191      """Sets the global placement of the `local_origin` of this SymPart.
192
193      Parameters
194      ----------
195      placement : `Tuple` of `Union[float, sympy.Expr, None]`
196         Global XYZ placement of the `local_origin` of the SymPart in meters. If `None` is
197         specified for any given axis, placement on that axis will be treated as a symbol.
198      local_origin : `Tuple[float, float, float]`
199         Local XYZ point on the unoriented SymPart to be used for static placement and rotation.
200         Each coordinate of the origin point should fall in the range `[0.0, 1.0]` and be relative
201         to the *x-axis* length, *y-axis* width, and *z-axis* height of the SymPart with its
202         origin at the front, left, center of the part.
203
204      Returns
205      -------
206      self : `SymPart`
207         The current SymPart being manipulated.
208      """
209      if self.static_placement is None:
210         self.static_placement = Coordinate(self.name + '_placement')
211      self.static_placement.set(x=placement[0], y=placement[1], z=placement[2])
212      if self.static_origin is None:
213         self.static_origin = Coordinate(self.name + '_origin')
214      self.static_origin.set(x=local_origin[0], y=local_origin[1], z=local_origin[2])
215      return self

Sets the global placement of the local_origin of this SymPart.

Parameters
  • placement (Tuple of Union[float, sympy.Expr, None]): Global XYZ placement of the local_origin of the SymPart in meters. If None is specified for any given axis, placement on that axis will be treated as a symbol.
  • local_origin (Tuple[float, float, float]): Local XYZ point on the unoriented SymPart to be used for static placement and rotation. Each coordinate of the origin point should fall in the range [0.0, 1.0] and be relative to the x-axis length, y-axis width, and z-axis height of the SymPart with its origin at the front, left, center of the part.
Returns
  • self (SymPart): The current SymPart being manipulated.
def set_orientation( self: ~SymPartSub, *, roll_deg: Union[float, sympy.core.expr.Expr, NoneType], pitch_deg: Union[float, sympy.core.expr.Expr, NoneType], yaw_deg: Union[float, sympy.core.expr.Expr, NoneType]) -> ~SymPartSub:
218   def set_orientation(self: SymPartSub, *, roll_deg: Union[float, Expr, None],
219                                            pitch_deg: Union[float, Expr, None],
220                                            yaw_deg: Union[float, Expr, None]) -> SymPartSub:
221      """Sets the global orientation of the SymPart when rotated about its `z-`, `y-`, then
222      `x-axis` (`yaw`, `pitch`, then `roll`) using a right-hand coordinate system.
223
224      A positive `roll_deg` will tilt the part to the left from the point of view of a location
225      inside the part. A positive `pitch_deg` will rotate the nose of the part downward, and a
226      positive `yaw_deg` will rotate the nose of the part toward the left from the point of view
227      of a location inside the part.
228
229      Parameters
230      ----------
231      roll_deg : `Union[float, sympy.Expr, None]`
232         Desired intrinsic roll angle in degrees. If `None` is specified, the angle will be
233         treated as a symbol.
234      pitch_deg : `Union[float, sympy.Expr, None]`
235         Desired intrinsic pitch angle in degrees. If `None` is specified, the angle will be
236         treated as a symbol.
237      yaw_deg : `Union[float, sympy.Expr, None]`
238         Desired intrinsic yaw angle in degress. If `None` is specified, the angle will be
239         treated as a symbol.
240
241      Returns
242      -------
243      self : `SymPart`
244         The current SymPart being manipulated.
245      """
246      self.orientation.set(roll_deg=roll_deg, pitch_deg=pitch_deg, yaw_deg=yaw_deg)
247      return self

Sets the global orientation of the SymPart when rotated about its z-, y-, then x-axis (yaw, pitch, then roll) using a right-hand coordinate system.

A positive roll_deg will tilt the part to the left from the point of view of a location inside the part. A positive pitch_deg will rotate the nose of the part downward, and a positive yaw_deg will rotate the nose of the part toward the left from the point of view of a location inside the part.

Parameters
  • roll_deg (Union[float, sympy.Expr, None]): Desired intrinsic roll angle in degrees. If None is specified, the angle will be treated as a symbol.
  • pitch_deg (Union[float, sympy.Expr, None]): Desired intrinsic pitch angle in degrees. If None is specified, the angle will be treated as a symbol.
  • yaw_deg (Union[float, sympy.Expr, None]): Desired intrinsic yaw angle in degress. If None is specified, the angle will be treated as a symbol.
Returns
  • self (SymPart): The current SymPart being manipulated.
def set_state(self: ~SymPartSub, state_names: Optional[List[str]]) -> ~SymPartSub:
250   def set_state(self: SymPartSub, state_names: Union[List[str], None]) -> SymPartSub:
251      """Sets the geometric configuration of the SymPart according to the indicated `state_names`.
252
253      The concrete, overriding `SymPart` class may use these `state_names` to alter its underlying
254      geometric properties.
255
256      Parameters
257      ----------
258      state_names : `Union[List[str], None]`
259         List of geometric states for which the part should be configured. If set to `None`,
260         the part will be configured in its default state.
261
262      Returns
263      -------
264      self : `SymPart`
265         The current SymPart being manipulated.
266      """
267      self.current_states = [] if not state_names else \
268                            [state for state in state_names if state in self.get_valid_states()]
269      return self

Sets the geometric configuration of the SymPart according to the indicated state_names.

The concrete, overriding SymPart class may use these state_names to alter its underlying geometric properties.

Parameters
  • state_names (Union[List[str], None]): List of geometric states for which the part should be configured. If set to None, the part will be configured in its default state.
Returns
  • self (SymPart): The current SymPart being manipulated.
def set_unexposed(self: ~SymPartSub) -> ~SymPartSub:
272   def set_unexposed(self: SymPartSub) -> SymPartSub:
273      """Specifies that the SymPart is environmentally unexposed.
274
275      This results in the part being excluded from certain geometric property calculations such
276      as `displaced_volume`.
277
278      Returns
279      -------
280      self : `SymPart`
281         The current SymPart being manipulated.
282      """
283      self.is_exposed = False
284      return self

Specifies that the SymPart is environmentally unexposed.

This results in the part being excluded from certain geometric property calculations such as displaced_volume.

Returns
  • self (SymPart): The current SymPart being manipulated.
def set_material_density(self: ~SymPartSub, material_density) -> ~SymPartSub:
287   def set_material_density(self: SymPartSub, material_density) -> SymPartSub:
288      """Sets the material density of the SymPart.
289
290      Parameters
291      ----------
292      material_density: `float`
293         Uniform material density in `kg/m^3` to be used in mass property calculations.
294
295      Returns
296      -------
297      self : `SymPart`
298         The current SymPart being manipulated.
299      """
300      self.material_density = material_density
301      return self

Sets the material density of the SymPart.

Parameters
  • material_density (float): Uniform material density in kg/m^3 to be used in mass property calculations.
Returns
  • self (SymPart): The current SymPart being manipulated.
def add_attachment_point( self: ~SymPartSub, attachment_point_id: str, *, x: Union[float, sympy.core.expr.Expr], y: Union[float, sympy.core.expr.Expr], z: Union[float, sympy.core.expr.Expr]) -> ~SymPartSub:
304   def add_attachment_point(self: SymPartSub, attachment_point_id: str, *,
305                                              x: Union[float, Expr],
306                                              y: Union[float, Expr],
307                                              z: Union[float, Expr]) -> SymPartSub:
308      """Adds a local attachment point to the SymPart.
309
310      Each coordinate of the attachment point should fall in the range `[0.0, 1.0]` and be
311      relative to the *x-axis* length, *y-axis* width, and *z-axis* height of the SymPart with
312      its origin at the front, left, center of the part.
313
314      Parameters
315      ----------
316      attachment_point_id : `str`
317         Unique identifier for the new attachment point.
318      x : `Union[float, sympy.Expr]`
319         Local x-axis placement of the attachment point on the SymPart relative to its length.
320      y : `Union[float, sympy.Expr]`
321         Local y-axis placement of the attachment point on the SymPart relative to its width.
322      z : `Union[float, sympy.Expr]`
323         Local z-axis placement of the attachment point on the SymPart relative to its height.
324
325      Returns
326      -------
327      self : `SymPart`
328         The current SymPart being manipulated.
329      """
330      if attachment_point_id in [point.name for point in self.attachment_points]:
331         raise ValueError('An attachment point with the ID "{}" already exists'
332                          .format(attachment_point_id))
333      self.attachment_points.append(Coordinate(attachment_point_id, x=x, y=y, z=z))
334      return self

Adds a local attachment point to the SymPart.

Each coordinate of the attachment point should fall in the range [0.0, 1.0] and be relative to the x-axis length, y-axis width, and z-axis height of the SymPart with its origin at the front, left, center of the part.

Parameters
  • attachment_point_id (str): Unique identifier for the new attachment point.
  • x (Union[float, sympy.Expr]): Local x-axis placement of the attachment point on the SymPart relative to its length.
  • y (Union[float, sympy.Expr]): Local y-axis placement of the attachment point on the SymPart relative to its width.
  • z (Union[float, sympy.Expr]): Local z-axis placement of the attachment point on the SymPart relative to its height.
Returns
  • self (SymPart): The current SymPart being manipulated.
def add_connection_port( self: ~SymPartSub, connection_port_id: str, *, x: Union[float, sympy.core.expr.Expr], y: Union[float, sympy.core.expr.Expr], z: Union[float, sympy.core.expr.Expr]) -> ~SymPartSub:
337   def add_connection_port(self: SymPartSub, connection_port_id: str, *,
338                                              x: Union[float, Expr],
339                                              y: Union[float, Expr],
340                                              z: Union[float, Expr]) -> SymPartSub:
341      """Adds a local connection port to the SymPart.
342
343      Each coordinate of the connection port should fall in the range `[0.0, 1.0]` and be
344      relative to the *x-axis* length, *y-axis* width, and *z-axis* height of the SymPart with
345      its origin at the front, left, center of the part.
346
347      Parameters
348      ----------
349      connection_port_id : `str`
350         Unique identifier for the new connection port.
351      x : `Union[float, sympy.Expr]`
352         Local x-axis placement of the connection port on the SymPart relative to its length.
353      y : `Union[float, sympy.Expr]`
354         Local y-axis placement of the connection port on the SymPart relative to its width.
355      z : `Union[float, sympy.Expr]`
356         Local z-axis placement of the connection port on the SymPart relative to its height.
357
358      Returns
359      -------
360      self : `SymPart`
361         The current SymPart being manipulated.
362      """
363      if connection_port_id in [port.name for port in self.connection_ports]:
364         raise ValueError('A connection port with the ID "{}" already exists'
365                          .format(connection_port_id))
366      self.connection_ports.append(Coordinate(connection_port_id, x=x, y=y, z=z))
367      return self

Adds a local connection port to the SymPart.

Each coordinate of the connection port should fall in the range [0.0, 1.0] and be relative to the x-axis length, y-axis width, and z-axis height of the SymPart with its origin at the front, left, center of the part.

Parameters
  • connection_port_id (str): Unique identifier for the new connection port.
  • x (Union[float, sympy.Expr]): Local x-axis placement of the connection port on the SymPart relative to its length.
  • y (Union[float, sympy.Expr]): Local y-axis placement of the connection port on the SymPart relative to its width.
  • z (Union[float, sympy.Expr]): Local z-axis placement of the connection port on the SymPart relative to its height.
Returns
  • self (SymPart): The current SymPart being manipulated.
def attach( self: ~SymPartSub, local_attachment_id: str, remote_part: SymPart, remote_attachment_id: str) -> ~SymPartSub:
370   def attach(self: SymPartSub, local_attachment_id: str,
371                                remote_part: SymPart,
372                                remote_attachment_id: str) -> SymPartSub:
373      """Creates a rigid attachment between a local and remote attachment point.
374
375      Parameters
376      ----------
377      local_attachment_id : `str`
378         Identifier of the local attachment point to which to attach.
379      remote_part : `SymPart`
380         The remote SymPart to which to make an attachment.
381      remote_attachment_id : `str`
382         Identifier of the remote attachment point to which to attach.
383
384      Returns
385      -------
386      self : `SymPart`
387         The current SymPart being manipulated.
388      """
389
390      # Ensure that the requested attachment is valid
391      if self.name == remote_part.name:
392         raise ValueError('The local and attached parts cannot both have the same name "{}"'
393                           .format(self.name))
394      if local_attachment_id not in [point.name for point in self.attachment_points]:
395         raise ValueError('The local attachment point identifier "{}" does not exist'
396                           .format(local_attachment_id))
397      if remote_attachment_id not in [point.name for point in remote_part.attachment_points]:
398         raise ValueError('The remote attachment point identifier "{}" does not exist'
399                           .format(remote_attachment_id))
400      if local_attachment_id in self.attachments:
401         raise ValueError('The local attachment point "{}" is already being used'
402                           .format(local_attachment_id))
403      if remote_attachment_id in remote_part.attachments:
404         raise ValueError('The remote attachment point "{}" is already being used'
405                           .format(remote_attachment_id))
406
407      # Make the rigid attachment in both directions
408      self.attachments[local_attachment_id] = remote_part.name + '#' + remote_attachment_id
409      remote_part.attachments[remote_attachment_id] = self.name + '#' + local_attachment_id
410      return self

Creates a rigid attachment between a local and remote attachment point.

Parameters
  • local_attachment_id (str): Identifier of the local attachment point to which to attach.
  • remote_part (SymPart): The remote SymPart to which to make an attachment.
  • remote_attachment_id (str): Identifier of the remote attachment point to which to attach.
Returns
  • self (SymPart): The current SymPart being manipulated.
def connect( self: ~SymPartSub, local_connection_id: str, remote_part: SymPart, remote_connection_id: str) -> ~SymPartSub:
413   def connect(self: SymPartSub, local_connection_id: str,
414                                 remote_part: SymPart,
415                                 remote_connection_id: str) -> SymPartSub:
416      """Creates a non-rigid connection between a local and remote connection port.
417
418      Parameters
419      ----------
420      local_connection_id : `str`
421         Identifier of the local connection port to which to connect.
422      remote_part : `SymPart`
423         The remote SymPart to which to make a connection.
424      remote_connection_id : `str`
425         Identifier of the remote connection port to which to connect.
426
427      Returns
428      -------
429      self : `SymPart`
430         The current SymPart being manipulated.
431      """
432
433      # Ensure that the requested connection is valid
434      if self.name == remote_part.name:
435         raise ValueError('The local and connected parts cannot both have the same name "{}"'
436                           .format(self.name))
437      if local_connection_id not in [port.name for port in self.connection_ports]:
438         raise ValueError('The local connection port identifier "{}" does not exist'
439                           .format(local_connection_id))
440      if remote_connection_id not in [port.name for port in remote_part.connection_ports]:
441         raise ValueError('The remote connection port identifier "{}" does not exist'
442                           .format(remote_connection_id))
443      if local_connection_id in self.connections:
444         raise ValueError('The local connection port "{}" is already being used'
445                           .format(local_connection_id))
446      if remote_connection_id in remote_part.connections:
447         raise ValueError('The remote connection port "{}" is already being used'
448                           .format(remote_connection_id))
449
450      # Make the flexible connection in both directions
451      self.connections[local_connection_id] = remote_part.name + '#' + remote_connection_id
452      remote_part.connections[remote_connection_id] = self.name + '#' + local_connection_id
453      return self

Creates a non-rigid connection between a local and remote connection port.

Parameters
  • local_connection_id (str): Identifier of the local connection port to which to connect.
  • remote_part (SymPart): The remote SymPart to which to make a connection.
  • remote_connection_id (str): Identifier of the remote connection port to which to connect.
Returns
  • self (SymPart): The current SymPart being manipulated.
def get_cad_physical_properties(self, normalize_origin: Optional[bool] = False) -> Dict[str, float]:
456   def get_cad_physical_properties(self,
457                                   normalize_origin: Optional[bool] = False) -> Dict[str, float]:
458      """Retrieves the set of physical properties of the SymPart as reported by the underlying
459      CAD model.
460
461      Parameters
462      ----------
463      normalize_origin : `bool`, optional, default=False
464         Return physical properties with respect to the front, left, bottom corner of the
465         underlying CAD model.
466
467      Returns
468      -------
469      `Dict[str, float]`
470         A list of physical properties as calculated from the underlying CAD model.
471      """
472      placement_center = self.static_origin.as_tuple() if \
473                            self.static_origin is not None else \
474                         (0.0, 0.0, 0.0)
475      return self.__cad__.get_physical_properties(self.geometry.__dict__,
476                                                  placement_center,
477                                                  self.orientation.as_tuple(),
478                                                  self.material_density,
479                                                  normalize_origin)

Retrieves the set of physical properties of the SymPart as reported by the underlying CAD model.

Parameters
  • normalize_origin (bool, optional, default=False): Return physical properties with respect to the front, left, bottom corner of the underlying CAD model.
Returns
  • Dict[str, float]: A list of physical properties as calculated from the underlying CAD model.
def export( self, save_path: str, export_type: Literal['freecad', 'step', 'stl']) -> None:
482   def export(self, save_path: str, export_type: Literal['freecad', 'step', 'stl']) -> None:
483      """Exports the SymPart to an external CAD representation.
484
485      Supported CAD model formats currently include FreeCAD, STEP, and STL.
486
487      Parameters
488      ----------
489      save_path : `str`
490         Output file path at which to store the the generated CAD model.
491      export_type : {'freecad', 'step', 'stl'}
492         Format of the CAD model to export.
493      """
494      placement_center = self.static_origin.as_tuple() if \
495                            self.static_origin is not None else \
496                         (0.0, 0.0, 0.0)
497      self.__cad__.export_model(save_path,
498                                export_type,
499                                self.geometry.__dict__,
500                                placement_center,
501                                self.orientation.as_tuple())

Exports the SymPart to an external CAD representation.

Supported CAD model formats currently include FreeCAD, STEP, and STL.

Parameters
  • save_path (str): Output file path at which to store the the generated CAD model.
  • export_type ({'freecad', 'step', 'stl'}): Format of the CAD model to export.
@abc.abstractmethod
def set_geometry(self: ~SymPartSub, **kwargs) -> ~SymPartSub:
506   @abc.abstractmethod
507   def set_geometry(self: SymPartSub, **kwargs) -> SymPartSub:
508      """Abstract method that must be overridden by a concrete `SymPart` class to allow setting
509      its physical geometry.
510
511      Parameters
512      ----------
513      **kwargs : `Dict`
514         Set of named parameters that define the geometry of a SymPart.
515
516      Raises
517      -------
518      NotImplementedError
519         If the implementing `SymPart` class does not override this method.
520      """
521      raise NotImplementedError

Abstract method that must be overridden by a concrete SymPart class to allow setting its physical geometry.

Parameters
  • **kwargs (Dict): Set of named parameters that define the geometry of a SymPart.
Raises
  • NotImplementedError: If the implementing SymPart class does not override this method.
@abc.abstractmethod
def get_geometric_parameter_bounds(self: ~SymPartSub, parameter: str) -> Tuple[float, float]:
523   @abc.abstractmethod
524   def get_geometric_parameter_bounds(self: SymPartSub, parameter: str) -> Tuple[float, float]:
525      """Abstract method that must be overridden by a concrete `SymPart` class to return the
526      minimum and maximum expected bounds for a given geometric parameter.
527
528      Parameters
529      ----------
530      parameter : `str`
531         Name of the geometric parameter for which to return the minimum and maximum bounds.
532
533      Returns
534      -------
535      `Tuple[float, float]`
536         Minimum and maximum bounds for the specified geometric `parameter`."""
537      raise NotImplementedError

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.
def get_valid_states(self: ~SymPartSub) -> List[str]:
539   def get_valid_states(self: SymPartSub) -> List[str]:
540      """Method that may be overridden by a concrete `SymPart` class to indicate the list of
541      geometric state names recognized by the part.
542
543      If this method is not overridden, it will return an empty list, indicating that the part
544      only has one valid geometric state.
545      """
546      return []

Method that may be overridden by a concrete SymPart class to indicate the list of geometric state names recognized by the part.

If this method is not overridden, it will return an empty list, indicating that the part only has one valid geometric state.

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

Mass (in kg) of the SymPart (read-only).

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).

oriented_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 oriented 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).

oriented_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 oriented 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).