# 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.
"""Classes associated with creating a ModalBodyDS from an H5 file."""
import h5py
import numpy as np
from Karana.KUtils.DataStruct import DataStruct
from Karana.Dynamics.SOAFlexDyn_types import ModalBodyDS, ModalNodeDS
from Karana.Dynamics.SOADyn_types import BodyDS
from Karana.Dynamics import HINGE_TYPE
from Karana.Dynamics.SOADyn_types import HingeDS
from pathlib import Path
from Karana.KUtils.Ktyping import NonNegativeFloat, Vec
from numpy.typing import NDArray
[docs]
class H5PhysicalModalBodyDS(DataStruct):
"""Configuration options for ReadH5PhysicalModalBody.
Parameters
----------
filename: Path | str
The name of the H5 file to read the flexible body data from.
mass_conversion : float = 1.0
Conversion factor for mass.
distance_conversion : float = 1.0
Conversion factor for distance.
p_node_grid : int | None
The grid point number associated with the pnode. This can be None if and only if
the hinge type is 6 DoF.
sensor_nodes : dict[int, str]
This dictionary defines the sensor nodes to add to the body. The keys
are grid point numbers from the NASTRAN file, and the values are the
names of the sensor nodes that will be created and are associated with
those grid point numbers.
force_nodes : dict[int, str]
This dictionary defines the force nodes to add to the body. The keys
are grid point numbers from the NASTRAN file, and the values are the
names of the force nodes that will be created and are associated with
those grid point numbers.
constraint_nodes: dict[int, str]
This dictionary defines the constraint nodes to add to the body. The keys
are grid point numbers from the NASTRAN file, and the values are the
names of the constraint nodes that will be created and are associated with
those grid point numbers.
damping : NonNegativeFloat | Vec
Defines the damping. If given as a NonNegativeFloat, then the damping
vector will be computed as 2*zeta*w, where w are the individual
frequencies in rad/s. If given as a Vec, then this will be used as
the damping vector directly.
"""
filename: Path | str
mass_conversion: float = 1.0
distance_conversion: float = 1.0
p_node_grid: int | None
sensor_nodes: dict[int, str] = {}
force_nodes: dict[int, str] = {}
constraint_nodes: dict[int, str] = {}
damping: NonNegativeFloat | Vec = 0.0
[docs]
class ReadH5PhysicalModalBody:
"""Create a ModalBodyDS from an H5 file.
Also, can be used to add inboard body information for child bodies of this modal body.
"""
def __init__(self, c: H5PhysicalModalBodyDS):
"""Create a ReadH5PhysicalModalBody.
Parameters
----------
c : H5PhysicalModalBodyDS
Configuration options.
"""
self._c = c
self.f = h5py.File(self._c.filename, "r")
self.n_rigid_modes = self.f["modal_analysis"]["n_rigid_modes"][()]
[docs]
def getNodalMatrix(self, grid: int) -> NDArray:
"""Retrieve the nodal matrix associated with the given grid point ID.
Parameters
----------
grid : int
The ID associated with the grid point to get the nodal matrix for.
Returns
-------
NDArray
Nodal matrix for the given grid point.
"""
nm = self.f["grids"][str(grid)]["nodal_matrix"][:, self.n_rigid_modes :]
nm[0:3, :] *= 1.0 / (self._c.distance_conversion)
nm *= 1.0 / (np.sqrt(self._c.mass_conversion))
return nm
[docs]
def addInbNodeToDS(self, body_ds: BodyDS, grid_id: int):
"""Add inboard body node (hinge o-node) to incoming BodyDS.
Parameters
----------
body_ds : BodyDS
The BodyDS to add the inboard node information to.
grid_id : int
The grid ID to use as the inboard node.
"""
body_ds.inb_nodal_matrix = self.getNodalMatrix(grid_id)
body_ds.inb_to_joint = self.f["grids"][str(grid_id)]["pos"][:]
[docs]
def getModalBodyDS(self, name: str, hinge: HingeDS) -> ModalBodyDS:
"""Create the ModalBodyDS from the H5 file given in the configuration.
Parameters
----------
name : str
The name to use for the ModalBodyDS.
hinge : HingeDS
The HingeDS to use for the ModalBodyDS.
Returns
-------
ModalBodyDS
The ModalBodyDS associated with the configuration and method options.
"""
if isinstance(self._c.damping, float) or isinstance(self._c.damping, int):
freq_rad = self.f["modal_analysis"]["freq_rad"][self.n_rigid_modes :]
damping = 2 * self._c.damping * freq_rad
else:
damping = self._c.damping[self.n_rigid_modes :]
# Get the nodal matrix
if hinge.hinge_type == HINGE_TYPE.FULL6DOF:
nodal_matrix = None
body_to_joint = np.zeros(3)
else:
if self._c.p_node_grid is None:
raise ValueError(
"The pnode grid point cannot be none if the joint type is not FULL6DOF"
)
nodal_matrix = self.getNodalMatrix(self._c.p_node_grid)
body_to_joint = self.f["grids"][str(self._c.p_node_grid)]["pos"][:]
# Add sensor nodes
sensor_nodes = []
for gid, nm in self._c.sensor_nodes.items():
sensor_nodes.append(
ModalNodeDS(
name=nm,
constraint_node=False,
force_node=False,
nodal_matrix=self.getNodalMatrix(gid),
translation=self.f["grids"][str(gid)]["pos"][:],
)
)
# Add force nodes
force_nodes = []
for gid, nm in self._c.force_nodes.items():
force_nodes.append(
ModalNodeDS(
name=nm,
constraint_node=False,
force_node=True,
nodal_matrix=self.getNodalMatrix(gid),
translation=self.f["grids"][str(gid)]["pos"][:],
)
)
# Add constraint nodes
constraint_nodes = []
for gid, nm in self._c.constraint_nodes.items():
constraint_nodes.append(
ModalNodeDS(
name=nm,
constraint_node=True,
force_node=True,
nodal_matrix=self.getNodalMatrix(gid),
translation=self.f["grids"][str(gid)]["pos"][:],
)
)
return ModalBodyDS(
name=name,
hinge=hinge,
stiffness_vector=self.f["modal_analysis"]["stiffness"][self.n_rigid_modes :],
damping_vector=damping,
mass=self.f["rigid_mass_props"]["mass"][()] * self._c.mass_conversion,
b2cm=self.f["rigid_mass_props"]["center_of_mass"][:] * self._c.distance_conversion,
inertia=self.f["rigid_mass_props"]["inertia"][:]
* self._c.mass_conversion
* self._c.distance_conversion**2,
nodal_matrix=nodal_matrix,
body_to_joint=body_to_joint,
sensor_nodes=sensor_nodes,
force_nodes=force_nodes,
constraint_nodes=constraint_nodes,
)