Module forcefield

Synopsis of forcefield Class

The forcefield class defines the core behavior for managing inter-particle interactions in molecular dynamics simulations or similar physics-based models. It provides methods to calculate interaction parameters, including pair styles, diagonal pair coefficients, and off-diagonal pair coefficients, which are crucial for forcefield models.

Key Attributes:

  • PAIR_STYLE (str): Defines the pair style command for the forcefield interactions.
  • PAIR_DIAGCOEFF (str): Defines the command for calculating diagonal pair coefficients.
  • PAIR_OFFDIAGCOEFF (str): Defines the command for calculating off-diagonal pair coefficients.
  • parameters (parameterforcefield): Stores the parameters used for interaction evaluations.
  • beadtype (int): The bead type used in the forcefield model.
  • userid (str): A unique identifier for the forcefield instance.

Key Methods:

  • pair_style(printflag=True): Returns the pair style command based on the current parameters, beadtype, and userid.
  • pair_diagcoeff(printflag=True, i=None): Returns the diagonal pair coefficient command based on the current bead type i and userid. The bead type can be overridden.
  • pair_offdiagcoeff(o=None, printflag=True, i=None): Returns the off-diagonal pair coefficient command for interactions between two bead types or forcefield objects. The bead type i and the interacting forcefield o can be specified or overridden.

    — forcefield methods for LAMMPS —

    The superclass provides a collection of classes to define materials and forcefields. Note that the following hierarchy is used: > forcefield() is the top class (to be called directly) > customff(forcefield) defines a new forcefield so called customff > customstyle(customff) defines a pair-style applicable to customff > custommaterial(customstyle) defines a new material

    — Material library (first implementations) —

    w = water(beadtype=1, userid="fluid")
    w.parameters.Cp = 20
    print("
    

    "*2,w)

    f = solidfood(beadtype=2, userid="elastic")
    print("
    

    "*2,f)

    r = rigidwall(beadtype=3, userid="wall")
    print("
    

    "*2,r)

    — Template to define a material —

      class mymaterial(myforcefield):
          userid = "short name"
          version = value
          def __init__(self, beadtype=1, userid=None):
              super().__init__()
              if userid!=None: self.userid = userid
              self.name.material"] = "short of the material"
              self.description.material"] = "full description"
              self.beadtype = beadtype
              self.parameters = parameterforcefield(
                  param1 = value1,
                  param2 = value2,
                  param3 = "math expression with ${param1, ${param2}"
              )
    

    — Example of outputs | LAMMPS:SMD:tlsph:solidfood —

    ========================== [ elastic | version=0.1 ] ===========================
    
      Bead of type 2 = [LAMMPS:SMD:tlsph:solidfood]
      -----------------:----------------------------------------
                    rho: 1000
                     c0: 10.0
                      E: 5*${c0}^2*${rho}
                     nu: 0.3
                     q1: 1.0
                     q2: 0.0
                     Hg: 10
                     Cp: 1.0
            sigma_yield: 0.1*${E}
              hardening: 0
          contact_scale: 1.5
      contact_stiffness: 2.5*${c0}^2*${rho}
      -----------------:----------------------------------------
    forcefield object with 12 parameters
    
    ............................... [ description ] ................................
    
            #       LAMMPS:SMD - solid, liquid, rigid forcefields (continuum mechanics)
            #       SMD:TLSPH - total Lagrangian for solids
            #       food beads - solid behavior
    
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [ methods ] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    
    replace FFi,FFj by your variable names <<<
            To assign a type, use: FFi.beadtype = integer value
            Use the methods FFi.pair_style() and FFi.pair_coeff(FFj)
            Note for pairs: the caller object is i (FFi), the argument is j (FFj or j)
    
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [ template ] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    
        # [2:elastic] PAIR STYLE SMD
        pair_style      hybrid/overlay smd/ulsph *DENSITY_CONTINUITY *VELOCITY_GRADIENT *NO_GRADIENT_CORRECTION &
                                       smd/tlsph smd/hertz 1.5
    
        # [2:elastic x 2:elastic] Diagonal pair coefficient tlsph
        pair_coeff      2 2 smd/tlsph *COMMON 1000 500000.0 0.3 1.0 0.0 10 1.0 &
                        *STRENGTH_LINEAR_PLASTIC 50000.0 0 &
                        *EOS_LINEAR &
                        *END
    
        # [2:elastic x 1:none] Off-diagonal pair coefficient (generic)
        pair_coeff      2 1 smd/hertz 250000.0
    
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    

    run this code by pressing

Expand source code
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Synopsis of forcefield Class
============================

The `forcefield` class defines the core behavior for managing inter-particle interactions
in molecular dynamics simulations or similar physics-based models. It provides methods
to calculate interaction parameters, including pair styles, diagonal pair coefficients,
and off-diagonal pair coefficients, which are crucial for forcefield models.

Key Attributes:
---------------
- `PAIR_STYLE` (str): Defines the pair style command for the forcefield interactions.
- `PAIR_DIAGCOEFF` (str): Defines the command for calculating diagonal pair coefficients.
- `PAIR_OFFDIAGCOEFF` (str): Defines the command for calculating off-diagonal pair coefficients.
- `parameters` (parameterforcefield): Stores the parameters used for interaction evaluations.
- `beadtype` (int): The bead type used in the forcefield model.
- `userid` (str): A unique identifier for the forcefield instance.

Key Methods:
------------
- `pair_style(printflag=True)`: Returns the pair style command based on the current
  `parameters`, `beadtype`, and `userid`.
- `pair_diagcoeff(printflag=True, i=None)`: Returns the diagonal pair coefficient command
  based on the current bead type `i` and `userid`. The bead type can be overridden.
- `pair_offdiagcoeff(o=None, printflag=True, i=None)`: Returns the off-diagonal pair
  coefficient command for interactions between two bead types or forcefield objects.
  The bead type `i` and the interacting forcefield `o` can be specified or overridden.



    --- forcefield methods for LAMMPS ---

    The superclass provides a collection of classes to define materials
    and forcefields. Note that the following hierarchy is used:
        > forcefield() is the top class (to be called directly)
        > customff(forcefield) defines a new forcefield so called customff
        > customstyle(customff) defines a pair-style applicable to customff
        > custommaterial(customstyle) defines a new material


    --- Material library (first implementations) ---

        w = water(beadtype=1, userid="fluid")
        w.parameters.Cp = 20
        print("\n"*2,w)

        f = solidfood(beadtype=2, userid="elastic")
        print("\n"*2,f)

        r = rigidwall(beadtype=3, userid="wall")
        print("\n"*2,r)


    --- Template to define a material ---

          class mymaterial(myforcefield):
              userid = "short name"
              version = value
              def __init__(self, beadtype=1, userid=None):
                  super().__init__()
                  if userid!=None: self.userid = userid
                  self.name.material"] = "short of the material"
                  self.description.material"] = "full description"
                  self.beadtype = beadtype
                  self.parameters = parameterforcefield(
                      param1 = value1,
                      param2 = value2,
                      param3 = "math expression with ${param1, ${param2}"
                  )


    --- Example of outputs | LAMMPS:SMD:tlsph:solidfood ---

        ========================== [ elastic | version=0.1 ] ===========================

          Bead of type 2 = [LAMMPS:SMD:tlsph:solidfood]
          -----------------:----------------------------------------
                        rho: 1000
                         c0: 10.0
                          E: 5*${c0}^2*${rho}
                         nu: 0.3
                         q1: 1.0
                         q2: 0.0
                         Hg: 10
                         Cp: 1.0
                sigma_yield: 0.1*${E}
                  hardening: 0
              contact_scale: 1.5
          contact_stiffness: 2.5*${c0}^2*${rho}
          -----------------:----------------------------------------
        forcefield object with 12 parameters

        ............................... [ description ] ................................

                #       LAMMPS:SMD - solid, liquid, rigid forcefields (continuum mechanics)
                #       SMD:TLSPH - total Lagrangian for solids
                #       food beads - solid behavior

        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [ methods ] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

        replace FFi,FFj by your variable names <<<
                To assign a type, use: FFi.beadtype = integer value
                Use the methods FFi.pair_style() and FFi.pair_coeff(FFj)
                Note for pairs: the caller object is i (FFi), the argument is j (FFj or j)

        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [ template ] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


            # [2:elastic] PAIR STYLE SMD
            pair_style      hybrid/overlay smd/ulsph *DENSITY_CONTINUITY *VELOCITY_GRADIENT *NO_GRADIENT_CORRECTION &
                                           smd/tlsph smd/hertz 1.5


            # [2:elastic x 2:elastic] Diagonal pair coefficient tlsph
            pair_coeff      2 2 smd/tlsph *COMMON 1000 500000.0 0.3 1.0 0.0 10 1.0 &
                            *STRENGTH_LINEAR_PLASTIC 50000.0 0 &
                            *EOS_LINEAR &
                            *END


            # [2:elastic x 1:none] Off-diagonal pair coefficient (generic)
            pair_coeff      2 1 smd/hertz 250000.0


        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


    run this code by pressing <F5>

"""

__project__ = "Pizza3"
__author__ = "Olivier Vitrac"
__copyright__ = "Copyright 2022"
__credits__ = ["Olivier Vitrac"]
__license__ = "GPLv3"
__maintainer__ = "Olivier Vitrac"
__email__ = "olivier.vitrac@agroparistech.fr"
__version__ = "1.006"


# INRAE\Olivier Vitrac - rev. 2025-02-18
# contact: olivier.vitrac@agroparistech.fr

# History
# 2022-02-12 early version
# 2022-02-13 release candidate
# 2022-02-20 made compatible with the update private.mstruct.py
# 2022-02-28 fix class inheritance with mutable type, update is carried with + and struct()
# 2022-03-02 fix off-diagonal order for i,j
# 2022-03-19 standardized pizza path
# 2022-04-16 add saltTLSPH() forcefield in the material library, and document it better
# 2022-05-16 force sortdefintions for + and += with parameterforcefield()
# 2022-05-17 direct use of pizza.private.mstruct.paramauto()
# 2023-07-25 fix forcefield (deepduplicate instead of duplicate)
# 2024-09-10 updated documentation for pizza.forcefield (to be read along with pizza.dforcefield)
# 2024-09-12 upgrading of parameterforcefield
# 2024-09-21 dforcefield and forcefield can be combined indifferently (no precedence)
# 2024-10-10 fix the dynamic parameterization with dforcefield (more overrides)
# 2025-02-18 add tlsphalone, ulsphalone (for special cases not requiring overlay)

# %% Dependencies
import types
# All forcefield parameters are stored à la Matlab in a structure
from pizza.private.mstruct import struct,paramauto

__all__ = ['forcefield', 'none', 'paramauto', 'parameterforcefield', 'rigidwall', 'saltTLSPH', 'smd', 'solidfood', 'struct', 'tlsph', 'tlsphalone', 'ulsph', 'ulsphalone', 'water']


# %% Parent class (not to be called directly)
# Note that some attributes are stored in the instances but in the class itself
#   recommendation 1: Recreate/derive a class when possible
#   recommendation 2: __dict__ list only properties in the instance,
#                     use getallattributes() to see all attributes

# container of forcefield parameters
class parameterforcefield(paramauto):
    """ class of forcefields parameters, derived from param
        note that conctanating two forcefields force them
        to to be sorted
    """
    _type = "FF"
    _fulltype = "forcefield"
    _ftype = "parameter"
    _maxdisplay = 80

    # same strategy as used in dscript for forcing  _returnerror = False (added 2024-09-12)
    def __init__(self, _protection=False, _evaluation=True, sortdefinitions=False, **kwargs):
        """
        Constructor for parameterforcefield. It forces the parent's _returnerror parameter to False.

        Parameters:
        -----------
        _protection : bool, optional
            Whether to enable protection on the parameters (default: False).
        _evaluation : bool, optional
            Whether evaluation is enabled for the parameters (default: True).
        sortdefinitions : bool, optional
            Whether to sort definitions upon initialization (default: False).
        **kwargs : dict
            Additional keyword arguments for the parent class.
        """
        # Call the parent class constructor
        super().__init__(_protection=_protection, _evaluation=_evaluation, sortdefinitions=sortdefinitions, **kwargs)
        # Override the _returnerror attribute at the instance level
        self._returnerror = False


# core class
class forcefield():
    """
    The `forcefield` class represents the core implementation of a forcefield model,
    defining interaction parameters and coefficients for simulations. This class provides
    methods to handle pair styles, diagonal pair coefficients, and off-diagonal pair coefficients,
    which are essential for simulating inter-particle interactions in molecular dynamics or
    other physics-based simulations.

    Attributes:
    -----------
    PAIR_STYLE : str
        The default pair style command for the forcefield interactions.

    PAIR_DIAGCOEFF : str
        The default command for calculating diagonal pair coefficients.

    PAIR_OFFDIAGCOEFF : str
        The default command for calculating off-diagonal pair coefficients.

    parameters : parameterforcefield
        An instance of `parameterforcefield` that stores the parameters for
        evaluating interaction commands.

    beadtype : int
        The bead type associated with the current forcefield instance.

    userid : str
        A unique identifier for the forcefield instance, used in interaction commands.

    Methods:
    --------
    pair_style(printflag=True):
        Generate and return the pair style command based on the current parameters,
        beadtype, and userid.

    pair_diagcoeff(printflag=True, i=None):
        Generate and return the diagonal pair coefficients based on the current parameters,
        beadtype, and userid. The bead type `i` can be overridden with an optional argument.

    pair_offdiagcoeff(o=None, printflag=True, i=None):
        Generate and return the off-diagonal pair coefficients between two different
        bead types or forcefield objects. The bead type `i` can be overridden, and the
        interaction with another forcefield object `o` can also be specified.

    Notes:
    ------
    - This class is intended to be extended by specific forcefield types such as `ulsph`.
    - The parameters used in the interaction commands are dynamically evaluated using
      the `parameterforcefield` class, which provides the required values during runtime.
    """

    # Main attributes (instance independent)
    name = struct(forcefield="undefined", style="undefined", material="undefined")
    description = struct(forcefield="missing", style="missing", material="missing")
    beadtype = 1  # default bead type
    parameters = parameterforcefield() # empty parameters object
    userid = "undefined"
    version = 0

    # print method for headers (static, no implicit argument)
    @staticmethod
    def printheader(txt,align="^",width=80,filler="~"):
        """ print header """
        if txt=="":
            print("\n"+filler*(width+6)+"\n")
        else:
            print(("\n{:"+filler+"{align}{width}}\n").format(' [ '+txt+' ] ', align=align, width=str(width)))

    # Display/representation method
    # The method provides full help for the end-user
    def __repr__(self):
        """ disp method """
        stamp = self.name.forcefield+":"+self.name.style+":"+self.name.material
        self.printheader("%s | version=%0.3g" % (self.userid,self.version),filler="=")
        print("  Bead of type %d = [%s]" % (self.beadtype,stamp))
        print(self.parameters)
        self.printheader("description",filler=".")
        print("\t# \t%s" % self.description.forcefield)
        print("\t# \t%s" % self.description.style)
        print("\t# \t%s" % self.description.material)
        self.printheader("methods")
        print("\t   >>> replace FFi,FFj by your variable names <<<")
        print("\tTo assign a type, use: FFi.beadtype = integer value")
        print("\tUse the methods FFi.pair_style() and FFi.pair_coeff(FFj)")
        print("\tNote for pairs: the caller object is i (FFi), the argument is j (FFj or j)")
        self.printheader("template")
        self.pair_style()
        self.pair_diagcoeff()
        self.pair_offdiagcoeff()
        self.printheader("")
        return stamp

    # Extract attributes within the class
    def getallattributes(self):
        """ advanced method to get all attributes including class ones"""
        return {k: getattr(self, k) for k in dir(self) \
                if (not k.startswith('_')) and (not isinstance(getattr(self, k),types.MethodType))}


    # Forcefield Methods: pair_style(), pair_coeff()
    # the substitution of LAMMPS variables is carried out with the method
    # parameters.format() method implemented in struct and inherited by parameterforcefield()
    def pair_style(self,printflag=False,verbose=True, raw=False,USER=None,beadtype=None,userid=None):
        """
        Generate and return the pair style command for the current forcefield instance.

        This method creates a formatted pair style command based on the interaction parameters
        stored in the `parameters` attribute. It allows customization of the command using the
        `beadtype` and `userid` arguments. The behavior can be altered by passing a `USER` object
        or opting for the raw command template.

        Parameters:
        -----------
        printflag : bool, optional, default=False
            If True, the generated pair style command is printed to the console.
        verbose : bool, optional, default=True
            If True, enables verbose output during the script generation.
        raw : bool, optional, default=False
            If True, returns the raw template of the pair style without any interpretation.
        USER : struct, optional, default=None
            A user-defined struct object used for overriding the default parameters.
            When provided, the method updates parameters using `USER` in conjunction with
            the instance's base parameters.
        beadtype : int, optional, default=None
            The bead type identifier used in the generated command. If not provided, the
            instance's beadtype is used.
        userid : str, optional, default=None
            The user identifier to include in the formatted command. Defaults to the instance's
            userid if not specified.

        Returns:
        --------
        str
            The formatted pair style command string.

        Raises:
        -------
        TypeError
            If `USER` is provided but is not of type `struct` or derived from `struct`.
        """
        # raw format
        if raw:
            return self.PAIR_STYLE
        # USER overrride if the forcefield class is inherited
        if USER is None: # ---- default behavior for forcefield
            parameters = self.parameters
            beadtype = self.beadtype
            userid = self.userid
        elif isinstance(USER,struct): # ---- behavior for dforcefield (using baseclass)
            parameters = self.parameters+USER
            beadtype = beadtype if beadtype is not None else USER.beadtype if hasattr(USER, 'beadtype') else self.beadtype
            userid = userid if userid is not None else USER.userid if hasattr(USER, 'userid') else self.userid
        else:
            raise TypeError(f'USER must be of type struct or derived from struct, not {type(USER)}')
        # cmd
        cmd = parameters.formateval(self.PAIR_STYLE)
        # Replace [comment] with the formatted comment (e.g., "[2:my_user_id]")
        cmd = cmd.replace("[comment]","[%d:%s]" % (beadtype, userid) if verbose else "")
        if printflag: print(cmd)
        return cmd


    def pair_diagcoeff(self,printflag=False,verbose=True, i=None,raw=False,USER=None,beadtype=None,userid=None):
        """
        Generate and return the diagonal pair coefficients for the current forcefield instance.

        This method evaluates the diagonal pair coefficients based on the interaction parameters,
        the bead type (`beadtype`), and the user identifier (`userid`). The bead type `i` can
        be overridden by passing it as an argument. The method supports returning the raw template
        without evaluation and modifying parameters using a `USER` object.

        Parameters:
        -----------
        printflag : bool, optional, default=False
            If True, the generated diagonal pair coefficient command is printed to the console.
        verbose : bool, optional, default=True
            If True, enables verbose output during the script generation.
        i : int, optional, default=None
            The bead type used for evaluating the diagonal pair coefficients. If not provided,
            defaults to the instance's bead type (`self.beadtype`).
        raw : bool, optional, default=False
            If True, returns the raw template for the diagonal pair coefficients without interpretation.
        USER : struct, optional, default=None
            A user-defined struct object used for overriding the default parameters.
            When provided, the method updates parameters using `USER` in conjunction with
            the instance's base parameters.
        beadtype : int, optional, default=None
            The bead type identifier to use in the command. Defaults to the instance's beadtype
            if not provided.
        userid : str, optional, default=None
            The user identifier to include in the formatted command. Defaults to the instance's
            userid if not specified.

        Returns:
        --------
        str
            The formatted diagonal pair coefficient command string.

        Raises:
        -------
        TypeError
            If `USER` is provided but is not of type `struct` or derived from `struct`.
        """
        # raw format
        if raw:
            return self.PAIR_DIAGCOEFF
        # USER overrride if the forcefield class is inherited
        if USER is None: # ---- default behavior for forcefield
            parameters = self.parameters
            beadtype = self.beadtype
            userid = self.userid
        elif isinstance(USER,struct): # ---- behavior for dforcefield (using baseclass)
            parameters = self.parameters+USER
            beadtype = beadtype if beadtype is not None else USER.beadtype if hasattr(USER, 'beadtype') else self.beadtype
            userid = userid if userid is not None else USER.userid if hasattr(USER, 'userid') else self.userid
        else:
            raise TypeError(f'USER must be of type struct or derived from struct, not {type(USER)}')
        # diagonal index
        i = i if i is not None else beadtype
        # cmd
        cmd = parameters.formateval(self.PAIR_DIAGCOEFF) % (i,i)
        # Replace [comment] with the formatted string, without using .format()
        cmd = cmd.replace("[comment]", "[%d:%s x %d:%s]" % (i, userid, i, userid) if verbose else "")
        if printflag: print(cmd)
        return cmd


    def pair_offdiagcoeff(self,o=None,printflag=False,verbose=True,i=None,raw=False,USER=None,beadtype=None,userid=None,oname=None):
        """
        Generate and return the off-diagonal pair coefficients for the current forcefield instance.

        This method evaluates the off-diagonal pair coefficients between two different bead types
        or forcefield objects, using the interaction parameters, bead type, and user identifier.
        The bead type `i` can be overridden, and the interaction with another forcefield object `o`
        can also be specified.

        Parameters:
        -----------
        o : forcefield or int, optional, default=None
            The second forcefield object or bead type used for calculating the off-diagonal
            pair coefficients. If not provided, the method assumes interactions between
            beads of the same type.
        printflag : bool, optional, default=False
            If True, the generated off-diagonal pair coefficient command is printed to the console.
        verbose : bool, optional, default=True
            If True, enables verbose output during the script generation.
        i : int, optional, default=None
            The bead type used for the current forcefield instance. If not provided,
            defaults to the instance's bead type (`self.beadtype`).
        raw : bool, optional, default=False
            If True, returns the raw template for the off-diagonal pair coefficients without interpretation.
        USER : struct, optional, default=None
            A user-defined struct object used for overriding the default parameters.
            When provided, the method updates parameters using `USER` in conjunction with
            the instance's base parameters.
        beadtype : int, optional, default=None
            The bead type identifier used in the command. Defaults to the instance's beadtype
            if not provided.
        userid : str, optional, default=None
            The user identifier included in the formatted command. Defaults to the instance's
            userid if not specified.
        oname : str, optional, default=None
            The user identifier for the second forcefield or bead type. If not provided, it
            defaults to `"none"`.

        Returns:
        --------
        str
            The formatted off-diagonal pair coefficient command string.

        Raises:
        -------
        TypeError
            If `USER` is not of type `struct` or derived from `struct`.
        IndexError
            If the first argument `o` is not a forcefield object or an integer.
        """

        # raw format
        if raw:
            return self.PAIR_OFFDIAGCOEFF
        # USER overrride if the forcefield class is inherited
        if USER is None: # ---- default behavior for forcefield
            parameters = self.parameters
            beadtype = self.beadtype
            userid = self.userid
            i = i if i is not None else beadtype
        elif isinstance(USER,struct): # ---- behavior for dforcefield (using baseclass)
            parameters = self.parameters+USER
            beadtype = beadtype if beadtype is not None else USER.beadtype if hasattr(USER, 'beadtype') else self.beadtype
            userid = userid if userid is not None else USER.userid if hasattr(USER, 'userid') else self.userid
        else:
            raise TypeError(f'USER must be of type struct or derived from struct, not {type(USER)}')
        # Determine the first bead type (i)
        i = i if i is not None else beadtype
        # Determine the second bead type (j) based on o
        if o is None:
            j = i
        elif hasattr(o, 'beadtype'):
            j = o.beadtype
        elif isinstance(o, (float, int)):
            j = int(o)
        else:
            raise IndexError("The first argument should be a forcefield object or an integer representing bead type.")
        # Adjust j if it matches i (to ensure off-diagonal interaction)
        if j == i:
            j = i - 1 if i > 1 else i + 1
        oname = oname if oname is not None else o.userid if hasattr(o, "userid") else "none"
        # cmd
        cmd = parameters.formateval(self.PAIR_OFFDIAGCOEFF) % (min(i,j),max(j,i))
        # Replace [comment] with the formatted string, without using .format()
        cmd = cmd.replace("[comment]", "[%d:%s x %d:%s]" % (i, self.userid, j, oname) if verbose else "")
        if printflag: print(cmd)
        return cmd


# %% Forecefield library
# This section can be upgraded by the end-user according to the manual of each style
# numerical value shoud be declared with variable/parameter with the same syntax as in LAMMPS.
# Note that you can copy directly LAMMPS code between triple """ for templates
# Main templates are strings: PAIR_STYLE, PAIR_DIAGCOEFF, PAIR_OFFDIAGCOEFF
#
# ${param} represents the variable called "param", and whose value will be defined
# in the parameters section of the material class as parameters.param = value
#
# use {comment} to add an automatic comment
# %d in PAIR_COEFF represent place holder for corresponding beadtype
#
# The subsitutions are managed by the parent class forcefield().

# BEGIN PAIR-STYLE FORCEFIELD ===========================
class smd(forcefield):
    """ SMD forcefield """
    name = forcefield.name + struct(forcefield="LAMMPS:SMD")
    description = forcefield.description + struct(forcefield="LAMMPS:SMD - solid, liquid, rigid forcefields (continuum mechanics)")

    # forcefield definition (LAMMPS code between triple """)
    PAIR_STYLE = """
    # [comment] PAIR STYLE SMD
    pair_style      hybrid/overlay smd/ulsph *DENSITY_CONTINUITY *VELOCITY_GRADIENT *NO_GRADIENT_CORRECTION &
                                   smd/tlsph smd/hertz ${contact_scale}
    """
# END PAIR-STYLE FORCEFIELD ===========================


# BEGIN PAIR-COEFF FORCEFIELD ===========================
class ulsph(smd):
    """ SMD:ULSPH forcefield (updated Lagrangian) """
    name = smd.name + struct(style="ulsph")
    description = smd.description + struct(style="SMD:ULSPH - updated Lagrangian for fluids - SPH-like")

    # style definition (LAMMPS code between triple """)
    PAIR_DIAGCOEFF = """
    # [comment] Pair diagonal coefficient ulsph
    pair_coeff      %d %d smd/ulsph *COMMON ${rho} ${c0} ${q1} ${Cp} 0 &
                    *EOS_TAIT ${taitexponent} &
                    *END
    """
    PAIR_OFFDIAGCOEFF = """
    # [comment] Off-diagonal pair coefficient (generic)
    pair_coeff      %d %d smd/hertz ${contact_stiffness}
    """
# END PAIR-COEFF FORCEFIELD ===========================


# BEGIN PAIR-COEFF FORCEFIELD ===========================
class tlsph(smd):
    """ SMD:TLSPH forcefield (total Lagrangian) """
    name = smd.name + struct(style="tlsph")
    description = smd.description + struct(style="SMD:TLSPH - total Lagrangian for solids")

    # style definition (LAMMPS code between triple """)
    PAIR_DIAGCOEFF = """
    # [comment] Diagonal pair coefficient tlsph
    pair_coeff      %d %d smd/tlsph *COMMON ${rho} ${E} ${nu} ${q1} ${q2} ${Hg} ${Cp} &
                    *STRENGTH_LINEAR_PLASTIC ${sigma_yield} ${hardening} &
                    *EOS_LINEAR &
                    *END
    """
    PAIR_OFFDIAGCOEFF = """
    # [comment] Off-diagonal pair coefficient (generic)
    pair_coeff      %d %d smd/hertz ${contact_stiffness}
    """
# END PAIR-COEFF FORCEFIELD ===========================


# BEGIN PAIR-COEFF FORCEFIELD ===========================
class ulsphalone(smd):
    """ SMD:ULSPH forcefield (updated Lagrangian) """
    name = smd.name + struct(style="ulsph")
    description = smd.description + struct(style="SMD:ULSPH - updated Lagrangian for fluids - SPH-like")

    # forcefield definition (LAMMPS code between triple """)
    PAIR_STYLE = """
    # [comment] PAIR STYLE SMD
    pair_style      smd/ulsph *DENSITY_CONTINUITY *VELOCITY_GRADIENT *NO_GRADIENT_CORRECTION
    """

    # style definition (LAMMPS code between triple """)
    PAIR_DIAGCOEFF = """
    # [comment] Pair diagonal coefficient ulsph
    pair_coeff      %d %d smd/ulsph *COMMON ${rho} ${c0} ${q1} ${Cp} 0 &
                    *EOS_TAIT ${taitexponent} &
                    *END
    """
    PAIR_OFFDIAGCOEFF = """
    # [comment] Off-diagonal pair coefficient (generic)
    pair_coeff      %d %d smd/hertz ${contact_stiffness}
    """
# END PAIR-COEFF FORCEFIELD ===========================


# BEGIN PAIR-COEFF FORCEFIELD ===========================
class tlsphalone(forcefield):
    """ SMD:TLSPH forcefield (total Lagrangian) """
    name = smd.name + struct(style="tlsph")
    description = smd.description + struct(style="SMD:TLSPH - total Lagrangian for solids")

    # forcefield definition (LAMMPS code between triple """)
    PAIR_STYLE = """
    # [comment] PAIR STYLE SMD
    pair_style      hybrid/overlay smd/tlsph smd/hertz ${contact_scale}
    """

    # style definition (LAMMPS code between triple """)
    PAIR_DIAGCOEFF = """
    # [comment] Diagonal pair coefficient tlsph
    pair_coeff      %d %d smd/tlsph *COMMON ${rho} ${E} ${nu} ${q1} ${q2} ${Hg} ${Cp} &
                    *STRENGTH_LINEAR_PLASTIC ${sigma_yield} ${hardening} &
                    *EOS_LINEAR &
                    *END
    """
    PAIR_OFFDIAGCOEFF = """
    # [comment] Off-diagonal pair coefficient (generic)
    pair_coeff      %d %d smd/hertz ${contact_stiffness}
    """
# END PAIR-COEFF FORCEFIELD ===========================


# BEGIN PAIR-COEFF FORCEFIELD ===========================
class none(smd):
    """ SMD:TLSPH forcefield (updated Lagrangian) """
    name = smd.name + struct(style="none")
    description = smd.description + struct(style="no interactions")

    # style definition (LAMMPS code between triple """)
    PAIR_DIAGCOEFF = """
    # [comment] Diagonal pair coefficient tlsph
    pair_coeff      %d %d none
    """
    PAIR_OFFDIAGCOEFF = """
    # [comment] Off-diagonal pair coefficient (generic)
    pair_coeff      %d %d smd/hertz ${contact_stiffness}
    """
# END PAIR-COEFF FORCEFIELD ===========================



# %% Material library
# template:
#   class mymaterial(myforcefield):
#       userid = "short name"
#       version = value
#       def __init__(self, beadtype=1, userid=None):
#           super().__init__()
#           if userid!=None: self.userid = userid
#           self.name.material"] = "short of the material"
#           self.description.material"] = "full description"
#           self.beadtype = beadtype
#           self.parameters = parameterforcefield(
#               param1 = value1,
#               param2 = value2,
#               param3 = "math expression with ${param1, ${param2}"
#           )
#
# ATTENTION: ${param1} and ${param2} cannot be used in an expression
#            if they do not have been prealably defined

# BEGIN MATERIAL: WATER ========================================
class water(ulsph):
    """ water material (smd:ulsph): generic water model
            water()
            water(beadtype=index, userid="myfluid", USER=...)

            override any propery with USER.parameter (set only the parameters you want to override)
                USER.rho: density in kg/m3 (default=1000)
                USER.c0: speed of the sound in m/s (default=10.0)
                USER.q1: standard artificial viscosity linear coefficient (default=1.0)
                USER.Cp: heat capacity of material -- not used here (default=1.0)
                USER.contact_scale: scaling coefficient for contact (default=1.5)
                USER.contact_stiffness: contact stifness in Pa (default="2.5*${c0}^2*${rho}")
    """
    name = ulsph.name + struct(material="water")
    description = ulsph.description + struct(material="water beads - SPH-like")
    userid = 'water'
    version = 0.1

    # constructor (do not forgert to include the constuctor)
    def __init__(self, beadtype=1, userid=None, USER=parameterforcefield()):
        """ water forcefield:
            water(beadtype=index, userid="myfluid") """
        if userid!=None: self.userid = userid
        self.beadtype = beadtype
        self.parameters = parameterforcefield(
            # water-water interactions
            rho = 1000,
            c0 = 10.0,
            q1 = 1.0,
            Cp = 1.0,
            taitexponent = 7,
            # hertz contacts
            contact_scale = 1.5,
            contact_stiffness = '2.5*${c0}^2*${rho}'
            ) + USER # update with user properties if any

# END MATERIAL: WATER ==========================================


# BEGIN MATERIAL: SOLID FOOD ========================================
class solidfood(tlsph):
    """ solidfood material (smd:tlsph): model solid food object
            solidfood()
            solidfood(beadtype=index, userid="myfood", USER=...)

            override any propery with USER.property=value (set only the parameters you want to override)
                USER.rho: density in kg/m3 (default=1000)
                USER.c0: speed of the sound in m/s (default=10.0)
                USER.E: Young's modulus in Pa (default="5*${c0}^2*${rho}")
                USER.nu: Poisson ratio (default=0.3)
                USER.q1: standard artificial viscosity linear coefficient (default=1.0)
                USER.q2: standard artificial viscosity quadratic coefficient (default=0)
                USER.Hg: hourglass control coefficient (default=10.0)
                USER.Cp: heat capacity of material -- not used here (default=1.0)
                USER.sigma_yield: plastic yield stress in Pa (default="0.1*${E}")
                USER.hardening: hardening parameter (default=0)
                USER.contact_scale: scaling coefficient for contact (default=1.5)
                USER.contact_stiffness: contact stifness in Pa (default="2.5*${c0}^2*${rho}")
    """
    name = tlsph.name + struct(material="solidfood")
    description = tlsph.description + struct(material="food beads - solid behavior")
    userid = 'solidfood'
    version = 0.1

    # constructor (do not forgert to include the constuctor)
    def __init__(self, beadtype=1, userid=None, USER=parameterforcefield()):
        """ food forcefield:
            solidfood(beadtype=index, userid="myfood") """
        # super().__init__()
        if userid!=None: self.userid = userid
        self.beadtype = beadtype
        self.parameters = parameterforcefield(
            # food-food interactions
            rho = 1000,
            c0 = 10.0,
            E = "5*${c0}^2*${rho}",
            nu = 0.3,
            q1 = 1.0,
            q2 = 0.0,
            Hg = 10.0,
            Cp = 1.0,
            sigma_yield = "0.1*${E}",
            hardening = 0,
            # hertz contacts
            contact_scale = 1.5,
            contact_stiffness = "2.5*${c0}^2*${rho}"
            ) + USER # update with user properties if any

# END MATERIAL: SOLID FOOD ==========================================


# BEGIN MATERIAL: SALT TLSPH ========================================
class saltTLSPH(tlsph):
    """ SALTLSPH (smd:tlsph): ongoing "salting" beadtype for rheology control
            saltTLSPH()
            saltTLSPH(beadtype=index, userid="salt", USER=...)

            override any property with USER.property = value
    """
    name = tlsph.name + struct(material="solidfood")
    description = tlsph.description + struct(material="food beads - solid behavior")
    userid = '"salt"'
    version = 0.1

    # constructor (do not forgert to include the constuctor)
    def __init__(self, beadtype=1, userid=None, USER=parameterforcefield()):
        """ saltTLSPH forcefield:
            saltTLSPH(beadtype=index, userid="salt") """
        # super().__init__()
        if userid!=None: self.userid = userid
        self.beadtype = beadtype
        self.parameters = parameterforcefield(
            # food-food interactions
            rho = 1000,
            c0 = 10.0,
            E = '5*${c0}^2*${rho}',
            nu = 0.3,
            q1 = 1.0,
            q2 = 0.0,
            Hg = 10,
            Cp = 1.0,
            sigma_yield = '0.1*${E}',
            hardening = 0,
            # hertz contacts
            contact_scale = 1.5,
            contact_stiffness = '2.5*${c0}^2*${rho}'
            ) + USER # update with user properties if any

# END MATERIAL: SOLID FOOD ==========================================



# BEGIN MATERIAL: RIGID WALLS ========================================
class rigidwall(none):
    """ rigid walls (smd:none):
            rigidwall()
            rigidwall(beadtype=index, userid="wall", USER=...)

            override any propery with USER.parameter (set only the parameters you want to override)
                USER.rho: density in kg/m3 (default=3000)
                USER.c0: speed of the sound in m/s (default=10.0)
                USER.contact_scale: scaling coefficient for contact (default=1.5)
                USER.contact_stiffness: contact stifness in Pa (default="2.5*${c0}^2*${rho}")
    """
    name = none.name + struct(material="walls")
    description = none.description + struct(material="rigid walls")
    userid = 'solidfood'
    version = 0.1

    # constructor (do not forgert to include the constuctor)
    def __init__(self, beadtype=1, userid=None, USER=parameterforcefield()):
        """ rigidwall forcefield:
            rigidwall(beadtype=index, userid="mywall") """
        # super().__init__()
        if userid!=None: self.userid = userid
        self.beadtype = beadtype
        self.parameters = parameterforcefield(
            rho = 3000,
            c0 = 10.0,
            contact_stiffness = '2.5*${c0}^2*${rho}',
            contact_scale = 1.5
            ) + USER # update with user properties if any


# END MATERIAL: RIGID WALLS ==========================================


# %% DEBUG
# ===================================================
# main()
# ===================================================
# for debugging purposes (code called as a script)
# the code is called from here
# ===================================================
if __name__ == '__main__':
    w = water(beadtype=1, userid="fluid")
    w.parameters.Cp = 20
    print("\n"*2,w)
    f = solidfood(beadtype=2, userid="elastic")
    print("\n"*2,f)
    r = rigidwall(beadtype=3, userid="wall")
    print("\n"*2,r)

Classes

class forcefield

The forcefield class represents the core implementation of a forcefield model, defining interaction parameters and coefficients for simulations. This class provides methods to handle pair styles, diagonal pair coefficients, and off-diagonal pair coefficients, which are essential for simulating inter-particle interactions in molecular dynamics or other physics-based simulations.

Attributes:

PAIR_STYLE : str The default pair style command for the forcefield interactions.

PAIR_DIAGCOEFF : str The default command for calculating diagonal pair coefficients.

PAIR_OFFDIAGCOEFF : str The default command for calculating off-diagonal pair coefficients.

parameters : parameterforcefield An instance of parameterforcefield that stores the parameters for evaluating interaction commands.

beadtype : int The bead type associated with the current forcefield instance.

userid : str A unique identifier for the forcefield instance, used in interaction commands.

Methods:

pair_style(printflag=True): Generate and return the pair style command based on the current parameters, beadtype, and userid.

pair_diagcoeff(printflag=True, i=None): Generate and return the diagonal pair coefficients based on the current parameters, beadtype, and userid. The bead type i can be overridden with an optional argument.

pair_offdiagcoeff(o=None, printflag=True, i=None): Generate and return the off-diagonal pair coefficients between two different bead types or forcefield objects. The bead type i can be overridden, and the interaction with another forcefield object o can also be specified.

Notes:

  • This class is intended to be extended by specific forcefield types such as ulsph.
  • The parameters used in the interaction commands are dynamically evaluated using the parameterforcefield class, which provides the required values during runtime.
Expand source code
class forcefield():
    """
    The `forcefield` class represents the core implementation of a forcefield model,
    defining interaction parameters and coefficients for simulations. This class provides
    methods to handle pair styles, diagonal pair coefficients, and off-diagonal pair coefficients,
    which are essential for simulating inter-particle interactions in molecular dynamics or
    other physics-based simulations.

    Attributes:
    -----------
    PAIR_STYLE : str
        The default pair style command for the forcefield interactions.

    PAIR_DIAGCOEFF : str
        The default command for calculating diagonal pair coefficients.

    PAIR_OFFDIAGCOEFF : str
        The default command for calculating off-diagonal pair coefficients.

    parameters : parameterforcefield
        An instance of `parameterforcefield` that stores the parameters for
        evaluating interaction commands.

    beadtype : int
        The bead type associated with the current forcefield instance.

    userid : str
        A unique identifier for the forcefield instance, used in interaction commands.

    Methods:
    --------
    pair_style(printflag=True):
        Generate and return the pair style command based on the current parameters,
        beadtype, and userid.

    pair_diagcoeff(printflag=True, i=None):
        Generate and return the diagonal pair coefficients based on the current parameters,
        beadtype, and userid. The bead type `i` can be overridden with an optional argument.

    pair_offdiagcoeff(o=None, printflag=True, i=None):
        Generate and return the off-diagonal pair coefficients between two different
        bead types or forcefield objects. The bead type `i` can be overridden, and the
        interaction with another forcefield object `o` can also be specified.

    Notes:
    ------
    - This class is intended to be extended by specific forcefield types such as `ulsph`.
    - The parameters used in the interaction commands are dynamically evaluated using
      the `parameterforcefield` class, which provides the required values during runtime.
    """

    # Main attributes (instance independent)
    name = struct(forcefield="undefined", style="undefined", material="undefined")
    description = struct(forcefield="missing", style="missing", material="missing")
    beadtype = 1  # default bead type
    parameters = parameterforcefield() # empty parameters object
    userid = "undefined"
    version = 0

    # print method for headers (static, no implicit argument)
    @staticmethod
    def printheader(txt,align="^",width=80,filler="~"):
        """ print header """
        if txt=="":
            print("\n"+filler*(width+6)+"\n")
        else:
            print(("\n{:"+filler+"{align}{width}}\n").format(' [ '+txt+' ] ', align=align, width=str(width)))

    # Display/representation method
    # The method provides full help for the end-user
    def __repr__(self):
        """ disp method """
        stamp = self.name.forcefield+":"+self.name.style+":"+self.name.material
        self.printheader("%s | version=%0.3g" % (self.userid,self.version),filler="=")
        print("  Bead of type %d = [%s]" % (self.beadtype,stamp))
        print(self.parameters)
        self.printheader("description",filler=".")
        print("\t# \t%s" % self.description.forcefield)
        print("\t# \t%s" % self.description.style)
        print("\t# \t%s" % self.description.material)
        self.printheader("methods")
        print("\t   >>> replace FFi,FFj by your variable names <<<")
        print("\tTo assign a type, use: FFi.beadtype = integer value")
        print("\tUse the methods FFi.pair_style() and FFi.pair_coeff(FFj)")
        print("\tNote for pairs: the caller object is i (FFi), the argument is j (FFj or j)")
        self.printheader("template")
        self.pair_style()
        self.pair_diagcoeff()
        self.pair_offdiagcoeff()
        self.printheader("")
        return stamp

    # Extract attributes within the class
    def getallattributes(self):
        """ advanced method to get all attributes including class ones"""
        return {k: getattr(self, k) for k in dir(self) \
                if (not k.startswith('_')) and (not isinstance(getattr(self, k),types.MethodType))}


    # Forcefield Methods: pair_style(), pair_coeff()
    # the substitution of LAMMPS variables is carried out with the method
    # parameters.format() method implemented in struct and inherited by parameterforcefield()
    def pair_style(self,printflag=False,verbose=True, raw=False,USER=None,beadtype=None,userid=None):
        """
        Generate and return the pair style command for the current forcefield instance.

        This method creates a formatted pair style command based on the interaction parameters
        stored in the `parameters` attribute. It allows customization of the command using the
        `beadtype` and `userid` arguments. The behavior can be altered by passing a `USER` object
        or opting for the raw command template.

        Parameters:
        -----------
        printflag : bool, optional, default=False
            If True, the generated pair style command is printed to the console.
        verbose : bool, optional, default=True
            If True, enables verbose output during the script generation.
        raw : bool, optional, default=False
            If True, returns the raw template of the pair style without any interpretation.
        USER : struct, optional, default=None
            A user-defined struct object used for overriding the default parameters.
            When provided, the method updates parameters using `USER` in conjunction with
            the instance's base parameters.
        beadtype : int, optional, default=None
            The bead type identifier used in the generated command. If not provided, the
            instance's beadtype is used.
        userid : str, optional, default=None
            The user identifier to include in the formatted command. Defaults to the instance's
            userid if not specified.

        Returns:
        --------
        str
            The formatted pair style command string.

        Raises:
        -------
        TypeError
            If `USER` is provided but is not of type `struct` or derived from `struct`.
        """
        # raw format
        if raw:
            return self.PAIR_STYLE
        # USER overrride if the forcefield class is inherited
        if USER is None: # ---- default behavior for forcefield
            parameters = self.parameters
            beadtype = self.beadtype
            userid = self.userid
        elif isinstance(USER,struct): # ---- behavior for dforcefield (using baseclass)
            parameters = self.parameters+USER
            beadtype = beadtype if beadtype is not None else USER.beadtype if hasattr(USER, 'beadtype') else self.beadtype
            userid = userid if userid is not None else USER.userid if hasattr(USER, 'userid') else self.userid
        else:
            raise TypeError(f'USER must be of type struct or derived from struct, not {type(USER)}')
        # cmd
        cmd = parameters.formateval(self.PAIR_STYLE)
        # Replace [comment] with the formatted comment (e.g., "[2:my_user_id]")
        cmd = cmd.replace("[comment]","[%d:%s]" % (beadtype, userid) if verbose else "")
        if printflag: print(cmd)
        return cmd


    def pair_diagcoeff(self,printflag=False,verbose=True, i=None,raw=False,USER=None,beadtype=None,userid=None):
        """
        Generate and return the diagonal pair coefficients for the current forcefield instance.

        This method evaluates the diagonal pair coefficients based on the interaction parameters,
        the bead type (`beadtype`), and the user identifier (`userid`). The bead type `i` can
        be overridden by passing it as an argument. The method supports returning the raw template
        without evaluation and modifying parameters using a `USER` object.

        Parameters:
        -----------
        printflag : bool, optional, default=False
            If True, the generated diagonal pair coefficient command is printed to the console.
        verbose : bool, optional, default=True
            If True, enables verbose output during the script generation.
        i : int, optional, default=None
            The bead type used for evaluating the diagonal pair coefficients. If not provided,
            defaults to the instance's bead type (`self.beadtype`).
        raw : bool, optional, default=False
            If True, returns the raw template for the diagonal pair coefficients without interpretation.
        USER : struct, optional, default=None
            A user-defined struct object used for overriding the default parameters.
            When provided, the method updates parameters using `USER` in conjunction with
            the instance's base parameters.
        beadtype : int, optional, default=None
            The bead type identifier to use in the command. Defaults to the instance's beadtype
            if not provided.
        userid : str, optional, default=None
            The user identifier to include in the formatted command. Defaults to the instance's
            userid if not specified.

        Returns:
        --------
        str
            The formatted diagonal pair coefficient command string.

        Raises:
        -------
        TypeError
            If `USER` is provided but is not of type `struct` or derived from `struct`.
        """
        # raw format
        if raw:
            return self.PAIR_DIAGCOEFF
        # USER overrride if the forcefield class is inherited
        if USER is None: # ---- default behavior for forcefield
            parameters = self.parameters
            beadtype = self.beadtype
            userid = self.userid
        elif isinstance(USER,struct): # ---- behavior for dforcefield (using baseclass)
            parameters = self.parameters+USER
            beadtype = beadtype if beadtype is not None else USER.beadtype if hasattr(USER, 'beadtype') else self.beadtype
            userid = userid if userid is not None else USER.userid if hasattr(USER, 'userid') else self.userid
        else:
            raise TypeError(f'USER must be of type struct or derived from struct, not {type(USER)}')
        # diagonal index
        i = i if i is not None else beadtype
        # cmd
        cmd = parameters.formateval(self.PAIR_DIAGCOEFF) % (i,i)
        # Replace [comment] with the formatted string, without using .format()
        cmd = cmd.replace("[comment]", "[%d:%s x %d:%s]" % (i, userid, i, userid) if verbose else "")
        if printflag: print(cmd)
        return cmd


    def pair_offdiagcoeff(self,o=None,printflag=False,verbose=True,i=None,raw=False,USER=None,beadtype=None,userid=None,oname=None):
        """
        Generate and return the off-diagonal pair coefficients for the current forcefield instance.

        This method evaluates the off-diagonal pair coefficients between two different bead types
        or forcefield objects, using the interaction parameters, bead type, and user identifier.
        The bead type `i` can be overridden, and the interaction with another forcefield object `o`
        can also be specified.

        Parameters:
        -----------
        o : forcefield or int, optional, default=None
            The second forcefield object or bead type used for calculating the off-diagonal
            pair coefficients. If not provided, the method assumes interactions between
            beads of the same type.
        printflag : bool, optional, default=False
            If True, the generated off-diagonal pair coefficient command is printed to the console.
        verbose : bool, optional, default=True
            If True, enables verbose output during the script generation.
        i : int, optional, default=None
            The bead type used for the current forcefield instance. If not provided,
            defaults to the instance's bead type (`self.beadtype`).
        raw : bool, optional, default=False
            If True, returns the raw template for the off-diagonal pair coefficients without interpretation.
        USER : struct, optional, default=None
            A user-defined struct object used for overriding the default parameters.
            When provided, the method updates parameters using `USER` in conjunction with
            the instance's base parameters.
        beadtype : int, optional, default=None
            The bead type identifier used in the command. Defaults to the instance's beadtype
            if not provided.
        userid : str, optional, default=None
            The user identifier included in the formatted command. Defaults to the instance's
            userid if not specified.
        oname : str, optional, default=None
            The user identifier for the second forcefield or bead type. If not provided, it
            defaults to `"none"`.

        Returns:
        --------
        str
            The formatted off-diagonal pair coefficient command string.

        Raises:
        -------
        TypeError
            If `USER` is not of type `struct` or derived from `struct`.
        IndexError
            If the first argument `o` is not a forcefield object or an integer.
        """

        # raw format
        if raw:
            return self.PAIR_OFFDIAGCOEFF
        # USER overrride if the forcefield class is inherited
        if USER is None: # ---- default behavior for forcefield
            parameters = self.parameters
            beadtype = self.beadtype
            userid = self.userid
            i = i if i is not None else beadtype
        elif isinstance(USER,struct): # ---- behavior for dforcefield (using baseclass)
            parameters = self.parameters+USER
            beadtype = beadtype if beadtype is not None else USER.beadtype if hasattr(USER, 'beadtype') else self.beadtype
            userid = userid if userid is not None else USER.userid if hasattr(USER, 'userid') else self.userid
        else:
            raise TypeError(f'USER must be of type struct or derived from struct, not {type(USER)}')
        # Determine the first bead type (i)
        i = i if i is not None else beadtype
        # Determine the second bead type (j) based on o
        if o is None:
            j = i
        elif hasattr(o, 'beadtype'):
            j = o.beadtype
        elif isinstance(o, (float, int)):
            j = int(o)
        else:
            raise IndexError("The first argument should be a forcefield object or an integer representing bead type.")
        # Adjust j if it matches i (to ensure off-diagonal interaction)
        if j == i:
            j = i - 1 if i > 1 else i + 1
        oname = oname if oname is not None else o.userid if hasattr(o, "userid") else "none"
        # cmd
        cmd = parameters.formateval(self.PAIR_OFFDIAGCOEFF) % (min(i,j),max(j,i))
        # Replace [comment] with the formatted string, without using .format()
        cmd = cmd.replace("[comment]", "[%d:%s x %d:%s]" % (i, self.userid, j, oname) if verbose else "")
        if printflag: print(cmd)
        return cmd

Subclasses

Class variables

var beadtype
var description
var name
var parameters
var userid
var version

Static methods

def printheader(txt, align='^', width=80, filler='~')

print header

Expand source code
@staticmethod
def printheader(txt,align="^",width=80,filler="~"):
    """ print header """
    if txt=="":
        print("\n"+filler*(width+6)+"\n")
    else:
        print(("\n{:"+filler+"{align}{width}}\n").format(' [ '+txt+' ] ', align=align, width=str(width)))

Methods

def getallattributes(self)

advanced method to get all attributes including class ones

Expand source code
def getallattributes(self):
    """ advanced method to get all attributes including class ones"""
    return {k: getattr(self, k) for k in dir(self) \
            if (not k.startswith('_')) and (not isinstance(getattr(self, k),types.MethodType))}
def pair_diagcoeff(self, printflag=False, verbose=True, i=None, raw=False, USER=None, beadtype=None, userid=None)

Generate and return the diagonal pair coefficients for the current forcefield instance.

This method evaluates the diagonal pair coefficients based on the interaction parameters, the bead type (beadtype), and the user identifier (userid). The bead type i can be overridden by passing it as an argument. The method supports returning the raw template without evaluation and modifying parameters using a USER object.

Parameters:

printflag : bool, optional, default=False If True, the generated diagonal pair coefficient command is printed to the console. verbose : bool, optional, default=True If True, enables verbose output during the script generation. i : int, optional, default=None The bead type used for evaluating the diagonal pair coefficients. If not provided, defaults to the instance's bead type (self.beadtype). raw : bool, optional, default=False If True, returns the raw template for the diagonal pair coefficients without interpretation. USER : struct, optional, default=None A user-defined struct object used for overriding the default parameters. When provided, the method updates parameters using USER in conjunction with the instance's base parameters. beadtype : int, optional, default=None The bead type identifier to use in the command. Defaults to the instance's beadtype if not provided. userid : str, optional, default=None The user identifier to include in the formatted command. Defaults to the instance's userid if not specified.

Returns:

str The formatted diagonal pair coefficient command string.

Raises:

TypeError If USER is provided but is not of type struct or derived from struct.

Expand source code
def pair_diagcoeff(self,printflag=False,verbose=True, i=None,raw=False,USER=None,beadtype=None,userid=None):
    """
    Generate and return the diagonal pair coefficients for the current forcefield instance.

    This method evaluates the diagonal pair coefficients based on the interaction parameters,
    the bead type (`beadtype`), and the user identifier (`userid`). The bead type `i` can
    be overridden by passing it as an argument. The method supports returning the raw template
    without evaluation and modifying parameters using a `USER` object.

    Parameters:
    -----------
    printflag : bool, optional, default=False
        If True, the generated diagonal pair coefficient command is printed to the console.
    verbose : bool, optional, default=True
        If True, enables verbose output during the script generation.
    i : int, optional, default=None
        The bead type used for evaluating the diagonal pair coefficients. If not provided,
        defaults to the instance's bead type (`self.beadtype`).
    raw : bool, optional, default=False
        If True, returns the raw template for the diagonal pair coefficients without interpretation.
    USER : struct, optional, default=None
        A user-defined struct object used for overriding the default parameters.
        When provided, the method updates parameters using `USER` in conjunction with
        the instance's base parameters.
    beadtype : int, optional, default=None
        The bead type identifier to use in the command. Defaults to the instance's beadtype
        if not provided.
    userid : str, optional, default=None
        The user identifier to include in the formatted command. Defaults to the instance's
        userid if not specified.

    Returns:
    --------
    str
        The formatted diagonal pair coefficient command string.

    Raises:
    -------
    TypeError
        If `USER` is provided but is not of type `struct` or derived from `struct`.
    """
    # raw format
    if raw:
        return self.PAIR_DIAGCOEFF
    # USER overrride if the forcefield class is inherited
    if USER is None: # ---- default behavior for forcefield
        parameters = self.parameters
        beadtype = self.beadtype
        userid = self.userid
    elif isinstance(USER,struct): # ---- behavior for dforcefield (using baseclass)
        parameters = self.parameters+USER
        beadtype = beadtype if beadtype is not None else USER.beadtype if hasattr(USER, 'beadtype') else self.beadtype
        userid = userid if userid is not None else USER.userid if hasattr(USER, 'userid') else self.userid
    else:
        raise TypeError(f'USER must be of type struct or derived from struct, not {type(USER)}')
    # diagonal index
    i = i if i is not None else beadtype
    # cmd
    cmd = parameters.formateval(self.PAIR_DIAGCOEFF) % (i,i)
    # Replace [comment] with the formatted string, without using .format()
    cmd = cmd.replace("[comment]", "[%d:%s x %d:%s]" % (i, userid, i, userid) if verbose else "")
    if printflag: print(cmd)
    return cmd
def pair_offdiagcoeff(self, o=None, printflag=False, verbose=True, i=None, raw=False, USER=None, beadtype=None, userid=None, oname=None)

Generate and return the off-diagonal pair coefficients for the current forcefield instance.

This method evaluates the off-diagonal pair coefficients between two different bead types or forcefield objects, using the interaction parameters, bead type, and user identifier. The bead type i can be overridden, and the interaction with another forcefield object o can also be specified.

Parameters:

o : forcefield or int, optional, default=None The second forcefield object or bead type used for calculating the off-diagonal pair coefficients. If not provided, the method assumes interactions between beads of the same type. printflag : bool, optional, default=False If True, the generated off-diagonal pair coefficient command is printed to the console. verbose : bool, optional, default=True If True, enables verbose output during the script generation. i : int, optional, default=None The bead type used for the current forcefield instance. If not provided, defaults to the instance's bead type (self.beadtype). raw : bool, optional, default=False If True, returns the raw template for the off-diagonal pair coefficients without interpretation. USER : struct, optional, default=None A user-defined struct object used for overriding the default parameters. When provided, the method updates parameters using USER in conjunction with the instance's base parameters. beadtype : int, optional, default=None The bead type identifier used in the command. Defaults to the instance's beadtype if not provided. userid : str, optional, default=None The user identifier included in the formatted command. Defaults to the instance's userid if not specified. oname : str, optional, default=None The user identifier for the second forcefield or bead type. If not provided, it defaults to "none".

Returns:

str The formatted off-diagonal pair coefficient command string.

Raises:

TypeError If USER is not of type struct or derived from struct. IndexError If the first argument o is not a forcefield object or an integer.

Expand source code
def pair_offdiagcoeff(self,o=None,printflag=False,verbose=True,i=None,raw=False,USER=None,beadtype=None,userid=None,oname=None):
    """
    Generate and return the off-diagonal pair coefficients for the current forcefield instance.

    This method evaluates the off-diagonal pair coefficients between two different bead types
    or forcefield objects, using the interaction parameters, bead type, and user identifier.
    The bead type `i` can be overridden, and the interaction with another forcefield object `o`
    can also be specified.

    Parameters:
    -----------
    o : forcefield or int, optional, default=None
        The second forcefield object or bead type used for calculating the off-diagonal
        pair coefficients. If not provided, the method assumes interactions between
        beads of the same type.
    printflag : bool, optional, default=False
        If True, the generated off-diagonal pair coefficient command is printed to the console.
    verbose : bool, optional, default=True
        If True, enables verbose output during the script generation.
    i : int, optional, default=None
        The bead type used for the current forcefield instance. If not provided,
        defaults to the instance's bead type (`self.beadtype`).
    raw : bool, optional, default=False
        If True, returns the raw template for the off-diagonal pair coefficients without interpretation.
    USER : struct, optional, default=None
        A user-defined struct object used for overriding the default parameters.
        When provided, the method updates parameters using `USER` in conjunction with
        the instance's base parameters.
    beadtype : int, optional, default=None
        The bead type identifier used in the command. Defaults to the instance's beadtype
        if not provided.
    userid : str, optional, default=None
        The user identifier included in the formatted command. Defaults to the instance's
        userid if not specified.
    oname : str, optional, default=None
        The user identifier for the second forcefield or bead type. If not provided, it
        defaults to `"none"`.

    Returns:
    --------
    str
        The formatted off-diagonal pair coefficient command string.

    Raises:
    -------
    TypeError
        If `USER` is not of type `struct` or derived from `struct`.
    IndexError
        If the first argument `o` is not a forcefield object or an integer.
    """

    # raw format
    if raw:
        return self.PAIR_OFFDIAGCOEFF
    # USER overrride if the forcefield class is inherited
    if USER is None: # ---- default behavior for forcefield
        parameters = self.parameters
        beadtype = self.beadtype
        userid = self.userid
        i = i if i is not None else beadtype
    elif isinstance(USER,struct): # ---- behavior for dforcefield (using baseclass)
        parameters = self.parameters+USER
        beadtype = beadtype if beadtype is not None else USER.beadtype if hasattr(USER, 'beadtype') else self.beadtype
        userid = userid if userid is not None else USER.userid if hasattr(USER, 'userid') else self.userid
    else:
        raise TypeError(f'USER must be of type struct or derived from struct, not {type(USER)}')
    # Determine the first bead type (i)
    i = i if i is not None else beadtype
    # Determine the second bead type (j) based on o
    if o is None:
        j = i
    elif hasattr(o, 'beadtype'):
        j = o.beadtype
    elif isinstance(o, (float, int)):
        j = int(o)
    else:
        raise IndexError("The first argument should be a forcefield object or an integer representing bead type.")
    # Adjust j if it matches i (to ensure off-diagonal interaction)
    if j == i:
        j = i - 1 if i > 1 else i + 1
    oname = oname if oname is not None else o.userid if hasattr(o, "userid") else "none"
    # cmd
    cmd = parameters.formateval(self.PAIR_OFFDIAGCOEFF) % (min(i,j),max(j,i))
    # Replace [comment] with the formatted string, without using .format()
    cmd = cmd.replace("[comment]", "[%d:%s x %d:%s]" % (i, self.userid, j, oname) if verbose else "")
    if printflag: print(cmd)
    return cmd
def pair_style(self, printflag=False, verbose=True, raw=False, USER=None, beadtype=None, userid=None)

Generate and return the pair style command for the current forcefield instance.

This method creates a formatted pair style command based on the interaction parameters stored in the parameters attribute. It allows customization of the command using the beadtype and userid arguments. The behavior can be altered by passing a USER object or opting for the raw command template.

Parameters:

printflag : bool, optional, default=False If True, the generated pair style command is printed to the console. verbose : bool, optional, default=True If True, enables verbose output during the script generation. raw : bool, optional, default=False If True, returns the raw template of the pair style without any interpretation. USER : struct, optional, default=None A user-defined struct object used for overriding the default parameters. When provided, the method updates parameters using USER in conjunction with the instance's base parameters. beadtype : int, optional, default=None The bead type identifier used in the generated command. If not provided, the instance's beadtype is used. userid : str, optional, default=None The user identifier to include in the formatted command. Defaults to the instance's userid if not specified.

Returns:

str The formatted pair style command string.

Raises:

TypeError If USER is provided but is not of type struct or derived from struct.

Expand source code
def pair_style(self,printflag=False,verbose=True, raw=False,USER=None,beadtype=None,userid=None):
    """
    Generate and return the pair style command for the current forcefield instance.

    This method creates a formatted pair style command based on the interaction parameters
    stored in the `parameters` attribute. It allows customization of the command using the
    `beadtype` and `userid` arguments. The behavior can be altered by passing a `USER` object
    or opting for the raw command template.

    Parameters:
    -----------
    printflag : bool, optional, default=False
        If True, the generated pair style command is printed to the console.
    verbose : bool, optional, default=True
        If True, enables verbose output during the script generation.
    raw : bool, optional, default=False
        If True, returns the raw template of the pair style without any interpretation.
    USER : struct, optional, default=None
        A user-defined struct object used for overriding the default parameters.
        When provided, the method updates parameters using `USER` in conjunction with
        the instance's base parameters.
    beadtype : int, optional, default=None
        The bead type identifier used in the generated command. If not provided, the
        instance's beadtype is used.
    userid : str, optional, default=None
        The user identifier to include in the formatted command. Defaults to the instance's
        userid if not specified.

    Returns:
    --------
    str
        The formatted pair style command string.

    Raises:
    -------
    TypeError
        If `USER` is provided but is not of type `struct` or derived from `struct`.
    """
    # raw format
    if raw:
        return self.PAIR_STYLE
    # USER overrride if the forcefield class is inherited
    if USER is None: # ---- default behavior for forcefield
        parameters = self.parameters
        beadtype = self.beadtype
        userid = self.userid
    elif isinstance(USER,struct): # ---- behavior for dforcefield (using baseclass)
        parameters = self.parameters+USER
        beadtype = beadtype if beadtype is not None else USER.beadtype if hasattr(USER, 'beadtype') else self.beadtype
        userid = userid if userid is not None else USER.userid if hasattr(USER, 'userid') else self.userid
    else:
        raise TypeError(f'USER must be of type struct or derived from struct, not {type(USER)}')
    # cmd
    cmd = parameters.formateval(self.PAIR_STYLE)
    # Replace [comment] with the formatted comment (e.g., "[2:my_user_id]")
    cmd = cmd.replace("[comment]","[%d:%s]" % (beadtype, userid) if verbose else "")
    if printflag: print(cmd)
    return cmd
class none

SMD:TLSPH forcefield (updated Lagrangian)

Expand source code
class none(smd):
    """ SMD:TLSPH forcefield (updated Lagrangian) """
    name = smd.name + struct(style="none")
    description = smd.description + struct(style="no interactions")

    # style definition (LAMMPS code between triple """)
    PAIR_DIAGCOEFF = """
    # [comment] Diagonal pair coefficient tlsph
    pair_coeff      %d %d none
    """
    PAIR_OFFDIAGCOEFF = """
    # [comment] Off-diagonal pair coefficient (generic)
    pair_coeff      %d %d smd/hertz ${contact_stiffness}
    """

Ancestors

Subclasses

Class variables

var PAIR_DIAGCOEFF
var PAIR_OFFDIAGCOEFF
var description
var name

Inherited members

class paramauto (sortdefinitions=False, debug=False, **kwargs)

Class: paramauto

A subclass of param with enhanced handling for automatic sorting and evaluation of definitions. The paramauto class ensures that all fields are sorted to resolve dependencies, allowing seamless stacking of partially defined objects.


Features

  • Inherits all functionalities of param.
  • Automatically sorts definitions for dependency resolution.
  • Simplifies handling of partial definitions in dynamic structures.
  • Supports safe concatenation of definitions.

Examples

Automatic Dependency Sorting

Definitions are automatically sorted to resolve dependencies:

p = paramauto(a=1, b="${a}+1", c="${a}+${b}")
p.disp()
# Output:
# --------
#      a: 1
#      b: ${a} + 1 (= 2)
#      c: ${a} + ${b} (= 3)
# --------

Handling Missing Definitions

Unresolved dependencies raise warnings but do not block execution:

p = paramauto(a=1, b="${a}+1", c="${a}+${d}")
p.disp()
# Output:
# --------
#      a: 1
#      b: ${a} + 1 (= 2)
#      c: ${a} + ${d} (= < undef definition "${d}" >)
# --------

Concatenation and Inheritance

Concatenating paramauto objects resolves definitions:

p1 = paramauto(a=1, b="${a}+2")
p2 = paramauto(c="${b}*3")
p3 = p1 + p2
p3.disp()
# Output:
# --------
#      a: 1
#      b: ${a} + 2 (= 3)
#      c: ${b} * 3 (= 9)
# --------

Utility Methods

Method Description
sortdefinitions() Automatically sorts fields to resolve dependencies.
eval() Evaluate all fields, resolving dependencies.
disp() Display all fields with their resolved values.

Overloaded Operators

Supported Operators

  • +: Concatenates two paramauto objects, resolving dependencies.
  • +=: Updates the current object with another, resolving dependencies.
  • len(): Number of fields.
  • in: Check for field existence.

Advanced Usage

Partial Definitions

The paramauto class simplifies handling of partially defined fields:

p = paramauto(a="${d}", b="${a}+1")
p.disp()
# Warning: Unable to resolve dependencies.
# --------
#      a: ${d} (= < undef definition "${d}" >)
#      b: ${a} + 1 (= < undef definition "${d}" >)
# --------

p.d = 10
p.disp()
# Dependencies are resolved:
# --------
#      d: 10
#      a: ${d} (= 10)
#      b: ${a} + 1 (= 11)
# --------

Notes

  • The paramauto class is computationally more intensive than param due to automatic sorting.
  • It is ideal for managing dynamic systems with complex interdependencies.

Examples

            p = paramauto()
            p.b = "${aa}"
            p.disp()
        yields
            WARNING: unable to interpret 1/1 expressions in "definitions"
              -----------:----------------------------------------
                        b: ${aa}
                         = < undef definition "${aa}" >
              -----------:----------------------------------------
              p.aa = 2
              p.disp()
        yields
            -----------:----------------------------------------
                     aa: 2
                      b: ${aa}
                       = 2
            -----------:----------------------------------------
            q = paramauto(c="${aa}+${b}")+p
            q.disp()
        yields
            -----------:----------------------------------------
                     aa: 2
                      b: ${aa}
                       = 2
                      c: ${aa}+${b}
                       = 4
            -----------:----------------------------------------
            q.aa = 30
            q.disp()
        yields
            -----------:----------------------------------------
                     aa: 30
                      b: ${aa}
                       = 30
                      c: ${aa}+${b}
                       = 60
            -----------:----------------------------------------
            q.aa = "${d}"
            q.disp()
        yields multiple errors (recursion)
        WARNING: unable to interpret 3/3 expressions in "definitions"
          -----------:----------------------------------------
                   aa: ${d}
                     = < undef definition "${d}" >
                    b: ${aa}
                     = Eval Error < invalid [...] (<string>, line 1) >
                    c: ${aa}+${b}
                     = Eval Error < invalid [...] (<string>, line 1) >
          -----------:----------------------------------------
            q.d = 100
            q.disp()
        yields
          -----------:----------------------------------------
                    d: 100
                   aa: ${d}
                     = 100
                    b: ${aa}
                     = 100
                    c: ${aa}+${b}
                     = 200
          -----------:----------------------------------------


    Example:

        p = paramauto(b="${a}+1",c="${a}+${d}",a=1)
        p.disp()
    generates:
        WARNING: unable to interpret 1/3 expressions in "definitions"
          -----------:----------------------------------------
                    a: 1
                    b: ${a}+1
                     = 2
                    c: ${a}+${d}
                     = < undef definition "${d}" >
          -----------:----------------------------------------
    setting p.d
        p.d = 2
        p.disp()
    produces
          -----------:----------------------------------------
                    a: 1
                    d: 2
                    b: ${a}+1
                     = 2
                    c: ${a}+${d}
                     = 3
          -----------:----------------------------------------

constructor

Expand source code
class paramauto(param):
    """
    Class: `paramauto`
    ==================

    A subclass of `param` with enhanced handling for automatic sorting and evaluation
    of definitions. The `paramauto` class ensures that all fields are sorted to resolve
    dependencies, allowing seamless stacking of partially defined objects.

    ---

    ### Features
    - Inherits all functionalities of `param`.
    - Automatically sorts definitions for dependency resolution.
    - Simplifies handling of partial definitions in dynamic structures.
    - Supports safe concatenation of definitions.

    ---

    ### Examples

    #### Automatic Dependency Sorting
    Definitions are automatically sorted to resolve dependencies:
    ```python
    p = paramauto(a=1, b="${a}+1", c="${a}+${b}")
    p.disp()
    # Output:
    # --------
    #      a: 1
    #      b: ${a} + 1 (= 2)
    #      c: ${a} + ${b} (= 3)
    # --------
    ```

    #### Handling Missing Definitions
    Unresolved dependencies raise warnings but do not block execution:
    ```python
    p = paramauto(a=1, b="${a}+1", c="${a}+${d}")
    p.disp()
    # Output:
    # --------
    #      a: 1
    #      b: ${a} + 1 (= 2)
    #      c: ${a} + ${d} (= < undef definition "${d}" >)
    # --------
    ```

    ---

    ### Concatenation and Inheritance
    Concatenating `paramauto` objects resolves definitions:
    ```python
    p1 = paramauto(a=1, b="${a}+2")
    p2 = paramauto(c="${b}*3")
    p3 = p1 + p2
    p3.disp()
    # Output:
    # --------
    #      a: 1
    #      b: ${a} + 2 (= 3)
    #      c: ${b} * 3 (= 9)
    # --------
    ```

    ---

    ### Utility Methods

    | Method                | Description                                            |
    |-----------------------|--------------------------------------------------------|
    | `sortdefinitions()`   | Automatically sorts fields to resolve dependencies.    |
    | `eval()`              | Evaluate all fields, resolving dependencies.           |
    | `disp()`              | Display all fields with their resolved values.         |

    ---

    ### Overloaded Operators

    #### Supported Operators
    - `+`: Concatenates two `paramauto` objects, resolving dependencies.
    - `+=`: Updates the current object with another, resolving dependencies.
    - `len()`: Number of fields.
    - `in`: Check for field existence.

    ---

    ### Advanced Usage

    #### Partial Definitions
    The `paramauto` class simplifies handling of partially defined fields:
    ```python
    p = paramauto(a="${d}", b="${a}+1")
    p.disp()
    # Warning: Unable to resolve dependencies.
    # --------
    #      a: ${d} (= < undef definition "${d}" >)
    #      b: ${a} + 1 (= < undef definition "${d}" >)
    # --------

    p.d = 10
    p.disp()
    # Dependencies are resolved:
    # --------
    #      d: 10
    #      a: ${d} (= 10)
    #      b: ${a} + 1 (= 11)
    # --------
    ```

    ---

    ### Notes
    - The `paramauto` class is computationally more intensive than `param` due to automatic sorting.
    - It is ideal for managing dynamic systems with complex interdependencies.

    ### Examples
                    p = paramauto()
                    p.b = "${aa}"
                    p.disp()
                yields
                    WARNING: unable to interpret 1/1 expressions in "definitions"
                      -----------:----------------------------------------
                                b: ${aa}
                                 = < undef definition "${aa}" >
                      -----------:----------------------------------------
                      p.aa = 2
                      p.disp()
                yields
                    -----------:----------------------------------------
                             aa: 2
                              b: ${aa}
                               = 2
                    -----------:----------------------------------------
                    q = paramauto(c="${aa}+${b}")+p
                    q.disp()
                yields
                    -----------:----------------------------------------
                             aa: 2
                              b: ${aa}
                               = 2
                              c: ${aa}+${b}
                               = 4
                    -----------:----------------------------------------
                    q.aa = 30
                    q.disp()
                yields
                    -----------:----------------------------------------
                             aa: 30
                              b: ${aa}
                               = 30
                              c: ${aa}+${b}
                               = 60
                    -----------:----------------------------------------
                    q.aa = "${d}"
                    q.disp()
                yields multiple errors (recursion)
                WARNING: unable to interpret 3/3 expressions in "definitions"
                  -----------:----------------------------------------
                           aa: ${d}
                             = < undef definition "${d}" >
                            b: ${aa}
                             = Eval Error < invalid [...] (<string>, line 1) >
                            c: ${aa}+${b}
                             = Eval Error < invalid [...] (<string>, line 1) >
                  -----------:----------------------------------------
                    q.d = 100
                    q.disp()
                yields
                  -----------:----------------------------------------
                            d: 100
                           aa: ${d}
                             = 100
                            b: ${aa}
                             = 100
                            c: ${aa}+${b}
                             = 200
                  -----------:----------------------------------------


            Example:

                p = paramauto(b="${a}+1",c="${a}+${d}",a=1)
                p.disp()
            generates:
                WARNING: unable to interpret 1/3 expressions in "definitions"
                  -----------:----------------------------------------
                            a: 1
                            b: ${a}+1
                             = 2
                            c: ${a}+${d}
                             = < undef definition "${d}" >
                  -----------:----------------------------------------
            setting p.d
                p.d = 2
                p.disp()
            produces
                  -----------:----------------------------------------
                            a: 1
                            d: 2
                            b: ${a}+1
                             = 2
                            c: ${a}+${d}
                             = 3
                  -----------:----------------------------------------

    """

    def __add__(self,p):
        return super().__add__(p,sortdefinitions=True,raiseerror=False)
        self._needs_sorting = True

    def __iadd__(self,p):
        return super().__iadd__(p,sortdefinitions=True,raiseerror=False)
        self._needs_sorting = True

    def __repr__(self):
        self.sortdefinitions(raiseerror=False)
        #super(param,self).__repr__()
        super().__repr__()
        return str(self)

    def setattr(self,key,value):
        """ set field and value """
        if isinstance(value,list) and len(value)==0 and key in self:
            delattr(self, key)
        else:
            self.__dict__[key] = value
            self.__dict__["_needs_sorting"] = True

    def sortdefinitions(self, raiseerror=True, silentmode=True):
        if self._needs_sorting:
            super().sortdefinitions(raiseerror=raiseerror, silentmode=silentmode)
            self._needs_sorting = False

Ancestors

  • pizza.private.mstruct.param
  • pizza.private.mstruct.struct

Subclasses

  • parameterforcefield
  • pizza.dscript.lambdaScriptdata
  • pizza.forcefield.parameterforcefield
  • pizza.region.regiondata

Methods

def setattr(self, key, value)

set field and value

Expand source code
def setattr(self,key,value):
    """ set field and value """
    if isinstance(value,list) and len(value)==0 and key in self:
        delattr(self, key)
    else:
        self.__dict__[key] = value
        self.__dict__["_needs_sorting"] = True
def sortdefinitions(self, raiseerror=True, silentmode=True)

sortdefintions sorts all definitions so that they can be executed as param(). If any inconsistency is found, an error message is generated.

Flags = default values raiseerror=True show erros of True silentmode=False no warning if True

Expand source code
def sortdefinitions(self, raiseerror=True, silentmode=True):
    if self._needs_sorting:
        super().sortdefinitions(raiseerror=raiseerror, silentmode=silentmode)
        self._needs_sorting = False
class parameterforcefield (sortdefinitions=False, **kwargs)

class of forcefields parameters, derived from param note that conctanating two forcefields force them to to be sorted

Constructor for parameterforcefield. It forces the parent's _returnerror parameter to False.

Parameters:

_protection : bool, optional Whether to enable protection on the parameters (default: False). _evaluation : bool, optional Whether evaluation is enabled for the parameters (default: True). sortdefinitions : bool, optional Whether to sort definitions upon initialization (default: False). **kwargs : dict Additional keyword arguments for the parent class.

Expand source code
class parameterforcefield(paramauto):
    """ class of forcefields parameters, derived from param
        note that conctanating two forcefields force them
        to to be sorted
    """
    _type = "FF"
    _fulltype = "forcefield"
    _ftype = "parameter"
    _maxdisplay = 80

    # same strategy as used in dscript for forcing  _returnerror = False (added 2024-09-12)
    def __init__(self, _protection=False, _evaluation=True, sortdefinitions=False, **kwargs):
        """
        Constructor for parameterforcefield. It forces the parent's _returnerror parameter to False.

        Parameters:
        -----------
        _protection : bool, optional
            Whether to enable protection on the parameters (default: False).
        _evaluation : bool, optional
            Whether evaluation is enabled for the parameters (default: True).
        sortdefinitions : bool, optional
            Whether to sort definitions upon initialization (default: False).
        **kwargs : dict
            Additional keyword arguments for the parent class.
        """
        # Call the parent class constructor
        super().__init__(_protection=_protection, _evaluation=_evaluation, sortdefinitions=sortdefinitions, **kwargs)
        # Override the _returnerror attribute at the instance level
        self._returnerror = False

Ancestors

  • pizza.private.mstruct.paramauto
  • pizza.private.mstruct.param
  • pizza.private.mstruct.struct
class rigidwall (beadtype=1, userid=None, USER=forcefield (FF object) with 0 parameters)

rigid walls (smd:none): rigidwall() rigidwall(beadtype=index, userid="wall", USER=…)

override any propery with USER.parameter (set only the parameters you want to override) USER.rho: density in kg/m3 (default=3000) USER.c0: speed of the sound in m/s (default=10.0) USER.contact_scale: scaling coefficient for contact (default=1.5) USER.contact_stiffness: contact stifness in Pa (default="2.5${c0}^2${rho}")

rigidwall forcefield: rigidwall(beadtype=index, userid="mywall")

Expand source code
class rigidwall(none):
    """ rigid walls (smd:none):
            rigidwall()
            rigidwall(beadtype=index, userid="wall", USER=...)

            override any propery with USER.parameter (set only the parameters you want to override)
                USER.rho: density in kg/m3 (default=3000)
                USER.c0: speed of the sound in m/s (default=10.0)
                USER.contact_scale: scaling coefficient for contact (default=1.5)
                USER.contact_stiffness: contact stifness in Pa (default="2.5*${c0}^2*${rho}")
    """
    name = none.name + struct(material="walls")
    description = none.description + struct(material="rigid walls")
    userid = 'solidfood'
    version = 0.1

    # constructor (do not forgert to include the constuctor)
    def __init__(self, beadtype=1, userid=None, USER=parameterforcefield()):
        """ rigidwall forcefield:
            rigidwall(beadtype=index, userid="mywall") """
        # super().__init__()
        if userid!=None: self.userid = userid
        self.beadtype = beadtype
        self.parameters = parameterforcefield(
            rho = 3000,
            c0 = 10.0,
            contact_stiffness = '2.5*${c0}^2*${rho}',
            contact_scale = 1.5
            ) + USER # update with user properties if any

Ancestors

Class variables

var description
var name
var userid
var version

Inherited members

class saltTLSPH (beadtype=1, userid=None, USER=forcefield (FF object) with 0 parameters)

SALTLSPH (smd:tlsph): ongoing "salting" beadtype for rheology control saltTLSPH() saltTLSPH(beadtype=index, userid="salt", USER=…)

override any property with USER.property = value

saltTLSPH forcefield: saltTLSPH(beadtype=index, userid="salt")

Expand source code
class saltTLSPH(tlsph):
    """ SALTLSPH (smd:tlsph): ongoing "salting" beadtype for rheology control
            saltTLSPH()
            saltTLSPH(beadtype=index, userid="salt", USER=...)

            override any property with USER.property = value
    """
    name = tlsph.name + struct(material="solidfood")
    description = tlsph.description + struct(material="food beads - solid behavior")
    userid = '"salt"'
    version = 0.1

    # constructor (do not forgert to include the constuctor)
    def __init__(self, beadtype=1, userid=None, USER=parameterforcefield()):
        """ saltTLSPH forcefield:
            saltTLSPH(beadtype=index, userid="salt") """
        # super().__init__()
        if userid!=None: self.userid = userid
        self.beadtype = beadtype
        self.parameters = parameterforcefield(
            # food-food interactions
            rho = 1000,
            c0 = 10.0,
            E = '5*${c0}^2*${rho}',
            nu = 0.3,
            q1 = 1.0,
            q2 = 0.0,
            Hg = 10,
            Cp = 1.0,
            sigma_yield = '0.1*${E}',
            hardening = 0,
            # hertz contacts
            contact_scale = 1.5,
            contact_stiffness = '2.5*${c0}^2*${rho}'
            ) + USER # update with user properties if any

Ancestors

Class variables

var description
var name
var userid
var version

Inherited members

class smd

SMD forcefield

Expand source code
class smd(forcefield):
    """ SMD forcefield """
    name = forcefield.name + struct(forcefield="LAMMPS:SMD")
    description = forcefield.description + struct(forcefield="LAMMPS:SMD - solid, liquid, rigid forcefields (continuum mechanics)")

    # forcefield definition (LAMMPS code between triple """)
    PAIR_STYLE = """
    # [comment] PAIR STYLE SMD
    pair_style      hybrid/overlay smd/ulsph *DENSITY_CONTINUITY *VELOCITY_GRADIENT *NO_GRADIENT_CORRECTION &
                                   smd/tlsph smd/hertz ${contact_scale}
    """

Ancestors

Subclasses

Class variables

var PAIR_STYLE
var description
var name

Inherited members

class solidfood (beadtype=1, userid=None, USER=forcefield (FF object) with 0 parameters)

solidfood material (smd:tlsph): model solid food object solidfood() solidfood(beadtype=index, userid="myfood", USER=…)

override any propery with USER.property=value (set only the parameters you want to override) USER.rho: density in kg/m3 (default=1000) USER.c0: speed of the sound in m/s (default=10.0) USER.E: Young's modulus in Pa (default="5${c0}^2${rho}") USER.nu: Poisson ratio (default=0.3) USER.q1: standard artificial viscosity linear coefficient (default=1.0) USER.q2: standard artificial viscosity quadratic coefficient (default=0) USER.Hg: hourglass control coefficient (default=10.0) USER.Cp: heat capacity of material – not used here (default=1.0) USER.sigma_yield: plastic yield stress in Pa (default="0.1${E}") USER.hardening: hardening parameter (default=0) USER.contact_scale: scaling coefficient for contact (default=1.5) USER.contact_stiffness: contact stifness in Pa (default="2.5${c0}^2*${rho}")

food forcefield: solidfood(beadtype=index, userid="myfood")

Expand source code
class solidfood(tlsph):
    """ solidfood material (smd:tlsph): model solid food object
            solidfood()
            solidfood(beadtype=index, userid="myfood", USER=...)

            override any propery with USER.property=value (set only the parameters you want to override)
                USER.rho: density in kg/m3 (default=1000)
                USER.c0: speed of the sound in m/s (default=10.0)
                USER.E: Young's modulus in Pa (default="5*${c0}^2*${rho}")
                USER.nu: Poisson ratio (default=0.3)
                USER.q1: standard artificial viscosity linear coefficient (default=1.0)
                USER.q2: standard artificial viscosity quadratic coefficient (default=0)
                USER.Hg: hourglass control coefficient (default=10.0)
                USER.Cp: heat capacity of material -- not used here (default=1.0)
                USER.sigma_yield: plastic yield stress in Pa (default="0.1*${E}")
                USER.hardening: hardening parameter (default=0)
                USER.contact_scale: scaling coefficient for contact (default=1.5)
                USER.contact_stiffness: contact stifness in Pa (default="2.5*${c0}^2*${rho}")
    """
    name = tlsph.name + struct(material="solidfood")
    description = tlsph.description + struct(material="food beads - solid behavior")
    userid = 'solidfood'
    version = 0.1

    # constructor (do not forgert to include the constuctor)
    def __init__(self, beadtype=1, userid=None, USER=parameterforcefield()):
        """ food forcefield:
            solidfood(beadtype=index, userid="myfood") """
        # super().__init__()
        if userid!=None: self.userid = userid
        self.beadtype = beadtype
        self.parameters = parameterforcefield(
            # food-food interactions
            rho = 1000,
            c0 = 10.0,
            E = "5*${c0}^2*${rho}",
            nu = 0.3,
            q1 = 1.0,
            q2 = 0.0,
            Hg = 10.0,
            Cp = 1.0,
            sigma_yield = "0.1*${E}",
            hardening = 0,
            # hertz contacts
            contact_scale = 1.5,
            contact_stiffness = "2.5*${c0}^2*${rho}"
            ) + USER # update with user properties if any

Ancestors

Class variables

var description
var name
var userid
var version

Inherited members

class struct (debug=False, **kwargs)

Class: struct

A lightweight class that mimics Matlab-like structures, with additional features such as dynamic field creation, indexing, concatenation, and compatibility with evaluated parameters (param).


Features

  • Dynamic creation of fields.
  • Indexing and iteration support for fields.
  • Concatenation and subtraction of structures.
  • Conversion to and from dictionaries.
  • Compatible with param and paramauto for evaluation and dependency handling.

Examples

Basic Usage

s = struct(a=1, b=2, c='${a} + ${b} # evaluate me if you can')
print(s.a)  # 1
s.d = 11    # Append a new field
delattr(s, 'd')  # Delete the field

Using param for Evaluation

p = param(a=1, b=2, c='${a} + ${b} # evaluate me if you can')
p.eval()
# Output:
# --------
#      a: 1
#      b: 2
#      c: ${a} + ${b} # evaluate me if you can (= 3)
# --------

Concatenation and Subtraction

Fields from the right-most structure overwrite existing values.

a = struct(a=1, b=2)
b = struct(c=3, d="d", e="e")
c = a + b
e = c - a

Practical Shorthands

Constructing a Structure from Keys

s = struct.fromkeys(["a", "b", "c", "d"])
# Output:
# --------
#      a: None
#      b: None
#      c: None
#      d: None
# --------

Building a Structure from Variables in a String

s = struct.scan("${a} + ${b} * ${c} / ${d} --- ${ee}")
s.a = 1
s.b = "test"
s.c = [1, "a", 2]
s.generator()
# Output:
# X = struct(
#      a=1,
#      b="test",
#      c=[1, 'a', 2],
#      d=None,
#      ee=None
# )

Indexing and Iteration

Structures can be indexed or sliced like lists.

c = a + b
c[0]      # Access the first field
c[-1]     # Access the last field
c[:2]     # Slice the structure
for field in c:
    print(field)

Dynamic Dependency Management

struct provides control over dependencies, sorting, and evaluation.

s = struct(d=3, e="${c} + {d}", c='${a} + ${b}', a=1, b=2)
s.sortdefinitions()
# Output:
# --------
#      d: 3
#      a: 1
#      b: 2
#      c: ${a} + ${b}
#      e: ${c} + ${d}
# --------

For dynamic evaluation, use param:

p = param(sortdefinitions=True, d=3, e="${c} + ${d}", c='${a} + ${b}', a=1, b=2)
# Output:
# --------
#      d: 3
#      a: 1
#      b: 2
#      c: ${a} + ${b}  (= 3)
#      e: ${c} + ${d}  (= 6)
# --------

Overloaded Methods and Operators

Supported Operators

  • +: Concatenation of two structures (__add__).
  • -: Subtraction of fields (__sub__).
  • <<: Import values from another structure (__lshift__)
  • len(): Number of fields (__len__).
  • in: Check for field existence (__contains__).

Method Overview

Method Description
check(default) Populate fields with defaults if missing.
clear() Remove all fields.
dict2struct(dico) Create a structure from a dictionary.
disp() Display the structure.
eval() Evaluate expressions within fields.
fromkeys(keys) Create a structure from a list of keys.
generator() Generate Python code representing the structure.
importfrom() Import undefined values from another struct or dict.
items() Return key-value pairs.
keys() Return all keys in the structure.
read(file) Load structure fields from a file.
scan(string) Extract variables from a string and populate fields.
sortdefinitions() Sort fields to resolve dependencies.
struct2dict() Convert the structure to a dictionary.
validkeys() Return valid keys
values() Return all field values.
write(file) Save the structure to a file.

Dynamic Properties

Property Description
isempty True if the structure is empty.
isdefined True if all fields are defined.

constructor, use debug=True to report eval errors

Expand source code
class struct():
    """
    Class: `struct`
    ================

    A lightweight class that mimics Matlab-like structures, with additional features
    such as dynamic field creation, indexing, concatenation, and compatibility with
    evaluated parameters (`param`).

    ---

    ### Features
    - Dynamic creation of fields.
    - Indexing and iteration support for fields.
    - Concatenation and subtraction of structures.
    - Conversion to and from dictionaries.
    - Compatible with `param` and `paramauto` for evaluation and dependency handling.

    ---

    ### Examples

    #### Basic Usage
    ```python
    s = struct(a=1, b=2, c='${a} + ${b} # evaluate me if you can')
    print(s.a)  # 1
    s.d = 11    # Append a new field
    delattr(s, 'd')  # Delete the field
    ```

    #### Using `param` for Evaluation
    ```python
    p = param(a=1, b=2, c='${a} + ${b} # evaluate me if you can')
    p.eval()
    # Output:
    # --------
    #      a: 1
    #      b: 2
    #      c: ${a} + ${b} # evaluate me if you can (= 3)
    # --------
    ```

    ---

    ### Concatenation and Subtraction
    Fields from the right-most structure overwrite existing values.
    ```python
    a = struct(a=1, b=2)
    b = struct(c=3, d="d", e="e")
    c = a + b
    e = c - a
    ```

    ---

    ### Practical Shorthands

    #### Constructing a Structure from Keys
    ```python
    s = struct.fromkeys(["a", "b", "c", "d"])
    # Output:
    # --------
    #      a: None
    #      b: None
    #      c: None
    #      d: None
    # --------
    ```

    #### Building a Structure from Variables in a String
    ```python
    s = struct.scan("${a} + ${b} * ${c} / ${d} --- ${ee}")
    s.a = 1
    s.b = "test"
    s.c = [1, "a", 2]
    s.generator()
    # Output:
    # X = struct(
    #      a=1,
    #      b="test",
    #      c=[1, 'a', 2],
    #      d=None,
    #      ee=None
    # )
    ```

    #### Indexing and Iteration
    Structures can be indexed or sliced like lists.
    ```python
    c = a + b
    c[0]      # Access the first field
    c[-1]     # Access the last field
    c[:2]     # Slice the structure
    for field in c:
        print(field)
    ```

    ---

    ### Dynamic Dependency Management
    `struct` provides control over dependencies, sorting, and evaluation.

    ```python
    s = struct(d=3, e="${c} + {d}", c='${a} + ${b}', a=1, b=2)
    s.sortdefinitions()
    # Output:
    # --------
    #      d: 3
    #      a: 1
    #      b: 2
    #      c: ${a} + ${b}
    #      e: ${c} + ${d}
    # --------
    ```

    For dynamic evaluation, use `param`:
    ```python
    p = param(sortdefinitions=True, d=3, e="${c} + ${d}", c='${a} + ${b}', a=1, b=2)
    # Output:
    # --------
    #      d: 3
    #      a: 1
    #      b: 2
    #      c: ${a} + ${b}  (= 3)
    #      e: ${c} + ${d}  (= 6)
    # --------
    ```

    ---

    ### Overloaded Methods and Operators
    #### Supported Operators
    - `+`: Concatenation of two structures (`__add__`).
    - `-`: Subtraction of fields (`__sub__`).
    - `<<`: Import values from another structure (`__lshift__`)
    - `len()`: Number of fields (`__len__`).
    - `in`: Check for field existence (`__contains__`).

    #### Method Overview
    | Method                | Description                                             |
    |-----------------------|---------------------------------------------------------|
    | `check(default)`      | Populate fields with defaults if missing.               |
    | `clear()`             | Remove all fields.                                      |
    | `dict2struct(dico)`   | Create a structure from a dictionary.                   |
    | `disp()`              | Display the structure.                                  |
    | `eval()`              | Evaluate expressions within fields.                     |
    | `fromkeys(keys)`      | Create a structure from a list of keys.                 |
    | `generator()`         | Generate Python code representing the structure.        |
    | `importfrom()`        | Import undefined values from another struct or dict.    |
    | `items()`             | Return key-value pairs.                                 |
    | `keys()`              | Return all keys in the structure.                       |
    | `read(file)`          | Load structure fields from a file.                      |
    | `scan(string)`        | Extract variables from a string and populate fields.    |
    | `sortdefinitions()`   | Sort fields to resolve dependencies.                    |
    | `struct2dict()`       | Convert the structure to a dictionary.                  |
    | `validkeys()`         | Return valid keys                                       |
    | `values()`            | Return all field values.                                |
    | `write(file)`         | Save the structure to a file.                           |

    ---

    ### Dynamic Properties
    | Property    | Description                            |
    |-------------|----------------------------------------|
    | `isempty`   | `True` if the structure is empty.      |
    | `isdefined` | `True` if all fields are defined.      |

    ---
    """

    # attributes to be overdefined
    _type = "struct"        # object type
    _fulltype = "structure" # full name
    _ftype = "field"        # field name
    _evalfeature = False    # true if eval() is available
    _maxdisplay = 40        # maximum number of characters to display (should be even)
    _propertyasattribute = False
    _precision = 4
    _needs_sorting = False

    # attributes for the iterator method
    # Please keep it static, duplicate the object before changing _iter_
    _iter_ = 0

    # excluded attributes (keep the , in the Tupple if it is singleton)
    _excludedattr = {'_iter_','__class__','_protection','_evaluation','_returnerror','_debug','_precision','_needs_sorting'} # used by keys() and len()


    # Methods
    def __init__(self,debug=False,**kwargs):
        """ constructor, use debug=True to report eval errors"""
        # Optionally extend _excludedattr here
        self._excludedattr = self._excludedattr | {'_excludedattr', '_type', '_fulltype','_ftype'} # addition 2024-10-11
        self._debug = debug
        self.set(**kwargs)

    def zip(self):
        """ zip keys and values """
        return zip(self.keys(),self.values())

    @staticmethod
    def dict2struct(dico,makeparam=False):
        """ create a structure from a dictionary """
        if isinstance(dico,dict):
            s = param() if makeparam else struct()
            s.set(**dico)
            return s
        raise TypeError("the argument must be a dictionary")

    def struct2dict(self):
        """ create a dictionary from the current structure """
        return dict(self.zip())

    def struct2param(self,protection=False,evaluation=True):
        """ convert an object struct() to param() """
        p = param(**self.struct2dict())
        for i in range(len(self)):
            if isinstance(self[i],pstr): p[i] = pstr(p[i])
        p._protection = protection
        p._evaluation = evaluation
        return p

    def set(self,**kwargs):
        """ initialization """
        self.__dict__.update(kwargs)

    def setattr(self,key,value):
        """ set field and value """
        if isinstance(value,list) and len(value)==0 and key in self:
            delattr(self, key)
        else:
            self.__dict__[key] = value

    def getattr(self,key):
        """Get attribute override to access both instance attributes and properties if allowed."""
        if key in self.__dict__:
            return self.__dict__[key]
        elif getattr(self, '_propertyasattribute', False) and \
             key not in self._excludedattr and \
             key in self.__class__.__dict__ and isinstance(self.__class__.__dict__[key], property):
            # If _propertyasattribute is True and it's a property, get its value
            return self.__class__.__dict__[key].fget(self)
        else:
            raise AttributeError(f'the {self._ftype} "{key}" does not exist')

    def hasattr(self, key):
        """Return true if the field exists, considering properties as regular attributes if allowed."""
        return key in self.__dict__ or (
            getattr(self, '_propertyasattribute', False) and
            key not in self._excludedattr and
            key in self.__class__.__dict__ and isinstance(self.__class__.__dict__[key], property)
        )

    def __getstate__(self):
        """ getstate for cooperative inheritance / duplication """
        return self.__dict__.copy()

    def __setstate__(self,state):
        """ setstate for cooperative inheritance / duplication """
        self.__dict__.update(state)

    def __getattr__(self,key):
        """ get attribute override """
        return pstr.eval(self.getattr(key))

    def __setattr__(self,key,value):
        """ set attribute override """
        self.setattr(key,value)

    def __contains__(self,item):
        """ in override """
        return self.hasattr(item)

    def keys(self):
        """ return the fields """
        # keys() is used by struct() and its iterator
        return [key for key in self.__dict__.keys() if key not in self._excludedattr]

    def keyssorted(self,reverse=True):
        """ sort keys by length() """
        klist = self.keys()
        l = [len(k) for k in klist]
        return [k for _,k in sorted(zip(l,klist),reverse=reverse)]

    def values(self):
        """ return the values """
        # values() is used by struct() and its iterator
        return [pstr.eval(value) for key,value in self.__dict__.items() if key not in self._excludedattr]

    @staticmethod
    def fromkeysvalues(keys,values,makeparam=False):
        """ struct.keysvalues(keys,values) creates a structure from keys and values
            use makeparam = True to create a param instead of struct
        """
        if keys is None: raise AttributeError("the keys must not empty")
        if not isinstance(keys,_list_types): keys = [keys]
        if not isinstance(values,_list_types): values = [values]
        nk,nv = len(keys), len(values)
        s = param() if makeparam else struct()
        if nk>0 and nv>0:
            iv = 0
            for ik in range(nk):
                s.setattr(keys[ik], values[iv])
                iv = min(nv-1,iv+1)
            for ik in range(nk,nv):
                s.setattr(f"key{ik}", values[ik])
        return s

    def items(self):
        """ return all elements as iterable key, value """
        return self.zip()

    def __getitem__(self,idx):
        """
            s[i] returns the ith element of the structure
            s[:4] returns a structure with the four first fields
            s[[1,3]] returns the second and fourth elements
        """
        if isinstance(idx,int):
            if idx<len(self):
                return self.getattr(self.keys()[idx])
            raise IndexError(f"the {self._ftype} index should be comprised between 0 and {len(self)-1}")
        elif isinstance(idx,slice):
            out = struct.fromkeysvalues(self.keys()[idx], self.values()[idx])
            if isinstance(self,paramauto):
                return paramauto(**out)
            elif isinstance(self,param):
                return param(**out)
            else:
                return out
        elif isinstance(idx,(list,tuple)):
            k,v= self.keys(), self.values()
            nk = len(k)
            s = param() if isinstance(self,param) else struct()
            for i in idx:
                if isinstance(i,int):
                    if -nk <= i < nk:  # Allow standard Python negative indexing
                        i = i % nk  # Convert negative index to positive equivalent
                        s.setattr(k[i],v[i])
                    else:
                        raise IndexError(f"idx must contain integers in range [-{nk}, {nk-1}], not {i}")
                elif isinstance(i,str):
                    if i in self:
                        s.setattr(i, self.getattr(i))
                    else:
                        raise KeyError((f'idx "{idx}" is not a valid key'))
                else:
                    TypeError("idx must contain only integers or strings")
            return s
        elif isinstance(idx,str):
            return self.getattr(idx)
        else:
            raise TypeError("The index must be an integer or a slice and not a %s" % type(idx).__name__)

    def __setitem__(self,idx,value):
        """ set the ith element of the structure  """
        if isinstance(idx,int):
            if idx<len(self):
                self.setattr(self.keys()[idx], value)
            else:
                raise IndexError(f"the {self._ftype} index should be comprised between 0 and {len(self)-1}")
        elif isinstance(idx,slice):
            k = self.keys()[idx]
            if len(value)<=1:
                for i in range(len(k)): self.setattr(k[i], value)
            elif len(k) == len(value):
                for i in range(len(k)): self.setattr(k[i], value[i])
            else:
                raise IndexError("the number of values (%d) does not match the number of elements in the slice (%d)" \
                       % (len(value),len(idx)))
        elif isinstance(idx,(list,tuple)):
            if len(value)<=1:
                for i in range(len(idx)): self[idx[i]]=value
            elif len(idx) == len(value):
                for i in range(len(idx)): self[idx[i]]=value[i]
            else:
                raise IndexError("the number of values (%d) does not match the number of indices (%d)" \
                                 % (len(value),len(idx)))

    def __len__(self):
        """ return the number of fields """
        # len() is used by struct() and its iterator
        return len(self.keys())

    def __iter__(self):
        """ struct iterator """
        # note that in the original object _iter_ is a static property not in dup
        dup = duplicate(self)
        dup._iter_ = 0
        return dup

    def __next__(self):
        """ increment iterator """
        self._iter_ += 1
        if self._iter_<=len(self):
            return self[self._iter_-1]
        self._iter_ = 0
        raise StopIteration(f"Maximum {self._ftype} iteration reached {len(self)}")

    def __add__(self, s, sortdefinitions=False, raiseerror=True, silentmode=True):
        """
        Add two structure objects, with precedence as follows:

          paramauto > param > struct

        In c = a + b, if b has a higher precedence than a then c will be of b's class,
        otherwise it will be of a's class.

        The new instance is created by copying the fields from the left-hand operand (a)
        and then updating with the fields from the right-hand operand (b).

        If self or s is of class paramauto, the current state of _needs_sorting is propagated
        but not forced to be true.

        """
        if not isinstance(s, struct):
            raise TypeError(f"the second operand must be {self._type}")

        # Define a helper to assign a precedence value.
        def get_precedence(obj):
            if isinstance(obj, paramauto):
                return 2
            elif isinstance(obj, param):
                return 1
            elif isinstance(obj, struct):
                return 0
            else:
                return 0  # fallback for unknown derivations

        # current classes
        leftprecedence = get_precedence(self)
        rightprecedence = get_precedence(s)
        # Determine which class to use for the duplicate.
        # If s (b) has a higher precedence than self (a), use s's class; otherwise, use self's.
        hi_class = self.__class__ if leftprecedence >= rightprecedence else s.__class__
        # Create a new instance of the chosen class by copying self's fields.
        dup = hi_class(**self)
        # Update with the fields from s.
        dup.update(**s)
        if sortdefinitions: # defer sorting by preserving the state of _needs_sorting
            if leftprecedence < rightprecedence == 2: # left is promoted
                dup._needs_sorting = s._needs_sorting
            elif rightprecedence < leftprecedence == 2: # right is promoted
                dup._needs_sorting = self._needs_sorting
            elif leftprecedence == rightprecedence == 2: # left and right are equivalent
                dup._needs_sorting = self._needs_sorting or s._needs_sorting
            # dup.sortdefinitions(raiseerror=raiseerror, silentmode=silentmode)
        return dup

    def __iadd__(self,s,sortdefinitions=False,raiseerror=False, silentmode=True):
        """ iadd a structure
            set sortdefintions=True to sort definitions (to maintain executability)
        """
        if not isinstance(s,struct):
            raise TypeError(f"the second operand must be {self._type}")
        self.update(**s)
        if sortdefinitions:
            self._needs_sorting = True
            # self.sortdefinitions(raiseerror=raiseerror,silentmode=silentmode)
        return self

    def __sub__(self,s):
        """ sub a structure """
        if not isinstance(s,struct):
            raise TypeError(f"the second operand must be {self._type}")
        dup = duplicate(self)
        listofkeys = dup.keys()
        for k in s.keys():
            if k in listofkeys:
                delattr(dup,k)
        return dup

    def __isub__(self,s):
        """ isub a structure """
        if not isinstance(s,struct):
            raise TypeError(f"the second operand must be {self._type}")
        listofkeys = self.keys()
        for k in s.keys():
            if k in listofkeys:
                delattr(self,k)
        return self

    def dispmax(self,content):
        """ optimize display """
        strcontent = str(content)
        if len(strcontent)>self._maxdisplay:
            nchar = round(self._maxdisplay/2)
            return strcontent[:nchar]+" [...] "+strcontent[-nchar:]
        else:
            return content

    def __repr__(self):
        """ display method """
        if self.__dict__=={}:
            print(f"empty {self._fulltype} ({self._type} object) with no {self._type}s")
            return f"empty {self._fulltype}"
        else:
            numfmt = f".{self._precision}g"
            tmp = self.eval() if self._evalfeature else []
            keylengths = [len(key) for key in self.__dict__]
            width = max(10,max(keylengths)+2)
            fmt = "%%%ss:" % width
            fmteval = fmt[:-1]+"="
            fmtcls =  fmt[:-1]+":"
            line = ( fmt % ('-'*(width-2)) ) + ( '-'*(min(40,width*5)) )
            print(line)
            for key,value in self.__dict__.items():
                if key not in self._excludedattr:
                    if isinstance(value,_numeric_types):
                        # old code (removed on 2025-01-18)
                        # if isinstance(value,pstr):
                        #     print(fmt % key,'p"'+self.dispmax(value)+'"')
                        # if isinstance(value,str) and value=="":
                        #     print(fmt % key,'""')
                        # else:
                        #     print(fmt % key,self.dispmax(value))
                        if isinstance(value,np.ndarray):
                            print(fmt % key, struct.format_array(value,numfmt=numfmt))
                        else:
                            print(fmt % key,self.dispmax(value))
                    elif isinstance(value,struct):
                        print(fmt % key,self.dispmax(value.__str__()))
                    elif isinstance(value,(type,dict)):
                        print(fmt % key,self.dispmax(str(value)))
                    else:
                        print(fmt % key,type(value))
                        print(fmtcls % "",self.dispmax(str(value)))
                    if self._evalfeature:
                        if isinstance(self,paramauto):
                            try:
                                if isinstance(value,pstr):
                                    print(fmteval % "",'p"'+self.dispmax(tmp.getattr(key))+'"')
                                elif isinstance(value,str):
                                    if value == "":
                                        print(fmteval % "",self.dispmax("<empty string>"))
                                    else:
                                        print(fmteval % "",self.dispmax(tmp.getattr(key)))
                            except Exception as err:
                                print(fmteval % "",err.message, err.args)
                        else:
                            if isinstance(value,pstr):
                                print(fmteval % "",'p"'+self.dispmax(tmp.getattr(key))+'"')
                            elif isinstance(value,str):
                                if value == "":
                                    print(fmteval % "",self.dispmax("<empty string>"))
                                else:
                                    calcvalue =tmp.getattr(key)
                                    if isinstance(calcvalue, str) and "error" in calcvalue.lower():
                                        print(fmteval % "",calcvalue)
                                    else:
                                        if isinstance(calcvalue,np.ndarray):
                                            print(fmteval % "", struct.format_array(calcvalue,numfmt=numfmt))
                                        else:
                                            print(fmteval % "",self.dispmax(calcvalue))
                            elif isinstance(value,list):
                                calcvalue =tmp.getattr(key)
                                print(fmteval % "",self.dispmax(str(calcvalue)))
            print(line)
            return f"{self._fulltype} ({self._type} object) with {len(self)} {self._ftype}s"

    def disp(self):
        """ display method """
        self.__repr__()

    def __str__(self):
        return f"{self._fulltype} ({self._type} object) with {len(self)} {self._ftype}s"

    @property
    def isempty(self):
        """ isempty is set to True for an empty structure """
        return len(self)==0

    def clear(self):
        """ clear() delete all fields while preserving the original class """
        for k in self.keys(): delattr(self,k)

    def format(self, s, escape=False, raiseerror=True):
        """
            Format a string with fields using {field} as placeholders.
            Handles expressions like ${variable1}.

            Args:
                s (str): The input string to format.
                escape (bool): If True, prevents replacing '${' with '{'.
                raiseerror (bool): If True, raises errors for missing fields.

            Note:
                NumPy vectors and matrices are converted into their text representation (default behavior)
                If expressions such ${var[1,2]} are used an error is expected, the original content will be used instead

            Returns:
                str: The formatted string.
        """
        tmp = self.np2str()
        if raiseerror:
            try:
                if escape:
                    try: # we try to evaluate with all np objects converted in to strings (default)
                        return s.format_map(AttrErrorDict(tmp.__dict__))
                    except: # if an error occurs, we use the orginal content
                        return s.format_map(AttrErrorDict(self.__dict__))
                else:
                    try: # we try to evaluate with all np objects converted in to strings (default)
                        return s.replace("${", "{").format_map(AttrErrorDict(tmp.__dict__))
                    except: # if an error occurs, we use the orginal content
                        return s.replace("${", "{").format_map(AttrErrorDict(self.__dict__))
            except AttributeError as attr_err:
                # Handle AttributeError for expressions with operators
                s_ = s.replace("{", "${")
                if self._debug:
                    print(f"WARNING: the {self._ftype} {attr_err} is undefined in '{s_}'")
                return s_  # Revert to using '${' for unresolved expressions
            except IndexError as idx_err:
                s_ = s.replace("{", "${")
                if self._debug:
                    print(f"Index Error {idx_err} in '{s_}'")
                raise IndexError from idx_err
            except Exception as other_err:
                s_ = s.replace("{", "${")
                raise RuntimeError from other_err
        else:
            if escape:
                try: # we try to evaluate with all np objects converted in to strings (default)
                    return s.format_map(AttrErrorDict(tmp.__dict__))
                except: # if an error occurs, we use the orginal content
                    return s.format_map(AttrErrorDict(self.__dict__))
            else:
                try: # we try to evaluate with all np objects converted in to strings (default)
                    return s.replace("${", "{").format_map(AttrErrorDict(tmp.__dict__))
                except:  # if an error occurs, we use the orginal content
                    return s.replace("${", "{").format_map(AttrErrorDict(self.__dict__))

    def format_legacy(self,s,escape=False,raiseerror=True):
        """
            format a string with field (use {field} as placeholders)
                s.replace(string), s.replace(string,escape=True)
                where:
                    s is a struct object
                    string is a string with possibly ${variable1}
                    escape is a flag to prevent ${} replaced by {}
        """
        if raiseerror:
            try:
                if escape:
                    return s.format(**self.__dict__)
                else:
                    return s.replace("${","{").format(**self.__dict__)
            except KeyError as kerr:
                s_ = s.replace("{","${")
                print(f"WARNING: the {self._ftype} {kerr} is undefined in '{s_}'")
                return s_ # instead of s (we put back $) - OV 2023/01/27
            except Exception as othererr:
                s_ = s.replace("{","${")
                raise RuntimeError from othererr
        else:
            if escape:
                return s.format(**self.__dict__)
            else:
                return s.replace("${","{").format(**self.__dict__)

    def fromkeys(self,keys):
        """ returns a structure from keys """
        return self+struct(**dict.fromkeys(keys,None))

    @staticmethod
    def scan(s):
        """ scan(string) scan a string for variables """
        if not isinstance(s,str): raise TypeError("scan() requires a string")
        tmp = struct()
        #return tmp.fromkeys(set(re.findall(r"\$\{(.*?)\}",s)))
        found = re.findall(r"\$\{(.*?)\}",s);
        uniq = []
        for x in found:
            if x not in uniq: uniq.append(x)
        return tmp.fromkeys(uniq)

    @staticmethod
    def isstrexpression(s):
        """ isstrexpression(string) returns true if s contains an expression  """
        if not isinstance(s,str): raise TypeError("s must a string")
        return re.search(r"\$\{.*?\}",s) is not None

    @property
    def isexpression(self):
        """ same structure with True if it is an expression """
        s = param() if isinstance(self,param) else struct()
        for k,v in self.items():
            if isinstance(v,str):
                s.setattr(k,struct.isstrexpression(v))
            else:
                s.setattr(k,False)
        return s

    @staticmethod
    def isstrdefined(s,ref):
        """ isstrdefined(string,ref) returns true if it is defined in ref  """
        if not isinstance(s,str): raise TypeError("s must a string")
        if not isinstance(ref,struct): raise TypeError("ref must be a structure")
        if struct.isstrexpression(s):
            k = struct.scan(s).keys()
            allfound,i,nk = True,0,len(k)
            while (i<nk) and allfound:
                allfound = k[i] in ref
                i += 1
            return allfound
        else:
            return False


    def isdefined(self,ref=None):
        """ isdefined(ref) returns true if it is defined in ref """
        s = param() if isinstance(self,param) else struct()
        k,v,isexpr = self.keys(), self.values(), self.isexpression.values()
        nk = len(k)
        if ref is None:
            for i in range(nk):
                if isexpr[i]:
                    s.setattr(k[i],struct.isstrdefined(v[i],self[:i]))
                else:
                    s.setattr(k[i],True)
        else:
            if not isinstance(ref,struct): raise TypeError("ref must be a structure")
            for i in range(nk):
                if isexpr[i]:
                    s.setattr(k[i],struct.isstrdefined(v[i],ref))
                else:
                    s.setattr(k[i],True)
        return s


    def sortdefinitions(self,raiseerror=True,silentmode=False):
        """ sortdefintions sorts all definitions
            so that they can be executed as param().
            If any inconsistency is found, an error message is generated.

            Flags = default values
                raiseerror=True show erros of True
                silentmode=False no warning if True
        """
        find = lambda xlist: [i for i, x in enumerate(xlist) if x]
        findnot = lambda xlist: [i for i, x in enumerate(xlist) if not x]
        k,v,isexpr =  self.keys(), self.values(), self.isexpression.values()
        istatic = findnot(isexpr)
        idynamic = find(isexpr)
        static = struct.fromkeysvalues(
            [ k[i] for i in istatic ],
            [ v[i] for i in istatic ],
            makeparam = False)
        dynamic = struct.fromkeysvalues(
            [ k[i] for i in idynamic ],
            [ v[i] for i in idynamic ],
            makeparam=False)
        current = static # make static the current structure
        nmissing, anychange, errorfound = len(dynamic), False, False
        while nmissing:
            itst, found = 0, False
            while itst<nmissing and not found:
                teststruct = current + dynamic[[itst]] # add the test field
                found = all(list(teststruct.isdefined()))
                ifound = itst
                itst += 1
            if found:
                current = teststruct # we accept the new field
                dynamic[ifound] = []
                nmissing -= 1
                anychange = True
            else:
                if raiseerror:
                    raise KeyError('unable to interpret %d/%d expressions in "%ss"' % \
                                   (nmissing,len(self),self._ftype))
                else:
                    if (not errorfound) and (not silentmode):
                        print('WARNING: unable to interpret %d/%d expressions in "%ss"' % \
                              (nmissing,len(self),self._ftype))
                    current = teststruct # we accept the new field (even if it cannot be interpreted)
                    dynamic[ifound] = []
                    nmissing -= 1
                    errorfound = True
        if anychange:
            self.clear() # reset all fields and assign them in the proper order
            k,v = current.keys(), current.values()
            for i in range(len(k)):
                self.setattr(k[i],v[i])


    def generator(self, printout=False):
        """
        Generate Python code of the equivalent structure.

        This method converts the current structure (an instance of `param`, `paramauto`, or `struct`)
        into Python code that, when executed, recreates an equivalent structure. The generated code is
        formatted with one field per line.

        By default (when `printout` is False), the generated code is returned as a raw string that starts
        directly with, for example, `param(` (or `paramauto(` or `struct(`), with no "X = " prefix or leading
        newline. When `printout` is True, the generated code is printed to standard output and includes a prefix
        "X = " to indicate the variable name.

        Parameters:
            printout (bool): If True, the generated code is printed to standard output with the "X = " prefix.
                             If False (default), the code is returned as a raw string starting with, e.g.,
                             `param(`.

        Returns:
            str: The generated Python code representing the structure (regardless of whether it was printed).
        """
        nk = len(self)
        tmp = self.np2str()
        # Compute the field format based on the maximum key length (with a minimum width of 10)
        fmt = "%%%ss =" % max(10, max([len(k) for k in self.keys()]) + 2)
        # Determine the appropriate class string for the current instance.
        if isinstance(self, param):
            classstr = "param"
        elif 'paramauto' in globals() and isinstance(self, paramauto):
            classstr = "paramauto"
        else:
            classstr = "struct"

        lines = []
        if nk == 0:
            # For an empty structure.
            if printout:
                lines.append(f"X = {classstr}()")
            else:
                lines.append(f"{classstr}()")
        else:
            # Header: include "X = " only if printing.
            if printout:
                header = f"X = {classstr}("
            else:
                header = f"{classstr}("
            lines.append(header)
            # Iterate over keys to generate each field line.
            for i, k in enumerate(self.keys()):
                v = getattr(self, k)
                if isinstance(v, np.ndarray):
                    vtmp = getattr(tmp, k)
                    field = fmt % k + " " + vtmp
                elif isinstance(v, (int, float)) or v is None:
                    field = fmt % k + " " + str(v)
                elif isinstance(v, str):
                    field = fmt % k + " " + f'"{v}"'
                elif isinstance(v, (list, tuple, dict)):
                    field = fmt % k + " " + str(v)
                else:
                    field = fmt % k + " " + "/* unsupported type */"
                # Append a comma after each field except the last one.
                if i < nk - 1:
                    field += ","
                lines.append(field)
            # Create a closing line that aligns the closing parenthesis.
            closing_line = fmt[:-1] % ")"
            lines.append(closing_line)
        result = "\n".join(lines)
        if printout:
            print(result)
            return None
        return result


    # copy and deep copy methpds for the class
    def __copy__(self):
        """ copy method """
        cls = self.__class__
        copie = cls.__new__(cls)
        copie.__dict__.update(self.__dict__)
        return copie

    def __deepcopy__(self, memo):
        """ deep copy method """
        cls = self.__class__
        copie = cls.__new__(cls)
        memo[id(self)] = copie
        for k, v in self.__dict__.items():
            setattr(copie, k, duplicatedeep(v, memo))
        return copie


    # write a file
    def write(self, file, overwrite=True, mkdir=False):
        """
            write the equivalent structure (not recursive for nested struct)
                write(filename, overwrite=True, mkdir=False)

            Parameters:
            - file: The file path to write to.
            - overwrite: Whether to overwrite the file if it exists (default: True).
            - mkdir: Whether to create the directory if it doesn't exist (default: False).
        """
        # Create a Path object for the file to handle cross-platform paths
        file_path = Path(file).resolve()

        # Check if the directory exists or if mkdir is set to True, create it
        if mkdir:
            file_path.parent.mkdir(parents=True, exist_ok=True)
        elif not file_path.parent.exists():
            raise FileNotFoundError(f"The directory {file_path.parent} does not exist.")
        # If overwrite is False and the file already exists, raise an exception
        if not overwrite and file_path.exists():
            raise FileExistsError(f"The file {file_path} already exists, and overwrite is set to False.")
        # Convert to static if needed
        if isinstance(p,(param,paramauto)):
            tmp = self.tostatic()
        else:
            tmp = self
        # Open and write to the file using the resolved path
        with file_path.open(mode="w", encoding='utf-8') as f:
            print(f"# {self._fulltype} with {len(self)} {self._ftype}s\n", file=f)
            for k, v in tmp.items():
                if v is None:
                    print(k, "=None", file=f, sep="")
                elif isinstance(v, (int, float)):
                    print(k, "=", v, file=f, sep="")
                elif isinstance(v, str):
                    print(k, '="', v, '"', file=f, sep="")
                else:
                    print(k, "=", str(v), file=f, sep="")


    # read a file
    @staticmethod
    def read(file):
        """
            read the equivalent structure
                read(filename)

            Parameters:
            - file: The file path to read from.
        """
        # Create a Path object for the file to handle cross-platform paths
        file_path = Path(file).resolve()
        # Check if the parent directory exists, otherwise raise an error
        if not file_path.parent.exists():
            raise FileNotFoundError(f"The directory {file_path.parent} does not exist.")
        # If the file does not exist, raise an exception
        if not file_path.exists():
            raise FileNotFoundError(f"The file {file_path} does not exist.")
        # Open and read the file
        with file_path.open(mode="r", encoding="utf-8") as f:
            s = struct()  # Assuming struct is defined elsewhere
            while True:
                line = f.readline()
                if not line:
                    break
                line = line.strip()
                expr = line.split(sep="=")
                if len(line) > 0 and line[0] != "#" and len(expr) > 0:
                    lhs = expr[0]
                    rhs = "".join(expr[1:]).strip()
                    if len(rhs) == 0 or rhs == "None":
                        v = None
                    else:
                        v = eval(rhs)
                    s.setattr(lhs, v)
        return s

    # argcheck
    def check(self,default):
        """
        populate fields from a default structure
            check(defaultstruct)
            missing field, None and [] values are replaced by default ones

            Note: a.check(b) is equivalent to b+a except for [] and None values
        """
        if not isinstance(default,struct):
            raise TypeError("the first argument must be a structure")
        for f in default.keys():
            ref = default.getattr(f)
            if f not in self:
                self.setattr(f, ref)
            else:
                current = self.getattr(f)
                if ((current is None)  or (current==[])) and \
                    ((ref is not None) and (ref!=[])):
                        self.setattr(f, ref)


    # update values based on key:value
    def update(self, **kwargs):
        """
        Update multiple fields at once, while protecting certain attributes.

        Parameters:
        -----------
        **kwargs : dict
            The fields to update and their new values.

        Protected attributes defined in _excludedattr are not updated.

        Usage:
        ------
        s.update(a=10, b=[1, 2, 3], new_field="new_value")
        """
        protected_attributes = getattr(self, '_excludedattr', ())
        for key, value in kwargs.items():
            if key in protected_attributes:
                print(f"Warning: Cannot update protected attribute '{key}'")
            else:
                self.setattr(key, value)


    # override () for subindexing structure with key names
    def __call__(self, *keys):
        """
        Extract a sub-structure based on the specified keys,
        keeping the same class type.

        Parameters:
        -----------
        *keys : str
            The keys for the fields to include in the sub-structure.

        Returns:
        --------
        struct
            A new instance of the same class as the original, containing
            only the specified keys.

        Usage:
        ------
        sub_struct = s('key1', 'key2', ...)
        """
        # Create a new instance of the same class
        sub_struct = self.__class__()

        # Get the full type and field type for error messages
        fulltype = getattr(self, '_fulltype', 'structure')
        ftype = getattr(self, '_ftype', 'field')

        # Add only the specified keys to the new sub-structure
        for key in keys:
            if key in self:
                sub_struct.setattr(key, self.getattr(key))
            else:
                raise KeyError(f"{fulltype} does not contain the {ftype} '{key}'.")

        return sub_struct


    def __delattr__(self, key):
        """ Delete an instance attribute if it exists and is not a class or excluded attribute. """
        if key in self._excludedattr:
            raise AttributeError(f"Cannot delete excluded attribute '{key}'")
        elif key in self.__class__.__dict__:  # Check if it's a class attribute
            raise AttributeError(f"Cannot delete class attribute '{key}'")
        elif key in self.__dict__:  # Delete only if in instance's __dict__
            del self.__dict__[key]
        else:
            raise AttributeError(f"{self._type} has no attribute '{key}'")


    # A la Matlab display method of vectors, matrices and ND-arrays
    @staticmethod
    def format_array(value,numfmt=".4g"):
        """
        Format NumPy array for display with distinctions for scalars, row/column vectors, and ND arrays.
        Recursively formats multi-dimensional arrays without introducing unwanted commas.

        Args:
            value (np.ndarray): The NumPy array to format.
            numfmt: numeric format to be used for the string conversion (default=".4g")

        Returns:
            str: A formatted string representation of the array.
        """
        dtype_str = {
            np.float64: "double",
            np.float32: "single",
            np.int32: "int32",
            np.int64: "int64",
            np.complex64: "complex single",
            np.complex128: "complex double",
        }.get(value.dtype.type, str(value.dtype))  # Default to dtype name if not in the map

        max_display = 10  # Maximum number of elements to display

        def format_recursive(arr):
            """
            Recursively formats the array based on its dimensions.

            Args:
                arr (np.ndarray): The array or sub-array to format.

            Returns:
                str: Formatted string of the array.
            """
            if arr.ndim == 0:
                return f"{arr.item()}"

            if arr.ndim == 1:
                if len(arr) <= max_display:
                    return "[" + " ".join(f"{v:{numfmt}}" for v in arr) + "]"
                else:
                    return f"[{len(arr)} elements]"

            if arr.ndim == 2:
                if arr.shape[1] == 1:
                    # Column vector
                    if arr.shape[0] <= max_display:
                        return "[" + " ".join(f"{v[0]:{numfmt}}" for v in arr) + "]T"
                    else:
                        return f"[{arr.shape[0]}×1 vector]"
                elif arr.shape[0] == 1:
                    # Row vector
                    if arr.shape[1] <= max_display:
                        return "[" + " ".join(f"{v:{numfmt}}" for v in arr[0]) + "]"
                    else:
                        return f"[1×{arr.shape[1]} vector]"
                else:
                    # General matrix
                    return f"[{arr.shape[0]}×{arr.shape[1]} matrix]"

            # For higher dimensions
            shape_str = "×".join(map(str, arr.shape))
            if arr.size <= max_display:
                # Show full content
                if arr.ndim > 2:
                    # Represent multi-dimensional arrays with nested brackets
                    return "[" + " ".join(format_recursive(subarr) for subarr in arr) + f"] ({shape_str} {dtype_str})"
            return f"[{shape_str} array ({dtype_str})]"

        if value.size == 0:
            return "[]"

        if value.ndim == 0 or value.size == 1:
            return f"{value.item()} ({dtype_str})"

        if value.ndim == 1 or value.ndim == 2:
            # Use existing logic for vectors and matrices
            if value.ndim == 1:
                if len(value) <= max_display:
                    formatted = "[" + " ".join(f"{v:{numfmt}}" for v in value) + f"] ({dtype_str})"
                else:
                    formatted = f"[{len(value)}×1 {dtype_str}]"
            elif value.ndim == 2:
                rows, cols = value.shape
                if cols == 1:  # Column vector
                    if rows <= max_display:
                        formatted = "[" + " ".join(f"{v[0]:{numfmt}}" for v in value) + f"]T ({dtype_str})"
                    else:
                        formatted = f"[{rows}×1 {dtype_str}]"
                elif rows == 1:  # Row vector
                    if cols <= max_display:
                        formatted = "[" + " ".join(f"{v:{numfmt}}" for v in value[0]) + f"] ({dtype_str})"
                    else:
                        formatted = f"[1×{cols} {dtype_str}]"
                else:  # General matrix
                    formatted = f"[{rows}×{cols} {dtype_str}]"
            return formatted

        # For higher-dimensional arrays
        if value.size <= max_display:
            formatted = format_recursive(value)
        else:
            shape_str = "×".join(map(str, value.shape))
            formatted = f"[{shape_str} array ({dtype_str})]"

        return formatted


    # convert all NumPy entries to "nestable" expressions
    def np2str(self):
        """ Convert all NumPy entries of s into their string representations, handling both lists and dictionaries. """
        out = struct()
        def format_numpy_result(value):
            """
            Converts a NumPy array or scalar into a string representation:
            - Scalars and single-element arrays (any number of dimensions) are returned as scalars without brackets.
            - Arrays with more than one element are formatted with proper nesting and commas to make them valid `np.array()` inputs.
            - If the value is a list or dict, the conversion is applied recursively.
            - Non-ndarray inputs that are not list/dict are returned without modification.

            Args:
                value (np.ndarray, scalar, list, dict, or other): The value to format.

            Returns:
                str, list, dict, or original type: A properly formatted string for NumPy arrays/scalars,
                a recursively converted list/dict, or the original value.
            """
            if isinstance(value, dict):
                # Recursively process each key in the dictionary.
                new_dict = {}
                for k, v in value.items():
                    new_dict[k] = format_numpy_result(v)
                return new_dict
            elif isinstance(value, list):
                # Recursively process each element in the list.
                return [format_numpy_result(x) for x in value]
            elif isinstance(value, tuple):
                return tuple(format_numpy_result(x) for x in value)
            elif isinstance(value, struct):
                return value.npstr()
            elif np.isscalar(value):
                # For scalars: if numeric, use str() to avoid extra quotes.
                if isinstance(value, (int, float, complex, str)) or value is None:
                    return value
                else:
                    return repr(value)
            elif isinstance(value, np.ndarray):
                # Check if the array has exactly one element.
                if value.size == 1:
                    # Extract the scalar value.
                    return repr(value.item())
                # Convert the array to a nested list.
                nested_list = value.tolist()
                # Recursively format the nested list into a valid string.
                def list_to_string(lst):
                    if isinstance(lst, list):
                        return "[" + ",".join(list_to_string(item) for item in lst) + "]"
                    else:
                        return repr(lst)
                return list_to_string(nested_list)
            else:
                # Return the input unmodified if not a NumPy array, list, dict, or scalar.
                return str(value) # str() preferred over repr() for concision
        # Process all entries in self.
        for key, value in self.items():
            out.setattr(key, format_numpy_result(value))
        return out

    # minimal replacement of placeholders by numbers or their string representations
    def numrepl(self, text):
        r"""
        Replace all placeholders of the form ${key} in the given text by the corresponding
        numeric value from the instance fields, under the following conditions:

        1. 'key' must be a valid field in self (i.e., if key in self).
        2. The value corresponding to 'key' is either:
             - an int,
             - a float, or
             - a string that represents a valid number (e.g., "1" or "1.0").

        Only when these conditions are met, the placeholder is substituted.
        The conversion preserves the original type: if the stored value is int, then the
        substitution will be done as an integer (e.g., 1 and not 1.0). Otherwise, if it is
        a float then it will be substituted as a float.

        Any placeholder for which the above conditions are not met remains unchanged.

        Placeholders are recognized by the pattern "${<key>}" where <key> is captured as all
        text until the next "}" (optionally allowing whitespace inside the braces).
        """
        # Pattern: match "${", then optional whitespace, capture all characters until "}",
        # then optional whitespace, then "}".
        placeholder_pattern = re.compile(r"\$\{\s*([^}]+?)\s*\}")

        def replace_match(match):
            key = match.group(1)
            # Check if the key exists in self.
            if key in self:
                value = self[key]
                # If the value is already numeric, substitute directly.
                if isinstance(value, (int, float)):
                    return str(value)
                # If the value is a string, try to interpret it as a numeric value.
                elif isinstance(value, str):
                    s = value.strip()
                    # Check if s is a valid integer representation.
                    if re.fullmatch(r"[+-]?\d+", s):
                        try:
                            num = int(s)
                            return str(num)
                        except ValueError:
                            # Should not occur because the regex already matched.
                            return match.group(0)
                    # Check if s is a valid float representation (including scientific notation).
                    elif re.fullmatch(r"[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?", s):
                        try:
                            num = float(s)
                            return str(num)
                        except ValueError:
                            return match.group(0)
            # If key not in self or value is not numeric (or numeric string), leave placeholder intact.
            return match.group(0)

        # Replace all placeholders in the text using the replacer function.
        return placeholder_pattern.sub(replace_match, text)

    # import method
    def importfrom(self, s, nonempty=True, replacedefaultvar=True):
        """
        Import values from 's' into self according to the following rules:

        - Only fields that already exist in self are considered.
        - If s is a dictionary, it is converted to a struct via struct(**s).
        - If the current value of a field in self is empty (None, "", [] or ()),
          then that field is updated from s.
        - If nonempty is True (default), then only non-empty values from s are imported.
        - If replacedefaultvar is True (default), then if a field in self exactly equals
          "${key}" (with key being the field name), it is replaced by the corresponding
          value from s if it is empty.
        - Raises a TypeError if s is not a dict or struct (i.e. if it doesn’t support keys()).
        """
        # If s is a dictionary, convert it to a struct instance.
        if isinstance(s, dict):
            s = struct(**s)
        elif not hasattr(s, "keys"):
            raise TypeError(f"s must be a struct or a dictionary not a type {type(s).__name__}")

        for key in self.keys():
            if key in s:
                s_value = getattr(s, key)
                current_value = getattr(self, key)
                if is_empty(current_value) or (replacedefaultvar and current_value == "${" + key + "}"):
                    if nonempty:
                        if not is_empty(s_value):
                            setattr(self, key, s_value)
                    else:
                        setattr(self, key, s_value)

    # importfrom with copy
    def __lshift__(self, other):
        """
        Allows the syntax:

            s = s1 << s2

        where a new instance is created as a copy of s1 (preserving its type, whether
        struct, param, or paramauto) and then updated with the values from s2 using
        importfrom.
        """
        # Create a new instance preserving the type of self.
        new_instance = type(self)(**{k: getattr(self, k) for k in self.keys()})
        # Import values from other (s2) into the new instance.
        new_instance.importfrom(other)
        return new_instance

    # returns only valid keys
    def validkeys(self, list_of_keys):
        """
        Validate and return the subset of keys from the provided list that are valid in the instance.

        Parameters:
        -----------
        list_of_keys : list
            A list of keys (as strings) to check against the instance’s attributes.

        Returns:
        --------
        list
            A list of keys from list_of_keys that are valid (i.e., exist as attributes in the instance).

        Raises:
        -------
        TypeError
            If list_of_keys is not a list or if any element in list_of_keys is not a string.

        Example:
        --------
        >>> s = struct()
        >>> s.foo = 42
        >>> s.bar = "hello"
        >>> valid = s.validkeys(["foo", "bar", "baz"])
        >>> print(valid)   # Output: ['foo', 'bar'] assuming 'baz' is not defined in s
        """
        # Check that list_of_keys is a list
        if not isinstance(list_of_keys, list):
            raise TypeError("list_of_keys must be a list")

        # Check that every entry in the list is a string
        for key in list_of_keys:
            if not isinstance(key, str):
                raise TypeError("Each key in list_of_keys must be a string")

        # Assuming valid keys are those present in the instance's __dict__
        return [key for key in list_of_keys if key in self]

Subclasses

  • pizza.private.mstruct.param
  • pizza.raster.collection
  • pizza.region.regioncollection
  • pizza.script.scriptobject
  • pizza.script.scriptobjectgroup

Static methods

def dict2struct(dico, makeparam=False)

create a structure from a dictionary

Expand source code
@staticmethod
def dict2struct(dico,makeparam=False):
    """ create a structure from a dictionary """
    if isinstance(dico,dict):
        s = param() if makeparam else struct()
        s.set(**dico)
        return s
    raise TypeError("the argument must be a dictionary")
def format_array(value, numfmt='.4g')

Format NumPy array for display with distinctions for scalars, row/column vectors, and ND arrays. Recursively formats multi-dimensional arrays without introducing unwanted commas.

Args

value : np.ndarray
The NumPy array to format.
numfmt
numeric format to be used for the string conversion (default=".4g")

Returns

str
A formatted string representation of the array.
Expand source code
@staticmethod
def format_array(value,numfmt=".4g"):
    """
    Format NumPy array for display with distinctions for scalars, row/column vectors, and ND arrays.
    Recursively formats multi-dimensional arrays without introducing unwanted commas.

    Args:
        value (np.ndarray): The NumPy array to format.
        numfmt: numeric format to be used for the string conversion (default=".4g")

    Returns:
        str: A formatted string representation of the array.
    """
    dtype_str = {
        np.float64: "double",
        np.float32: "single",
        np.int32: "int32",
        np.int64: "int64",
        np.complex64: "complex single",
        np.complex128: "complex double",
    }.get(value.dtype.type, str(value.dtype))  # Default to dtype name if not in the map

    max_display = 10  # Maximum number of elements to display

    def format_recursive(arr):
        """
        Recursively formats the array based on its dimensions.

        Args:
            arr (np.ndarray): The array or sub-array to format.

        Returns:
            str: Formatted string of the array.
        """
        if arr.ndim == 0:
            return f"{arr.item()}"

        if arr.ndim == 1:
            if len(arr) <= max_display:
                return "[" + " ".join(f"{v:{numfmt}}" for v in arr) + "]"
            else:
                return f"[{len(arr)} elements]"

        if arr.ndim == 2:
            if arr.shape[1] == 1:
                # Column vector
                if arr.shape[0] <= max_display:
                    return "[" + " ".join(f"{v[0]:{numfmt}}" for v in arr) + "]T"
                else:
                    return f"[{arr.shape[0]}×1 vector]"
            elif arr.shape[0] == 1:
                # Row vector
                if arr.shape[1] <= max_display:
                    return "[" + " ".join(f"{v:{numfmt}}" for v in arr[0]) + "]"
                else:
                    return f"[1×{arr.shape[1]} vector]"
            else:
                # General matrix
                return f"[{arr.shape[0]}×{arr.shape[1]} matrix]"

        # For higher dimensions
        shape_str = "×".join(map(str, arr.shape))
        if arr.size <= max_display:
            # Show full content
            if arr.ndim > 2:
                # Represent multi-dimensional arrays with nested brackets
                return "[" + " ".join(format_recursive(subarr) for subarr in arr) + f"] ({shape_str} {dtype_str})"
        return f"[{shape_str} array ({dtype_str})]"

    if value.size == 0:
        return "[]"

    if value.ndim == 0 or value.size == 1:
        return f"{value.item()} ({dtype_str})"

    if value.ndim == 1 or value.ndim == 2:
        # Use existing logic for vectors and matrices
        if value.ndim == 1:
            if len(value) <= max_display:
                formatted = "[" + " ".join(f"{v:{numfmt}}" for v in value) + f"] ({dtype_str})"
            else:
                formatted = f"[{len(value)}×1 {dtype_str}]"
        elif value.ndim == 2:
            rows, cols = value.shape
            if cols == 1:  # Column vector
                if rows <= max_display:
                    formatted = "[" + " ".join(f"{v[0]:{numfmt}}" for v in value) + f"]T ({dtype_str})"
                else:
                    formatted = f"[{rows}×1 {dtype_str}]"
            elif rows == 1:  # Row vector
                if cols <= max_display:
                    formatted = "[" + " ".join(f"{v:{numfmt}}" for v in value[0]) + f"] ({dtype_str})"
                else:
                    formatted = f"[1×{cols} {dtype_str}]"
            else:  # General matrix
                formatted = f"[{rows}×{cols} {dtype_str}]"
        return formatted

    # For higher-dimensional arrays
    if value.size <= max_display:
        formatted = format_recursive(value)
    else:
        shape_str = "×".join(map(str, value.shape))
        formatted = f"[{shape_str} array ({dtype_str})]"

    return formatted
def fromkeysvalues(keys, values, makeparam=False)

struct.keysvalues(keys,values) creates a structure from keys and values use makeparam = True to create a param instead of struct

Expand source code
@staticmethod
def fromkeysvalues(keys,values,makeparam=False):
    """ struct.keysvalues(keys,values) creates a structure from keys and values
        use makeparam = True to create a param instead of struct
    """
    if keys is None: raise AttributeError("the keys must not empty")
    if not isinstance(keys,_list_types): keys = [keys]
    if not isinstance(values,_list_types): values = [values]
    nk,nv = len(keys), len(values)
    s = param() if makeparam else struct()
    if nk>0 and nv>0:
        iv = 0
        for ik in range(nk):
            s.setattr(keys[ik], values[iv])
            iv = min(nv-1,iv+1)
        for ik in range(nk,nv):
            s.setattr(f"key{ik}", values[ik])
    return s
def isstrdefined(s, ref)

isstrdefined(string,ref) returns true if it is defined in ref

Expand source code
@staticmethod
def isstrdefined(s,ref):
    """ isstrdefined(string,ref) returns true if it is defined in ref  """
    if not isinstance(s,str): raise TypeError("s must a string")
    if not isinstance(ref,struct): raise TypeError("ref must be a structure")
    if struct.isstrexpression(s):
        k = struct.scan(s).keys()
        allfound,i,nk = True,0,len(k)
        while (i<nk) and allfound:
            allfound = k[i] in ref
            i += 1
        return allfound
    else:
        return False
def isstrexpression(s)

isstrexpression(string) returns true if s contains an expression

Expand source code
@staticmethod
def isstrexpression(s):
    """ isstrexpression(string) returns true if s contains an expression  """
    if not isinstance(s,str): raise TypeError("s must a string")
    return re.search(r"\$\{.*?\}",s) is not None
def read(file)

read the equivalent structure read(filename)

Parameters: - file: The file path to read from.

Expand source code
@staticmethod
def read(file):
    """
        read the equivalent structure
            read(filename)

        Parameters:
        - file: The file path to read from.
    """
    # Create a Path object for the file to handle cross-platform paths
    file_path = Path(file).resolve()
    # Check if the parent directory exists, otherwise raise an error
    if not file_path.parent.exists():
        raise FileNotFoundError(f"The directory {file_path.parent} does not exist.")
    # If the file does not exist, raise an exception
    if not file_path.exists():
        raise FileNotFoundError(f"The file {file_path} does not exist.")
    # Open and read the file
    with file_path.open(mode="r", encoding="utf-8") as f:
        s = struct()  # Assuming struct is defined elsewhere
        while True:
            line = f.readline()
            if not line:
                break
            line = line.strip()
            expr = line.split(sep="=")
            if len(line) > 0 and line[0] != "#" and len(expr) > 0:
                lhs = expr[0]
                rhs = "".join(expr[1:]).strip()
                if len(rhs) == 0 or rhs == "None":
                    v = None
                else:
                    v = eval(rhs)
                s.setattr(lhs, v)
    return s
def scan(s)

scan(string) scan a string for variables

Expand source code
@staticmethod
def scan(s):
    """ scan(string) scan a string for variables """
    if not isinstance(s,str): raise TypeError("scan() requires a string")
    tmp = struct()
    #return tmp.fromkeys(set(re.findall(r"\$\{(.*?)\}",s)))
    found = re.findall(r"\$\{(.*?)\}",s);
    uniq = []
    for x in found:
        if x not in uniq: uniq.append(x)
    return tmp.fromkeys(uniq)

Instance variables

var isempty

isempty is set to True for an empty structure

Expand source code
@property
def isempty(self):
    """ isempty is set to True for an empty structure """
    return len(self)==0
var isexpression

same structure with True if it is an expression

Expand source code
@property
def isexpression(self):
    """ same structure with True if it is an expression """
    s = param() if isinstance(self,param) else struct()
    for k,v in self.items():
        if isinstance(v,str):
            s.setattr(k,struct.isstrexpression(v))
        else:
            s.setattr(k,False)
    return s

Methods

def check(self, default)

populate fields from a default structure check(defaultstruct) missing field, None and [] values are replaced by default ones

Note: a.check(b) is equivalent to b+a except for [] and None values
Expand source code
def check(self,default):
    """
    populate fields from a default structure
        check(defaultstruct)
        missing field, None and [] values are replaced by default ones

        Note: a.check(b) is equivalent to b+a except for [] and None values
    """
    if not isinstance(default,struct):
        raise TypeError("the first argument must be a structure")
    for f in default.keys():
        ref = default.getattr(f)
        if f not in self:
            self.setattr(f, ref)
        else:
            current = self.getattr(f)
            if ((current is None)  or (current==[])) and \
                ((ref is not None) and (ref!=[])):
                    self.setattr(f, ref)
def clear(self)

clear() delete all fields while preserving the original class

Expand source code
def clear(self):
    """ clear() delete all fields while preserving the original class """
    for k in self.keys(): delattr(self,k)
def disp(self)

display method

Expand source code
def disp(self):
    """ display method """
    self.__repr__()
def dispmax(self, content)

optimize display

Expand source code
def dispmax(self,content):
    """ optimize display """
    strcontent = str(content)
    if len(strcontent)>self._maxdisplay:
        nchar = round(self._maxdisplay/2)
        return strcontent[:nchar]+" [...] "+strcontent[-nchar:]
    else:
        return content
def format(self, s, escape=False, raiseerror=True)

Format a string with fields using {field} as placeholders. Handles expressions like ${variable1}.

Args

s : str
The input string to format.
escape : bool
If True, prevents replacing '${' with '{'.
raiseerror : bool
If True, raises errors for missing fields.

Note

NumPy vectors and matrices are converted into their text representation (default behavior) If expressions such ${var[1,2]} are used an error is expected, the original content will be used instead

Returns

str
The formatted string.
Expand source code
def format(self, s, escape=False, raiseerror=True):
    """
        Format a string with fields using {field} as placeholders.
        Handles expressions like ${variable1}.

        Args:
            s (str): The input string to format.
            escape (bool): If True, prevents replacing '${' with '{'.
            raiseerror (bool): If True, raises errors for missing fields.

        Note:
            NumPy vectors and matrices are converted into their text representation (default behavior)
            If expressions such ${var[1,2]} are used an error is expected, the original content will be used instead

        Returns:
            str: The formatted string.
    """
    tmp = self.np2str()
    if raiseerror:
        try:
            if escape:
                try: # we try to evaluate with all np objects converted in to strings (default)
                    return s.format_map(AttrErrorDict(tmp.__dict__))
                except: # if an error occurs, we use the orginal content
                    return s.format_map(AttrErrorDict(self.__dict__))
            else:
                try: # we try to evaluate with all np objects converted in to strings (default)
                    return s.replace("${", "{").format_map(AttrErrorDict(tmp.__dict__))
                except: # if an error occurs, we use the orginal content
                    return s.replace("${", "{").format_map(AttrErrorDict(self.__dict__))
        except AttributeError as attr_err:
            # Handle AttributeError for expressions with operators
            s_ = s.replace("{", "${")
            if self._debug:
                print(f"WARNING: the {self._ftype} {attr_err} is undefined in '{s_}'")
            return s_  # Revert to using '${' for unresolved expressions
        except IndexError as idx_err:
            s_ = s.replace("{", "${")
            if self._debug:
                print(f"Index Error {idx_err} in '{s_}'")
            raise IndexError from idx_err
        except Exception as other_err:
            s_ = s.replace("{", "${")
            raise RuntimeError from other_err
    else:
        if escape:
            try: # we try to evaluate with all np objects converted in to strings (default)
                return s.format_map(AttrErrorDict(tmp.__dict__))
            except: # if an error occurs, we use the orginal content
                return s.format_map(AttrErrorDict(self.__dict__))
        else:
            try: # we try to evaluate with all np objects converted in to strings (default)
                return s.replace("${", "{").format_map(AttrErrorDict(tmp.__dict__))
            except:  # if an error occurs, we use the orginal content
                return s.replace("${", "{").format_map(AttrErrorDict(self.__dict__))
def format_legacy(self, s, escape=False, raiseerror=True)

format a string with field (use {field} as placeholders) s.replace(string), s.replace(string,escape=True) where: s is a struct object string is a string with possibly ${variable1} escape is a flag to prevent ${} replaced by {}

Expand source code
def format_legacy(self,s,escape=False,raiseerror=True):
    """
        format a string with field (use {field} as placeholders)
            s.replace(string), s.replace(string,escape=True)
            where:
                s is a struct object
                string is a string with possibly ${variable1}
                escape is a flag to prevent ${} replaced by {}
    """
    if raiseerror:
        try:
            if escape:
                return s.format(**self.__dict__)
            else:
                return s.replace("${","{").format(**self.__dict__)
        except KeyError as kerr:
            s_ = s.replace("{","${")
            print(f"WARNING: the {self._ftype} {kerr} is undefined in '{s_}'")
            return s_ # instead of s (we put back $) - OV 2023/01/27
        except Exception as othererr:
            s_ = s.replace("{","${")
            raise RuntimeError from othererr
    else:
        if escape:
            return s.format(**self.__dict__)
        else:
            return s.replace("${","{").format(**self.__dict__)
def fromkeys(self, keys)

returns a structure from keys

Expand source code
def fromkeys(self,keys):
    """ returns a structure from keys """
    return self+struct(**dict.fromkeys(keys,None))
def generator(self, printout=False)

Generate Python code of the equivalent structure.

This method converts the current structure (an instance of param, paramauto, or struct) into Python code that, when executed, recreates an equivalent structure. The generated code is formatted with one field per line.

By default (when printout is False), the generated code is returned as a raw string that starts directly with, for example, param( (or paramauto( or struct(), with no "X = " prefix or leading newline. When printout is True, the generated code is printed to standard output and includes a prefix "X = " to indicate the variable name.

Parameters

printout (bool): If True, the generated code is printed to standard output with the "X = " prefix. If False (default), the code is returned as a raw string starting with, e.g., param(.

Returns

str
The generated Python code representing the structure (regardless of whether it was printed).
Expand source code
def generator(self, printout=False):
    """
    Generate Python code of the equivalent structure.

    This method converts the current structure (an instance of `param`, `paramauto`, or `struct`)
    into Python code that, when executed, recreates an equivalent structure. The generated code is
    formatted with one field per line.

    By default (when `printout` is False), the generated code is returned as a raw string that starts
    directly with, for example, `param(` (or `paramauto(` or `struct(`), with no "X = " prefix or leading
    newline. When `printout` is True, the generated code is printed to standard output and includes a prefix
    "X = " to indicate the variable name.

    Parameters:
        printout (bool): If True, the generated code is printed to standard output with the "X = " prefix.
                         If False (default), the code is returned as a raw string starting with, e.g.,
                         `param(`.

    Returns:
        str: The generated Python code representing the structure (regardless of whether it was printed).
    """
    nk = len(self)
    tmp = self.np2str()
    # Compute the field format based on the maximum key length (with a minimum width of 10)
    fmt = "%%%ss =" % max(10, max([len(k) for k in self.keys()]) + 2)
    # Determine the appropriate class string for the current instance.
    if isinstance(self, param):
        classstr = "param"
    elif 'paramauto' in globals() and isinstance(self, paramauto):
        classstr = "paramauto"
    else:
        classstr = "struct"

    lines = []
    if nk == 0:
        # For an empty structure.
        if printout:
            lines.append(f"X = {classstr}()")
        else:
            lines.append(f"{classstr}()")
    else:
        # Header: include "X = " only if printing.
        if printout:
            header = f"X = {classstr}("
        else:
            header = f"{classstr}("
        lines.append(header)
        # Iterate over keys to generate each field line.
        for i, k in enumerate(self.keys()):
            v = getattr(self, k)
            if isinstance(v, np.ndarray):
                vtmp = getattr(tmp, k)
                field = fmt % k + " " + vtmp
            elif isinstance(v, (int, float)) or v is None:
                field = fmt % k + " " + str(v)
            elif isinstance(v, str):
                field = fmt % k + " " + f'"{v}"'
            elif isinstance(v, (list, tuple, dict)):
                field = fmt % k + " " + str(v)
            else:
                field = fmt % k + " " + "/* unsupported type */"
            # Append a comma after each field except the last one.
            if i < nk - 1:
                field += ","
            lines.append(field)
        # Create a closing line that aligns the closing parenthesis.
        closing_line = fmt[:-1] % ")"
        lines.append(closing_line)
    result = "\n".join(lines)
    if printout:
        print(result)
        return None
    return result
def getattr(self, key)

Get attribute override to access both instance attributes and properties if allowed.

Expand source code
def getattr(self,key):
    """Get attribute override to access both instance attributes and properties if allowed."""
    if key in self.__dict__:
        return self.__dict__[key]
    elif getattr(self, '_propertyasattribute', False) and \
         key not in self._excludedattr and \
         key in self.__class__.__dict__ and isinstance(self.__class__.__dict__[key], property):
        # If _propertyasattribute is True and it's a property, get its value
        return self.__class__.__dict__[key].fget(self)
    else:
        raise AttributeError(f'the {self._ftype} "{key}" does not exist')
def hasattr(self, key)

Return true if the field exists, considering properties as regular attributes if allowed.

Expand source code
def hasattr(self, key):
    """Return true if the field exists, considering properties as regular attributes if allowed."""
    return key in self.__dict__ or (
        getattr(self, '_propertyasattribute', False) and
        key not in self._excludedattr and
        key in self.__class__.__dict__ and isinstance(self.__class__.__dict__[key], property)
    )
def importfrom(self, s, nonempty=True, replacedefaultvar=True)

Import values from 's' into self according to the following rules:

  • Only fields that already exist in self are considered.
  • If s is a dictionary, it is converted to a struct via struct(**s).
  • If the current value of a field in self is empty (None, "", [] or ()), then that field is updated from s.
  • If nonempty is True (default), then only non-empty values from s are imported.
  • If replacedefaultvar is True (default), then if a field in self exactly equals "${key}" (with key being the field name), it is replaced by the corresponding value from s if it is empty.
  • Raises a TypeError if s is not a dict or struct (i.e. if it doesn’t support keys()).
Expand source code
def importfrom(self, s, nonempty=True, replacedefaultvar=True):
    """
    Import values from 's' into self according to the following rules:

    - Only fields that already exist in self are considered.
    - If s is a dictionary, it is converted to a struct via struct(**s).
    - If the current value of a field in self is empty (None, "", [] or ()),
      then that field is updated from s.
    - If nonempty is True (default), then only non-empty values from s are imported.
    - If replacedefaultvar is True (default), then if a field in self exactly equals
      "${key}" (with key being the field name), it is replaced by the corresponding
      value from s if it is empty.
    - Raises a TypeError if s is not a dict or struct (i.e. if it doesn’t support keys()).
    """
    # If s is a dictionary, convert it to a struct instance.
    if isinstance(s, dict):
        s = struct(**s)
    elif not hasattr(s, "keys"):
        raise TypeError(f"s must be a struct or a dictionary not a type {type(s).__name__}")

    for key in self.keys():
        if key in s:
            s_value = getattr(s, key)
            current_value = getattr(self, key)
            if is_empty(current_value) or (replacedefaultvar and current_value == "${" + key + "}"):
                if nonempty:
                    if not is_empty(s_value):
                        setattr(self, key, s_value)
                else:
                    setattr(self, key, s_value)
def isdefined(self, ref=None)

isdefined(ref) returns true if it is defined in ref

Expand source code
def isdefined(self,ref=None):
    """ isdefined(ref) returns true if it is defined in ref """
    s = param() if isinstance(self,param) else struct()
    k,v,isexpr = self.keys(), self.values(), self.isexpression.values()
    nk = len(k)
    if ref is None:
        for i in range(nk):
            if isexpr[i]:
                s.setattr(k[i],struct.isstrdefined(v[i],self[:i]))
            else:
                s.setattr(k[i],True)
    else:
        if not isinstance(ref,struct): raise TypeError("ref must be a structure")
        for i in range(nk):
            if isexpr[i]:
                s.setattr(k[i],struct.isstrdefined(v[i],ref))
            else:
                s.setattr(k[i],True)
    return s
def items(self)

return all elements as iterable key, value

Expand source code
def items(self):
    """ return all elements as iterable key, value """
    return self.zip()
def keys(self)

return the fields

Expand source code
def keys(self):
    """ return the fields """
    # keys() is used by struct() and its iterator
    return [key for key in self.__dict__.keys() if key not in self._excludedattr]
def keyssorted(self, reverse=True)

sort keys by length()

Expand source code
def keyssorted(self,reverse=True):
    """ sort keys by length() """
    klist = self.keys()
    l = [len(k) for k in klist]
    return [k for _,k in sorted(zip(l,klist),reverse=reverse)]
def np2str(self)

Convert all NumPy entries of s into their string representations, handling both lists and dictionaries.

Expand source code
def np2str(self):
    """ Convert all NumPy entries of s into their string representations, handling both lists and dictionaries. """
    out = struct()
    def format_numpy_result(value):
        """
        Converts a NumPy array or scalar into a string representation:
        - Scalars and single-element arrays (any number of dimensions) are returned as scalars without brackets.
        - Arrays with more than one element are formatted with proper nesting and commas to make them valid `np.array()` inputs.
        - If the value is a list or dict, the conversion is applied recursively.
        - Non-ndarray inputs that are not list/dict are returned without modification.

        Args:
            value (np.ndarray, scalar, list, dict, or other): The value to format.

        Returns:
            str, list, dict, or original type: A properly formatted string for NumPy arrays/scalars,
            a recursively converted list/dict, or the original value.
        """
        if isinstance(value, dict):
            # Recursively process each key in the dictionary.
            new_dict = {}
            for k, v in value.items():
                new_dict[k] = format_numpy_result(v)
            return new_dict
        elif isinstance(value, list):
            # Recursively process each element in the list.
            return [format_numpy_result(x) for x in value]
        elif isinstance(value, tuple):
            return tuple(format_numpy_result(x) for x in value)
        elif isinstance(value, struct):
            return value.npstr()
        elif np.isscalar(value):
            # For scalars: if numeric, use str() to avoid extra quotes.
            if isinstance(value, (int, float, complex, str)) or value is None:
                return value
            else:
                return repr(value)
        elif isinstance(value, np.ndarray):
            # Check if the array has exactly one element.
            if value.size == 1:
                # Extract the scalar value.
                return repr(value.item())
            # Convert the array to a nested list.
            nested_list = value.tolist()
            # Recursively format the nested list into a valid string.
            def list_to_string(lst):
                if isinstance(lst, list):
                    return "[" + ",".join(list_to_string(item) for item in lst) + "]"
                else:
                    return repr(lst)
            return list_to_string(nested_list)
        else:
            # Return the input unmodified if not a NumPy array, list, dict, or scalar.
            return str(value) # str() preferred over repr() for concision
    # Process all entries in self.
    for key, value in self.items():
        out.setattr(key, format_numpy_result(value))
    return out
def numrepl(self, text)

Replace all placeholders of the form ${key} in the given text by the corresponding numeric value from the instance fields, under the following conditions:

  1. 'key' must be a valid field in self (i.e., if key in self).
  2. The value corresponding to 'key' is either:
    • an int,
    • a float, or
    • a string that represents a valid number (e.g., "1" or "1.0").

Only when these conditions are met, the placeholder is substituted. The conversion preserves the original type: if the stored value is int, then the substitution will be done as an integer (e.g., 1 and not 1.0). Otherwise, if it is a float then it will be substituted as a float.

Any placeholder for which the above conditions are not met remains unchanged.

Placeholders are recognized by the pattern "${}" where is captured as all text until the next "}" (optionally allowing whitespace inside the braces).

Expand source code
def numrepl(self, text):
    r"""
    Replace all placeholders of the form ${key} in the given text by the corresponding
    numeric value from the instance fields, under the following conditions:

    1. 'key' must be a valid field in self (i.e., if key in self).
    2. The value corresponding to 'key' is either:
         - an int,
         - a float, or
         - a string that represents a valid number (e.g., "1" or "1.0").

    Only when these conditions are met, the placeholder is substituted.
    The conversion preserves the original type: if the stored value is int, then the
    substitution will be done as an integer (e.g., 1 and not 1.0). Otherwise, if it is
    a float then it will be substituted as a float.

    Any placeholder for which the above conditions are not met remains unchanged.

    Placeholders are recognized by the pattern "${<key>}" where <key> is captured as all
    text until the next "}" (optionally allowing whitespace inside the braces).
    """
    # Pattern: match "${", then optional whitespace, capture all characters until "}",
    # then optional whitespace, then "}".
    placeholder_pattern = re.compile(r"\$\{\s*([^}]+?)\s*\}")

    def replace_match(match):
        key = match.group(1)
        # Check if the key exists in self.
        if key in self:
            value = self[key]
            # If the value is already numeric, substitute directly.
            if isinstance(value, (int, float)):
                return str(value)
            # If the value is a string, try to interpret it as a numeric value.
            elif isinstance(value, str):
                s = value.strip()
                # Check if s is a valid integer representation.
                if re.fullmatch(r"[+-]?\d+", s):
                    try:
                        num = int(s)
                        return str(num)
                    except ValueError:
                        # Should not occur because the regex already matched.
                        return match.group(0)
                # Check if s is a valid float representation (including scientific notation).
                elif re.fullmatch(r"[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?", s):
                    try:
                        num = float(s)
                        return str(num)
                    except ValueError:
                        return match.group(0)
        # If key not in self or value is not numeric (or numeric string), leave placeholder intact.
        return match.group(0)

    # Replace all placeholders in the text using the replacer function.
    return placeholder_pattern.sub(replace_match, text)
def set(self, **kwargs)

initialization

Expand source code
def set(self,**kwargs):
    """ initialization """
    self.__dict__.update(kwargs)
def setattr(self, key, value)

set field and value

Expand source code
def setattr(self,key,value):
    """ set field and value """
    if isinstance(value,list) and len(value)==0 and key in self:
        delattr(self, key)
    else:
        self.__dict__[key] = value
def sortdefinitions(self, raiseerror=True, silentmode=False)

sortdefintions sorts all definitions so that they can be executed as param(). If any inconsistency is found, an error message is generated.

Flags = default values raiseerror=True show erros of True silentmode=False no warning if True

Expand source code
def sortdefinitions(self,raiseerror=True,silentmode=False):
    """ sortdefintions sorts all definitions
        so that they can be executed as param().
        If any inconsistency is found, an error message is generated.

        Flags = default values
            raiseerror=True show erros of True
            silentmode=False no warning if True
    """
    find = lambda xlist: [i for i, x in enumerate(xlist) if x]
    findnot = lambda xlist: [i for i, x in enumerate(xlist) if not x]
    k,v,isexpr =  self.keys(), self.values(), self.isexpression.values()
    istatic = findnot(isexpr)
    idynamic = find(isexpr)
    static = struct.fromkeysvalues(
        [ k[i] for i in istatic ],
        [ v[i] for i in istatic ],
        makeparam = False)
    dynamic = struct.fromkeysvalues(
        [ k[i] for i in idynamic ],
        [ v[i] for i in idynamic ],
        makeparam=False)
    current = static # make static the current structure
    nmissing, anychange, errorfound = len(dynamic), False, False
    while nmissing:
        itst, found = 0, False
        while itst<nmissing and not found:
            teststruct = current + dynamic[[itst]] # add the test field
            found = all(list(teststruct.isdefined()))
            ifound = itst
            itst += 1
        if found:
            current = teststruct # we accept the new field
            dynamic[ifound] = []
            nmissing -= 1
            anychange = True
        else:
            if raiseerror:
                raise KeyError('unable to interpret %d/%d expressions in "%ss"' % \
                               (nmissing,len(self),self._ftype))
            else:
                if (not errorfound) and (not silentmode):
                    print('WARNING: unable to interpret %d/%d expressions in "%ss"' % \
                          (nmissing,len(self),self._ftype))
                current = teststruct # we accept the new field (even if it cannot be interpreted)
                dynamic[ifound] = []
                nmissing -= 1
                errorfound = True
    if anychange:
        self.clear() # reset all fields and assign them in the proper order
        k,v = current.keys(), current.values()
        for i in range(len(k)):
            self.setattr(k[i],v[i])
def struct2dict(self)

create a dictionary from the current structure

Expand source code
def struct2dict(self):
    """ create a dictionary from the current structure """
    return dict(self.zip())
def struct2param(self, protection=False, evaluation=True)

convert an object struct() to param()

Expand source code
def struct2param(self,protection=False,evaluation=True):
    """ convert an object struct() to param() """
    p = param(**self.struct2dict())
    for i in range(len(self)):
        if isinstance(self[i],pstr): p[i] = pstr(p[i])
    p._protection = protection
    p._evaluation = evaluation
    return p
def update(self, **kwargs)

Update multiple fields at once, while protecting certain attributes.

Parameters:

**kwargs : dict The fields to update and their new values.

Protected attributes defined in _excludedattr are not updated.

Usage:

s.update(a=10, b=[1, 2, 3], new_field="new_value")

Expand source code
def update(self, **kwargs):
    """
    Update multiple fields at once, while protecting certain attributes.

    Parameters:
    -----------
    **kwargs : dict
        The fields to update and their new values.

    Protected attributes defined in _excludedattr are not updated.

    Usage:
    ------
    s.update(a=10, b=[1, 2, 3], new_field="new_value")
    """
    protected_attributes = getattr(self, '_excludedattr', ())
    for key, value in kwargs.items():
        if key in protected_attributes:
            print(f"Warning: Cannot update protected attribute '{key}'")
        else:
            self.setattr(key, value)
def validkeys(self, list_of_keys)

Validate and return the subset of keys from the provided list that are valid in the instance.

Parameters:

list_of_keys : list A list of keys (as strings) to check against the instance’s attributes.

Returns:

list A list of keys from list_of_keys that are valid (i.e., exist as attributes in the instance).

Raises:

TypeError If list_of_keys is not a list or if any element in list_of_keys is not a string.

Example:

>>> s = struct()
>>> s.foo = 42
>>> s.bar = "hello"
>>> valid = s.validkeys(["foo", "bar", "baz"])
>>> print(valid)   # Output: ['foo', 'bar'] assuming 'baz' is not defined in s
Expand source code
def validkeys(self, list_of_keys):
    """
    Validate and return the subset of keys from the provided list that are valid in the instance.

    Parameters:
    -----------
    list_of_keys : list
        A list of keys (as strings) to check against the instance’s attributes.

    Returns:
    --------
    list
        A list of keys from list_of_keys that are valid (i.e., exist as attributes in the instance).

    Raises:
    -------
    TypeError
        If list_of_keys is not a list or if any element in list_of_keys is not a string.

    Example:
    --------
    >>> s = struct()
    >>> s.foo = 42
    >>> s.bar = "hello"
    >>> valid = s.validkeys(["foo", "bar", "baz"])
    >>> print(valid)   # Output: ['foo', 'bar'] assuming 'baz' is not defined in s
    """
    # Check that list_of_keys is a list
    if not isinstance(list_of_keys, list):
        raise TypeError("list_of_keys must be a list")

    # Check that every entry in the list is a string
    for key in list_of_keys:
        if not isinstance(key, str):
            raise TypeError("Each key in list_of_keys must be a string")

    # Assuming valid keys are those present in the instance's __dict__
    return [key for key in list_of_keys if key in self]
def values(self)

return the values

Expand source code
def values(self):
    """ return the values """
    # values() is used by struct() and its iterator
    return [pstr.eval(value) for key,value in self.__dict__.items() if key not in self._excludedattr]
def write(self, file, overwrite=True, mkdir=False)

write the equivalent structure (not recursive for nested struct) write(filename, overwrite=True, mkdir=False)

Parameters: - file: The file path to write to. - overwrite: Whether to overwrite the file if it exists (default: True). - mkdir: Whether to create the directory if it doesn't exist (default: False).

Expand source code
def write(self, file, overwrite=True, mkdir=False):
    """
        write the equivalent structure (not recursive for nested struct)
            write(filename, overwrite=True, mkdir=False)

        Parameters:
        - file: The file path to write to.
        - overwrite: Whether to overwrite the file if it exists (default: True).
        - mkdir: Whether to create the directory if it doesn't exist (default: False).
    """
    # Create a Path object for the file to handle cross-platform paths
    file_path = Path(file).resolve()

    # Check if the directory exists or if mkdir is set to True, create it
    if mkdir:
        file_path.parent.mkdir(parents=True, exist_ok=True)
    elif not file_path.parent.exists():
        raise FileNotFoundError(f"The directory {file_path.parent} does not exist.")
    # If overwrite is False and the file already exists, raise an exception
    if not overwrite and file_path.exists():
        raise FileExistsError(f"The file {file_path} already exists, and overwrite is set to False.")
    # Convert to static if needed
    if isinstance(p,(param,paramauto)):
        tmp = self.tostatic()
    else:
        tmp = self
    # Open and write to the file using the resolved path
    with file_path.open(mode="w", encoding='utf-8') as f:
        print(f"# {self._fulltype} with {len(self)} {self._ftype}s\n", file=f)
        for k, v in tmp.items():
            if v is None:
                print(k, "=None", file=f, sep="")
            elif isinstance(v, (int, float)):
                print(k, "=", v, file=f, sep="")
            elif isinstance(v, str):
                print(k, '="', v, '"', file=f, sep="")
            else:
                print(k, "=", str(v), file=f, sep="")
def zip(self)

zip keys and values

Expand source code
def zip(self):
    """ zip keys and values """
    return zip(self.keys(),self.values())
class tlsph

SMD:TLSPH forcefield (total Lagrangian)

Expand source code
class tlsph(smd):
    """ SMD:TLSPH forcefield (total Lagrangian) """
    name = smd.name + struct(style="tlsph")
    description = smd.description + struct(style="SMD:TLSPH - total Lagrangian for solids")

    # style definition (LAMMPS code between triple """)
    PAIR_DIAGCOEFF = """
    # [comment] Diagonal pair coefficient tlsph
    pair_coeff      %d %d smd/tlsph *COMMON ${rho} ${E} ${nu} ${q1} ${q2} ${Hg} ${Cp} &
                    *STRENGTH_LINEAR_PLASTIC ${sigma_yield} ${hardening} &
                    *EOS_LINEAR &
                    *END
    """
    PAIR_OFFDIAGCOEFF = """
    # [comment] Off-diagonal pair coefficient (generic)
    pair_coeff      %d %d smd/hertz ${contact_stiffness}
    """

Ancestors

Subclasses

Class variables

var PAIR_DIAGCOEFF
var PAIR_OFFDIAGCOEFF
var description
var name

Inherited members

class tlsphalone

SMD:TLSPH forcefield (total Lagrangian)

Expand source code
class tlsphalone(forcefield):
    """ SMD:TLSPH forcefield (total Lagrangian) """
    name = smd.name + struct(style="tlsph")
    description = smd.description + struct(style="SMD:TLSPH - total Lagrangian for solids")

    # forcefield definition (LAMMPS code between triple """)
    PAIR_STYLE = """
    # [comment] PAIR STYLE SMD
    pair_style      hybrid/overlay smd/tlsph smd/hertz ${contact_scale}
    """

    # style definition (LAMMPS code between triple """)
    PAIR_DIAGCOEFF = """
    # [comment] Diagonal pair coefficient tlsph
    pair_coeff      %d %d smd/tlsph *COMMON ${rho} ${E} ${nu} ${q1} ${q2} ${Hg} ${Cp} &
                    *STRENGTH_LINEAR_PLASTIC ${sigma_yield} ${hardening} &
                    *EOS_LINEAR &
                    *END
    """
    PAIR_OFFDIAGCOEFF = """
    # [comment] Off-diagonal pair coefficient (generic)
    pair_coeff      %d %d smd/hertz ${contact_stiffness}
    """

Ancestors

Class variables

var PAIR_DIAGCOEFF
var PAIR_OFFDIAGCOEFF
var PAIR_STYLE
var description
var name

Inherited members

class ulsph

SMD:ULSPH forcefield (updated Lagrangian)

Expand source code
class ulsph(smd):
    """ SMD:ULSPH forcefield (updated Lagrangian) """
    name = smd.name + struct(style="ulsph")
    description = smd.description + struct(style="SMD:ULSPH - updated Lagrangian for fluids - SPH-like")

    # style definition (LAMMPS code between triple """)
    PAIR_DIAGCOEFF = """
    # [comment] Pair diagonal coefficient ulsph
    pair_coeff      %d %d smd/ulsph *COMMON ${rho} ${c0} ${q1} ${Cp} 0 &
                    *EOS_TAIT ${taitexponent} &
                    *END
    """
    PAIR_OFFDIAGCOEFF = """
    # [comment] Off-diagonal pair coefficient (generic)
    pair_coeff      %d %d smd/hertz ${contact_stiffness}
    """

Ancestors

Subclasses

Class variables

var PAIR_DIAGCOEFF
var PAIR_OFFDIAGCOEFF
var description
var name

Inherited members

class ulsphalone

SMD:ULSPH forcefield (updated Lagrangian)

Expand source code
class ulsphalone(smd):
    """ SMD:ULSPH forcefield (updated Lagrangian) """
    name = smd.name + struct(style="ulsph")
    description = smd.description + struct(style="SMD:ULSPH - updated Lagrangian for fluids - SPH-like")

    # forcefield definition (LAMMPS code between triple """)
    PAIR_STYLE = """
    # [comment] PAIR STYLE SMD
    pair_style      smd/ulsph *DENSITY_CONTINUITY *VELOCITY_GRADIENT *NO_GRADIENT_CORRECTION
    """

    # style definition (LAMMPS code between triple """)
    PAIR_DIAGCOEFF = """
    # [comment] Pair diagonal coefficient ulsph
    pair_coeff      %d %d smd/ulsph *COMMON ${rho} ${c0} ${q1} ${Cp} 0 &
                    *EOS_TAIT ${taitexponent} &
                    *END
    """
    PAIR_OFFDIAGCOEFF = """
    # [comment] Off-diagonal pair coefficient (generic)
    pair_coeff      %d %d smd/hertz ${contact_stiffness}
    """

Ancestors

Class variables

var PAIR_DIAGCOEFF
var PAIR_OFFDIAGCOEFF
var PAIR_STYLE
var description
var name

Inherited members

class water (beadtype=1, userid=None, USER=forcefield (FF object) with 0 parameters)

water material (smd:ulsph): generic water model water() water(beadtype=index, userid="myfluid", USER=…)

override any propery with USER.parameter (set only the parameters you want to override) USER.rho: density in kg/m3 (default=1000) USER.c0: speed of the sound in m/s (default=10.0) USER.q1: standard artificial viscosity linear coefficient (default=1.0) USER.Cp: heat capacity of material – not used here (default=1.0) USER.contact_scale: scaling coefficient for contact (default=1.5) USER.contact_stiffness: contact stifness in Pa (default="2.5${c0}^2${rho}")

water forcefield: water(beadtype=index, userid="myfluid")

Expand source code
class water(ulsph):
    """ water material (smd:ulsph): generic water model
            water()
            water(beadtype=index, userid="myfluid", USER=...)

            override any propery with USER.parameter (set only the parameters you want to override)
                USER.rho: density in kg/m3 (default=1000)
                USER.c0: speed of the sound in m/s (default=10.0)
                USER.q1: standard artificial viscosity linear coefficient (default=1.0)
                USER.Cp: heat capacity of material -- not used here (default=1.0)
                USER.contact_scale: scaling coefficient for contact (default=1.5)
                USER.contact_stiffness: contact stifness in Pa (default="2.5*${c0}^2*${rho}")
    """
    name = ulsph.name + struct(material="water")
    description = ulsph.description + struct(material="water beads - SPH-like")
    userid = 'water'
    version = 0.1

    # constructor (do not forgert to include the constuctor)
    def __init__(self, beadtype=1, userid=None, USER=parameterforcefield()):
        """ water forcefield:
            water(beadtype=index, userid="myfluid") """
        if userid!=None: self.userid = userid
        self.beadtype = beadtype
        self.parameters = parameterforcefield(
            # water-water interactions
            rho = 1000,
            c0 = 10.0,
            q1 = 1.0,
            Cp = 1.0,
            taitexponent = 7,
            # hertz contacts
            contact_scale = 1.5,
            contact_stiffness = '2.5*${c0}^2*${rho}'
            ) + USER # update with user properties if any

Ancestors

Class variables

var description
var name
var userid
var version

Inherited members