Source code for fmm

# SPDX-FileCopyrightText: 2023 SAP SE
#
# SPDX-License-Identifier: Apache-2.0
#
# This file is part of FEDEM - https://openfedem.org

"""
Python wrapper for the Fedem Mechanism Model database library.
Used for convenience in order to hide native type convertions.
"""

from ctypes import c_bool, c_char_p, c_double, c_int, cdll, create_string_buffer

from fedempy.enums import Enum, FmType


def _convert_char(arg):
    """
    Converts a string argument from Python to C type.
    """
    if arg is None:
        return c_char_p()

    if isinstance(arg, str):
        return c_char_p(arg.encode("utf-8"))

    if isinstance(arg, c_char_p):
        return arg

    raise TypeError(f"Expected {str} or {c_char_p}, got {type(arg)}.")


def _convert_int(arg, enum_type=None, int_or_none=False):
    """
    Converts an integer from Python to C type.
    If `enum_type` is specified, then `arg` needs to be an enum of that type.
    If `int_or_none` is True, then `None` is returned if `arg` is not an int,
    instead to raising an exception.
    """
    if arg is None:
        return c_int(0)

    if enum_type is not None:
        if isinstance(arg, enum_type):
            return c_int(arg.value)
        raise TypeError(f"Expected {enum_type}, got {type(arg)}.")

    if isinstance(arg, Enum):
        return c_int(arg.value)

    if isinstance(arg, int):
        return c_int(arg)

    if isinstance(arg, c_int):
        return arg

    if int_or_none:
        return None

    raise TypeError(f"Expected {Enum}, {int} or {c_int}, got {type(arg)}.")


def _convert_real(arg):
    """
    Converts a float from Python to C type.
    """
    if arg is None:
        return c_double(0)

    if isinstance(arg, (float, int)):
        return c_double(arg)

    if isinstance(arg, c_double):
        return arg

    raise TypeError(f"Expected {int}, {float} or {c_double}, got {type(arg)}.")


