Module dforcefield

Synopsis of dforcefield Class

The dforcefield class enables the dynamic creation and modification of forcefields at runtime, in contrast to the static material customization approach used in the forcefield class. Whereas the forcefield class relies on predefined material classes (e.g., in pizza.generic) and supports inheritance to manage complex materials, dforcefield allows for flexible and rapid creation of forcefields without a predefined library. This makes dforcefield ideal for prototyping and experimenting with forcefield models.

The class seamlessly integrates both static and dynamic forcefields, which can be used interchangeably to define objects built with atoms, set the interactions between those objects, and generate the equivalent scripts. This flexibility enables users to choose between dynamic instances (dforcefield) and static classes (forcefield) for defining scriptobject interactions.

Moreover, dynamic forcefields can also be defined using methods from generic classes. These generic classes allow creating high-level forcefields dynamically, providing another layer of customization that can be combined with both static and dynamic forcefields.

Key Differences Between forcefield, generic, and dforcefield:

  • forcefield:

    • New materials are defined using classes, often within a material library (e.g., pizza.generic).
    • Supports inheritance to manage complex materials and extend functionality.
    • Primarily used for well-defined, library-based material management.
  • generic:

    • Provides high-level forcefield definitions via methods that can be dynamically called.
    • These dynamic definitions can be used interchangeably with static forcefields.
    • Suitable for defining specialized forcefields on the fly without modifying the core library.
  • dforcefield:

    • Enables dynamic definition of forcefields at runtime without needing a predefined library.
    • Ideal for rapid prototyping and testing new configurations on the fly.
    • Attributes like parameters, beadtype, and userid can be modified at runtime and automatically injected into the base forcefield class for flexible interaction modeling.
    • Provides a method, scriptobject, to create objects compatible with static forcefields.
    • Allows integrating high-level forcefield definitions from generic methods, enhancing dynamic forcefield customization.

Key Attributes:

  • base_class (forcefield): The base forcefield class (e.g., ulsph) from which behavior is inherited.
  • parameters (parameterforcefield): Stores interaction parameters that are dynamically injected into the base_class.
  • beadtype (int): The bead type associated with the dforcefield instance, used in forcefield calculations.
  • userid (str): A unique identifier for the forcefield instance, used in interaction commands.
  • name (struct or str): A human-readable name for the dforcefield instance.
  • description (struct or str): A brief description of the dforcefield instance.
  • version (float): The version number of the dforcefield instance.

Key Methods:

  • _inject_attributes(): Injects dforcefield attributes (like parameters, beadtype, and userid) into the base_class to ensure it operates with the correct attributes.
  • pair_style(printflag=True, raw=False): Delegates the pair style computation to the base_class, ensuring it uses the current dforcefield attributes.
  • pair_diagcoeff(printflag=True, i=None, raw=False): Delegates diagonal pair coefficient computation to the base_class, allowing bead type i to be overridden.
  • pair_offdiagcoeff(o=None, printflag=True, i=None, raw=False): Delegates off-diagonal pair coefficient computation to the base_class, allowing bead type i and interacting forcefield o to be overridden.
  • scriptobject(beadtype=None, name=None, fullname=None, filename=None, group=None, style=None, USER=scriptdata()): Creates a scriptobject from the current dforcefield instance, making it compatible with static forcefields.
  • missingVariables(isimplicit_missing=True, output_aslist=False): Lists undefined variables in parameters, identifying missing implicit definitions (e.g., ${varname}).

Usage Example:

# Create a dynamic water forcefield using ulsph as the base class
dynamic_water = dforcefield(
    base_class=ulsph,
    beadtype=1,
    userid="dynamic_water",
    USER=parameterforcefield(
        rho=1000,
        c0=10.0,
        q1=1.0,
        Cp=1.0,
        taitexponent=7,
        contact_scale=1.5,
        contact_stiffness="2.5*${c0}^2*${rho}"
    )
)
print(f"Water parameters: {dynamic_water.parameters}")
print(f"Water Cp: {dynamic_water.Cp}")
dynamic_water.pair_style()  # Outputs the pair style command


# Create a dynamic solidfood forcefield using tlsph as the base class
dynamic_solidfood = dforcefield(
    base_class=tlsph,
    beadtype=2,
    userid="dynamic_solidfood",
    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,
    contact_scale=1.5,
    contact_stiffness="2.5*${c0}^2*${rho}"
)
print(f"Solidfood parameters: {dynamic_solidfood.parameters}")
dynamic_solidfood.pair_style()  # Outputs the pair style command

# Create a new solidfood variant and save it
new_food = dynamic_solidfood.copy(rho=2100, q1=4, E=1000, name="new food")
new_food.save(overwrite=True)

# Load the saved forcefield and compare
loaded_food = dforcefield.load(new_food.userid + ".txt")
new_food.compare(loaded_food, printflag=True)

# Check for missing variables
missing_vars = new_food.missingVariables()
print(f"Missing variables: {missing_vars}")


# Generate a forcefield from text
ff_text = '''
# DFORCEFIELD SAVE FILE

# Forcefield attributes
base_class="tlsph"
beadtype = 2
userid = dynamic_solidfood (copy)
version = 0.1

# Description of the forcefield
description:{forcefield="LAMMPS:SMD - solid, liquid, rigid forcefields (continuum mechanics)", style="SMD:TLSPH - total Lagrangian for solids", material="dforcefield beads - SPH-like"}

# Name of the forcefield
name:{forcefield="LAMMPS:SMD", style="tlsph", material="new food"}

# Parameters for the forcefield
contact_scale = 1.5
E = 1000
nu = 0.3
q2 = 0.0
hardening = 0
Hg = 10.0
rho = 2100
Cp = 1.0
q1 = 4
c0 = 10.0
sigma_yield = 0.1*${E}
contact_stiffness = 2.5*${c0}^2*${rho}
'''

# Parse the forcefield from text
parsed_ff = dforcefield.parse(ff_text)
print(parsed_ff.script)  # Outputs the raw content from the pair_* methods

# Check missing variables
missing_vars = parsed_ff.missingVariables()
print(f"Missing variables: {missing_vars}")


# *********************************************************************************************
# Production Example: Scriptobject Creation and Combination
#
# This example demonstrates the creation and combination of <code><a title="dforcefield.scriptobject" href="#dforcefield.scriptobject">scriptobject</a></code> instances from both
# static forcefield classes and dynamic forcefield instances (via <code><a title="dforcefield.dforcefield" href="#dforcefield.dforcefield">dforcefield</a></code>).
#
# Key Points:
# ------------
# 1. **Static and Dynamic Forcefields**:
#    - Scriptobjects can be created using static forcefields (e.g., <code>rigidwall</code>, <code>solidfood</code>, etc.)
#      or dynamic forcefields generated from a <code><a title="dforcefield.dforcefield" href="#dforcefield.dforcefield">dforcefield</a></code> instance (e.g., <code>waterFF</code>).
#    - Dynamic forcefields can either be passed directly to the <code>pizza.script.scriptobject()</code>
#      constructor or instantiated through the <code><a title="dforcefield.scriptobject" href="#dforcefield.scriptobject">scriptobject</a></code> method of any <code>pizza.dforcefield</code> object.
#
# 2. **Combining Scriptobjects**:
#    - Scriptobjects can be combined using the `+` operator to create a collection of objects.
#    - This collection of scriptobjects can then be scripted dynamically using the <code>script</code> property
#      to generate their interaction definitions.
#
# 3. **Geometry Input**:
#    - Geometry for the scriptobjects can be provided either via an input file (using <code>filename</code>)
#      or dynamically using <code>pizza.region.region()</code>.
#
# **********************************************************************************************

from pizza.forcefield import rigidwall, solidfood, water

# Create a dynamic forcefield for water with a specific density (rho=900) and beadtype=1
waterFF = dforcefield(base_class="water", rho=900, beadtype=1)

# Define water beads using the dforcefield instance's scriptobject method
bwater = waterFF.scriptobject(name="water", group=["A", "D"], filename="mygeom")

# Alternatively, define water beads by passing the dynamic forcefield directly to scriptobject
bwater2 = scriptobject(name="water", group=["A", "D"], filename="mygeom", forcefield=waterFF)

# Define other dummy beads using static forcefields
b1 = scriptobject(name="bead 1", group=["A", "B", "C"], filename='myfile1', forcefield=rigidwall())
b2 = scriptobject(name="bead 2", group=["B", "C"], filename='myfile1', forcefield=rigidwall())
b3 = scriptobject(name="bead 3", group=["B", "D", "E"], forcefield=solidfood())
b4 = scriptobject(name="bead 4", group="D", beadtype=1, filename="myfile2", forcefield=water())

# Combine the scriptobjects into a collection using the '+' operator
collection = bwater + b1 + b2 + b3 + b4

# Generate the script for the interactions in the collection
collection.script

# Similarly, using the alternate water bead definition (bwater2)
collection2 = bwater2 + b1 + b2 + b3 + b4
collection2.script

Created on Tue Sep 10 17:11:40 2024

Dependencies

  • Python 3.x
  • LAMMPS
  • Pizza3.pizza

Installation

To use the Pizza3.pizza module, ensure that you have Python 3.x and LAMMPS installed. You can integrate the module into your project by placing the region.py file in your working directory or your Python path.

License

This project is licensed under the terms of the GPLv3 license.

Contact

For any queries or contributions, please contact the maintainer: - Olivier Vitrac, Han Chen - Email: olivier.vitrac@agroparistech.fr

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

"""
Synopsis of `dforcefield` Class
===============================

The `dforcefield` class enables the dynamic creation and modification of forcefields at runtime,
in contrast to the static material customization approach used in the `forcefield` class.
Whereas the `forcefield` class relies on predefined material classes (e.g., in `pizza.generic`)
and supports inheritance to manage complex materials, `dforcefield` allows for flexible and
rapid creation of forcefields without a predefined library. This makes `dforcefield` ideal for
prototyping and experimenting with forcefield models.

The class seamlessly integrates both static and dynamic forcefields, which can be used interchangeably
to define objects built with atoms, set the interactions between those objects, and generate the
equivalent scripts. This flexibility enables users to choose between dynamic instances (`dforcefield`)
and static classes (`forcefield`) for defining `scriptobject` interactions.

Moreover, dynamic forcefields can also be defined using methods from `generic` classes. These
`generic` classes allow creating high-level forcefields dynamically, providing another layer of
customization that can be combined with both static and dynamic forcefields.

Key Differences Between `forcefield`, `generic`, and `dforcefield`:
-------------------------------------------------------------------
- **`forcefield`**:
    - New materials are defined using classes, often within a material library (e.g., `pizza.generic`).
    - Supports inheritance to manage complex materials and extend functionality.
    - Primarily used for well-defined, library-based material management.

- **`generic`**:
    - Provides high-level forcefield definitions via methods that can be dynamically called.
    - These dynamic definitions can be used interchangeably with static forcefields.
    - Suitable for defining specialized forcefields on the fly without modifying the core library.

- **`dforcefield`**:
    - Enables dynamic definition of forcefields at runtime without needing a predefined library.
    - Ideal for rapid prototyping and testing new configurations on the fly.
    - Attributes like `parameters`, `beadtype`, and `userid` can be modified at runtime and
      automatically injected into the base forcefield class for flexible interaction modeling.
    - Provides a method, `scriptobject()`, to create objects compatible with static forcefields.
    - Allows integrating high-level forcefield definitions from `generic` methods, enhancing
      dynamic forcefield customization.

Key Attributes:
---------------
- `base_class` (`forcefield`): The base forcefield class (e.g., `ulsph`) from which behavior
  is inherited.
- `parameters` (`parameterforcefield`): Stores interaction parameters that are dynamically
  injected into the `base_class`.
- `beadtype` (int): The bead type associated with the `dforcefield` instance, used in
  forcefield calculations.
- `userid` (str): A unique identifier for the forcefield instance, used in interaction commands.
- `name` (struct or str): A human-readable name for the `dforcefield` instance.
- `description` (struct or str): A brief description of the `dforcefield` instance.
- `version` (float): The version number of the `dforcefield` instance.

Key Methods:
------------
- `_inject_attributes()`: Injects `dforcefield` attributes (like `parameters`, `beadtype`,
  and `userid`) into the `base_class` to ensure it operates with the correct attributes.
- `pair_style(printflag=True, raw=False)`: Delegates the pair style computation to the `base_class`,
  ensuring it uses the current `dforcefield` attributes.
- `pair_diagcoeff(printflag=True, i=None, raw=False)`: Delegates diagonal pair coefficient
  computation to the `base_class`, allowing bead type `i` to be overridden.
- `pair_offdiagcoeff(o=None, printflag=True, i=None, raw=False)`: Delegates off-diagonal pair coefficient
  computation to the `base_class`, allowing bead type `i` and interacting forcefield `o` to be overridden.
- `scriptobject(beadtype=None, name=None, fullname=None, filename=None, group=None, style=None, USER=scriptdata())`:
  Creates a `scriptobject` from the current `dforcefield` instance, making it compatible with static forcefields.
- `missingVariables(isimplicit_missing=True, output_aslist=False)`: Lists undefined variables in `parameters`,
  identifying missing implicit definitions (e.g., `${varname}`).

Usage Example:
--------------
    # Create a dynamic water forcefield using ulsph as the base class
    dynamic_water = dforcefield(
        base_class=ulsph,
        beadtype=1,
        userid="dynamic_water",
        USER=parameterforcefield(
            rho=1000,
            c0=10.0,
            q1=1.0,
            Cp=1.0,
            taitexponent=7,
            contact_scale=1.5,
            contact_stiffness="2.5*${c0}^2*${rho}"
        )
    )
    print(f"Water parameters: {dynamic_water.parameters}")
    print(f"Water Cp: {dynamic_water.Cp}")
    dynamic_water.pair_style()  # Outputs the pair style command


    # Create a dynamic solidfood forcefield using tlsph as the base class
    dynamic_solidfood = dforcefield(
        base_class=tlsph,
        beadtype=2,
        userid="dynamic_solidfood",
        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,
        contact_scale=1.5,
        contact_stiffness="2.5*${c0}^2*${rho}"
    )
    print(f"Solidfood parameters: {dynamic_solidfood.parameters}")
    dynamic_solidfood.pair_style()  # Outputs the pair style command

    # Create a new solidfood variant and save it
    new_food = dynamic_solidfood.copy(rho=2100, q1=4, E=1000, name="new food")
    new_food.save(overwrite=True)

    # Load the saved forcefield and compare
    loaded_food = dforcefield.load(new_food.userid + ".txt")
    new_food.compare(loaded_food, printflag=True)

    # Check for missing variables
    missing_vars = new_food.missingVariables()
    print(f"Missing variables: {missing_vars}")


    # Generate a forcefield from text
    ff_text = '''
    # DFORCEFIELD SAVE FILE

    # Forcefield attributes
    base_class="tlsph"
    beadtype = 2
    userid = dynamic_solidfood (copy)
    version = 0.1

    # Description of the forcefield
    description:{forcefield="LAMMPS:SMD - solid, liquid, rigid forcefields (continuum mechanics)", style="SMD:TLSPH - total Lagrangian for solids", material="dforcefield beads - SPH-like"}

    # Name of the forcefield
    name:{forcefield="LAMMPS:SMD", style="tlsph", material="new food"}

    # Parameters for the forcefield
    contact_scale = 1.5
    E = 1000
    nu = 0.3
    q2 = 0.0
    hardening = 0
    Hg = 10.0
    rho = 2100
    Cp = 1.0
    q1 = 4
    c0 = 10.0
    sigma_yield = 0.1*${E}
    contact_stiffness = 2.5*${c0}^2*${rho}
    '''

    # Parse the forcefield from text
    parsed_ff = dforcefield.parse(ff_text)
    print(parsed_ff.script)  # Outputs the raw content from the pair_* methods

    # Check missing variables
    missing_vars = parsed_ff.missingVariables()
    print(f"Missing variables: {missing_vars}")


    # *********************************************************************************************
    # Production Example: Scriptobject Creation and Combination
    #
    # This example demonstrates the creation and combination of `scriptobject` instances from both
    # static forcefield classes and dynamic forcefield instances (via `dforcefield`).
    #
    # Key Points:
    # ------------
    # 1. **Static and Dynamic Forcefields**:
    #    - Scriptobjects can be created using static forcefields (e.g., `rigidwall`, `solidfood`, etc.)
    #      or dynamic forcefields generated from a `dforcefield` instance (e.g., `waterFF`).
    #    - Dynamic forcefields can either be passed directly to the `pizza.script.scriptobject()`
    #      constructor or instantiated through the `scriptobject` method of any `pizza.dforcefield` object.
    #
    # 2. **Combining Scriptobjects**:
    #    - Scriptobjects can be combined using the `+` operator to create a collection of objects.
    #    - This collection of scriptobjects can then be scripted dynamically using the `script` property
    #      to generate their interaction definitions.
    #
    # 3. **Geometry Input**:
    #    - Geometry for the scriptobjects can be provided either via an input file (using `filename`)
    #      or dynamically using `pizza.region.region()`.
    #
    # **********************************************************************************************

    from pizza.forcefield import rigidwall, solidfood, water

    # Create a dynamic forcefield for water with a specific density (rho=900) and beadtype=1
    waterFF = dforcefield(base_class="water", rho=900, beadtype=1)

    # Define water beads using the dforcefield instance's scriptobject method
    bwater = waterFF.scriptobject(name="water", group=["A", "D"], filename="mygeom")

    # Alternatively, define water beads by passing the dynamic forcefield directly to scriptobject
    bwater2 = scriptobject(name="water", group=["A", "D"], filename="mygeom", forcefield=waterFF)

    # Define other dummy beads using static forcefields
    b1 = scriptobject(name="bead 1", group=["A", "B", "C"], filename='myfile1', forcefield=rigidwall())
    b2 = scriptobject(name="bead 2", group=["B", "C"], filename='myfile1', forcefield=rigidwall())
    b3 = scriptobject(name="bead 3", group=["B", "D", "E"], forcefield=solidfood())
    b4 = scriptobject(name="bead 4", group="D", beadtype=1, filename="myfile2", forcefield=water())

    # Combine the scriptobjects into a collection using the '+' operator
    collection = bwater + b1 + b2 + b3 + b4

    # Generate the script for the interactions in the collection
    collection.script

    # Similarly, using the alternate water bead definition (bwater2)
    collection2 = bwater2 + b1 + b2 + b3 + b4
    collection2.script



Created on Tue Sep 10 17:11:40 2024


Dependencies
------------
- Python 3.x
- LAMMPS
- Pizza3.pizza

Installation
------------
To use the Pizza3.pizza module, ensure that you have Python 3.x and LAMMPS installed. You can integrate the module into your project by placing the `region.py` file in your working directory or your Python path.

License
-------
This project is licensed under the terms of the GPLv3 license.

Contact
-------
For any queries or contributions, please contact the maintainer:
- Olivier Vitrac, Han Chen
- Email: olivier.vitrac@agroparistech.fr

"""

__project__ = "Pizza3"
__author__ = "Olivier Vitrac, Han Chen, Joseph Fine"
__copyright__ = "Copyright 2024"
__credits__ = ["Olivier Vitrac", "Han Chen", "Joseph Fine"]
__license__ = "GPLv3"
__maintainer__ = "Olivier Vitrac"
__email__ = "olivier.vitrac@agroparistech.fr"
__version__ = "0.9972"



# INRAE\Olivier Vitrac - rev. 2024-10-12 (community)
# contact: olivier.vitrac@agroparistech.fr, han.chen@inrae.fr

# Revision history
# 2024-09-10 release candidate
# 2024-09-12 file management and parsing
# 2024-09-14 major update fully compatible with scriptobject, script
# 2024-09-21 fully compatible with forcefields of class generic
# 2024-10-12 add |


# Dependencies
import os, sys, tempfile, getpass, socket
import re, string, random
import inspect, copy
from datetime import datetime
from pizza.private.mstruct import struct
from pizza.forcefield import forcefield, parameterforcefield
from pizza.script import scriptdata, scriptobject
from pizza.generic import generic, genericdata, USERSMD

__all__ = ['USERSMD', 'autoname', 'dforcefield', 'forcefield', 'generic', 'genericdata', 'parameterforcefield', 'remove_comments', 'scriptdata', 'scriptobject', 'struct']


# %% Private Functions

def autoname(numChars=8):
    """ generate automatically names """
    return ''.join(random.choices(string.ascii_letters, k=numChars))  # Generates a random name of numChars letters

def remove_comments(content, split_lines=False):
    """
    Removes comments from a single or multi-line string. Handles quotes and escaped characters.

    Parameters:
    -----------
    content : str
        The input string, which may contain multiple lines. Each line will be processed
        individually to remove comments, while preserving content inside quotes.
    split_lines : bool, optional (default: False)
        If True, the function will return a list of processed lines. If False, it will
        return a single string with all lines joined by newlines.

    Returns:
    --------
    str or list of str
        The processed content with comments removed. Returns a list of lines if
        `split_lines` is True, or a single string if False.
    """
    def process_line(line):
        """Remove comments from a single line while handling quotes and escapes."""
        in_single_quote = False
        in_double_quote = False
        escaped = False
        result = []

        for i, char in enumerate(line):
            if escaped:
                result.append(char)
                escaped = False
                continue

            if char == '\\':  # Handle escape character
                escaped = True
                result.append(char)
                continue

            # Toggle state for single and double quotes
            if char == "'" and not in_double_quote:
                in_single_quote = not in_single_quote
            elif char == '"' and not in_single_quote:
                in_double_quote = not in_double_quote

            # If we encounter a '#' and we're not inside quotes, it's a comment
            if char == '#' and not in_single_quote and not in_double_quote:
                break  # Stop processing the line when a comment is found

            result.append(char)

        return ''.join(result).strip()

    # Split the input content into lines
    lines = content.split('\n')

    # Process each line, skipping empty lines and those starting with #
    processed_lines = []
    for line in lines:
        stripped_line = line.strip()
        if not stripped_line or stripped_line.startswith('#'):
            continue  # Skip empty lines and lines that are pure comments
        processed_line = process_line(line)
        if processed_line:  # Only add non-empty lines
            processed_lines.append(process_line(line))

    if split_lines:
        return processed_lines  # Return list of processed lines
    else:
        return '\n'.join(processed_lines)  # Join lines back into a single string


