# SPDX-FileCopyrightText: 2023 SAP SE
#
# SPDX-License-Identifier: Apache-2.0
#
# This file is part of FEDEM - https://openfedem.org
"""
Python wrapper for the native fedem modeler.
This module provides functionality for creating new Fedem models,
and for modifying existing ones.
"""
from ctypes import POINTER, byref, c_bool, c_char_p, c_double, c_int
from os import environ, path
from fedempy.enums import FmDof, FmDofStat, FmType, FmVar
from fedempy.fmm import FedemModel, _convert_char, _convert_int, _convert_real
def _convert_bool(arg):
"""
Converts a boolean from Python to C type.
"""
if arg is None:
return c_bool(False)
if isinstance(arg, bool):
return c_bool(arg)
if isinstance(arg, c_bool):
return arg
raise TypeError(f"Expected {bool} or {c_bool}, got {type(arg)}.")
def _convert_int_array(arg):
"""
Converts an array of integers from Python to C type.
"""
if arg is None:
return c_int(0), None
if isinstance(arg, (list, tuple)):
argc = len(arg)
if argc > 0:
argv = (c_int * argc)()
argv[:] = arg
else:
argv = None
return c_int(argc), argv
if hasattr(arg, "_length_") and getattr(arg, "_type_", None) is c_int:
return c_int(len(arg)), arg
raise TypeError(f"Expected {list} or {c_int}, got {type(arg)}.")
def _convert_float_array(arg):
"""
Converts an array of floats from Python to C type.
"""
if arg is None:
return c_int(0), None
if isinstance(arg, (list, tuple)):
argc = len(arg)
if argc > 0:
argv = (c_double * argc)()
argv[:] = arg
else:
argv = None
return c_int(argc), argv
if hasattr(arg, "_length_") and getattr(arg, "_type_", None) is c_double:
return c_int(len(arg)), arg
raise TypeError(f"Expected {list} or {c_double}, got {type(arg)}.")
def _extract_dofs(arg):
"""
Extracts 6 DOF values into a c_double array from a dictionary.
"""
if not isinstance(arg, dict):
return None
has_dofs = False
dof_tags = ["Tx", "Ty", "Tz", "Rx", "Ry", "Rz"]
dof_vals = (c_double * 6)(*([0.0] * 6))
for key, value in arg.items():
if key in dof_tags:
dof_vals[dof_tags.index(key)] = _convert_real(value)
has_dofs = True
if has_dofs:
return dof_vals
return None # Return nothing if no DOF tags were found
def _extract_polyline(arg):
"""
Extracts data for a polyline function from a dictionary.
"""
coord = arg["xy"]
n_ = c_int(len(coord))
x_ = (c_double * len(coord))()
y_ = (c_double * len(coord))()
x_[:] = [c_double(c[0]) for c in coord]
y_[:] = [c_double(c[1]) for c in coord]
extrap = ["NONE", "FLAT", "LINEAR"]
e_type = arg.get("extrapol_type", extrap[0])
if e_type in extrap:
e_ = c_int(extrap.index(e_type))
else:
e_ = c_int(0)
return n_, x_, y_, e_
[docs]
class FedemModeler(FedemModel):
"""
This subclass of :class:`fmm.FedemModel` adds some basic modeling methods.
Parameters
----------
model_file : str, default=None
Absolute path to the fedem model file to open
force_new : bool, default=False
If True, any existing model will be overwritten on save
plugins : str or list of str, default=None
Plugin libraries with user-defined elements and functions
Methods
-------
open:
Opens the specified model file
save:
Saves current model into the specified model file
close:
Closes the currently open model
make_triad:
Creates a triad at specified location or node
make_beam:
Creates a string of beam elements
make_beam_section:
Creates a beam cross section property object
make_beam_material:
Creates a material property object
make_spring:
Creates a spring element
make_damper:
Creates a damper element
make_joint:
Creates a joint object
make_load:
Creates an external load object
make_function:
Creates a general function object
make_sensor:
Creates a sensor object
make_fe_part:
Creates an FE part
make_strain_rosette:
Creates a strain rosette on an FE part
make_udelm:
Creates a string of user-defined elements
edit_triad:
Modifies an existing triad
edit_part:
Modifies an existing FE part
edit_joint:
Modifies an existing joint
edit_function:
Modifies an existing function
solver_setup:
Setting dynamics solver parameters
"""
def __init__(self, model_file=None, force_new=False, plugins=None):
"""
Constructor.
Optionally opens a Fedem model, if a model file name is specified.
"""
fedem_lib = environ.get("FEDEM_MDB")
if not fedem_lib:
print("\n *** Environment variable FEDEM_MDB is not set!", flush=True)
return # This will probably cause crash later, but not here
if isinstance(plugins, str):
super().__init__(fedem_lib, plugins)
elif isinstance(plugins, list) and len(plugins) > 0:
if len(plugins) > 1:
super().__init__(fedem_lib, plugins[0], plugins[1])
else:
super().__init__(fedem_lib, plugins[0])
else:
super().__init__(fedem_lib)
self._fmlib.FmCreateTriad.restype = c_int
self._fmlib.FmCreateTriad.argtypes = [
c_char_p,
c_double,
c_double,
c_double,
c_double,
c_double,
c_double,
]
self._fmlib.FmTriadOnNode.restype = c_int
self._fmlib.FmTriadOnNode.argtypes = [c_char_p, c_int, c_int]
self._fmlib.FmCreateBeam.restype = c_int
self._fmlib.FmCreateBeam.argtypes = [c_char_p, c_int, c_int]
self._fmlib.FmCreateBeamProperty.restype = c_int
self._fmlib.FmCreateBeamProperty.argtypes = [
c_char_p,
c_int,
c_int,
POINTER(c_double),
]
self._fmlib.FmCreateMaterialProperty.restype = c_int
self._fmlib.FmCreateMaterialProperty.argtypes = [
c_char_p,
c_int,
POINTER(c_double),
]
self._fmlib.FmCreateSpring.restype = c_int
self._fmlib.FmCreateSpring.argtypes = [
c_char_p,
c_int,
c_int,
c_double,
c_bool,
c_double,
POINTER(c_int),
c_int,
POINTER(c_double),
POINTER(c_double),
c_int,
c_int,
]
self._fmlib.FmCreateDamper.restype = c_int
self._fmlib.FmCreateDamper.argtypes = [
c_char_p,
c_int,
c_int,
c_bool,
c_double,
POINTER(c_int),
c_int,
POINTER(c_double),
POINTER(c_double),
c_int,
]
self._fmlib.FmCreateJoint.restype = c_int
self._fmlib.FmCreateJoint.argtypes = [
c_char_p,
c_int,
c_int,
POINTER(c_int),
c_int,
]
self._fmlib.FmCreateLoad.restype = c_int
self._fmlib.FmCreateLoad.argtypes = [
c_char_p,
c_int,
c_int,
c_double,
c_double,
c_double,
c_char_p,
c_int,
]
self._fmlib.FmCreateMathExprFunc.restype = c_int
self._fmlib.FmCreateMathExprFunc.argtypes = [
c_char_p,
c_char_p,
c_char_p,
c_bool,
]
self._fmlib.FmCreateExternalFunc.restype = c_int
self._fmlib.FmCreateExternalFunc.argtypes = [c_char_p, c_char_p, c_bool]
self._fmlib.FmCreateSineFunc.restype = c_int
self._fmlib.FmCreateSineFunc.argtypes = [
c_char_p,
c_char_p,
POINTER(c_double),
c_bool,
]
self._fmlib.FmCreateLinearFunc.restype = c_int
self._fmlib.FmCreateLinearFunc.argstypes = [
c_char_p,
c_char_p,
POINTER(c_double),
c_bool,
]
self._fmlib.FmCreatePolyFunc.restype = c_int
self._fmlib.FmCreatePolyFunc.argtypes = [
c_char_p,
c_char_p,
c_int,
POINTER(c_double),
POINTER(c_double),
c_int,
c_bool,
]
self._fmlib.FmCreateDeviceFunc.restype = c_int
self._fmlib.FmCreateDeviceFunc.argtypes = [
c_char_p,
c_char_p,
c_char_p,
c_char_p,
c_double,
c_bool,
c_double,
c_bool,
]
self._fmlib.FmCreateSensor.restype = c_int
self._fmlib.FmCreateSensor.argtypes = [
c_char_p,
c_char_p,
c_int,
c_int,
c_int,
c_int,
]
self._fmlib.FmSetFunctionArg.restype = c_bool
self._fmlib.FmSetFunctionArg.argtypes = [c_int, c_int, c_int, c_int, c_int]
self._fmlib.FmLoadPart.restype = c_int
self._fmlib.FmLoadPart.argtypes = [c_char_p, c_char_p]
self._fmlib.FmCreateStrainRosette.restype = c_int
self._fmlib.FmCreateStrainRosette.argtypes = [
c_char_p,
c_int,
c_int,
POINTER(c_int),
POINTER(c_double),
c_double,
c_bool,
]
self._fmlib.FmCreateUDE2.restype = c_int
self._fmlib.FmCreateUDE2.argtypes = [c_char_p, c_int, c_int]
self._fmlib.FmCreateAssembly.restype = c_int
self._fmlib.FmCreateAssembly.argtypes = [c_char_p, c_int, POINTER(c_int)]
self._fmlib.FmMoveObject.restype = c_bool
self._fmlib.FmMoveObject.argtypes = [c_int, POINTER(c_double), c_int, c_int]
self._fmlib.FmConstrainObject.restype = c_bool
self._fmlib.FmConstrainObject.argtypes = [c_int, c_int, c_int]
self._fmlib.FmAddMass.restype = c_bool
self._fmlib.FmAddMass.argtypes = [c_int, c_int, POINTER(c_double), c_int]
self._fmlib.FmDofProperty.restype = c_bool
self._fmlib.FmDofProperty.argtypes = [c_int, c_int, c_int, c_double, c_int]
self._fmlib.FmStructDamp.restype = c_bool
self._fmlib.FmStructDamp.argtypes = [c_int, c_double, c_double]
self._fmlib.FmReduceOpts.restype = c_bool
self._fmlib.FmReduceOpts.argtypes = [c_int, c_int, c_bool]
self._fmlib.FmRecoverOpts.restype = c_bool
self._fmlib.FmRecoverOpts.argtypes = [c_int, c_int, c_bool]
if model_file is None or not path.isfile(model_file) or force_new:
self.fm_new(model_file)
elif not self.fm_open(model_file):
print(" *** Failed to open model file", model_file)
def _convert_ids(self, obj_ids, obj_type):
"""
Convenience method, to reduce cognitive complexity.
"""
if isinstance(obj_ids, list):
return obj_ids
return self._convert_id(obj_ids, obj_type, True)
[docs]
def open(self, model_file):
"""
Opens the specified model file and prints out some key model parameters.
Parameters
----------
model_file : str
Absolute path of the Fedem model file to open
Returns
-------
bool
True on success, otherwise False
"""
if not self.fm_open(model_file):
print(" *** Failed to open model file", model_file)
return False
print(" * Model file", model_file, "successfully opened")
print(" Number of Triads:", self.fm_count(FmType.TRIAD))
print(" Number of Beams:", self.fm_count(FmType.BEAM))
print(" Number of Parts:", self.fm_count(FmType.FEPART))
print(" Total number of mechanism objects:", self.fm_count())
return True
[docs]
def save(self, model_file=None):
"""
Saves current model into the specified model file.
If no model_file is given, that last opened model file is overwritten.
Parameters
----------
model_file : str, default=None
Absolute path of the Fedem model file to save to
Returns
-------
bool
True on success, otherwise False
"""
status = self.fm_save(model_file)
if not status:
print(" *** Failed to save model file", model_file)
return status
[docs]
def close(self, save=False, remove_singletons=False):
"""
Closes the currently open model.
Parameters
----------
save : bool, default=False
If True, the model file is updated with the current model
remove_singletons : bool, default=False
If True, heap-allocated singelton objects are also released
Returns
-------
bool
True on success, otherwise False
"""
status = self.fm_save() if save else True
if not status:
print(" *** Failed to save current fedem model")
self.fm_close(True)
else:
self.fm_close(remove_singletons)
return status
[docs]
def make_triad(self, name, pos=None, rot=None, node=0, on_part=0, tag=None):
"""
Creates a new triad at specified location or nodal point.
Parameters
----------
name : str
Description of the new triad
pos : (float, float, float)
Global XYZ-coordinates of new triad
rot : (float, float, float), default=None
Global Euler angles giving the orientation of new triad
node : int, default=0
FE node number to associate the triad with.
Used only if `on_part` is specified.
on_part : int or str, default=0
Base Id or tag of the part that this triad should be attached to.
You can also specify the Reference plane here,
to create a new triad that is attached to ground.
tag : str, default=None
Tag to associate the created triad with
Returns
-------
int
Base Id of new triad, zero or negative on error
"""
part_ = self._convert_id(on_part)
if node > 0 and part_.value > 2:
triad = self._fmlib.FmTriadOnNode(
_convert_char(name), _convert_int(node), part_
)
elif len(pos) < 3:
print(" *** Invalid Triad position", pos)
triad = -1
elif rot is None:
if len(pos) > 5:
triad = self._fmlib.FmCreateTriad(
_convert_char(name),
_convert_real(pos[0]),
_convert_real(pos[1]),
_convert_real(pos[2]),
_convert_real(pos[3]),
_convert_real(pos[4]),
_convert_real(pos[5]),
part_,
)
else:
triad = self._fmlib.FmCreateTriad(
_convert_char(name),
_convert_real(pos[0]),
_convert_real(pos[1]),
_convert_real(pos[2]),
c_double(0),
c_double(0),
c_double(0),
part_,
)
elif len(rot) < 3:
print(" *** Invalid Triad rotation", rot)
triad = -1
else:
triad = self._fmlib.FmCreateTriad(
_convert_char(name),
_convert_real(pos[0]),
_convert_real(pos[1]),
_convert_real(pos[2]),
_convert_real(rot[0]),
_convert_real(rot[1]),
_convert_real(rot[2]),
part_,
)
if tag is not None and triad > 0:
self.fm_tag_object(triad, tag)
return triad
[docs]
def make_beam(self, name, triads, bprop=None, tag=None):
"""
Creates a string of beam elements.
Parameters
----------
name : str
Description of the new beam(s)
triads : list of int or list of str
List of base Ids or tags of the connected triads
bprop : int or str, default=None
Base Id or tag of beam property to use
tag : str, default=None
Tag to associate the created beam(s) with
Returns
-------
list of int
Base Ids of the created beams, None if error
"""
if len(triads) < 2:
print(" *** make_beam: At least two triads must be specified")
return None
base_ids = []
for i in range(len(triads) - 1):
base_id = self._fmlib.FmCreateBeam(
_convert_char(name),
self._convert_id(triads[i], FmType.TRIAD),
self._convert_id(triads[i + 1], FmType.TRIAD),
self._convert_id(bprop, FmType.BEAM_PROP),
)
if base_id < 1:
return None
base_ids.append(base_id)
if tag is not None:
self.fm_tag_object(base_ids, tag)
return base_ids
[docs]
def make_beam_section(self, name, mat, bprops, tag=None):
"""
Creates a beam cross section property object.
Parameters
----------
name : str
Description of the new beam property
mat : int or str
Cross section type flag.
If zero, a Generic cross section is defined.
If str or a non-zero int, a Pipe cross section is defined,
and the value gives the tag or base Id of the material to use.
bprop : list of float
List of property data
tag : str, default=None
Tag to associate the created beam property with
Returns
-------
int
Base Id of beam property object, zero or negative on error
"""
nprop_, prop_ = _convert_float_array(bprops)
bprop = self._fmlib.FmCreateBeamProperty(
_convert_char(name),
self._convert_id(mat, FmType.MAT_PROP),
nprop_,
prop_,
)
if tag is not None and bprop > 0:
self.fm_tag_object(bprop, tag)
return bprop
[docs]
def make_beam_material(self, name, mprops, tag=None):
"""
Creates a material property object.
Parameters
----------
name : str
Description of the new material property
mprop : list of float
List of property data
tag : str, default=None
Tag to associate the created material property with
Returns
-------
int
Base Id of material property object, zero or negative on error
"""
nprop_, prop_ = _convert_float_array(mprops)
mprop = self._fmlib.FmCreateMaterialProperty(_convert_char(name), nprop_, prop_)
if tag is not None and mprop > 0:
self.fm_tag_object(mprop, tag)
return mprop
[docs]
def make_spring(self, name, triads, **kwargs):
"""
Creates axial spring elements.
Parameters
----------
name : str
Description of the new spring(s)
triads : (str, str) or (int, int) or list of (int, int)
Tags or base Ids of the connected triads.
If a list of tuples is specified,
one spring is created for each tuple.
tag : str, default=None
Tag to associate the created beam(s) with
kwargs : dict
Keyword arguments defining the spring properties.
The following keywords are currently supported:
* `tag` : Tag to associate the created spring(s) with
* `constDefl`: Constant stress free deflection
* `constLength`: Constant stress free length
* `length`: User Id of general function defining stress free length
* `init_Stiff_Coeff`: Constant spring stiffness coefficient
* `fn`: Base Id of spring stiffness function
* `xy`: List of XY-pairs giving a piece-wise linear spring stiffness
* `extrapol_type`: String, either "NONE" (default), "FLAT" or "LINEAR"
* `spring_characteristics`: String, either "SPR_TRA_STIFF" (default)
or "SPR_TRA_FORCE"
Returns
-------
int or list of int
Base Id(s) of the created spring(s), None if an error occurs
"""
n_ = c_int(0)
x_ = None
y_ = None
e_ = c_int(0)
use_constant_defl_ = c_bool(True) # default is constant deflection
if "constDefl" in kwargs:
const_length_defl_ = _convert_real(kwargs["constDefl"])
elif "constLength" in kwargs:
const_length_defl_ = _convert_real(kwargs["constLength"])
use_constant_defl_ = c_bool(False)
else:
const_length_defl_ = c_double(0)
if "fn" in kwargs: # Base Id of existing spring function
sp_ = _convert_int(kwargs["fn"])
else:
sp_ = c_int(0)
if "xy" in kwargs:
# We have data of a new spring stiffness function
n_, x_, y_, e_ = _extract_polyline(kwargs)
charac = [
"SPR_TRA_STIFF", # stiffness - translational deflection
"SPR_TRA_FORCE", # force - translational deflection
]
sp_charac = kwargs.get("spring_characteristics", charac[0])
if sp_charac in charac:
# Notice negative value to indicate spring function type
sp_ = c_int(-charac.index(sp_charac))
one_spring = isinstance(triads, tuple)
if one_spring:
triads = [triads] # Only one spring is created
base_ids = []
for triad in triads:
s_id = self._fmlib.FmCreateSpring(
_convert_char(name),
self._convert_id(triad[0], FmType.TRIAD),
self._convert_id(triad[1], FmType.TRIAD),
const_length_defl_,
use_constant_defl_,
_convert_real(kwargs.get("init_Stiff_Coeff", 0.0)),
byref(sp_),
n_,
x_,
y_,
e_,
_convert_int(kwargs.get("length", 0)),
)
if s_id > 0:
base_ids.append(s_id)
if len(base_ids) < len(triads):
return None # Failure creating at least one spring
if "tag" in kwargs:
self.fm_tag_object(base_ids, kwargs["tag"])
if one_spring:
return base_ids[0]
return base_ids
[docs]
def make_damper(self, name, triads, **kwargs):
"""
Creates axial damper elements.
Parameters
----------
name : str
Description of the new damper(s)
triads : (str, str) or (int, int) or list of (int, int)
Tags or base Ids of the connected triads.
If a list of tuples is specified,
one damper is created for each tuple.
kwargs : dict
Keyword arguments defining the damper properties.
The following keywords are currently supported:
* `tag` : Tag to associate the created damper(s) with
* `def_vel_damper`: If True, use deformational velocity
* `init_Damp_Coeff`: Constant damping coefficient
* `fn`: Base Id of damping coefficient function
* `xy`: List of XY-pairs giving a piece-wise linear damping coefficient
* `extrapol_type`: String, either "NONE" (default), "FLAT" or "LINEAR"
* `damp_characteristics`: String, either "DA_TRA_COEFF" (default)
or "DA_TRA_FORCE"
Returns
-------
int or list of int
Base Id(s) of the created damper(s), None if an error occurs
"""
n_ = c_int(0)
x_ = None
y_ = None
e_ = c_int(0)
if "fn" in kwargs: # Base Id of existing damper function
da_ = _convert_int(kwargs["fn"])
else:
da_ = c_int(0)
if "xy" in kwargs:
# We have data of a new damping coefficient function
n_, x_, y_, e_ = _extract_polyline(kwargs)
charac = [
"DA_TRA_COEFF", # damping coefficient - translational velocity
"DA_TRA_FORCE", # force - translational velocity
]
da_charac = kwargs.get("damp_characteristics", charac[0])
if da_charac in charac:
# Notice negative value to indicate damper function type
da_ = c_int(-charac.index(da_charac))
one_damper = isinstance(triads, tuple)
if one_damper:
triads = [triads] # Only one damper is created
base_ids = []
for triad in triads:
d_id = self._fmlib.FmCreateDamper(
_convert_char(name),
self._convert_id(triad[0], FmType.TRIAD),
self._convert_id(triad[1], FmType.TRIAD),
_convert_bool(kwargs.get("def_vel_damper", False)),
_convert_real(kwargs.get("init_Damp_Coeff", 0.0)),
byref(da_),
n_,
x_,
y_,
e_,
)
if d_id > 0:
base_ids.append(d_id)
if len(base_ids) < len(triads):
return None # Failure creating at least one damper
if "tag" in kwargs:
self.fm_tag_object(base_ids, kwargs["tag"])
if one_damper:
return base_ids[0]
return base_ids
[docs]
def make_joint(self, name, joint_type, follower, followed=None, tag=None):
"""
Creates a joint object.
Parameters
----------
name : str
Description of the new joint
joint_type : FmType
Type of joint
follower : int or str
Base Id or tag of the dependent joint triad
followed : int or str or list of int, default=None
Base Id or tag of the independent joint triad(s).
If None, the joint is connected to ground and the independent
triad is created at the same location as the dependent triad.
For point-to-path joints, the first two triads specified
are taken as the end points of the glider.
tag : str, default=None
Tag to associate the created joint with
Returns
-------
int
Base Id of joint object, zero or negative on error
"""
id_ = self._convert_id(follower, FmType.TRIAD)
if isinstance(followed, list):
nids_, ids_ = _convert_int_array(followed)
else:
nids_ = c_int(1)
ids_ = (c_int * 1)(self._convert_id(followed, FmType.TRIAD))
joint = self._fmlib.FmCreateJoint(
_convert_char(name), _convert_int(joint_type), id_, ids_, nids_
)
if tag is not None and joint > 0:
self.fm_tag_object(joint, tag)
return joint
[docs]
def make_load(
self, name, load_type, triad, load_dir, magnitude=None, fn=0, tag=None
):
"""
Creates an external load object.
Parameters
----------
name : str
Description of the new load
load_type : FmLoadType
Type of load
triad : int or str
Base Id or tag of triad where the load attacks
load_dir : (float, float, float)
Load direction vector
magnitude : str, default=None
Load magnitude expression
fn : int, default=0
User Id of load magnitude function
tag : str, default=None
Tag to associate the created load with
Returns
-------
int
Base Id of load object, zero or negative on error
"""
if len(load_dir) < 3:
print(" *** Invalid load direction vector", load_dir)
return -1
load = self._fmlib.FmCreateLoad(
_convert_char(name),
_convert_int(load_type),
self._convert_id(triad, FmType.TRIAD),
_convert_real(load_dir[0]),
_convert_real(load_dir[1]),
_convert_real(load_dir[2]),
_convert_char(magnitude),
_convert_int(fn),
)
if tag is not None and load > 0:
self.fm_tag_object(load, tag)
return load
[docs]
def make_function(self, name, **kwargs):
"""
Creates a general function of time.
The type of function to be created is determined
by which keyword arguments are provided.
The keyword determining the function type is below marked
by an asterix (:sup:`*`) in each case.
Parameters
----------
name : str
Description of the new function
kwargs : dict
Keyword arguments depending on function type.
The following **function types** and `keywords`
are currently supported:
1. **Polyline**
| `xy`:sup:`*`: List of `XY`-pairs giving a
piece-wise linear curve
| `extrapol_type`: String, either "NONE" (default),
"FLAT" or "LINEAR"
2. **Polyline-from-file**
| `filename`:sup:`*`: Name of file containing `XY`-pairs
| `ch_name`: String identifying the column to use
for multi-column files
| `sc_factor`: Scaling factor, default=1.0
| `z_adjust`: If True, the `y`-values are shifted
such that the first value is zero, default=False
| `v_shift`: Additional shift of the `y`-values, default=0.0
3. **Sine**
| `frequency`:sup:`*`: Angular frequency
| `amplitude`: Scaling factor, default=1.0
| `delay`: Phase shift, default=0.0
| `mean_value`: Constant shift, default=0.0
| `end`: Default=0.0, if greater than zero, the function
value is constant for `x` greater than `end`
The Sine function therefore evaluates to::
f(x) = amplitude*sin(frequency*x-delay) + mean_value
for `x`-values less than `end`, whereas ``f(x) = f(end)``
for `x` greater than `end`.
4. **Constant**
| `value`:sup:`*`: The constant function value, i.e.,
``f(x) = value`` for any `x`
5. **Linear**
| `slope`:sup:`*`: The scaling factor, i.e., ``f(x) = slope*x``
6. **Ramp**
| `start_ramp`:sup:`*`: Start point of sloped function domain
| `start_val`: Function value before `start_ramp`, default=0.0
| `slope`: The scaling factor, default=1.0
The Ramp function therefore evaluates to::
f(x) = start_val + slope*(x-start_ramp)
| for `x`-values greater than `start_ramp`, whereas
| ``f(x) = start_val`` for `x` less than or equal to `start_ramp`.
7. **Limited Ramp**
| `start_ramp`: Start point of sloped function domain, default=0.0
| `end_ramp`:sup:`*`: End point of sloped function domain
| `start_val`: Function value before `start_ramp`, default=0.0
| `slope`: The scaling factor, default=1.0
The Limited Ramp function therefore evaluates to::
f(x) = start_val + slope*(x-start_ramp)
| for `x`-values in the range [`start_ramp`, `end_ramp`]
| whereas ``f(x) = start_val`` for `x` less than `start_ramp`,
| and ``f(x) = f(end_ramp)`` for `x` greater than `end_ramp`.
8. **Math expression**
| `expression`:sup:`*`: String containing the expression to use
9. **External function**
| No keywords
In addition, the `tag` keyword is accepted for all function types,
to associate a specified tag with the created function, and the
`base_id` keyword is used to indicate if the base Id of the created
function should be returned, instead of the default user Id.
Returns
-------
int
User Id or base Id of created function object.
Will be 0 or negative if an error occurs.
"""
name_ = _convert_char(name)
tag_ = _convert_char(kwargs.get("tag", None))
bid_ = _convert_bool(kwargs.get("base_id", False))
if "xy" in kwargs:
# Polyline function, get abscissa and ordinate values
n_, x_, y_, e_ = _extract_polyline(kwargs)
return self._fmlib.FmCreatePolyFunc(name_, tag_, n_, x_, y_, e_, bid_)
if "frequency" in kwargs:
# Sine function, get function parameters
sine_par = (c_double * 5)()
sine_par[0] = c_double(kwargs.get("frequency", 1.0))
sine_par[1] = c_double(kwargs.get("delay", 0.0))
sine_par[2] = c_double(kwargs.get("amplitude", 1.0))
sine_par[3] = c_double(kwargs.get("mean_value", 0.0))
sine_par[4] = c_double(kwargs.get("end", 0.0))
return self._fmlib.FmCreateSineFunc(name_, tag_, sine_par, bid_)
if "filename" in kwargs:
# Polyline-from-file function
return self._fmlib.FmCreateDeviceFunc(
name_,
tag_,
_convert_char(kwargs.get("filename", None)),
_convert_char(kwargs.get("ch_name", None)),
_convert_real(kwargs.get("sc_factor", 1.0)),
_convert_bool(kwargs.get("z_adjust", False)),
_convert_real(kwargs.get("v_shift", 0.0)),
bid_,
)
if "expression" in kwargs:
# Math expression function
return self._fmlib.FmCreateMathExprFunc(
name_, tag_, _convert_char(kwargs["expression"]), bid_
)
if "slope" in kwargs or "value" in kwargs:
params = (c_double * 4)(*([0.0] * 4))
if "end_ramp" in kwargs: # Limited ramp function
params[0] = c_double(kwargs.get("slope", 1.0))
params[1] = c_double(kwargs.get("start_val", 0.0))
params[2] = c_double(kwargs.get("start_ramp", 0.0))
params[3] = c_double(kwargs.get("end_ramp", 5.0))
elif "start_ramp" in kwargs: # Ramp function
params[0] = c_double(kwargs.get("slope", 1.0))
params[1] = c_double(kwargs.get("start_val", 0.0))
params[2] = c_double(kwargs.get("start_ramp", 0.0))
params[3] = params[2]
elif "slope" in kwargs: # Linear function
params[0] = c_double(kwargs.get("slope", 1.0))
else: # Constant function
params[1] = c_double(kwargs.get("value", 0.0))
return self._fmlib.FmCreateLinearFunc(name_, tag_, params, bid_)
# By default (if kwargs is empty), create an external function
return self._fmlib.FmCreateExternalFunc(name_, tag_, bid_)
[docs]
def make_sensor(self, name, obj, var, dof=None, tag=None):
"""
Creates a sensor object.
Parameters
----------
name : str
Description of the new sensor
obj : int or str or (int, int) or (str, str)
Base Id or tag of object(s) to measure
var : FmVar
The variable to measure
dof : FmDof, default=None
Local DOF to measure if `var` is a multi-DOF quantity
tag : str, default=None
Tag to associate the created sensor with
Returns
-------
int
User Id of sensor object, zero or negative on error
"""
if isinstance(obj, tuple):
obj1_ = self._convert_id(obj[0])
obj2_ = self._convert_id(obj[1])
else:
obj1_ = self._convert_id(obj)
obj2_ = c_int(0)
return self._fmlib.FmCreateSensor(
_convert_char(name),
_convert_char(tag),
_convert_int(var),
_convert_int(dof),
obj1_,
obj2_,
)
[docs]
def make_fe_part(self, file, name=None, tag=None):
"""
Creates an FE part.
Parameters
----------
file : str
Absolute path to the FE data file
name : str, default=None
Description of the new part, use file basename if not specified
tag : str, default=None
Tag to associate the created part with
Returns
-------
int
Base Id of the FE part object, zero or negative on error
"""
part = self._fmlib.FmLoadPart(_convert_char(file), _convert_char(name))
if tag is not None and part > 0:
self.fm_tag_object(part, tag)
return part
[docs]
def make_strain_rosette(self, name, part_id, **kwargs):
"""
Creates a strain rosette on an FE part.
Parameters
----------
name : str
Description of the new strain rosette
part_id : int or str
Base Id or tag of FE part on which the rosette will be created
kwargs : dict
Keyword arguments defining the strain rosette properties.
The following keywords are recognized:
* `tag` : Tag to associate the created strain rosette with
* `nodes`: List of nodes to connect the strain rosette to
* `pos`: List of connection point coordinates, on the format
`[(x1,y1,z1), (x2,y2,z2), (x3,y3,z3), (x4,y4,z4)]`.
The last tuple is skipped if a 3-noded rosette is desired.
* `direction`: Tuple `(x,y,z)` defining the local `X`-axis
direction vector of the strain rosette
* `angle` : Angle between strain gage direction and local X-axis
* `start_at_zero` : If True, the start strains are set to zero
Returns
-------
int
Base Id of the strain rosette, zero or negative on error
"""
name_ = _convert_char(name)
part_ = self._convert_id(part_id, FmType.FEPART)
nodes_ = (c_int * 4)()
node_id = kwargs.get("nodes", None)
node_pos = kwargs.get("pos", None)
if isinstance(node_pos, list):
# Nodal positions are provided, find the closest FE nodes
nnod_ = c_int(len(node_pos))
for idx, pos in enumerate(node_pos):
nodes_[idx] = self.fm_get_node(part_, pos)
elif isinstance(node_id, list):
# The node Id's are provided directly
nnod_ = c_int(len(node_id))
nodes_[0 : len(node_id)] = node_id
else:
print(" *** Neither nodes nor pos is specified")
return -1
dirvec_ = (c_double * 3)()
direction = kwargs.get("direction", None)
if isinstance(direction, tuple):
dirvec_[:] = direction
else:
dirvec_[:] = [1.0, 0.0, 0.0]
ros = self._fmlib.FmCreateStrainRosette(
name_,
part_,
nnod_,
nodes_,
dirvec_,
_convert_real(kwargs.get("angle", 0.0)),
_convert_bool(kwargs.get("start_at_zero", True)),
)
if ros > 0: # Activate strain gage recovery during solve for this part
self._fmlib.FmRecoverOpts(part_, c_int(2), c_bool(True))
if "tag" in kwargs and ros > 0:
self.fm_tag_object(ros, _convert_char(kwargs["tag"]))
return ros
[docs]
def make_udelm(self, name, triads, **kwargs):
"""
Creates a string of 2-noded user-defined elements.
Parameters
----------
name : str
Description of the new element(s)
triads : list of int or list of str
List of base Ids or tags of the connected triads
kwargs : dict
Keyword arguments defining some element properties.
Currently, the following keywords are recognized:
* `alpha1` : Mass-proportional damping coefficient
* `alpha2` : Stiffness-proportional damping coefficient
* `tag` : Tag to associate the created element with
Returns
-------
list of int
Base Ids of the created elements, None if error
"""
if len(triads) < 2:
print(" *** make_udelm: At least two triads must be specified")
return None
base_ids = []
for i in range(len(triads) - 1):
base_id = self._fmlib.FmCreateUDE2(
_convert_char(name),
self._convert_id(triads[i], FmType.TRIAD),
self._convert_id(triads[i + 1], FmType.TRIAD),
)
if base_id < 1:
return None
base_ids.append(base_id)
if "alpha1" in kwargs or "alpha2" in kwargs:
self._fmlib.FmStructDamp(
base_id,
_convert_real(kwargs.get("alpha1", 0.0)),
_convert_real(kwargs.get("alpha2", 0.0)),
)
if "tag" in kwargs:
self.fm_tag_object(base_ids, _convert_char(kwargs["tag"]))
return base_ids
[docs]
def make_assembly(self, name, objs=None):
"""
Creates a sub-assembly and moves the specified objects into it.
Parameters
----------
name : str
Description of the new sub-assembly
objs : list of int or str or list of str, default=None
List of base Ids or tags of the objects to put into the new sub-assembly
Returns
-------
int
Base Id of the created sub-assembly
"""
obj_ids = []
if isinstance(objs, (list, tuple)):
for obj in objs:
id_ = self._convert_id(obj, FmType.ALL, True)
if isinstance(id_, list):
obj_ids.extend(id_)
else:
obj_ids.append(id_)
elif isinstance(objs, str):
obj_ids = self._convert_id(objs, FmType.ALL, True)
elif isinstance(objs, (int, c_int)):
obj_ids = [objs]
num_obj_, obj_ids_ = _convert_int_array(obj_ids)
return self._fmlib.FmCreateAssembly(_convert_char(name), num_obj_, obj_ids_)
def _move_object(self, base_id, kwargs):
"""
Moves an existing object identified by its base Id.
"""
dofs_ = _extract_dofs(kwargs)
if dofs_ is None:
return True # No movement specified, silently ignore
return self._fmlib.FmMoveObject(
_convert_int(base_id),
dofs_,
_convert_int(kwargs.get("tra_ref", 0)),
_convert_int(kwargs.get("rot_ref", 0)),
)
def _constrain_object(self, base_id, kwargs):
"""
Constrains DOFs in an existing object identified by its base Id.
"""
if kwargs is None:
return True # No constraints specified, silently ignore
dof_tags = ["Tx", "Ty", "Tz", "Rx", "Ry", "Rz", "All"]
for key, value in kwargs.items():
if key in dof_tags:
dof_ = c_int(dof_tags.index(key))
con_ = _convert_int(value, FmDofStat)
print(f"Constraining {key} in object [{base_id.value}]", value)
if not self._fmlib.FmConstrainObject(base_id, dof_, con_):
return False
return True
def _dof_property(self, base_id, prt, kwargs):
"""
Assigns a DOF property in an existing object identified by its base Id.
"""
if kwargs is None:
return True # No properties specified, silently ignore
pr_type_ = c_int(prt)
dof_tags = ["Tx", "Ty", "Tz", "Rx", "Ry", "Rz"]
for key, value in kwargs.items():
if key in dof_tags:
dof_ = c_int(dof_tags.index(key))
if prt in (1, 4):
fcn_ = _convert_int(value, int_or_none=True)
else:
fcn_ = None
if fcn_ is None: # A constant property value is specified
val_ = _convert_real(value)
fcn_ = c_int(0)
else: # The user Id of a general function is specified
val_ = c_double(0)
print(f"{key} property {prt} in object [{base_id.value}]", value, fcn_)
if not self._fmlib.FmDofProperty(base_id, dof_, pr_type_, val_, fcn_):
return False
return True
[docs]
def edit_triad(self, obj_id, **kwargs):
"""
Modifies an existing triad.
Parameters
----------
obj_id : int or str or list of int or list of str
Base Id or tag of the triad(s) to modify
kwargs : dict
Keyword arguments containing the new triad attributes to assign.
Currently, the following keywords are recognized:
* Tx, Ty, Tz : Translation offset in global X, Y, and Z-direction
* Rx, Ry, Rz : Euler angles (in degrees) for rotating the Triad
* tra_ref : Base Id of object used as translation reference
* rot_ref : Base Id of object used as rotation reference
* mass : Additional mass (and inertia) on the Triad
* mass_func : User Id or base Id (if negative) of mass scaling function
* constraints : Dictionary with keywords for constraing DOFs, i.e.,
``constraints={"Tx" : <value>, ..., "Rz" : <value>}``, where
``<value>`` can be any of the enum values :class:`enums.FmDofStat`.
Only the DOFs that should be changed need to be specified.
You can also use "All" as key which implies all DOFs in the triad.
* init_vel : Dictionary with keywords specifying initial velocities,
i.e., ``init_vel={"Tx" : <u01>, ..., "Rz" : <u06>}``, where
``<u0i>`` is the initial velocity to be assigned local dof `i`.
* load : Dictionary with keywords specifying constant DOF loads,
i.e., ``load={"Tx" : <f1>, ..., "Rz" : <f6>}``, where
``<fi>`` is the user Id of a general function defining the load
in local dof `i` if the type is int, otherwise it is taken as the
constant load to be assigned local dof `i`.
* motion : Dictionary with keywords specifying prescribed motions,
i.e., ``motion={"Tx" : <u1>, ..., "Rz" : <u6>}``, where
``<ui>`` is the user Id of a general function defining the motion
in local dof `i` if the type is int, otherwise it is taken as the
constant motion to be prescribed in local dof `i`.
Returns
-------
bool
True if the input is valid, otherwise False
"""
# Convert tag to base Id or list of base Id
base_id = self._convert_ids(obj_id, FmType.TRIAD)
retval = True
if isinstance(base_id, list): # Process a list of triads
for bid in base_id:
retval &= self.edit_triad(bid, **kwargs)
return retval
retval = self._move_object(base_id, kwargs)
if not retval:
print(" *** Failed to move triad", base_id.value)
mass = kwargs.get("mass", None)
if mass is not None:
if isinstance(mass, tuple):
n_, m_ = _convert_float_array(mass)
else:
n_ = c_int(1)
m_ = (c_double * 1)(_convert_real(mass))
mfunc_ = _convert_int(kwargs.get("mass_function", 0))
if not self._fmlib.FmAddMass(base_id, n_, m_, mfunc_):
print(" *** Failed to add mass on triad", base_id.value)
retval = False
if not self._constrain_object(base_id, kwargs.get("constraints", None)):
print(" *** Failed to constrain triad", base_id.value)
retval = False
if not self._dof_property(base_id, 0, kwargs.get("init_vel", None)):
print(" *** Failed to assign initial velocity to triad", base_id.value)
retval = False
load = kwargs.get("load", None)
if load is None:
load = kwargs.get("motion", None)
if not self._dof_property(base_id, 1, load):
print(" *** Failed to assign load/motion to triad", base_id.value)
retval = False
return retval
[docs]
def edit_part(self, obj_id, **kwargs):
"""
Modifies an existing FE part.
Parameters
----------
obj_id : int or str or list of int or list of str
Base Id or tag of the part(s) to modify
kwargs : dict
Keyword arguments containing the new part attributes to assign.
Currently, the following keywords are recognized:
* Tx, Ty, Tz : Translation offset in global X, Y, and Z-direction
* Rx, Ry, Rz : Euler angles (in degrees) for rotating the Part
* tra_ref : Base Id of object used as translation reference
* rot_ref : Base Id of object used as rotation reference
* alpha1 : Mass-proportional damping coefficient
* alpha2 : Stiffness-proportional damping coefficient
* component_modes : Number of component modes
* consistent_mass : If True, use consistent mass (lumped is default)
* recovery : Flag for activating FE part recovery during solve
(0=off, 1=stress recovery, 2=strain gage recovery, 3=both)
Returns
-------
bool
True if the input is valid, otherwise False
"""
# Convert tag to base Id or list of base Id
base_id = self._convert_ids(obj_id, FmType.FEPART)
retval = True
if isinstance(base_id, list): # Process a list of parts
for bid in base_id:
retval &= self.edit_part(bid, **kwargs)
return retval
retval = self._move_object(base_id, kwargs)
if "alpha1" in kwargs or "alpha2" in kwargs:
retval = retval and self._fmlib.FmStructDamp(
base_id,
_convert_real(kwargs.get("alpha1", 0.0)),
_convert_real(kwargs.get("alpha2", 0.0)),
)
if "component_modes" in kwargs or "consistent_mass" in kwargs:
retval = retval and self._fmlib.FmReduceOpts(
base_id,
_convert_int(kwargs.get("component_modes", 0)),
_convert_bool(kwargs.get("consistent_mass", False)),
)
if "recovery" in kwargs:
retval = retval and self._fmlib.FmRecoverOpts(
base_id, _convert_int(kwargs["recovery"]), c_bool(False)
)
if not retval:
print(" *** Failed to edit properties on part", base_id.value)
return retval
[docs]
def edit_joint(self, obj_id, **kwargs):
"""
Modifies an existing joint.
Parameters
----------
obj_id : int or str or list of int or list of str
Base Id or tag of the joint(s) to modify
kwargs : dict
Keyword arguments containing the new joint attributes to assign.
Currently, the following keywords are recognized:
* Tx, Ty, Tz : Translation offset in global X, Y, and Z-direction
* Rx, Ry, Rz : Euler angles (in degrees) for rotating the joint
* tra_ref : Base Id of object used as translation reference
* rot_ref : Base Id of object used as rotation reference
* constraints : Dictionary with keywords for constraing DOFs, i.e.,
``constraints={"Tx" : <value>, ..., "Rz" : <value>}``, where
``<value>`` can be any of the enum values :class:`enums.FmDofStat`.
Only the DOFs that should be changed need to be specified.
You can also use "All" as key which implies all DOFs in the joint.
* init_vel : Dictionary with keywords specifying initial velocities,
i.e., ``init_vel={"Tx" : <u01>, ..., "Rz" : <u06>}``, where
``<u0i>`` is the initial velocity to be assigned local dof `i`.
* load : Dictionary with keywords specifying constant DOF loads,
i.e., ``load={"Tx" : <f1>, ..., "Rz" : <f6>}``, where
``<fi>`` is the user Id of a general function defining the load
in local dof `i` if the type is int, otherwise it is taken as the
constant load to be assigned local dof `i`.
* motion : Dictionary with keywords specifying prescribed motions,
i.e., ``motion={"Tx" : <u1>, ..., "Rz" : <u6>}``, where
``<ui>`` is the user Id of a general function defining the motion
in local dof `i` if the type is int, otherwise it is taken as the
constant motion to be prescribed in local dof `i`.
* spring : Dictionary with keywords specifying spring stiffnesses,
i.e., ``spring={"Tx" : <k1>, ..., "Rz" : <k6>}``, where
``<ki>`` is the constant stiffness to be assigned local dof `i`.
* damper : Dictionary with keywords specifying damping coefficients,
i.e., ``load={"Tx" : <c1>, ..., "Rz" : <c6>}``, where
``<ci>`` is the constant damping to be assigned local dof `i`.
* length : Dictionary with keywords specifying stress-free lengths,
i.e., ``length={"Tx" : <l1>, ..., "Rz" : <l6>}``, where
``<li>`` is the user Id of a general function defining the
stress-free length in local dof `i`.
Returns
-------
bool
True if the input is valid, otherwise False
"""
# Convert tag to base Id or list of base Id
base_id = self._convert_ids(obj_id, FmType.JOINT)
retval = True
if isinstance(base_id, list): # Process a list of joints
for bid in base_id:
retval &= self.edit_joint(bid, **kwargs)
return retval
retval = self._move_object(base_id, kwargs)
if not retval:
print(" *** Failed to move joint", base_id.value)
if not self._constrain_object(base_id, kwargs.get("constraints", None)):
print(" *** Failed to constrain joint", base_id.value)
retval = False
if not self._dof_property(base_id, 0, kwargs.get("init_vel", None)):
print(" *** Failed to assign initial velocity to joint", base_id.value)
retval = False
load = kwargs.get("load", None)
if load is None:
load = kwargs.get("motion", None)
if not self._dof_property(base_id, 1, load):
print(" *** Failed to assign load/motion to joint", base_id.value)
retval = False
if not self._dof_property(base_id, 2, kwargs.get("spring", None)):
print(" *** Failed to assign spring stiffness to joint", base_id.value)
retval = False
if not self._dof_property(base_id, 3, kwargs.get("damper", None)):
print(" *** Failed to assign damping coefficient to joint", base_id.value)
retval = False
if not self._dof_property(base_id, 4, kwargs.get("length", None)):
print(" *** Failed to assign stress-free lengths to joint", base_id.value)
retval = False
return retval
[docs]
def edit_function(self, func_id, obj, var=None, dof=None):
"""
Modifies an existing function by changing its argument.
Parameters
----------
func_id : int
User Id (or base Id, if negative) of the function to modify
obj : int or str or (int, int) or (str, str)
Base Id or tag of object(s) producing the response quantity to use
var : FmVar, default=None=0
Response quantity to use as function argument
dof : FmDof, default=None=0
Which component of `var` to be used if a multi-DOF quantity
Returns
-------
bool
True if the input is valid, otherwise False
"""
if isinstance(obj, tuple):
obj1_ = self._convert_id(obj[0])
obj2_ = self._convert_id(obj[1])
else:
obj1_ = self._convert_id(obj)
obj2_ = c_int(0)
retval = self._fmlib.FmSetFunctionArg(
_convert_int(func_id),
_convert_int(var),
_convert_int(dof),
obj1_,
obj2_,
)
if not retval:
print(" *** Failed to change argument of Function", func_id)
print(" to", obj, var, dof)
return retval
[docs]
def solver_setup(self, **kwargs):
"""
Setting up solver parameters, t_quasi, t_inc and t_end.
Switching off initial equilibrium by setting t_quasi negative.
Parameters
----------
kwargs : dict
Keyword arguments containing the solver settings to assign.
Currently, the following keywords are recognized:
* t_start : Start time
* t_end : Stop time
* t_inc : Time step size
* t_quasi : Stop time for quasi-static simulation.
If equal to t_start, perform initial equilibrium analysis
before starting the dynamics time integration.
* n_modes : If non-zero, perform eigenvalue analysis during the simulation,
and calculate this number of modes each time
* e_inc: Time between each eigenvalue analysis
* add_opt: Additional solver options
* tol_ene: Convergence tolerance in energy norm
* tol_dis: Convergence tolerance in displacement norm
* tol_vel: Convergence tolerance in velocity norm
* tol_res: Convergence tolerance in force residual norm
"""
self.fm_solver_setup(
kwargs.get("t_start", 0.0),
kwargs.get("t_end", 1.0),
kwargs.get("t_inc", 0.01),
kwargs.get("t_quasi", 0.0),
kwargs.get("n_modes", 0),
kwargs.get("e_inc", 0.0),
kwargs.get("add_opt", None),
)
if len(set(kwargs.keys()) & {"tol_ene", "tol_dis", "tol_vel", "tol_res"}) > 0:
self.fm_solver_tol(
kwargs.get("tol_ene", -1.0),
kwargs.get("tol_dis", -1.0),
kwargs.get("tol_vel", -1.0),
kwargs.get("tol_res", -1.0),
)