Source code for Karana.Dynamics.SOADyn_types

# Copyright (c) 2024-2026 Karana Dynamics Pty Ltd. All rights reserved.
#
# NOTICE TO USER:
#
# This source code and/or documentation (the "Licensed Materials") is
# the confidential and proprietary information of Karana Dynamics Inc.
# Use of these Licensed Materials is governed by the terms and conditions
# of a separate software license agreement between Karana Dynamics and the
# Licensee ("License Agreement"). Unless expressly permitted under that
# agreement, any reproduction, modification, distribution, or disclosure
# of the Licensed Materials, in whole or in part, to any third party
# without the prior written consent of Karana Dynamics is strictly prohibited.
#
# THE LICENSED MATERIALS ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
# KARANA DYNAMICS DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING
# BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, NON-INFRINGEMENT, AND
# FITNESS FOR A PARTICULAR PURPOSE.
#
# IN NO EVENT SHALL KARANA DYNAMICS BE LIABLE FOR ANY DAMAGES WHATSOEVER,
# INCLUDING BUT NOT LIMITED TO LOSS OF PROFITS, DATA, OR USE, EVEN IF
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGES, WHETHER IN CONTRACT, TORT,
# OR OTHERWISE ARISING OUT OF OR IN CONNECTION WITH THE LICENSED MATERIALS.
#
# U.S. Government End Users: The Licensed Materials are a "commercial item"
# as defined at 48 C.F.R. 2.101, and are provided to the U.S. Government
# only as a commercial end item under the terms of this license.
#
# Any use of the Licensed Materials in individual or commercial software must
# include, in the user documentation and internal source code comments,
# this Notice, Disclaimer, and U.S. Government Use Provision.

"""Collection of types that represent SOADyn classes as DataStructs.

There are DataStructs that represent the classes (the structure of the Multibody and associated classes) and 
their states. For example, MultibodyDS is used to represent the data needed to create a Multibody class and all
the associated bodies, nodes, etc. SubGraphStateDS is used to represent the coordinates of the various subhinges,
force nodes, etc. that make up the SubGraph. Together, they can be used to represent the Multibody at an instance
in time (both the structure and state).
"""

import numpy as np
from typing import (
    Generic,
    Self,
    TypeVar,
    cast,
    TypeAlias,
    ClassVar,
    Optional,
    Annotated,
    Literal,
    TYPE_CHECKING,
)
from pathlib import Path
from Karana.KUtils.DataStruct import DataStruct, IdMixin, NestedBaseMixin
from Karana.KUtils.Ktyping import (
    Vec3,
    Mass,
    Length3,
    Inertia,
    Vec,
    normCheck,
    Acceleration3,
    Mat6n,
    npSizeCheck,
)
from Karana.KUtils.Kquantities import convert
from Karana.Math import UnitQuaternion, HomTran, SpatialInertia, RotationMatrix, SpatialVector
from Karana.Math import IntegratorType
from Karana.Math.Integrator_types import IntegratorOptionsDS
from Karana.Dynamics import (
    PhysicalBody,
    HingeType,
    PhysicalSubhinge,
    ModelManager,
    StatePropagator,
    MMSolverType,
    SubTree,
    SubGraph,
    Multibody,
    CoordData,
    SubhingeType,
    LockedSubhinge,
    PinSubhinge,
    LinearSubhinge,
    Linear3Subhinge,
    SphericalSubhinge,
    ScrewSubhinge,
    PhysicalHinge,
    HingeBase,
    LoopConstraintBase,
    LoopConstraintCutJoint,
    LoopConstraintConVel,
    FramePairHinge,
    ConstraintNode,
    CoordinateConstraint,
    CoordBase,
    Node,
    ConstraintNode,
    BilateralConstraintType,
)
from Karana.Frame import FrameContainer, Frame, PrescribedFrameToFrame
from urchin import Link, Joint, URDF, Inertial, JointLimit
from Karana.Scene.Scene_types import ScenePartSpecDS
from Karana.Scene import (
    BoxGeometry,
    Color,
    CylinderGeometry,
    SphereGeometry,
    PhysicalMaterial,
    AssimpImporter,
    PhysicalMaterialInfo,
    StaticMeshGeometry,
    LAYER_PHYSICAL_GRAPHICS,
    LAYER_COLLISION,
)
from Karana.Models import BaseKModel
from Karana.Scene import ProxyScene
from Karana.Core import warn
import os
from contextlib import contextmanager
from abc import abstractmethod
from pydantic import SerializeAsAny

if TYPE_CHECKING:
    from Karana.Dynamics import PhysicalModalBody, ModalNodeDeformationProvider

    HAVE_MODAL_FLEX = True

try:
    # Import only for type hints
    from Karana.Dynamics import PhysicalModalBody, ModalNodeDeformationProvider

    HAVE_MODAL_FLEX = True
except:
    HAVE_MODAL_FLEX = False


SubhingePyType = (
    LockedSubhinge
    | PinSubhinge
    | LinearSubhinge
    | Linear3Subhinge
    | ScrewSubhinge
    | SphericalSubhinge
)