# %% Main class
class dforcefield:
    """
    The `dforcefield` class represents a dynamic extension of a base forcefield class. It allows for dynamic inheritance,
    delegation of methods, and flexible management of forcefield-specific attributes. The class supports the customization
    and injection of forcefield parameters at runtime, making it highly versatile for scientific simulations involving
    various forcefield models like `ulsph`, `tlsph`, and others.

    Key Features:
    -------------
    - Dynamic inheritance and delegation of base class methods.
    - Merging of `name` and `description` attributes, with automatic handling of overlapping fields.
    - Detection of variables used in `pair_style`, `pair_diagcoeff`, and `pair_offdiagcoeff` outputs.
    - Automatic handling of missing variables, including implicit variable detection and assignment.
    - Supports additional modules to be dynamically loaded for forcefield definitions.
    - Management of `RULES`, `GLOBAL`, and `LOCAL` parameters, which can be updated dynamically at runtime.

    Attributes:
    -----------
    - `base_class` (forcefield): The base class for forcefield behavior, inherited from `forcefield` subclasses like `ulsph`.
    - `parameters` (parameterforcefield): Stores interaction parameters dynamically injected into the base class.
    - `beadtype` (int): The bead type identifier associated with the forcefield instance.
    - `userid` (str): A unique identifier for the forcefield instance.
    - `name` (struct or str): A human-readable name for the forcefield instance, merged with `description`.
    - `description` (struct or str): A brief description of the forcefield, merged with `name`.
    - `version` (float): Version number for the forcefield instance.
    - `RULES` (genericdata): Defines specific rules or formulae applied to forcefield calculations.
    - `GLOBAL` (genericdata): Stores global parameters that apply to the entire forcefield.
    - `LOCAL` (genericdata): Contains local parameters specific to certain forcefield interactions.
    - `USER` (scriptdata): Contains USER parameters for scriptobject

    Methods:
    --------
    ### High-Level Methods:
    These methods are primarily used for interacting with the `dforcefield` instance:

    - **`__init__`**: Initializes the dynamic forcefield with base class, parameters, and custom attributes.
    - **`pair_style`**: Delegates the pair style computation to the `base_class`.
    - **`pair_diagcoeff`**: Delegates diagonal pair coefficient computation to the `base_class`.
    - **`pair_offdiagcoeff`**: Delegates off-diagonal pair coefficient computation to the `base_class`.
    - **`save`**: Saves the forcefield instance to a file, with headers, attributes, and parameters.
    - **`load`**: Loads a `dforcefield` instance from a file and validates its format.
    - **`parse`**: Parses content of a forcefield file to create a new `dforcefield` instance.
    - **`copy`**: Creates a copy of the current instance, optionally overriding core attributes.
    - **`compare`**: Compares the current instance with another `dforcefield` instance, showing differences.
    - **`missingVariables`**: Lists undefined variables in `parameters`.
    - **`detectVariables`**: Detects variables in the `pair_style`, `pair_diagcoeff`, and `pair_offdiagcoeff` outputs.
    - **`to_dict`**: Serializes the forcefield instance into a dictionary.
    - **`from_dict`**: Creates a `dforcefield` instance from a dictionary of attributes.
    - **`reset`**: Resets the forcefield instance to its initial state.
    - **`validate`**: Validates the forcefield instance to ensure all required attributes are set.
    - **`base_repr`**: Returns the representation of the `base_class`.
    - **`script`**: Returns the raw outputs of pair methods as a script.
    - **`scriptobject`**: Method to return a `scriptobject` based on the current `dforcefield` instance.

    ### Low-Level Methods:
    These methods handle internal logic and lower-level operations:

    - **`_inject_attributes`**: Injects the dynamic attributes into the `base_class`.
    - **`__getattr__`**: Dynamically accesses attributes in `name`, `description`, `parameters`, or `base_class`.
    - **`__setattr__`**: Manages setting core and dynamic attributes.
    - **`_load_base_class`**: Dynamically loads the base class from a string name.
    - **`list_forcefield_subclasses`**: Lists all subclasses of `forcefield` including those from additional modules.
    - **`extract_default_parameters`**: Extracts default parameters from the base class or its ancestors.
    - **`dispmax`**: Truncates the display of long content for concise output.
    - **`__hasattr__`**: Checks if an attribute exists in the instance, parameters, or base class.
    - **`__contains__`**: Checks if an attribute exists in the instance or `parameters`.
    - **`__len__`**: Returns the number of parameters in the forcefield.
    - **`__iter__`**: Iterates over all keys, including those in `name`, `description`, `parameters`, and scalar attributes.
    - **`keys`**: Returns the keys from the merged `name`, `description`, and `parameters`.
    - **`values`**: Returns the values from the merged `name`, `description`, and `parameters`.
    - **`items`**: Returns an iterator of key-value pairs from `name`, `description`, and `parameters`.
    - **`merged_name_description`**: Merges `name` and `description` fields into a struct.
    - **`_get_available_forcefields`**: Retrieves a list of available forcefield subclasses.
    - **`__repr__`**: Custom representation of the `dforcefield` instance.
    - **`__str__`**: Returns a string representation of the `dforcefield` instance.
    - **`_convert_value`**: Converts a string value to the appropriate Python type.
    - **`_parse_global_params`**: Parses global parameters from the content between `{}`.
    - **`_parse_struct_block`**: Parses key-value pairs from `description` or `name` blocks.
    - **`__copy__`**: Creates a shallow copy of the `dforcefield` instance, copying only the attributes at the top level without duplicating nested objects.
    - **`__deepcopy__`**: Creates a deep copy of the `dforcefield` instance, fully duplicating all attributes, including nested objects, while leaving class references intact.
    - **`get_global`**: Returns the `GLOBAL` parameters.
    - **`get_local`**: Returns the `LOCAL` parameters.
    - **`get_rules`**: Returns the `RULES` parameters.
    - **`set_global`**: Updates the `GLOBAL` parameters and recalculates the combined parameters.
    - **`set_local`**: Updates the `LOCAL` parameters and recalculates the combined parameters.
    - **`set_rules`**: Updates the `RULES` parameters and recalculates the combined parameters.
    - **`combine_parameters`**: Combines `GLOBAL`, `LOCAL`, and `RULES` into the current parameter configuration.
    - **`update_parameters`**: Updates `self.parameters` after changes to `GLOBAL`, `LOCAL`, or `RULES`.

    Example Usage:
    --------------
        >>> dynamic_ff = dforcefield(ulsph, beadtype=1, userid="dynamic_water", USER=parameterforcefield(rho=1000))
        >>> dynamic_ff.pair_style()  # Uses attributes from the dforcefield instance
        lj/cut
        >>> dynamic_ff.compare(another_ff_instance, printflag=True)
    """


    # Display
    _maxdisplay = 40
    # Class attribute for the six specific attributes + 3 generic attributes + USER set by scriptobject
    _dforcefield_specific_attributes = {'name', 'description', 'beadtype', 'userid', 'version', 'parameters','RULES','GLOBAL','LOCAL','USER'}
    # Class attribute: construction flag is True by default for all instances
    _in_construction = True
    RULES = parameterforcefield()  # Default empty RULES at the class level

    def __init__(self, base_class=None, beadtype=1, userid=None, USER=parameterforcefield(),
                 name=None, description=None, version=0.1, additional_modules=None,
                 printflag=False, verbose=False,
                 GLOBAL=None, LOCAL=None, **kwargs):
        """
        Initialize a dynamic forcefield with default or custom values.

        Args:
            base_class (str or class): The base class to use (e.g., 'ulsph', tlsph, etc.) or the actual class.
            beadtype (int): The bead type identifier. Default is 1.
            userid (str): User ID for the material. Default is None.
            name (str): Human-readable name for the forcefield. Default is None.
            description (str): Description of the forcefield. Default is None.
            version (float): Version number for the forcefield. Default is 0.1.
            USER (parameterforcefield): Custom parameters to override defaults.
            additional_modules (module or list of modules): Additional modules to search for forcefields.
            GLOBAL (parameterforcefield): Global parameters to be used. Default is None.
            LOCAL (parameterforcefield): Local parameters to be used. Default is None.
            kwargs: Additional parameters passed to the base class.
        """

        # Initialize GLOBAL and LOCAL containers
        self.GLOBAL = GLOBAL if GLOBAL else genericdata()
        self.LOCAL = LOCAL if LOCAL else genericdata()
        self.RULES = genericdata()  # Initialize RULES, to be populated later if applicable

        # Initialize USER containter (which is used by scriptobject)
        self.USER = scriptdata()

        #• Initialize print/verbose behavior
        self.printflag = printflag
        self.verbose = verbose

        # Step 1a: Handle base_class, either a string or a class reference
        print(f"\nInitializing dforcefield with base_class: {base_class}")

        # Handle base_class loading (using additional_modules if needed)
        if isinstance(base_class, str):
            # Get available forcefields and short names mapping
            available_forcefields, short_name_mapping = self.list_forcefield_subclasses(
                printflag=False,
                additional_modules=additional_modules
            )
            # Check if the provided base_class name matches either the full or short name
            if base_class not in available_forcefields and base_class not in short_name_mapping:
                available_classes = ", ".join(short_name_mapping.keys())
                raise ValueError(
                    f"Invalid base_class: '{base_class}'. Must be one of the available forcefields: {available_classes}"
            )
            # Use the full name from the short name if necessary
            # if base_class in short_name_mapping:
            #     base_class = short_name_mapping[base_class]

            # Load the base class or method
            base_class = dforcefield._load_base_class(base_class, additional_modules=additional_modules)

        # Ensure the base_class is valid
        if not (inspect.isclass(base_class) and issubclass(base_class, forcefield)) and not (
            callable(base_class) and hasattr(base_class, '__qualname__') and
            base_class.__qualname__.split('.')[0] in {cls.__name__ for cls in generic.__subclasses__()}
        ):
            raise ValueError(
                f"Invalid base_class: {base_class}. It must be a valid subclass of 'forcefield' or a callable forcefield method "
                f"from a 'generic' subclass."
        )

        # Step 1b: Initialize the base_class
        # If base_class is callable (like a method), call it to get an instance; otherwise, instantiate directly
        if callable(base_class):
            # Check if the base_class is a method of a generic subclass
            qualname = base_class.__qualname__.split('.')
            parent_class_name = qualname[0] if len(qualname) > 1 else None
            parent_class = next((cls for cls in generic.__subclasses__() if cls.__name__ == parent_class_name), None)

            if parent_class:
                # Instantiate the parent class and call the method on it
                parent_instance = parent_class()  # Create an instance of the parent class (e.g., USERSMD)
                self.base_class = base_class(parent_instance)  # Call the method with the instance context
            else:
                # Call the method directly if no parent class context is needed
                self.base_class = base_class()
        else:
            self.base_class = base_class()  # Instantiate the class if it's not a method

        # Ensure the base_class is valid and handle classes and methods separately
        if inspect.isclass(base_class):
            # Step 2a: Extract default parameters from the base class
            extracted_info = self.extract_default_parameters(base_class, displayflag=True)
        elif callable(base_class) and hasattr(base_class, '__qualname__'):
            # If base_class is a method, extract the parent class name
            parent_class_name = base_class.__qualname__.split('.')[0]
            parent_class = next(
                (cls for cls in generic.__subclasses__() if cls.__name__ == parent_class_name), None
            )
            if parent_class is None:
                raise ValueError(f"Unable to find parent class '{parent_class_name}' for method '{base_class.__name__}'.")

            # Step 2b: Extract default parameters from the method within its parent class
            extracted_info = self.extract_default_parameters(parent_class, method_name=base_class.__name__, displayflag=True)
        else:
            raise ValueError(
                f"Invalid base_class: {base_class}. It must be a valid subclass of 'forcefield' or a callable forcefield method."
            )

        # Populate RULES, GLOBAL, and LOCAL if extracted
        self.RULES = extracted_info.get("RULES", genericdata())
        self.GLOBAL = extracted_info.get("GLOBAL", genericdata()) + self.GLOBAL
        self.LOCAL = extracted_info.get("LOCAL", genericdata()) + self.LOCAL

        # Step 3: Initialize the base_class
        #self.base_class = base_class()  # Instantiate the base_class if it's callable

        # Step 4: Initialize name and description
        default_name = self.base_class.name  # Access base_class.name after ensuring it's properly set

        # Handling 'name'
        if name:
            if isinstance(name, str):
                name = default_name + struct(material=name)
            elif isinstance(name, struct):
                name = default_name + name
            else:
                raise TypeError(f"name must be of type str or struct, not {type(name)}")
        else:
            name = default_name + struct(material=self.__class__.__name__.lower())

        self.name = name

        # Handling 'description'
        default_description = self.base_class.description
        if description:
            if isinstance(description, str):
                description = default_description + struct(material=description)
            elif isinstance(description, struct):
                description = default_description + description
            else:
                raise TypeError(f"description must be of type parameterforcefield, not {type(description)}")
        else:
            description = default_description + struct(material=f"{self.__class__.__name__.lower()} beads - SPH-like")

        self.description = description

        # Step 5: Other properties and parameters
        self.userid = userid if userid else self.__class__.__name__.lower()
        self.version = version
        self.beadtype = beadtype

        # Ensure USER is of the correct type
        if not isinstance(USER, parameterforcefield):
            raise TypeError(f"USER must be of type parameterforcefield, not {type(USER)}")

        # Merge USER and kwargs into parameters
        # If no default parameters, proceed with user parameters
        if extracted_info["parameters"] is None:
            self.parameters = self.GLOBAL + self.LOCAL + self.RULES + USER + parameterforcefield(**kwargs)
        else:
            self.parameters = extracted_info["parameters"] + self.GLOBAL + self.LOCAL + self.RULES + USER + parameterforcefield(**kwargs)
        # After merging parameters, ensure USER is not part of it
        # if 'USER' in self.parameters:
        #     del self.parameters['USER']

        # End of construction
        self._in_construction = False

        # Inject dforcefield attributes into the base class
        self._inject_attributes()


    # New methods to access GLOBAL, LOCAL, and RULES

    def get_global(self):
        """Return the GLOBAL parameters for this dforcefield instance."""
        return self.GLOBAL

    def get_local(self):
        """Return the LOCAL parameters for this dforcefield instance."""
        return self.LOCAL

    def get_rules(self):
        """Return the RULES parameters for this dforcefield instance."""
        return self.RULES

    # Method to combine GLOBAL, LOCAL, and RULES
    def combine_parameters(self):
        """
        Combine GLOBAL, LOCAL, and RULES to get the current parameter configuration.
        """
        return self.GLOBAL + self.LOCAL + self.RULES


    def update_parameters(self):
        """
        Update self.parameters by combining GLOBAL, LOCAL, RULES, and USER parameters.
        """
        self.parameters = self.parameters + self.combine_parameters()
        self._inject_attributes()


    def set_local(self, new_local=None, **kwargs):
        """
        Update the LOCAL parameters and adjust the combined parameters accordingly.

        Args:
        -----
        new_local : parameterforcefield, optional
            The new LOCAL parameters to set. If None, only the parameters in kwargs are modified.
        kwargs : dict, optional
            Keyword arguments representing individual parameters to add or modify in LOCAL.
        """
        if new_local is not None and not isinstance(new_local, parameterforcefield):
            raise TypeError(f"LOCAL must be of type parameterforcefield, not {type(new_local)}.")

        # Combine current LOCAL, new_local, and additional parameters in kwargs
        self.LOCAL = (self.LOCAL + (new_local or genericdata()) + genericdata(**kwargs))
        self.update_parameters()

    def set_global(self, new_global=None, **kwargs):
        """
        Update the GLOBAL parameters and adjust the combined parameters accordingly.

        Args:
        -----
        new_global : parameterforcefield, optional
            The new GLOBAL parameters to set. If None, only the parameters in kwargs are modified.
        kwargs : dict, optional
            Keyword arguments representing individual parameters to add or modify in GLOBAL.
        """
        if new_global is not None and not isinstance(new_global, parameterforcefield):
            raise TypeError(f"GLOBAL must be of type parameterforcefield, not {type(new_global)}.")

        # Combine current GLOBAL, new_global, and additional parameters in kwargs
        self.GLOBAL = (self.GLOBAL + (new_global or genericdata()) + genericdata(**kwargs))
        self.update_parameters()

    def set_rules(self, new_rules=None, **kwargs):
        """
        Update the RULES parameters and adjust the combined parameters accordingly.

        Args:
        -----
        new_rules : parameterforcefield, optional
            The new RULES parameters to set. If None, only the parameters in kwargs are modified.
        kwargs : dict, optional
            Keyword arguments representing individual parameters to add or modify in RULES.
        """
        if new_rules is not None and not isinstance(new_rules, parameterforcefield):
            raise TypeError(f"RULES must be of type parameterforcefield, not {type(new_rules)}.")

        # Combine current RULES, new_rules, and additional parameters in kwargs
        self.RULES = (self.RULES + (new_rules or genericdata()) + genericdata(**kwargs))
        self.update_parameters()


    @classmethod
    def _load_base_class(cls, base_class_name, additional_modules=None):
        """
        Dynamically load the base class by its string name from the available forcefield subclasses
        or additional modules.

        Args:
            base_class_name (str): The name of the forcefield class or method to load.
            additional_modules (list): Optional list of additional modules to search for the base class.

        Returns:
            class or function: The base class or function if found.
        """
        # Retrieve subclass information and short name mappings
        subclasses_info, short_name_mapping = cls.list_forcefield_subclasses(
            printflag=False, additional_modules=additional_modules
            )
        print(f"Trying to load base_class: {base_class_name}")

        # Directly look for the exact name in the subclasses_info
        if base_class_name in subclasses_info:
            info = subclasses_info[base_class_name]
            if not info['loaded']:
                module_name = info['module']
                try:
                    print(f"Attempting to import module '{module_name}' and load '{base_class_name}'")
                    module = __import__(module_name, fromlist=[base_class_name])
                    target = getattr(module, base_class_name)

                    # Check if the target is a class or method
                    if inspect.isclass(target) and issubclass(target, forcefield):
                        return target
                    elif inspect.isfunction(target):
                        return target
                    else:
                        raise ValueError(f"Loaded target '{base_class_name}' is not a suitable forcefield class or method.")
                except (ImportError, AttributeError) as e:
                    available_classes = cls._get_available_forcefields()
                    raise ImportError(f"Failed to load the target '{base_class_name}' from module '{module_name}'. "
                                      f"Available forcefields: {available_classes}") from e

            # If already loaded, return the class directly
            return sys.modules[info['module']].__dict__.get(base_class_name)

        # If not found directly, check for nested methods within classes
        for key, info in subclasses_info.items():
            # Check if base_class_name is a method of a class
            if '.' in key and key.split('.')[-1] == base_class_name:
                parent_class_name = key.split('.')[0]
                if parent_class_name in subclasses_info:
                    # Load the parent class if it hasn't been loaded yet
                    parent_class_info = subclasses_info[parent_class_name]
                    if not parent_class_info['loaded']:
                        parent_class = cls._load_base_class(parent_class_name, additional_modules=additional_modules)
                    else:
                        # Directly access the parent class from the loaded module
                        module = sys.modules[parent_class_info['module']]
                        parent_class = getattr(module, parent_class_name)

                    # Get the method from the loaded parent class
                    method = getattr(parent_class, base_class_name, None)
                    if method and callable(method):
                        return method
                    else:
                        raise ValueError(f"Method '{base_class_name}' was not found in the parent class '{parent_class_name}' or is not callable.")

        # If the target is a direct class but not a method or nested class, throw an appropriate error
        raise ValueError(f"Target '{base_class_name}' is not a recognized class or method. "
                         f"Available entries: {cls._get_available_forcefields()}")


    @classmethod
    def _get_available_forcefields(cls):
        """
        Retrieve the list of available forcefield subclasses.

        Returns:
            list: A list of available forcefield subclass names.
        """
        subclasses_info = cls.list_forcefield_subclasses(printflag=False)
        return list(subclasses_info.keys())


    @classmethod
    def extract_default_parameters(cls, base_class, method_name=None, displayflag=True):
        """
        Extract default parameters from the base class or its method by instantiating the class.

        If the base class is derived from `generic`, this function will also extract RULES, LOCAL,
        and GLOBAL attributes if they are defined once the instance is created.

        Parameters:
        -----------
        base_class : class
            The base class from which to extract default parameterforcefield values or a method.
        method_name : str, optional
            The name of the method to call on the instance, if applicable (e.g., `newtonianfluid`).
        displayflag : bool, optional (default=True)
            If True, prints a message when no default parameters are found.

        Returns:
        --------
        dict
            A dictionary containing the default parameters extracted from the base class or method.
            Also includes RULES, LOCAL, and GLOBAL if applicable.
        """
        extracted_info = {
            "parameters": parameterforcefield(),
            "RULES": genericdata(),
            "GLOBAL": genericdata(),
            "LOCAL": genericdata()
        }

        try:
            # Create an instance of the base class
            instance = base_class()

            # Extract parameters if the instance has them
            if hasattr(instance, 'parameters') and isinstance(instance.parameters, parameterforcefield):
                extracted_info["parameters"] = instance.parameters

            # Extract RULES, GLOBAL, and LOCAL if they exist
            for attr in ["RULES", "GLOBAL", "LOCAL"]:
                if hasattr(instance, attr) and isinstance(getattr(instance, attr), parameterforcefield):
                    extracted_info[attr] = getattr(instance, attr)

            # If a method is specified, call it to get additional parameters
            if method_name and hasattr(instance, method_name):
                method = getattr(instance, method_name)
                if callable(method):
                    try:
                        result = method()  # Call the method (ensure it returns the desired forcefield object)
                        if hasattr(result, 'parameters') and isinstance(result.parameters, parameterforcefield):
                            extracted_info["parameters"] = result.parameters
                    except Exception as e:
                        if displayflag:
                            print(f"Error calling method {method_name} on {base_class.__name__}: {str(e)}")

        except TypeError as e:
            if displayflag:
                print(f"Could not instantiate {base_class.__name__}: {str(e)}")

        # If no parameters found or cannot instantiate the class, return None
        if displayflag and not extracted_info["parameters"]:
            print(f"No default parameters found in {base_class.__name__} or its ancestors.")

        return extracted_info


    def _inject_attributes(self):
        """Inject dforcefield attributes into the base class, bypassing __setattr__."""
        if not self._in_construction:  # Prevent injection during construction
            # Check if base_class is a class (not a method)
            if inspect.isclass(self.base_class):
                self.base_class.__dict__.update({
                    'name': self.name,
                    'description': self.description,
                    'beadtype': self.beadtype,
                    'userid': self.userid,
                    'version': self.version,
                    'parameters': self.parameters
                })
            else:
                # For methods, set attributes on the instance itself, as methods can't have attributes directly
                # Methods are often accessed from an instance, so we update the instance's attributes instead
                self.base_class.name = self.name
                self.base_class.description = self.description
                self.base_class.beadtype = self.beadtype
                self.base_class.userid = self.userid
                self.base_class.version = self.version
                self.base_class.parameters = self.parameters


    def __getattr__(self, attr):
        """
        Shorthand for accessing parameters, base class attributes, or attributes in 'name' and 'description'.
        If an attribute exists in both 'name' and 'description', their contents are combined with a newline.
        """
        # Check if the attribute exists in the instance's __dict__
        if attr in self.__dict__:
            return self.__dict__[attr]
        # Check if the attribute exists in 'name' and 'description'
        name_attr = getattr(self.name, attr, None) if isinstance(self.name, struct) else None
        description_attr = getattr(self.description, attr, None) if isinstance(self.description, struct) else None
        # If the attribute exists in both 'name' and 'description', combine them with a newline
        if name_attr and description_attr:
            return [name_attr, description_attr]
        # If the attribute exists in 'name' only
        if name_attr:
            return name_attr
        # If the attribute exists in 'description' only
        if description_attr:
            return description_attr
        # Check if the attribute exists in 'parameters'
        if hasattr(self.parameters, attr):
            return getattr(self.parameters, attr)
        # Check if the attribute exists in the 'base_class'
        if hasattr(self.base_class, attr):
            return getattr(self.base_class, attr)
        # Raise an AttributeError if the attribute is not found
        raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{attr}'")



    def __setattr__(self, attr, value):
        """
        Shorthand for setting attributes. Attributes specific to dforcefield are handled separately.
        New attributes are added to parameters if they are not part of the dforcefield-specific attributes.
        """
        # Handle internal attributes like _in_construction directly in the instance
        # and Check if the attribute is part of the dforcefield-specific attributes
        if hasattr(self.__class__, attr) or attr in self._dforcefield_specific_attributes:
            if attr=="parameters":
                if not isinstance(value,parameterforcefield):
                    raise TypeError("parameters must be of type parameterforcefield, not of type {}".format(type(value)))
                self.__dict__[attr] = self.detectVariables() + value # check that all variables are defined
            else:
                self.__dict__[attr] = value
            if not self._in_construction and attr in self._dforcefield_specific_attributes:
                self._inject_attributes()
        # Handle dynamic setting of attributes in parameters
        elif 'parameters' in self.__dict__:
            # Update parameters (existing or new attributes)
            setattr(self.parameters, attr, value)

            # Ensure injection occurs when parameters is updated
            if not self._in_construction:
                self._inject_attributes()
        else:
            # Fallback to default behavior if the attribute isn't part of parameters or specific attributes
            super().__setattr__(attr, value)


    def __hasattr__(self, attr):
        """Check if an attribute exists in the dforcefield instance, class, parameters, or the base class."""
        # Check for instance attribute
        if attr in self.__dict__:
            return True
        # Check for class attribute
        if hasattr(self.__class__, attr):
            return True
        # Check for parameters
        if attr in self.parameters.keys():
            return True
        # Check for attribute in base class
        if hasattr(self.base_class, attr):
            return True
        return False


    def __contains__(self, item):
        """Check if an attribute exists in the dforcefield instance or its parameters."""
        return item in self.keys()


    def __len__(self):
        """
        Return the number of parameters in the forcefield.
        This will use the len method of parameters.
        """
        return len(self.keys())


    @property
    def merged_name_description(self):
        """
        Return a struct containing the merged content of 'name' and 'description'.
        If an attribute exists in both, their values are combined into a list.
        """
        merged_data = {}

        # Add attributes from 'name' if it's a struct
        if isinstance(self.name, struct):
            merged_data.update(self.name.__dict__)

        # Add/merge attributes from 'description' if it's a struct
        if isinstance(self.description, struct):
            for key, value in self.description.__dict__.items():
                if key in merged_data:
                    # Combine the values into a list if the key already exists
                    existing_value = merged_data[key]
                    if not isinstance(existing_value, list):
                        existing_value = [existing_value]  # Convert to list if not already a list
                    merged_data[key] = existing_value + [value]  # Add the new value to the list
                else:
                    merged_data[key] = value

        # Return the merged struct
        return struct(**merged_data)



    def keys(self):
        """
        Return the keys of the merged struct, parameters, and scalar attributes.
        """
        keys_set = set(self.merged_name_description.__dict__.keys())
        keys_set.update(self.parameters.keys())
        keys_set.update(['version', 'userid', 'beadtype'])
        return list(keys_set)

    def values(self):
        """
        Return the values of the merged struct, parameters, and scalar attributes.
        """
        values_list = list(self.merged_name_description.__dict__.values())
        values_list.extend(self.parameters.values())
        values_list.extend([self.version, self.userid, self.beadtype])
        return values_list

    def items(self):
        """
        Return an iterator over (key, value) pairs from the merged struct, parameters, and scalar attributes.
        """
        # Yield items from merged struct
        for item in self.merged_name_description.__dict__.items():
            yield item

        # Yield items from parameters
        for item in self.parameters.items():
            yield item

        # Yield scalar attribute items
        yield ('version', self.version)
        yield ('userid', self.userid)
        yield ('beadtype', self.beadtype)

    def __iter__(self):
        """
        Iterate over all keys, including those in the merged struct, parameters, and scalar attributes.
        """
        for key in self.merged_name_description.__dict__:
            yield key
        for key in self.parameters:
            yield key
        yield 'version'
        yield 'userid'
        yield 'beadtype'


    def __repr__(self):
        """
        Custom __repr__ method that indicates it is a dforcefield instance and provides information
        about the base class, name, and parameters dynamically.
        """
        base_class_name = self.base_class.__class__.__name__ if self.base_class else "None"
        # Generate the name table dynamically (iterating over key-value pairs)
        sep = "  "+"-"*20+":"+"-"*30
        name_table = sep + "[ BEADTYPE ]\n"
        key = "beadtype"
        name_table += f"  {key:<20}: {self.beadtype}\n"
        name_table += sep + "[ FF CLASS (read only) ]\n"
        # Generate the name table dynamically (iterating over key-value pairs)
        for key, value in self.merged_name_description.items():
            # Check if the value is a list (when the field exists in both name and description)
            if isinstance(value, list):
                for idx, val in enumerate(value):
                    value_lines = self.dispmax(val).splitlines()

                    # Apply {key:<20} to the first line of the first item in the list
                    if idx == 0:
                        name_table += f"  {key:<20}: {value_lines[0]}\n"
                    else:
                        name_table += f"  {'':<20}  {value_lines[0]}\n"  # Align subsequent items
                    # Handle multi-line values within each list item
                    for line in value_lines[1:]:
                        name_table += f"  {'':<20}  {line}\n"  # Indent the remaining lines
            else:
                # Handle non-list values
                value_lines = self.dispmax(value).splitlines()
                # Apply {key:<20} to the first line and indent the subsequent lines
                name_table += f"  {key:<20}: {value_lines[0]}\n"  # First line with key aligned
                for line in value_lines[1:]:
                    name_table += f"  {'':<20}  {line}\n"  # Indent the remaining lines
        # Evaluate all values of parameters
        tmp = self.parameters.eval()
        tmpkey= ""
        # Generate the parameters table dynamically (iterating over key-value pairs)
        parameters_table = sep+ "[ PARAMS ]\n"
        missing = 0
        for key, value in self.parameters.items():  # Assuming parameters is a class with __dict__
            parameters_table += f"  {key:<20}: {value}"  # Align the keys and values dynamically
            if isinstance(value,str):
                if value=="${"+key+"}":
                    parameters_table += "\t < implicit definition >\n"
                    missing += 1
                else:
                    parameters_table += f"\n  {tmpkey:<20}= {self.dispmax(tmp.getattr(key))}\n"
            else:
                parameters_table += "\n"
        parameters_table += sep+f" >> {len(self.parameters)} definitions ({missing} missing)\n"

        # Combine the name and parameters table with a general instance representation
        print(f'\ndforcefield "{self.userid}" derived from "{base_class_name}"\n{name_table}{parameters_table}\n')
        return self.__str__()

    def base_repr(self):
        """Returns the representation of the base_class."""
        self.base_class.__repr__()
        return self.base_class.__str__()

    @property
    def script(self):
        """
        Return the raw content of the pair_* outputs combined into a single script-like format.

        Returns:
        --------
        str
            The raw outputs from pair_style, pair_diagcoeff, and pair_offdiagcoeff concatenated into a script.
        """
        # Get the raw outputs from the pair_* methods
        pair_style_output = self.pair_style(printflag=False, raw=True)
        pair_diagcoeff_output = self.pair_diagcoeff(printflag=False, raw=True)
        pair_offdiagcoeff_output = self.pair_offdiagcoeff(printflag=False, raw=True)

        # Combine the outputs into a script-like format
        script_content = "\n".join([
            "# Script output from dforcefield:",
            "# Pair style:",
            pair_style_output,
            "\n# Diagonal coefficients:",
            pair_diagcoeff_output,
            "\n# Off-diagonal coefficients:",
            pair_offdiagcoeff_output
        ])

        return script_content

    def __str__(self):
        base_class_name = self.base_class.__class__.__name__ if self.base_class else "None"
        return f"<dforcefield instance with base class: {base_class_name}, userid: {self.userid}, beadtype: {self.beadtype}>"

    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 show(self):
        """Show the corresponding base_class forcefield definition """
        self.base_class.__repr__()
        return self.base_class.__str__()

    def pair_style(self, printflag=None, verbose=None, raw=False):
        """Delegate pair_style to the base class, ensuring it uses the correct attributes."""
        printflag = self.printflag if printflag is None else printflag
        verbose = self.verbose if verbose is None else verbose
        self._inject_attributes()
        return self.base_class.pair_style(printflag=printflag, verbose=verbose, raw=raw,USER=None, beadtype=self.beadtype, userid=self.userid)


    def pair_diagcoeff(self, printflag=None, verbose=None, i=None, raw=False):
        """Delegate pair_diagcoeff to the base class, ensuring it uses the correct attributes."""
        printflag = self.printflag if printflag is None else printflag
        verbose = self.verbose if verbose is None else verbose
        self._inject_attributes()
        return self.base_class.pair_diagcoeff(printflag=printflag, verbose=verbose, i=i, raw=raw, USER=None, beadtype=self.beadtype, userid=self.userid)


    def pair_offdiagcoeff(self, o=None, printflag=None, verbose=None, i=None, raw=False,oname=None):
        """Delegate pair_offdiagcoeff to the base class, ensuring it uses the correct attributes."""
        printflag = self.printflag if printflag is None else printflag
        verbose = self.verbose if verbose is None else verbose
        self._inject_attributes()
        return self.base_class.pair_offdiagcoeff(o=o, printflag=printflag, verbose=verbose, i=i, raw=raw, USER=None, beadtype=self.beadtype, userid=self.userid,oname=oname)


    def __add__(self, other):
        """Concatenate dforcefield attributes, i

        ncluding RULES, GLOBAL, and LOCAL, if different, else keep the version of self."""
        if not isinstance(other, dforcefield):
            raise TypeError(f"Cannot concatenate {type(other)} with dforcefield.")

        if self.base_class != other.base_class:
            raise ValueError(f"Cannot concatenate dforcefield instances with different base classes ({self.base_class} != {other.base_class}).")

        # Concatenate name, description, beadtype, userid, and parameters if different, else keep self's version
        new_name = self.name if self.name == other.name else f"{self.name}_{other.name}"
        new_description = self.description if self.description == other.description else f"{self.description} + {other.description}"
        new_beadtype = self.beadtype if self.beadtype == other.beadtype else f"{self.beadtype}, {other.beadtype}"
        new_parameters = self.parameters + other.parameters  # Assuming parameters supports '+'
        new_userid = self.userid if self.userid == other.userid else f"{self.userid}_{other.userid}"

        # Concatenate RULES, GLOBAL, and LOCAL, preserving their combined nature
        new_rules = self.RULES + other.RULES
        new_global = self.GLOBAL + other.GLOBAL
        new_local = self.LOCAL + other.LOCAL

        # Keep the version from self
        new_version = self.version

        # Return a new dforcefield instance with concatenated attributes
        return dforcefield(
            base_class=self.base_class,
            beadtype=new_beadtype,
            userid=new_userid,
            name=new_name,
            description=new_description,
            version=new_version,
            USER=new_parameters,
            RULES=new_rules,
            GLOBAL=new_global,
            LOCAL=new_local
        )


    def __or__(self, other):
        """ Overload | pipe operator in dscript """
        # Convert the dscript instance into a pipescript
        leftarg = self.pscript()
        # Simply use the existing pipe operator for pipescript
        return leftarg | other


    def copy(self, beadtype=None, userid=None, name=None, description=None, version=None, USER=parameterforcefield(), RULES=None, GLOBAL=None, LOCAL=None, **kwargs):
        """
        Create a new instance of dforcefield with the option to override key attributes including RULES, GLOBAL, and LOCAL.
        """
        # Use the current instance's values as defaults, and override if values are provided
        new_beadtype = beadtype if beadtype is not None else self.beadtype
        new_userid = userid if userid is not None else self.userid
        new_name = name if name is not None else self.name
        new_description = description if description is not None else self.description
        new_version = version if version is not None else self.version
        if new_userid == self.userid:
            new_userid = new_userid + " (copy)"
        # USER is computed by combining self.parameters + USER + parameterforcefield(**kwargs)
        new_parameters = self.parameters + USER

        # Combine or override RULES, GLOBAL, and LOCAL if provided
        new_rules = RULES if RULES is not None else self.RULES
        new_global = GLOBAL if GLOBAL is not None else self.GLOBAL
        new_local = LOCAL if LOCAL is not None else self.LOCAL

        # Create and return the new instance with overridden values
        return self.__class__(
            base_class=self.base_class.__class__,
            beadtype=new_beadtype,
            userid=new_userid,
            name=new_name,
            description=new_description,
            version=new_version,
            USER=new_parameters,
            RULES=new_rules,
            GLOBAL=new_global,
            LOCAL=new_local,
            **kwargs
        )


    def update(self, **kwargs):
        """
        Update multiple attributes of the dforcefield instance at once, including RULES, GLOBAL, and LOCAL.

        Args:
            kwargs: Key-value pairs of attributes to update.
        """
        for key, value in kwargs.items():
            if key == "RULES":
                self.RULES = self.RULES + value if self.RULES else value
            elif key == "GLOBAL":
                self.GLOBAL = self.GLOBAL + value if self.GLOBAL else value
            elif key == "LOCAL":
                self.LOCAL = self.LOCAL + value if self.LOCAL else value
            else:
                setattr(self, key, value)



    def to_dict(self):
        """
        Serialize the dforcefield instance to a dictionary, including RULES, GLOBAL, and LOCAL.

        Returns:
            dict: A dictionary containing all the attributes and their current values.

        Example:
            config = dynamic_water.to_dict()
            print(config)
        """
        data = {
            "name": self.name,
            "description": self.description,
            "beadtype": self.beadtype,
            "userid": self.userid,
            "version": self.version,
            "parameters": dict(self.parameters),  # Convert parameters to a standard dict
            "base_class": self.base_class.__class__.__name__,
            "RULES": dict(self.RULES) if self.RULES else {},  # Include RULES if defined
            "GLOBAL": dict(self.GLOBAL) if self.GLOBAL else {},  # Include GLOBAL if defined
            "LOCAL": dict(self.LOCAL) if self.LOCAL else {}  # Include LOCAL if defined
        }
        return data



    @classmethod
    def from_dict(cls, data):
        """
        Create a dforcefield instance from a dictionary, including RULES, GLOBAL, and LOCAL.

        Args:
            data (dict): A dictionary containing the attributes to initialize the dforcefield.

        Returns:
            dforcefield: A new dforcefield instance.

        Example:
            config = {
                "name": "water_ff",
                "description": "water forcefield",
                "beadtype": 2,
                "userid": "water_sim",
                "version": 1.0,
                "parameters": {"rho": 1000, "sigma": 0.5},
                "base_class": "ulsph",
                "RULES": {"some_rule": "value"},
                "GLOBAL": {"global_param": 10},
                "LOCAL": {"local_param": 5}
            }

            new_ff = dforcefield.from_dict(config)
        """
        # Extract base class information
        base_class_name = data.pop("base_class", None)
        base_class = globals().get(base_class_name) if base_class_name else None

        # Extract RULES, GLOBAL, and LOCAL from the data dictionary if they exist
        rules = data.pop("RULES", parameterforcefield())
        global_params = data.pop("GLOBAL", parameterforcefield())
        local_params = data.pop("LOCAL", parameterforcefield())

        # Create and return the new dforcefield instance with extracted or default RULES, GLOBAL, and LOCAL
        return cls(
            base_class=base_class,
            RULES=rules,
            GLOBAL=global_params,
            LOCAL=local_params,
            **data
        )


    def reset(self):
        """
        Reset the dforcefield instance to its initial state, reapplying the default values
        including RULES, GLOBAL, and LOCAL.
        """
        # Preserve the current RULES, GLOBAL, and LOCAL or reset them
        current_rules = self.RULES
        current_global = self.GLOBAL
        current_local = self.LOCAL

        # Reinitialize the instance with existing or reset RULES, GLOBAL, and LOCAL
        self.__init__(
            base_class=self.base_class.__class__,
            beadtype=self.beadtype,
            userid=self.userid,
            name=self.name,
            description=self.description,
            version=self.version,
            RULES=current_rules,
            GLOBAL=current_global,
            LOCAL=current_local
            )

    def validate(self):
        """
        Validate the dforcefield instance to ensure all required attributes are set.

        Raises:
            ValueError: If any required attributes are missing or invalid.
        """
        required_fields = self._dforcefield_specific_attributes

        for field in required_fields:
            if not getattr(self, field):
                raise ValueError(f"{field} is required and not set.")

        print("Validation successful.")


    def compare(self, other, printflag=False):
        """
        Compare the current instance with another dforcefield instance, including RULES, GLOBAL, and LOCAL.

        Args:
        -----
        other : dforcefield
            The other instance to compare.

        printflag : bool, optional (default: False)
            If True, prints a table showing all parameters, with differences marked by '*'.

        Returns:
        --------
        dict
            A dictionary showing differences between the two instances.

        Raises:
        -------
        TypeError: If the other object is not a dforcefield instance.

        Example:
        --------
        diffs = dynamic_water.compare(dynamic_salt)
        if printflag:
            Prints a comparison table with a type column and a legend at the end.
        """
        # Define abbreviations for types
        type_abbreviations = {
            str: 'STR',
            int: 'INT',
            float: 'FLT',
            bool: 'BOOL',
            list: 'LST',
            tuple: 'TPL',
            dict: 'DCT',
            None: 'NON',
            'missing': 'MIS'
        }

        def get_type_abbrev(value):
            """Return the abbreviation for the type of the value."""
            if value is None:
                return type_abbreviations[None]
            return type_abbreviations.get(type(value), 'UNK')  # Use 'UNK' for unknown types

        def format_simple_type(value):
            """Format a simple value (str, number, or bool) for display."""
            return self.dispmax(str(value))

        def format_iterable(value_self, value_other, key, comparison_table, difference):
            """Handle lists, tuples, and other iterable types."""
            max_length = max(len(value_self), len(value_other))
            for i in range(max_length):
                item_self = value_self[i] if i < len(value_self) else 'MISSING'
                item_other = value_other[i] if i < len(value_other) else 'MISSING'
                sub_difference = '*' if item_self != item_other else ' '
                comparison_table.append(
                    f"  {key if i == 0 else '':<15}: {sub_difference:^3}: {self.dispmax(str(item_self)):<30}: {get_type_abbrev(item_self):<5}: {self.dispmax(str(item_other)):<30}: {get_type_abbrev(item_other):<5}"
                )

        def format_dict_like(value_self, value_other, key, comparison_table, difference):
            """Handle dictionary-like objects (e.g., structs or dicts)."""
            comparison_table.append(f"  {key:<15}: {difference:^3}: ")
            comparison_table.append(f"  {'':<15}  {'Self':<30} {'Type':<5} {'Other':<30} {'Type':<5}")
            sub_sep = "  "+"-"*15+":"+"-"*3+":"+"-"*30+":"+"-"*5+":"+"-"*30+":"+"-"*5
            comparison_table.append(sub_sep)
            sub_keys = set(value_self.keys()).union(value_other.keys())
            for sub_key in sorted(sub_keys):
                sub_value_self = value_self.get(sub_key, 'MISSING')
                sub_value_other = value_other.get(sub_key, 'MISSING')
                sub_difference = '*' if sub_value_self != sub_value_other else ' '
                comparison_table.append(
                    f"  {sub_key:<15}: {sub_difference:^3}: {self.dispmax(str(sub_value_self)):<30}: {get_type_abbrev(sub_value_self):<5}: {self.dispmax(str(sub_value_other)):<30}: {get_type_abbrev(sub_value_other):<5}"
                )
            comparison_table.append(sub_sep)

        def format_complex_type(value_self, value_other, key, comparison_table, difference):
            """Handle complex types, expanding them into multiple lines."""
            value_self_lines = str(value_self).splitlines()
            value_other_lines = str(value_other).splitlines()
            max_lines = max(len(value_self_lines), len(value_other_lines))
            for i in range(max_lines):
                line_self = value_self_lines[i] if i < len(value_self_lines) else 'MISSING'
                line_other = value_other_lines[i] if i < len(value_other_lines) else 'MISSING'
                comparison_table.append(
                    f"  {key if i == 0 else '':<15}: {difference if i == 0 else ' ':^3}: {self.dispmax(line_self):<30}: {get_type_abbrev(line_self):<5}: {self.dispmax(line_other):<30}: {get_type_abbrev(line_other):<5}"
                )

        if not isinstance(other, dforcefield):
            raise TypeError("Can only compare with another dforcefield instance.")

        diffs = {}

        # Collect all keys from both instances, including RULES, GLOBAL, and LOCAL
        all_keys = set(self.keys()).union(other.keys(), {'RULES', 'GLOBAL', 'LOCAL'})

        # Iterate over all keys and compare the values
        for key in all_keys:
            value_self = getattr(self, key, None)
            value_other = getattr(other, key, None)

            if value_self != value_other:
                diffs[key] = {"self": value_self, "other": value_other}

        # If printflag is True, print the comparison in a table format
        if printflag:
            sep = "  "+"-"*15 + ":" + "-"*3 + ":" + "-"*30 + ":" + "-"*5 + ":" + "-"*30 + ":" + "-"*5
            header = f"\n  {'Attribute':<15}: {'*':^3}: {'Self':<30}: {'Type':<5}: {'Other':<30}: {'Type':<5}\n" + sep
            comparison_table = [header]

            # Iterate over all parameters and mark differences with '*'
            for key in sorted(all_keys):  # Sort keys for a cleaner output
                value_self = getattr(self, key, 'MISSING')
                value_other = getattr(other, key, 'MISSING')
                difference = '*' if value_self != value_other else ' '

                # Check if the value is a simple type (str, number, or bool)
                if isinstance(value_self, (str, int, float, bool)) and isinstance(value_other, (str, int, float, bool)):
                    comparison_table.append(
                        f"  {key:<15}: {difference:^3}: {format_simple_type(value_self):<30}: {get_type_abbrev(value_self):<5}: {format_simple_type(value_other):<30}: {get_type_abbrev(value_other):<5}"
                    )
                # Check if the value is a list or tuple
                elif isinstance(value_self, (list, tuple)) and isinstance(value_other, (list, tuple)):
                    format_iterable(value_self, value_other, key, comparison_table, difference)
                # Check if the value is a dictionary-like object
                elif hasattr(value_self, 'keys') and hasattr(value_other, 'keys'):
                    format_dict_like(value_self, value_other, key, comparison_table, difference)
                else:
                    # Handle other complex types
                    format_complex_type(value_self, value_other, key, comparison_table, difference)

            comparison_table.append(sep)
            # Add a legend for type abbreviations
            legend = "\nType Legend: " + ", ".join(f"{abbrev} = {('NoneType' if type_ is None else type_.__name__.upper())}" for type_, abbrev in type_abbreviations.items() if type_ != 'missing')

            comparison_table.append(legend)

            # Print the comparison table
            print("\n".join(comparison_table))

        return diffs



    def save(self, filename=None, foldername=None, overwrite=False):
        """
        Save the dforcefield instance to a file with a header, formatted content, and the base_class.

        Args:
        -----
        filename (str): The name of the file to save. Defaults to self.userid if not provided.
        foldername (str): The folder in which to save the file. Defaults to a temporary directory.
        overwrite (bool): Whether to overwrite the file if it already exists. Defaults to False.

        Raises:
        -------
        ValueError: If base_class is None or not a subclass of forcefield.
        FileExistsError: If the file already exists and overwrite is False.

        Notes:
        ------
        The file format follows a specific structure for saving forcefield attributes, base_class,
        name, description, and parameters.

        Example Output: (without the comments)
        ---------------
        # DFORCEFIELD SAVE FILE
        base_class = "tlsph"
        beadtype = 1
        userid = "dynamic_water"
        version = 1.0

        description:{forcefield="LAMMPS:SMD", style="tlsph", material="water"}
        name:{forcefield="LAMMPS:SMD", material="water"}

        rho = 1000
        E = "5*${c0}^2*${rho}"
        nu = 0.3
        """

        # Use self.userid if filename is not provided
        if filename is None:
            filename = self.userid

        # Ensure the filename ends with '.txt'
        if not filename.endswith('.txt'):
            filename += '.txt'

        # Default folder to tempdir if foldername is not provided
        if foldername is None:
            foldername = tempfile.gettempdir()

        # Construct full path
        if not os.path.isabs(filename):
            filepath = os.path.join(foldername, filename)
        else:
            filepath = filename

        # Check if file already exists, raise exception if it does and overwrite is False
        if os.path.exists(filepath) and not overwrite:
            raise FileExistsError(f"The file '{filepath}' already exists.")

        # Header with current date, username, and host
        user = getpass.getuser()
        host = socket.gethostname()
        date = datetime.now().strftime('%Y-%m-%d')
        header = f"# DFORCEFIELD SAVE FILE\n# generated on {date} by {user}@{host}\n"
        header += f'#\tuserid = "{self.userid}"\n'
        header += f'#\tpath = "{filepath}"\n\n'

        # Open the file for writing
        with open(filepath, 'w') as f:
            # Write the header
            f.write(header)

            # Write beadtype, userid, and version with comments
            f.write("# Forcefield attributes\n")
            f.write(f'base_class="{self.base_class.__class__.__name__}"\n')
            f.write(f"beadtype = {self.beadtype}\n")
            f.write(f"userid = {self.userid}\n")
            f.write(f"version = {self.version}\n\n")

            # Write description with comment
            f.write("# Description of the forcefield\n")
            f.write("description:{")
            f.write(", ".join(f'{key}="{value}"' for key, value in self.description.items()))
            f.write("}\n\n")

            # Write name with comment
            f.write("# Name of the forcefield\n")
            f.write("name:{")
            f.write(", ".join(f'{key}="{value}"' for key, value in self.name.items()))
            f.write("}\n\n")

            # Write parameters with comment
            f.write("# Parameters for the forcefield\n")
            for key, value in self.parameters.items():
                f.write(f"{key} = {value}\n")

        # return filepath
        print(f"\nScript saved to {filepath}")

        return filepath



    @classmethod
    def load(cls, filename, foldername=None):
        """
        Load a dforcefield instance from a file with more control over the folder location and file validation.

        Args:
            filename (str): The name of the file to load. The '.txt' extension will be added if not present.
            foldername (str): The folder from which to load the file. Defaults to a temporary directory.

        Returns:
            dforcefield: A new dforcefield instance parsed from the file content.
        """
        # Step 0: Validate and construct filepath
        if not filename.endswith('.txt'):
            filename += '.txt'
        if foldername is None:
            foldername = tempfile.gettempdir()
        if not os.path.isabs(filename):
            filepath = os.path.join(foldername, filename)
        else:
            filepath = filename

        # Check if file exists
        if not os.path.exists(filepath):
            raise FileNotFoundError(f"The file '{filepath}' does not exist.")

        # Read the file contents
        with open(filepath, 'r') as f:
            content = f.read()

        # Extract the filename and name
        fname = os.path.basename(filepath)  # Extracts the filename (e.g., "forcefield.txt")
        name, _ = os.path.splitext(fname)   # Removes the extension, e.g., "forcefield"

        # Call the parse method to create a new dforcefield instance
        return cls.parse(content)


    @classmethod
    def parse(cls, content):
        """
        Parse the string content of a forcefield file to create a dforcefield instance.

        Parameters:
        -----------
        content : str
            The string content to be parsed.

        Returns:
        --------
        dforcefield
            A new `dforcefield` instance populated with the parsed content.

        Raises:
        -------
        ValueError
            If the content does not start with the correct magic line or if the format is invalid,
            or if base_class is not a valid subclass of forcefield.

        Notes:
        ------
        - Handles different sections including parameters, name, and description.
        - Accepts empty lines, comments, and the use of { } for attributes.
        """
        # Split the content into lines
        lines = content.splitlines()
        lines = [line for line in lines if line.strip()]  # Remove blank or empty lines
        # Raise an error if no content is left after removing blank lines
        if not lines:
            raise ValueError("File/Content is empty or only contains blank lines.")

        # Initialize containers for parsed data
        inside_global_params = False
        parameters = {}
        attributes = {}
        description = {}
        name = {}
        base_class = None

        # Step 1: Authenticate the file by checking the first line
        if not lines[0].strip().startswith("# DFORCEFIELD SAVE FILE"):
            raise ValueError("File/Content is not a valid DFORCEFIELD file.")

        # Step 2: Process each line dynamically
        for line in lines[1:]:
            stripped = line.strip()

            # Ignore empty lines and comments
            if not stripped or stripped.startswith("#"):
                continue

            # Remove trailing comments
            stripped = remove_comments(stripped)

            # Step 3: Detect base_class
            if stripped.startswith("base_class="):
                base_class_name = stripped.split("=", 1)[1].strip().strip('"')
                # Dynamically set the base class based on the string value (e.g., "tlsph")
                base_class = cls._load_base_class(base_class_name)
                if base_class is None or not issubclass(base_class, forcefield):
                    raise ValueError(f"Invalid base_class: '{base_class_name}' must be a subclass of forcefield.")
                continue

            # Step 4: Handle global parameters inside {...}
            if stripped.startswith("{"):
                inside_global_params = True
                global_params_content = stripped[stripped.index('{') + 1:].strip()

                if '}' in global_params_content:
                    global_params_content = global_params_content[:global_params_content.index('}')].strip()
                    inside_global_params = False
                    cls._parse_global_params(global_params_content.strip(), attributes)
                    global_params_content = ""  # Reset for the next block
                continue

            if inside_global_params:
                if stripped.endswith("}"):
                    global_params_content += " " + stripped[:stripped.index('}')].strip()
                    inside_global_params = False
                    cls._parse_global_params(global_params_content.strip(), attributes)
                    global_params_content = ""
                else:
                    global_params_content += " " + stripped
                continue

            # Step 5: Detect the start of name or description blocks
            if stripped.startswith("description:{"):
                desc_content = stripped.split("description:")[1].strip()[1:-1]
                cls._parse_struct_block(desc_content, description)
                continue

            if stripped.startswith("name:{"):
                name_content = stripped.split("name:")[1].strip()[1:-1]
                cls._parse_struct_block(name_content, name)
                continue

            # Step 6: Handle parameters in key=value format
            if "=" in stripped:
                key, value = stripped.split("=", 1)
                key = key.strip()
                value = value.strip().strip('"')
                parameters[key] = cls._convert_value(value)

        # Step 7: Validate base_class and create a new dforcefield instance
        if base_class is None:
            raise ValueError("base_class must be specified and valid in the forcefield file.")

        # Step 8: Create and return the new dforcefield instance
        newFF = cls(
            base_class=base_class,
            beadtype=int(attributes.get('beadtype', 1)),
            userid=attributes.get('userid', "unknown"),
            name=struct(**name),
            description=struct(**description),
            version=float(attributes.get('version', 0.1)),
            USER=parameterforcefield(**parameters)
        )

        # Step 9: detect variables in templates and propagates undefined variables
        dvars = newFF.detectVariables()
        newFF.parameters = dvars + newFF.parameters
        return newFF


    @classmethod
    def _parse_global_params(cls, content, global_params):
        """Parse global parameters from the accumulated content between {}."""
        lines = re.split(r',(?![^(){}\[\]]*[\)\}\]])', content)
        for line in lines:
            line = line.strip()
            match = re.match(r'([\w_]+)\s*=\s*(.+)', line)
            if match:
                key, value = match.groups()
                key = key.strip()
                value = value.strip()
                global_params[key] = cls._convert_value(value)



    @classmethod
    def _parse_struct_block(cls, content, struct_block):
        """
        Parse blocks like description or name with key=value pairs, handling commas inside quotes.

        Parameters:
        -----------
        content : str
            The content to parse, which contains key=value pairs.

        struct_block : dict
            A dictionary to store the parsed key-value pairs.
        """
        # Split on commas that are not inside quotes
        pairs = re.split(r',\s*(?![^"]*\"\s*,\s*[^"]*")', content)

        for pair in pairs:
            if '=' in pair:
                key, value = pair.split('=', 1)  # Ensure splitting only on the first '='
            struct_block[key.strip()] = value.strip().strip('"')



    @classmethod
    def _convert_value(cls, value):
        """Convert a string representation of a value to the appropriate Python type."""
        value = value.strip()

        # Boolean conversion
        if value.lower() == 'true':
            return True
        elif value.lower() == 'false':
            return False

        # Handle quoted strings
        if (value.startswith('"') and value.endswith('"')) or (value.startswith("'") and value.endswith("'")):
            return value[1:-1]

        # Handle lists
        if value.startswith('[') and value.endswith(']'):
            return eval(value)  # Using eval to parse lists safely in this controlled scenario

        # Handle numbers
        try:
            if '.' in value:
                return float(value)
            return int(value)
        except ValueError:
            return value


    def detectVariables(self):
        """
        Detects variables in the form ${variable} from the outputs of pair_style, pair_diagcoeff,
        and pair_offdiagcoeff.

        Returns:
        --------
        struct
            A struct containing the detected variable names that are not yet defined.
        """
        # Extract variables from pair_style, pair_diagcoeff, and pair_offdiagcoeff
        detected_vars = set()

        # Define a regex pattern to detect variables in the form ${variable}
        pattern = r'\$\{(\w+)\}'

        # Detect variables in pair_style output
        pair_style_output = self.pair_style(printflag=False, raw=True)
        detected_vars.update(re.findall(pattern, pair_style_output))

        # Detect variables in pair_diagcoeff output
        pair_diagcoeff_output = self.pair_diagcoeff(printflag=False, raw=True)
        detected_vars.update(re.findall(pattern, pair_diagcoeff_output))

        # Detect variables in pair_offdiagcoeff output
        pair_offdiagcoeff_output = self.pair_offdiagcoeff(printflag=False, raw=True)
        detected_vars.update(re.findall(pattern, pair_offdiagcoeff_output))

        # Convert the detected variables into a parameterforcefield() for further propagation
        return parameterforcefield(**{var: "${" + var + "}" for var in detected_vars})


    def missingVariables(self, isimplicit_missing=True, output_aslist=False):
        """
        List missing variables (undefined in parameters).

        Parameters:
        -----------
        isimplicit_missing : bool, optional (default: True)
            If True, variables defined implicitly as ${varname} in parameters are considered missing.

        output_aslist : bool, optional (default: False)
            If True, returns a list of missing variable names.
            If False, returns a parameterforcefield-like structure with implicit definitions.

        Returns:
        --------
        List of str or parameterforcefield
            If output_aslist is True, returns a list of variable names that are missing from the parameters.
            If output_aslist is False, returns a parameterforcefield-like structure with implicit definitions.
        """
        # Detect all variables used in the forcefield using detectVariables
        detected_vars = self.detectVariables()

        # Initialize missing variables
        missing_vars = []

        # Initialize a dictionary to store variables as parameterforcefield when output_aslist=False
        missing_vars_struct = {}

        # Iterate over detected variables and check if they are missing in parameters
        for varname in detected_vars.keys():
            if varname not in self.parameters:
                # If the variable is missing, add it to both the list and struct
                missing_vars.append(varname)
                missing_vars_struct[varname] = "${" + varname + "}"
            elif isimplicit_missing:
                # If the variable exists but is implicitly defined as ${varname}, treat it as missing
                param_value = self.parameters.getattr(varname)
                if param_value == "${" + varname + "}":
                    missing_vars.append(varname)
                    missing_vars_struct[varname] = "${" + varname + "}"

        # Return the result based on the output_aslist flag
        if output_aslist:
            return missing_vars
        else:
            return parameterforcefield(**missing_vars_struct)


    @classmethod
    def list_forcefield_subclasses(cls, printflag=True, additional_modules=None):
        """
        Class method to list all subclasses of the `forcefield` and `generic` classes, including their methods.

        Parameters:
        -----------
        printflag : bool, optional (default=True)
            If True, prints the subclasses of `forcefield`, `generic`, their load status, and their default parameters.
        additional_modules : module or list of modules, optional
            A module or list of additional modules to search for subclasses and methods.

        Returns:
        --------
        dict
            A dictionary where keys are class names or method names and values are dictionaries containing:
            - "loaded": Whether the class or method is already loaded in memory.
            - "module": The module path of the class or method.
            - "default_parameters": Extracted parameters, RULES, LOCAL, and GLOBAL from the class or method.
        dict
            A dictionary mapping short names to full names.
        """
        subclasses = set()
        classes_to_check = [forcefield, generic]  # Start with both forcefield and generic

        # Ensure additional_modules is a list, even if it's a single module
        if additional_modules is not None and not isinstance(additional_modules, list):
            additional_modules = [additional_modules]

        # Include classes from additional modules
        if additional_modules:
            for mod in additional_modules:
                for name, obj in inspect.getmembers(mod):
                    # Add classes that are subclasses of forcefield or generic
                    if inspect.isclass(obj) and (issubclass(obj, forcefield) or issubclass(obj, generic)) and obj not in {forcefield, generic}:
                        subclasses.add(obj)
                        classes_to_check.append(obj)
                    # Also add relevant high-level methods/functions that do not start with '_'
                    elif (inspect.isfunction(obj) or inspect.ismethod(obj)) and not name.startswith('_'):
                        # Check if the method belongs to a class derived from generic, not forcefield
                        parent_class_name = obj.__qualname__.split('.')[0]
                        parent_class = next((cls for cls in subclasses if cls.__name__ == parent_class_name), None)
                        if parent_class and issubclass(parent_class, generic) and not issubclass(parent_class, forcefield):
                            subclasses.add((parent_class, name, obj))  # Add (parent_class, method_name, function/method)

        # Traverse through all found subclasses
        while classes_to_check:
            parent_class = classes_to_check.pop()
            for subclass in parent_class.__subclasses__():
                subclasses.add(subclass)
                classes_to_check.append(subclass)

        # Prepare the output dictionary with load status flags and default parameters
        subclass_info = {}
        short_name_mapping = {}

        for subclass in subclasses:
            if inspect.isclass(subclass):
                class_name = subclass.__name__
                is_loaded = class_name in globals()  # Check if class is already loaded

                # Extract default parameters if they exist
                extracted_params = cls.extract_default_parameters(subclass, displayflag=False)
                num_defaults = len(extracted_params.get('parameters', {}).keys()) if extracted_params else None

                # Check if RULES, GLOBAL, and LOCAL are defined
                rules_defined = ''
                if extracted_params:
                    rules_defined += 'R' if extracted_params["RULES"] else '-'
                    rules_defined += 'G' if extracted_params["GLOBAL"] else '-'
                    rules_defined += 'L' if extracted_params["LOCAL"] else '-'

                # Add entry for the class
                subclass_info[class_name] = {
                    "loaded": is_loaded,
                    "module": subclass.__module__,
                    "num_defaults": num_defaults,
                    "rules_defined": rules_defined if rules_defined else '---'  # Display as '---' if none defined
                }

                # Map short name to full name
                short_name_mapping[class_name] = class_name

                # Inspect and add high-level methods from the class itself if not forcefield
                if not issubclass(subclass, forcefield):
                    for method_name, method in inspect.getmembers(subclass, predicate=inspect.isfunction):
                        if not method_name.startswith('_'):  # Skip private methods
                            method_full_name = f"{class_name}.{method_name}"
                            extracted_method_params = cls.extract_default_parameters(subclass, method_name=method_name, displayflag=False)
                            num_defaults = len(extracted_method_params.get('parameters', {}).keys()) if extracted_method_params else None
                            rules_defined = extracted_method_params.get("rules_defined", "---")
                            subclass_info[method_full_name] = {
                                "loaded": True,
                                "module": subclass.__module__,
                                "num_defaults": num_defaults,
                                "rules_defined": rules_defined
                            }
                            # Add method short name to mapping
                            short_name_mapping[method_name] = method_full_name

            else:
                # Handle function/method cases
                parent_class, func_name, func_obj = subclass
                is_loaded = func_name in globals() or parent_class.__module__ in sys.modules
                extracted_method_params = cls.extract_default_parameters(parent_class, method_name=func_name, displayflag=False)
                num_defaults = len(extracted_method_params.get('parameters', {}).keys()) if extracted_method_params else None
                rules_defined = extracted_method_params.get("rules_defined", "---")
                func_full_name = f"{parent_class.__name__}.{func_name}"
                subclass_info[func_full_name] = {
                    "loaded": is_loaded,
                    "module": parent_class.__module__,
                    "num_defaults": num_defaults,
                    "rules_defined": rules_defined
                }
                # Add function/method to short names
                short_name_mapping[func_name] = func_full_name

        # Optionally print the subclasses and their load status with parameter info
        if printflag:
            print("List of available forcefields and relevant methods:\n")
            print(f"{'Short Name':<15} {'Class/Method Name':<30} {'Module Path':<30} {'Status':<10} {'Default Params':<10} {'RULES':<5}")
            print("=" * 105)
            for short_name, class_name in sorted(short_name_mapping.items(), key=lambda x: x[0]):
                info = subclass_info[class_name]
                status = "Loaded" if info["loaded"] else "Not Loaded"
                num_defaults = info["num_defaults"] if info["num_defaults"] is not None else "None"
                rules_defined = info["rules_defined"]
                print(f"{short_name:<15} {class_name:<30} {info['module']:<30} {status:<10} {num_defaults:<10} {rules_defined:<5}")
            print(f"\nTotal number of forcefields and methods: {len(subclass_info)}")

        return subclass_info, short_name_mapping



    def scriptobject(self, beadtype=None, name=None, userid=None, fullname=None, filename=None, group=None, style=None, USER=scriptdata()):
        """
        Method to return a scriptobject based on the current dforcefield instance.

        Parameters:
        ------------
        beadtype : int, optional
            The bead type identifier. Defaults to the instance's beadtype.

        name : str, optional
            A short name for the scriptobject. If not provided, uses 'forcefield' from self.name.

        fullname : str, optional
            A comprehensive name for the object. Defaults to "beads of type {self.beadtype} | object of forcefield: {self.name.forcefield}".

        group : list, optional
            A group of scriptobjects that this object belongs to. Defaults to an empty list.

        style : str, optional
            The style of the scriptobject. Defaults to `self.description.style` if available, otherwise "smd".

        USER : scriptdata, optional
            User-defined data for additional parameters. Defaults to a blank `scriptdata()` instance.

        Returns:
        --------
        scriptobject
            A scriptobject instance populated with the current `dforcefield` instance's attributes or provided arguments.
        """
        # Set defaults using instance attributes if parameters are None
        if beadtype is None:
            beadtype = self.beadtype

        # Extract a meaningful name from the forcefield's name structure
        if name is None:
            if isinstance(self.name, struct) and hasattr(self.name, 'material'):
                name = f"{self.name.material} bead"
            else:
                name = f"beadtype_{beadtype}"

        if userid is None:
            userid = name

        if fullname is None:
            fullname = f"beads of type {beadtype} | object of forcefield: {self.name.forcefield if isinstance(self.name, struct) and hasattr(self.name, 'forcefield') else 'unknown'}"

        if group is None:
            group = []

        # Use `self.description.style` as the default for style if it exists, otherwise fall back to "smd"
        if style is None:
            if isinstance(self.description, struct) and hasattr(self.description, 'style'):
                style = self.description.style
            else:
                style = "undefined style"

        # The current dforcefield instance behaves as the forcefield for the scriptobject
        forcefield = copy.deepcopy(self)
        forcefield.beadtype = beadtype
        forcefield.name = name
        forcefield.userid = userid

        # Apply any user-defined parameters to the forcefield
        # This is the standard behavior of scriptobject
        forcefield.parameters = forcefield.parameters + USER

        # Create and return the scriptobject instance
        return scriptobject(
            beadtype=beadtype,
            name=name,
            fullname=fullname,
            filename=filename,
            style=style,
            forcefield=forcefield,  # Use the current dforcefield instance
            group=group,
            USER=USER
        )


    def __copy__(self):
        """
        Shallow copy method for dforcefield.

        Creates a shallow copy of the dforcefield instance, meaning that references
        to mutable objects like 'parameters' will not be deeply copied.
        """
        cls = self.__class__
        copie = cls.__new__(cls)

        # Copy the simple attributes
        copie.__dict__.update(self.__dict__)

        # Return the shallow copy
        return copie


    def __deepcopy__(self, memo):
        """
        Deep copy method for dforcefield.

        Creates a deep copy of the dforcefield instance, including its complex attributes
        like 'parameters'. This ensures that all mutable objects are fully copied, not just referenced.
        """
        cls = self.__class__
        copie = cls.__new__(cls)
        memo[id(self)] = copie

        # Recursively deep copy each attribute
        for k, v in self.__dict__.items():
            if k == "base_class":
                # The base_class should not be deeply copied; it's a class reference
                setattr(copie, k, self.base_class)
            else:
                # Deep copy other attributes
                setattr(copie, k, copy.deepcopy(v, memo))

        return copie # return copied instance


