Source code for Karana.Dynamics.fa2ta

# Copyright (c) 2024-2025 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.

"""Convert a FA model into a TA model.

Consider converting to C++.

"""

__all__ = ["TreeConverter"]

from collections.abc import Mapping, Container

from Karana.Dynamics import (
    HINGE_TYPE,
    LoopConstraintHinge,
    LoopConstraintBase,
    PhysicalBody,
)
from Karana.Core import discard
from Karana.Dynamics import Multibody


[docs] class TreeConverter: """Class to do an FA to TA conversion on a Multibody Whenever a hinge-based loop constraint is between a pair of bodies where at least one of the bodies is FULL6DOF, the loop constraint can be converted to a regular hinge. Using this approach, this class can greedily convert fully or partially FA models to TA models with a minimal number of loop constraints, reducing the total number of constraints and degrees of freedom in the system. """ def __init__( self, parent_prefs: Mapping[int, int] | None = None, skip_prefs: Mapping[int, int] | None = None, verbose: bool = False, ): """TreeConverter constructor Parameters ---------- parent_prefs: Mapping[int, int] | None A-priori preferences for parents. The keys are body ids, and the values the preferred parent ids. skip_prefs: Mapping[int, int] | None The loop constraint conversions to skip. The key/value ids for the body pair whose loop constraint conversion should be skipped. verbose: bool If True, enable verbose output throughout the conversion process. Defaults to False. """ self.donebds = [] self.skipbds = [] self.donelcs = [] # map a-priori preference for parents. The keys are body ids, and the # values the preferred parent ids self.parent_prefs = parent_prefs | {} # map for the loop constraint conversions to skip. The key/values ids # for the body pair whose loop constraint conversion should be skipped self.skip_prefs = skip_prefs | {} self.todo = [] self.visited = [] self.verbose = verbose
[docs] def canConvertToHinge(self, lc: LoopConstraintBase, basebds: Container[PhysicalBody]): """Whether the loop contraint can be converted to a hinge It must be hinge-based an have at least one free body """ # can only convert hinge-based loop constraints if not lc.hasHinge(): return False # skip CUSTOM hinge constraints for now since they likely came from # JPRIM. We do not want them to be used in the initial topology # creation. We can always do another pass at the end in case any of # these are convertable into hinges. if lc.hinge().hingeType() == HINGE_TYPE.CUSTOM: # print("SKIPPING IN FIRST PASS", lc.name(), len(second_pass_lcs)) # second_pass_lcs.append(lc) return False snd = lc.sourceNode() tnd = lc.targetNode() sbd = snd.parentBody() if snd else None tbd = tnd.parentBody() if snd else None parent = None # check whether skipping or a parent body preference has been # specified if sbd and tbd: sbdid = sbd.id() tbdid = tbd.id() # check whether there is an entry in skip_prefs directing that # this loop constraint conversion should be skipped if self.skip_prefs.get(sbdid, -1) == tbdid: return False if self.skip_prefs.get(tbdid, -1) == sbdid: return False # check whether a parent body preference has been registered if self.parent_prefs.get(sbdid, -1) == tbdid: parent = tbd if self.verbose: print(f"Found parent preference {tbd.name()} for {sbd.name()}") elif self.parent_prefs.get(tbdid, -1) == sbdid: parent = sbd if self.verbose: print(f"Found parent preference {sbd.name()} for {tbd.name()}") # if no parent has been found, auto-detect if not parent: if sbd in basebds: parent = sbd if tbd in basebds: parent = tbd if parent: if self.verbose: print( " HAVE PARENT", parent.name(), sbd.name(), sbd.parentHinge().hingeType(), tbd.name(), tbd.parentHinge().hingeType(), ) # the parent body has been specified if sbd and sbd.id() == parent.id(): # the source body is the parent, so can convert if the # target body has a 6 dof hinge return tbd and tbd.parentHinge().hingeType() == HINGE_TYPE.FULL6DOF else: # the source body is not the parent assert tbd assert tbd.id() == parent.id() # can convert if the source body has a 6 dof hinge return sbd and sbd.parentHinge().hingeType() == HINGE_TYPE.FULL6DOF assert 0 if self.verbose: print( " NO PARENT", sbd.name(), sbd.parentHinge().hingeType(), tbd.name(), tbd.parentHinge().hingeType(), ) # no parent body has been specified, so just need one of the source # or target bodies to have a 6 dof hinge if sbd and sbd.parentHinge().hingeType() == HINGE_TYPE.FULL6DOF: return True if tbd and tbd.parentHinge().hingeType() == HINGE_TYPE.FULL6DOF: return True return False
[docs] def convertToHinge(self, lc: LoopConstraintHinge, basebds: Container[PhysicalBody]): """ Convert the loop constraint into a hinge and return the new child body. """ if self.verbose: print(f" Converting constraint {lc.name()}/{lc.hinge().hingeType()}") snd = lc.sourceNode() tnd = lc.targetNode() sbd = snd.parentBody() if snd else None tbd = tnd.parentBody() if snd else None if self.verbose and sbd: print(f" source bd={sbd.name()}/{sbd.parentHinge().hingeType()}") if self.verbose and tnd: print(f" target bd={tbd.name()}/{sbd.parentHinge().hingeType()}") parent = None # figure out the parent if none has been specified if not parent: if sbd in basebds: parent = sbd if tbd in basebds: parent = tbd else: # the specified parent should be either sbd or tbd assert (sbd.id() == parent.id()) or (tbd.id() == parent.id()) if self.verbose and parent: print(f" parent={parent.name()}") # figure out the orientation of the hinge found_orientation = False if parent: # the parent body has been specified. The polarity will depend # on whether this body is the parent of the source or the target # node if sbd: if sbd.id() == parent.id(): reverse = False found_orientation = True if not found_orientation: # the source node did not work out, check the target node assert tbd assert tbd.id() == parent.id() reverse = True found_orientation = True else: # no parent body, so the hinge polarity has to be auto-determined # based on which of the nodes has a 6 dof hinge if sbd: if not tbd: # no target body, so the source has to be the parent assert sbd.parentHinge().hingeType() == HINGE_TYPE.FULL6DOF reverse = False found_orientation = True else: # we do have a target body, and need to make sure that # the polarity is towards a body with the 6dof hinge if tbd.parentHinge().hingeType() != HINGE_TYPE.FULL6DOF: # the target body does not have a 6dof hinge, so the # target has to be the parent body reverse = True found_orientation = True else: # the target does has a 6dof hinge reverse = sbd.parentHinge().hingeType() == HINGE_TYPE.FULL6DOF found_orientation = True else: # no source body, so the target has to be the parent body assert tbd # assert (tbd.parentHinge().hingeType() == HINGE_TYPE.FULL6DOF) reverse = True found_orientation = True if self.verbose: print(f" reverse={reverse}") hge = LoopConstraintHinge.toPhysicalHinge(lc, reverse) self.donelcs.append(lc.id()) if self.verbose: print( " Created hinge:", hge.onode().parentBody().name(), " --> ", hge.pnode().parentBody().name(), hge.hingeType(), ) if snd: del snd if tnd: del tnd if sbd: del sbd if tbd: del tbd del parent # bodies.remove(hge.onode().parentBody()) parid = hge.onode().parentBody().id() if parid not in self.donebds: self.donebds.append(parid) return hge.pnode().parentBody()
[docs] def convertBranch(self, bd: PhysicalBody, basebds: Container[PhysicalBody]): """Convert this bd's loop constraints to hinges, and continue down the branch to do this for downstream bodies as well. If bd is in the basebds list, then use it as the parent body. """ bdid = bd.id() if bdid in self.donebds: return multibody = bd.multibody() lcs = [x for x in multibody.getBodyLoopConstraints(bd) if x.hasHinge()] if self.verbose: print(" Doing ", bd.name(), "lcs=", len(lcs)) if lcs: children = [] for k in range(len(lcs)): if not self.canConvertToHinge(lcs[k], basebds): if self.verbose: print(f" Skipping conversion of {lcs[k].name()} loop constraint") continue child_body = self.convertToHinge(lcs[k], basebds) discard(lcs[k]) lcs[k] = None children.append(child_body) for child in children: self.convertBranch(child, basebds) else: if bdid not in self.skipbds: self.skipbds.append(bdid)
[docs] def convertBranchBF(self, basebds: Container[PhysicalBody]): """Convert this bd's loop constraints to hinges, and continue down the branch to do this for downstream bodies as well. If bd is in the basebds list, then use it as the parent body. Breadth first traverse. """ bd = self.visited.pop() if bd in self.todo: self.todo.remove(bd) multibody = bd.multibody() lcs = [x for x in multibody.getBodyLoopConstraints(bd) if x.hasHinge()] if self.verbose: print(" Doing ", bd.name(), "lcs=", len(lcs)) if lcs: children = [] for k in range(len(lcs)): if not self.canConvertToHinge(lcs[k], basebds): if self.verbose: print(f" Skipping conversion of {lcs[k].name()} loop constraint") continue child_body = self.convertToHinge(lcs[k], basebds) discard(lcs[k]) lcs[k] = None self.visited.append(child_body)
""" FIXME: can this orphaned string be deleted? - create visited queue and put basebodies in it - while queue not empty, call convert2BranchBF - this will process the first entry in the queue, and add all children to teh back """