[docs] class FedemModel: """ This class wraps the Fedem model file handling functionality. Parameters ---------- lib_path : str Absolute path to the Fedem model database shared object library plugin1 : str, default=None Absolute path to user-defined element plugin (optional) plugin2 : str, default=None Absolute path to user-defined function plugin (optional) Methods ------- fm_open: Opens the specified Fedem model file fm_save: Saves current model with updated results to file fm_new: Initializes an empty model fm_close: Closes current model fm_count: Counts mechanism objects of specified type fm_get_objects: Returns a list of base Ids for all objects of specified type fm_tag_object: Tags the specified object(s) in the model fm_get_pos: Returns the global position of the specified object fm_get_node: Returns the FE node number matching a spatial point fm_sync_part: Syncronizes an FE part with content on disk fm_write_reducer: Writes reducer input files for an FE part fm_write_solver: Writes solver input files fm_solver_setup: Defines some basis solver setup parameters fm_solver_tol: Defines the solver convergence tolerances def fm_get_func_tag: Returns the tag of an indexed external function """ def __init__(self, lib_path, plugin1=None, plugin2=None): """ Constructor. Initializes the internal datastructure of the shared object library. """ self._fmlib = cdll.LoadLibrary(lib_path) self._fmlib.FmInit(_convert_char(plugin1), _convert_char(plugin2)) self._fmlib.FmOpen.restype = c_bool self._fmlib.FmOpen.argtypes = [c_char_p] self._fmlib.FmSave.restype = c_bool self._fmlib.FmSave.argtypes = [c_char_p] self._fmlib.FmCount.restype = c_int self._fmlib.FmCount.argstype = [c_int] self._fmlib.FmGetObjects.restype = c_int self._fmlib.FmTagObjects.restype = c_int self._fmlib.FmGetPosition.restype = c_bool self._fmlib.FmGetNode.restype = c_int self._fmlib.FmSync.restype = c_bool self._fmlib.FmSync.argstype = [c_int] self._fmlib.FmReduce.restype = c_bool self._fmlib.FmReduce.argtypes = [c_char_p, c_int] self._fmlib.FmSolve.restype = c_bool self._fmlib.FmSolve.argtypes = [c_char_p, c_bool, c_char_p, c_char_p] self._fmlib.FmGetFuncTag.restype = c_bool self._fmlib.FmGetFuncTag.argtypes = [c_int, c_char_p] def _convert_id(self, obj_id, obj_type=FmType.ALL, multiple=False): """ Converts `obj_id`, which can be either an integer base Id or a string tag, into C type base Id. If `multiple` is True and `obj_id` is a tag, a list of base Ids is returned, even if only one (or none) item matches. """ if not isinstance(obj_id, str): # The base Id is specified return _convert_int(obj_id) # The object tag is specified base_id = self.fm_get_objects(obj_type, obj_id) if multiple: return [c_int(bid) for bid in base_id] if len(base_id) == 1: return c_int(base_id[0]) # Expected one object, but found none or many return c_int(0)
[docs] def fm_open(self, fname): """ This method opens the specified Fedem model file and reads its content into the internal datastructure. Parameters ---------- fname : str Absolute path to the fmm-file to read Returns ------- bool True on success, otherwise False """ if fname is None: return False return self._fmlib.FmOpen(_convert_char(fname))
[docs] def fm_save(self, fname=None): """ This method saves the currently open Fedem model such that the model file is updated to reflect the current results data found on disk. Parameters ---------- fname : str, default=None Absolute path to the fmm-file to write to. If not given, the current model file as specified by the last call to fm_open() or fm_new() is overwritten. Returns ------- bool True on success, otherwise False """ return self._fmlib.FmSave(_convert_char(fname))
[docs] def fm_new(self, fname=None): """ This method initializes an empty Fedem model. Parameters ---------- fname : str, default=None Absolute path to the fmm-file to write to on next save. If not given, the name "untitled.fmm" will be used. Returns ------- bool Always True """ self._fmlib.FmNew(_convert_char(fname)) return True
[docs] def fm_close(self, remove_singletons=False): """ This method closes the currently open Fedem model to clean up memory. Parameters ---------- remove_singletons : bool, default=False If True, heap-allocated singelton objects are also released Returns ------- bool Always True """ if isinstance(remove_singletons, bool): self._fmlib.FmClose(c_bool(remove_singletons)) elif isinstance(remove_singletons, c_bool): self._fmlib.FmClose(remove_singletons) else: self._fmlib.FmClose(c_bool(False)) return True
[docs] def fm_count(self, object_type=FmType.ALL): """ This method returns the number of objects of the given type. Parameters ---------- object_type : FmType, default=ALL Enum value identifying which object type to count the instances of Returns ------- int Number of instances of the indicated object type """ return self._fmlib.FmCount(_convert_int(object_type, FmType))
[docs] def fm_get_objects(self, object_type=FmType.ALL, tag=None): """ This method returns the base Id of all objects of the given type, and/or those with the specified tag. Parameters ---------- object_type : FmType, default=ALL Enum value identifying which object type to return base Id of tag : str or list of str, default=None Return objects having this tag only, unless None Returns ------- list Base Id list of all instances in the model of the specified type """ base_id = [] if isinstance(tag, list): # Process a list of tags for atag in tag: base_id.extend(self.fm_get_objects(object_type, atag)) return base_id num_objs = self.fm_count(object_type) base_id_ = (c_int * num_objs)() obj_typ_ = c_int(object_type.value) num_objs = self._fmlib.FmGetObjects(base_id_, obj_typ_, _convert_char(tag)) base_id = [0] * num_objs base_id[:] = [base_id_[i] for i in range(num_objs)] return base_id
[docs] def fm_tag_object(self, base_id, tag): """ This method tags the specified object(s) identified by the base Id. Parameters ---------- base_id : int or list of int Base Id of the object(s) to tag tag : str The tag to assign Returns ------- int Number of tagged objects """ if isinstance(base_id, list): num_obj_ = c_int(len(base_id)) base_id_ = (c_int * len(base_id))() base_id_[:] = base_id else: num_obj_ = c_int(1) base_id_ = (c_int * 1)() base_id_[0] = base_id return self._fmlib.FmTagObjects(base_id_, num_obj_, _convert_char(tag))
[docs] def fm_get_pos(self, object_id): """ This method returns the global position of the specified object. Parameters ---------- object_id : int or str Base Id or tag of the object to return the position for Returns ------- (float, float, float) Global X-, Y-, and Z-coordinate """ pos_ = (c_double * 3)() if not self._fmlib.FmGetPosition(self._convert_id(object_id), pos_): return None return (pos_[0], pos_[1], pos_[2])
[docs] def fm_get_node(self, part_id, pos): """ This method returns the FE node number matching a spatial point. Parameters ---------- part_id : int Base Id or tag of the FE part that the node belongs to pos : (float, float, float) X-, Y-, and Z-coordinate of the node Returns ------- int Node Id """ pos_ = (c_double * 3)() pos_[:] = pos[0:3] return self._fmlib.FmGetNode(self._convert_id(part_id), pos_)
[docs] def fm_sync_part(self, base_id): """ This method syncronizes the currently open Fedem model with contents on disk for the specified FE part. It is typically used after the reduction process for an FE part is finished. Parameters ---------- base_id : int Base Id of the FE part to syncronize the model and disk content for Returns ------- bool True on success, otherwise (e.g., if base_id is invalid) False """ return self._fmlib.FmSync(_convert_int(base_id))
[docs] def fm_write_reducer(self, base_id): """ This method writes reducer input files for the specified FE part, unless that part is already reduced. Parameters ---------- base_id : int Base Id of the FE part to generate reducer input files for Returns ------- str Absolute path to the working directory of the reduction process """ rdbdir = create_string_buffer(512) if self._fmlib.FmReduce(rdbdir, _convert_int(base_id)): return rdbdir.value.decode("utf-8") return ""
[docs] def fm_write_solver(self, keep_old_res=False, ude=None, udf=None): """ This method writes solver input files for the currently loaded model. Parameters ---------- keep_old_res : bool, default=False Option to not overwrite any existing res-file in the RDB directory ude : str, default=None Absolute path to user-defined element plugin (optional) udf : str, default=None Absolute path to user-defined function plugin (optional) Returns ------- str Absolute path to the working directory of the solver process """ rdbdir = create_string_buffer(512) if self._fmlib.FmSolve( rdbdir, c_bool(keep_old_res), _convert_char(ude), _convert_char(udf) ): return rdbdir.value.decode("utf-8") return ""
[docs] def fm_solver_setup( self, t_start=0.0, t_end=1.0, t_inc=0.01, t_quasi=0.0, n_modes=0, e_inc=0.0, add_opt=None, ): """ This method (re)defines some basis solver setup parameters. Parameters ---------- t_start : float, default=0 Start time t_end : float, default=1 Stop time t_inc : float, default=0.01 Time step size t_quasi : float, default=0 Stop time for quasi-static simulation. If equal to t_start, also perform initial equilibrium analysis before starting the time stepping. n_modes : int, default=0 If non-zero, perform eigenvalue analysis during the simulation, and calculate this number of modes each time e_inc : float, default=0 Time between each eigenvalue analysis add_opt : str, default=None Additional solver options """ self._fmlib.FmSolveSetup( _convert_real(t_start), _convert_real(t_inc), _convert_real(t_end), _convert_real(t_quasi), _convert_real(e_inc), _convert_int(n_modes), _convert_char(add_opt), )
[docs] def fm_solver_tol(self, tol_ene=-1.0, tol_dis=-1.0, tol_vel=-1.0, tol_res=-1.0): """ This method (re)defines the solver convergence tolerances. Parameters ---------- tol_ene : float, default=-1.0 Energy norm tolerance tol_dis : float, default=-1.0 Displacement norm tolerance tol_vel : float, default=-1.0 Velocity norm tolerance tol_res : float, default=-1.0 Residual force norm tolerance """ self._fmlib.FmSolverTol( _convert_real(tol_ene), _convert_real(tol_dis), _convert_real(tol_vel), _convert_real(tol_res), )
[docs] def fm_get_func_tag(self, channel): """ This method returns the tag of an indexed external function. Parameters ---------- channel : int Channel index of the external function Returns ------- str The tag of the function, None if channel is out of range """ tag = create_string_buffer(512) if self._fmlib.FmGetFuncTag(_convert_int(channel), tag): return tag.value.decode("utf-8") return None