# %% DEBUG
# ===================================================
# main()
# ===================================================
# for debugging purposes (code called as a script)
# the code is called from here
# ============================
if __name__ == '__main__':

    # ---------------------------------------------------------------
    # The examples below reconstruct dynamycally the FF:
    #       * water from pizza.forcefield.water()
    #       * solidfood from pizza.forcefield.solidfood()
    #       * salt from pizza.forcefield.saltTLSPH()
    #       * rigidwall from pizza.forcefield.rigidwall()
    # ---------------------------------------------------------------

    # List available forcefields
    dforcefield.list_forcefield_subclasses(printflag=True,additional_modules=None) # we could add other modules

    # We reuse a high-level forcefield
    mywater = dforcefield(
        base_class="water",
        userid = "my customized water",
        rho = 900,
        q1 = 0.1
        )

    # Test dynamic water class using ulsph as the base class
    dynamic_water = dforcefield(
        base_class='ulsph',
        beadtype=1,
        userid="dynamic_water",
        USER=parameterforcefield(
            rho=1000,
            c0=10.0,
            q1=1.0,
            Cp=1.0,
            taitexponent=7,
            contact_scale=1.5,
            contact_stiffness="2.5*${c0}^2*${rho}"
        )
    )
    print(f"Water parameters: {dynamic_water.parameters}")
    print(f"Water name: {dynamic_water.name}")
    print(f"Water Cp: {dynamic_water.Cp}")
    dynamic_water

    # Test dynamic solidfood class using tlsph as the base class
    dynamic_solidfood = dforcefield(
        base_class='tlsph',
        beadtype=2,
        userid="dynamic_solidfood",
        #USER=parameterforcefield( #<--- note that USER is not used in this case
            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,
            contact_scale=1.5,
            contact_stiffness="2.5*${c0}^2*${rho}"
        #)
    )
    print(f"Solidfood parameters: {dynamic_solidfood.parameters}")
    print(f"Solidfood name: {dynamic_solidfood.name}")
    repr(dynamic_solidfood)

    # Test dynamic saltTLSPH class using tlsph as the base class
    dynamic_salt = dforcefield(
        base_class='tlsph',
        beadtype=3,
        userid="dynamic_salt",
        USER=parameterforcefield(
            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,
            contact_scale=1.5,
            contact_stiffness="2.5*${c0}^2*${rho}"
        )
    )
    print(f"Salt TLSPH parameters: {dynamic_salt.parameters}")
    print(f"Salt TLSPH name: {dynamic_salt.name}")
    repr(dynamic_salt)

    # Test dynamic rigidwall class using none as the base class
    dynamic_rigidwall = dforcefield(
        base_class='none',  # Assuming a class `none` exists
        beadtype=4,
        userid="dynamic_rigidwall",
        USER=parameterforcefield(
            rho=3000,
            c0=10.0,
            contact_scale=1.5,
            contact_stiffness="2.5*${c0}^2*${rho}"
        )
    )
    print(f"Rigidwall parameters: {dynamic_rigidwall.parameters}")
    print(f"Rigidwall name: {dynamic_rigidwall.name}")
    repr(dynamic_rigidwall)


    # create a new food and save it on disk
    newfood = dynamic_solidfood.copy(rho=2100,q1=4,E=1000,name="new food")
    repr(newfood)
    newfood.base_repr()
    fname = newfood.save(overwrite=True)

    # load again the same file
    newfood2 = dforcefield.load(fname)

    # compare the content
    newfood.compare(newfood2,printflag=True)

    # note that the variables are automatically identified and added to parameters if missing
    newfood.parameters = parameterforcefield(a=1,b=2)
    missingvars = newfood.missingVariables()
    print('updated newfood:\n')
    repr(newfood)
    print('missing variables in updated newfood:\n')
    repr(missingvars)

    # compare the content
    newfood.compare(newfood2,printflag=True)

    # check the parser
    content = """
# DFORCEFIELD SAVE FILE
base_class="tlsph"
beadtype = 1
userid = "dynamic_water"
version = 1.0

description:{forcefield="LAMMPS:SMD", style="tlsph", material="water"}
name:{forcefield="LAMMPS:SMD", material="water"}

rho = 1000
E = "5*${c0}^2*${rho}"
nu = 0.3
"""
    parsed_forcefield = dforcefield.parse(content)
    print(parsed_forcefield.script)

    # create a script object
    obj = parsed_forcefield .scriptobject(group="A")


    # *********************************************************************************************
    # Production Example: Scriptobject Creation and Combination
    #
    # This example demonstrates the creation and combination of `scriptobject` instances from both
    # static forcefield classes and dynamic forcefield instances (via `dforcefield`).
    #
    # Key Points:
    # ------------
    # 1. **Static and Dynamic Forcefields**:
    #    - Scriptobjects can be created using static forcefields (e.g., `rigidwall`, `solidfood`, etc.)
    #      or dynamic forcefields generated from a `dforcefield` instance (e.g., `waterFF`).
    #    - Dynamic forcefields can either be passed directly to the `pizza.script.scriptobject()`
    #      constructor or instantiated through the `scriptobject` method of any `pizza.dforcefield` object.
    #
    # 2. **Combining Scriptobjects**:
    #    - Scriptobjects can be combined using the `+` operator to create a collection of objects.
    #    - This collection of scriptobjects can then be scripted dynamically using the `script` property
    #      to generate their interaction definitions.
    #
    # 3. **Geometry Input**:
    #    - Geometry for the scriptobjects can be provided either via an input file (using `filename`)
    #      or dynamically using `pizza.region.region()`.
    #
    # **********************************************************************************************

    from pizza.forcefield import rigidwall, solidfood, water

    # Create a dynamic forcefield for water with a specific density (rho=900) and beadtype=1
    waterFF = dforcefield(base_class="water", rho=900, beadtype=1)

    # Define water beads using the dforcefield instance's scriptobject method
    bwater = waterFF.scriptobject(name="water", group=["A", "D"], filename="mygeom")

    # Alternatively, define water beads by passing the dynamic forcefield directly to scriptobject
    bwater2 = scriptobject(name="water", group=["A", "D"], filename="mygeom", forcefield=waterFF)

    # Define other dummy beads using static forcefields
    b1 = scriptobject(name="bead 1", group=["A", "B", "C"], filename='myfile1', forcefield=rigidwall())
    b2 = scriptobject(name="bead 2", group=["B", "C"], filename='myfile1', forcefield=rigidwall())
    b3 = scriptobject(name="bead 3", group=["B", "D", "E"], forcefield=solidfood())
    b4 = scriptobject(name="bead 4", group="D", beadtype=1, filename="myfile2", forcefield=water())

    # Combine the scriptobjects into a collection using the '+' operator
    collection = bwater + b1 + b2 + b3 + b4

    # Generate the script for the interactions in the collection
    collection.script

    # Similarly, using the alternate water bead definition (bwater2)
    collection2 = bwater2 + b1 + b2 + b3 + b4
    collection2.script


    # *********************************************************************************************
    # Example: Using Dynamic Forcefields from `pizza.generic` alongside `forcefield` Classes
    #
    # This example demonstrates how to use dynamic forcefields defined within the `pizza.generic`
    # module and seamlessly integrate them with existing static `forcefield` classes for simulation scripting.
    #
    # Key Points:
    # ------------
    # 1. **Dynamic Forcefields via Generic Classes**:
    #    - The `generic` module enables the creation of dynamic forcefields using high-level methods
    #      (e.g., `newtonianfluid` from the `USERSMD` class) without the need to define them statically.
    #    - These dynamic forcefields can be instantiated using the `dforcefield` class and linked
    #      to the high-level methods in the `generic` module via `additional_modules`.
    #
    # 2. **Integration with Existing Forcefields**:
    #    - `dforcefield` instances created from `generic` methods are fully compatible with static
    #      forcefield classes such as `rigidwall`, `solidfood`, and `water`.
    #    - Scriptobjects created from dynamic instances can be combined with other forcefield-derived objects,
    #      making them versatile for complex simulations.
    #
    # 3. **Workflow**:
    #    - **Step 1**: Specify the additional modules (`additional_modules`) where the high-level forcefield
    #      definitions are located (e.g., the `generic` module).
    #    - **Step 2**: Initialize the `dforcefield` with a specific method name (e.g., `newtonianfluid`)
    #      to dynamically create the forcefield. The properties can be set directly during initialization or updated later.
    #    - **Step 3**: Use the created dynamic forcefield instance to define scriptobjects and combine them
    #      with other static forcefields in a collection for further interactive scripting.
    #
    # 4. **Adjusting Parameters Dynamically**:
    #    - `RULES`, `GLOBAL`, and `LOCAL` parameters are integral parts of the `generic` module, providing additional
    #      flexibility to modify forcefield behavior on-the-fly.
    #    - Use methods like `get_rules()`, `set_rules()`, `set_local()`, and `set_global()` to adjust these parameters
    #      dynamically according to the simulation needs.
    #
    # *********************************************************************************************

    from pizza.forcefield import rigidwall, solidfood, water
    from pizza.generic import generic

    # Step 1: Specify the additional module(s) where USERSMD and its methods are located.
    # The `additional_modules` list informs `dforcefield` where to find high-level method definitions like `newtonianfluid`.
    additional_modules = [generic]  # You can also use a string like "generic" if imports resolve correctly.

    # Step 2: Initialize a dynamic forcefield using `newtonianfluid` from the USERSMD class via additional_modules.
    dynamic_ff = dforcefield(
        base_class="newtonianfluid",  # Use the name of the function from USERSMD.
        beadtype=6,                   # Specify the bead type for the forcefield.
        userid="fluid_instance",      # Assign a unique identifier for the forcefield instance.
        additional_modules=additional_modules,  # Pass the module list containing high-level forcefield methods.
        rho=900,                      # Define fluid density directly during initialization.
        nu=1e-4                       # Define kinematic viscosity directly during initialization.
    )

    # Step 3: Read and update RULES parameters dynamically.
    # RULES are predefined formulas or constants that affect forcefield behavior and can be adjusted on-the-fly.
    current_RULES = dynamic_ff.get_rules()
    print(f"Dynamic RULES apply to {dynamic_ff.name}:")
    repr(current_RULES)
    # Update a specific RULE, which impacts how forces are calculated in the simulation.
    current_RULES.q1 = "10*${nu}/(${c0}*${h})"
    dynamic_ff.set_rules(current_RULES)  # Update the RULES parameters in the dforcefield instance.

    # Step 4: Update LOCAL parameters dynamically.
    # LOCAL parameters represent properties specific to the instance, such as material properties.
    dynamic_ff.set_local(rho=1011, nu=0.00123)  # Update properties directly with keyword arguments.

    # Display the current state of the dynamic forcefield instance.
    print("\nDynamic dforcefield instance based on generic.newtonianfluid:\n")
    repr(dynamic_ff)

    # Define a scriptobject using the dynamic forcefield instance.
    # Scriptobjects represent physical entities in simulations, defined using the forcefield properties.
    bwater3 = dynamic_ff.scriptobject(name="water_newtonian", group=["A", "D"], filename="mygeomXX")

    # Step 5: Combine this scriptobject with an existing collection of scriptobjects.
    # Collections enable interactions and complex definitions in simulations.
    collection3 = collection2 + bwater3

    # Step 6: Generate the script for the interactions in the new collection.
    # The `script` attribute provides a formatted script ready for use in simulations.
    collection3.script