[docs] class ConstraintFrameDS(DataStruct): """DataStruct used to represent a frame that is part of a constraint. Parameters ---------- name : str Name of the frame. translation : Vec3 Translation of the frame relative to its parent. unit_quaternion : UnitQuaternion UnitQuaternion that represents the orientation of the frame relative to its parent. parent : Literal["newtonian","vroot"] Parent frame. For constraint frames, this can only be the newtonian frame or the vroot (virtual root). """ name: str translation: Length3 unit_quaternion: UnitQuaternion parent: Literal["newtonian", "vroot"] @property def rel_transform(self) -> HomTran: """The HomTran of the constraint frame relative to it's parent.""" return HomTran(self.unit_quaternion, convert(self.translation))
[docs] def toFrame(self, mb: Multibody) -> Frame: """Create an instance of a Frame from this ConstraintFrameDS. Parameters ---------- mb : Multibody The Multibody to add the constraint frame to. Returns ------- Frame Frame instance created from this ConstraintFrameDS. """ fc = mb.getNewtonianFrame().container() if self.parent == "newtonian": parent = mb.getNewtonianFrame() else: parent = cast(Frame, mb.virtualRoot()) frame = Frame(self.name, fc) pf2f = PrescribedFrameToFrame(parent, frame) pf2f.setRelTransform(self.rel_transform) return frame
[docs] @classmethod def fromFrame(cls, frame: Frame, mb: Multibody) -> Self: """Create an instance of ConstraintFrameDS from the provided Frame. Parameters ---------- frame : Frame The Frame used to create the ConstraintFrameDS. mb : Multibody The Multibody using the constraint frame. Returns ------- Self A ConstraintFrameDS that represents the provided Frame. """ dark = frame.edge().oframe() if dark == mb.getNewtonianFrame(): parent = "newtonian" elif dark == cast(Frame, mb.virtualRoot()): parent = "vroot" else: raise ValueError( f'Frame "{frame.name()}" is not attached to the multibody\'s newtonian frame or virtual root frame.' ) T = frame.edge().relTransform() return cls( name=frame.name(), translation=T.getTranslation(), unit_quaternion=T.getUnitQuaternion(), parent=parent, )
[docs] class NodeDS(DataStruct, IdMixin[Node], NestedBaseMixin): """DataStruct used to represent a Node. Parameters ---------- name : str The name of the node. force_node : bool Whether or not this node is a force node. translation : Length3 = np.zeros(3) The body-to-node translation. unit_quaternion : UnitQuaternion = UnitQuaternion(0.0, 0.0, 0.0, 1.0) The body-to-node unit quaternion. """ name: str constraint_node: bool force_node: bool translation: Length3 = np.zeros(3) unit_quaternion: UnitQuaternion = UnitQuaternion(0.0, 0.0, 0.0, 1.0) @property def body_to_node_transform(self) -> HomTran: """A HomTran that represents the body to node transform of the node. Returns ------- HomTran HomTran that represents the body to node transform of the node. """ return HomTran(self.unit_quaternion, convert(self.translation))
[docs] def toNode(self, body: PhysicalBody) -> Node: """Create a Node from this NodeDS. Parameters ---------- body : PhysicalBody The body to attach the node to. """ if self.constraint_node: nd = ConstraintNode.lookupOrCreate(self.name, body) else: nd = Node.lookupOrCreate(self.name, bd=body, force_node=self.force_node) nd.setBodyToNodeTransform(self.body_to_node_transform) self.addObjectFromId(nd) return nd
[docs] @classmethod def fromNode(cls, nd: Node) -> Self: """Create a NodeDS from the given Node. Parameters ---------- nd : Node The Node to use to create this NodeDS. """ T = nd.getBodyToNodeTransform() dark = cls( name=nd.name(), constraint_node=isinstance(nd, ConstraintNode), force_node=nd.isExternalForceNode(), translation=T.getTranslation(), unit_quaternion=T.getUnitQuaternion(), ) dark._id = nd.id() return dark
[docs] class ForceNodeStateDS(DataStruct): """This class holds state data for a force node. Parameters ---------- name : str Name of the node. external_spatial_force : SpatialVector The external spatial force on the node in the local node frame. """ name: str external_spatial_force: SpatialVector
[docs] @classmethod def fromNode(cls, force_node: Node) -> Self: """Create a ForceNodeStateDS from the provided node. Parameters ---------- force_node : Node The force node used to create the ForceNodeStateDS. Returns ------- Self The ForceNodeStateDS. """ return cls(name=force_node.name(), external_spatial_force=force_node.getSpForce())
[docs] def toNode(self, nd: Node) -> None: """Set the state of the provided node. Parameters ---------- nd : Node The node to set the state of. """ nd.setExternalSpForce(self.external_spatial_force)
[docs] class SubhingeDS(DataStruct, NestedBaseMixin): """Base class for all subhinges. Parameters ---------- subhinge_type: SubhingeType The subhinge type associated with the subhinge. """ subhinge_type: SubhingeType
[docs] @classmethod def fromSubhinge(cls, sh: SubhingePyType) -> "SubhingeDS": """Create an instance of SubhingeDS from the provided Subhinge. Parameters ---------- sh : SubhingeType The Subhinge used to create the SubhingeDS. Returns ------- Self A SubhingeDS that represents the provided Subhinge. """ if isinstance(sh, PinSubhinge): return PinSubhingeDS.fromSubhinge(sh) elif isinstance(sh, LockedSubhinge): return LockedSubhingeDS.fromSubhinge(sh) elif isinstance(sh, LinearSubhinge): return LinearSubhingeDS.fromSubhinge(sh) elif isinstance(sh, SphericalSubhinge): return SphericalSubhingeDS.fromSubhinge(sh) elif isinstance(sh, Linear3Subhinge): return Linear3SubhingeDS.fromSubhinge(sh) else: # Screw subhinge sh = cast(ScrewSubhinge, sh) return ScrewSubhingeDS.fromSubhinge(sh)
[docs] @abstractmethod def setSubhingeValues(self, sh: SubhingePyType) -> None: """Set the subhinge values for the provided subhinge.""" ...
[docs] class PinSubhingeDS(SubhingeDS): """PinSubhinge DataStruct. Parameters ---------- unit_axis : Vec3 Unit axis for the pin subhinge. joint_limits: Annotated[Vec, npSizeCheck((2,))] The lower and upper bound for the joint limit. prescribed : bool Determines if the subhinge is prescribed or not. """ subhinge_type: SubhingeType = SubhingeType.PIN unit_axis: Annotated[Vec3, normCheck(1.0)] joint_limits: Annotated[Vec, npSizeCheck((2,))] = np.array([-np.inf, np.inf]) prescribed: bool
[docs] @classmethod def fromSubhinge(cls, sh: PinSubhinge) -> Self: # pyright: ignore - override is okay. """Create a PinSubhingeDS from a PinSubhinge.""" joint_limits = sh.getJointLimits() if np.all(np.isnan(joint_limits)): joint_limits = np.array([-np.inf, np.inf]) return cls( subhinge_type=SubhingeType.PIN, unit_axis=sh.getUnitAxis(), prescribed=sh.getPrescribed(), joint_limits=joint_limits, )
[docs] def setSubhingeValues(self, sh: PinSubhinge) -> None: # pyright: ignore - override is okay. """Set the values of a PinSubhinge.""" sh.setUnitAxis(self.unit_axis) sh.setPrescribed(self.prescribed) if not np.all(self.joint_limits == np.array([-np.inf, np.inf])): sh.setJointLimits(self.joint_limits)
[docs] class LinearSubhingeDS(SubhingeDS): """LinearSubhinge DataStruct. Parameters ---------- unit_axis : Vec3 Unit axis for the pin subhinge. joint_limits: Annotated[Vec, npSizeCheck((2,))] The lower and upper bound for the joint limit. prescribed : bool Determines if the subhinge is prescribed or not. """ subhinge_type: SubhingeType = SubhingeType.LINEAR unit_axis: Annotated[Vec3, normCheck(1.0)] joint_limits: Annotated[Vec, npSizeCheck((2,))] = np.array([-np.inf, np.inf]) prescribed: bool
[docs] @classmethod def fromSubhinge(cls, sh: LinearSubhinge) -> Self: # pyright: ignore - override is okay. """Create a LinearSubhingeDS from a LinearSubhinge.""" joint_limits = sh.getJointLimits() if np.all(np.isnan(joint_limits)): joint_limits = np.array([-np.inf, np.inf]) return cls( unit_axis=sh.getUnitAxis(), prescribed=sh.getPrescribed(), joint_limits=joint_limits )
[docs] def setSubhingeValues(self, sh: LinearSubhinge) -> None: # pyright: ignore - override is okay. """Set the values of a LinearSubhinge.""" sh.setUnitAxis(self.unit_axis) sh.setPrescribed(self.prescribed) if not np.all(self.joint_limits == np.array([-np.inf, np.inf])): sh.setJointLimits(self.joint_limits)
[docs] class SphericalSubhingeDS(SubhingeDS): """SphericalSubhinge DataStruct. prescribed : bool Determines if the subhinge is prescribed or not. """ subhinge_type: SubhingeType = SubhingeType.SPHERICAL prescribed: bool
[docs] @classmethod def fromSubhinge(cls, sh: SphericalSubhinge) -> Self: # pyright: ignore - override is okay. """Create a SphericalSubhingeDS from a SphericalSubhinge.""" return cls(prescribed=sh.getPrescribed())
[docs] def setSubhingeValues( self, sh: SphericalSubhinge ) -> None: # pyright: ignore - override is okay. """Set the values of a SphericalSubhinge.""" sh.setPrescribed(self.prescribed)
[docs] class Linear3SubhingeDS(SubhingeDS): """Linear3Subhinge DataStruct. Parameters ---------- prescribed : bool Determines if the subhinge is prescribed or not. """ subhinge_type: SubhingeType = SubhingeType.LINEAR3 prescribed: bool
[docs] @classmethod def fromSubhinge(cls, sh: Linear3Subhinge) -> Self: # pyright: ignore - override is okay. """Create a Linear3SubhingeDS from a Linear3Subhinge.""" return cls(prescribed=sh.getPrescribed())
[docs] def setSubhingeValues(self, sh: Linear3Subhinge) -> None: # pyright: ignore - override is okay. """Set the values of a Linear3Subhinge.""" sh.setPrescribed(self.prescribed)
[docs] class LockedSubhingeDS(SubhingeDS): """LockedSubhinge DataStruct.""" subhinge_type: SubhingeType = SubhingeType.LOCKED
[docs] @classmethod def fromSubhinge(cls, _: LockedSubhinge) -> Self: # pyright: ignore - override is okay. """Create a LockedSubhingeDS from a LockedSubhinge.""" return cls()
[docs] def setSubhingeValues(self, _: LockedSubhinge) -> None: # pyright: ignore - override is okay. """Set the values of a LockedSubhinge.""" pass
[docs] class ScrewSubhingeDS(SubhingeDS): """ScrewSubhinge DataStruct. Parameters ---------- prescribed : bool Determines if the subhinge is prescribed or not. unit_axis : Vec3 Unit axis for the pin subhinge. pitch : float The pitch of the screw subhinge. joint_limits: Annotated[Vec, npSizeCheck((2,))] The lower and upper bound for the joint limit. """ subhinge_type: SubhingeType = SubhingeType.SCREW prescribed: bool unit_axis: Annotated[Vec3, normCheck(1.0)] pitch: float joint_limits: Annotated[Vec, npSizeCheck((2,))] = np.array([-np.inf, np.inf])
[docs] @classmethod def fromSubhinge(cls, sh: ScrewSubhinge) -> Self: # pyright: ignore - override is okay. """Create a ScrewSubhingeDS from a ScrewSubhinge.""" joint_limits = sh.getJointLimits() if np.all(np.isnan(joint_limits)): joint_limits = np.array([-np.inf, np.inf]) return cls( unit_axis=sh.getUnitAxis(), pitch=sh.getPitch(), prescribed=sh.getPrescribed(), joint_limits=joint_limits, )
[docs] def setSubhingeValues(self, sh: ScrewSubhinge) -> None: # pyright: ignore - override is okay. """Set the values of a ScrewSubhinge.""" sh.setUnitAxis(self.unit_axis) sh.setPitch(self.pitch) sh.setPrescribed(self.prescribed) if not np.all(self.joint_limits == np.array([-np.inf, np.inf])): sh.setJointLimits(self.joint_limits)
[docs] class SubhingeStateDS(DataStruct): """This class holds the state of a subhinge. Parameters ---------- Q : Vec Positional state of the subhinge. U : Vec Velocity state of the subhinge. Udot : Vec Acceleration state of the subhinge. T : Vec Force state of the subhinge. """ Q: Vec U: Vec Udot: Vec T: Vec
[docs] @classmethod def fromSubhinge(cls, sh: SubhingePyType) -> Self: """Create a SubhingeStateDS from a subhinge. Parameters ---------- sh : SubhingeBase The subhinge to create this from. Returns ------- SubhingeStateDS A SubhingeStateDS that reflects the state of the provided subhinge. """ return cls(Q=sh.getQ(), U=sh.getU(), Udot=sh.getUdot(), T=sh.getT())
[docs] def toSubhinge(self, sh: SubhingePyType) -> None: """Set the state of the provided subhinge. Parameters ---------- sh : SubhingeBase The subhinge whose state will be set using this. """ sh.setQ(self.Q) sh.setU(self.U) sh.setUdot(self.Udot) sh.setT(self.T)
[docs] class HingeDS(DataStruct): """Base class for all hinge types. Hinges that have 0 axes associated with them, e.g., BALL, use this DataStruct. Classes with more axes (or information) will use derived classes. Parameters ---------- hinge_type : HingeType The hinge type associated with the hinge. subhinges: list[SubhingeDS] A list of subhinges that make up this hinge. """ hinge_type: HingeType subhinges: list[SerializeAsAny[SubhingeDS]]
[docs] def toHinge(self, parent_bd: PhysicalBody, child_bd: PhysicalBody) -> PhysicalHinge: """Create a Hinge between the provided bodies. Parameters ---------- parent_bd : PhysicalBody The parent body for the hinge. child_bd : PhysicalBody The child body for the hinge. Returns ------- PhysicalHinge The hinge that was created between the two provided bodies. """ if self.hinge_type == HingeType.CUSTOM: hge = PhysicalHinge( parent_bd, child_bd, HingeType.CUSTOM, [x.subhinge_type for x in self.subhinges] ) else: hge = PhysicalHinge(parent_bd, child_bd, self.hinge_type) for k in range(hge.nSubhinges()): self.subhinges[k].setSubhingeValues( hge.subhinge(k) # pyright: ignore These will be the correct type. ) return hge
[docs] @classmethod def fromHinge(cls, hge: FramePairHinge, htype: HingeType) -> Self: """Create a HingeDS from the provided hinge. Parameters ---------- hge : FramePairHinge The FramePairHinge used to create the HingeDS. htype : HingeType The hinge type associated with the FramePairHinge. Returns ------- Self A HingeDS that represents the provided FramePairHinge. """ return cls( hinge_type=htype, subhinges=cast( list[SubhingeDS], [ SubhingeDS.fromSubhinge(cast(SubhingePyType, hge.subhinge(k))) for k in range(hge.nSubhinges()) ], ), )
[docs] class HingeStateDS(DataStruct): """Represents the state of a hinge. Parameters ---------- subhinges : list[SubhingeStateDS] Data for the hinge's subhinges. """ subhinges: list[SubhingeStateDS]
[docs] @classmethod def fromHinge(cls, hge: HingeBase) -> Self: """Create a HingeStateDS from the provided hinge. Parameters ---------- hge : HingeBase The hinge used to this HingeStateDS. Returns ------- HingeStateDS A HingeStateDS that reflects the state of the provided hinge. """ return cls( subhinges=[ SubhingeStateDS.fromSubhinge(cast(SubhingePyType, hge.subhinge(k))) for k in range(hge.nSubhinges()) ] )
[docs] def toHinge(self, hge: HingeBase) -> None: """Set the state of the provided hinge using this. Parameters ---------- hge : HingeBase The hinge whose state will be set from this. """ for k, sh in enumerate(self.subhinges): sh.toSubhinge(cast(SubhingePyType, hge.subhinge(k)))
[docs] class LoopConstraintDS(DataStruct, NestedBaseMixin): """Represents a loop constraint. Parameters ---------- name: str The name of the LoopConstraintCutJoint. get_oframe: dict[Literal["body","frame","node"],str] A dictionary that specifies how to get to the associated oframe of the LoopConstraint. This uses body names, frame names, and node names to drill down to the correct location. get_pframe: dict[Literal["body","frame","node"],str] A dictionary that specifies how to get to the associated pframe of the LoopConstraint. This uses body names, frame names, and node names to drill down to the correct location. """ name: str get_oframe: dict[Literal["body", "frame", "node"], str] get_pframe: dict[Literal["body", "frame", "node"], str]
[docs] @classmethod def fromLoopConstraint(cls, lc: LoopConstraintBase) -> Self: """Create a LoopConstraintDS from the provided LoopConstraintBase. Parameters ---------- lc : LoopConstraintBase The LoopConstraintBase used to build this LoopConstraintDS. Returns ------- LoopConstraintDS A LoopConstraintDS with the provided LoopConstraint information. """ if isinstance(lc, LoopConstraintCutJoint): return cast(Self, LoopConstraintCutJointDS.fromLoopConstraintCutJoint(lc)) if isinstance(lc, LoopConstraintConVel): return cast(Self, LoopConstraintConVelDS.fromLoopConstraintConVel(lc)) raise NotImplementedError(lc)
[docs] def toCutJointConstraint(self, mb: Multibody) -> LoopConstraintBase: """Create a LoopConstraintBase from the LoopConstraintDS. Parameters ---------- mb : Multibody The multibody to add this loop constraint to. Returns ------- LoopConstraintBase The LoopConstraintBase created from this LoopConstraintDS. """ if isinstance(self, LoopConstraintCutJointDS): return self.toCutJointConstraintCutJoint(mb) if isinstance(self, LoopConstraintConVelDS): return self.toCutJointConstraintConVel(mb) raise NotImplementedError(repr(self))
[docs] class LoopConstraintCutJointDS(LoopConstraintDS): """Represents a loop constraint across a hinge. Parameters ---------- name: str The name of the LoopConstraintCutJoint. hinge: HingeDS The hinge information associated with the LoopConstraintCutJoint. get_oframe: dict[Literal["body","frame","node"],str] A dictionary that specifies how to get to the associated oframe of the LoopConstraintCutJoint. This uses body names, frame names, and node names to drill down to the correct location. get_pframe: dict[Literal["body","frame","node"],str] A dictionary that specifies how to get to the associated pframe of the LoopConstraintCutJoint. This uses body names, frame names, and node names to drill down to the correct location. """ loop_constraint_type: Literal["LoopConstraintCutJoint"] = "LoopConstraintCutJoint" hinge: HingeDS
[docs] @classmethod def fromLoopConstraintCutJoint(cls, lc: LoopConstraintCutJoint) -> Self: """Create a LoopConstraintCutJointDS from the provided LoopConstraintCutJoint. Parameters ---------- lc : LoopConstraintCutJoint The LoopConstraintCutJoint used to build this LoopConstraintDS. Returns ------- LoopConstraintDS A LoopConstraintDS with the provided LoopConstraintCutJoint information. """ get_oframe: dict[Literal["body", "frame", "node"], str] get_pframe: dict[Literal["body", "frame", "node"], str] if isinstance(of := lc.hinge().oframe(), ConstraintNode): get_oframe = {"body": of.parentBody().name(), "node": of.name()} else: get_oframe = {"frame": of.name()} if isinstance(pf := lc.errorFrameToFrame().pframe(), ConstraintNode): get_pframe = {"body": pf.parentBody().name(), "node": pf.name()} else: get_pframe = {"frame": pf.name()} return cls( name=lc.name(), get_oframe=get_oframe, get_pframe=get_pframe, hinge=HingeDS.fromHinge(lc.hinge(), lc.hinge().hingeType()), )
[docs] def toCutJointConstraintCutJoint(self, mb: Multibody) -> LoopConstraintCutJoint: """Create a LoopConstraintCutJoint from the LoopConstraintDS. Parameters ---------- mb : Multibody The multibody to add this loop constraint to. Returns ------- LoopConstraintCutJoint The LoopConstraintCutJoint created from this LoopConstraintDS. """ def _getNodeOrFrame( d: dict[Literal["body", "frame", "node"], str] ) -> ConstraintNode | Frame: obj = None for k, v in d.items(): if k == "body": obj = mb.getBody(v) elif k == "node": obj = obj.getConstraintNode(v) # pyright: ignore else: fc = mb.getNewtonianFrame().container() obj = fc.lookupFrame(v)[0] if not (isinstance(obj, Frame) or isinstance(obj, ConstraintNode)): raise ValueError("Had issue retrieving oframe or pframe.") return obj of = _getNodeOrFrame(self.get_oframe) pf = _getNodeOrFrame(self.get_pframe) if self.hinge.hinge_type == HingeType.CUSTOM: lc = LoopConstraintCutJoint( mb, of.frameToFrame(pf), self.name, self.hinge.hinge_type, [x.subhinge_type for x in self.hinge.subhinges], ) else: lc = LoopConstraintCutJoint( self.name, mb, of.frameToFrame(pf), htype=self.hinge.hinge_type ) for k in range(lc.hinge().nSubhinges()): self.hinge.subhinges[k].setSubhingeValues( lc.hinge().subhinge(k) # pyright: ignore These will be the correct type. ) return lc
[docs] class LoopConstraintConVelDS(LoopConstraintDS): """Represents a constant velocity loop constraint. Parameters ---------- name: str The name of the LoopConstraintConVel. get_oframe: dict[Literal["body","frame","node"],str] A dictionary that specifies how to get to the associated oframe of the LoopConstraintConVel. This uses body names, frame names, and node names to drill down to the correct location. get_pframe: dict[Literal["body","frame","node"],str] A dictionary that specifies how to get to the associated pframe of the LoopConstraintConVel. This uses body names, frame names, and node names to drill down to the correct location. unit_axis: Annotated[Vec3, normCheck(1.0)] The constraint unit axis is_rotational: bool Whether the constraint is rotational or translational """ loop_constraint_type: Literal["LoopConstraintConVel"] = "LoopConstraintConVel" unit_axis: Annotated[Vec3, normCheck(1.0)] is_rotational: bool
[docs] @classmethod def fromLoopConstraintConVel(cls, lc: LoopConstraintConVel) -> Self: """Create a LoopConstraintConVelDS from the provided LoopConstraintConVel. Parameters ---------- lc : LoopConstraintConVel The LoopConstraintConVel used to build this LoopConstraintConVelDS. Returns ------- LoopConstraintConVelDS A LoopConstraintConVelDS with the provided LoopConstraintConVel information. """ get_oframe = { "body": lc.sourceNode().parentBody().name(), "node": lc.sourceNode().name(), } get_pframe = { "body": lc.targetNode().parentBody().name(), "node": lc.targetNode().name(), } unit_axis = lc.getUnitAxis() is_rotational = lc.isRotational() return cls( name=lc.name(), get_oframe=get_oframe, # pyright: ignore - keys are okay get_pframe=get_pframe, # pyright: ignore - keys are okay unit_axis=unit_axis, is_rotational=is_rotational, )
[docs] def toCutJointConstraintConVel(self, mb: Multibody) -> LoopConstraintConVel: """Create a LoopConstraintConVel from the LoopConstraintConVelDS. Parameters ---------- mb : Multibody The multibody to add this loop constraint to. Returns ------- LoopConstraintConVel The LoopConstraintConVel created from this LoopConstraintConVelDS. """ def lookupFrame( d: dict[Literal["body", "frame", "node"], str], / ) -> ConstraintNode | Frame: return cast(PhysicalBody, mb.getBody(d["body"])).getConstraintNode(d["node"]) of = lookupFrame(self.get_oframe) pf = lookupFrame(self.get_pframe) lc = LoopConstraintConVel(self.name, mb, of.frameToFrame(pf)) lc.setUnitAxis(self.unit_axis, self.is_rotational) return lc
[docs] class BodyDS(DataStruct, IdMixin[PhysicalBody], NestedBaseMixin): """A DataStruct that represents a PhysicalBody. Parameters ---------- name : str The name of the body. sensor_nodes : list[NodeDS] A list of sensor nodes attached to the body. force_nodes : list[NodeDS] = [] A list of force nodes attached to the body. hinge: HingeDS The HingeDS that describes the hinge. mass : Mass The mass of the body. body_to_cm : Length3 = np.zeros(3) The body-to-center-of-mass vector. inertia : Inertia = np.zeros(3,3) The inertia of the body, given in the body frame. body_to_joint : Length3 = np.zeros(3) The body-to-joint translation. body_to_joint_quat : UnitQuaternion = UnitQuaternion(0.0, 0.0, 0.0, 1.0) The body-to-joint unit quaternion. inb_to_joint : Length3 = np.zeros(3) The inboard-body-to-joint translation. inb_to_joint_quat : UnitQuaternion = UnitQuaternion(0.0, 0.0, 0.0, 1.0) The inboard-body-to-joint unit quaternion. inb_nodal_matrix : Optional[Mat6n] = None 6xn matrix for the inboard body nodal matrix. Should be set if the inboard body is a PhysicalModalBody. Should be None otherwise. scene_parts: List[ScenePartSpecDS] List of import parts that make up the geometry. inb_nodal_matrix: Optional[Mat6n] = None Nodal matrix for the onode of the parent body if the parent body is a PhysicalModalBody. """ name: str sensor_nodes: list[SerializeAsAny[NodeDS]] = [] force_nodes: list[SerializeAsAny[NodeDS]] = [] constraint_nodes: list[SerializeAsAny[NodeDS]] = [] hinge: HingeDS mass: Mass body_to_cm: Length3 = np.zeros(3) inertia: Inertia = np.zeros((3, 3)) body_to_joint: Length3 = np.zeros(3) body_to_joint_quat: UnitQuaternion = UnitQuaternion(0.0, 0.0, 0.0, 1.0) inb_to_joint: Length3 = np.zeros(3) inb_to_joint_quat: UnitQuaternion = UnitQuaternion(0.0, 0.0, 0.0, 1.0) scene_parts: list[ScenePartSpecDS] = [] inb_nodal_matrix: Optional[Mat6n] = None @property def body_to_joint_transform(self) -> HomTran: """The HomTran between the body frame and the pnode of the hinge.""" return HomTran(self.body_to_joint_quat, convert(self.body_to_joint)) @property def inb_to_joint_transform(self) -> HomTran: """The HomTran between the parent body frame and the onode of the hinge.""" return HomTran(self.inb_to_joint_quat, convert(self.inb_to_joint)) @property def spatial_inertia(self) -> SpatialInertia: """The spatial inertia of the body.""" mass = cast(float, convert(self.mass)) return SpatialInertia(mass, convert(self.body_to_cm), convert(self.inertia))
[docs] @classmethod def fromBody(cls, body: PhysicalBody) -> Self: """Create a BodyDS from a PhysicalBody. Parameters ---------- body : PhysicalBody The PhysicalBody used to create the BodyDS. Returns ------- Self The BodyDS. """ spatial_inertia = body.getSpatialInertia() inb_to_joint_transform = body.onode().getBodyToNodeTransform() body_to_joint_transform = body.getBodyToJointTransform() hinge_type = body.parentHinge().hingeType() if hinge_type is None: raise ValueError( f"Cannot create Body DataStruct from '{body.name()}', since it does not have a hinge." ) hinge_axes = [] if hinge_type.name == "PIN": from Karana.Dynamics import PinSubhinge sh = cast(PinSubhinge, body.parentHinge().subhinge(0)) hinge_axes.append(sh.getUnitAxis()) sensor_nodes, force_nodes, constraint_nodes = cls._fromBodyNodes(body) scene_parts = [ScenePartSpecDS.fromScenePartSpec(sp) for sp in body.getScenePartSpecs()] htype = body.parentHinge().hingeType() if htype is None: raise ValueError(f"Body '{body.name()}' does not have a hinge yet.") if HAVE_MODAL_FLEX and isinstance( body.physicalParentBody(), PhysicalModalBody, # pyright: ignore Won't be unbond if HAVE_MODAL_FLEX is True ): dp = cast( ModalNodeDeformationProvider, body.onode().deformationProvider(), # pyright: ignore Won't be unbond if HAVE_MODAL_FLEX is True ) inb_nodal_matrix = dp.getNodalMatrix() else: inb_nodal_matrix = None dark = cls( name=body.name(), hinge=HingeDS.fromHinge(cast(PhysicalHinge, body.parentHinge()), htype), mass=spatial_inertia.mass(), body_to_cm=spatial_inertia.bodyToCm(), inertia=spatial_inertia.inertia(), body_to_joint=body_to_joint_transform.getTranslation(), body_to_joint_quat=body_to_joint_transform.getUnitQuaternion(), inb_to_joint=inb_to_joint_transform.getTranslation(), inb_to_joint_quat=inb_to_joint_transform.getUnitQuaternion(), force_nodes=force_nodes, sensor_nodes=sensor_nodes, constraint_nodes=constraint_nodes, scene_parts=scene_parts, inb_nodal_matrix=inb_nodal_matrix, ) dark._id = body.id() return dark
@classmethod def _fromBodyNodes(cls, body: PhysicalBody) -> tuple[list[NodeDS], list[NodeDS], list[NodeDS]]: """Retrieve nodes from the provided body. This helper method is defined so that derived classes like ModalBodyDS can update to use different node types if applicable. Parameters ---------- body : PhysicalBody The body to extract node information from. Returns ------- tuple[list[NodeDS], list[NodeDS], list[NodeDS]] Three lists of NodeDSs. These are the sensor nodes, force nodes, and constraint nodes associated with the body. """ sensor_nodes = [] force_nodes = [] for nd in body.nodeList(): if isinstance(nd, ConstraintNode) or nd.isExternalForceNode(): force_nodes.append(NodeDS.fromNode(nd)) else: sensor_nodes.append(NodeDS.fromNode(nd)) constraint_nodes = [NodeDS.fromNode(nd) for nd in body.constraintNodeList()] return sensor_nodes, force_nodes, constraint_nodes
[docs] def toBody(self, parent_body: PhysicalBody) -> PhysicalBody: """Create a body from the BodyDS. Parameters ---------- parent_body: PhysicalBody Body to attach the new body to. Returns ------- PhysicalBody The new body. """ mbody = parent_body.multibody() new_body = PhysicalBody(self.name, mbody) self._toBody(parent_body, new_body) self.addObjectFromId(new_body) return new_body
def _toBody(self, parent_body: PhysicalBody, new_body: PhysicalBody) -> None: """Assign parameters from this BodyDS to a given body. This helper method is simlar to toBody, but takes in a body argument. This allows derived classes, e.g., ModalBodyDS, to create a body and pass it in to have its parameters set. Parameters ---------- parent_body: PhysicalBody Body to attach the new body to. body: PhysicalBody The body to set the parameters of. """ # Create the body hinge = self.hinge.toHinge(parent_body, new_body) if hinge.hingeType() != HingeType.FULL6DOF: new_body.setBodyToJointTransform(self.body_to_joint_transform) else: new_body.setBodyToJointTransform(HomTran()) hinge.onode().setBodyToNodeTransform(self.inb_to_joint_transform) # Set the inb_nodal_matrix if appropriate if HAVE_MODAL_FLEX and isinstance( parent_body, PhysicalModalBody # pyright: ignore It is bound if we are here. ): if self.inb_nodal_matrix is None: raise ValueError( f'Tried to set nodal matrix for the onode on "{parent_body.name()}" that connects it to "{new_body.name()}", but the value is None.' ) dp = cast( ModalNodeDeformationProvider, hinge.onode().deformationProvider(), # pyright: ignore It is bound if we are here. ) dp.setNodalMatrix(self.inb_nodal_matrix) # Set the spatial inertia new_body.setSpatialInertia(self.spatial_inertia) # Add the nodes for nd in self.sensor_nodes + self.force_nodes + self.constraint_nodes: nd.toNode(new_body) # Add the scene geometry for p in self.scene_parts: new_body.addScenePartSpec(p.toScenePartSpec()) _hinge_to_joint_map: ClassVar = { HingeType.LOCKED: "fixed", HingeType.REVOLUTE: "continuous", HingeType.SLIDER: "prismatic", HingeType.FULL6DOF: "floating", } _joint_to_hinge_map: ClassVar = {v: k for k, v in _hinge_to_joint_map.items()} | { "revolute": HingeType.REVOLUTE }
[docs] def toUrdf(self, parent_link: str) -> tuple[Link, Joint]: """Convert the BodyDS to a urchin Link and Joint. These can be used to create a URDF file. Parameters ---------- parent_link : str The name of the parent link for the Joint. Returns ------- tuple[Link, Joint] The Link and Joint associated with this body. """ # In URDF, the Link frame (the URDF body frame) is the same as the joint frame. # This means they have one less frame than we do. This means we need to premultiply by T^-1, # where T is the body_to_joint_transform to transform things like the inertia. # See https://wiki.ros.org/urdf/XML/joint for details body_to_cm_T = HomTran(self.body_to_cm) joint_to_cm_T = self.body_to_joint_transform.inverse() * body_to_cm_T i = Inertial( mass=self.mass, inertia=self.spatial_inertia.cmInertia(), origin=joint_to_cm_T.getMatrix(), ) from urchin import Box, Cylinder, Sphere, Mesh, Material, Visual, Geometry, Collision visuals = [] collisions = [] for p in self.scene_parts: # Get the geometry if isinstance(p.geometry, BoxGeometry): geom = Geometry( box=Box(np.array([p.geometry.width, p.geometry.height, p.geometry.depth])) ) elif isinstance(p.geometry, CylinderGeometry): geom = Geometry(cylinder=Cylinder(p.geometry.radius, p.geometry.height)) elif isinstance(p.geometry, SphereGeometry): geom = Geometry(sphere=Sphere(p.geometry.radius)) elif isinstance(p.geometry, StaticMeshGeometry): if p.geometry.filename != Path(): geom = Geometry( mesh=Mesh( str(p.geometry.filename), False, scale=p.scale, lazy_filename=str(p.geometry.filename), ) ) else: raise ValueError( f"StaticMeshGeometry for {p.name} has no filepath. Not sure how to save to a URDF." ) else: raise ValueError(f"Geometry of type {p.geometry} is not supported by URDF.") # Get the material mat = Material( p.name + "_material", color=np.array( [ p.material.info.color.r(), p.material.info.color.g(), p.material.info.color.b(), p.material.info.color.alpha(), ] ), ) T = HomTran(p.unit_quaternion, p.translation) if (p.layers & LAYER_PHYSICAL_GRAPHICS) != 0: visuals.append(Visual(geom, name=p.name, material=mat, origin=T.getMatrix())) if (p.layers & LAYER_COLLISION) != 0: collisions.append(Collision(name=p.name, origin=T.getMatrix(), geometry=geom)) l = Link(name=self.name, inertial=i, visuals=visuals, collisions=collisions) htype = self.hinge.hinge_type joint_type = self._hinge_to_joint_map[htype] if htype == HingeType.SLIDER: limits = cast(LinearSubhingeDS, self.hinge.subhinges[0]).joint_limits if np.any(np.isnan(limits)): jl = JointLimit(effort=np.inf, velocity=np.inf, lower=None, upper=None) else: jl = JointLimit(effort=np.inf, velocity=np.inf, lower=limits[0], upper=limits[1]) elif htype == HingeType.REVOLUTE: limits = cast(PinSubhingeDS, self.hinge.subhinges[0]).joint_limits if not np.any(np.isnan(limits)): # Set limits and also change joint type to revolute (will be continuous otherwise) jl = JointLimit(effort=np.inf, velocity=np.inf, lower=limits[0], upper=limits[1]) joint_type = "revolute" else: jl = None else: jl = None j = Joint( name=f"{parent_link}_to_{self.name}", joint_type=joint_type, parent=parent_link, child=self.name, origin=self.inb_to_joint_transform.getMatrix(), limit=jl, ) if htype in [HingeType.SLIDER, HingeType.REVOLUTE]: j.axis = self.hinge.subhinges[ 0 ].unit_axis # pyright: ignore Ignoring, we know this will have the correct type. if len(self.sensor_nodes) > 0 or len(self.force_nodes) > 0: warn( f"There is more than one node on body {self.name}. These will not be transfered over to the URDF." ) return (l, j)
[docs] @classmethod def fromUrdf(cls, link: Link, joint: Joint) -> Self: """Convert from a urchin Link and Joint to a BodyDS. Parameters ---------- link : Link The URDF Link. joint : Joint The URDF parent joint. Returns ------- Self BodyDS for the link and joint. """ # The URDF has one less frame than we do, since they always put their body frame at the joint. Hence, we don't know # where the body frame actually is. What we do instead, is put the body frame at the center of mass. Hence, we set the # body_to_joint based on the inertia's origin, and body_to_cm is always 0. hinge_type = cls._joint_to_hinge_map.get(joint.joint_type, None) if hinge_type is None: raise ValueError( f"Got a URDF with joint type {joint.joint_type}, which does not have a complimentary hinge type." ) if hinge_type == HingeType.REVOLUTE: joint_limits = np.array([np.nan, np.nan]) if (dark := joint.limit) is not None: lower = -np.inf if dark.lower is None else dark.lower upper = np.inf if dark.upper is None else dark.upper joint_limits = np.array([lower, upper]) hinge = HingeDS( hinge_type=hinge_type, subhinges=cast( list[SubhingeDS], [ PinSubhingeDS( unit_axis=joint.axis, prescribed=False, joint_limits=joint_limits ) ], ), ) elif hinge_type == HingeType.SLIDER: joint_limits = np.array([np.nan, np.nan]) if (dark := joint.limit) is not None: lower = -np.inf if dark.lower is None else dark.lower upper = np.inf if dark.upper is None else dark.upper joint_limits = np.array([lower, upper]) hinge = HingeDS( hinge_type=hinge_type, subhinges=cast( list[SubhingeDS], [ LinearSubhingeDS( unit_axis=joint.axis, prescribed=False, joint_limits=joint_limits ) ], ), ) elif hinge_type == HingeType.LOCKED: hinge = HingeDS( hinge_type=hinge_type, subhinges=cast( list[SubhingeDS], [LockedSubhingeDS()], ), ) elif hinge_type == HingeType.FULL6DOF: hinge = HingeDS( hinge_type=hinge_type, subhinges=[ Linear3SubhingeDS(prescribed=False), SphericalSubhingeDS(prescribed=False), ], ) elif hinge_type == HingeType.LOCKED: hinge = HingeDS(hinge_type=hinge_type, subhinges=[]) else: raise ValueError(f"Hinge type {hinge_type} should not exist on URDFs.") # In URDFs, the link frame is at the pnode location. Thus, we need to get the inerita # relative to that. dark_quat = UnitQuaternion(RotationMatrix(link.inertial.origin[0:3, 0:3])) cm_to_body_T = HomTran(dark_quat, link.inertial.origin[0:3, 3].flatten()) cm_inertia = SpatialInertia(link.inertial.mass, np.zeros(3), link.inertial.inertia) # Inertia about the link frame inertia = cm_inertia.parallelAxis(cm_to_body_T) inb_to_joint_quat = UnitQuaternion(RotationMatrix(joint.origin[0:3, 0:3])) from urchin import Visual scene_parts = [] for p in link.visuals + link.collisions: quat = UnitQuaternion(RotationMatrix(p.origin[0:3, 0:3])) trans = p.origin[0:3, 3].flatten() if p.geometry.box is not None: b = p.geometry.box geom = BoxGeometry(*b.size) elif p.geometry.cylinder is not None: c = p.geometry.cylinder geom = CylinderGeometry(c.radius, c.length) elif p.geometry.sphere is not None: s = p.geometry.sphere geom = SphereGeometry(s.radius) else: if p.geometry.mesh.filename: importer = AssimpImporter() res = importer.importFrom(p.geometry.mesh.filename) for k, part in enumerate(res.parts): dark = ScenePartSpecDS.fromScenePartSpec(part) if p.name is None: import random tag = int(random.random() * 10000) p.name = f"unnamed{tag}" if len(res.parts) == 1: dark.name = p.name else: dark.name = p.name + f"_mesh{k}" dark.translation = trans dark.unit_quaternion = quat dark.layers = ( LAYER_PHYSICAL_GRAPHICS if isinstance(p, Visual) else LAYER_COLLISION ) scene_parts.append(dark) continue else: raise ValueError(f"Not sure what to do with geometry {p.geometry}") info = PhysicalMaterialInfo() if isinstance(p, Visual): info.color = Color.fromRGBA(*p.material.color) mat = PhysicalMaterial(info) scene_parts.append( ScenePartSpecDS( name=p.name if p.name is not None else "unnamed", material=mat, geometry=geom, unit_quaternion=quat, translation=trans, scale=np.ones(3), layers=LAYER_PHYSICAL_GRAPHICS if isinstance(p, Visual) else LAYER_COLLISION, ) ) return cls( name=link.name, hinge=hinge, mass=inertia.mass(), body_to_cm=inertia.bodyToCm(), inertia=inertia.inertia(), body_to_joint=np.zeros(3), body_to_joint_quat=UnitQuaternion(0, 0, 0, 1), inb_to_joint=joint.origin[0:3, 3].flatten(), inb_to_joint_quat=inb_to_joint_quat, scene_parts=scene_parts, )
[docs] class BodyStateDS(DataStruct, NestedBaseMixin): """This class holds state data for a body. Parameters ---------- name : str The name of the body. grav_accel : Acceleration3 The acceleration due to gravity. hinge : HingeStateDS The state of the hinge. force_node_states : list[ForceNodeStateDS] = [] The states of the force nodes on the body. """ name: str grav_accel: Acceleration3 hinge: HingeStateDS force_node_states: list[ForceNodeStateDS] = []
[docs] @classmethod def fromBody(cls, bd: PhysicalBody) -> Self: """Create a BodyStateDS from the provided body. Parameters ---------- bd: PhysicalBody The body used to create the state. Returns ------- Self The BodyStateDS. """ return cls( name=bd.name(), grav_accel=bd.getGravAccel(), hinge=HingeStateDS.fromHinge(bd.parentHinge()), force_node_states=[ ForceNodeStateDS.fromNode(nd) for nd in bd.nodeList() if nd.isExternalForceNode() or isinstance(nd, ConstraintNode) ], )
[docs] def toBody(self, bd: PhysicalBody) -> None: """Set the state of the provided body. Parameters ---------- bd : PhysicalBody The body to set the state of. """ bd.setGravAccel(convert(self.grav_accel)) self.hinge.toHinge(bd.parentHinge()) for fn in self.force_node_states: # skip if the force node does not exist (may be since the # node may be contact node created on the fly - so not part # of the original model) nd = bd.getNode(fn.name) if nd: fn.toNode(nd) else: print(f"WARNING: Unable to find {fn.name} force node - skipping")
LinkJointTree: TypeAlias = list[tuple[Link, Joint, "LinkJointTree"]] from Karana.Dynamics._body_ds_type import body_dict, body_state_dict
[docs] class BodyWithContextDS(DataStruct): """A BodyDS with context, i.e., with children that are defined by `BodyDS`s. Parameters ---------- body : BodyDS The body we are defining with context. children : list[BodyWithContextDS] The child bodies of this body, also given as `BodyWithContextDS`s. """ body: SerializeAsAny[BodyDS] children: list["BodyWithContextDS"] = []
[docs] def toBody(self, parent_body: PhysicalBody) -> PhysicalBody: """Create a body from the BodyWithContextDS. Parameters ---------- parent_body: PhysicalBody Body to attach the new body to. Returns ------- PhysicalBody The new body. """ body = self.body.toBody(parent_body) for c in self.children: c.toBody(body) return body
[docs] @classmethod def fromBody(cls, body: PhysicalBody, st: SubTree) -> Self: """Create a BodyWithContextDS from a PhysicalBody. Parameters ---------- body : PhysicalBody The PhysicalBody used to create the BodyWithContextDS. st : SubTree The SubTree to use for context (the body's children in the bu-tree may be a subset of the full set of physical children bodies Returns ------- Self The BodyWithContextDS. """ children = [BodyWithContextDS.fromBody(x, st) for x in st.childrenBodies(body)] body_ds = None for k, v in body_dict.items(): if isinstance(body, k): body_ds = v(body) break if body_ds is None: body_ds = BodyDS.fromBody(body) return cls(body=cast(BodyDS, body_ds), children=children)
[docs] def toUrdf(self, parent_link: str) -> tuple[list[Link], list[Joint]]: """Convert this BodyWithContextDS to a list of Links and Joints. These can be used to create a URDF file. Parameters ---------- parent_link : str The name of the parent link for the body of this BodyWithContextDS. Returns ------- tuple[list[Link], list[Joint]] The Links and Joints associated with the body and children of this BodyWithContextDS. """ links = [] joints = [] # Add the body first l, j = self.body.toUrdf(parent_link) links.append(l) joints.append(j) # Now, add the children bodies for c in self.children: c_links, c_joints = c.toUrdf(self.body.name) links += c_links joints += c_joints return links, joints
[docs] @classmethod def fromUrdf(cls, link: Link, joint: Joint, children_urdf: LinkJointTree): """Convert from links and joints to BodyWithContextDS. Parameters ---------- link : Link The link to use for creating the body. joint : Joint The joint to use for creating the body. children_urdf : LinkJointTree A LinkJointTree that defines the links and joints to use for the children. This is an arbitrarily nested list of tuples of links and joints, each of which can be used to define a body. Returns ------- Self An instance of BodyWithContextDS. """ body = BodyDS.fromUrdf(link, joint) children = [] for child in children_urdf: children.append(BodyWithContextDS.fromUrdf(*child)) return cls(body=body, children=children)
[docs] class BodyStateWithContextDS(DataStruct): """A BodyStateDS with context, i.e., with children that are defined by `BodyStateDS`s. Parameters ---------- body : BodyStateDS The body state we are defining with context. children : list[BodyStateWithContextDS] The state of child bodies of this body, also given as `BodyStateWithContextDS`s. Returns ------- Self The BodyStateWithContextDS. """ body: SerializeAsAny[BodyStateDS] children: list["BodyStateWithContextDS"] = []
[docs] @classmethod def fromBody(cls, bd: PhysicalBody, st: SubTree) -> Self: """Create a BodyStateWithContextDS from the provided PhysicalBody. This will capture the provided PhysicalBody state as a BodyStateDS in the body field and all of its children as BodyStateWithContextDSs in the children field. This happens recursively. Parameters ---------- bd : PhysicalBody The body used to create the BodyStateWithContextDS. st : SubTree The SubTree to use for context (the body's children in the bu-tree may be a subset of the full set of physical children bodies Returns ------- Self An instance of BodyStateWithContextDS that represents the provided PhysicalBody. """ body_state_ds = None for k, v in body_state_dict.items(): if isinstance(bd, k): body_state_ds = v(bd) break if body_state_ds is None: body_state_ds = BodyStateDS.fromBody(bd) return cls( body=cast(BodyStateDS, body_state_ds), children=[BodyStateWithContextDS.fromBody(x, st) for x in st.childrenBodies(bd)], )
[docs] def toBody(self, bd: PhysicalBody) -> None: """Set the state of the provided body and its children. Parameters ---------- bd : PhysicalBody The body to set the state of. """ self.body.toBody(bd) # don't worry about sub-tree context here, since we are checking # if the body is available or not anyway cbodies = {c.name(): c for c in bd.multibody().childrenBodies(bd)} for c in self.children: cbody = cbodies.get(c.body.name, None) if cbody is None: raise ValueError( f'Cannot find body with name "{c.body.name}" attached to body with name "{bd.name()}".' ) c.toBody(cbody)
[docs] class CoordinateConstraintDS(DataStruct, IdMixin[CoordinateConstraint]): """CoordinateConstraint DataStruct. Parameters ---------- name : str Name of the CoordinateConstraint. obody_name: str Name of the body whose parent hinge the osubhinge belongs to osubhinge_index: int Index of the osubhinge within its hinge pbody_name: str Name of the body whose parent hinge the psubhinge belongs to psubhinge_index: int Index of the psubhinge within its hinge scale_ratio: float The coordinate scale ratio """ name: str obody_name: str osubhinge_index: int pbody_name: str psubhinge_index: int scale_ratio: float
[docs] @classmethod def fromCoordinateConstraint(cls, cc: CoordinateConstraint) -> Self: """Create a CoordinateConstraintDS from a CoordinateConstraint. Parameters ---------- cc : CoordinateConstraint The CoordinateConstraint used to create the CoordinateConstraintDS. Returns ------- Self The CoordinateConstraintDS. """ osubhinge = cc.osubhinge() psubhinge = cc.psubhinge() return cls( name=cc.name(), obody_name=cast(PhysicalHinge, osubhinge.parentHinge()).pnode().parentBody().name(), osubhinge_index=osubhinge.getIndex(), pbody_name=cast(PhysicalHinge, psubhinge.parentHinge()).pnode().parentBody().name(), psubhinge_index=psubhinge.getIndex(), scale_ratio=cc.getScaleRatio(), )
[docs] def toCoordinateConstraint(self, mbody: Multibody) -> CoordinateConstraint: """Create a CoordinateConstraint from the CoordinateConstraintDS. Parameters ---------- mbody : Multibody Multibody the new CoordinateConstraint will belong to Returns ------- CoordinateConstraint The CoordinateConstraint. """ obody = mbody.getBody(self.obody_name) osubhinge = cast(PhysicalSubhinge, obody.parentHinge().subhinge(self.osubhinge_index)) pbody = mbody.getBody(self.pbody_name) psubhinge = cast(PhysicalSubhinge, pbody.parentHinge().subhinge(self.psubhinge_index)) cc = CoordinateConstraint(self.name, mbody, osubhinge, psubhinge) cc.setScaleRatio(self.scale_ratio) return cc
[docs] class MultibodyDS(DataStruct, IdMixin[Multibody]): """Multibody DataStruct. Parameters ---------- name : str Name of the multibody. base_bodies : list[BodyWithContextDS] The bodies attached to the virtual root. loop_constraints: list[LoopConstraintDS] Loop constraints in the multibody constraint_frames: list[ConstraintFrameDS] Extra frames that are part of constraints coordinate_constraints: list[CoordinateConstraintDS] Coordinate constraints in the multibody """ name: str base_bodies: list[BodyWithContextDS] loop_constraints: list[SerializeAsAny[LoopConstraintDS]] = [] constraint_frames: list[ConstraintFrameDS] = [] coordinate_constraints: list[CoordinateConstraintDS] = []
[docs] def toMultibody( self, fc: FrameContainer, newtonian_frame: Optional[Frame] = None, scene: Optional[ProxyScene] = None, ) -> Multibody: """Create a Multibody from this MultibodyDS. Parameters ---------- fc: FrameContainer FrameContainer to use to create the multibody. Returns ------- Multibody The new multibody. """ # Create the empty multibody mbody = Multibody( self.name, fc, newtonian_frame=newtonian_frame ) # pyright: ignore (not picking up that None is okay) if scene is not None: mbody.setScene(scene) # Attach bodies to the virtualRoot vroot = cast(PhysicalBody, mbody.virtualRoot()) for body in self.base_bodies: body.toBody(vroot) # Create any frames that are needed for f in self.constraint_frames: f.toFrame(mbody) # Create loop constraints for lc in self.loop_constraints: lc.toCutJointConstraint(mbody) for cc in self.coordinate_constraints: cc.toCoordinateConstraint(mbody) self.addObjectFromId(mbody) mbody.ensureHealthy() return mbody
[docs] def attachToBody(self, root_bd: PhysicalBody, prefix: str = "") -> list[PhysicalBody]: """Create the SubGraph of bodies in this DS and attach it to the specified root body. Parameters ---------- root_bd: PhysicalBody The root body to attach to. Returns ------- list[PhysicalBody] List of new base_bodies for the new bodies """ def _recurseRename(b: BodyWithContextDS, prefix: str): """Rename bodies with prefix.""" b.body.name = prefix + b.body.name for c in b.children: _recurseRename(c, prefix) # Attach bodies to the specified body new_bodies = [] for b in self.base_bodies: if prefix: _recurseRename(b, prefix) new_bodies.append(b.toBody(root_bd)) # Create any frames that are needed mbody = root_bd.multibody() for f in self.constraint_frames: f.toFrame(mbody) # Create loop constraints for lc in self.loop_constraints: lc.toCutJointConstraint(mbody) for cc in self.coordinate_constraints: cc.toCoordinateConstraint(mbody) return new_bodies
[docs] @classmethod def fromSubTree(cls, st: SubTree) -> Self: """Create a MultibodyDS from a SubTree. This will include only the bodies of the provided SubTree. Parameters ---------- st : SubTree The SubTree used to create the MultibodyDS. Returns ------- Self The MultibodyDS. """ # vroot = cast(PhysicalBody, st.virtualRoot()) # base_bodies = [BodyWithContextDS.fromBody(x) for x in vroot.childBodies()] base_bodies = [BodyWithContextDS.fromBody(x, st) for x in st.baseBodies()] return cls( name=st.name(), base_bodies=base_bodies, )
[docs] @classmethod def fromMultibody(cls, mb: Multibody) -> Self: """Create a MultibodyDS from a Multibody. Parameters ---------- mb : Multibody The Multibody used to create the MultibodyDS. Returns ------- Self The MultibodyDS. """ return cls.fromSubGraph(mb)
[docs] @classmethod def fromSubGraph(cls, sg: SubGraph) -> Self: """Create a MultibodyDS from a SubGraph. This will be a MultibodyDS with only the bodies and constraints included in the SubGraph. Parameters ---------- sg : SubGraph The SubGraph used to create the MultibodyDS. Returns ------- Self The MultibodyDS. """ mb = sg.multibody() # get the base bodies for the SubGraph. When doing so, pass the # SubGraph as context since the number of children bodies for a # body in the SubGraph may be a subset of its full set of bodies base_bodies = [BodyWithContextDS.fromBody(x, sg) for x in sg.baseBodies()] constraint_frames = [] loop_constraints = [] for lc in sg.enabledConstraints(): if lc.type() in [ BilateralConstraintType.CUTJOINT_LOOP, BilateralConstraintType.CONVEL_LOOP, ]: loop_constraints.append(LoopConstraintDS.fromLoopConstraint(lc)) # if not isinstance(of := lc.hinge().oframe(), ConstraintNode): cf2f = lc.constraintFrameToFrame() snd = lc.sourceNode() if not snd: constraint_frames.append(ConstraintFrameDS.fromFrame(cf2f.oframe(), mb)) tnd = lc.targetNode() # if not isinstance(pf := lc.errorFrameToFrame().pframe(), ConstraintNode): if not tnd: # constraint_frames.append(ConstraintFrameDS.fromFrame(pf, mb)) constraint_frames.append(ConstraintFrameDS.fromFrame(cf2f.pframe(), mb)) coordinate_constraints = [ CoordinateConstraintDS.fromCoordinateConstraint(cc) for cc in sg.enabledConstraints() if cc.type() == BilateralConstraintType.COORDINATE ] dark = cls( name=sg.name(), base_bodies=base_bodies, loop_constraints=loop_constraints, constraint_frames=constraint_frames, coordinate_constraints=coordinate_constraints, ) dark._id = sg.id() return dark
[docs] def toUrdf(self, file: Path | str): """Convert the MultibodyDS to a URDF file. Parameters ---------- file: Path | str The file to write the URDF to. """ if isinstance(file, str): file = Path(file) # Create a dummy root body to attach everything to. links = [ Link( name="root", inertial=Inertial(mass=1.0, inertia=np.eye(3)), visuals=None, collisions=None, ) ] joints = [] # Add all the links and joints for the base_bodies and their children for b in self.base_bodies: b_links, b_joints = b.toUrdf("root") links += b_links joints += b_joints # Print everything to a URDF urdf = URDF(name=self.name, links=links, joints=joints) urdf.save(file)
[docs] @classmethod def fromUrdf(cls, file: Path | str) -> Self: """Read a URDF file and convert it to a MultibodyDS. Parameters ---------- file : Path | str The file to write the URDF from. Returns ------- MultibodyDS A MultibodyDS that corresponds to the URDF. """ @contextmanager def changeDir(dir: Path): original_dir = os.getcwd() try: os.chdir(dir) yield finally: os.chdir(original_dir) # Convert to Path if it is a string if isinstance(file, str): file = Path(file) dir = file.parent name = file.name # URDFs have mesh files relative to the URDF file itself. Therefore, we want to # change directories to that URDF file so mesh loading works appropriately. with changeDir(dir): urdf = URDF.load(name, lazy_load_meshes=True) # Get the root node in the graph root = urdf.base_link if root is None: raise ValueError("Could not get the root of the URDF.") root = cast(Link, root) # Use the graph to create link-joint pairs # The top-level link-joint pairs are what we want to be base bodies # in the MultibodyDS def _recurseLink(parent: Link) -> LinkJointTree: """Create a LinkJointTree from a parent node. This is done recusively. """ link_joint_tree: LinkJointTree = [] for child in urdf._G.predecessors(parent): child = cast(Link, child) grandchildren = _recurseLink(child) joint: Joint = urdf._G.get_edge_data(child, parent)["joint"] link_joint_tree.append((child, joint, grandchildren)) return link_joint_tree link_joint_tree = _recurseLink(root) return cls( name=urdf.name, base_bodies=[BodyWithContextDS.fromUrdf(*x) for x in link_joint_tree], )
[docs] class BaseDataDS(DataStruct): """DataStruct for BaseData. Parameters ---------- Q : Vec Position-level coordinates. U : Vec Velocity-level coordinates. Udot : Vec Acceleration-level coordinates. T : Vec Force-level coordinates. """ Q: Vec U: Vec Udot: Vec T: Vec
[docs] def toCoordBase(self, b: CoordBase): """Set the state of the provided CoordBase using this BaseDataDS. Parameters ---------- b : CoordBase The CoordBase whose values will be set. """ b.setQ(self.Q) b.setU(self.U) b.setUdot(self.Udot) b.setT(self.T)
[docs] class CoordDataDS(DataStruct): """DataStruct for CoordData. Parameters ---------- base_data : dict[str, BaseDataDS] Data from the BaseData. """ base_data: dict[str, BaseDataDS]
[docs] @classmethod def fromCoordData(cls, c: CoordData) -> Self: """Create a CoordDataDS that represents the provided CoordData. Parameters ---------- c : CoordData The CoordData to represent as a CoordDataDS. Returns ------- An instance of CoordDataDS that represents the provided CoordData. """ return cls( base_data={ k: BaseDataDS(Q=v[0], U=v[1], Udot=v[2], T=v[3]) for k, v in c.toMap().items() } )
[docs] def toCoordData(self, c: CoordData) -> None: """Set the state of the provided `CoordData`. Parameters ---------- c : CoordData The CoordData to set the state of. """ for b in c.coordBases(): b = cast(CoordBase, b) self.base_data[ b.name() # pyright: ignore - All the CoordBases we use should have a name ].toCoordBase(b)
[docs] class SubTreeStateDS(DataStruct): """SubTree State DataStruct. Parameters ---------- base_bodies : list[BodyStateWithContextDS] The state of the bodies attached to the virtual root. """ base_bodies: list[BodyStateWithContextDS]
[docs] @classmethod def fromSubTree(cls, st: SubTree) -> Self: """Create a SubTreeStateDS from a SubTree. Parameters ---------- st : SubTree The SubTree used to create the SubTreeStateDS. Returns ------- Self The SubTreeStateDS. """ # vroot = cast(PhysicalBody, st.virtualRoot()) all_bodies = st.sortedPhysicalBodiesList() return cls( base_bodies=[ # BodyStateWithContextDS.fromBody(x) for x in vroot.childBodies() if x in all_bodies BodyStateWithContextDS.fromBody(x, st) for x in st.baseBodies() if x in all_bodies ] )
[docs] def toSubTree(self, st: SubTree) -> None: """Set the state of the provided multibody. Parameters ---------- st : SubTree The multibody to set the state of. """ # vroot = cast(PhysicalBody, st.virtualRoot()) # cbodies = {c.name(): c for c in vroot.childBodies()} cbodies = {c.name(): c for c in st.baseBodies()} for c in self.base_bodies: cbody = cbodies.get(c.body.name, None) if cbody is None: raise ValueError( f'Cannot find body with name "{c.body.name}" attached to the virtual root.' ) c.toBody(cbody)
[docs] class SubGraphStateDS(DataStruct): """SubGraph State DataStruct. Parameters ---------- sub_tree_state : SubTreeStateDS The state of the subtree. constraint_coord_data: CoordDataDS The state of the constraint coordinate data. """ sub_tree_state: SubTreeStateDS constraint_coord_data: CoordDataDS
[docs] @classmethod def fromSubGraph(cls, sg: SubGraph) -> Self: """Create a SubGraphStateDS from a SubGraph. Parameters ---------- sg : SubGraph The SubGraph used to create the SubGraphStateDS. Returns ------- Self The SubGraphStateDS. """ return cls( sub_tree_state=SubTreeStateDS.fromSubTree(sg), constraint_coord_data=CoordDataDS.fromCoordData(sg.cutjointCoordData()), )
[docs] def toSubGraph(self, sg: SubGraph) -> None: """Set the state of the provided multibody. Parameters ---------- sg : SubGraph The multibody to set the state of. """ self.sub_tree_state.toSubTree(sg) self.constraint_coord_data.toCoordData(sg.cutjointCoordData())
T = TypeVar("T", bound=BaseKModel)
[docs] class KModelDS(DataStruct, NestedBaseMixin, IdMixin[T], Generic[T]): """A base class for representing a KModel as a DataStruct.""" name: str period: float | np.timedelta64
[docs] @abstractmethod def toModel(self, mm: ModelManager) -> BaseKModel: """Create an model instance from this KModelDS. Parameters ---------- mm : ModelManager The ModelManager to associate the newly created model instance with. Returns ------- BaseKModel A model instance created from this KModelDS. """ raise NotImplementedError("Derived classes must implement toModel")
[docs] class StatePropagatorDS(DataStruct): """A DataStruct that represents the StatePropagator. Parameters ---------- integrator_type: IntegratorType Type of integrator used. integrator_options: Optional[IntegratorOptionsDS] = None The options associated with the integrator. solver_type: Optional[MMSolverType] = MMSolverType.UNDEFINED The options associated with the integrator. models: list[KModelDS] The models registered with the StatePropagator. """ integrator_type: IntegratorType integrator_options: Optional[IntegratorOptionsDS] = None solver_type: Optional[MMSolverType] = MMSolverType.UNDEFINED models: list[SerializeAsAny[KModelDS]]
[docs] @classmethod def fromStatePropagator(cls, sp: StatePropagator) -> Self: """Create a StatePropagatorDS from a StatePropagator. Parameters ---------- body : StatePropagator The StatePropagator used to create the StatePropagatorDS. Returns ------- Self The StatePropagatorDS. """ integ = sp.getIntegrator() solver_type = sp.solverType() opts = IntegratorOptionsDS.fromIntegrator(integ) models: list[KModelDS] = [] for model in sp.getRegisteredModels(): if not IdMixin.objectHasDS(model.id()): # Only try to create a DS for the model if it was not done already. # If it was, then this model is already handled by something else, e.g., a Prefab. if hasattr(model, "toDS"): models.append( model.toDS() # pyright: ignore - Will have toDS if we are in this branch ) else: warn( f'Model {model.name()} ({model.typeString()}) does not have a "toDS" method, so it cannot be added to the StatePropagatorDS.' ) return cls( integrator_type=integ.getIntegratorType(), integrator_options=opts, solver_type=solver_type, models=models, )
[docs] def toStatePropagatorNoModels(self, st: SubTree) -> StatePropagator: """Create a StatePropagator from the StatePropagatorDS. This does NOT add the models to the StatePropagator. If you want the models to, use the toStatePropagator method. Parameters ---------- st : SubTree The SubTree to use to create the StatePropagator. Returns ------- StatePropagator The StatePropagator associated with this StatePropagatorDS, but without any models. """ if self.integrator_options is not None: return StatePropagator( st, self.integrator_type, self.integrator_options.toIntegratorOptions(), None, # TODO - update to save and restore solver options self.solver_type, ) else: return StatePropagator(st, self.integrator_type)
[docs] def toStatePropagator(self, st: SubTree) -> StatePropagator: """Create a StatePropagator from the StatePropagatorDS. Parameters ---------- st: SubTree The SubTree to associate with the StatePropagator. """ sp = self.toStatePropagatorNoModels(st) sp.setTime(0.0) sp.setState(sp.assembleState()) for model_ds in self.models: model_ds.toModel(sp) return sp