Functions

def autoname(numChars=8)

generate automatically names

Expand source code
def autoname(numChars=8):
    """ generate automatically names """
    return ''.join(random.choices(string.ascii_letters, k=numChars))  # Generates a random name of numChars letters
def remove_comments(content, split_lines=False)

Removes comments from a single or multi-line string. Handles quotes and escaped characters.

Parameters:

content : str The input string, which may contain multiple lines. Each line will be processed individually to remove comments, while preserving content inside quotes. split_lines : bool, optional (default: False) If True, the function will return a list of processed lines. If False, it will return a single string with all lines joined by newlines.

Returns:

str or list of str The processed content with comments removed. Returns a list of lines if split_lines is True, or a single string if False.

Expand source code
def remove_comments(content, split_lines=False):
    """
    Removes comments from a single or multi-line string. Handles quotes and escaped characters.

    Parameters:
    -----------
    content : str
        The input string, which may contain multiple lines. Each line will be processed
        individually to remove comments, while preserving content inside quotes.
    split_lines : bool, optional (default: False)
        If True, the function will return a list of processed lines. If False, it will
        return a single string with all lines joined by newlines.

    Returns:
    --------
    str or list of str
        The processed content with comments removed. Returns a list of lines if
        `split_lines` is True, or a single string if False.
    """
    def process_line(line):
        """Remove comments from a single line while handling quotes and escapes."""
        in_single_quote = False
        in_double_quote = False
        escaped = False
        result = []

        for i, char in enumerate(line):
            if escaped:
                result.append(char)
                escaped = False
                continue

            if char == '\\':  # Handle escape character
                escaped = True
                result.append(char)
                continue

            # Toggle state for single and double quotes
            if char == "'" and not in_double_quote:
                in_single_quote = not in_single_quote
            elif char == '"' and not in_single_quote:
                in_double_quote = not in_double_quote

            # If we encounter a '#' and we're not inside quotes, it's a comment
            if char == '#' and not in_single_quote and not in_double_quote:
                break  # Stop processing the line when a comment is found

            result.append(char)

        return ''.join(result).strip()

    # Split the input content into lines
    lines = content.split('\n')

    # Process each line, skipping empty lines and those starting with #
    processed_lines = []
    for line in lines:
        stripped_line = line.strip()
        if not stripped_line or stripped_line.startswith('#'):
            continue  # Skip empty lines and lines that are pure comments
        processed_line = process_line(line)
        if processed_line:  # Only add non-empty lines
            processed_lines.append(process_line(line))

    if split_lines:
        return processed_lines  # Return list of processed lines
    else:
        return '\n'.join(processed_lines)  # Join lines back into a single string

Classes

class USERSMD (name=None, type=None, h=0.1, Ma=0.1, vmax=0.1, USER=generic data (gendata object) with 0 items)

class helper for the module LAMMPS.USERSMD

generic helper constructor

Expand source code
class USERSMD(generic):
    """ class helper for the module LAMMPS.USERSMD """

    # specific tules (add formula, missing parameters will be provided by GLOBAL or LOCAL)
    RULES = genericdata(
        # list here default forcefield rules (USER can overload these rules)
        c0 = "${vmax}/${Ma}",
        q1 = "8*${nu}/(${c0}*${h})",
        Cp = 1.0,
        contact_stiffness = '2.5*${c0}^2*${rho}'
        )

    # constructor of USERSMD (note that USER can override all definitions)
    def __init__(self, name=None, type=None, h=0.1, Ma=0.1, vmax=0.1, USER = genericdata()):
        """ generic helper constructor """
        super().__init__()
        self.name = name
        self.type = type
        self.GLOBAL = genericdata(
            # variables which apply to all forcefields
            h = h,
            Ma = Ma,
            vmax = vmax
            ) + USER

    # forcefield derived from pizza.forcefield.water()
    def newtonianfluid(self, beadtype=1, userid = None, rho=1000.0, nu = 1e-6, mu = None, USER = genericdata()):
        """
            newtonianfluid() returns a parameterized ULSPH forcefield
            with prescribed viscosity (mu [Pa.s] or nu in [m2/s])
            and density (rho).
            Based on recommendations of J. Comput. Phys 1997, 136, 214–226
        """
        self.LOCAL = genericdata(
            rho = rho,
            nu = nu, # note that that nu is preferred as it is independent of rho
            mu = mu # dynamic viscosity (dependent on rho)
            ) + USER
        if self.LOCAL.nu is None: self.LOCAL.nu = self.LOCAL.mu / self.LOCAL.rho
        if self.LOCAL.nu is None: raise(ValueError("bad kinematic viscosity value: nu [m2/s]"))
        if self.LOCAL.mu is None: self.LOCAL.mu = self.LOCAL.nu * self.LOCAL.rho
        if userid == "" or userid is None:
            userid = "newtonianfluid rho=%0.4g [kg/m3] and nu=%0.4g [m2/s]" \
                           % (self.LOCAL.rho,self.LOCAL.nu)
        # this simple line with +
        # manages inheritance (the las definition as always higher precendence)
        # sorts definitions to enable execution (struct.sortdefinitions())
        # (see pizza.forcefield.parameterforcefield())
        CURRENT = self.GLOBAL+self.LOCAL+self.RULES
        return water(beadtype=beadtype, userid=userid, USER=CURRENT)

Ancestors

  • pizza.generic.generic

Class variables

var RULES

Methods

def newtonianfluid(self, beadtype=1, userid=None, rho=1000.0, nu=1e-06, mu=None, USER=generic data (gendata object) with 0 items)

newtonianfluid() returns a parameterized ULSPH forcefield with prescribed viscosity (mu [Pa.s] or nu in [m2/s]) and density (rho). Based on recommendations of J. Comput. Phys 1997, 136, 214–226

Expand source code
def newtonianfluid(self, beadtype=1, userid = None, rho=1000.0, nu = 1e-6, mu = None, USER = genericdata()):
    """
        newtonianfluid() returns a parameterized ULSPH forcefield
        with prescribed viscosity (mu [Pa.s] or nu in [m2/s])
        and density (rho).
        Based on recommendations of J. Comput. Phys 1997, 136, 214–226
    """
    self.LOCAL = genericdata(
        rho = rho,
        nu = nu, # note that that nu is preferred as it is independent of rho
        mu = mu # dynamic viscosity (dependent on rho)
        ) + USER
    if self.LOCAL.nu is None: self.LOCAL.nu = self.LOCAL.mu / self.LOCAL.rho
    if self.LOCAL.nu is None: raise(ValueError("bad kinematic viscosity value: nu [m2/s]"))
    if self.LOCAL.mu is None: self.LOCAL.mu = self.LOCAL.nu * self.LOCAL.rho
    if userid == "" or userid is None:
        userid = "newtonianfluid rho=%0.4g [kg/m3] and nu=%0.4g [m2/s]" \
                       % (self.LOCAL.rho,self.LOCAL.nu)
    # this simple line with +
    # manages inheritance (the las definition as always higher precendence)
    # sorts definitions to enable execution (struct.sortdefinitions())
    # (see pizza.forcefield.parameterforcefield())
    CURRENT = self.GLOBAL+self.LOCAL+self.RULES
    return water(beadtype=beadtype, userid=userid, USER=CURRENT)
class dforcefield (base_class=None, beadtype=1, userid=None, USER=forcefield (FF object) with 0 parameters, name=None, description=None, version=0.1, additional_modules=None, printflag=False, verbose=False, GLOBAL=None, LOCAL=None, **kwargs)

The dforcefield class represents a dynamic extension of a base forcefield class. It allows for dynamic inheritance, delegation of methods, and flexible management of forcefield-specific attributes. The class supports the customization and injection of forcefield parameters at runtime, making it highly versatile for scientific simulations involving various forcefield models like ulsph, tlsph, and others.

Key Features:

  • Dynamic inheritance and delegation of base class methods.
  • Merging of name and description attributes, with automatic handling of overlapping fields.
  • Detection of variables used in pair_style, pair_diagcoeff, and pair_offdiagcoeff outputs.
  • Automatic handling of missing variables, including implicit variable detection and assignment.
  • Supports additional modules to be dynamically loaded for forcefield definitions.
  • Management of RULES, GLOBAL, and LOCAL parameters, which can be updated dynamically at runtime.

Attributes:

  • base_class (forcefield): The base class for forcefield behavior, inherited from forcefield subclasses like ulsph.
  • parameters (parameterforcefield): Stores interaction parameters dynamically injected into the base class.
  • beadtype (int): The bead type identifier associated with the forcefield instance.
  • userid (str): A unique identifier for the forcefield instance.
  • name (struct or str): A human-readable name for the forcefield instance, merged with description.
  • description (struct or str): A brief description of the forcefield, merged with name.
  • version (float): Version number for the forcefield instance.
  • RULES (genericdata): Defines specific rules or formulae applied to forcefield calculations.
  • GLOBAL (genericdata): Stores global parameters that apply to the entire forcefield.
  • LOCAL (genericdata): Contains local parameters specific to certain forcefield interactions.
  • USER (scriptdata): Contains USER parameters for scriptobject

Methods:

High-Level Methods:

These methods are primarily used for interacting with the dforcefield instance:

  • __init__: Initializes the dynamic forcefield with base class, parameters, and custom attributes.
  • pair_style: Delegates the pair style computation to the base_class.
  • pair_diagcoeff: Delegates diagonal pair coefficient computation to the base_class.
  • pair_offdiagcoeff: Delegates off-diagonal pair coefficient computation to the base_class.
  • save: Saves the forcefield instance to a file, with headers, attributes, and parameters.
  • load: Loads a dforcefield instance from a file and validates its format.
  • parse: Parses content of a forcefield file to create a new dforcefield instance.
  • copy: Creates a copy of the current instance, optionally overriding core attributes.
  • compare: Compares the current instance with another dforcefield instance, showing differences.
  • missingVariables: Lists undefined variables in parameters.
  • detectVariables: Detects variables in the pair_style, pair_diagcoeff, and pair_offdiagcoeff outputs.
  • to_dict: Serializes the forcefield instance into a dictionary.
  • from_dict: Creates a dforcefield instance from a dictionary of attributes.
  • reset: Resets the forcefield instance to its initial state.
  • validate: Validates the forcefield instance to ensure all required attributes are set.
  • base_repr: Returns the representation of the base_class.
  • script: Returns the raw outputs of pair methods as a script.
  • scriptobject: Method to return a scriptobject based on the current dforcefield instance.

Low-Level Methods:

These methods handle internal logic and lower-level operations:

  • _inject_attributes: Injects the dynamic attributes into the base_class.
  • __getattr__: Dynamically accesses attributes in name, description, parameters, or base_class.
  • __setattr__: Manages setting core and dynamic attributes.
  • _load_base_class: Dynamically loads the base class from a string name.
  • list_forcefield_subclasses: Lists all subclasses of forcefield including those from additional modules.
  • extract_default_parameters: Extracts default parameters from the base class or its ancestors.
  • dispmax: Truncates the display of long content for concise output.
  • __hasattr__: Checks if an attribute exists in the instance, parameters, or base class.
  • __contains__: Checks if an attribute exists in the instance or parameters.
  • __len__: Returns the number of parameters in the forcefield.
  • __iter__: Iterates over all keys, including those in name, description, parameters, and scalar attributes.
  • keys: Returns the keys from the merged name, description, and parameters.
  • values: Returns the values from the merged name, description, and parameters.
  • items: Returns an iterator of key-value pairs from name, description, and parameters.
  • merged_name_description: Merges name and description fields into a struct.
  • _get_available_forcefields: Retrieves a list of available forcefield subclasses.
  • __repr__: Custom representation of the dforcefield instance.
  • __str__: Returns a string representation of the dforcefield instance.
  • _convert_value: Converts a string value to the appropriate Python type.
  • _parse_global_params: Parses global parameters from the content between {}.
  • _parse_struct_block: Parses key-value pairs from description or name blocks.
  • __copy__: Creates a shallow copy of the dforcefield instance, copying only the attributes at the top level without duplicating nested objects.
  • __deepcopy__: Creates a deep copy of the dforcefield instance, fully duplicating all attributes, including nested objects, while leaving class references intact.
  • get_global: Returns the GLOBAL parameters.
  • get_local: Returns the LOCAL parameters.
  • get_rules: Returns the RULES parameters.
  • set_global: Updates the GLOBAL parameters and recalculates the combined parameters.
  • set_local: Updates the LOCAL parameters and recalculates the combined parameters.
  • set_rules: Updates the RULES parameters and recalculates the combined parameters.
  • combine_parameters: Combines GLOBAL, LOCAL, and RULES into the current parameter configuration.
  • update_parameters: Updates self.parameters after changes to GLOBAL, LOCAL, or RULES.

Example Usage:

>>> dynamic_ff = dforcefield(ulsph, beadtype=1, userid="dynamic_water", USER=parameterforcefield(rho=1000))
>>> dynamic_ff.pair_style()  # Uses attributes from the dforcefield instance
lj/cut
>>> dynamic_ff.compare(another_ff_instance, printflag=True)

Initialize a dynamic forcefield with default or custom values.

Args

base_class : str or class
The base class to use (e.g., 'ulsph', tlsph, etc.) or the actual class.
beadtype : int
The bead type identifier. Default is 1.
userid : str
User ID for the material. Default is None.
name : str
Human-readable name for the forcefield. Default is None.
description : str
Description of the forcefield. Default is None.
version : float
Version number for the forcefield. Default is 0.1.
USER : parameterforcefield
Custom parameters to override defaults.
additional_modules : module or list of modules
Additional modules to search for forcefields.
GLOBAL : parameterforcefield
Global parameters to be used. Default is None.
LOCAL : parameterforcefield
Local parameters to be used. Default is None.
kwargs
Additional parameters passed to the base class.
Expand source code
class dforcefield:
    """
    The `dforcefield` class represents a dynamic extension of a base forcefield class. It allows for dynamic inheritance,
    delegation of methods, and flexible management of forcefield-specific attributes. The class supports the customization
    and injection of forcefield parameters at runtime, making it highly versatile for scientific simulations involving
    various forcefield models like `ulsph`, `tlsph`, and others.

    Key Features:
    -------------
    - Dynamic inheritance and delegation of base class methods.
    - Merging of `name` and `description` attributes, with automatic handling of overlapping fields.
    - Detection of variables used in `pair_style`, `pair_diagcoeff`, and `pair_offdiagcoeff` outputs.
    - Automatic handling of missing variables, including implicit variable detection and assignment.
    - Supports additional modules to be dynamically loaded for forcefield definitions.
    - Management of `RULES`, `GLOBAL`, and `LOCAL` parameters, which can be updated dynamically at runtime.

    Attributes:
    -----------
    - `base_class` (forcefield): The base class for forcefield behavior, inherited from `forcefield` subclasses like `ulsph`.
    - `parameters` (parameterforcefield): Stores interaction parameters dynamically injected into the base class.
    - `beadtype` (int): The bead type identifier associated with the forcefield instance.
    - `userid` (str): A unique identifier for the forcefield instance.
    - `name` (struct or str): A human-readable name for the forcefield instance, merged with `description`.
    - `description` (struct or str): A brief description of the forcefield, merged with `name`.
    - `version` (float): Version number for the forcefield instance.
    - `RULES` (genericdata): Defines specific rules or formulae applied to forcefield calculations.
    - `GLOBAL` (genericdata): Stores global parameters that apply to the entire forcefield.
    - `LOCAL` (genericdata): Contains local parameters specific to certain forcefield interactions.
    - `USER` (scriptdata): Contains USER parameters for scriptobject

    Methods:
    --------
    ### High-Level Methods:
    These methods are primarily used for interacting with the `dforcefield` instance:

    - **`__init__`**: Initializes the dynamic forcefield with base class, parameters, and custom attributes.
    - **`pair_style`**: Delegates the pair style computation to the `base_class`.
    - **`pair_diagcoeff`**: Delegates diagonal pair coefficient computation to the `base_class`.
    - **`pair_offdiagcoeff`**: Delegates off-diagonal pair coefficient computation to the `base_class`.
    - **`save`**: Saves the forcefield instance to a file, with headers, attributes, and parameters.
    - **`load`**: Loads a `dforcefield` instance from a file and validates its format.
    - **`parse`**: Parses content of a forcefield file to create a new `dforcefield` instance.
    - **`copy`**: Creates a copy of the current instance, optionally overriding core attributes.
    - **`compare`**: Compares the current instance with another `dforcefield` instance, showing differences.
    - **`missingVariables`**: Lists undefined variables in `parameters`.
    - **`detectVariables`**: Detects variables in the `pair_style`, `pair_diagcoeff`, and `pair_offdiagcoeff` outputs.
    - **`to_dict`**: Serializes the forcefield instance into a dictionary.
    - **`from_dict`**: Creates a `dforcefield` instance from a dictionary of attributes.
    - **`reset`**: Resets the forcefield instance to its initial state.
    - **`validate`**: Validates the forcefield instance to ensure all required attributes are set.
    - **`base_repr`**: Returns the representation of the `base_class`.
    - **`script`**: Returns the raw outputs of pair methods as a script.
    - **`scriptobject`**: Method to return a `scriptobject` based on the current `dforcefield` instance.

    ### Low-Level Methods:
    These methods handle internal logic and lower-level operations:

    - **`_inject_attributes`**: Injects the dynamic attributes into the `base_class`.
    - **`__getattr__`**: Dynamically accesses attributes in `name`, `description`, `parameters`, or `base_class`.
    - **`__setattr__`**: Manages setting core and dynamic attributes.
    - **`_load_base_class`**: Dynamically loads the base class from a string name.
    - **`list_forcefield_subclasses`**: Lists all subclasses of `forcefield` including those from additional modules.
    - **`extract_default_parameters`**: Extracts default parameters from the base class or its ancestors.
    - **`dispmax`**: Truncates the display of long content for concise output.
    - **`__hasattr__`**: Checks if an attribute exists in the instance, parameters, or base class.
    - **`__contains__`**: Checks if an attribute exists in the instance or `parameters`.
    - **`__len__`**: Returns the number of parameters in the forcefield.
    - **`__iter__`**: Iterates over all keys, including those in `name`, `description`, `parameters`, and scalar attributes.
    - **`keys`**: Returns the keys from the merged `name`, `description`, and `parameters`.
    - **`values`**: Returns the values from the merged `name`, `description`, and `parameters`.
    - **`items`**: Returns an iterator of key-value pairs from `name`, `description`, and `parameters`.
    - **`merged_name_description`**: Merges `name` and `description` fields into a struct.
    - **`_get_available_forcefields`**: Retrieves a list of available forcefield subclasses.
    - **`__repr__`**: Custom representation of the `dforcefield` instance.
    - **`__str__`**: Returns a string representation of the `dforcefield` instance.
    - **`_convert_value`**: Converts a string value to the appropriate Python type.
    - **`_parse_global_params`**: Parses global parameters from the content between `{}`.
    - **`_parse_struct_block`**: Parses key-value pairs from `description` or `name` blocks.
    - **`__copy__`**: Creates a shallow copy of the `dforcefield` instance, copying only the attributes at the top level without duplicating nested objects.
    - **`__deepcopy__`**: Creates a deep copy of the `dforcefield` instance, fully duplicating all attributes, including nested objects, while leaving class references intact.
    - **`get_global`**: Returns the `GLOBAL` parameters.
    - **`get_local`**: Returns the `LOCAL` parameters.
    - **`get_rules`**: Returns the `RULES` parameters.
    - **`set_global`**: Updates the `GLOBAL` parameters and recalculates the combined parameters.
    - **`set_local`**: Updates the `LOCAL` parameters and recalculates the combined parameters.
    - **`set_rules`**: Updates the `RULES` parameters and recalculates the combined parameters.
    - **`combine_parameters`**: Combines `GLOBAL`, `LOCAL`, and `RULES` into the current parameter configuration.
    - **`update_parameters`**: Updates `self.parameters` after changes to `GLOBAL`, `LOCAL`, or `RULES`.

    Example Usage:
    --------------
        >>> dynamic_ff = dforcefield(ulsph, beadtype=1, userid="dynamic_water", USER=parameterforcefield(rho=1000))
        >>> dynamic_ff.pair_style()  # Uses attributes from the dforcefield instance
        lj/cut
        >>> dynamic_ff.compare(another_ff_instance, printflag=True)
    """


    # Display
    _maxdisplay = 40
    # Class attribute for the six specific attributes + 3 generic attributes + USER set by scriptobject
    _dforcefield_specific_attributes = {'name', 'description', 'beadtype', 'userid', 'version', 'parameters','RULES','GLOBAL','LOCAL','USER'}
    # Class attribute: construction flag is True by default for all instances
    _in_construction = True
    RULES = parameterforcefield()  # Default empty RULES at the class level

    def __init__(self, base_class=None, beadtype=1, userid=None, USER=parameterforcefield(),
                 name=None, description=None, version=0.1, additional_modules=None,
                 printflag=False, verbose=False,
                 GLOBAL=None, LOCAL=None, **kwargs):
        """
        Initialize a dynamic forcefield with default or custom values.

        Args:
            base_class (str or class): The base class to use (e.g., 'ulsph', tlsph, etc.) or the actual class.
            beadtype (int): The bead type identifier. Default is 1.
            userid (str): User ID for the material. Default is None.
            name (str): Human-readable name for the forcefield. Default is None.
            description (str): Description of the forcefield. Default is None.
            version (float): Version number for the forcefield. Default is 0.1.
            USER (parameterforcefield): Custom parameters to override defaults.
            additional_modules (module or list of modules): Additional modules to search for forcefields.
            GLOBAL (parameterforcefield): Global parameters to be used. Default is None.
            LOCAL (parameterforcefield): Local parameters to be used. Default is None.
            kwargs: Additional parameters passed to the base class.
        """

        # Initialize GLOBAL and LOCAL containers
        self.GLOBAL = GLOBAL if GLOBAL else genericdata()
        self.LOCAL = LOCAL if LOCAL else genericdata()
        self.RULES = genericdata()  # Initialize RULES, to be populated later if applicable

        # Initialize USER containter (which is used by scriptobject)
        self.USER = scriptdata()

        #• Initialize print/verbose behavior
        self.printflag = printflag
        self.verbose = verbose

        # Step 1a: Handle base_class, either a string or a class reference
        print(f"\nInitializing dforcefield with base_class: {base_class}")

        # Handle base_class loading (using additional_modules if needed)
        if isinstance(base_class, str):
            # Get available forcefields and short names mapping
            available_forcefields, short_name_mapping = self.list_forcefield_subclasses(
                printflag=False,
                additional_modules=additional_modules
            )
            # Check if the provided base_class name matches either the full or short name
            if base_class not in available_forcefields and base_class not in short_name_mapping:
                available_classes = ", ".join(short_name_mapping.keys())
                raise ValueError(
                    f"Invalid base_class: '{base_class}'. Must be one of the available forcefields: {available_classes}"
            )
            # Use the full name from the short name if necessary
            # if base_class in short_name_mapping:
            #     base_class = short_name_mapping[base_class]

            # Load the base class or method
            base_class = dforcefield._load_base_class(base_class, additional_modules=additional_modules)

        # Ensure the base_class is valid
        if not (inspect.isclass(base_class) and issubclass(base_class, forcefield)) and not (
            callable(base_class) and hasattr(base_class, '__qualname__') and
            base_class.__qualname__.split('.')[0] in {cls.__name__ for cls in generic.__subclasses__()}
        ):
            raise ValueError(
                f"Invalid base_class: {base_class}. It must be a valid subclass of 'forcefield' or a callable forcefield method "
                f"from a 'generic' subclass."
        )

        # Step 1b: Initialize the base_class
        # If base_class is callable (like a method), call it to get an instance; otherwise, instantiate directly
        if callable(base_class):
            # Check if the base_class is a method of a generic subclass
            qualname = base_class.__qualname__.split('.')
            parent_class_name = qualname[0] if len(qualname) > 1 else None
            parent_class = next((cls for cls in generic.__subclasses__() if cls.__name__ == parent_class_name), None)

            if parent_class:
                # Instantiate the parent class and call the method on it
                parent_instance = parent_class()  # Create an instance of the parent class (e.g., USERSMD)
                self.base_class = base_class(parent_instance)  # Call the method with the instance context
            else:
                # Call the method directly if no parent class context is needed
                self.base_class = base_class()
        else:
            self.base_class = base_class()  # Instantiate the class if it's not a method

        # Ensure the base_class is valid and handle classes and methods separately
        if inspect.isclass(base_class):
            # Step 2a: Extract default parameters from the base class
            extracted_info = self.extract_default_parameters(base_class, displayflag=True)
        elif callable(base_class) and hasattr(base_class, '__qualname__'):
            # If base_class is a method, extract the parent class name
            parent_class_name = base_class.__qualname__.split('.')[0]
            parent_class = next(
                (cls for cls in generic.__subclasses__() if cls.__name__ == parent_class_name), None
            )
            if parent_class is None:
                raise ValueError(f"Unable to find parent class '{parent_class_name}' for method '{base_class.__name__}'.")

            # Step 2b: Extract default parameters from the method within its parent class
            extracted_info = self.extract_default_parameters(parent_class, method_name=base_class.__name__, displayflag=True)
        else:
            raise ValueError(
                f"Invalid base_class: {base_class}. It must be a valid subclass of 'forcefield' or a callable forcefield method."
            )

        # Populate RULES, GLOBAL, and LOCAL if extracted
        self.RULES = extracted_info.get("RULES", genericdata())
        self.GLOBAL = extracted_info.get("GLOBAL", genericdata()) + self.GLOBAL
        self.LOCAL = extracted_info.get("LOCAL", genericdata()) + self.LOCAL

        # Step 3: Initialize the base_class
        #self.base_class = base_class()  # Instantiate the base_class if it's callable

        # Step 4: Initialize name and description
        default_name = self.base_class.name  # Access base_class.name after ensuring it's properly set

        # Handling 'name'
        if name:
            if isinstance(name, str):
                name = default_name + struct(material=name)
            elif isinstance(name, struct):
                name = default_name + name
            else:
                raise TypeError(f"name must be of type str or struct, not {type(name)}")
        else:
            name = default_name + struct(material=self.__class__.__name__.lower())

        self.name = name

        # Handling 'description'
        default_description = self.base_class.description
        if description:
            if isinstance(description, str):
                description = default_description + struct(material=description)
            elif isinstance(description, struct):
                description = default_description + description
            else:
                raise TypeError(f"description must be of type parameterforcefield, not {type(description)}")
        else:
            description = default_description + struct(material=f"{self.__class__.__name__.lower()} beads - SPH-like")

        self.description = description

        # Step 5: Other properties and parameters
        self.userid = userid if userid else self.__class__.__name__.lower()
        self.version = version
        self.beadtype = beadtype

        # Ensure USER is of the correct type
        if not isinstance(USER, parameterforcefield):
            raise TypeError(f"USER must be of type parameterforcefield, not {type(USER)}")

        # Merge USER and kwargs into parameters
        # If no default parameters, proceed with user parameters
        if extracted_info["parameters"] is None:
            self.parameters = self.GLOBAL + self.LOCAL + self.RULES + USER + parameterforcefield(**kwargs)
        else:
            self.parameters = extracted_info["parameters"] + self.GLOBAL + self.LOCAL + self.RULES + USER + parameterforcefield(**kwargs)
        # After merging parameters, ensure USER is not part of it
        # if 'USER' in self.parameters:
        #     del self.parameters['USER']

        # End of construction
        self._in_construction = False

        # Inject dforcefield attributes into the base class
        self._inject_attributes()


    # New methods to access GLOBAL, LOCAL, and RULES

    def get_global(self):
        """Return the GLOBAL parameters for this dforcefield instance."""
        return self.GLOBAL

    def get_local(self):
        """Return the LOCAL parameters for this dforcefield instance."""
        return self.LOCAL

    def get_rules(self):
        """Return the RULES parameters for this dforcefield instance."""
        return self.RULES

    # Method to combine GLOBAL, LOCAL, and RULES
    def combine_parameters(self):
        """
        Combine GLOBAL, LOCAL, and RULES to get the current parameter configuration.
        """
        return self.GLOBAL + self.LOCAL + self.RULES


    def update_parameters(self):
        """
        Update self.parameters by combining GLOBAL, LOCAL, RULES, and USER parameters.
        """
        self.parameters = self.parameters + self.combine_parameters()
        self._inject_attributes()


    def set_local(self, new_local=None, **kwargs):
        """
        Update the LOCAL parameters and adjust the combined parameters accordingly.

        Args:
        -----
        new_local : parameterforcefield, optional
            The new LOCAL parameters to set. If None, only the parameters in kwargs are modified.
        kwargs : dict, optional
            Keyword arguments representing individual parameters to add or modify in LOCAL.
        """
        if new_local is not None and not isinstance(new_local, parameterforcefield):
            raise TypeError(f"LOCAL must be of type parameterforcefield, not {type(new_local)}.")

        # Combine current LOCAL, new_local, and additional parameters in kwargs
        self.LOCAL = (self.LOCAL + (new_local or genericdata()) + genericdata(**kwargs))
        self.update_parameters()

    def set_global(self, new_global=None, **kwargs):
        """
        Update the GLOBAL parameters and adjust the combined parameters accordingly.

        Args:
        -----
        new_global : parameterforcefield, optional
            The new GLOBAL parameters to set. If None, only the parameters in kwargs are modified.
        kwargs : dict, optional
            Keyword arguments representing individual parameters to add or modify in GLOBAL.
        """
        if new_global is not None and not isinstance(new_global, parameterforcefield):
            raise TypeError(f"GLOBAL must be of type parameterforcefield, not {type(new_global)}.")

        # Combine current GLOBAL, new_global, and additional parameters in kwargs
        self.GLOBAL = (self.GLOBAL + (new_global or genericdata()) + genericdata(**kwargs))
        self.update_parameters()

    def set_rules(self, new_rules=None, **kwargs):
        """
        Update the RULES parameters and adjust the combined parameters accordingly.

        Args:
        -----
        new_rules : parameterforcefield, optional
            The new RULES parameters to set. If None, only the parameters in kwargs are modified.
        kwargs : dict, optional
            Keyword arguments representing individual parameters to add or modify in RULES.
        """
        if new_rules is not None and not isinstance(new_rules, parameterforcefield):
            raise TypeError(f"RULES must be of type parameterforcefield, not {type(new_rules)}.")

        # Combine current RULES, new_rules, and additional parameters in kwargs
        self.RULES = (self.RULES + (new_rules or genericdata()) + genericdata(**kwargs))
        self.update_parameters()


    @classmethod
    def _load_base_class(cls, base_class_name, additional_modules=None):
        """
        Dynamically load the base class by its string name from the available forcefield subclasses
        or additional modules.

        Args:
            base_class_name (str): The name of the forcefield class or method to load.
            additional_modules (list): Optional list of additional modules to search for the base class.

        Returns:
            class or function: The base class or function if found.
        """
        # Retrieve subclass information and short name mappings
        subclasses_info, short_name_mapping = cls.list_forcefield_subclasses(
            printflag=False, additional_modules=additional_modules
            )
        print(f"Trying to load base_class: {base_class_name}")

        # Directly look for the exact name in the subclasses_info
        if base_class_name in subclasses_info:
            info = subclasses_info[base_class_name]
            if not info['loaded']:
                module_name = info['module']
                try:
                    print(f"Attempting to import module '{module_name}' and load '{base_class_name}'")
                    module = __import__(module_name, fromlist=[base_class_name])
                    target = getattr(module, base_class_name)

                    # Check if the target is a class or method
                    if inspect.isclass(target) and issubclass(target, forcefield):
                        return target
                    elif inspect.isfunction(target):
                        return target
                    else:
                        raise ValueError(f"Loaded target '{base_class_name}' is not a suitable forcefield class or method.")
                except (ImportError, AttributeError) as e:
                    available_classes = cls._get_available_forcefields()
                    raise ImportError(f"Failed to load the target '{base_class_name}' from module '{module_name}'. "
                                      f"Available forcefields: {available_classes}") from e

            # If already loaded, return the class directly
            return sys.modules[info['module']].__dict__.get(base_class_name)

        # If not found directly, check for nested methods within classes
        for key, info in subclasses_info.items():
            # Check if base_class_name is a method of a class
            if '.' in key and key.split('.')[-1] == base_class_name:
                parent_class_name = key.split('.')[0]
                if parent_class_name in subclasses_info:
                    # Load the parent class if it hasn't been loaded yet
                    parent_class_info = subclasses_info[parent_class_name]
                    if not parent_class_info['loaded']:
                        parent_class = cls._load_base_class(parent_class_name, additional_modules=additional_modules)
                    else:
                        # Directly access the parent class from the loaded module
                        module = sys.modules[parent_class_info['module']]
                        parent_class = getattr(module, parent_class_name)

                    # Get the method from the loaded parent class
                    method = getattr(parent_class, base_class_name, None)
                    if method and callable(method):
                        return method
                    else:
                        raise ValueError(f"Method '{base_class_name}' was not found in the parent class '{parent_class_name}' or is not callable.")

        # If the target is a direct class but not a method or nested class, throw an appropriate error
        raise ValueError(f"Target '{base_class_name}' is not a recognized class or method. "
                         f"Available entries: {cls._get_available_forcefields()}")


    @classmethod
    def _get_available_forcefields(cls):
        """
        Retrieve the list of available forcefield subclasses.

        Returns:
            list: A list of available forcefield subclass names.
        """
        subclasses_info = cls.list_forcefield_subclasses(printflag=False)
        return list(subclasses_info.keys())


    @classmethod
    def extract_default_parameters(cls, base_class, method_name=None, displayflag=True):
        """
        Extract default parameters from the base class or its method by instantiating the class.

        If the base class is derived from `generic`, this function will also extract RULES, LOCAL,
        and GLOBAL attributes if they are defined once the instance is created.

        Parameters:
        -----------
        base_class : class
            The base class from which to extract default parameterforcefield values or a method.
        method_name : str, optional
            The name of the method to call on the instance, if applicable (e.g., `newtonianfluid`).
        displayflag : bool, optional (default=True)
            If True, prints a message when no default parameters are found.

        Returns:
        --------
        dict
            A dictionary containing the default parameters extracted from the base class or method.
            Also includes RULES, LOCAL, and GLOBAL if applicable.
        """
        extracted_info = {
            "parameters": parameterforcefield(),
            "RULES": genericdata(),
            "GLOBAL": genericdata(),
            "LOCAL": genericdata()
        }

        try:
            # Create an instance of the base class
            instance = base_class()

            # Extract parameters if the instance has them
            if hasattr(instance, 'parameters') and isinstance(instance.parameters, parameterforcefield):
                extracted_info["parameters"] = instance.parameters

            # Extract RULES, GLOBAL, and LOCAL if they exist
            for attr in ["RULES", "GLOBAL", "LOCAL"]:
                if hasattr(instance, attr) and isinstance(getattr(instance, attr), parameterforcefield):
                    extracted_info[attr] = getattr(instance, attr)

            # If a method is specified, call it to get additional parameters
            if method_name and hasattr(instance, method_name):
                method = getattr(instance, method_name)
                if callable(method):
                    try:
                        result = method()  # Call the method (ensure it returns the desired forcefield object)
                        if hasattr(result, 'parameters') and isinstance(result.parameters, parameterforcefield):
                            extracted_info["parameters"] = result.parameters
                    except Exception as e:
                        if displayflag:
                            print(f"Error calling method {method_name} on {base_class.__name__}: {str(e)}")

        except TypeError as e:
            if displayflag:
                print(f"Could not instantiate {base_class.__name__}: {str(e)}")

        # If no parameters found or cannot instantiate the class, return None
        if displayflag and not extracted_info["parameters"]:
            print(f"No default parameters found in {base_class.__name__} or its ancestors.")

        return extracted_info


    def _inject_attributes(self):
        """Inject dforcefield attributes into the base class, bypassing __setattr__."""
        if not self._in_construction:  # Prevent injection during construction
            # Check if base_class is a class (not a method)
            if inspect.isclass(self.base_class):
                self.base_class.__dict__.update({
                    'name': self.name,
                    'description': self.description,
                    'beadtype': self.beadtype,
                    'userid': self.userid,
                    'version': self.version,
                    'parameters': self.parameters
                })
            else:
                # For methods, set attributes on the instance itself, as methods can't have attributes directly
                # Methods are often accessed from an instance, so we update the instance's attributes instead
                self.base_class.name = self.name
                self.base_class.description = self.description
                self.base_class.beadtype = self.beadtype
                self.base_class.userid = self.userid
                self.base_class.version = self.version
                self.base_class.parameters = self.parameters


    def __getattr__(self, attr):
        """
        Shorthand for accessing parameters, base class attributes, or attributes in 'name' and 'description'.
        If an attribute exists in both 'name' and 'description', their contents are combined with a newline.
        """
        # Check if the attribute exists in the instance's __dict__
        if attr in self.__dict__:
            return self.__dict__[attr]
        # Check if the attribute exists in 'name' and 'description'
        name_attr = getattr(self.name, attr, None) if isinstance(self.name, struct) else None
        description_attr = getattr(self.description, attr, None) if isinstance(self.description, struct) else None
        # If the attribute exists in both 'name' and 'description', combine them with a newline
        if name_attr and description_attr:
            return [name_attr, description_attr]
        # If the attribute exists in 'name' only
        if name_attr:
            return name_attr
        # If the attribute exists in 'description' only
        if description_attr:
            return description_attr
        # Check if the attribute exists in 'parameters'
        if hasattr(self.parameters, attr):
            return getattr(self.parameters, attr)
        # Check if the attribute exists in the 'base_class'
        if hasattr(self.base_class, attr):
            return getattr(self.base_class, attr)
        # Raise an AttributeError if the attribute is not found
        raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{attr}'")



    def __setattr__(self, attr, value):
        """
        Shorthand for setting attributes. Attributes specific to dforcefield are handled separately.
        New attributes are added to parameters if they are not part of the dforcefield-specific attributes.
        """
        # Handle internal attributes like _in_construction directly in the instance
        # and Check if the attribute is part of the dforcefield-specific attributes
        if hasattr(self.__class__, attr) or attr in self._dforcefield_specific_attributes:
            if attr=="parameters":
                if not isinstance(value,parameterforcefield):
                    raise TypeError("parameters must be of type parameterforcefield, not of type {}".format(type(value)))
                self.__dict__[attr] = self.detectVariables() + value # check that all variables are defined
            else:
                self.__dict__[attr] = value
            if not self._in_construction and attr in self._dforcefield_specific_attributes:
                self._inject_attributes()
        # Handle dynamic setting of attributes in parameters
        elif 'parameters' in self.__dict__:
            # Update parameters (existing or new attributes)
            setattr(self.parameters, attr, value)

            # Ensure injection occurs when parameters is updated
            if not self._in_construction:
                self._inject_attributes()
        else:
            # Fallback to default behavior if the attribute isn't part of parameters or specific attributes
            super().__setattr__(attr, value)


    def __hasattr__(self, attr):
        """Check if an attribute exists in the dforcefield instance, class, parameters, or the base class."""
        # Check for instance attribute
        if attr in self.__dict__:
            return True
        # Check for class attribute
        if hasattr(self.__class__, attr):
            return True
        # Check for parameters
        if attr in self.parameters.keys():
            return True
        # Check for attribute in base class
        if hasattr(self.base_class, attr):
            return True
        return False


    def __contains__(self, item):
        """Check if an attribute exists in the dforcefield instance or its parameters."""
        return item in self.keys()


    def __len__(self):
        """
        Return the number of parameters in the forcefield.
        This will use the len method of parameters.
        """
        return len(self.keys())


    @property
    def merged_name_description(self):
        """
        Return a struct containing the merged content of 'name' and 'description'.
        If an attribute exists in both, their values are combined into a list.
        """
        merged_data = {}

        # Add attributes from 'name' if it's a struct
        if isinstance(self.name, struct):
            merged_data.update(self.name.__dict__)

        # Add/merge attributes from 'description' if it's a struct
        if isinstance(self.description, struct):
            for key, value in self.description.__dict__.items():
                if key in merged_data:
                    # Combine the values into a list if the key already exists
                    existing_value = merged_data[key]
                    if not isinstance(existing_value, list):
                        existing_value = [existing_value]  # Convert to list if not already a list
                    merged_data[key] = existing_value + [value]  # Add the new value to the list
                else:
                    merged_data[key] = value

        # Return the merged struct
        return struct(**merged_data)



    def keys(self):
        """
        Return the keys of the merged struct, parameters, and scalar attributes.
        """
        keys_set = set(self.merged_name_description.__dict__.keys())
        keys_set.update(self.parameters.keys())
        keys_set.update(['version', 'userid', 'beadtype'])
        return list(keys_set)

    def values(self):
        """
        Return the values of the merged struct, parameters, and scalar attributes.
        """
        values_list = list(self.merged_name_description.__dict__.values())
        values_list.extend(self.parameters.values())
        values_list.extend([self.version, self.userid, self.beadtype])
        return values_list

    def items(self):
        """
        Return an iterator over (key, value) pairs from the merged struct, parameters, and scalar attributes.
        """
        # Yield items from merged struct
        for item in self.merged_name_description.__dict__.items():
            yield item

        # Yield items from parameters
        for item in self.parameters.items():
            yield item

        # Yield scalar attribute items
        yield ('version', self.version)
        yield ('userid', self.userid)
        yield ('beadtype', self.beadtype)

    def __iter__(self):
        """
        Iterate over all keys, including those in the merged struct, parameters, and scalar attributes.
        """
        for key in self.merged_name_description.__dict__:
            yield key
        for key in self.parameters:
            yield key
        yield 'version'
        yield 'userid'
        yield 'beadtype'


    def __repr__(self):
        """
        Custom __repr__ method that indicates it is a dforcefield instance and provides information
        about the base class, name, and parameters dynamically.
        """
        base_class_name = self.base_class.__class__.__name__ if self.base_class else "None"
        # Generate the name table dynamically (iterating over key-value pairs)
        sep = "  "+"-"*20+":"+"-"*30
        name_table = sep + "[ BEADTYPE ]\n"
        key = "beadtype"
        name_table += f"  {key:<20}: {self.beadtype}\n"
        name_table += sep + "[ FF CLASS (read only) ]\n"
        # Generate the name table dynamically (iterating over key-value pairs)
        for key, value in self.merged_name_description.items():
            # Check if the value is a list (when the field exists in both name and description)
            if isinstance(value, list):
                for idx, val in enumerate(value):
                    value_lines = self.dispmax(val).splitlines()

                    # Apply {key:<20} to the first line of the first item in the list
                    if idx == 0:
                        name_table += f"  {key:<20}: {value_lines[0]}\n"
                    else:
                        name_table += f"  {'':<20}  {value_lines[0]}\n"  # Align subsequent items
                    # Handle multi-line values within each list item
                    for line in value_lines[1:]:
                        name_table += f"  {'':<20}  {line}\n"  # Indent the remaining lines
            else:
                # Handle non-list values
                value_lines = self.dispmax(value).splitlines()
                # Apply {key:<20} to the first line and indent the subsequent lines
                name_table += f"  {key:<20}: {value_lines[0]}\n"  # First line with key aligned
                for line in value_lines[1:]:
                    name_table += f"  {'':<20}  {line}\n"  # Indent the remaining lines
        # Evaluate all values of parameters
        tmp = self.parameters.eval()
        tmpkey= ""
        # Generate the parameters table dynamically (iterating over key-value pairs)
        parameters_table = sep+ "[ PARAMS ]\n"
        missing = 0
        for key, value in self.parameters.items():  # Assuming parameters is a class with __dict__
            parameters_table += f"  {key:<20}: {value}"  # Align the keys and values dynamically
            if isinstance(value,str):
                if value=="${"+key+"}":
                    parameters_table += "\t < implicit definition >\n"
                    missing += 1
                else:
                    parameters_table += f"\n  {tmpkey:<20}= {self.dispmax(tmp.getattr(key))}\n"
            else:
                parameters_table += "\n"
        parameters_table += sep+f" >> {len(self.parameters)} definitions ({missing} missing)\n"

        # Combine the name and parameters table with a general instance representation
        print(f'\ndforcefield "{self.userid}" derived from "{base_class_name}"\n{name_table}{parameters_table}\n')
        return self.__str__()

    def base_repr(self):
        """Returns the representation of the base_class."""
        self.base_class.__repr__()
        return self.base_class.__str__()

    @property
    def script(self):
        """
        Return the raw content of the pair_* outputs combined into a single script-like format.

        Returns:
        --------
        str
            The raw outputs from pair_style, pair_diagcoeff, and pair_offdiagcoeff concatenated into a script.
        """
        # Get the raw outputs from the pair_* methods
        pair_style_output = self.pair_style(printflag=False, raw=True)
        pair_diagcoeff_output = self.pair_diagcoeff(printflag=False, raw=True)
        pair_offdiagcoeff_output = self.pair_offdiagcoeff(printflag=False, raw=True)

        # Combine the outputs into a script-like format
        script_content = "\n".join([
            "# Script output from dforcefield:",
            "# Pair style:",
            pair_style_output,
            "\n# Diagonal coefficients:",
            pair_diagcoeff_output,
            "\n# Off-diagonal coefficients:",
            pair_offdiagcoeff_output
        ])

        return script_content

    def __str__(self):
        base_class_name = self.base_class.__class__.__name__ if self.base_class else "None"
        return f"<dforcefield instance with base class: {base_class_name}, userid: {self.userid}, beadtype: {self.beadtype}>"

    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 show(self):
        """Show the corresponding base_class forcefield definition """
        self.base_class.__repr__()
        return self.base_class.__str__()

    def pair_style(self, printflag=None, verbose=None, raw=False):
        """Delegate pair_style to the base class, ensuring it uses the correct attributes."""
        printflag = self.printflag if printflag is None else printflag
        verbose = self.verbose if verbose is None else verbose
        self._inject_attributes()
        return self.base_class.pair_style(printflag=printflag, verbose=verbose, raw=raw,USER=None, beadtype=self.beadtype, userid=self.userid)


    def pair_diagcoeff(self, printflag=None, verbose=None, i=None, raw=False):
        """Delegate pair_diagcoeff to the base class, ensuring it uses the correct attributes."""
        printflag = self.printflag if printflag is None else printflag
        verbose = self.verbose if verbose is None else verbose
        self._inject_attributes()
        return self.base_class.pair_diagcoeff(printflag=printflag, verbose=verbose, i=i, raw=raw, USER=None, beadtype=self.beadtype, userid=self.userid)


    def pair_offdiagcoeff(self, o=None, printflag=None, verbose=None, i=None, raw=False,oname=None):
        """Delegate pair_offdiagcoeff to the base class, ensuring it uses the correct attributes."""
        printflag = self.printflag if printflag is None else printflag
        verbose = self.verbose if verbose is None else verbose
        self._inject_attributes()
        return self.base_class.pair_offdiagcoeff(o=o, printflag=printflag, verbose=verbose, i=i, raw=raw, USER=None, beadtype=self.beadtype, userid=self.userid,oname=oname)


    def __add__(self, other):
        """Concatenate dforcefield attributes, i

        ncluding RULES, GLOBAL, and LOCAL, if different, else keep the version of self."""
        if not isinstance(other, dforcefield):
            raise TypeError(f"Cannot concatenate {type(other)} with dforcefield.")

        if self.base_class != other.base_class:
            raise ValueError(f"Cannot concatenate dforcefield instances with different base classes ({self.base_class} != {other.base_class}).")

        # Concatenate name, description, beadtype, userid, and parameters if different, else keep self's version
        new_name = self.name if self.name == other.name else f"{self.name}_{other.name}"
        new_description = self.description if self.description == other.description else f"{self.description} + {other.description}"
        new_beadtype = self.beadtype if self.beadtype == other.beadtype else f"{self.beadtype}, {other.beadtype}"
        new_parameters = self.parameters + other.parameters  # Assuming parameters supports '+'
        new_userid = self.userid if self.userid == other.userid else f"{self.userid}_{other.userid}"

        # Concatenate RULES, GLOBAL, and LOCAL, preserving their combined nature
        new_rules = self.RULES + other.RULES
        new_global = self.GLOBAL + other.GLOBAL
        new_local = self.LOCAL + other.LOCAL

        # Keep the version from self
        new_version = self.version

        # Return a new dforcefield instance with concatenated attributes
        return dforcefield(
            base_class=self.base_class,
            beadtype=new_beadtype,
            userid=new_userid,
            name=new_name,
            description=new_description,
            version=new_version,
            USER=new_parameters,
            RULES=new_rules,
            GLOBAL=new_global,
            LOCAL=new_local
        )


    def __or__(self, other):
        """ Overload | pipe operator in dscript """
        # Convert the dscript instance into a pipescript
        leftarg = self.pscript()
        # Simply use the existing pipe operator for pipescript
        return leftarg | other


    def copy(self, beadtype=None, userid=None, name=None, description=None, version=None, USER=parameterforcefield(), RULES=None, GLOBAL=None, LOCAL=None, **kwargs):
        """
        Create a new instance of dforcefield with the option to override key attributes including RULES, GLOBAL, and LOCAL.
        """
        # Use the current instance's values as defaults, and override if values are provided
        new_beadtype = beadtype if beadtype is not None else self.beadtype
        new_userid = userid if userid is not None else self.userid
        new_name = name if name is not None else self.name
        new_description = description if description is not None else self.description
        new_version = version if version is not None else self.version
        if new_userid == self.userid:
            new_userid = new_userid + " (copy)"
        # USER is computed by combining self.parameters + USER + parameterforcefield(**kwargs)
        new_parameters = self.parameters + USER

        # Combine or override RULES, GLOBAL, and LOCAL if provided
        new_rules = RULES if RULES is not None else self.RULES
        new_global = GLOBAL if GLOBAL is not None else self.GLOBAL
        new_local = LOCAL if LOCAL is not None else self.LOCAL

        # Create and return the new instance with overridden values
        return self.__class__(
            base_class=self.base_class.__class__,
            beadtype=new_beadtype,
            userid=new_userid,
            name=new_name,
            description=new_description,
            version=new_version,
            USER=new_parameters,
            RULES=new_rules,
            GLOBAL=new_global,
            LOCAL=new_local,
            **kwargs
        )


    def update(self, **kwargs):
        """
        Update multiple attributes of the dforcefield instance at once, including RULES, GLOBAL, and LOCAL.

        Args:
            kwargs: Key-value pairs of attributes to update.
        """
        for key, value in kwargs.items():
            if key == "RULES":
                self.RULES = self.RULES + value if self.RULES else value
            elif key == "GLOBAL":
                self.GLOBAL = self.GLOBAL + value if self.GLOBAL else value
            elif key == "LOCAL":
                self.LOCAL = self.LOCAL + value if self.LOCAL else value
            else:
                setattr(self, key, value)



    def to_dict(self):
        """
        Serialize the dforcefield instance to a dictionary, including RULES, GLOBAL, and LOCAL.

        Returns:
            dict: A dictionary containing all the attributes and their current values.

        Example:
            config = dynamic_water.to_dict()
            print(config)
        """
        data = {
            "name": self.name,
            "description": self.description,
            "beadtype": self.beadtype,
            "userid": self.userid,
            "version": self.version,
            "parameters": dict(self.parameters),  # Convert parameters to a standard dict
            "base_class": self.base_class.__class__.__name__,
            "RULES": dict(self.RULES) if self.RULES else {},  # Include RULES if defined
            "GLOBAL": dict(self.GLOBAL) if self.GLOBAL else {},  # Include GLOBAL if defined
            "LOCAL": dict(self.LOCAL) if self.LOCAL else {}  # Include LOCAL if defined
        }
        return data



    @classmethod
    def from_dict(cls, data):
        """
        Create a dforcefield instance from a dictionary, including RULES, GLOBAL, and LOCAL.

        Args:
            data (dict): A dictionary containing the attributes to initialize the dforcefield.

        Returns:
            dforcefield: A new dforcefield instance.

        Example:
            config = {
                "name": "water_ff",
                "description": "water forcefield",
                "beadtype": 2,
                "userid": "water_sim",
                "version": 1.0,
                "parameters": {"rho": 1000, "sigma": 0.5},
                "base_class": "ulsph",
                "RULES": {"some_rule": "value"},
                "GLOBAL": {"global_param": 10},
                "LOCAL": {"local_param": 5}
            }

            new_ff = dforcefield.from_dict(config)
        """
        # Extract base class information
        base_class_name = data.pop("base_class", None)
        base_class = globals().get(base_class_name) if base_class_name else None

        # Extract RULES, GLOBAL, and LOCAL from the data dictionary if they exist
        rules = data.pop("RULES", parameterforcefield())
        global_params = data.pop("GLOBAL", parameterforcefield())
        local_params = data.pop("LOCAL", parameterforcefield())

        # Create and return the new dforcefield instance with extracted or default RULES, GLOBAL, and LOCAL
        return cls(
            base_class=base_class,
            RULES=rules,
            GLOBAL=global_params,
            LOCAL=local_params,
            **data
        )


    def reset(self):
        """
        Reset the dforcefield instance to its initial state, reapplying the default values
        including RULES, GLOBAL, and LOCAL.
        """
        # Preserve the current RULES, GLOBAL, and LOCAL or reset them
        current_rules = self.RULES
        current_global = self.GLOBAL
        current_local = self.LOCAL

        # Reinitialize the instance with existing or reset RULES, GLOBAL, and LOCAL
        self.__init__(
            base_class=self.base_class.__class__,
            beadtype=self.beadtype,
            userid=self.userid,
            name=self.name,
            description=self.description,
            version=self.version,
            RULES=current_rules,
            GLOBAL=current_global,
            LOCAL=current_local
            )

    def validate(self):
        """
        Validate the dforcefield instance to ensure all required attributes are set.

        Raises:
            ValueError: If any required attributes are missing or invalid.
        """
        required_fields = self._dforcefield_specific_attributes

        for field in required_fields:
            if not getattr(self, field):
                raise ValueError(f"{field} is required and not set.")

        print("Validation successful.")


    def compare(self, other, printflag=False):
        """
        Compare the current instance with another dforcefield instance, including RULES, GLOBAL, and LOCAL.

        Args:
        -----
        other : dforcefield
            The other instance to compare.

        printflag : bool, optional (default: False)
            If True, prints a table showing all parameters, with differences marked by '*'.

        Returns:
        --------
        dict
            A dictionary showing differences between the two instances.

        Raises:
        -------
        TypeError: If the other object is not a dforcefield instance.

        Example:
        --------
        diffs = dynamic_water.compare(dynamic_salt)
        if printflag:
            Prints a comparison table with a type column and a legend at the end.
        """
        # Define abbreviations for types
        type_abbreviations = {
            str: 'STR',
            int: 'INT',
            float: 'FLT',
            bool: 'BOOL',
            list: 'LST',
            tuple: 'TPL',
            dict: 'DCT',
            None: 'NON',
            'missing': 'MIS'
        }

        def get_type_abbrev(value):
            """Return the abbreviation for the type of the value."""
            if value is None:
                return type_abbreviations[None]
            return type_abbreviations.get(type(value), 'UNK')  # Use 'UNK' for unknown types

        def format_simple_type(value):
            """Format a simple value (str, number, or bool) for display."""
            return self.dispmax(str(value))

        def format_iterable(value_self, value_other, key, comparison_table, difference):
            """Handle lists, tuples, and other iterable types."""
            max_length = max(len(value_self), len(value_other))
            for i in range(max_length):
                item_self = value_self[i] if i < len(value_self) else 'MISSING'
                item_other = value_other[i] if i < len(value_other) else 'MISSING'
                sub_difference = '*' if item_self != item_other else ' '
                comparison_table.append(
                    f"  {key if i == 0 else '':<15}: {sub_difference:^3}: {self.dispmax(str(item_self)):<30}: {get_type_abbrev(item_self):<5}: {self.dispmax(str(item_other)):<30}: {get_type_abbrev(item_other):<5}"
                )

        def format_dict_like(value_self, value_other, key, comparison_table, difference):
            """Handle dictionary-like objects (e.g., structs or dicts)."""
            comparison_table.append(f"  {key:<15}: {difference:^3}: ")
            comparison_table.append(f"  {'':<15}  {'Self':<30} {'Type':<5} {'Other':<30} {'Type':<5}")
            sub_sep = "  "+"-"*15+":"+"-"*3+":"+"-"*30+":"+"-"*5+":"+"-"*30+":"+"-"*5
            comparison_table.append(sub_sep)
            sub_keys = set(value_self.keys()).union(value_other.keys())
            for sub_key in sorted(sub_keys):
                sub_value_self = value_self.get(sub_key, 'MISSING')
                sub_value_other = value_other.get(sub_key, 'MISSING')
                sub_difference = '*' if sub_value_self != sub_value_other else ' '
                comparison_table.append(
                    f"  {sub_key:<15}: {sub_difference:^3}: {self.dispmax(str(sub_value_self)):<30}: {get_type_abbrev(sub_value_self):<5}: {self.dispmax(str(sub_value_other)):<30}: {get_type_abbrev(sub_value_other):<5}"
                )
            comparison_table.append(sub_sep)

        def format_complex_type(value_self, value_other, key, comparison_table, difference):
            """Handle complex types, expanding them into multiple lines."""
            value_self_lines = str(value_self).splitlines()
            value_other_lines = str(value_other).splitlines()
            max_lines = max(len(value_self_lines), len(value_other_lines))
            for i in range(max_lines):
                line_self = value_self_lines[i] if i < len(value_self_lines) else 'MISSING'
                line_other = value_other_lines[i] if i < len(value_other_lines) else 'MISSING'
                comparison_table.append(
                    f"  {key if i == 0 else '':<15}: {difference if i == 0 else ' ':^3}: {self.dispmax(line_self):<30}: {get_type_abbrev(line_self):<5}: {self.dispmax(line_other):<30}: {get_type_abbrev(line_other):<5}"
                )

        if not isinstance(other, dforcefield):
            raise TypeError("Can only compare with another dforcefield instance.")

        diffs = {}

        # Collect all keys from both instances, including RULES, GLOBAL, and LOCAL
        all_keys = set(self.keys()).union(other.keys(), {'RULES', 'GLOBAL', 'LOCAL'})

        # Iterate over all keys and compare the values
        for key in all_keys:
            value_self = getattr(self, key, None)
            value_other = getattr(other, key, None)

            if value_self != value_other:
                diffs[key] = {"self": value_self, "other": value_other}

        # If printflag is True, print the comparison in a table format
        if printflag:
            sep = "  "+"-"*15 + ":" + "-"*3 + ":" + "-"*30 + ":" + "-"*5 + ":" + "-"*30 + ":" + "-"*5
            header = f"\n  {'Attribute':<15}: {'*':^3}: {'Self':<30}: {'Type':<5}: {'Other':<30}: {'Type':<5}\n" + sep
            comparison_table = [header]

            # Iterate over all parameters and mark differences with '*'
            for key in sorted(all_keys):  # Sort keys for a cleaner output
                value_self = getattr(self, key, 'MISSING')
                value_other = getattr(other, key, 'MISSING')
                difference = '*' if value_self != value_other else ' '

                # Check if the value is a simple type (str, number, or bool)
                if isinstance(value_self, (str, int, float, bool)) and isinstance(value_other, (str, int, float, bool)):
                    comparison_table.append(
                        f"  {key:<15}: {difference:^3}: {format_simple_type(value_self):<30}: {get_type_abbrev(value_self):<5}: {format_simple_type(value_other):<30}: {get_type_abbrev(value_other):<5}"
                    )
                # Check if the value is a list or tuple
                elif isinstance(value_self, (list, tuple)) and isinstance(value_other, (list, tuple)):
                    format_iterable(value_self, value_other, key, comparison_table, difference)
                # Check if the value is a dictionary-like object
                elif hasattr(value_self, 'keys') and hasattr(value_other, 'keys'):
                    format_dict_like(value_self, value_other, key, comparison_table, difference)
                else:
                    # Handle other complex types
                    format_complex_type(value_self, value_other, key, comparison_table, difference)

            comparison_table.append(sep)
            # Add a legend for type abbreviations
            legend = "\nType Legend: " + ", ".join(f"{abbrev} = {('NoneType' if type_ is None else type_.__name__.upper())}" for type_, abbrev in type_abbreviations.items() if type_ != 'missing')

            comparison_table.append(legend)

            # Print the comparison table
            print("\n".join(comparison_table))

        return diffs



    def save(self, filename=None, foldername=None, overwrite=False):
        """
        Save the dforcefield instance to a file with a header, formatted content, and the base_class.

        Args:
        -----
        filename (str): The name of the file to save. Defaults to self.userid if not provided.
        foldername (str): The folder in which to save the file. Defaults to a temporary directory.
        overwrite (bool): Whether to overwrite the file if it already exists. Defaults to False.

        Raises:
        -------
        ValueError: If base_class is None or not a subclass of forcefield.
        FileExistsError: If the file already exists and overwrite is False.

        Notes:
        ------
        The file format follows a specific structure for saving forcefield attributes, base_class,
        name, description, and parameters.

        Example Output: (without the comments)
        ---------------
        # DFORCEFIELD SAVE FILE
        base_class = "tlsph"
        beadtype = 1
        userid = "dynamic_water"
        version = 1.0

        description:{forcefield="LAMMPS:SMD", style="tlsph", material="water"}
        name:{forcefield="LAMMPS:SMD", material="water"}

        rho = 1000
        E = "5*${c0}^2*${rho}"
        nu = 0.3
        """

        # Use self.userid if filename is not provided
        if filename is None:
            filename = self.userid

        # Ensure the filename ends with '.txt'
        if not filename.endswith('.txt'):
            filename += '.txt'

        # Default folder to tempdir if foldername is not provided
        if foldername is None:
            foldername = tempfile.gettempdir()

        # Construct full path
        if not os.path.isabs(filename):
            filepath = os.path.join(foldername, filename)
        else:
            filepath = filename

        # Check if file already exists, raise exception if it does and overwrite is False
        if os.path.exists(filepath) and not overwrite:
            raise FileExistsError(f"The file '{filepath}' already exists.")

        # Header with current date, username, and host
        user = getpass.getuser()
        host = socket.gethostname()
        date = datetime.now().strftime('%Y-%m-%d')
        header = f"# DFORCEFIELD SAVE FILE\n# generated on {date} by {user}@{host}\n"
        header += f'#\tuserid = "{self.userid}"\n'
        header += f'#\tpath = "{filepath}"\n\n'

        # Open the file for writing
        with open(filepath, 'w') as f:
            # Write the header
            f.write(header)

            # Write beadtype, userid, and version with comments
            f.write("# Forcefield attributes\n")
            f.write(f'base_class="{self.base_class.__class__.__name__}"\n')
            f.write(f"beadtype = {self.beadtype}\n")
            f.write(f"userid = {self.userid}\n")
            f.write(f"version = {self.version}\n\n")

            # Write description with comment
            f.write("# Description of the forcefield\n")
            f.write("description:{")
            f.write(", ".join(f'{key}="{value}"' for key, value in self.description.items()))
            f.write("}\n\n")

            # Write name with comment
            f.write("# Name of the forcefield\n")
            f.write("name:{")
            f.write(", ".join(f'{key}="{value}"' for key, value in self.name.items()))
            f.write("}\n\n")

            # Write parameters with comment
            f.write("# Parameters for the forcefield\n")
            for key, value in self.parameters.items():
                f.write(f"{key} = {value}\n")

        # return filepath
        print(f"\nScript saved to {filepath}")

        return filepath



    @classmethod
    def load(cls, filename, foldername=None):
        """
        Load a dforcefield instance from a file with more control over the folder location and file validation.

        Args:
            filename (str): The name of the file to load. The '.txt' extension will be added if not present.
            foldername (str): The folder from which to load the file. Defaults to a temporary directory.

        Returns:
            dforcefield: A new dforcefield instance parsed from the file content.
        """
        # Step 0: Validate and construct filepath
        if not filename.endswith('.txt'):
            filename += '.txt'
        if foldername is None:
            foldername = tempfile.gettempdir()
        if not os.path.isabs(filename):
            filepath = os.path.join(foldername, filename)
        else:
            filepath = filename

        # Check if file exists
        if not os.path.exists(filepath):
            raise FileNotFoundError(f"The file '{filepath}' does not exist.")

        # Read the file contents
        with open(filepath, 'r') as f:
            content = f.read()

        # Extract the filename and name
        fname = os.path.basename(filepath)  # Extracts the filename (e.g., "forcefield.txt")
        name, _ = os.path.splitext(fname)   # Removes the extension, e.g., "forcefield"

        # Call the parse method to create a new dforcefield instance
        return cls.parse(content)


    @classmethod
    def parse(cls, content):
        """
        Parse the string content of a forcefield file to create a dforcefield instance.

        Parameters:
        -----------
        content : str
            The string content to be parsed.

        Returns:
        --------
        dforcefield
            A new `dforcefield` instance populated with the parsed content.

        Raises:
        -------
        ValueError
            If the content does not start with the correct magic line or if the format is invalid,
            or if base_class is not a valid subclass of forcefield.

        Notes:
        ------
        - Handles different sections including parameters, name, and description.
        - Accepts empty lines, comments, and the use of { } for attributes.
        """
        # Split the content into lines
        lines = content.splitlines()
        lines = [line for line in lines if line.strip()]  # Remove blank or empty lines
        # Raise an error if no content is left after removing blank lines
        if not lines:
            raise ValueError("File/Content is empty or only contains blank lines.")

        # Initialize containers for parsed data
        inside_global_params = False
        parameters = {}
        attributes = {}
        description = {}
        name = {}
        base_class = None

        # Step 1: Authenticate the file by checking the first line
        if not lines[0].strip().startswith("# DFORCEFIELD SAVE FILE"):
            raise ValueError("File/Content is not a valid DFORCEFIELD file.")

        # Step 2: Process each line dynamically
        for line in lines[1:]:
            stripped = line.strip()

            # Ignore empty lines and comments
            if not stripped or stripped.startswith("#"):
                continue

            # Remove trailing comments
            stripped = remove_comments(stripped)

            # Step 3: Detect base_class
            if stripped.startswith("base_class="):
                base_class_name = stripped.split("=", 1)[1].strip().strip('"')
                # Dynamically set the base class based on the string value (e.g., "tlsph")
                base_class = cls._load_base_class(base_class_name)
                if base_class is None or not issubclass(base_class, forcefield):
                    raise ValueError(f"Invalid base_class: '{base_class_name}' must be a subclass of forcefield.")
                continue

            # Step 4: Handle global parameters inside {...}
            if stripped.startswith("{"):
                inside_global_params = True
                global_params_content = stripped[stripped.index('{') + 1:].strip()

                if '}' in global_params_content:
                    global_params_content = global_params_content[:global_params_content.index('}')].strip()
                    inside_global_params = False
                    cls._parse_global_params(global_params_content.strip(), attributes)
                    global_params_content = ""  # Reset for the next block
                continue

            if inside_global_params:
                if stripped.endswith("}"):
                    global_params_content += " " + stripped[:stripped.index('}')].strip()
                    inside_global_params = False
                    cls._parse_global_params(global_params_content.strip(), attributes)
                    global_params_content = ""
                else:
                    global_params_content += " " + stripped
                continue

            # Step 5: Detect the start of name or description blocks
            if stripped.startswith("description:{"):
                desc_content = stripped.split("description:")[1].strip()[1:-1]
                cls._parse_struct_block(desc_content, description)
                continue

            if stripped.startswith("name:{"):
                name_content = stripped.split("name:")[1].strip()[1:-1]
                cls._parse_struct_block(name_content, name)
                continue

            # Step 6: Handle parameters in key=value format
            if "=" in stripped:
                key, value = stripped.split("=", 1)
                key = key.strip()
                value = value.strip().strip('"')
                parameters[key] = cls._convert_value(value)

        # Step 7: Validate base_class and create a new dforcefield instance
        if base_class is None:
            raise ValueError("base_class must be specified and valid in the forcefield file.")

        # Step 8: Create and return the new dforcefield instance
        newFF = cls(
            base_class=base_class,
            beadtype=int(attributes.get('beadtype', 1)),
            userid=attributes.get('userid', "unknown"),
            name=struct(**name),
            description=struct(**description),
            version=float(attributes.get('version', 0.1)),
            USER=parameterforcefield(**parameters)
        )

        # Step 9: detect variables in templates and propagates undefined variables
        dvars = newFF.detectVariables()
        newFF.parameters = dvars + newFF.parameters
        return newFF


    @classmethod
    def _parse_global_params(cls, content, global_params):
        """Parse global parameters from the accumulated content between {}."""
        lines = re.split(r',(?![^(){}\[\]]*[\)\}\]])', content)
        for line in lines:
            line = line.strip()
            match = re.match(r'([\w_]+)\s*=\s*(.+)', line)
            if match:
                key, value = match.groups()
                key = key.strip()
                value = value.strip()
                global_params[key] = cls._convert_value(value)



    @classmethod
    def _parse_struct_block(cls, content, struct_block):
        """
        Parse blocks like description or name with key=value pairs, handling commas inside quotes.

        Parameters:
        -----------
        content : str
            The content to parse, which contains key=value pairs.

        struct_block : dict
            A dictionary to store the parsed key-value pairs.
        """
        # Split on commas that are not inside quotes
        pairs = re.split(r',\s*(?![^"]*\"\s*,\s*[^"]*")', content)

        for pair in pairs:
            if '=' in pair:
                key, value = pair.split('=', 1)  # Ensure splitting only on the first '='
            struct_block[key.strip()] = value.strip().strip('"')



    @classmethod
    def _convert_value(cls, value):
        """Convert a string representation of a value to the appropriate Python type."""
        value = value.strip()

        # Boolean conversion
        if value.lower() == 'true':
            return True
        elif value.lower() == 'false':
            return False

        # Handle quoted strings
        if (value.startswith('"') and value.endswith('"')) or (value.startswith("'") and value.endswith("'")):
            return value[1:-1]

        # Handle lists
        if value.startswith('[') and value.endswith(']'):
            return eval(value)  # Using eval to parse lists safely in this controlled scenario

        # Handle numbers
        try:
            if '.' in value:
                return float(value)
            return int(value)
        except ValueError:
            return value


    def detectVariables(self):
        """
        Detects variables in the form ${variable} from the outputs of pair_style, pair_diagcoeff,
        and pair_offdiagcoeff.

        Returns:
        --------
        struct
            A struct containing the detected variable names that are not yet defined.
        """
        # Extract variables from pair_style, pair_diagcoeff, and pair_offdiagcoeff
        detected_vars = set()

        # Define a regex pattern to detect variables in the form ${variable}
        pattern = r'\$\{(\w+)\}'

        # Detect variables in pair_style output
        pair_style_output = self.pair_style(printflag=False, raw=True)
        detected_vars.update(re.findall(pattern, pair_style_output))

        # Detect variables in pair_diagcoeff output
        pair_diagcoeff_output = self.pair_diagcoeff(printflag=False, raw=True)
        detected_vars.update(re.findall(pattern, pair_diagcoeff_output))

        # Detect variables in pair_offdiagcoeff output
        pair_offdiagcoeff_output = self.pair_offdiagcoeff(printflag=False, raw=True)
        detected_vars.update(re.findall(pattern, pair_offdiagcoeff_output))

        # Convert the detected variables into a parameterforcefield() for further propagation
        return parameterforcefield(**{var: "${" + var + "}" for var in detected_vars})


    def missingVariables(self, isimplicit_missing=True, output_aslist=False):
        """
        List missing variables (undefined in parameters).

        Parameters:
        -----------
        isimplicit_missing : bool, optional (default: True)
            If True, variables defined implicitly as ${varname} in parameters are considered missing.

        output_aslist : bool, optional (default: False)
            If True, returns a list of missing variable names.
            If False, returns a parameterforcefield-like structure with implicit definitions.

        Returns:
        --------
        List of str or parameterforcefield
            If output_aslist is True, returns a list of variable names that are missing from the parameters.
            If output_aslist is False, returns a parameterforcefield-like structure with implicit definitions.
        """
        # Detect all variables used in the forcefield using detectVariables
        detected_vars = self.detectVariables()

        # Initialize missing variables
        missing_vars = []

        # Initialize a dictionary to store variables as parameterforcefield when output_aslist=False
        missing_vars_struct = {}

        # Iterate over detected variables and check if they are missing in parameters
        for varname in detected_vars.keys():
            if varname not in self.parameters:
                # If the variable is missing, add it to both the list and struct
                missing_vars.append(varname)
                missing_vars_struct[varname] = "${" + varname + "}"
            elif isimplicit_missing:
                # If the variable exists but is implicitly defined as ${varname}, treat it as missing
                param_value = self.parameters.getattr(varname)
                if param_value == "${" + varname + "}":
                    missing_vars.append(varname)
                    missing_vars_struct[varname] = "${" + varname + "}"

        # Return the result based on the output_aslist flag
        if output_aslist:
            return missing_vars
        else:
            return parameterforcefield(**missing_vars_struct)


    @classmethod
    def list_forcefield_subclasses(cls, printflag=True, additional_modules=None):
        """
        Class method to list all subclasses of the `forcefield` and `generic` classes, including their methods.

        Parameters:
        -----------
        printflag : bool, optional (default=True)
            If True, prints the subclasses of `forcefield`, `generic`, their load status, and their default parameters.
        additional_modules : module or list of modules, optional
            A module or list of additional modules to search for subclasses and methods.

        Returns:
        --------
        dict
            A dictionary where keys are class names or method names and values are dictionaries containing:
            - "loaded": Whether the class or method is already loaded in memory.
            - "module": The module path of the class or method.
            - "default_parameters": Extracted parameters, RULES, LOCAL, and GLOBAL from the class or method.
        dict
            A dictionary mapping short names to full names.
        """
        subclasses = set()
        classes_to_check = [forcefield, generic]  # Start with both forcefield and generic

        # Ensure additional_modules is a list, even if it's a single module
        if additional_modules is not None and not isinstance(additional_modules, list):
            additional_modules = [additional_modules]

        # Include classes from additional modules
        if additional_modules:
            for mod in additional_modules:
                for name, obj in inspect.getmembers(mod):
                    # Add classes that are subclasses of forcefield or generic
                    if inspect.isclass(obj) and (issubclass(obj, forcefield) or issubclass(obj, generic)) and obj not in {forcefield, generic}:
                        subclasses.add(obj)
                        classes_to_check.append(obj)
                    # Also add relevant high-level methods/functions that do not start with '_'
                    elif (inspect.isfunction(obj) or inspect.ismethod(obj)) and not name.startswith('_'):
                        # Check if the method belongs to a class derived from generic, not forcefield
                        parent_class_name = obj.__qualname__.split('.')[0]
                        parent_class = next((cls for cls in subclasses if cls.__name__ == parent_class_name), None)
                        if parent_class and issubclass(parent_class, generic) and not issubclass(parent_class, forcefield):
                            subclasses.add((parent_class, name, obj))  # Add (parent_class, method_name, function/method)

        # Traverse through all found subclasses
        while classes_to_check:
            parent_class = classes_to_check.pop()
            for subclass in parent_class.__subclasses__():
                subclasses.add(subclass)
                classes_to_check.append(subclass)

        # Prepare the output dictionary with load status flags and default parameters
        subclass_info = {}
        short_name_mapping = {}

        for subclass in subclasses:
            if inspect.isclass(subclass):
                class_name = subclass.__name__
                is_loaded = class_name in globals()  # Check if class is already loaded

                # Extract default parameters if they exist
                extracted_params = cls.extract_default_parameters(subclass, displayflag=False)
                num_defaults = len(extracted_params.get('parameters', {}).keys()) if extracted_params else None

                # Check if RULES, GLOBAL, and LOCAL are defined
                rules_defined = ''
                if extracted_params:
                    rules_defined += 'R' if extracted_params["RULES"] else '-'
                    rules_defined += 'G' if extracted_params["GLOBAL"] else '-'
                    rules_defined += 'L' if extracted_params["LOCAL"] else '-'

                # Add entry for the class
                subclass_info[class_name] = {
                    "loaded": is_loaded,
                    "module": subclass.__module__,
                    "num_defaults": num_defaults,
                    "rules_defined": rules_defined if rules_defined else '---'  # Display as '---' if none defined
                }

                # Map short name to full name
                short_name_mapping[class_name] = class_name

                # Inspect and add high-level methods from the class itself if not forcefield
                if not issubclass(subclass, forcefield):
                    for method_name, method in inspect.getmembers(subclass, predicate=inspect.isfunction):
                        if not method_name.startswith('_'):  # Skip private methods
                            method_full_name = f"{class_name}.{method_name}"
                            extracted_method_params = cls.extract_default_parameters(subclass, method_name=method_name, displayflag=False)
                            num_defaults = len(extracted_method_params.get('parameters', {}).keys()) if extracted_method_params else None
                            rules_defined = extracted_method_params.get("rules_defined", "---")
                            subclass_info[method_full_name] = {
                                "loaded": True,
                                "module": subclass.__module__,
                                "num_defaults": num_defaults,
                                "rules_defined": rules_defined
                            }
                            # Add method short name to mapping
                            short_name_mapping[method_name] = method_full_name

            else:
                # Handle function/method cases
                parent_class, func_name, func_obj = subclass
                is_loaded = func_name in globals() or parent_class.__module__ in sys.modules
                extracted_method_params = cls.extract_default_parameters(parent_class, method_name=func_name, displayflag=False)
                num_defaults = len(extracted_method_params.get('parameters', {}).keys()) if extracted_method_params else None
                rules_defined = extracted_method_params.get("rules_defined", "---")
                func_full_name = f"{parent_class.__name__}.{func_name}"
                subclass_info[func_full_name] = {
                    "loaded": is_loaded,
                    "module": parent_class.__module__,
                    "num_defaults": num_defaults,
                    "rules_defined": rules_defined
                }
                # Add function/method to short names
                short_name_mapping[func_name] = func_full_name

        # Optionally print the subclasses and their load status with parameter info
        if printflag:
            print("List of available forcefields and relevant methods:\n")
            print(f"{'Short Name':<15} {'Class/Method Name':<30} {'Module Path':<30} {'Status':<10} {'Default Params':<10} {'RULES':<5}")
            print("=" * 105)
            for short_name, class_name in sorted(short_name_mapping.items(), key=lambda x: x[0]):
                info = subclass_info[class_name]
                status = "Loaded" if info["loaded"] else "Not Loaded"
                num_defaults = info["num_defaults"] if info["num_defaults"] is not None else "None"
                rules_defined = info["rules_defined"]
                print(f"{short_name:<15} {class_name:<30} {info['module']:<30} {status:<10} {num_defaults:<10} {rules_defined:<5}")
            print(f"\nTotal number of forcefields and methods: {len(subclass_info)}")

        return subclass_info, short_name_mapping



    def scriptobject(self, beadtype=None, name=None, userid=None, fullname=None, filename=None, group=None, style=None, USER=scriptdata()):
        """
        Method to return a scriptobject based on the current dforcefield instance.

        Parameters:
        ------------
        beadtype : int, optional
            The bead type identifier. Defaults to the instance's beadtype.

        name : str, optional
            A short name for the scriptobject. If not provided, uses 'forcefield' from self.name.

        fullname : str, optional
            A comprehensive name for the object. Defaults to "beads of type {self.beadtype} | object of forcefield: {self.name.forcefield}".

        group : list, optional
            A group of scriptobjects that this object belongs to. Defaults to an empty list.

        style : str, optional
            The style of the scriptobject. Defaults to `self.description.style` if available, otherwise "smd".

        USER : scriptdata, optional
            User-defined data for additional parameters. Defaults to a blank `scriptdata()` instance.

        Returns:
        --------
        scriptobject
            A scriptobject instance populated with the current `dforcefield` instance's attributes or provided arguments.
        """
        # Set defaults using instance attributes if parameters are None
        if beadtype is None:
            beadtype = self.beadtype

        # Extract a meaningful name from the forcefield's name structure
        if name is None:
            if isinstance(self.name, struct) and hasattr(self.name, 'material'):
                name = f"{self.name.material} bead"
            else:
                name = f"beadtype_{beadtype}"

        if userid is None:
            userid = name

        if fullname is None:
            fullname = f"beads of type {beadtype} | object of forcefield: {self.name.forcefield if isinstance(self.name, struct) and hasattr(self.name, 'forcefield') else 'unknown'}"

        if group is None:
            group = []

        # Use `self.description.style` as the default for style if it exists, otherwise fall back to "smd"
        if style is None:
            if isinstance(self.description, struct) and hasattr(self.description, 'style'):
                style = self.description.style
            else:
                style = "undefined style"

        # The current dforcefield instance behaves as the forcefield for the scriptobject
        forcefield = copy.deepcopy(self)
        forcefield.beadtype = beadtype
        forcefield.name = name
        forcefield.userid = userid

        # Apply any user-defined parameters to the forcefield
        # This is the standard behavior of scriptobject
        forcefield.parameters = forcefield.parameters + USER

        # Create and return the scriptobject instance
        return scriptobject(
            beadtype=beadtype,
            name=name,
            fullname=fullname,
            filename=filename,
            style=style,
            forcefield=forcefield,  # Use the current dforcefield instance
            group=group,
            USER=USER
        )


    def __copy__(self):
        """
        Shallow copy method for dforcefield.

        Creates a shallow copy of the dforcefield instance, meaning that references
        to mutable objects like 'parameters' will not be deeply copied.
        """
        cls = self.__class__
        copie = cls.__new__(cls)

        # Copy the simple attributes
        copie.__dict__.update(self.__dict__)

        # Return the shallow copy
        return copie


    def __deepcopy__(self, memo):
        """
        Deep copy method for dforcefield.

        Creates a deep copy of the dforcefield instance, including its complex attributes
        like 'parameters'. This ensures that all mutable objects are fully copied, not just referenced.
        """
        cls = self.__class__
        copie = cls.__new__(cls)
        memo[id(self)] = copie

        # Recursively deep copy each attribute
        for k, v in self.__dict__.items():
            if k == "base_class":
                # The base_class should not be deeply copied; it's a class reference
                setattr(copie, k, self.base_class)
            else:
                # Deep copy other attributes
                setattr(copie, k, copy.deepcopy(v, memo))

        return copie # return copied instance

Class variables

var RULES

Static methods

def extract_default_parameters(base_class, method_name=None, displayflag=True)

Extract default parameters from the base class or its method by instantiating the class.

If the base class is derived from generic, this function will also extract RULES, LOCAL, and GLOBAL attributes if they are defined once the instance is created.

Parameters:

base_class : class The base class from which to extract default parameterforcefield values or a method. method_name : str, optional The name of the method to call on the instance, if applicable (e.g., newtonianfluid). displayflag : bool, optional (default=True) If True, prints a message when no default parameters are found.

Returns:

dict A dictionary containing the default parameters extracted from the base class or method. Also includes RULES, LOCAL, and GLOBAL if applicable.

Expand source code
@classmethod
def extract_default_parameters(cls, base_class, method_name=None, displayflag=True):
    """
    Extract default parameters from the base class or its method by instantiating the class.

    If the base class is derived from `generic`, this function will also extract RULES, LOCAL,
    and GLOBAL attributes if they are defined once the instance is created.

    Parameters:
    -----------
    base_class : class
        The base class from which to extract default parameterforcefield values or a method.
    method_name : str, optional
        The name of the method to call on the instance, if applicable (e.g., `newtonianfluid`).
    displayflag : bool, optional (default=True)
        If True, prints a message when no default parameters are found.

    Returns:
    --------
    dict
        A dictionary containing the default parameters extracted from the base class or method.
        Also includes RULES, LOCAL, and GLOBAL if applicable.
    """
    extracted_info = {
        "parameters": parameterforcefield(),
        "RULES": genericdata(),
        "GLOBAL": genericdata(),
        "LOCAL": genericdata()
    }

    try:
        # Create an instance of the base class
        instance = base_class()

        # Extract parameters if the instance has them
        if hasattr(instance, 'parameters') and isinstance(instance.parameters, parameterforcefield):
            extracted_info["parameters"] = instance.parameters

        # Extract RULES, GLOBAL, and LOCAL if they exist
        for attr in ["RULES", "GLOBAL", "LOCAL"]:
            if hasattr(instance, attr) and isinstance(getattr(instance, attr), parameterforcefield):
                extracted_info[attr] = getattr(instance, attr)

        # If a method is specified, call it to get additional parameters
        if method_name and hasattr(instance, method_name):
            method = getattr(instance, method_name)
            if callable(method):
                try:
                    result = method()  # Call the method (ensure it returns the desired forcefield object)
                    if hasattr(result, 'parameters') and isinstance(result.parameters, parameterforcefield):
                        extracted_info["parameters"] = result.parameters
                except Exception as e:
                    if displayflag:
                        print(f"Error calling method {method_name} on {base_class.__name__}: {str(e)}")

    except TypeError as e:
        if displayflag:
            print(f"Could not instantiate {base_class.__name__}: {str(e)}")

    # If no parameters found or cannot instantiate the class, return None
    if displayflag and not extracted_info["parameters"]:
        print(f"No default parameters found in {base_class.__name__} or its ancestors.")

    return extracted_info
def from_dict(data)

Create a dforcefield instance from a dictionary, including RULES, GLOBAL, and LOCAL.

Args

data : dict
A dictionary containing the attributes to initialize the dforcefield.

Returns

dforcefield
A new dforcefield instance.

Example

config = { "name": "water_ff", "description": "water forcefield", "beadtype": 2, "userid": "water_sim", "version": 1.0, "parameters": {"rho": 1000, "sigma": 0.5}, "base_class": "ulsph", "RULES": {"some_rule": "value"}, "GLOBAL": {"global_param": 10}, "LOCAL": {"local_param": 5} }

new_ff = dforcefield.from_dict(config)

Expand source code
@classmethod
def from_dict(cls, data):
    """
    Create a dforcefield instance from a dictionary, including RULES, GLOBAL, and LOCAL.

    Args:
        data (dict): A dictionary containing the attributes to initialize the dforcefield.

    Returns:
        dforcefield: A new dforcefield instance.

    Example:
        config = {
            "name": "water_ff",
            "description": "water forcefield",
            "beadtype": 2,
            "userid": "water_sim",
            "version": 1.0,
            "parameters": {"rho": 1000, "sigma": 0.5},
            "base_class": "ulsph",
            "RULES": {"some_rule": "value"},
            "GLOBAL": {"global_param": 10},
            "LOCAL": {"local_param": 5}
        }

        new_ff = dforcefield.from_dict(config)
    """
    # Extract base class information
    base_class_name = data.pop("base_class", None)
    base_class = globals().get(base_class_name) if base_class_name else None

    # Extract RULES, GLOBAL, and LOCAL from the data dictionary if they exist
    rules = data.pop("RULES", parameterforcefield())
    global_params = data.pop("GLOBAL", parameterforcefield())
    local_params = data.pop("LOCAL", parameterforcefield())

    # Create and return the new dforcefield instance with extracted or default RULES, GLOBAL, and LOCAL
    return cls(
        base_class=base_class,
        RULES=rules,
        GLOBAL=global_params,
        LOCAL=local_params,
        **data
    )
def list_forcefield_subclasses(printflag=True, additional_modules=None)

Class method to list all subclasses of the forcefield and generic classes, including their methods.

Parameters:

printflag : bool, optional (default=True) If True, prints the subclasses of forcefield, generic, their load status, and their default parameters. additional_modules : module or list of modules, optional A module or list of additional modules to search for subclasses and methods.

Returns:

dict A dictionary where keys are class names or method names and values are dictionaries containing: - "loaded": Whether the class or method is already loaded in memory. - "module": The module path of the class or method. - "default_parameters": Extracted parameters, RULES, LOCAL, and GLOBAL from the class or method. dict A dictionary mapping short names to full names.

Expand source code
@classmethod
def list_forcefield_subclasses(cls, printflag=True, additional_modules=None):
    """
    Class method to list all subclasses of the `forcefield` and `generic` classes, including their methods.

    Parameters:
    -----------
    printflag : bool, optional (default=True)
        If True, prints the subclasses of `forcefield`, `generic`, their load status, and their default parameters.
    additional_modules : module or list of modules, optional
        A module or list of additional modules to search for subclasses and methods.

    Returns:
    --------
    dict
        A dictionary where keys are class names or method names and values are dictionaries containing:
        - "loaded": Whether the class or method is already loaded in memory.
        - "module": The module path of the class or method.
        - "default_parameters": Extracted parameters, RULES, LOCAL, and GLOBAL from the class or method.
    dict
        A dictionary mapping short names to full names.
    """
    subclasses = set()
    classes_to_check = [forcefield, generic]  # Start with both forcefield and generic

    # Ensure additional_modules is a list, even if it's a single module
    if additional_modules is not None and not isinstance(additional_modules, list):
        additional_modules = [additional_modules]

    # Include classes from additional modules
    if additional_modules:
        for mod in additional_modules:
            for name, obj in inspect.getmembers(mod):
                # Add classes that are subclasses of forcefield or generic
                if inspect.isclass(obj) and (issubclass(obj, forcefield) or issubclass(obj, generic)) and obj not in {forcefield, generic}:
                    subclasses.add(obj)
                    classes_to_check.append(obj)
                # Also add relevant high-level methods/functions that do not start with '_'
                elif (inspect.isfunction(obj) or inspect.ismethod(obj)) and not name.startswith('_'):
                    # Check if the method belongs to a class derived from generic, not forcefield
                    parent_class_name = obj.__qualname__.split('.')[0]
                    parent_class = next((cls for cls in subclasses if cls.__name__ == parent_class_name), None)
                    if parent_class and issubclass(parent_class, generic) and not issubclass(parent_class, forcefield):
                        subclasses.add((parent_class, name, obj))  # Add (parent_class, method_name, function/method)

    # Traverse through all found subclasses
    while classes_to_check:
        parent_class = classes_to_check.pop()
        for subclass in parent_class.__subclasses__():
            subclasses.add(subclass)
            classes_to_check.append(subclass)

    # Prepare the output dictionary with load status flags and default parameters
    subclass_info = {}
    short_name_mapping = {}

    for subclass in subclasses:
        if inspect.isclass(subclass):
            class_name = subclass.__name__
            is_loaded = class_name in globals()  # Check if class is already loaded

            # Extract default parameters if they exist
            extracted_params = cls.extract_default_parameters(subclass, displayflag=False)
            num_defaults = len(extracted_params.get('parameters', {}).keys()) if extracted_params else None

            # Check if RULES, GLOBAL, and LOCAL are defined
            rules_defined = ''
            if extracted_params:
                rules_defined += 'R' if extracted_params["RULES"] else '-'
                rules_defined += 'G' if extracted_params["GLOBAL"] else '-'
                rules_defined += 'L' if extracted_params["LOCAL"] else '-'

            # Add entry for the class
            subclass_info[class_name] = {
                "loaded": is_loaded,
                "module": subclass.__module__,
                "num_defaults": num_defaults,
                "rules_defined": rules_defined if rules_defined else '---'  # Display as '---' if none defined
            }

            # Map short name to full name
            short_name_mapping[class_name] = class_name

            # Inspect and add high-level methods from the class itself if not forcefield
            if not issubclass(subclass, forcefield):
                for method_name, method in inspect.getmembers(subclass, predicate=inspect.isfunction):
                    if not method_name.startswith('_'):  # Skip private methods
                        method_full_name = f"{class_name}.{method_name}"
                        extracted_method_params = cls.extract_default_parameters(subclass, method_name=method_name, displayflag=False)
                        num_defaults = len(extracted_method_params.get('parameters', {}).keys()) if extracted_method_params else None
                        rules_defined = extracted_method_params.get("rules_defined", "---")
                        subclass_info[method_full_name] = {
                            "loaded": True,
                            "module": subclass.__module__,
                            "num_defaults": num_defaults,
                            "rules_defined": rules_defined
                        }
                        # Add method short name to mapping
                        short_name_mapping[method_name] = method_full_name

        else:
            # Handle function/method cases
            parent_class, func_name, func_obj = subclass
            is_loaded = func_name in globals() or parent_class.__module__ in sys.modules
            extracted_method_params = cls.extract_default_parameters(parent_class, method_name=func_name, displayflag=False)
            num_defaults = len(extracted_method_params.get('parameters', {}).keys()) if extracted_method_params else None
            rules_defined = extracted_method_params.get("rules_defined", "---")
            func_full_name = f"{parent_class.__name__}.{func_name}"
            subclass_info[func_full_name] = {
                "loaded": is_loaded,
                "module": parent_class.__module__,
                "num_defaults": num_defaults,
                "rules_defined": rules_defined
            }
            # Add function/method to short names
            short_name_mapping[func_name] = func_full_name

    # Optionally print the subclasses and their load status with parameter info
    if printflag:
        print("List of available forcefields and relevant methods:\n")
        print(f"{'Short Name':<15} {'Class/Method Name':<30} {'Module Path':<30} {'Status':<10} {'Default Params':<10} {'RULES':<5}")
        print("=" * 105)
        for short_name, class_name in sorted(short_name_mapping.items(), key=lambda x: x[0]):
            info = subclass_info[class_name]
            status = "Loaded" if info["loaded"] else "Not Loaded"
            num_defaults = info["num_defaults"] if info["num_defaults"] is not None else "None"
            rules_defined = info["rules_defined"]
            print(f"{short_name:<15} {class_name:<30} {info['module']:<30} {status:<10} {num_defaults:<10} {rules_defined:<5}")
        print(f"\nTotal number of forcefields and methods: {len(subclass_info)}")

    return subclass_info, short_name_mapping
def load(filename, foldername=None)

Load a dforcefield instance from a file with more control over the folder location and file validation.

Args

filename : str
The name of the file to load. The '.txt' extension will be added if not present.
foldername : str
The folder from which to load the file. Defaults to a temporary directory.

Returns

dforcefield
A new dforcefield instance parsed from the file content.
Expand source code
@classmethod
def load(cls, filename, foldername=None):
    """
    Load a dforcefield instance from a file with more control over the folder location and file validation.

    Args:
        filename (str): The name of the file to load. The '.txt' extension will be added if not present.
        foldername (str): The folder from which to load the file. Defaults to a temporary directory.

    Returns:
        dforcefield: A new dforcefield instance parsed from the file content.
    """
    # Step 0: Validate and construct filepath
    if not filename.endswith('.txt'):
        filename += '.txt'
    if foldername is None:
        foldername = tempfile.gettempdir()
    if not os.path.isabs(filename):
        filepath = os.path.join(foldername, filename)
    else:
        filepath = filename

    # Check if file exists
    if not os.path.exists(filepath):
        raise FileNotFoundError(f"The file '{filepath}' does not exist.")

    # Read the file contents
    with open(filepath, 'r') as f:
        content = f.read()

    # Extract the filename and name
    fname = os.path.basename(filepath)  # Extracts the filename (e.g., "forcefield.txt")
    name, _ = os.path.splitext(fname)   # Removes the extension, e.g., "forcefield"

    # Call the parse method to create a new dforcefield instance
    return cls.parse(content)
def parse(content)

Parse the string content of a forcefield file to create a dforcefield instance.

Parameters:

content : str The string content to be parsed.

Returns:

dforcefield A new dforcefield instance populated with the parsed content.

Raises:

ValueError If the content does not start with the correct magic line or if the format is invalid, or if base_class is not a valid subclass of forcefield.

Notes:

  • Handles different sections including parameters, name, and description.
  • Accepts empty lines, comments, and the use of { } for attributes.
Expand source code
@classmethod
def parse(cls, content):
    """
    Parse the string content of a forcefield file to create a dforcefield instance.

    Parameters:
    -----------
    content : str
        The string content to be parsed.

    Returns:
    --------
    dforcefield
        A new `dforcefield` instance populated with the parsed content.

    Raises:
    -------
    ValueError
        If the content does not start with the correct magic line or if the format is invalid,
        or if base_class is not a valid subclass of forcefield.

    Notes:
    ------
    - Handles different sections including parameters, name, and description.
    - Accepts empty lines, comments, and the use of { } for attributes.
    """
    # Split the content into lines
    lines = content.splitlines()
    lines = [line for line in lines if line.strip()]  # Remove blank or empty lines
    # Raise an error if no content is left after removing blank lines
    if not lines:
        raise ValueError("File/Content is empty or only contains blank lines.")

    # Initialize containers for parsed data
    inside_global_params = False
    parameters = {}
    attributes = {}
    description = {}
    name = {}
    base_class = None

    # Step 1: Authenticate the file by checking the first line
    if not lines[0].strip().startswith("# DFORCEFIELD SAVE FILE"):
        raise ValueError("File/Content is not a valid DFORCEFIELD file.")

    # Step 2: Process each line dynamically
    for line in lines[1:]:
        stripped = line.strip()

        # Ignore empty lines and comments
        if not stripped or stripped.startswith("#"):
            continue

        # Remove trailing comments
        stripped = remove_comments(stripped)

        # Step 3: Detect base_class
        if stripped.startswith("base_class="):
            base_class_name = stripped.split("=", 1)[1].strip().strip('"')
            # Dynamically set the base class based on the string value (e.g., "tlsph")
            base_class = cls._load_base_class(base_class_name)
            if base_class is None or not issubclass(base_class, forcefield):
                raise ValueError(f"Invalid base_class: '{base_class_name}' must be a subclass of forcefield.")
            continue

        # Step 4: Handle global parameters inside {...}
        if stripped.startswith("{"):
            inside_global_params = True
            global_params_content = stripped[stripped.index('{') + 1:].strip()

            if '}' in global_params_content:
                global_params_content = global_params_content[:global_params_content.index('}')].strip()
                inside_global_params = False
                cls._parse_global_params(global_params_content.strip(), attributes)
                global_params_content = ""  # Reset for the next block
            continue

        if inside_global_params:
            if stripped.endswith("}"):
                global_params_content += " " + stripped[:stripped.index('}')].strip()
                inside_global_params = False
                cls._parse_global_params(global_params_content.strip(), attributes)
                global_params_content = ""
            else:
                global_params_content += " " + stripped
            continue

        # Step 5: Detect the start of name or description blocks
        if stripped.startswith("description:{"):
            desc_content = stripped.split("description:")[1].strip()[1:-1]
            cls._parse_struct_block(desc_content, description)
            continue

        if stripped.startswith("name:{"):
            name_content = stripped.split("name:")[1].strip()[1:-1]
            cls._parse_struct_block(name_content, name)
            continue

        # Step 6: Handle parameters in key=value format
        if "=" in stripped:
            key, value = stripped.split("=", 1)
            key = key.strip()
            value = value.strip().strip('"')
            parameters[key] = cls._convert_value(value)

    # Step 7: Validate base_class and create a new dforcefield instance
    if base_class is None:
        raise ValueError("base_class must be specified and valid in the forcefield file.")

    # Step 8: Create and return the new dforcefield instance
    newFF = cls(
        base_class=base_class,
        beadtype=int(attributes.get('beadtype', 1)),
        userid=attributes.get('userid', "unknown"),
        name=struct(**name),
        description=struct(**description),
        version=float(attributes.get('version', 0.1)),
        USER=parameterforcefield(**parameters)
    )

    # Step 9: detect variables in templates and propagates undefined variables
    dvars = newFF.detectVariables()
    newFF.parameters = dvars + newFF.parameters
    return newFF

Instance variables

var merged_name_description

Return a struct containing the merged content of 'name' and 'description'. If an attribute exists in both, their values are combined into a list.

Expand source code
@property
def merged_name_description(self):
    """
    Return a struct containing the merged content of 'name' and 'description'.
    If an attribute exists in both, their values are combined into a list.
    """
    merged_data = {}

    # Add attributes from 'name' if it's a struct
    if isinstance(self.name, struct):
        merged_data.update(self.name.__dict__)

    # Add/merge attributes from 'description' if it's a struct
    if isinstance(self.description, struct):
        for key, value in self.description.__dict__.items():
            if key in merged_data:
                # Combine the values into a list if the key already exists
                existing_value = merged_data[key]
                if not isinstance(existing_value, list):
                    existing_value = [existing_value]  # Convert to list if not already a list
                merged_data[key] = existing_value + [value]  # Add the new value to the list
            else:
                merged_data[key] = value

    # Return the merged struct
    return struct(**merged_data)
var script

Return the raw content of the pair_* outputs combined into a single script-like format.

Returns:

str The raw outputs from pair_style, pair_diagcoeff, and pair_offdiagcoeff concatenated into a script.

Expand source code
@property
def script(self):
    """
    Return the raw content of the pair_* outputs combined into a single script-like format.

    Returns:
    --------
    str
        The raw outputs from pair_style, pair_diagcoeff, and pair_offdiagcoeff concatenated into a script.
    """
    # Get the raw outputs from the pair_* methods
    pair_style_output = self.pair_style(printflag=False, raw=True)
    pair_diagcoeff_output = self.pair_diagcoeff(printflag=False, raw=True)
    pair_offdiagcoeff_output = self.pair_offdiagcoeff(printflag=False, raw=True)

    # Combine the outputs into a script-like format
    script_content = "\n".join([
        "# Script output from dforcefield:",
        "# Pair style:",
        pair_style_output,
        "\n# Diagonal coefficients:",
        pair_diagcoeff_output,
        "\n# Off-diagonal coefficients:",
        pair_offdiagcoeff_output
    ])

    return script_content

Methods

def base_repr(self)

Returns the representation of the base_class.

Expand source code
def base_repr(self):
    """Returns the representation of the base_class."""
    self.base_class.__repr__()
    return self.base_class.__str__()
def combine_parameters(self)

Combine GLOBAL, LOCAL, and RULES to get the current parameter configuration.

Expand source code
def combine_parameters(self):
    """
    Combine GLOBAL, LOCAL, and RULES to get the current parameter configuration.
    """
    return self.GLOBAL + self.LOCAL + self.RULES
def compare(self, other, printflag=False)

Compare the current instance with another dforcefield instance, including RULES, GLOBAL, and LOCAL.

Args:

other : dforcefield The other instance to compare.

printflag : bool, optional (default: False) If True, prints a table showing all parameters, with differences marked by '*'.

Returns:

dict A dictionary showing differences between the two instances.

Raises:

TypeError: If the other object is not a dforcefield instance.

Example:

diffs = dynamic_water.compare(dynamic_salt) if printflag: Prints a comparison table with a type column and a legend at the end.

Expand source code
def compare(self, other, printflag=False):
    """
    Compare the current instance with another dforcefield instance, including RULES, GLOBAL, and LOCAL.

    Args:
    -----
    other : dforcefield
        The other instance to compare.

    printflag : bool, optional (default: False)
        If True, prints a table showing all parameters, with differences marked by '*'.

    Returns:
    --------
    dict
        A dictionary showing differences between the two instances.

    Raises:
    -------
    TypeError: If the other object is not a dforcefield instance.

    Example:
    --------
    diffs = dynamic_water.compare(dynamic_salt)
    if printflag:
        Prints a comparison table with a type column and a legend at the end.
    """
    # Define abbreviations for types
    type_abbreviations = {
        str: 'STR',
        int: 'INT',
        float: 'FLT',
        bool: 'BOOL',
        list: 'LST',
        tuple: 'TPL',
        dict: 'DCT',
        None: 'NON',
        'missing': 'MIS'
    }

    def get_type_abbrev(value):
        """Return the abbreviation for the type of the value."""
        if value is None:
            return type_abbreviations[None]
        return type_abbreviations.get(type(value), 'UNK')  # Use 'UNK' for unknown types

    def format_simple_type(value):
        """Format a simple value (str, number, or bool) for display."""
        return self.dispmax(str(value))

    def format_iterable(value_self, value_other, key, comparison_table, difference):
        """Handle lists, tuples, and other iterable types."""
        max_length = max(len(value_self), len(value_other))
        for i in range(max_length):
            item_self = value_self[i] if i < len(value_self) else 'MISSING'
            item_other = value_other[i] if i < len(value_other) else 'MISSING'
            sub_difference = '*' if item_self != item_other else ' '
            comparison_table.append(
                f"  {key if i == 0 else '':<15}: {sub_difference:^3}: {self.dispmax(str(item_self)):<30}: {get_type_abbrev(item_self):<5}: {self.dispmax(str(item_other)):<30}: {get_type_abbrev(item_other):<5}"
            )

    def format_dict_like(value_self, value_other, key, comparison_table, difference):
        """Handle dictionary-like objects (e.g., structs or dicts)."""
        comparison_table.append(f"  {key:<15}: {difference:^3}: ")
        comparison_table.append(f"  {'':<15}  {'Self':<30} {'Type':<5} {'Other':<30} {'Type':<5}")
        sub_sep = "  "+"-"*15+":"+"-"*3+":"+"-"*30+":"+"-"*5+":"+"-"*30+":"+"-"*5
        comparison_table.append(sub_sep)
        sub_keys = set(value_self.keys()).union(value_other.keys())
        for sub_key in sorted(sub_keys):
            sub_value_self = value_self.get(sub_key, 'MISSING')
            sub_value_other = value_other.get(sub_key, 'MISSING')
            sub_difference = '*' if sub_value_self != sub_value_other else ' '
            comparison_table.append(
                f"  {sub_key:<15}: {sub_difference:^3}: {self.dispmax(str(sub_value_self)):<30}: {get_type_abbrev(sub_value_self):<5}: {self.dispmax(str(sub_value_other)):<30}: {get_type_abbrev(sub_value_other):<5}"
            )
        comparison_table.append(sub_sep)

    def format_complex_type(value_self, value_other, key, comparison_table, difference):
        """Handle complex types, expanding them into multiple lines."""
        value_self_lines = str(value_self).splitlines()
        value_other_lines = str(value_other).splitlines()
        max_lines = max(len(value_self_lines), len(value_other_lines))
        for i in range(max_lines):
            line_self = value_self_lines[i] if i < len(value_self_lines) else 'MISSING'
            line_other = value_other_lines[i] if i < len(value_other_lines) else 'MISSING'
            comparison_table.append(
                f"  {key if i == 0 else '':<15}: {difference if i == 0 else ' ':^3}: {self.dispmax(line_self):<30}: {get_type_abbrev(line_self):<5}: {self.dispmax(line_other):<30}: {get_type_abbrev(line_other):<5}"
            )

    if not isinstance(other, dforcefield):
        raise TypeError("Can only compare with another dforcefield instance.")

    diffs = {}

    # Collect all keys from both instances, including RULES, GLOBAL, and LOCAL
    all_keys = set(self.keys()).union(other.keys(), {'RULES', 'GLOBAL', 'LOCAL'})

    # Iterate over all keys and compare the values
    for key in all_keys:
        value_self = getattr(self, key, None)
        value_other = getattr(other, key, None)

        if value_self != value_other:
            diffs[key] = {"self": value_self, "other": value_other}

    # If printflag is True, print the comparison in a table format
    if printflag:
        sep = "  "+"-"*15 + ":" + "-"*3 + ":" + "-"*30 + ":" + "-"*5 + ":" + "-"*30 + ":" + "-"*5
        header = f"\n  {'Attribute':<15}: {'*':^3}: {'Self':<30}: {'Type':<5}: {'Other':<30}: {'Type':<5}\n" + sep
        comparison_table = [header]

        # Iterate over all parameters and mark differences with '*'
        for key in sorted(all_keys):  # Sort keys for a cleaner output
            value_self = getattr(self, key, 'MISSING')
            value_other = getattr(other, key, 'MISSING')
            difference = '*' if value_self != value_other else ' '

            # Check if the value is a simple type (str, number, or bool)
            if isinstance(value_self, (str, int, float, bool)) and isinstance(value_other, (str, int, float, bool)):
                comparison_table.append(
                    f"  {key:<15}: {difference:^3}: {format_simple_type(value_self):<30}: {get_type_abbrev(value_self):<5}: {format_simple_type(value_other):<30}: {get_type_abbrev(value_other):<5}"
                )
            # Check if the value is a list or tuple
            elif isinstance(value_self, (list, tuple)) and isinstance(value_other, (list, tuple)):
                format_iterable(value_self, value_other, key, comparison_table, difference)
            # Check if the value is a dictionary-like object
            elif hasattr(value_self, 'keys') and hasattr(value_other, 'keys'):
                format_dict_like(value_self, value_other, key, comparison_table, difference)
            else:
                # Handle other complex types
                format_complex_type(value_self, value_other, key, comparison_table, difference)

        comparison_table.append(sep)
        # Add a legend for type abbreviations
        legend = "\nType Legend: " + ", ".join(f"{abbrev} = {('NoneType' if type_ is None else type_.__name__.upper())}" for type_, abbrev in type_abbreviations.items() if type_ != 'missing')

        comparison_table.append(legend)

        # Print the comparison table
        print("\n".join(comparison_table))

    return diffs
def copy(self, beadtype=None, userid=None, name=None, description=None, version=None, USER=forcefield (FF object) with 0 parameters, RULES=None, GLOBAL=None, LOCAL=None, **kwargs)

Create a new instance of dforcefield with the option to override key attributes including RULES, GLOBAL, and LOCAL.

Expand source code
def copy(self, beadtype=None, userid=None, name=None, description=None, version=None, USER=parameterforcefield(), RULES=None, GLOBAL=None, LOCAL=None, **kwargs):
    """
    Create a new instance of dforcefield with the option to override key attributes including RULES, GLOBAL, and LOCAL.
    """
    # Use the current instance's values as defaults, and override if values are provided
    new_beadtype = beadtype if beadtype is not None else self.beadtype
    new_userid = userid if userid is not None else self.userid
    new_name = name if name is not None else self.name
    new_description = description if description is not None else self.description
    new_version = version if version is not None else self.version
    if new_userid == self.userid:
        new_userid = new_userid + " (copy)"
    # USER is computed by combining self.parameters + USER + parameterforcefield(**kwargs)
    new_parameters = self.parameters + USER

    # Combine or override RULES, GLOBAL, and LOCAL if provided
    new_rules = RULES if RULES is not None else self.RULES
    new_global = GLOBAL if GLOBAL is not None else self.GLOBAL
    new_local = LOCAL if LOCAL is not None else self.LOCAL

    # Create and return the new instance with overridden values
    return self.__class__(
        base_class=self.base_class.__class__,
        beadtype=new_beadtype,
        userid=new_userid,
        name=new_name,
        description=new_description,
        version=new_version,
        USER=new_parameters,
        RULES=new_rules,
        GLOBAL=new_global,
        LOCAL=new_local,
        **kwargs
    )
def detectVariables(self)

Detects variables in the form ${variable} from the outputs of pair_style, pair_diagcoeff, and pair_offdiagcoeff.

Returns:

struct A struct containing the detected variable names that are not yet defined.

Expand source code
def detectVariables(self):
    """
    Detects variables in the form ${variable} from the outputs of pair_style, pair_diagcoeff,
    and pair_offdiagcoeff.

    Returns:
    --------
    struct
        A struct containing the detected variable names that are not yet defined.
    """
    # Extract variables from pair_style, pair_diagcoeff, and pair_offdiagcoeff
    detected_vars = set()

    # Define a regex pattern to detect variables in the form ${variable}
    pattern = r'\$\{(\w+)\}'

    # Detect variables in pair_style output
    pair_style_output = self.pair_style(printflag=False, raw=True)
    detected_vars.update(re.findall(pattern, pair_style_output))

    # Detect variables in pair_diagcoeff output
    pair_diagcoeff_output = self.pair_diagcoeff(printflag=False, raw=True)
    detected_vars.update(re.findall(pattern, pair_diagcoeff_output))

    # Detect variables in pair_offdiagcoeff output
    pair_offdiagcoeff_output = self.pair_offdiagcoeff(printflag=False, raw=True)
    detected_vars.update(re.findall(pattern, pair_offdiagcoeff_output))

    # Convert the detected variables into a parameterforcefield() for further propagation
    return parameterforcefield(**{var: "${" + var + "}" for var in detected_vars})
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 get_global(self)

Return the GLOBAL parameters for this dforcefield instance.

Expand source code
def get_global(self):
    """Return the GLOBAL parameters for this dforcefield instance."""
    return self.GLOBAL
def get_local(self)

Return the LOCAL parameters for this dforcefield instance.

Expand source code
def get_local(self):
    """Return the LOCAL parameters for this dforcefield instance."""
    return self.LOCAL
def get_rules(self)

Return the RULES parameters for this dforcefield instance.

Expand source code
def get_rules(self):
    """Return the RULES parameters for this dforcefield instance."""
    return self.RULES
def items(self)

Return an iterator over (key, value) pairs from the merged struct, parameters, and scalar attributes.

Expand source code
def items(self):
    """
    Return an iterator over (key, value) pairs from the merged struct, parameters, and scalar attributes.
    """
    # Yield items from merged struct
    for item in self.merged_name_description.__dict__.items():
        yield item

    # Yield items from parameters
    for item in self.parameters.items():
        yield item

    # Yield scalar attribute items
    yield ('version', self.version)
    yield ('userid', self.userid)
    yield ('beadtype', self.beadtype)
def keys(self)

Return the keys of the merged struct, parameters, and scalar attributes.

Expand source code
def keys(self):
    """
    Return the keys of the merged struct, parameters, and scalar attributes.
    """
    keys_set = set(self.merged_name_description.__dict__.keys())
    keys_set.update(self.parameters.keys())
    keys_set.update(['version', 'userid', 'beadtype'])
    return list(keys_set)
def missingVariables(self, isimplicit_missing=True, output_aslist=False)

List missing variables (undefined in parameters).

Parameters:

isimplicit_missing : bool, optional (default: True) If True, variables defined implicitly as ${varname} in parameters are considered missing.

output_aslist : bool, optional (default: False) If True, returns a list of missing variable names. If False, returns a parameterforcefield-like structure with implicit definitions.

Returns:

List of str or parameterforcefield If output_aslist is True, returns a list of variable names that are missing from the parameters. If output_aslist is False, returns a parameterforcefield-like structure with implicit definitions.

Expand source code
def missingVariables(self, isimplicit_missing=True, output_aslist=False):
    """
    List missing variables (undefined in parameters).

    Parameters:
    -----------
    isimplicit_missing : bool, optional (default: True)
        If True, variables defined implicitly as ${varname} in parameters are considered missing.

    output_aslist : bool, optional (default: False)
        If True, returns a list of missing variable names.
        If False, returns a parameterforcefield-like structure with implicit definitions.

    Returns:
    --------
    List of str or parameterforcefield
        If output_aslist is True, returns a list of variable names that are missing from the parameters.
        If output_aslist is False, returns a parameterforcefield-like structure with implicit definitions.
    """
    # Detect all variables used in the forcefield using detectVariables
    detected_vars = self.detectVariables()

    # Initialize missing variables
    missing_vars = []

    # Initialize a dictionary to store variables as parameterforcefield when output_aslist=False
    missing_vars_struct = {}

    # Iterate over detected variables and check if they are missing in parameters
    for varname in detected_vars.keys():
        if varname not in self.parameters:
            # If the variable is missing, add it to both the list and struct
            missing_vars.append(varname)
            missing_vars_struct[varname] = "${" + varname + "}"
        elif isimplicit_missing:
            # If the variable exists but is implicitly defined as ${varname}, treat it as missing
            param_value = self.parameters.getattr(varname)
            if param_value == "${" + varname + "}":
                missing_vars.append(varname)
                missing_vars_struct[varname] = "${" + varname + "}"

    # Return the result based on the output_aslist flag
    if output_aslist:
        return missing_vars
    else:
        return parameterforcefield(**missing_vars_struct)
def pair_diagcoeff(self, printflag=None, verbose=None, i=None, raw=False)

Delegate pair_diagcoeff to the base class, ensuring it uses the correct attributes.

Expand source code
def pair_diagcoeff(self, printflag=None, verbose=None, i=None, raw=False):
    """Delegate pair_diagcoeff to the base class, ensuring it uses the correct attributes."""
    printflag = self.printflag if printflag is None else printflag
    verbose = self.verbose if verbose is None else verbose
    self._inject_attributes()
    return self.base_class.pair_diagcoeff(printflag=printflag, verbose=verbose, i=i, raw=raw, USER=None, beadtype=self.beadtype, userid=self.userid)
def pair_offdiagcoeff(self, o=None, printflag=None, verbose=None, i=None, raw=False, oname=None)

Delegate pair_offdiagcoeff to the base class, ensuring it uses the correct attributes.

Expand source code
def pair_offdiagcoeff(self, o=None, printflag=None, verbose=None, i=None, raw=False,oname=None):
    """Delegate pair_offdiagcoeff to the base class, ensuring it uses the correct attributes."""
    printflag = self.printflag if printflag is None else printflag
    verbose = self.verbose if verbose is None else verbose
    self._inject_attributes()
    return self.base_class.pair_offdiagcoeff(o=o, printflag=printflag, verbose=verbose, i=i, raw=raw, USER=None, beadtype=self.beadtype, userid=self.userid,oname=oname)
def pair_style(self, printflag=None, verbose=None, raw=False)

Delegate pair_style to the base class, ensuring it uses the correct attributes.

Expand source code
def pair_style(self, printflag=None, verbose=None, raw=False):
    """Delegate pair_style to the base class, ensuring it uses the correct attributes."""
    printflag = self.printflag if printflag is None else printflag
    verbose = self.verbose if verbose is None else verbose
    self._inject_attributes()
    return self.base_class.pair_style(printflag=printflag, verbose=verbose, raw=raw,USER=None, beadtype=self.beadtype, userid=self.userid)
def reset(self)

Reset the dforcefield instance to its initial state, reapplying the default values including RULES, GLOBAL, and LOCAL.

Expand source code
def reset(self):
    """
    Reset the dforcefield instance to its initial state, reapplying the default values
    including RULES, GLOBAL, and LOCAL.
    """
    # Preserve the current RULES, GLOBAL, and LOCAL or reset them
    current_rules = self.RULES
    current_global = self.GLOBAL
    current_local = self.LOCAL

    # Reinitialize the instance with existing or reset RULES, GLOBAL, and LOCAL
    self.__init__(
        base_class=self.base_class.__class__,
        beadtype=self.beadtype,
        userid=self.userid,
        name=self.name,
        description=self.description,
        version=self.version,
        RULES=current_rules,
        GLOBAL=current_global,
        LOCAL=current_local
        )
def save(self, filename=None, foldername=None, overwrite=False)

Save the dforcefield instance to a file with a header, formatted content, and the base_class.

Args:

filename (str): The name of the file to save. Defaults to self.userid if not provided. foldername (str): The folder in which to save the file. Defaults to a temporary directory. overwrite (bool): Whether to overwrite the file if it already exists. Defaults to False.

Raises:

ValueError: If base_class is None or not a subclass of forcefield. FileExistsError: If the file already exists and overwrite is False.

Notes:

The file format follows a specific structure for saving forcefield attributes, base_class, name, description, and parameters.

Example Output: (without the comments)

DFORCEFIELD SAVE FILE

base_class = "tlsph" beadtype = 1 userid = "dynamic_water" version = 1.0

description:{forcefield="LAMMPS:SMD", style="tlsph", material="water"} name:{forcefield="LAMMPS:SMD", material="water"}

rho = 1000 E = "5${c0}^2${rho}" nu = 0.3

Expand source code
def save(self, filename=None, foldername=None, overwrite=False):
    """
    Save the dforcefield instance to a file with a header, formatted content, and the base_class.

    Args:
    -----
    filename (str): The name of the file to save. Defaults to self.userid if not provided.
    foldername (str): The folder in which to save the file. Defaults to a temporary directory.
    overwrite (bool): Whether to overwrite the file if it already exists. Defaults to False.

    Raises:
    -------
    ValueError: If base_class is None or not a subclass of forcefield.
    FileExistsError: If the file already exists and overwrite is False.

    Notes:
    ------
    The file format follows a specific structure for saving forcefield attributes, base_class,
    name, description, and parameters.

    Example Output: (without the comments)
    ---------------
    # DFORCEFIELD SAVE FILE
    base_class = "tlsph"
    beadtype = 1
    userid = "dynamic_water"
    version = 1.0

    description:{forcefield="LAMMPS:SMD", style="tlsph", material="water"}
    name:{forcefield="LAMMPS:SMD", material="water"}

    rho = 1000
    E = "5*${c0}^2*${rho}"
    nu = 0.3
    """

    # Use self.userid if filename is not provided
    if filename is None:
        filename = self.userid

    # Ensure the filename ends with '.txt'
    if not filename.endswith('.txt'):
        filename += '.txt'

    # Default folder to tempdir if foldername is not provided
    if foldername is None:
        foldername = tempfile.gettempdir()

    # Construct full path
    if not os.path.isabs(filename):
        filepath = os.path.join(foldername, filename)
    else:
        filepath = filename

    # Check if file already exists, raise exception if it does and overwrite is False
    if os.path.exists(filepath) and not overwrite:
        raise FileExistsError(f"The file '{filepath}' already exists.")

    # Header with current date, username, and host
    user = getpass.getuser()
    host = socket.gethostname()
    date = datetime.now().strftime('%Y-%m-%d')
    header = f"# DFORCEFIELD SAVE FILE\n# generated on {date} by {user}@{host}\n"
    header += f'#\tuserid = "{self.userid}"\n'
    header += f'#\tpath = "{filepath}"\n\n'

    # Open the file for writing
    with open(filepath, 'w') as f:
        # Write the header
        f.write(header)

        # Write beadtype, userid, and version with comments
        f.write("# Forcefield attributes\n")
        f.write(f'base_class="{self.base_class.__class__.__name__}"\n')
        f.write(f"beadtype = {self.beadtype}\n")
        f.write(f"userid = {self.userid}\n")
        f.write(f"version = {self.version}\n\n")

        # Write description with comment
        f.write("# Description of the forcefield\n")
        f.write("description:{")
        f.write(", ".join(f'{key}="{value}"' for key, value in self.description.items()))
        f.write("}\n\n")

        # Write name with comment
        f.write("# Name of the forcefield\n")
        f.write("name:{")
        f.write(", ".join(f'{key}="{value}"' for key, value in self.name.items()))
        f.write("}\n\n")

        # Write parameters with comment
        f.write("# Parameters for the forcefield\n")
        for key, value in self.parameters.items():
            f.write(f"{key} = {value}\n")

    # return filepath
    print(f"\nScript saved to {filepath}")

    return filepath
def scriptobject(self, beadtype=None, name=None, userid=None, fullname=None, filename=None, group=None, style=None, USER=script data (SD object) with 0 definitions)

Method to return a scriptobject based on the current dforcefield instance.

Parameters:

beadtype : int, optional The bead type identifier. Defaults to the instance's beadtype.

name : str, optional A short name for the scriptobject. If not provided, uses 'forcefield' from self.name.

fullname : str, optional A comprehensive name for the object. Defaults to "beads of type {self.beadtype} | object of forcefield: {self.name.forcefield}".

group : list, optional A group of scriptobjects that this object belongs to. Defaults to an empty list.

style : str, optional The style of the scriptobject. Defaults to self.description.style if available, otherwise "smd".

USER : scriptdata, optional User-defined data for additional parameters. Defaults to a blank scriptdata instance.

Returns:

scriptobject A scriptobject instance populated with the current dforcefield instance's attributes or provided arguments.

Expand source code
def scriptobject(self, beadtype=None, name=None, userid=None, fullname=None, filename=None, group=None, style=None, USER=scriptdata()):
    """
    Method to return a scriptobject based on the current dforcefield instance.

    Parameters:
    ------------
    beadtype : int, optional
        The bead type identifier. Defaults to the instance's beadtype.

    name : str, optional
        A short name for the scriptobject. If not provided, uses 'forcefield' from self.name.

    fullname : str, optional
        A comprehensive name for the object. Defaults to "beads of type {self.beadtype} | object of forcefield: {self.name.forcefield}".

    group : list, optional
        A group of scriptobjects that this object belongs to. Defaults to an empty list.

    style : str, optional
        The style of the scriptobject. Defaults to `self.description.style` if available, otherwise "smd".

    USER : scriptdata, optional
        User-defined data for additional parameters. Defaults to a blank `scriptdata()` instance.

    Returns:
    --------
    scriptobject
        A scriptobject instance populated with the current `dforcefield` instance's attributes or provided arguments.
    """
    # Set defaults using instance attributes if parameters are None
    if beadtype is None:
        beadtype = self.beadtype

    # Extract a meaningful name from the forcefield's name structure
    if name is None:
        if isinstance(self.name, struct) and hasattr(self.name, 'material'):
            name = f"{self.name.material} bead"
        else:
            name = f"beadtype_{beadtype}"

    if userid is None:
        userid = name

    if fullname is None:
        fullname = f"beads of type {beadtype} | object of forcefield: {self.name.forcefield if isinstance(self.name, struct) and hasattr(self.name, 'forcefield') else 'unknown'}"

    if group is None:
        group = []

    # Use `self.description.style` as the default for style if it exists, otherwise fall back to "smd"
    if style is None:
        if isinstance(self.description, struct) and hasattr(self.description, 'style'):
            style = self.description.style
        else:
            style = "undefined style"

    # The current dforcefield instance behaves as the forcefield for the scriptobject
    forcefield = copy.deepcopy(self)
    forcefield.beadtype = beadtype
    forcefield.name = name
    forcefield.userid = userid

    # Apply any user-defined parameters to the forcefield
    # This is the standard behavior of scriptobject
    forcefield.parameters = forcefield.parameters + USER

    # Create and return the scriptobject instance
    return scriptobject(
        beadtype=beadtype,
        name=name,
        fullname=fullname,
        filename=filename,
        style=style,
        forcefield=forcefield,  # Use the current dforcefield instance
        group=group,
        USER=USER
    )
def set_global(self, new_global=None, **kwargs)

Update the GLOBAL parameters and adjust the combined parameters accordingly.

Args:

new_global : parameterforcefield, optional The new GLOBAL parameters to set. If None, only the parameters in kwargs are modified. kwargs : dict, optional Keyword arguments representing individual parameters to add or modify in GLOBAL.

Expand source code
def set_global(self, new_global=None, **kwargs):
    """
    Update the GLOBAL parameters and adjust the combined parameters accordingly.

    Args:
    -----
    new_global : parameterforcefield, optional
        The new GLOBAL parameters to set. If None, only the parameters in kwargs are modified.
    kwargs : dict, optional
        Keyword arguments representing individual parameters to add or modify in GLOBAL.
    """
    if new_global is not None and not isinstance(new_global, parameterforcefield):
        raise TypeError(f"GLOBAL must be of type parameterforcefield, not {type(new_global)}.")

    # Combine current GLOBAL, new_global, and additional parameters in kwargs
    self.GLOBAL = (self.GLOBAL + (new_global or genericdata()) + genericdata(**kwargs))
    self.update_parameters()
def set_local(self, new_local=None, **kwargs)

Update the LOCAL parameters and adjust the combined parameters accordingly.

Args:

new_local : parameterforcefield, optional The new LOCAL parameters to set. If None, only the parameters in kwargs are modified. kwargs : dict, optional Keyword arguments representing individual parameters to add or modify in LOCAL.

Expand source code
def set_local(self, new_local=None, **kwargs):
    """
    Update the LOCAL parameters and adjust the combined parameters accordingly.

    Args:
    -----
    new_local : parameterforcefield, optional
        The new LOCAL parameters to set. If None, only the parameters in kwargs are modified.
    kwargs : dict, optional
        Keyword arguments representing individual parameters to add or modify in LOCAL.
    """
    if new_local is not None and not isinstance(new_local, parameterforcefield):
        raise TypeError(f"LOCAL must be of type parameterforcefield, not {type(new_local)}.")

    # Combine current LOCAL, new_local, and additional parameters in kwargs
    self.LOCAL = (self.LOCAL + (new_local or genericdata()) + genericdata(**kwargs))
    self.update_parameters()
def set_rules(self, new_rules=None, **kwargs)

Update the RULES parameters and adjust the combined parameters accordingly.

Args:

new_rules : parameterforcefield, optional The new RULES parameters to set. If None, only the parameters in kwargs are modified. kwargs : dict, optional Keyword arguments representing individual parameters to add or modify in RULES.

Expand source code
def set_rules(self, new_rules=None, **kwargs):
    """
    Update the RULES parameters and adjust the combined parameters accordingly.

    Args:
    -----
    new_rules : parameterforcefield, optional
        The new RULES parameters to set. If None, only the parameters in kwargs are modified.
    kwargs : dict, optional
        Keyword arguments representing individual parameters to add or modify in RULES.
    """
    if new_rules is not None and not isinstance(new_rules, parameterforcefield):
        raise TypeError(f"RULES must be of type parameterforcefield, not {type(new_rules)}.")

    # Combine current RULES, new_rules, and additional parameters in kwargs
    self.RULES = (self.RULES + (new_rules or genericdata()) + genericdata(**kwargs))
    self.update_parameters()
def show(self)

Show the corresponding base_class forcefield definition

Expand source code
def show(self):
    """Show the corresponding base_class forcefield definition """
    self.base_class.__repr__()
    return self.base_class.__str__()
def to_dict(self)

Serialize the dforcefield instance to a dictionary, including RULES, GLOBAL, and LOCAL.

Returns

dict
A dictionary containing all the attributes and their current values.

Example

config = dynamic_water.to_dict() print(config)

Expand source code
def to_dict(self):
    """
    Serialize the dforcefield instance to a dictionary, including RULES, GLOBAL, and LOCAL.

    Returns:
        dict: A dictionary containing all the attributes and their current values.

    Example:
        config = dynamic_water.to_dict()
        print(config)
    """
    data = {
        "name": self.name,
        "description": self.description,
        "beadtype": self.beadtype,
        "userid": self.userid,
        "version": self.version,
        "parameters": dict(self.parameters),  # Convert parameters to a standard dict
        "base_class": self.base_class.__class__.__name__,
        "RULES": dict(self.RULES) if self.RULES else {},  # Include RULES if defined
        "GLOBAL": dict(self.GLOBAL) if self.GLOBAL else {},  # Include GLOBAL if defined
        "LOCAL": dict(self.LOCAL) if self.LOCAL else {}  # Include LOCAL if defined
    }
    return data
def update(self, **kwargs)

Update multiple attributes of the dforcefield instance at once, including RULES, GLOBAL, and LOCAL.

Args

kwargs
Key-value pairs of attributes to update.
Expand source code
def update(self, **kwargs):
    """
    Update multiple attributes of the dforcefield instance at once, including RULES, GLOBAL, and LOCAL.

    Args:
        kwargs: Key-value pairs of attributes to update.
    """
    for key, value in kwargs.items():
        if key == "RULES":
            self.RULES = self.RULES + value if self.RULES else value
        elif key == "GLOBAL":
            self.GLOBAL = self.GLOBAL + value if self.GLOBAL else value
        elif key == "LOCAL":
            self.LOCAL = self.LOCAL + value if self.LOCAL else value
        else:
            setattr(self, key, value)
def update_parameters(self)

Update self.parameters by combining GLOBAL, LOCAL, RULES, and USER parameters.

Expand source code
def update_parameters(self):
    """
    Update self.parameters by combining GLOBAL, LOCAL, RULES, and USER parameters.
    """
    self.parameters = self.parameters + self.combine_parameters()
    self._inject_attributes()
def validate(self)

Validate the dforcefield instance to ensure all required attributes are set.

Raises

ValueError
If any required attributes are missing or invalid.
Expand source code
def validate(self):
    """
    Validate the dforcefield instance to ensure all required attributes are set.

    Raises:
        ValueError: If any required attributes are missing or invalid.
    """
    required_fields = self._dforcefield_specific_attributes

    for field in required_fields:
        if not getattr(self, field):
            raise ValueError(f"{field} is required and not set.")

    print("Validation successful.")
def values(self)

Return the values of the merged struct, parameters, and scalar attributes.

Expand source code
def values(self):
    """
    Return the values of the merged struct, parameters, and scalar attributes.
    """
    values_list = list(self.merged_name_description.__dict__.values())
    values_list.extend(self.parameters.values())
    values_list.extend([self.version, self.userid, self.beadtype])
    return values_list
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

  • pizza.forcefield.smd

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 generic

generic helper class

Expand source code
class generic:
    """ generic helper class """
    _version = 0.1
    _description = "generic helper for forcefield parameterization"
    RULES = genericdata()

    def __init__(self):
        self.name = None
        self.type = None

    def __repr__(self):
        """ representation of generic() """
        id = ""
        if self.type is not None: id = str(self.type)+":"
        if self.name is not None: id = id+str(self.name)
        if id !="": id = '"'+id+'"'
        idstr = '  generic helper: %s\n\t (class "%s")' % (id,self.__class__)
        print("%s" % idstr)
        if len(self.GLOBAL):
            print("  with GLOBAL data")
            self.GLOBAL.__repr__()
        return idstr

Subclasses

  • pizza.generic.USERSMD

Class variables

var RULES
class genericdata (sortdefinitions=False, **kwargs)

generic data class to be used along with pizza.generic

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 genericdata(parameterforcefield):
    """ generic data class to be used along with pizza.generic """
    _type = "gendata"        # object type
    _fulltype = "generic data" # full name
    _ftype = "item"        # field name
    _maxdisplay = 80

Ancestors

  • pizza.forcefield.parameterforcefield
  • pizza.private.mstruct.paramauto
  • pizza.private.mstruct.param
  • pizza.private.mstruct.struct
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

Subclasses

  • pizza.generic.genericdata
class scriptdata (sortdefinitions=False, **kwargs)

class of script parameters Typical constructor: DEFINITIONS = scriptdata( var1 = value1, var2 = value2 ) See script, struct, param to get review all methods attached to it

constructor

Expand source code
class scriptdata(param):
    """
        class of script parameters
            Typical constructor:
                DEFINITIONS = scriptdata(
                    var1 = value1,
                    var2 = value2
                    )
        See script, struct, param to get review all methods attached to it
    """
    _type = "SD"
    _fulltype = "script data"
    _ftype = "definition"

Ancestors

  • pizza.private.mstruct.param
  • pizza.private.mstruct.struct
class scriptobject (beadtype=1, name=None, fullname='', filename='', style='smd', mass=1.0, forcefield=LAMMPS:SMD:none:walls, group=[], USER=script data (SD object) with 0 definitions)

scriptobject: A Class for Managing Script Objects in LAMMPS

The scriptobject class is designed to represent individual objects in LAMMPS scripts, such as beads, atoms, or other components. Each object is associated with a forcefield instance that defines the physical interactions of the object, and the class supports a variety of properties for detailed object definition. Additionally, scriptobject instances can be grouped together and compared based on their properties, such as beadtype and name.

Key Features:

  • Forcefield Integration: Each scriptobject is associated with a forcefield instance, allowing for customized physical interactions. Forcefields can be passed via the USER keyword for dynamic parameterization.
  • Grouping: Multiple scriptobject instances can be combined into a scriptobjectgroup using the + operator, allowing for complex collections of objects.
  • Object Comparison: scriptobject instances can be compared and sorted based on their beadtype and name, enabling efficient organization and manipulation of objects.
  • Piping and Execution: Supports the pipe (|) operator, allowing scriptobject instances to be used in script pipelines alongside other script elements.

Practical Use Cases:

  • Object Definition in LAMMPS: Use scriptobject to represent individual objects in a simulation, including their properties and associated forcefields.
  • Forcefield Parameterization: Pass customized parameters to the forcefield via the USER keyword to dynamically adjust the physical interactions.
  • Grouping and Sorting: Combine multiple objects into groups, or sort them based on their properties (e.g., beadtype) for easier management in complex simulations.

Methods:

init(self, beadtype=1, name="undefined", fullname="", filename="", style="smd", forcefield=rigidwall(), group=[], USER=scriptdata()): Initializes a new scriptobject with the specified properties, including beadtype, name, forcefield, and optional group.

str(self): Returns a string representation of the scriptobject, showing its beadtype and name.

add(self, SO): Combines two scriptobject instances or a scriptobject with a scriptobjectgroup. Raises an error if the two objects have the same name or if the second operand is not a valid scriptobject or scriptobjectgroup.

or(self, pipe): Overloads the pipe (|) operator to integrate the scriptobject into a pipeline.

eq(self, SO): Compares two scriptobject instances, returning True if they have the same beadtype and name.

ne(self, SO): Returns True if the two scriptobject instances differ in either beadtype or name.

lt(self, SO): Compares the beadtype of two scriptobject instances, returning True if the left object's beadtype is less than the right object's.

gt(self, SO): Compares the beadtype of two scriptobject instances, returning True if the left object's beadtype is greater than the right object's.

le(self, SO): Returns True if the beadtype of the left scriptobject is less than or equal to the right scriptobject.

ge(self, SO): Returns True if the beadtype of the left scriptobject is greater than or equal to the right scriptobject.

Attributes:

beadtype : int The type of bead or object, used for distinguishing between different types in the simulation. name : str A short name for the object, useful for quick identification. fullname : str A comprehensive name for the object. If not provided, defaults to the name with "object definition". filename : str The path to the file containing the input data for the object. style : str The style of the object (e.g., "smd" for smoothed dynamics). forcefield : forcefield The forcefield instance associated with the object, defining its physical interactions. group : list A list of other scriptobject instances that are grouped with this object. USER : scriptdata A collection of user-defined variables for customizing the forcefield or other properties.

Original Content:

The scriptobject class enables the definition of objects within LAMMPS scripts, providing: - Beadtype and Naming: Objects are distinguished by their beadtype and name, allowing for comparison and sorting based on these properties. - Forcefield Support: Objects are linked to a forcefield instance, and user-defined forcefield parameters can be passed through the USER keyword. - Group Management: Multiple objects can be grouped together using the + operator, forming a scriptobjectgroup. - Comparison Operators: Objects can be compared based on their beadtype and name, using standard comparison operators (==, <, >, etc.). - Pipelines: scriptobject instances can be integrated into pipelines, supporting the | operator for use in sequential script execution.

Example Usage:

from pizza.scriptobject import scriptobject, rigidwall, scriptdata

# Define a script object with custom properties
obj1 = scriptobject(beadtype=1, name="bead1", forcefield=rigidwall(USER=scriptdata(param1=10)))

# Combine two objects into a group
obj2 = scriptobject(beadtype=2, name="bead2")
group = obj1 + obj2

# Print object information
print(obj1)
print(group)

The output will be:

script object | type=1 | name=bead1
scriptobjectgroup containing 2 objects

Overview

class of script object
    OBJ = scriptobject(...)
    Implemented properties:
        beadtype=1,2,...
        name="short name"
        fullname = "comprehensive name"
        filename = "/path/to/your/inputfile"
        style = "smd"
        forcefield = any valid forcefield instance (default = rigidwall())
        mass = 1.0

note: use a forcefield instance with the keywork USER to pass user FF parameters
examples:   rigidwall(USER=scriptdata(...))
            solidfood(USER==scriptdata(...))
            water(USER==scriptdata(...))

group objects with OBJ1+OBJ2... into scriptobjectgroups

objects can be compared and sorted based on beadtype and name

constructor

Expand source code
class scriptobject(struct):
    """
    scriptobject: A Class for Managing Script Objects in LAMMPS

    The `scriptobject` class is designed to represent individual objects in LAMMPS scripts,
    such as beads, atoms, or other components. Each object is associated with a `forcefield`
    instance that defines the physical interactions of the object, and the class supports
    a variety of properties for detailed object definition. Additionally, `scriptobject`
    instances can be grouped together and compared based on their properties, such as
    `beadtype` and `name`.

    Key Features:
    -------------
    - **Forcefield Integration**: Each `scriptobject` is associated with a `forcefield`
      instance, allowing for customized physical interactions. Forcefields can be passed
      via the `USER` keyword for dynamic parameterization.
    - **Grouping**: Multiple `scriptobject` instances can be combined into a
      `scriptobjectgroup` using the `+` operator, allowing for complex collections of objects.
    - **Object Comparison**: `scriptobject` instances can be compared and sorted based on
      their `beadtype` and `name`, enabling efficient organization and manipulation of objects.
    - **Piping and Execution**: Supports the pipe (`|`) operator, allowing `scriptobject`
      instances to be used in script pipelines alongside other script elements.

    Practical Use Cases:
    --------------------
    - **Object Definition in LAMMPS**: Use `scriptobject` to represent individual objects in
      a simulation, including their properties and associated forcefields.
    - **Forcefield Parameterization**: Pass customized parameters to the forcefield via the
      `USER` keyword to dynamically adjust the physical interactions.
    - **Grouping and Sorting**: Combine multiple objects into groups, or sort them based
      on their properties (e.g., `beadtype`) for easier management in complex simulations.

    Methods:
    --------
    __init__(self, beadtype=1, name="undefined", fullname="", filename="", style="smd",
             forcefield=rigidwall(), group=[], USER=scriptdata()):
        Initializes a new `scriptobject` with the specified properties, including `beadtype`,
        `name`, `forcefield`, and optional `group`.

    __str__(self):
        Returns a string representation of the `scriptobject`, showing its `beadtype` and `name`.

    __add__(self, SO):
        Combines two `scriptobject` instances or a `scriptobject` with a `scriptobjectgroup`.
        Raises an error if the two objects have the same `name` or if the second operand is not
        a valid `scriptobject` or `scriptobjectgroup`.

    __or__(self, pipe):
        Overloads the pipe (`|`) operator to integrate the `scriptobject` into a pipeline.

    __eq__(self, SO):
        Compares two `scriptobject` instances, returning `True` if they have the same
        `beadtype` and `name`.

    __ne__(self, SO):
        Returns `True` if the two `scriptobject` instances differ in either `beadtype` or `name`.

    __lt__(self, SO):
        Compares the `beadtype` of two `scriptobject` instances, returning `True` if the
        left object's `beadtype` is less than the right object's.

    __gt__(self, SO):
        Compares the `beadtype` of two `scriptobject` instances, returning `True` if the
        left object's `beadtype` is greater than the right object's.

    __le__(self, SO):
        Returns `True` if the `beadtype` of the left `scriptobject` is less than or equal to
        the right `scriptobject`.

    __ge__(self, SO):
        Returns `True` if the `beadtype` of the left `scriptobject` is greater than or equal
        to the right `scriptobject`.

    Attributes:
    -----------
    beadtype : int
        The type of bead or object, used for distinguishing between different types in the simulation.
    name : str
        A short name for the object, useful for quick identification.
    fullname : str
        A comprehensive name for the object. If not provided, defaults to the `name` with "object definition".
    filename : str
        The path to the file containing the input data for the object.
    style : str
        The style of the object (e.g., "smd" for smoothed dynamics).
    forcefield : forcefield
        The forcefield instance associated with the object, defining its physical interactions.
    group : list
        A list of other `scriptobject` instances that are grouped with this object.
    USER : scriptdata
        A collection of user-defined variables for customizing the forcefield or other properties.

    Original Content:
    -----------------
    The `scriptobject` class enables the definition of objects within LAMMPS scripts, providing:
    - **Beadtype and Naming**: Objects are distinguished by their `beadtype` and `name`, allowing
      for comparison and sorting based on these properties.
    - **Forcefield Support**: Objects are linked to a forcefield instance, and user-defined forcefield
      parameters can be passed through the `USER` keyword.
    - **Group Management**: Multiple objects can be grouped together using the `+` operator, forming
      a `scriptobjectgroup`.
    - **Comparison Operators**: Objects can be compared based on their `beadtype` and `name`, using
      standard comparison operators (`==`, `<`, `>`, etc.).
    - **Pipelines**: `scriptobject` instances can be integrated into pipelines, supporting the `|`
      operator for use in sequential script execution.

    Example Usage:
    --------------
    ```
    from pizza.scriptobject import scriptobject, rigidwall, scriptdata

    # Define a script object with custom properties
    obj1 = scriptobject(beadtype=1, name="bead1", forcefield=rigidwall(USER=scriptdata(param1=10)))

    # Combine two objects into a group
    obj2 = scriptobject(beadtype=2, name="bead2")
    group = obj1 + obj2

    # Print object information
    print(obj1)
    print(group)
    ```

    The output will be:
    ```
    script object | type=1 | name=bead1
    scriptobjectgroup containing 2 objects
    ```

    OVERVIEW
    --------------

        class of script object
            OBJ = scriptobject(...)
            Implemented properties:
                beadtype=1,2,...
                name="short name"
                fullname = "comprehensive name"
                filename = "/path/to/your/inputfile"
                style = "smd"
                forcefield = any valid forcefield instance (default = rigidwall())
                mass = 1.0

        note: use a forcefield instance with the keywork USER to pass user FF parameters
        examples:   rigidwall(USER=scriptdata(...))
                    solidfood(USER==scriptdata(...))
                    water(USER==scriptdata(...))

        group objects with OBJ1+OBJ2... into scriptobjectgroups

        objects can be compared and sorted based on beadtype and name

    """
    _type = "SO"
    _fulltype = "script object"
    _ftype = "propertie"

    def __init__(self,
                 beadtype = 1,
                 name = None,
                 fullname="",
                 filename="",
                 style="smd",
                 mass=1.0, # added on 2024-11-29
                 forcefield=rigidwall(),
                 group=[],
                 USER = scriptdata()
                 ):
        name = f"beadtype={beadtype}" if name is None else name
        if not isinstance(name,str):
            TypeError(f"name must a string or None got {type(name)}")
        if fullname=="": fullname = name + " object definition"
        if not isinstance(group,list): group = [group]
        forcefield.beadtype = beadtype
        forcefield.userid = name
        forcefield.USER = USER
        super(scriptobject,self).__init__(
              beadtype = beadtype,
                  name = name,
              fullname = fullname,
              filename = filename,
                 style = style,
            forcefield = forcefield,
                  mass = mass,
                 group = group,
                  USER = USER
                 )

    def __str__(self):
        """ string representation """
        return f"{self._fulltype} | type={self.beadtype} | name={self.name}"

    def __add__(self, SO):
        if isinstance(SO,scriptobject):
            if SO.name != self.name:
                if SO.beadtype == self.beadtype:
                   SO.beadtype =  self.beadtype+1
                return scriptobjectgroup(self,SO)
            else:
                raise ValueError('the object "%s" already exists' % SO.name)
        elif isinstance(SO,scriptobjectgroup):
            return scriptobjectgroup(self)+SO
        else:
            return ValueError("The object should a script object or its container")

    def __or__(self, pipe):
        """ overload | or for pipe """
        if isinstance(pipe,(pipescript,script,scriptobject,scriptobjectgroup)):
            return pipescript(self) | pipe
        else:
            raise ValueError("the argument must a pipescript, a scriptobject or a scriptobjectgroup")

    def __eq__(self, SO):
        return isinstance(SO,scriptobject) and (self.beadtype == SO.beadtype) and (self.mass == SO.mass) \
            and (self.name == SO.name)

    def __ne__(self, SO):
        return not isinstance(SO,scriptobject) or (self.beadtype != SO.beadtype) or (self.mass != SO.mass) or (self.name != SO.name)

    def __lt__(self, SO):
        return self.beadtype < SO.beadtype

    def __gt__(self, SO):
        return self.beadtype > SO.beadtype

    def __le__(self, SO):
        return self.beadtype <= SO.beadtype

    def __ge__(self, SO):
        return self.beadtype >= SO.beadtype

Ancestors

  • pizza.private.mstruct.struct
class struct (**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__).
  • 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.
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.
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

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__`).
    - `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.        |
    | `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.                  |
    | `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

    # 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'} # used by keys() and len()


    # Methods
    def __init__(self,**kwargs):
        """ constructor """
        # Optionally extend _excludedattr here
        self._excludedattr = self._excludedattr | {'_excludedattr', '_type', '_fulltype','_ftype'} # addition 2024-10-11
        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,tuple,np.ndarray,np.generic)): keys = [keys]
        if not isinstance(values,(list,tuple,np.ndarray,np.generic)): 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):
            return struct.fromkeysvalues(self.keys()[idx], self.values()[idx])
        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) and i>=0 and i<nk:
                    s.setattr(k[i],v[i])
                else:
                    raise IndexError("idx must contains only integers ranged between 0 and %d" % (nk-1))
            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 slive (%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 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}")
        dup = duplicate(self)
        dup.__dict__.update(s.__dict__)
        if sortdefinitions: 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.__dict__.update(s.__dict__)
        if sortdefinitions: 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:
            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,(int,float,str,list,tuple,np.ndarray,np.generic)):
                        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))
                    elif isinstance(value,struct):
                        print(fmt % key,self.dispmax(value.__str__()))
                    elif isinstance(value,type):
                        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:
                                    print(fmteval % "",self.dispmax(tmp.getattr(key)))
            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 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):
        """ generate Python code of the equivalent structure """
        nk = len(self)
        if nk==0:
            print("X = struct()")
        else:
            ik = 0
            fmt = "%%%ss=" % max(10,max([len(k) for k in self.keys()])+2)
            print("\nX = struct(")
            for k in self.keys():
                ik += 1
                end = ",\n" if ik<nk else "\n"+(fmt[:-1] % ")")+"\n"
                v = getattr(self,k)
                if isinstance(v,(int,float)) or v == None:
                    print(fmt % k,v,end=end)
                elif isinstance(v,str):
                    print(fmt % k,f'"{v}"',end=end)
                elif isinstance(v,(list,tuple)):
                    print(fmt % k,v,end=end)
                else:
                    print(fmt % k,"/* unsupported type */",end=end)

    # 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.")
        # 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 self.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}'")

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 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,tuple,np.ndarray,np.generic)): keys = [keys]
    if not isinstance(values,(list,tuple,np.ndarray,np.generic)): 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 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(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)

generate Python code of the equivalent structure

Expand source code
def generator(self):
    """ generate Python code of the equivalent structure """
    nk = len(self)
    if nk==0:
        print("X = struct()")
    else:
        ik = 0
        fmt = "%%%ss=" % max(10,max([len(k) for k in self.keys()])+2)
        print("\nX = struct(")
        for k in self.keys():
            ik += 1
            end = ",\n" if ik<nk else "\n"+(fmt[:-1] % ")")+"\n"
            v = getattr(self,k)
            if isinstance(v,(int,float)) or v == None:
                print(fmt % k,v,end=end)
            elif isinstance(v,str):
                print(fmt % k,f'"{v}"',end=end)
            elif isinstance(v,(list,tuple)):
                print(fmt % k,v,end=end)
            else:
                print(fmt % k,"/* unsupported type */",end=end)
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 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 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 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.")
    # 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 self.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())