Module script

The class script() and derived facilitate the coding in LAMMPS Each section is remplaced by a template as a class inherited from script()

The class include two important attribues:
    TEMPLATE is a string  efines between  the LAMMPS code
    The variables used by TEMPLATE are stored in DEFINITIONS.
    DEFINITIONS is a scripdata() object accepting scalar, mathematical expressions,
    text almost as in LAMMPS.

    Variables can be inherited between sections using + or += operator

Toy example
    G = globalsection()
    print(G)
    c = initializesection()
    print(c)
    g = geometrysection()
    print(g)
    d = discretizationsection()
    print(d)
    b = boundarysection()
    print(b)
    i = interactionsection()
    print(i)
    t = integrationsection()
    print(t)
    d = dumpsection()
    print(d)
    s = statussection()
    print(s)
    r = runsection()
    print(r)

    # all sections as a single script
    myscript = G+c+g+d+b+i+t+d+s+r
    print("

"4,'='80,'

this is the full script

','='*80,' ') print(myscript.do())

Additional classes: scriptobject(), scriptobjectgroup(), pipescript()
They generate dynamic scripts from objects, collection of objects or scripts

Variables (DEFINITIONS and USER) are stored in scriptdata() objects


How the variables are stored and used.
     STATIC: set in the script class (with the attribute DEFINITIONS)
     GLOBAL: set in the instance of the script during construction
             or within the USER scriptdata(). These values can be changed
             at runtime but the values are overwritten if the script are
             combined with the operator +
      LOCAL: set (bypass) in the pipeline with the keyword USER[step]

Example with pipelines:
    Build pipelines with:
        p = G | c | g # using the symbol pipe "|"
        p = pipescript(G)*4 # using the constructor pipescript()

            Pipeline with 4 scripts and
            D(STATIC:GLOBAL:LOCAL) DEFINITIONS
              ------------:----------------------------------------
              [-]  00: script:global:example with D(19: 0: 0)
              [-]  01: script:global:example with D(19: 0: 0)
              [-]  02: script:global:example with D(19: 0: 0)
              [-]  03: script:global:example with D(19: 0: 0)
              ------------:----------------------------------------

Change the GLOBAL variables for script with idx=0
    p.scripts[0].USER.a=1  # set a=1 for all scripts onwards
    p.scripts[0].USER.b=2  # set b=2
Change the LOCAL variables for script with idx=0
    p.USER[0].a=10        # set a=10 for the script 00

            ------------:----------------------------------------
            [-]  00: script:global:example with D(19: 2: 1)
            [-]  01: script:global:example with D(19: 0: 0)
            [-]  02: script:global:example with D(19: 0: 0)
            [-]  03: script:global:example with D(19: 0: 0)
            ------------:----------------------------------------

Summary of pipeline indexing and scripting
    p[i], p[i:j], p[[i,j]] copy pipeline segments
    LOCAL: p.USER[i],p.USER[i].variable modify the user space of only p[i]
    GLOBAL: p.scripts[i].USER.var to modify the user space from p[i] and onwards
    STATIC: p.scripts[i].DEFINITIONS
    p.rename(idx=range(2),name=["A","B"]), p.clear(idx=[0,3,4])
    p.script(), p.script(idx=range(5)), p[0:5].script()

Created on Sat Feb 19 11:00:43 2022

@author: olivi

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

"""

    The class script() and derived facilitate the coding in LAMMPS
    Each section is remplaced by a template as a class inherited from script()

    The class include two important attribues:
        TEMPLATE is a string  efines between """ """ the LAMMPS code
        The variables used by TEMPLATE are stored in DEFINITIONS.
        DEFINITIONS is a scripdata() object accepting scalar, mathematical expressions,
        text almost as in LAMMPS.

        Variables can be inherited between sections using + or += operator

    Toy example
        G = globalsection()
        print(G)
        c = initializesection()
        print(c)
        g = geometrysection()
        print(g)
        d = discretizationsection()
        print(d)
        b = boundarysection()
        print(b)
        i = interactionsection()
        print(i)
        t = integrationsection()
        print(t)
        d = dumpsection()
        print(d)
        s = statussection()
        print(s)
        r = runsection()
        print(r)

        # all sections as a single script
        myscript = G+c+g+d+b+i+t+d+s+r
        print("\n"*4,'='*80,'\n\n this is the full script\n\n','='*80,'\n')
        print(myscript.do())

    Additional classes: scriptobject(), scriptobjectgroup(), pipescript()
    They generate dynamic scripts from objects, collection of objects or scripts

    Variables (DEFINITIONS and USER) are stored in scriptdata() objects


    How the variables are stored and used.
         STATIC: set in the script class (with the attribute DEFINITIONS)
         GLOBAL: set in the instance of the script during construction
                 or within the USER scriptdata(). These values can be changed
                 at runtime but the values are overwritten if the script are
                 combined with the operator +
          LOCAL: set (bypass) in the pipeline with the keyword USER[step]

    Example with pipelines:
        Build pipelines with:
            p = G | c | g # using the symbol pipe "|"
            p = pipescript(G)*4 # using the constructor pipescript()

                Pipeline with 4 scripts and
                D(STATIC:GLOBAL:LOCAL) DEFINITIONS
                  ------------:----------------------------------------
                  [-]  00: script:global:example with D(19: 0: 0)
                  [-]  01: script:global:example with D(19: 0: 0)
                  [-]  02: script:global:example with D(19: 0: 0)
                  [-]  03: script:global:example with D(19: 0: 0)
                  ------------:----------------------------------------

    Change the GLOBAL variables for script with idx=0
        p.scripts[0].USER.a=1  # set a=1 for all scripts onwards
        p.scripts[0].USER.b=2  # set b=2
    Change the LOCAL variables for script with idx=0
        p.USER[0].a=10        # set a=10 for the script 00

                ------------:----------------------------------------
                [-]  00: script:global:example with D(19: 2: 1)
                [-]  01: script:global:example with D(19: 0: 0)
                [-]  02: script:global:example with D(19: 0: 0)
                [-]  03: script:global:example with D(19: 0: 0)
                ------------:----------------------------------------

    Summary of pipeline indexing and scripting
        p[i], p[i:j], p[[i,j]] copy pipeline segments
        LOCAL: p.USER[i],p.USER[i].variable modify the user space of only p[i]
        GLOBAL: p.scripts[i].USER.var to modify the user space from p[i] and onwards
        STATIC: p.scripts[i].DEFINITIONS
        p.rename(idx=range(2),name=["A","B"]), p.clear(idx=[0,3,4])
        p.script(), p.script(idx=range(5)), p[0:5].script()

Created on Sat Feb 19 11:00:43 2022

@author: olivi
"""

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



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


# Revision history
# 2022-02-20 RC with documentation and 10 section templates
# 2022-02-21 add + += operators, expand help
# 2022-02-26 add USER to enable instance variables with higher precendence
# 2022-02-27 add write() method and overload & operator
# 2022-02-28 overload * (+ expansion) and ** (& expansion) operators
# 2022-03-01 expand lists (with space as separator) and tupples (with ,)
# 2022-03-02 first implementation of scriptobject(), object container pending
# 2022-03-03 extensions of scriptobject() and scriptobjectgroup()
# 2022-03-04 consolidation of scriptobject() and scriptobjectgroup()
# 2022-03-05 [major update] scriptobjectgroup() can generate scripts for loading, setting groups and forcefields
# 2022-03-12 [major update] pipescript() enables to store scripts in dynamic pipelines
# 2022-03-15 implement a userspace within the pipeline
# 2022-03-15 fix scriptobject | pipescript, smooth and document syntaxes with pipescripts
# 2022-03-16 overload +=, *, several fixes and update help for pipescript
# 2022-03-18 modification to script method, \ttype specified for groups
# 2022-03-19 standardized pizza path
# 2023-01-09 update __repr__() for scripts to show both DEFINITIONS and USER if verbose = True
# 2023-01-19 fix do() for pipescripts when the pipe has been already fully executed (statically)
# 2023-01-20 add picker() and possibility to get list indices in pipe
# 2023-01-26 add pipescript.join()
# 2023-01-27 add tmpwrite()
# 2023-01-27 use % instead of $ for lists in script.do(), in line with the new feature implemented in param.eval()
# 2023-01-31 fix the temporary file on Linux
# 2023-07-14 add and implement persistentfile and peristenfolder in scripts
# 2023-07-20 add header to script.tmpwrite()
# 2023-07-20 add a persident script.preview.clean copy
# 2023-08-17 fix span() when vector is "" or str
# 2024-04-16 fix the method tmpwrite(self) on windows with proper error handling
# 2024-04-18 fix scriptobjectgroup.script for empty and None filename
# 2024-09-01 script accepts persistentfolder=None for inheritance
# 2024-10-09 verbosity handling with script.do() and pscript.do() methods, remove_comments moved to script from dscript (circular reference)
# 2024-10-12 implement | for dscript objects
# 2024-10-14 finalization of dscript integration, improved doc
# 2024-10-18 add dscript() method to generate a dscript object from a pipescript
# 2024-10-19 script.do() convert literal \\n back to \n
# 2024-10-22 fix | for non-native pipescript objects
# 2024-11-12 add flexibility to remove_comments(), comment_chars="#%", continuation_marker="..."
# 2024-11-23 improve write and do() methods (the old pipescript.do() method is available as pipescript.do_legacy() )
# 2024-11-25 clear distinction between pipescript and scrupt headers
# 2024-11-29 improved save features
# 2024-12-01 standarize scripting features, automatically call script/pscript methods
# 2024-12-02 standardize script.header(), pscript.header() and dscript.header()
# 2024-12-09 get-metadata() use globals()
# 2025-01-02 improve + and | for script and pipescript
# 2025-01-04 add VariableOccurrences, pipescript.list_values(), pipescript.list_multiple_values(), pipescript.plot_value_distribution()
# 2025-01-06 script.dscript() forces autorefresh=False to prevent automatic assignement of variables not definet yet (see dscript.ScriptTemplate constructor)
# 2025-01-07 add VariableOccurrences.export() in Markdown and HTML, pipescript.generate_report() (version 1.0)
# 2025-01-18 consistent implementation of do() between dscript and script for indexed variables
# 2025-02-18 fix pipescipt | scriptobjectgroup (the script method was not called before)

# %% Dependencies
import os, sys, datetime, socket, getpass, tempfile, types, re, inspect
from copy import copy as duplicate
from copy import deepcopy as deepduplicate
from shutil import copy as copyfile
# To facilitate data review with `VariableOccurrences` class
from collections import defaultdict
import matplotlib.pyplot as plt


# All forcefield parameters are stored à la Matlab in a structure
from pizza.private.mstruct import param,struct
from pizza.forcefield import *

__all__ = ['CallableScript', 'VariableOccurrences', 'boundarysection', 'discretizationsection', 'dumpsection', 'forcefield', 'frame_header', 'geometrysection', 'get_metadata', 'get_tmp_location', 'globalsection', 'initializesection', 'integrationsection', 'interactionsection', 'is_scalar', 'make_hashable', 'none', 'param', 'paramauto', 'parameterforcefield', 'picker', 'pipescript', 'remove_comments', 'rigidwall', 'runsection', 'saltTLSPH', 'script', 'scriptdata', 'scriptobject', 'scriptobjectgroup', 'smd', 'solidfood', 'span', 'statussection', 'struct', 'tlsph', 'tlsphalone', 'ulsph', 'ulsphalone', 'water']



# span vector into a single string
def span(vector,sep=" ",left="",right=""):
    return left + (vector if isinstance(vector, str) else sep.join(map(str, vector))) + right if vector is not None else ""

# select elements from a list L based on indices as L(indices) in Matlab
def picker(L,indices): return [L[i] for i in indices if (i>=0 and i<len(L))]

# Get the location of the `tmp` directory, in a system-independent way.
get_tmp_location = lambda: tempfile.gettempdir()

# UTF-8 encoded Byte Order Mark (sequence: 0xef, 0xbb, 0xbf)
BOM_UTF8 = b'\xef\xbb\xbf'


# %% Private functions and classes
def remove_comments(content, split_lines=False, emptylines=False, comment_chars="#", continuation_marker="\\\\", remove_continuation_marker=False):
    """
    Removes comments from a single or multi-line string, handling quotes, escaped characters, and line continuation.

    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.
    emptylines : bool, optional (default: False)
        If True, empty lines will be preserved in the output. If False, empty lines
        will be removed from the output.
    comment_chars : str, optional (default: "#")
        A string containing characters to identify the start of a comment.
        Any of these characters will mark the beginning of a comment unless within quotes.
    continuation_marker : str or None, optional (default: "\\\\")
        A string containing characters to indicate line continuation (use `\\` to specify).
        Any characters after the continuation marker are ignored as a comment. If set to `None`
        or an empty string, line continuation will not be processed.
    remove_continuation_marker : bool, optional (default: False)
        If True, the continuation marker itself is removed from the processed line, keeping
        only the characters before it. If False, the marker is retained as part of the line.

    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 and handle line continuation within a single line while managing quotes and escapes."""
        in_single_quote = False
        in_double_quote = False
        escaped = False
        result = []

        i = 0
        while i < len(line):
            char = line[i]

            if escaped:
                result.append(char)
                escaped = False
                i += 1
                continue

            # Handle escape character within quoted strings
            if char == '\\' and (in_single_quote or in_double_quote):
                escaped = True
                result.append(char)
                i += 1
                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

            # Check for line continuation marker if it's set and outside of quotes
            if continuation_marker and not in_single_quote and not in_double_quote:
                # Check if the remaining part of the line matches the continuation marker
                if line[i:].startswith(continuation_marker):
                    # Optionally remove the continuation marker
                    if remove_continuation_marker:
                        result.append(line[:i].rstrip())  # Keep everything before the marker
                    else:
                        result.append(line[:i + len(continuation_marker)].rstrip())  # Include the marker itself
                    return ''.join(result).strip()

            # Check for comment start characters outside of quotes
            if char in comment_chars and not in_single_quote and not in_double_quote:
                break  # Stop processing the line when a comment is found

            result.append(char)
            i += 1

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

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

    # Process each line, considering the emptylines flag
    processed_lines = []
    for line in lines:
        stripped_line = line.strip()
        if not stripped_line and not emptylines:
            continue  # Skip empty lines if emptylines is False
        if any(stripped_line.startswith(c) for c in comment_chars):
            continue  # Skip lines that are pure comments
        processed_line = process_line(line)
        if processed_line or emptylines:  # Only add non-empty lines if emptylines is False
            processed_lines.append(processed_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


# returns the metadata
def get_metadata():
    """Return a dictionary of explicitly defined metadata."""
    # Define the desired metadata keys
    metadata_keys = [
        "__project__",
        "__author__",
        "__copyright__",
        "__credits__",
        "__license__",
        "__maintainer__",
        "__email__",
        "__version__",
    ]
    # Filter only the desired keys from the current module's globals
    return {key.strip("_"): globals()[key] for key in metadata_keys if key in globals()}


# frames headers
def frame_header(
    lines,
    padding=2,
    style=1,
    corner_symbols=None,  # Can be a string or a tuple
    horizontal_symbol=None,
    vertical_symbol=None,
    empty_line_symbol=None,
    line_fill_symbol=None,
    comment="#"
):
    """
    Format the header content into an ASCII framed box with customizable properties.

    Parameters:
        lines (list or tuple): The lines to include in the header.
            - Empty strings "" are replaced with lines of `line_fill_symbol`.
            - None values are treated as empty lines.

        padding (int, optional): Number of spaces to pad on each side of the content. Default is 2.
        style (int, optional): Style index (1 to 6) for predefined frame styles. Default is 1.
        corner_symbols (str or tuple, optional): Symbols for the corners (top-left, top-right, bottom-left, bottom-right).
                                                 Can be a string (e.g., "+") for uniform corners.
        horizontal_symbol (str, optional): Symbol to use for horizontal lines.
        vertical_symbol (str, optional): Symbol to use for vertical lines.
        empty_line_symbol (str, optional): Symbol to use for empty lines inside the frame.
        line_fill_symbol (str, optional): Symbol to fill lines that replace empty strings.
        comment (str, optional): Comment symbol to prefix each line. Can be multiple characters. Default is "#".

    Returns:
        str: The formatted header as a string.

    Raises:
        ValueError: If the specified style is undefined or `corner_symbols` is invalid.
    """
    # Predefined styles
    styles = {
        1: {
            "corner_symbols": ("+", "+", "+", "+"),
            "horizontal_symbol": "-",
            "vertical_symbol": "|",
            "empty_line_symbol": " ",
            "line_fill_symbol": "-"
        },
        2: {
            "corner_symbols": ("╔", "╗", "╚", "╝"),
            "horizontal_symbol": "═",
            "vertical_symbol": "║",
            "empty_line_symbol": " ",
            "line_fill_symbol": "═"
        },
        3: {
            "corner_symbols": (".", ".", "'", "'"),
            "horizontal_symbol": "-",
            "vertical_symbol": "|",
            "empty_line_symbol": " ",
            "line_fill_symbol": "-"
        },
        4: {
            "corner_symbols": ("#", "#", "#", "#"),
            "horizontal_symbol": "=",
            "vertical_symbol": "#",
            "empty_line_symbol": " ",
            "line_fill_symbol": "="
        },
        5: {
            "corner_symbols": ("┌", "┐", "└", "┘"),
            "horizontal_symbol": "─",
            "vertical_symbol": "│",
            "empty_line_symbol": " ",
            "line_fill_symbol": "─"
        },
        6: {
            "corner_symbols": (".", ".", ".", "."),
            "horizontal_symbol": ".",
            "vertical_symbol": ":",
            "empty_line_symbol": " ",
            "line_fill_symbol": "."
        }
    }

    # Validate style and set defaults
    if style not in styles:
        raise ValueError(f"Undefined style {style}. Valid styles are {list(styles.keys())}.")

    selected_style = styles[style]

    # Convert corner_symbols to a tuple of 4 values
    if isinstance(corner_symbols, str):
        corner_symbols = (corner_symbols,) * 4
    elif isinstance(corner_symbols, (list, tuple)) and len(corner_symbols) == 1:
        corner_symbols = tuple(corner_symbols * 4)
    elif isinstance(corner_symbols, (list, tuple)) and len(corner_symbols) == 2:
        corner_symbols = (corner_symbols[0], corner_symbols[1], corner_symbols[0], corner_symbols[1])
    elif corner_symbols is None:
        corner_symbols = selected_style["corner_symbols"]
    elif not isinstance(corner_symbols, (list, tuple)) or len(corner_symbols) != 4:
        raise ValueError("corner_symbols must be a string or a tuple/list of 1, 2, or 4 elements.")

    # Apply overrides or defaults
    horizontal_symbol = horizontal_symbol or selected_style["horizontal_symbol"]
    vertical_symbol = vertical_symbol or selected_style["vertical_symbol"]
    empty_line_symbol = empty_line_symbol or selected_style["empty_line_symbol"]
    line_fill_symbol = line_fill_symbol or selected_style["line_fill_symbol"]

    # Process lines: Replace "" with line_fill placeholders, None with empty lines
    processed_lines = []
    max_content_width = 0
    for line in lines:
        if line == "":
            processed_lines.append("<LINE_FILL>")
        elif line is None:
            processed_lines.append(None)
        else:
            processed_lines.append(line)
            max_content_width = max(max_content_width, len(line))

    # Adjust width for padding
    frame_width = max_content_width + padding * 2

    # Build the top border
    top_border = f"{corner_symbols[0]}{horizontal_symbol * frame_width}{corner_symbols[1]}"

    # Build content lines with vertical borders
    framed_lines = [top_border]
    for line in processed_lines:
        if line is None:
            empty_line = f"{vertical_symbol}{empty_line_symbol * frame_width}{vertical_symbol}"
            framed_lines.append(empty_line)
        elif line == "<LINE_FILL>":
            fill_line = f"{vertical_symbol}{line_fill_symbol * frame_width}{vertical_symbol}"
            framed_lines.append(fill_line)
        else:
            content = line.center(frame_width)
            framed_line = f"{vertical_symbol}{content}{vertical_symbol}"
            framed_lines.append(framed_line)

    # Build the bottom border
    bottom_border = f"{corner_symbols[2]}{horizontal_symbol * frame_width}{corner_symbols[3]}"
    framed_lines.append(bottom_border)

    # Ensure all lines start with the comment symbol
    commented_lines = [
        line if line.startswith(comment) else f"{comment} {line}" for line in framed_lines
    ]

    return "\n".join(commented_lines)+"\n"


def make_hashable(val):
    """
    Recursively converts lists and dictionaries to tuples to make them hashable.
    """
    if isinstance(val, list):
        return tuple(make_hashable(item) for item in val)
    elif isinstance(val, dict):
        return tuple(sorted((k, make_hashable(v)) for k, v in val.items()))
    return val

def is_scalar(val):
    """
    Determines if a value is scalar (not a list, dict, or tuple).
    """
    return not isinstance(val, (list, dict, tuple))


# descriptor for callable script
class CallableScript:
    """
    A descriptor that allows the method Interactions to be accessed both as a property and as a callable function.

    This class enables a method to behave like a property when accessed without parentheses,
    returning a function that can be called with default parameters. It also allows the method
    to be called directly with optional parameters, providing flexibility in usage.

    Attributes:
    -----------
    func : function
        The original function that is decorated, which will be used for both property access
        and direct calls.

    Methods:
    --------
    __get__(self, instance, owner)
        Returns a lambda function to call the original function with default parameters
        when accessed as a property.

    __call__(self, instance, printflag=False, verbosity=2)
        Allows the original function to be called directly with specified parameters.
    """
    def __init__(self, func):
        self.func = func

    def __get__(self, instance, owner):
        # When accessed as a property, return a lambda that calls the original function
        return lambda printflag=False, verbosity=2, verbose=None: self.func(instance, printflag=printflag, verbosity=verbosity, verbose=verbose)

    def __call__(self, instance, printflag=False, verbosity=2, verbose=None):
        # Allow calling the function directly with specified parameters
        return self.func(instance, printflag=printflag, verbosity=verbosity)



# This class encapsulates the raw data and provides utility methods for analyzing variable occurrences.
class VariableOccurrences:
    """
    The `VariableOccurrences` class encapsulates raw data and provides utility methods for analyzing variable occurrences
    across various scopes. It is designed to handle both single and multiple variables, supporting flexible data structures
    and analysis operations.

    Class Methods:
    --------------
    - `__init__(data, variables=None)`: Initializes the class with raw data and optional variable names.
    - `_determine_scopes()`: Determines the unique scopes present across all variables.
    - `get_raw_data()`: Returns the raw data as provided during initialization.
    - `get_steps_with_value(value, within_list=False)`: Retrieves steps where the variable equals the specified value.
    - `get_all_values()`: Retrieves all unique values of the variable(s).
    - `get_all_elements_in_lists()`: Retrieves all unique elements within list-type variable values.
    - `get_usage_count(value, within_list=False)`: Counts occurrences of a specific value.
    - `get_steps_with_value_in_scope(variable, scope, value, within_list=False)`: Retrieves steps where a variable equals a value within a specific scope.
    - `summarize()`: Summarizes the occurrences of variables, including counts and unique elements.
    - `export(filename=None, scopes='all', variables='all', include_headers=True, return_content=False)`:
      Exports variable occurrences as Markdown, plain text, or HTML.

    Attributes:
    -----------
    - `data`: Dictionary containing the raw variable data.
      - Single variable: `{'scope1': [...], 'scope2': [...]}`.
      - Multiple variables: `{'var1': {...}, 'var2': {...}, ...}`.
    - `variables`: List of variable names managed by the instance.
    - `scopes`: List of unique scopes across all variables.

    Usage:
    ------
    The class is useful for tracking and analyzing variable values across different contexts, such as configuration files,
    programming environments, or simulation data. It supports advanced querying, summary generation, and export functionality.

    Example:
    --------
    # Initialize with single variable
    data = {'global': [(0, 1), (1, 2)], 'local': [(0, 3), (1, 4)]}
    vo = VariableOccurrences(data, variables="var1")

    # Initialize with multiple variables
    data = {
        "var1": {"global": [(0, 1), (1, 2)], "local": [(0, 3), (1, 4)]},
        "var2": {"global": [1, 2], "local": [3, 4]},
    }
    vo = VariableOccurrences(data)

    # Query steps where a value is present
    steps = vo.get_steps_with_value(2)

    # Export data to a Markdown file
    vo.export("output.md")
    """

    def __init__(self, data, variables=None):
        """
        Initializes the VariableOccurrences object.

        Parameters:
        - data:
            - For single variable: dict with scopes as keys and lists of values or a single value.
            - For multiple variables: dict mapping variable names to their respective scope data.
        - variables (str or list/tuple, optional):
            - If single variable: string representing the variable name.
            - If multiple variables: list or tuple of variable names.
            - If None: assumes data is for multiple variables without specifying names.
        """
        if variables is None:
            # Assume data is a dict mapping variable names to their scope data
            if not isinstance(data, dict):
                raise ValueError("For multiple variables, data must be a dict mapping variable names to their scope data.")
            self.variables = list(data.keys())
            self.data = data  # {var1: {scope1: [...], scope2: [...]}, var2: {...}, ...}
        elif isinstance(variables, str):
            # Single variable case
            self.variables = [variables]
            if not isinstance(data, dict):
                raise ValueError("For single variable, data must be a dict with scopes as keys and lists of values or single values as values.")
            self.data = {variables: data}  # {var: {scope1: [...], scope2: [...], ...}}
        elif isinstance(variables, (list, tuple)):
            # Multiple variables specified
            if not isinstance(data, dict):
                raise ValueError("For multiple variables, data must be a dict mapping variable names to their scope data.")
            if set(variables) > set(data.keys()):
                missing_vars = set(variables) - set(data.keys())
                raise ValueError(f"Data does not contain entries for variables: {', '.join(missing_vars)}")
            self.variables = list(variables)
            self.data = {var: data[var] for var in variables}  # {var1: {...}, var2: {...}, ...}
        else:
            raise ValueError("Parameter 'variables' must be a string, list, tuple, or None.")

        self.scopes = self._determine_scopes()

    def _determine_scopes(self):
        """Determines the unique scopes present across all variables."""
        scopes = set()
        for var in self.variables:
            var_scopes = self.data[var].keys()
            scopes.update(scope.lower() for scope in var_scopes)
        return sorted(list(scopes))

    def get_raw_data(self):
        """
        Returns the raw data.

        Returns:
        - The raw data as provided during initialization.
        """
        return self.data

    def get_steps_with_value(self, value, within_list=False):
        """
        Retrieves the steps where the variable equals the specified value.

        Parameters:
        - value: The value to search for.
        - within_list (bool): If True, searches for the value within list-type variable values.

        Returns:
        - If single variable:
            - A dict with scopes as keys and lists of step indices or values as values.
        - If multiple variables:
            - A dict mapping each variable name to their respective scope-step/value mappings.
        """
        result = {}
        for var in self.variables:
            var_result = {}
            for scope, occurrences in self.data[var].items():
                if is_scalar(occurrences):
                    # Scalar value
                    if within_list:
                        continue  # Cannot search within a scalar
                    if occurrences == value:
                        var_result[scope] = occurrences
                elif isinstance(occurrences, list):
                    # List of values or list of tuples
                    steps = []
                    for item in occurrences:
                        if isinstance(item, tuple) and len(item) == 2:
                            step, val = item
                            if within_list:
                                if isinstance(val, list) and value in val:
                                    steps.append(step)
                            else:
                                if val == value:
                                    steps.append(step)
                        else:
                            # List of values
                            if within_list:
                                if isinstance(item, list) and value in item:
                                    steps.append(item)
                            else:
                                if item == value:
                                    steps.append(item)
                    if steps:
                        var_result[scope] = steps
                else:
                    # Other types (e.g., dict), can be extended as needed
                    pass
            if var_result:
                result[var] = var_result
        return result

    def get_all_values(self):
        """
        Retrieves all unique values of the variable(s).

        Returns:
        - A dict mapping each variable to its set of unique values per scope.
        """
        result = {}
        for var in self.variables:
            var_unique = {}
            for scope, occurrences in self.data[var].items():
                unique_vals = set()
                if is_scalar(occurrences):
                    unique_vals.add(occurrences)
                elif isinstance(occurrences, list):
                    for item in occurrences:
                        if isinstance(item, tuple) and len(item) == 2:
                            _, val = item
                            hashable_val = make_hashable(val)
                            unique_vals.add(hashable_val)
                        else:
                            hashable_val = make_hashable(item)
                            unique_vals.add(hashable_val)
                var_unique[scope] = unique_vals
            result[var] = var_unique
        return result

    def get_all_elements_in_lists(self):
        """
        Retrieves all unique elements within list-type variable values.

        Returns:
        - A dict mapping each variable to its set of unique elements in lists per scope.
        """
        result = {}
        for var in self.variables:
            var_elements = {}
            for scope, occurrences in self.data[var].items():
                unique_elems = set()
                if is_scalar(occurrences):
                    if isinstance(occurrences, list):
                        unique_elems.update(occurrences)
                elif isinstance(occurrences, list):
                    for item in occurrences:
                        if isinstance(item, tuple) and len(item) == 2:
                            _, val = item
                            if isinstance(val, list):
                                unique_elems.update(val)
                        elif isinstance(item, list):
                            unique_elems.update(item)
                var_elements[scope] = unique_elems
            result[var] = var_elements
        return result

    def get_usage_count(self, value, within_list=False):
        """
        Counts how many times a specific value is used.

        Parameters:
        - value: The value to count.
        - within_list (bool): If True, counts occurrences within list-type variable values.

        Returns:
        - If single variable:
            - A dict with scopes as keys and integer counts or counts of values as values.
        - If multiple variables:
            - A dict mapping each variable name to their respective scope-count/value mappings.
        """
        result = {}
        for var in self.variables:
            var_count = {}
            for scope, occurrences in self.data[var].items():
                count = 0
                if is_scalar(occurrences):
                    if within_list:
                        if isinstance(occurrences, list) and value in occurrences:
                            count += 1
                    else:
                        if occurrences == value:
                            count += 1
                elif isinstance(occurrences, list):
                    for item in occurrences:
                        if isinstance(item, tuple) and len(item) == 2:
                            _, val = item
                            if within_list:
                                if isinstance(val, list) and value in val:
                                    count += 1
                            else:
                                if val == value:
                                    count += 1
                        else:
                            if within_list:
                                if isinstance(item, list) and value in item:
                                    count += 1
                            else:
                                if item == value:
                                    count += 1
                if count > 0:
                    var_count[scope] = count
            if var_count:
                result[var] = var_count
        return result

    def get_steps_with_value_in_scope(self, variable, scope, value, within_list=False):
        """
        Retrieves the steps within a specific scope where the variable equals the specified value.

        Parameters:
        - variable (str): The variable name.
        - scope (str): The scope to search within ("static", "global", "local").
        - value: The value to search for.
        - within_list (bool): If True, searches for the value within list-type variable values.

        Returns:
        - A list of step indices or values where the variable equals the value within the specified scope.
        """
        if variable not in self.variables:
            raise ValueError(f"Variable '{variable}' is not managed by this instance.")

        scope = scope.lower()
        occurrences = self.data[variable].get(scope, [])
        steps = []

        if is_scalar(occurrences):
            if within_list:
                if isinstance(occurrences, list) and value in occurrences:
                    steps.append(occurrences)
            else:
                if occurrences == value:
                    steps.append(occurrences)
        elif isinstance(occurrences, list):
            for item in occurrences:
                if isinstance(item, tuple) and len(item) == 2:
                    step, val = item
                    if within_list:
                        if isinstance(val, list) and value in val:
                            steps.append(step)
                    else:
                        if val == value:
                            steps.append(step)
                else:
                    if within_list:
                        if isinstance(item, list) and value in item:
                            steps.append(item)
                    else:
                        if item == value:
                            steps.append(item)
        return steps

    def summarize(self):
        """
        Provides a summary of the variable occurrences.

        Returns:
        - A dict mapping each variable to their respective summaries per scope.
        """
        summary = {}
        for var in self.variables:
            var_summary = {}
            for scope, occurrences in self.data[var].items():
                unique_vals = set()
                unique_elements = set()
                value_counts = defaultdict(int)
                element_counts = defaultdict(int)

                if is_scalar(occurrences):
                    # Scalar value
                    hashable_val = make_hashable(occurrences)
                    unique_vals.add(hashable_val)
                    value_counts[hashable_val] += 1
                    if isinstance(occurrences, list):
                        unique_elements.update(occurrences)
                        for elem in occurrences:
                            element_counts[elem] += 1
                elif isinstance(occurrences, list):
                    for item in occurrences:
                        if isinstance(item, tuple) and len(item) == 2:
                            # Tuple: (step, value)
                            step, val = item
                            hashable_val = make_hashable(val)
                            unique_vals.add(hashable_val)
                            value_counts[hashable_val] += 1
                            if isinstance(val, list):
                                unique_elements.update(val)
                                for elem in val:
                                    element_counts[elem] += 1
                            elif isinstance(val, dict):
                                # Handle nested dictionaries if necessary
                                for sub_val in val.values():
                                    if isinstance(sub_val, list):
                                        unique_elements.update(sub_val)
                                        for elem in sub_val:
                                            element_counts[elem] += 1
                        else:
                            # Direct value
                            hashable_val = make_hashable(item)
                            unique_vals.add(hashable_val)
                            value_counts[hashable_val] += 1
                            if isinstance(item, list):
                                unique_elements.update(item)
                                for elem in item:
                                    element_counts[elem] += 1
                            elif isinstance(item, dict):
                                for sub_val in item.values():
                                    if isinstance(sub_val, list):
                                        unique_elements.update(sub_val)
                                        for elem in sub_val:
                                            element_counts[elem] += 1
                else:
                    # Other types can be handled here if needed
                    pass

                var_summary[scope] = {
                    "total_occurrences": len(occurrences),
                    "unique_values": unique_vals,
                    "unique_elements_in_lists": unique_elements,
                    "value_counts": dict(value_counts),
                    "element_counts_in_lists": dict(element_counts)
                }
            summary[var] = var_summary
        return summary

    def export(self, filename=None, scopes='all', variables='all', include_headers=True, return_content=False):
        """
        Exports the variable occurrences to a file or returns the content as a string.

        Parameters:
        - filename (str, optional): Path to the output file. Must end with .md, .txt, or .html. Required if return_content is False.
        - scopes (str or list/tuple, optional): 'all', a single scope string, or a list of scope strings. Defaults to 'all'.
        - variables (str or list/tuple, optional): 'all', a single variable string, or a list of variable strings. Defaults to 'all'.
        - include_headers (bool, optional): If True, includes headers in the exported content. Defaults to True.
        - return_content (bool, optional): If True, returns the content as a string instead of writing to a file. Defaults to False.

        Returns:
        - str: The exported content as a string if return_content is True.
        - None: Writes to file if return_content is False.

        Raises:
        - ValueError: If 'filename' is not provided when return_content is False.
        - ValueError: If 'scopes' or 'variables' are of incorrect types.
        """
        # Determine file extension if filename is provided
        if filename:
            _, ext = os.path.splitext(filename)
            ext = ext.lower()

            if ext in ['.md', '.txt']:
                export_format = 'markdown'
            elif ext == '.html':
                export_format = 'html'
            else:
                raise ValueError("Unsupported file extension. Supported extensions are .md, .txt, and .html.")
        elif not return_content:
            raise ValueError("Filename must be provided if return_content is False.")

        # Determine scopes
        if isinstance(scopes, str):
            if scopes.lower() == 'all':
                selected_scopes = self.scopes
            else:
                selected_scopes = [scopes.lower()]
        elif isinstance(scopes, (list, tuple)):
            scopes_lower = [s.lower() for s in scopes]
            if 'all' in scopes_lower:
                selected_scopes = self.scopes
            else:
                selected_scopes = scopes_lower
        else:
            raise ValueError("Parameter 'scopes' must be a string or a list/tuple of strings.")

        # Determine variables
        if isinstance(variables, str):
            if variables.lower() == 'all':
                selected_variables = self.variables
            else:
                if variables not in self.variables:
                    print(f"Warning: Variable '{variables}' not managed by this instance.")
                    selected_variables = []
                else:
                    selected_variables = [variables]
        elif isinstance(variables, (list, tuple)):
            variables_lower = [v for v in variables]
            selected_variables = [v for v in variables_lower if v in self.variables]
            missing_vars = set(variables_lower) - set(selected_variables)
            if missing_vars:
                print(f"Warning: Variables '{', '.join(missing_vars)}' not managed by this instance.")
        else:
            raise ValueError("Parameter 'variables' must be a string or a list/tuple of strings.")

        # Generate content
        content = ""
        if include_headers:
            if export_format == 'markdown':
                content += f"## Variable: `{self.variables[0]}`\n\n" if len(self.variables) == 1 else ""
            elif export_format == 'html':
                content += f"<h2>Variable: {self.variables[0]}</h2>\n" if len(self.variables) == 1 else ""

        summary = self.summarize()
        for var in selected_variables:
            if include_headers:
                if export_format == 'markdown':
                    content += f"### Variable: `{var}`\n\n"
                elif export_format == 'html':
                    content += f"<h3>Variable: {var}</h3>\n"
            var_summary = summary[var]
            for scope, details in var_summary.items():
                if scope not in selected_scopes:
                    continue
                if include_headers or True:
                    if export_format == 'markdown':
                        content += f"#### Scope: {scope.capitalize()}\n\n"
                    elif export_format == 'html':
                        content += f"<h4>Scope: {scope.capitalize()}</h4>\n"
                # Add content based on format
                if export_format == 'markdown':
                    content += f"- **Total Occurrences**: {details['total_occurrences']}\n"
                    unique_vals_formatted = ', '.join(map(str, details['unique_values']))
                    content += f"- **Unique Values**: {unique_vals_formatted}\n"
                    if details['unique_elements_in_lists']:
                        unique_elems_formatted = ', '.join(map(str, details['unique_elements_in_lists']))
                        content += f"- **Unique Elements in Lists**: {unique_elems_formatted}\n\n"
                        # Element Counts Table
                        content += "**Element Counts in Lists:**\n\n"
                        content += "| Element | Count |\n"
                        content += "|---------|-------|\n"
                        for elem, count in details['element_counts_in_lists'].items():
                            content += f"| {elem} | {count} |\n"
                        content += "\n"
                elif export_format == 'html':
                    content += "<ul>"
                    content += f"<li><strong>Total Occurrences</strong>: {details['total_occurrences']}</li>"
                    unique_vals_formatted = ', '.join(map(str, details['unique_values']))
                    content += f"<li><strong>Unique Values</strong>: {unique_vals_formatted}</li>"
                    if details['unique_elements_in_lists']:
                        unique_elems_formatted = ', '.join(map(str, details['unique_elements_in_lists']))
                        content += f"<li><strong>Unique Elements in Lists</strong>: {unique_elems_formatted}</li>"
                    content += "</ul>\n"

                    if details['element_counts_in_lists']:
                        content += "<h5>Element Counts in Lists:</h5>\n"
                        content += "<table>\n<tr><th>Element</th><th>Count</th></tr>\n"
                        for elem, count in details['element_counts_in_lists'].items():
                            content += f"<tr><td>{elem}</td><td>{count}</td></tr>\n"
                        content += "</table>\n"

            # Add a horizontal line between variables
            if include_headers and len(selected_variables) > 1:
                if export_format == 'markdown':
                    content += "\n---\n\n"
                elif export_format == 'html':
                    content += "<hr/>\n"

        # Handle format-specific headers
        if return_content:
            if export_format == 'markdown':
                return content
            elif export_format == 'html':
                return f"<html><head><meta charset='UTF-8'><title>Variable Report</title></head><body>{content}</body></html>"
        else:
            if not filename:
                raise ValueError("Filename must be provided if return_content is False.")

            # Determine export format based on file extension
            if ext in ['.md', '.txt']:
                export_format = 'markdown'
            elif ext == '.html':
                export_format = 'html'
            else:
                raise ValueError("Unsupported file extension. Supported extensions are .md, .txt, and .html.")

            # Prepare full content for HTML
            if export_format == 'html' and not include_headers:
                full_content = f"<!DOCTYPE html>\n<html>\n<head>\n<meta charset='UTF-8'>\n<title>Variable Report</title>\n</head>\n<body>\n{content}\n</body>\n</html>"
            else:
                full_content = content

            # Write to file
            try:
                with open(filename, 'w', encoding='utf-8') as file:
                    file.write(full_content)
                print(f"Report successfully generated at '{filename}'.")
            except Exception as e:
                raise Exception(f"Failed to write the report to '{filename}': {e}")


# %% Top generic classes for storing script data and objects
# they are not intended to be used outside script data and objects

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"


# object data (for scripts)
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


# group of script objects  (special kind of list)
class scriptobjectgroup(struct):
    """
    scriptobjectgroup: A Class for Managing Groups of Script Objects in LAMMPS

    The `scriptobjectgroup` class is designed to represent a group of `scriptobject` instances,
    such as beads or atoms in a simulation. This class allows users to group objects together
    based on their properties (e.g., beadtype, name), and provides tools to generate scripts
    that define interactions, groups, and forcefields for these objects in LAMMPS.

    Key Features:
    -------------
    - **Group Management**: Objects can be combined into a group, where each `beadtype` occurs
      once. The class ensures that objects are uniquely identified by their `beadtype` and `name`.
    - **Dynamic Properties**: The group’s properties (e.g., `beadtype`, `name`, `groupname`)
      are dynamically calculated, ensuring that the group reflects the current state of the objects.
    - **Script Generation**: Provides methods to generate scripts based on the group's objects,
      including interaction forcefields and group definitions.
    - **Interaction Accumulation**: Automatically accumulates and updates all forcefield
      interactions for the objects in the group.

    Practical Use Cases:
    --------------------
    - **LAMMPS Group Definitions**: Define groups of objects for use in LAMMPS simulations,
      based on properties like `beadtype` and `groupname`.
    - **Forcefield Management**: Automatically manage and update interaction forcefields for
      objects in the group.
    - **Script Generation**: Generate LAMMPS-compatible scripts that include group definitions,
      input file handling, and interaction forcefields.

    Methods:
    --------
    __init__(self, *SOgroup):
        Initializes a new `scriptobjectgroup` with one or more `scriptobject` instances.

    __str__(self):
        Returns a string representation of the `scriptobjectgroup`, showing the number of objects
        in the group and their `beadtypes`.

    __add__(self, SOgroup):
        Combines two `scriptobjectgroup` instances or a `scriptobject` with an existing group,
        ensuring that `beadtype` values are unique.

    __or__(self, pipe):
        Overloads the pipe (`|`) operator to integrate the group into a pipeline.

    select(self, beadtype=None):
        Selects and returns a subset of the group based on the specified `beadtype`.

    script(self, printflag=False, verbosity=2, verbose=None):
        Generates a script based on the current collection of objects, including input file
        handling, group definitions, and interaction forcefields.

    interactions(self, printflag=False, verbosity=2, verbose=None):
        Updates and accumulates all forcefields for the objects in the group.

    group_generator(self, name=None):
        Generates and returns a `group` object, based on the existing group structure.

    Properties:
    -----------
    - list : Converts the group into a sorted list of objects.
    - zip : Returns a sorted list of tuples containing `beadtype`, `name`, `group`, and `filename`
      for each object.
    - n : Returns the number of objects in the group.
    - beadtype : Returns a list of the `beadtypes` for all objects in the group.
    - name : Returns a list of the `names` for all objects in the group.
    - groupname : Returns a list of all group names (synonyms).
    - filename : Returns a dictionary mapping filenames to the objects that use them.
    - str : Returns a string representation of the group's `beadtypes`.
    - min : Returns the minimum `beadtype` in the group.
    - max : Returns the maximum `beadtype` in the group.
    - minmax : Returns a tuple of the minimum and maximum `beadtypes` in the group.
    - forcefield : Returns the interaction forcefields for the group.

    Original Content:
    -----------------
    The `scriptobjectgroup` class enables the collection and management of multiple
    `scriptobject` instances, providing the following functionalities:
    - **Group Creation**: Groups are automatically formed by combining individual objects
      using the `+` operator. Each `beadtype` occurs only once in the group, and errors are
      raised if an object with the same `name` or `beadtype` already exists.
    - **Dynamic Properties**: Properties such as `beadtype`, `name`, `groupname`, and `filename`
      are dynamically calculated, reflecting the current state of the objects.
    - **Forcefield Handling**: Forcefields are automatically managed for the objects in the group,
      including diagonal and off-diagonal terms for pair interactions.
    - **Script Generation**: Scripts are generated to define the interactions, groups, and
      input file handling for LAMMPS.

    Example Usage:
    --------------
    ```
    from pizza.scriptobject import scriptobject, scriptobjectgroup, rigidwall, solidfood, water

    # Define some script objects
    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 objects into a group
    collection = b1 + b2 + b3 + b4

    # Select a subset of objects and generate a script
    grp_typ1 = collection.select(1)
    grpB = collection.group.B
    script12 = collection.select([1, 2]).script()
    ```

    Output:
    ```
    script object group with 4 objects (1 2 3 4)
    script
    ```

    OVERVIEW:
    --------------


        class of script object group
            script object groups are built from script objects OBJ1, OBJ2,..
            GRP = scriptobjectgroup(OBJ1,OBJ2,...)
            GRP = OBJ1+OBJ2+...

        note: each beadtype occurs once in the group (if not an error message is generated)

        List of methods
            struct() converts data as structure
            select([1,2,4]) selects objects with matching beadtypes

        List of properties (dynamically calculated)
            converted data: list, str, zip, beadtype, name, groupname, group, filename
            numeric: len, min, max, minmax
            forcefield related: interactions, forcefield
            script: generate the script (load,group,forcefield)

        Full syntax (toy example)

    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())

        note: beadtype are incremented during the collection (effect of order)

            # generate a collection, select a typ 1 and a subgroup, generate the script for 1,2

            collection = b1+b2+b3+b4
            grp_typ1 = collection.select(1)
            grpB = collection.group.B
            script12 = collection.select([1,2]).script

        note: collection.group.B returns a strcture with 6 fields
        -----------:----------------------------------------
            groupid: 2 <-- automatic group numbering
        groupidname: B <-- group name
          groupname: ['A', 'B', 'C', 'D', 'E'] <--- snonyms
           beadtype: [1, 2, 3] <-- beads belonging to B
               name: ['bead 1', 'bead 2', 'bead 3'] <-- their names
                str: group B 1 2 3 <-- LAMMPS syntax
        -----------:----------------------------------------

    """
    _type = "SOG"
    _fulltype = "script object group"
    _ftype = "object"
    _propertyasattribute = True

    def __init__(self,*SOgroup):
        """ SOG constructor """
        super(scriptobjectgroup,self).__init__()
        beadtypemax = 0
        names = []
        for k in range(len(SOgroup)):
            if isinstance(SOgroup[k],scriptobject):
                if SOgroup[k].beadtype<beadtypemax or SOgroup[k].beadtype==None:
                    beadtypemax +=1
                    SOgroup[k].beadtype = beadtypemax
                if SOgroup[k].name not in names:
                    self.setattr(SOgroup[k].name,SOgroup[k])
                    beadtypemax = SOgroup[k].beadtype
                else:
                    raise ValueError('the script object "%s" already exists' % SOgroup[k].name)
                names.append(SOgroup[k].name)
            else:
                raise ValueError("the argument #%d is not a script object")

    def __str__(self):
        """ string representation """
        return f"{self._fulltype} with {len(self)} {self._ftype}s ({span(self.beadtype)})"

    def __add__(self, SOgroup):
        """ overload + """
        beadlist = self.beadtype
        dup = duplicate(self)
        if isinstance(SOgroup,scriptobject):
            if SOgroup.name not in self.keys():
                if SOgroup.beadtype in beadlist and \
                  (SOgroup.beadtype==None or SOgroup.beadtype==self.min):
                      SOgroup.beadtype = self.max+1
                if SOgroup.beadtype not in beadlist:
                    dup.setattr(SOgroup.name, SOgroup)
                    beadlist.append(SOgroup.beadtype)
                    return dup
                else:
                    raise ValueError('%s (beadtype=%d) is already in use, same beadtype' \
                                     % (SOgroup.name,SOgroup.beadtype))
            else:
                raise ValueError('the object "%s" is already in the list' % SOgroup.name)
        elif isinstance(SOgroup,scriptobjectgroup):
            for k in SOgroup.keys():
                if k not in dup.keys():
                    if SOgroup.getattr(k).beadtype not in beadlist:
                        dup.setattr(k,SOgroup.getattr(k))
                        beadlist.append(SOgroup.getattr(k).beadtype)
                    else:
                        raise ValueError('%s (beadtype=%d) is already in use, same beadtype' \
                                         % (k,SOgroup.getattr(k).beadtype))
                else:
                    raise ValueError('the object "%s" is already in the list' % k)
            return dup
        else:
            raise ValueError("the argument #%d is not a script object or a script object group")

    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")

    @property
    def list(self):
        """ convert into a list """
        return sorted(self)

    @property
    def zip(self):
        """ zip beadtypes and names """
        return sorted( \
            [(self.getattr(k).beadtype,self.getattr(k).name,self.getattr(k).group,self.getattr(k).filename) \
            for k in self.keys()])

    @property
    def n(self):
        """ returns the number of bead types """
        return len(self)

    @property
    def beadtype(self):
        """ returns the beads in the group """
        return [x for x,_,_,_ in self.zip]

    @property
    def name(self):
        """ "return the list of names """
        return [x for _,x,_,_ in self.zip]

    @property
    def groupname(self):
        """ "return the list of groupnames """
        grp = []
        for _,_,glist,_ in self.zip:
            for g in glist:
                if g not in grp: grp.append(g)
        return grp

    @property
    def filename(self):
        """ "return the list of names as a dictionary """
        files = {}
        for _,n,_,fn in self.zip:
            if fn != "":
                if fn not in files:
                    files[fn] = [n]
                else:
                    files[fn].append(n)
        return files

    @property
    def str(self):
        return span(self.beadtype)

    def struct(self,groupid=1,groupidname="undef"):
        """ create a group with name """
        return struct(
                groupid = groupid,
            groupidname = groupidname,
              groupname = self.groupname, # meaning is synonyms
               beadtype = self.beadtype,
                   name = self.name,
                    str = "group %s %s" % (groupidname, span(self.beadtype))
               )

    @property
    def minmax(self):
        """ returns the min,max of beadtype """
        return self.min,self.max

    @property
    def min(self):
        """ returns the min of beadtype """
        return min(self.beadtype)

    @property
    def max(self):
        """ returns the max of beadtype """
        return max(self.beadtype)

    def select(self,beadtype=None):
        """ select bead from a keep beadlist """
        if beadtype==None: beadtype = list(range(self.min,self.max+1))
        if not isinstance(beadtype,(list,tuple)): beadtype = [beadtype]
        dup = scriptobjectgroup()
        for b,n,_,_ in self.zip:
            if b in beadtype:
                dup = dup + self.getattr(n)
                dup.getattr(n).USER = self.getattr(n).USER
                dup.getattr(n).forcefield = self.getattr(n).forcefield
        return dup

    @property
    def group(self):
        """ build groups from group (groupname contains synonyms) """
        groupdef = struct()
        gid = 0
        bng = self.zip
        for g in self.groupname:
            gid +=1
            b =[x for x,_,gx,_ in bng if g in gx]
            groupdef.setattr(g,self.select(b).struct(groupid = gid, groupidname = g))
        return groupdef

    @CallableScript
    def interactions(self, printflag=False, verbosity=2, verbose=None):
        """ update and accumulate all forcefields """
        verbosity = 0 if verbose is False else verbosity
        FF = []
        for b in self.beadtype:
            selection = deepduplicate(self.select(b)[0])
            selection.forcefield.beadtype = selection.beadtype
            selection.forcefield.userid = selection.name
            FF.append(selection.forcefield)
        # initialize interactions with pair_style
        TEMPLATE = "\n# ===== [ BEGIN FORCEFIELD SECTION ] "+"="*80 if verbosity>0 else ""
        TEMPLATE = FF[0].pair_style(verbose=verbosity>0)
        # pair diagonal terms
        for i in range(len(FF)):
            TEMPLATE += FF[i].pair_diagcoeff(verbose=verbosity>0)
        # pair off-diagonal terms
        for j in range(1,len(FF)):
            for i in range(0,j):
                TEMPLATE += FF[i].pair_offdiagcoeff(o=FF[j],verbose=verbosity>0)
        # end
        TEMPLATE += "\n# ===== [ END FORCEFIELD SECTION ] "+"="*82+"\n"  if verbosity>0 else ""
        return FF,TEMPLATE

    @property
    def forcefield(self):
        """ interaction forcefields """
        FF,_ = self.interactions
        return FF

    @CallableScript
    def script(self, printflag=False, verbosity=None, verbose=None):
        """
            Generate a script based on the current collection of script objects

            Parameters:
            -----------
            printflag : bool, optional, default=False
                If True, prints the generated script.
            verbosity (int, optional): Controls the level of detail in the generated script.
                - 0: Minimal output, no comments.
                - 1: Basic comments for run steps.
                - 2: Detailed comments with additional information.
                Default is 2

            Returns:
            --------
            script
                The generated script describing the interactions between script objects.
        """
        printflag = self.printflag if printflag is None else printflag
        verbose = verbosity > 0 if verbosity is not None else (self.verbose if verbose is None else verbose)
        verbosity = 0 if verbose is False else verbosity
        TEMPFILES = ""
        isfirst = True
        files_added = False
        if self.filename:
            for fn, cfn in self.filename.items():
                if fn and cfn:
                    if not files_added:
                        files_added = True
                        TEMPFILES += "\n# ===== [ BEGIN INPUT FILES SECTION ] " + "=" * 79 + "\n" if verbosity>0 else ""
                    TEMPFILES += span(cfn, sep=", ", left="\n# load files for objects: ", right="\n") if verbosity>1 else ""
                    if isfirst:
                        isfirst = False
                        TEMPFILES += f"\tread_data {fn}\n"  # First file, no append
                    else:
                        TEMPFILES += f"\tread_data {fn} add append\n"  # Subsequent files, append
        # define groups
        TEMPGRP = "\n# ===== [ BEGIN GROUP SECTION ] "+"="*85 + "\n" if verbosity>0 else ""
        for g in self.group:
            TEMPGRP += f'\n\t#\tDefinition of group {g.groupid}:{g.groupidname}\n' if verbosity>1 else ""
            TEMPGRP += f'\t#\t={span(g.name,sep=", ")}\n' if verbosity>1 else ""
            TEMPGRP += f'\t#\tSimilar groups: {span(g.groupname,sep=", ")}\n' if verbosity>1 else ""
            TEMPGRP += f'\tgroup \t {g.groupidname} \ttype \t {span(g.beadtype)}\n'
        TEMPGRP += "\n# ===== [ END GROUP SECTION ] "+"="*87+"\n\n" if verbosity>0 else ""
        # define interactions
        _,TEMPFF = self.interactions(printflag=printflag, verbosity=verbosity)
        # chain strings into a script
        tscript = script(printflag=False,verbose=verbosity>1)
        tscript.name = "scriptobject script"        # name
        tscript.description = str(self)             # description
        tscript.userid = "scriptobject"             # user name
        tscript.TEMPLATE = TEMPFILES+TEMPGRP+TEMPFF
        if verbosity==0:
            tscript.TEMPLATE = remove_comments(tscript.TEMPLATE)
        if printflag:
            repr(tscript)
        return tscript

    def group_generator(self, name=None):
        """
        Generate and return a group object.

        This method creates a new `group` object, optionally with a specified name.
        If no name is provided, it generates a default name based on the current
        instance's `name` attribute, formatted with the `span` function. The method
        then iterates through the existing groups in `self.group`, adding each group
        to the new `group` object based on its `groupidname` and `beadtype`.

        Parameters:
        -----------
        name : str, optional
            The name for the generated group object. If not provided, a default name
            is generated based on the current instance's `name`.

        Returns:
        --------
        group
            A newly created `group` object with criteria set based on the existing groups.
        """
        from pizza.group import group
        # Use the provided name or generate a default name using the span function
        G = group(name=name if name is not None else span(self.name, ",", "[", "]"))
        # Add criteria for each group in self.group
        for g in self.group:
            G.add_group_criteria(g.groupidname, type=g.beadtype)
        return G


    def mass(self, name=None, default_mass="${mass}", printflag=False, verbosity=2, verbose=True):
        """
        Generates LAMMPS mass commands for each unique beadtype in the collection.

        The method iterates through all `scriptobjectgroup` instances in the collection,
        collects unique beadtypes, and ensures that each beadtype has a consistent mass.
        If a beadtype has `mass=None`, it assigns a default mass as specified by `default_mass`.

        ### Parameters:
            name (str, optional):
                The name to assign to the resulting `script` object. Defaults to a generated name.
            default_mass (str, int, or float, optional):
                The default mass value to assign when a beadtype's mass is `None`.
                Can be a string, integer, or floating-point value. Defaults to `"${mass}"`.
            printflag (bool, optional):
                If `True`, prints the representation of the resulting `script` object. Defaults to `False`.
            verbosity (int, optional):
                The verbosity level for logging or debugging. Higher values indicate more detailed output.
                Defaults to `2`.
            verbose (bool, optional):
                If `True`, includes a comment header in the output. Overrides `verbosity` when `False`.
                Defaults to `True`.

        ### Returns:
            script: A `script` object containing the mass commands for each beadtype, formatted as follows:
                     ```
                     mass 1 1.0
                     mass 2 ${mass}
                     mass 3 2.5
                     ```
                     The `TEMPLATE` attribute of the `script` object holds the formatted mass commands as a single string.

        ### Raises:
            ValueError: If a beadtype has inconsistent mass values across different `scriptobjectgroup` instances.

        ### Example:
            ```python
            # Create scriptobjectgroup instances
            obj1 = scriptobjectgroup(beadtype=1, group=["all", "A"], mass=1.0)
            obj2 = scriptobjectgroup(beadtype=2, group=["all", "B", "C"])
            obj3 = scriptobjectgroup(beadtype=3, group="C", mass=2.5)

            # Initialize a script group with the scriptobjectgroup instances
            G = scriptobjectgroup([obj1, obj2, obj3])

            # Generate mass commands
            M = G.mass()
            print(M.do())
            ```

            **Output:**
            ```
            # <script:group:mass> definitions for 3 beads
            mass 1 1.0
            mass 2 ${mass}
            mass 3 2.5
            ```
        """
        verbosity = 0 if verbose is False else verbosity
        beadtype_mass = {}
        for iobj in range(0,len(self)):
            obj = self[iobj]
            bt = obj.beadtype
            mass = obj.mass if obj.mass is not None else default_mass
            if bt in beadtype_mass:
                if beadtype_mass[bt] != mass:
                    raise ValueError(
                        f"Inconsistent masses for beadtype {bt}: {beadtype_mass[bt]} vs {mass}"
                    )
            else:
                beadtype_mass[bt] = mass
        # Sort beadtypes for consistent ordering
        sorted_beadtypes = sorted(beadtype_mass.keys())
        # Generate mass commands
        lines = [f"mass {bt} {beadtype_mass[bt]}" for bt in sorted_beadtypes]
        # return a script object
        nameid = f"<script:group:{self.name}:mass>"
        description = f"{nameid} definitions for {len(self)} beads"
        if verbose:
            lines.insert(0, "# "+description)
        mscript = script(printflag=False,verbose=verbosity>1)
        mscript.name = nameid if name is None else name
        mscript.description = description
        mscript.userid = "scriptobject"             # user name
        mscript.TEMPLATE = "\n".join(lines)
        mscript.DEFINITIONS.mass = default_mass
        if printflag:
            repr(mscript)
        return mscript


# %% script core class
# note: please derive this class when you use it, do not alter it
class script:
    """
    script: A Core Class for Flexible LAMMPS Script Generation

    The `script` class provides a flexible framework for generating dynamic LAMMPS
    script sections. It supports various LAMMPS sections such as "GLOBAL", "INITIALIZE",
    "GEOMETRY", "INTERACTIONS", and more, while allowing users to define custom sections
    with variable definitions, templates, and dynamic evaluation of script content.

    Key Features:
    -------------
    - **Dynamic Script Generation**: Easily define and manage script sections,
      using templates and definitions to dynamically generate LAMMPS-compatible scripts.
    - **Script Concatenation**: Combine multiple script sections while managing
      variable precedence and ensuring that definitions propagate as expected.
    - **Flexible Variable Management**: Separate `DEFINITIONS` for static variables and
      `USER` for user-defined variables, with clear rules for inheritance and precedence.
    - **Operators for Advanced Script Handling**: Use `+`, `&`, `>>`, `|`, and `**` operators
      for script merging, static execution, right-shifting of definitions, and more.
    - **Pipeline Support**: Integrate scripts into pipelines, with full support for
      staged execution, variable inheritance, and reordering of script sections.

    Practical Use Cases:
    --------------------
    - **LAMMPS Automation**: Automate the generation of complex LAMMPS scripts by defining
      reusable script sections with variables and templates.
    - **Multi-Step Simulations**: Manage multi-step simulations by splitting large scripts
      into smaller, manageable sections and combining them as needed.
    - **Advanced Script Control**: Dynamically modify script behavior by overriding variables
      or using advanced operators to concatenate, pipe, or merge scripts.

    Methods:
    --------
    __init__(self, persistentfile=True, persistentfolder=None, printflag=False, verbose=False, **userdefinitions):
        Initializes a new `script` object, with optional user-defined variables
        passed as `userdefinitions`.

    do(self, printflag=None, verbose=None):
        Generates the LAMMPS script based on the current configuration, evaluating
        templates and definitions to produce the final output.

    script(self, idx=None, printflag=True, verbosity=2, verbose=None, forced=False):
        Generate the final LAMMPS script from the pipeline or a subset of the pipeline.

    add(self, s):
        Overloads the `+` operator to concatenate script objects, merging definitions
        and templates while maintaining variable precedence.

    and(self, s):
        Overloads the `&` operator for static execution, combining the generated scripts
        of two script objects without merging their definitions.

    __mul__(self, ntimes):
        Overloads the `*` operator to repeat the script `ntimes`, returning a new script
        object with repeated sections.

    __pow__(self, ntimes):
        Overloads the `**` operator to concatenate the script with itself `ntimes`,
        similar to the `&` operator, but repeated.

    __or__(self, pipe):
        Overloads the pipe (`|`) operator to integrate the script into a pipeline,
        returning a `pipescript` object.

    write(self, file, printflag=True, verbose=False):
        Writes the generated script to a file, including headers with metadata.

    tmpwrite(self):
        Writes the script to a temporary file, creating both a full version and a clean
        version without comments.

    printheader(txt, align="^", width=80, filler="~"):
        Static method to print formatted headers, useful for organizing output.

    __copy__(self):
        Creates a shallow copy of the script object.

    __deepcopy__(self, memo):
        Creates a deep copy of the script object, duplicating all internal variables.

    Additional Features:
    --------------------
    - **Customizable Templates**: Use string templates with variable placeholders
      (e.g., `${value}`) to dynamically generate script lines.
    - **Static and User-Defined Variables**: Manage global `DEFINITIONS` for static
      variables and `USER` variables for dynamic, user-defined settings.
    - **Advanced Operators**: Leverage a range of operators (`+`, `>>`, `|`, `&`) to
      manipulate script content, inherit definitions, and control variable precedence.
    - **Verbose Output**: Control verbosity to include detailed comments and debugging
      information in generated scripts.

    Original Content:
    -----------------
    The `script` class supports LAMMPS section generation and variable management with
    features such as:
    - **Dynamic Evaluation of Scripts**: Definitions and templates are evaluated at runtime,
      allowing for flexible and reusable scripts.
    - **Inheritance of Definitions**: Variable definitions can be inherited from previous
      sections, allowing for modular script construction.
    - **Precedence Rules for Variables**: When scripts are concatenated, definitions from
      the left take precedence, ensuring that the first defined values are preserved.
    - **Instance and Global Variables**: Instance variables are set via the `USER` object,
      while global variables (shared across instances) are managed in `DEFINITIONS`.
    - **Script Pipelines**: Scripts can be integrated into pipelines for sequential execution
      and dynamic variable propagation.
    - **Flexible Output Formats**: Lists are expanded into space-separated strings, while
      tuples are expanded with commas, making the output more readable.

    Example Usage:
    --------------
    ```
    from pizza.script import script, scriptdata

    class example_section(script):
        DEFINITIONS = scriptdata(
            X = 10,
            Y = 20,
            result = "${X} + ${Y}"
        )
        TEMPLATE = "${result} = ${X} + ${Y}"

    s1 = example_section()
    s1.USER.X = 5
    s1.do()
    ```

    The output for `s1.do()` will be:
    ```
    25 = 5 + 20
    ```

    With additional sections, scripts can be concatenated and executed as a single
    entity, with inheritance of variables and customizable behavior.


        --------------------------------------
           OVERVIEW ANDE DETAILED FEATURES
        --------------------------------------

        The class script enables to generate dynamically LAMMPS sections
        "NONE","GLOBAL","INITIALIZE","GEOMETRY","DISCRETIZATION",
        "BOUNDARY","INTERACTIONS","INTEGRATION","DUMP","STATUS","RUN"


        # %% This the typical construction for a class
        class XXXXsection(script):
            "" " LAMMPS script: XXXX session "" "
            name = "XXXXXX"
            description = name+" section"
            position = 0
            section = 0
            userid = "example"
            version = 0.1

            DEFINITIONS = scriptdata(
                 value= 1,
            expression= "${value+1}",
                  text= "$my text"
                )

            TEMPLATE = "" "
        # :UNDEF SECTION:
        #   to be defined
        LAMMPS code with ${value}, ${expression}, ${text}
            "" "

        DEFINTIONS can be inherited from a previous section
        DEFINITIONS = previousection.DEFINTIONS + scriptdata(
                 value= 1,
            expression= "${value+1}",
                  text= "$my text"
            )


        Recommandation: Split a large script into a small classes or actions
        An example of use could be:
            move1 = translation(displacement=10)+rotation(angle=30)
            move2 = shear(rate=0.1)+rotation(angle=20)
            bigmove = move1+move2+move1
            script = bigmove.do() generates the script

        NOTE1: Use the print() and the method do() to get the script interpreted

        NOTE2: DEFINITIONS can be pretified using DEFINITIONS.generator()

        NOTE3: Variables can extracted from a template using TEMPLATE.scan()

        NOTE4: Scripts can be joined (from top down to bottom).
        The first definitions keep higher precedence. Please do not use
        a variable twice with different contents.

        myscript = s1 + s2 + s3 will propagate the definitions
        without overwritting previous values). myscript will be
        defined as s1 (same name, position, userid, etc.)

        myscript += s appends the script section s to myscript

        NOTE5: rules of precedence when script are concatenated
        The attributes from the class (name, description...) are kept from the left
        The values of the right overwrite all DEFINITIONS

        NOTE6: user variables (instance variables) can set with USER or at the construction
        myclass_instance = myclass(myvariable = myvalue)
        myclass_instance.USER.myvariable = myvalue

        NOTE7: how to change variables for all instances at once?
        In the example below, let x is a global variable (instance independent)
        and y a local variable (instance dependent)
        instance1 = myclass(y=1) --> y=1 in instance1
        instance2 = myclass(y=2) --> y=2 in instance2
        instance3.USER.y=3 --> y=3 in instance3
        instance1.DEFINITIONS.x = 10 --> x=10 in all instances (1,2,3)

        If x is also defined in the USER section, its value will be used
        Setting instance3.USER.x = 30 will assign x=30 only in instance3

        NOTE8: if a the script is used with different values for a same parameter
        use the operator & to concatenate the results instead of the script
        example: load(file="myfile1") & load(file="myfile2) & load(file="myfile3")+...

        NOTE9: lists (e.g., [1,2,'a',3] are expanded ("1 2 a 3")
               tuples (e.g. (1,2)) are expanded ("1,2")
               It is easier to read ["lost","ignore"] than "$ lost ignore"

        NOTE 10: New operators >> and || extend properties
            + merge all scripts but overwrite definitions
            & execute statically script content
            >> pass only DEFINITIONS but not TEMPLATE to the right
            | pipe execution such as in Bash, the result is a pipeline

        NOTE 11: Scripts in pipelines are very flexible, they support
        full indexing à la Matlab, including staged executions
            method do(idx) generates the script corresponding to indices idx
            method script(idx) generates the corresponding script object

        --------------------------[ FULL EXAMPLE ]-----------------------------

        # Import the class
        from pizza.script import *

        # Override the class globalsection
        class scriptexample(globalsection):
            description = "demonstrate commutativity of additions"
            verbose = True

            DEFINITIONS = scriptdata(
                X = 10,
                Y = 20,
                R1 = "${X}+${Y}",
                R2 = "${Y}+${X}"
                )
            TEMPLATE = "" "
            # Property of the addition
            ${R1} = ${X} + ${Y}
            ${R2} = ${Y} + ${X}
         "" "

        # derived from scriptexample, X and Y are reused
        class scriptexample2(scriptexample):
            description = "demonstrate commutativity of multiplications"
            verbose = True
            DEFINITIONS = scriptexample.DEFINITIONS + scriptdata(
                R3 = "${X} * ${Y}",
                R4 = "${Y} * ${X}",
                )
            TEMPLATE = "" "
            # Property of the multiplication
            ${R3} = ${X} * ${Y}
            ${R4} = ${Y} * ${X}
         "" "

        # call the first class and override the values X and Y
        s1 = scriptexample()
        s1.USER.X = 1  # method 1 of override
        s1.USER.Y = 2
        s1.do()
        # call the second class and override the values X and Y
        s2 = scriptexample2(X=1000,Y=2000) # method 2
        s2.do()
        # Merge the two scripts
        s = s1+s2
        print("this is my full script")
        s.description
        s.do()

        # The result for s1 is
            3 = 1 + 2
            3 = 2 + 1
        # The result for s2 is
            2000000 = 1000 * 2000
            2000000 = 2000 * 1000
        # The result for s=s1+s2 is
            # Property of the addition
            3000 = 1000 + 2000
            3000 = 2000 + 1000
            # Property of the multiplication
            2000000 = 1000 * 2000
            2000000 = 2000 * 1000

    """

    # metadata
    metadata = get_metadata()               # retrieve all metadata

    type = "script"                         # type (class name)
    name = "empty script"                   # name
    description = "it is an empty script"   # description
    position = 0                            # 0 = root
    section = 0                             # section (0=undef)
    userid = "undefined"                    # user name
    version = metadata["version"]           # version
    license = metadata["license"]
    email = metadata["email"]               # email

    verbose = False                         # set it to True to force verbosity
    _contact = ("INRAE\SAYFOOD\olivier.vitrac@agroparistech.fr",
                "INRAE\SAYFOOD\william.jenkinson@agroparistech.fr",
                "INRAE\SAYFOOD\han.chen@inrae.fr")

    SECTIONS = ["NONE","GLOBAL","INITIALIZE","GEOMETRY","DISCRETIZATION",
                "BOUNDARY","INTERACTIONS","INTEGRATION","DUMP","STATUS","RUN"]

    # Main class variables
    # These definitions are for instances
    DEFINITIONS = scriptdata()
    TEMPLATE = """
        # empty LAMMPS script
    """

    # constructor
    def __init__(self,persistentfile=True,
                 persistentfolder = None,
                 printflag = False,
                 verbose = False,
                 verbosity = None,
                 **userdefinitions):
        """ constructor adding instance definitions stored in USER """
        if persistentfolder is None: persistentfolder = get_tmp_location()
        self.persistentfile = persistentfile
        self.persistentfolder = persistentfolder
        self.printflag = printflag
        self.verbose = verbose if verbosity is None else verbosity>0
        self.verbosity = 0 if not verbose else verbosity
        self.USER = scriptdata(**userdefinitions)

    # 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)))

    # String representation
    def __str__(self):
        """ string representation """
        return f"{self.type}:{self.name}:{self.userid}"

    # Display/representation method
    def __repr__(self):
        """ disp method """
        stamp = str(self)
        self.printheader(f"{stamp} | version={self.version}",filler="/")
        self.printheader("POSITION & DESCRIPTION",filler="-",align=">")
        print(f"     position: {self.position}")
        print(f"         role: {self.role} (section={self.section})")
        print(f"  description: {self.description}")
        self.printheader("DEFINITIONS",filler="-",align=">")
        if len(self.DEFINITIONS)<15:
            self.DEFINITIONS.__repr__()
        else:
            print("too many definitions: ",self.DEFINITIONS)
        if self.verbose:
            self.printheader("USER",filler="-",align=">")
            self.USER.__repr__()
            self.printheader("TEMPLATE",filler="-",align=">")
            print(self.TEMPLATE)
            self.printheader("SCRIPT",filler="-",align=">")
        print(self.do(printflag=False))
        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))}

    # Generate the script
    def do(self,printflag=None,verbose=None):
        """
        Generate the LAMMPS script based on the current configuration.

        This method generates a LAMMPS-compatible script from the templates and definitions
        stored in the `script` object. The generated script can be displayed, returned,
        and optionally include comments for debugging or clarity.

        Parameters:
        -----------
        - printflag (bool, optional): If True, the generated script is printed to the console.
                                      Default is True.
        - verbose (bool, optional): If True, comments and additional information are included
                                    in the generated script. If False, comments are removed.
                                    Default is True.

        Returns:
        ---------
        - str: The generated LAMMPS script.

        Method Behavior:
        - The method first collects all key-value pairs from `DEFINITIONS` and `USER` objects,
          which store the configuration data for the script.
        - Lists and tuples in the collected data are formatted into a readable string with proper
          separators (space for lists, comma for tuples) and prefixed with a '%' to indicate comments.
        - The generated command template is formatted and evaluated using the collected data.
        - If `verbose` is set to False, comments in the generated script are removed.
        - The script is then printed if `printflag` is True.
        - Finally, the formatted script is returned as a string.


        Example Usage:
        --------------
        >>> s = script()
        >>> s.do(printflag=True, verbose=True)
        units           si
        dimension       3
        boundary        f f f
        # Additional script commands...

        >>> s.do(printflag=False, verbose=False)
        'units si\ndimension 3\nboundary f f f\n# Additional script commands...'

        Notes:
        - Comments are indicated in the script with '%' or '#'.
        - The [position {self.position}:{self.userid}] marker is inserted for tracking
          script sections or modifications.


        Known Issues for indexed variables
        ----------------------------------
        List and tupples used indexed in templates, such as varlist[1], are not converted into strings so that
        each element can be considered as a variable and accessed as ${varlist[1]}. If varlist is also used in
        full, such as ${varlist}. Then it is preferable to define varlist as a string:
            "![v1,v2,...]" where v1,v2,v3 can be int, float, static str or evaluable str (i.e., subjected to nested
             evaluation).
        The control character ! in lists, such as in var=![v1,v2,v3] preserves the Pythonic definition by do()
        at later stages during the evaluation so that its content can be indexed.

        """
        printflag = self.printflag if printflag is None else printflag
        verbose = self.verbose if verbose is None else verbose
        inputs = self.DEFINITIONS + self.USER
        usedvariables = self.detect_variables(with_index=False,only_indexed=False)
        variables_used_with_index = self.detect_variables(with_index=False,only_indexed=True)
        usedvariables_withoutindex = [ var for var in usedvariables if var not in variables_used_with_index ]
        for k in inputs.keys():
            if k in usedvariables_withoutindex:
                if isinstance(inputs.getattr(k),list):
                    inputs.setattr(k,"% "+span(inputs.getattr(k)))
                elif isinstance(inputs.getattr(k),tuple):
                    inputs.setattr(k,"% "+span(inputs.getattr(k),sep=","))
        cmd = inputs.formateval(self.TEMPLATE)
        cmd = cmd.replace("[comment]",f"[position {self.position}:{self.userid}]")
        if not verbose: cmd=remove_comments(cmd)
        if printflag: print(cmd)
        return cmd

    # Return the role of the script (based on section)
    @property
    def role(self):
        """ convert section index into a role (section name) """
        if self.section in range(len(self.SECTIONS)):
            return self.SECTIONS[self.section]
        else:
            return ""

    # override +
    def __add__(self,s):
        """ overload addition operator """
        from pizza.dscript import dscript
        from pizza.group import group, groupcollection
        from pizza.region import region
        if isinstance(s,script):
            dup = duplicate(self)
            dup.DEFINITIONS = dup.DEFINITIONS + s.DEFINITIONS
            dup.USER = dup.USER + s.USER
            dup.TEMPLATE = "\n".join([dup.TEMPLATE,s.TEMPLATE])
            return dup
        elif isinstance(s,pipescript):
            return pipescript(self, printflag=s.printflag,verbose=s.verbose,verbosity=s.verbosity) | s
        elif isinstance(s,dscript):
            return self + s.script(printflag=s.printflag,verbose=s.verbose,verbosity=s.verbosity)
        elif isinstance(s, scriptobjectgroup):
            return self + s.script(printflag=self.printflag,verbose=self.verbose,verbosity=self.verbosity)
        elif isinstance(s, group):
            return self + s.script(printflag=s.printflag,verbose=s.verbose,verbosity=s.verbosity)
        elif isinstance(s, groupcollection):
            return self + s.script(printflag=self.printflag,verbose=self.verbose,verbosity=self.verbosity)
        elif isinstance(s,region):
            return self + s.script(printflag=s.printflag,verbose=s.verbose,verbosity=s.verbosity)
        raise TypeError(f"the second operand in + must a script, pipescript, scriptobjectgroup,\n group, groupcollection or region object not {type(s)}")

    # override +=
    def _iadd__(self,s):
        """ overload addition operator """
        if isinstance(s,script):
            self.DEFINITIONS = self.DEFINITIONS + s.DEFINITIONS
            self.USER = self.USER + s.USER
            self.TEMPLATE = "\n".join([self.TEMPLATE,s.TEMPLATE])
        else:
            raise TypeError("the second operand must a script object")

    # override >>
    def __rshift__(self,s):
        """ overload right  shift operator (keep only the last template) """
        if isinstance(s,script):
            dup = duplicate(self)
            dup.DEFINITIONS = dup.DEFINITIONS + s.DEFINITIONS
            dup.USER = dup.USER + s.USER
            dup.TEMPLATE = s.TEMPLATE
            return dup
        else:
            raise TypeError(f"the second operand in >> must a script object not {type(s)}")

    # override &
    def __and__(self,s):
        """ overload and operator """
        if isinstance(s,script):
            dup = duplicate(self)
            dup.TEMPLATE = "\n".join([self.do(printflag=False,verbose=False),s.do(printflag=False,verbose=False)])
            return dup
        raise TypeError(f"the second operand in & must a script object not {type(s)}")

    # override *
    def __mul__(self,ntimes):
        """ overload * operator """
        if isinstance(ntimes, int) and ntimes>0:
           res = duplicate(self)
           if ntimes>1:
               for n in range(1,ntimes): res += self
           return res
        raise ValueError("multiplicator should be a strictly positive integer")

    # override **
    def __pow__(self,ntimes):
        """ overload ** operator """
        if isinstance(ntimes, int) and ntimes>0:
           res = duplicate(self)
           if ntimes>1:
               for n in range(1,ntimes): res = res & self
           return res
        raise ValueError("multiplicator should be a strictly positive integer")

    # pipe scripts
    def __or__(self,pipe):
        """ overload | or for pipe """
        from pizza.dscript import dscript
        from pizza.group import group, groupcollection
        from pizza.region import region
        if isinstance(pipe, dscript):
            rightarg = pipe.pipescript(printflag=pipe.printflag,verbose=pipe.verbose,verbosity=pipe.verbosity)
        elif isinstance(pipe,group):
            rightarg = pipe.script(printflag=pipe.printflag,verbose=pipe.verbose,verbosity=pipe.verbosity)
        elif isinstance(pipe,groupcollection):
            rightarg = pipe.script(printflag=self.printflag,verbose=self.verbose,verbosity=self.verbosity)
        elif isinstance(pipe,region):
            rightarg = pipe.pscript(printflag=self.printflag,verbose=self.verbose,verbosity=self.verbosity)
        else:
            rightarg = pipe
        if isinstance(rightarg,(pipescript,script,scriptobject,scriptobjectgroup)):
            return pipescript(self,printflag=self.printflag,verbose=self.verbose,verbosity=self.verbosity) | rightarg
        else:
            raise ValueError(f"the argument in | must a pipescript, a scriptobject or a scriptobjectgroup not {type(s)}")


    def header(self, verbose=True, verbosity=None, style=2):
        """
        Generate a formatted header for the script file.

        Parameters:
            verbosity (bool, optional): If specified, overrides the instance's `verbose` setting.
                                        Defaults to the instance's `verbose`.
            style (int from 1 to 6, optional): ASCII style to frame the header (default=2)

        Returns:
            str: A formatted string representing the script's metadata and initialization details.
                 Returns an empty string if verbosity is False.

        The header includes:
            - Script version, license, and contact email.
            - User ID and the number of initialized definitions.
            - Current system user, hostname, and working directory.
            - Persistent filename and folder path.
            - Timestamp of the header generation.
        """
        verbose = verbosity > 0 if verbosity is not None else (self.verbose if verbose is None else verbose)
        verbosity = 0 if not verbose else verbosity
        if not verbose:
            return ""
        # Prepare the header content
        lines = [
            f"PIZZA.SCRIPT FILE v{script.version} | License: {script.license} | Email: {script.email}",
            "",
            f"<{str(self)}>",
            f"Initialized with {len(self.USER)} definitions | Verbosity: {verbosity}",
            f"Persistent file: \"{self.persistentfile}\" | Folder: \"{self.persistentfolder}\"",
            "",
            f"Generated on: {getpass.getuser()}@{socket.gethostname()}:{os.getcwd()}",
            f"{datetime.datetime.now().strftime('%A, %B %d, %Y at %H:%M:%S')}",
        ]
        # Use the shared method to format the header
        return frame_header(lines,style=style)


    # write file
    def write(self, file, printflag=True, verbose=False, overwrite=False, style=2):
        """
        Write the script to a file.

        Parameters:
            - file (str): The file path where the script will be saved.
            - printflag (bool): Flag to enable/disable printing of details.
            - verbose (bool): Flag to enable/disable verbose mode.
            - overwrite (bool): Whether to overwrite the file if it already exists.
            - style (int, optional):
                Defines the ASCII frame style for the header.
                Valid values are integers from 1 to 6, corresponding to predefined styles:
                    1. Basic box with `+`, `-`, and `|`
                    2. Double-line frame with `╔`, `═`, and `║`
                    3. Rounded corners with `.`, `'`, `-`, and `|`
                    4. Thick outer frame with `#`, `=`, and `#`
                    5. Box drawing characters with `┌`, `─`, and `│`
                    6. Minimalist dotted frame with `.`, `:`, and `.`
                Default is `2` (frame with rounded corners).

        Returns:
            str: The full absolute path of the file written.

        Raises:
            FileExistsError: If the file already exists and overwrite is False.
        """
        # Resolve full path
        full_path = os.path.abspath(file)
        if os.path.exists(full_path) and not overwrite:
            raise FileExistsError(f"The file '{full_path}' already exists. Use overwrite=True to overwrite it.")
        if os.path.exists(full_path) and overwrite and verbose:
            print(f"Warning: Overwriting the existing file '{full_path}'.")
        # Generate the script and write to the file
        cmd = self.do(printflag=printflag, verbose=verbose)
        with open(full_path, "w") as f:
            print(self.header(verbosity=verbose, style=style), "\n", file=f)
            print(cmd, file=f)
        # Return the full path of the written file
        return full_path

    def tmpwrite(self, verbose=False, style=1):
        """
        Write the script to a temporary file and create optional persistent copies.

        Parameters:
            verbose (bool, optional): Controls verbosity during script generation. Defaults to False.

        The method:
            - Creates a temporary file for the script, with platform-specific behavior:
                - On Windows (`os.name == 'nt'`), the file is not automatically deleted.
                - On other systems, the file is temporary and deleted upon closure.
            - Writes a header and the script content into the temporary file.
            - Optionally creates a persistent copy in the `self.persistentfolder` directory:
                - `script.preview.<suffix>`: A persistent copy of the temporary file.
                - `script.preview.clean.<suffix>`: A clean copy with comments and empty lines removed.
            - Handles cleanup and exceptions gracefully to avoid leaving orphaned files.
            - style (int, optional):
                Defines the ASCII frame style for the header.
                Valid values are integers from 1 to 6, corresponding to predefined styles:
                    1. Basic box with `+`, `-`, and `|`
                    2. Double-line frame with `╔`, `═`, and `║`
                    3. Rounded corners with `.`, `'`, `-`, and `|`
                    4. Thick outer frame with `#`, `=`, and `#`
                    5. Box drawing characters with `┌`, `─`, and `│`
                    6. Minimalist dotted frame with `.`, `:`, and `.`
                Default is `1` (basic box).

        Returns:
            TemporaryFile: The temporary file handle (non-Windows systems only).
            None: On Windows, the file is closed and not returned.

        Raises:
            Exception: If there is an error creating or writing to the temporary file.
        """
        try:
            # OS-specific temporary file behavior
            if os.name == 'nt':  # Windows
                ftmp = tempfile.NamedTemporaryFile(mode="w+b", prefix="script_", suffix=".txt", delete=False)
            else:  # Other platforms
                ftmp = tempfile.NamedTemporaryFile(mode="w+b", prefix="script_", suffix=".txt")

            # Generate header and content
            header = (
                f"# TEMPORARY PIZZA.SCRIPT FILE\n"
                f"# {'-' * 40}\n"
                f"{self.header(verbosity=verbose, style=style)}"
            )
            content = (
                header
                + "\n# This is a temporary file (it will be deleted automatically)\n\n"
                + self.do(printflag=False, verbose=verbose)
            )

            # Write content to the temporary file
            ftmp.write(BOM_UTF8 + content.encode('utf-8'))
            ftmp.seek(0)  # Reset file pointer to the beginning

        except Exception as e:
            # Handle errors gracefully
            ftmp.close()
            os.remove(ftmp.name)  # Clean up the temporary file
            raise Exception(f"Failed to write to or handle the temporary file: {e}") from None

        print("\nTemporary File Header:\n", header, "\n")
        print("A temporary file has been generated here:\n", ftmp.name)

        # Persistent copy creation
        if self.persistentfile:
            ftmpname = os.path.basename(ftmp.name)
            fcopyname = os.path.join(self.persistentfolder, f"script.preview.{ftmpname.rsplit('_', 1)[1]}")
            copyfile(ftmp.name, fcopyname)
            print("A persistent copy has been created here:\n", fcopyname)

            # Create a clean copy without empty lines or comments
            with open(ftmp.name, "r") as f:
                lines = f.readlines()
            bom_utf8_str = BOM_UTF8.decode("utf-8")
            clean_lines = [
                line for line in lines
                if line.strip() and not line.lstrip().startswith("#") and not line.startswith(bom_utf8_str)
            ]
            fcleanname = os.path.join(self.persistentfolder, f"script.preview.clean.{ftmpname.rsplit('_', 1)[1]}")
            with open(fcleanname, "w") as f:
                f.writelines(clean_lines)
            print("A clean copy has been created here:\n", fcleanname)

            # Handle file closure for Windows
            if os.name == 'nt':
                ftmp.close()
                return None
            else:
                return ftmp


    # Note that it was not the original intent to copy scripts
    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, deepduplicate(v, memo))
        return copie


    def detect_variables(self, with_index=False, only_indexed=False):
        """
        Detects variables in the content of the template using an extended pattern
        to include indexed variables (e.g., ${var[i]}) if `with_index` is True.

        Parameters:
        -----------
        with_index : bool, optional
            If True, include indexed variables with their indices (e.g., ${var[i]}). Default is False.
        only_indexed : bool, optional
            If True, target only variables that are used with an index (e.g., ${var[i]}). Default is False.

        Returns:
        --------
        list
            A list of unique variable names detected in the content based on the flags.
        """
        # Regular expression to match variables with optional indexing
        variable_pattern = re.compile(r'\$\{(\w+)(\[\w+\])?\}')
        # Ensure TEMPLATE is iterable (split string into lines if needed)
        if isinstance(self.TEMPLATE, str):
            lines = self.TEMPLATE.splitlines()  # Split string into lines
        elif isinstance(self.TEMPLATE, list):
            lines = self.TEMPLATE
        else:
            raise TypeError("TEMPLATE must be a string or a list of strings.")
        # Detect variables from all lines
        detected_vars = set()
        for line in lines:
            matches = variable_pattern.findall(line)
            for match in matches:
                variable_name = match[0]  # Base variable name
                index = match[1]          # Optional index (e.g., '[i]')
                if only_indexed and not index:
                    continue  # Skip non-indexed variables if targeting only indexed ones
                if with_index and index:
                    detected_vars.add(f"{variable_name}{index}")  # Include the full indexed variable
                elif not with_index:
                    detected_vars.add(variable_name)  # Include only the base variable

        # Return the list of unique variables
        return list(detected_vars)


# %% pipe script
class pipescript:
    """
    pipescript: A Class for Managing Script Pipelines

    The `pipescript` class stores scripts in a pipeline where multiple scripts,
    script objects, or script object groups can be combined and executed
    sequentially. Scripts in the pipeline are executed using the pipe (`|`) operator,
    allowing for dynamic control over execution order, script concatenation, and
    variable management.

    Key Features:
    -------------
    - **Pipeline Construction**: Create pipelines of scripts, combining multiple
      script objects, `script`, `scriptobject`, or `scriptobjectgroup` instances.
      The pipe operator (`|`) is overloaded to concatenate scripts.
    - **Sequential Execution**: Execute all scripts in the pipeline in the order
      they were added, with support for reordering, selective execution, and
      clearing of individual steps.
    - **User and Definition Spaces**: Manage local and global user-defined variables
      (`USER` space) and static definitions for each script in the pipeline.
      Global definitions apply to all scripts in the pipeline, while local variables
      apply to specific steps.
    - **Flexible Script Handling**: Indexing, slicing, reordering, and renaming
      scripts in the pipeline are supported. Scripts can be accessed, replaced,
      and modified like array elements.

    Practical Use Cases:
    --------------------
    - **LAMMPS Script Automation**: Automate the generation of multi-step simulation
      scripts for LAMMPS, combining different simulation setups into a single pipeline.
    - **Script Management**: Combine and manage multiple scripts, tracking user
      variables and ensuring that execution order can be adjusted easily.
    - **Advanced Script Execution**: Perform partial pipeline execution, reorder
      steps, or clear completed steps while maintaining the original pipeline structure.

    Methods:
    --------
    __init__(self, s=None):
        Initializes a new `pipescript` object, optionally starting with a script
        or script-like object (`script`, `scriptobject`, `scriptobjectgroup`).

    setUSER(self, idx, key, value):
        Set a user-defined variable (`USER`) for the script at the specified index.

    getUSER(self, idx, key):
        Get the value of a user-defined variable (`USER`) for the script at the
        specified index.

    clear(self, idx=None):
        Clear the execution status of scripts in the pipeline, allowing them to
        be executed again.

    do(self, idx=None, printflag=True, verbosity=2, verbose=None, forced=False):
        Execute the pipeline or a subset of the pipeline, generating a combined
        LAMMPS-compatible script.

    script(self, idx=None, printflag=True, verbosity=2, verbose=None, forced=False):
        Generate the final LAMMPS script from the pipeline or a subset of the pipeline.

    rename(self, name="", idx=None):
        Rename the scripts in the pipeline, assigning new names to specific
        indices or all scripts.

    write(self, file, printflag=True, verbosity=2, verbose=None):
        Write the generated script to a file.

    dscript(self, verbose=None, **USER)
        Convert the current pipescript into a dscript object

    header(self, verbose=True,verbosity=None, style=4):
        Generate a formatted header for the pipescript file.

    list_values(self, varname, what="all"):
        List all occurrences and values of a variable across the pipeline scripts.

    list_multiple_values(self, varnames, what="all"):
        List all occurrences and values of multiple variables across the pipeline scripts.

    plot_value_distribution(self, varname, what="all"):
        Plot the distribution of values for a given variable across specified scopes.

    generate_report(self, filename, varnames=None, scopes="all"):
        Generate a comprehensive report for specified variables and writes it to a file.


    Static Methods:
    ---------------
    join(liste):
        Combine a list of `script` and `pipescript` objects into a single pipeline.

    Additional Features:
    --------------------
    - **Indexing and Slicing**: Use array-like indexing (`p[0]`, `p[1:3]`) to access
      and manipulate scripts in the pipeline.
    - **Deep Copy Support**: The pipeline supports deep copying, preserving the
      entire pipeline structure and its scripts.
    - **Verbose and Print Options**: Control verbosity and printing behavior for
      generated scripts, allowing for detailed output or minimal script generation.

    Original Content:
    -----------------
    The `pipescript` class supports a variety of pipeline operations, including:
    - Sequential execution with `cmd = p.do()`.
    - Reordering pipelines with `p[[2, 0, 1]]`.
    - Deleting steps with `p[[0, 1]] = []`.
    - Accessing local and global user space variables via `p.USER[idx].var` and
      `p.scripts[idx].USER.var`.
    - Managing static definitions for each script in the pipeline.
    - Example usage:
      ```
      p = pipescript()
      p | i
      p = G | c | g | d | b | i | t | d | s | r
      p.rename(["G", "c", "g", "d", "b", "i", "t", "d", "s", "r"])
      cmd = p.do([0, 1, 4, 7])
      sp = p.script([0, 1, 4, 7])
      ```
    - Scripts in the pipeline are executed sequentially, and definitions propagate
      from left to right. The `USER` space and `DEFINITIONS` are managed separately
      for each script in the pipeline.

    OVERVIEW
    -----------------
        Pipescript class stores scripts in pipelines
            By assuming: s0, s1, s2... scripts, scriptobject or scriptobjectgroup
            p = s0 | s1 | s2 generates a pipe script

            Example of pipeline:
          ------------:----------------------------------------
          [-]  00: G with (0>>0>>19) DEFINITIONS
          [-]  01: c with (0>>0>>10) DEFINITIONS
          [-]  02: g with (0>>0>>26) DEFINITIONS
          [-]  03: d with (0>>0>>19) DEFINITIONS
          [-]  04: b with (0>>0>>28) DEFINITIONS
          [-]  05: i with (0>>0>>49) DEFINITIONS
          [-]  06: t with (0>>0>>2) DEFINITIONS
          [-]  07: d with (0>>0>>19) DEFINITIONS
          [-]  08: s with (0>>0>>1) DEFINITIONS
          [-]  09: r with (0>>0>>20) DEFINITIONS
          ------------:----------------------------------------
        Out[35]: pipescript containing 11 scripts with 8 executed[*]

        note: XX>>YY>>ZZ represents the number of stored variables
             and the direction of propagation (inheritance from left)
             XX: number of definitions in the pipeline USER space
             YY: number of definitions in the script instance (frozen in the pipeline)
             ZZ: number of definitions in the script (frozen space)

            pipelines are executed sequentially (i.e. parameters can be multivalued)
                cmd = p.do()
                fullscript = p.script()

            pipelines are indexed
                cmd = p[[0,2]].do()
                cmd = p[0:2].do()
                cmd = p.do([0,2])

            pipelines can be reordered
                q = p[[2,0,1]]

            steps can be deleted
                p[[0,1]] = []

            clear all executions with
                p.clear()
                p.clear(idx=1,2)

            local USER space can be accessed via
            (affects only the considered step)
                p.USER[0].a = 1
                p.USER[0].b = [1 2]
                p.USER[0].c = "$ hello world"

            global USER space can accessed via
            (affects all steps onward)
                p.scripts[0].USER.a = 10
                p.scripts[0].USER.b = [10 20]
                p.scripts[0].USER.c = "$ bye bye"

            static definitions
                p.scripts[0].DEFINITIONS

            steps can be renamed with the method rename()

            syntaxes are à la Matlab:
                p = pipescript()
                p | i
                p = collection | G
                p[0]
                q = p | p
                q[0] = []
                p[0:1] = q[0:1]
                p = G | c | g | d | b | i | t | d | s | r
                p.rename(["G","c","g","d","b","i","t","d","s","r"])
                cmd = p.do([0,1,4,7])
                sp = p.script([0,1,4,7])
                r = collection | p

            join joins a list (static method)
                p = pipescript.join([p1,p2,s3,s4])


            Pending: mechanism to store LAMMPS results (dump3) in the pipeline
    """

    def __init__(self,s=None, name=None, printflag=False, verbose=True, verbosity = None):
        """ constructor """
        self.globalscript = None
        self.listscript = []
        self.listUSER = []
        self.printflag = printflag
        self.verbose = verbose if verbosity is None else verbosity>0
        self.verbosity = 0 if not verbose else verbosity
        self.cmd = ""
        if isinstance(s,script):
            self.listscript = [duplicate(s)]
            self.listUSER = [scriptdata()]
        elif isinstance(s,scriptobject):
            self.listscript = [scriptobjectgroup(s).script]
            self.listUSER = [scriptdata()]
        elif isinstance(s,scriptobjectgroup):
            self.listscript = [s.script]
            self.listUSER = [scriptdata()]
        else:
            ValueError("the argument should be a scriptobject or scriptobjectgroup")
        if s != None:
            self.name = [str(s)]
            self.executed = [False]
        else:
            self.name = []
            self.executed = []

    def setUSER(self,idx,key,value):
        """
            setUSER sets USER variables
            setUSER(idx,varname,varvalue)
        """
        if isinstance(idx,int) and (idx>=0) and (idx<self.n):
            self.listUSER[idx].setattr(key,value)
        else:
            raise IndexError(f"the index in the pipe should be comprised between 0 and {len(self)-1}")

    def getUSER(self,idx,key):
        """
            getUSER get USER variable
            getUSER(idx,varname)
        """
        if isinstance(idx,int) and (idx>=0) and (idx<self.n):
            self.listUSER[idx].getattr(key)
        else:
            raise IndexError(f"the index in the pipe should be comprised between 0 and {len(self)-1}")

    @property
    def USER(self):
        """
            p.USER[idx].var returns the value of the USER variable var
            p.USER[idx].var = val assigns the value val to the USER variable var
        """
        return self.listUSER  # override listuser

    @property
    def scripts(self):
        """
            p.scripts[idx].USER.var returns the value of the USER variable var
            p.scripts[idx].USER.var = val assigns the value val to the USER variable var
        """
        return self.listscript # override listuser

    def __add__(self,s):
        """ overload + as pipe with copy """
        from pizza.dscript import dscript
        if isinstance(s,(pipescript,script)):
            dup = deepduplicate(self)
            return dup | s      # + or | are synonyms
        elif isinstance(s,scriptobject):
            return self + s.script(printflag=self.printflag,verbose=self.verbose,verbosity=self.verbosity)
        elif isinstance(s,dscript):
            return self + s.pipescript(printflag=s.printflag,verbose=s.verbose,verbosity=s.verbosity)
        else:
            raise TypeError(f"The operand should be a pipescript/script/dscript/scriptobjectgroup and not '{type(s).__name__}'")

    def __iadd__(self,s):
        """ overload += as pipe without copy """
        if isinstance(s,pipescript):
            return self | s      # + or | are synonyms
        else:
            raise TypeError(f"The operand should be a pipescript and not '{type(s).__name__}'")

    def __mul__(self,ntimes):
        """ overload * as multiple pipes with copy """
        if isinstance(self,pipescript):
            res = deepduplicate(self)
            if ntimes>1:
                for n in range(1,ntimes): res += self
            return res
        else:
            raise TypeError(f"The operand should be a pipescript and not '{type(s).__name__}'")



    def __or__(self, s):
        """ Overload | pipe operator in pipescript """
        leftarg = deepduplicate(self)  # Make a deep copy of the current object
        # Local import only when dscript type needs to be checked
        from pizza.dscript import dscript
        from pizza.group import group, groupcollection
        from pizza.region import region
        # Convert rightarg to pipescript if needed
        if isinstance(s, dscript):
            rightarg = s.pipescript(printflag=s.printflag,verbose=s.verbose,verbosity=s.verbosity)  # Convert the dscript object to a pipescript
            native = False
        elif isinstance(s,script):
            rightarg = pipescript(s,printflag=s.printflag,verbose=s.verbose,verbosity=s.verbosity)  # Convert the script-like object into a pipescript
            native = False
        elif isinstance(s,(scriptobject,scriptobjectgroup)):
            stmp = s.script(printflag=self.printflag,verbose=self.verbose,verbosity=self.verbosity)
            rightarg =  pipescript(stmp,printflag=self.printflag,verbose=self.verbose,verbosity=self.verbosity)  # Convert the script-like object into a pipescript
            native = False
        elif isinstance(s, group):
            stmp = s.script(printflag=s.printflag,verbose=s.verbose,verbosity=s.verbosity)
            rightarg = pipescript(stmp,printflag=s.printflag,verbose=s.verbose,verbosity=s.verbosity)  # Convert the script-like object into a pipescript
            native = False
        elif isinstance(s, groupcollection):
            stmp = s.script(printflag=self.printflag,verbose=self.verbose,verbosity=self.verbosity)
            rightarg = pipescript(stmp,printflag=self.printflag,verbose=self.verbose,verbosity=self.verbosity)  # Convert the script-like object into a pipescript
            native = False
        elif isinstance(s, region):
            rightarg = s.pipescript(printflag=self.printflag,verbose=self.verbose,verbosity=self.verbosity)  # Convert the script-like object into a pipescript
            native = False
        elif isinstance(s,pipescript):
            rightarg = s
            native = True
        else:
            raise TypeError(f"The operand should be a pipescript, dscript, script, scriptobject, scriptobjectgroup, group or groupcollection not {type(s)}")
        # Native piping
        if native:
            leftarg.listscript = leftarg.listscript + rightarg.listscript
            leftarg.listUSER = leftarg.listUSER + rightarg.listUSER
            leftarg.name = leftarg.name + rightarg.name
            for i in range(len(rightarg)):
                rightarg.executed[i] = False
            leftarg.executed = leftarg.executed + rightarg.executed
            return leftarg
        # Piping for non-native objects (dscript or script-like objects)
        else:
            # Loop through all items in rightarg and concatenate them
            for i in range(rightarg.n):
                leftarg.listscript.append(rightarg.listscript[i])
                leftarg.listUSER.append(rightarg.listUSER[i])
                leftarg.name.append(rightarg.name[i])
                leftarg.executed.append(False)
            return leftarg



    def __str__(self):
        """ string representation """
        return f"pipescript containing {self.n} scripts with {self.nrun} executed[*]"


    def __repr__(self):
        """ display method """
        line = "  "+"-"*12+":"+"-"*40
        if self.verbose:
            print("","Pipeline with %d scripts and" % self.n,
                  "D(STATIC:GLOBAL:LOCAL) DEFINITIONS",line,sep="\n")
        else:
            print(line)
        for i in range(len(self)):
            if self.executed[i]:
                state = "*"
            else:
                state = "-"
            print("%10s" % ("[%s]  %02d:" % (state,i)),
                  self.name[i],"with D(%2d:%2d:%2d)" % (
                       len(self.listscript[i].DEFINITIONS),
                       len(self.listscript[i].USER),
                       len(self.listUSER[i])                 )
                  )
        if self.verbose:
            print(line,"::: notes :::","p[i], p[i:j], p[[i,j]] copy pipeline segments",
                  "LOCAL: p.USER[i],p.USER[i].variable modify the user space of only p[i]",
                  "GLOBAL: p.scripts[i].USER.var to modify the user space from p[i] and onwards",
                  "STATIC: p.scripts[i].DEFINITIONS",
                  'p.rename(idx=range(2),name=["A","B"]), p.clear(idx=[0,3,4])',
                  "p.script(), p.script(idx=range(5)), p[0:5].script()","",sep="\n")
        else:
             print(line)
        return str(self)

    def __len__(self):
        """ len() method """
        return len(self.listscript)

    @property
    def n(self):
        """ number of scripts """
        return len(self)

    @property
    def nrun(self):
        """ number of scripts executed continuously from origin """
        n, nmax  = 0, len(self)
        while n<nmax and self.executed[n]: n+=1
        return n

    def __getitem__(self,idx):
        """ return the ith or slice element(s) of the pipe  """
        dup = deepduplicate(self)
        if isinstance(idx,slice):
            dup.listscript = dup.listscript[idx]
            dup.listUSER = dup.listUSER[idx]
            dup.name = dup.name[idx]
            dup.executed = dup.executed[idx]
        elif isinstance(idx,int):
            if idx<len(self):
                dup.listscript = dup.listscript[idx:idx+1]
                dup.listUSER = dup.listUSER[idx:idx+1]
                dup.name = dup.name[idx:idx+1]
                dup.executed = dup.executed[idx:idx+1]
            else:
                raise IndexError(f"the index in the pipe should be comprised between 0 and {len(self)-1}")
        elif isinstance(idx,list):
            dup.listscript = picker(dup.listscript,idx)
            dup.listUSER = picker(dup.listUSER,idx)
            dup.name = picker(dup.name,idx)
            dup.executed = picker(dup.executed,idx)
        else:
            raise IndexError("the index needs to be a slice or an integer")
        return dup

    def __setitem__(self,idx,s):
        """
            modify the ith element of the pipe
                p[4] = [] removes the 4th element
                p[4:7] = [] removes the elements from position 4 to 6
                p[2:4] = p[0:2] copy the elements 0 and 1 in positions 2 and 3
                p[[3,4]]=p[0]
        """
        if isinstance(s,(script,scriptobject,scriptobjectgroup)):
            dup = pipescript(s)
        elif isinstance(s,pipescript):
            dup = s
        elif s==[]:
            dup = []
        else:
            raise ValueError("the value must be a pipescript, script, scriptobject, scriptobjectgroup")
        if len(s)<1: # remove (delete)
            if isinstance(idx,slice) or idx<len(self):
                del self.listscript[idx]
                del self.listUSER[idx]
                del self.name[idx]
                del self.executed[idx]
            else:
                raise IndexError("the index must be a slice or an integer")
        elif len(s)==1: # scalar
            if isinstance(idx,int):
                if idx<len(self):
                    self.listscript[idx] = dup.listscript[0]
                    self.listUSER[idx] = dup.listUSER[0]
                    self.name[idx] = dup.name[0]
                    self.executed[idx] = False
                elif idx==len(self):
                    self.listscript.append(dup.listscript[0])
                    self.listUSER.append(dup.listUSER[0])
                    self.name.append(dup.name[0])
                    self.executed.append(False)
                else:
                    raise IndexError(f"the index must be ranged between 0 and {self.n}")
            elif isinstance(idx,list):
                for i in range(len(idx)):
                    self.__setitem__(idx[i], s) # call as a scalar
            elif isinstance(idx,slice):
                for i in range(*idx.indices(len(self)+1)):
                    self.__setitem__(i, s)
            else:
                raise IndexError("unrocognized index value, the index should be an integer or a slice")
        else: # many values
            if isinstance(idx,list): # list call à la Matlab
                if len(idx)==len(s):
                    for i in range(len(s)):
                        self.__setitem__(idx[i], s[i]) # call as a scalar
                else:
                    raise IndexError(f"the number of indices {len(list)} does not match the number of values {len(s)}")
            elif isinstance(idx,slice):
                ilist = list(range(*idx.indices(len(self)+len(s))))
                self.__setitem__(ilist, s) # call as a list
            else:
                raise IndexError("unrocognized index value, the index should be an integer or a slice")

    def rename(self,name="",idx=None):
        """
            rename scripts in the pipe
                p.rename(idx=[0,2,3],name=["A","B","C"])
        """
        if isinstance(name,list):
            if len(name)==len(self) and idx==None:
                self.name = name
            elif len(name) == len(idx):
                for i in range(len(idx)):
                    self.rename(name[i],idx[i])
            else:
                IndexError(f"the number of indices {len(idx)} does not match the number of names {len(name)}")
        elif idx !=None and idx<len(self) and name!="":
            self.name[idx] = name
        else:
            raise ValueError("provide a non empty name and valid index")

    def clear(self,idx=None):
        if len(self)>0:
            if idx==None:
                for i in range(len(self)):
                    self.clear(i)
            else:
                if isinstance(idx,(range,list)):
                    for i in idx:
                        self.clear(idx=i)
                elif isinstance(idx,int) and idx<len(self):
                    self.executed[idx] = False
                else:
                    raise IndexError(f"the index should be ranged between 0 and {self.n-1}")
            if not self.executed[0]:
                self.globalscript = None
                self.cmd = ""



    def do(self, idx=None, printflag=True, verbosity=2, verbose=None, forced=False):
        """
        Execute the pipeline or a part of the pipeline and generate the LAMMPS script.

        Parameters:
            idx (list, range, or int, optional): Specifies which steps of the pipeline to execute.
            printflag (bool, optional): Whether to print the script for each step. Default is True.
            verbosity (int, optional): Level of verbosity for the output.
            verbose (bool, optional): Override for verbosity. If False, sets verbosity to 0.
            forced (bool, optional): If True, forces the pipeline to regenerate all scripts.

        Returns:
            str: Combined LAMMPS script for the specified pipeline steps.

            Execute the pipeline or a part of the pipeline and generate the LAMMPS script.

            This method processes the pipeline of script objects, executing each step to generate
            a combined LAMMPS-compatible script. The execution can be done for the entire pipeline
            or for a specified range of indices. The generated script can include comments and
            metadata based on the verbosity level.


        Method Workflow:
            - The method first checks if there are any script objects in the pipeline.
              If the pipeline is empty, it returns a message indicating that there is nothing to execute.
            - It determines the start and stop indices for the range of steps to execute.
              If idx is not provided, it defaults to executing all steps from the last executed position.
            - If a specific index or list of indices is provided, it executes only those steps.
            - The pipeline steps are executed in order, combining the scripts using the
              >> operator for sequential execution.
            - The generated script includes comments indicating the current run step and pipeline range,
              based on the specified verbosity level.
            - The final combined script is returned as a string.

        Example Usage:
        --------------
            >>> p = pipescript()
            >>> # Execute the entire pipeline
            >>> full_script = p.do()
            >>> # Execute steps 0 and 2 only
            >>> partial_script = p.do([0, 2])
            >>> # Execute step 1 with minimal verbosity
            >>> minimal_script = p.do(idx=1, verbosity=0)

            Notes:
            - The method uses modular arithmetic to handle index wrapping, allowing
              for cyclic execution of pipeline steps.
            - If the pipeline is empty, the method returns the string "# empty pipe - nothing to do".
            - The globalscript is initialized or updated with each step's script,
              and the USER definitions are accumulated across the steps.
            - The command string self.cmd is updated with the generated script for
              each step in the specified range.

            Raises:
            - None: The method does not raise exceptions directly, but an empty pipeline will
                    result in the return of "# empty pipe - nothing to do".
        """
        verbosity = 0 if verbose is False else verbosity
        if len(self) == 0:
            return "# empty pipe - nothing to do"

        # Check if not all steps are executed or if there are gaps
        not_all_executed = not all(self.executed[:self.nrun])  # Check up to the last executed step

        # Determine pipeline range
        total_steps = len(self)
        if self.globalscript is None or forced or not_all_executed:
            start = 0
            self.cmd = ""
        else:
            start = self.nrun
            self.cmd = self.cmd.rstrip("\n") + "\n\n"

        if idx is None:
            idx = range(start, total_steps)
        if isinstance(idx, int):
            idx = [idx]
        if isinstance(idx, range):
            idx = list(idx)

        idx = [i % total_steps for i in idx]
        start, stop = min(idx), max(idx)

        # Prevent re-executing already completed steps
        if not forced:
            idx = [step for step in idx if not self.executed[step]]

        # Execute pipeline steps
        for step in idx:
            step_wrapped = step % total_steps

            # Combine scripts
            if step_wrapped == 0:
                self.globalscript = self.listscript[step_wrapped]
            else:
                self.globalscript = self.globalscript >> self.listscript[step_wrapped]

            # Step label
            step_name = f"<{self.name[step]}>"
            step_label = f"# [{step+1} of {total_steps} from {start}:{stop}] {step_name}"

            # Get script content for the step
            step_output = self.globalscript.do(printflag=printflag, verbose=verbosity > 1)

            # Add comments and content
            if step_output.strip():
                self.cmd += f"{step_label}\n{step_output.strip()}\n\n"
            elif verbosity > 0:
                self.cmd += f"{step_label} :: no content\n\n"

            # Update USER definitions
            self.globalscript.USER += self.listUSER[step]
            self.executed[step] = True

        # Clean up and finalize script
        self.cmd = self.cmd.replace("\\n", "\n").strip()  # Remove literal \\n and extra spaces
        self.cmd += "\n"  # Ensure trailing newline
        return remove_comments(self.cmd) if verbosity == 0 else self.cmd


    def do_legacy(self, idx=None, printflag=True, verbosity=2, verbose=None, forced=False):
        """
        Execute the pipeline or a part of the pipeline and generate the LAMMPS script.

        This method processes the pipeline of script objects, executing each step to generate
        a combined LAMMPS-compatible script. The execution can be done for the entire pipeline
        or for a specified range of indices. The generated script can include comments and
        metadata based on the verbosity level.

        Parameters:
        - idx (list, range, or int, optional): Specifies which steps of the pipeline to execute.
                                               If None, all steps from the current position to
                                               the end are executed. A list of indices can be
                                               provided to execute specific steps, or a single
                                               integer can be passed to execute a specific step.
                                               Default is None.
        - printflag (bool, optional): If True, the generated script for each step is printed
                                      to the console. Default is True.
        - verbosity (int, optional): Controls the level of detail in the generated script.
                                     - 0: Minimal output, no comments.
                                     - 1: Basic comments for run steps.
                                     - 2: Detailed comments with additional information.
                                     Default is 2.
        - forced (bool, optional): If True, all scripts are regenerated

        Returns:
        - str: The combined LAMMPS script generated from the specified steps of the pipeline.

        Method Workflow:
        - The method first checks if there are any script objects in the pipeline.
          If the pipeline is empty, it returns a message indicating that there is nothing to execute.
        - It determines the start and stop indices for the range of steps to execute.
          If idx is not provided, it defaults to executing all steps from the last executed position.
        - If a specific index or list of indices is provided, it executes only those steps.
        - The pipeline steps are executed in order, combining the scripts using the
          >> operator for sequential execution.
        - The generated script includes comments indicating the current run step and pipeline range,
          based on the specified verbosity level.
        - The final combined script is returned as a string.

        Example Usage:
        --------------
        >>> p = pipescript()
        >>> # Execute the entire pipeline
        >>> full_script = p.do()
        >>> # Execute steps 0 and 2 only
        >>> partial_script = p.do([0, 2])
        >>> # Execute step 1 with minimal verbosity
        >>> minimal_script = p.do(idx=1, verbosity=0)

        Notes:
        - The method uses modular arithmetic to handle index wrapping, allowing
          for cyclic execution of pipeline steps.
        - If the pipeline is empty, the method returns the string "# empty pipe - nothing to do".
        - The globalscript is initialized or updated with each step's script,
          and the USER definitions are accumulated across the steps.
        - The command string self.cmd is updated with the generated script for
          each step in the specified range.

        Raises:
        - None: The method does not raise exceptions directly, but an empty pipeline will
                result in the return of "# empty pipe - nothing to do".
        """

        verbosity = 0 if verbose is False else verbosity
        if len(self)>0:
            # ranges
            ntot = len(self)
            stop = ntot-1
            if (self.globalscript == None) or (self.globalscript == []) or not self.executed[0] or forced:
                start = 0
                self.cmd = ""
            else:
                start = self.nrun
            if start>stop: return self.cmd
            if idx is None: idx = range(start,stop+1)
            if isinstance(idx,range): idx = list(idx)
            if isinstance(idx,int): idx = [idx]
            start,stop = min(idx),max(idx)
            # do
            for i in idx:
                j = i % ntot
                if j==0:
                    self.globalscript = self.listscript[j]
                else:
                    self.globalscript = self.globalscript >> self.listscript[j]
                name = "  "+self.name[i]+"  "
                if verbosity>0:
                    self.cmd += "\n\n#\t --- run step [%d/%d] --- [%s]  %20s\n" % \
                            (j,ntot-1,name.center(50,"="),"pipeline [%d]-->[%d]" %(start,stop))
                else:
                    self.cmd +="\n"
                self.globalscript.USER = self.globalscript.USER + self.listUSER[j]
                self.cmd += self.globalscript.do(printflag=printflag,verbose=verbosity>1)
                self.executed[i] = True
            self.cmd = self.cmd.replace("\\n", "\n") # remove literal \\n if any (dscript.save add \\n)
            return remove_comments(self.cmd) if verbosity==0 else self.cmd
        else:
            return "# empty pipe - nothing to do"


    def script(self,idx=None, printflag=True, verbosity=2, verbose=None, forced=False, style=4):
        """
            script the pipeline or parts of the pipeline
                s = p.script()
                s = p.script([0,2])

        Parameters:
        - idx (list, range, or int, optional): Specifies which steps of the pipeline to execute.
                                               If None, all steps from the current position to
                                               the end are executed. A list of indices can be
                                               provided to execute specific steps, or a single
                                               integer can be passed to execute a specific step.
                                               Default is None.
        - printflag (bool, optional): If True, the generated script for each step is printed
                                      to the console. Default is True.
        - verbosity (int, optional): Controls the level of detail in the generated script.
                                     - 0: Minimal output, no comments.
                                     - 1: Basic comments for run steps.
                                     - 2: Detailed comments with additional information.
                                     Default is 2.
        - forced (bool, optional): If True, all scripts are regenerated
        - style (int, optional):
            Defines the ASCII frame style for the header.
            Valid values are integers from 1 to 6, corresponding to predefined styles:
                1. Basic box with `+`, `-`, and `|`
                2. Double-line frame with `╔`, `═`, and `║`
                3. Rounded corners with `.`, `'`, `-`, and `|`
                4. Thick outer frame with `#`, `=`, and `#`
                5. Box drawing characters with `┌`, `─`, and `│`
                6. Minimalist dotted frame with `.`, `:`, and `.`
            Default is `4` (thick outer frame).

        """
        printflag = self.printflag if printflag is None else printflag
        verbose = verbosity > 0 if verbosity is not None else (self.verbose if verbose is None else verbose)
        verbosity=0 if verbose is False else verbosity
        s = script(printflag=printflag, verbose=verbosity>0)
        s.name = "pipescript"
        s.description = "pipeline with %d scripts" % len(self)
        if len(self)>1:
            s.userid = self.name[0]+"->"+self.name[-1]
        elif len(self)==1:
            s.userid = self.name[0]
        else:
            s.userid = "empty pipeline"
        s.TEMPLATE = self.header(verbosity=verbosity, style=style) + "\n" +\
            self.do(idx, printflag=printflag, verbosity=verbosity, verbose=verbose, forced=forced)
        s.DEFINITIONS = duplicate(self.globalscript.DEFINITIONS)
        s.USER = duplicate(self.globalscript.USER)
        return s

    @staticmethod
    def join(liste):
        """
            join a combination scripts and pipescripts within a pipescript
                p = pipescript.join([s1,s2,p3,p4,p5...])
        """
        if not isinstance(liste,list):
            raise ValueError("the argument should be a list")
        ok = True
        for i in range(len(liste)):
            ok = ok and isinstance(liste[i],(script,pipescript))
            if not ok:
                raise ValueError(f"the entry [{i}] should be a script or pipescript")
        if len(liste)<1:
            return liste
        out = liste[0]
        for i in range(1,len(liste)):
            out = out | liste[i]
        return out

    # Note that it was not the original intent to copy pipescripts
    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, deepduplicate(v, memo))
        return copie

    # write file
    def write(self, file, printflag=False, verbosity=2, verbose=None, overwrite=False):
       """
       Write the combined script to a file.

       Parameters:
           file (str): The file path where the script will be saved.
           printflag (bool): Flag to enable/disable printing of details.
           verbosity (int): Level of verbosity for the script generation.
           verbose (bool or None): If True, enables verbose mode; if None, defaults to the instance's verbosity.
           overwrite (bool): Whether to overwrite the file if it already exists. Default is False.

        Returns:
            str: The full absolute path of the file written.

       Raises:
           FileExistsError: If the file already exists and overwrite is False.

       Notes:
           - This method combines the individual scripts within the `pipescript` object
             and saves the resulting script to the specified file.
           - If `overwrite` is False and the file exists, an error is raised.
           - If `verbose` is True and the file is overwritten, a warning is displayed.
       """
       # Generate the combined script
       myscript = self.script(printflag=printflag, verbosity=verbosity, verbose=verbose, forced=True)
       # Call the script's write method with the overwrite parameter
       return myscript.write(file, printflag=printflag, verbose=verbose, overwrite=overwrite)


    def dscript(self, name=None, printflag=None, verbose=None, verbosity=None, clean="fixing", **USER):
        """
        Convert the current pipescript object to a dscript object.

        This method merges the STATIC, GLOBAL, and LOCAL variable spaces from each step
        in the pipescript into a single dynamic script per step in the dscript.
        Each step in the pipescript is transformed into a dynamic script in the dscript,
        where variable spaces are combined using the following order:

        1. STATIC: Definitions specific to each script in the pipescript.
        2. GLOBAL: User variables shared across steps from a specific point onwards.
        3. LOCAL: User variables for each individual step.

        Parameters:
        -----------
        verbose : bool, optional
            Controls verbosity of the dynamic scripts in the resulting dscript object.
            If None, the verbosity setting of the pipescript will be used.

        clean : "fixing" or "removing"
            - 'removing': Completely remove the empty step from TEMPLATE.
            - 'fixing': Replace the content of the empty step with a comment.

        **USER : scriptobjectdata(), optional
            Additional user-defined variables that can override existing static variables
            in the dscript object or be added to it.

        Returns:
        --------
        outd : dscript
            A dscript object that contains all steps of the pipescript as dynamic scripts.
            Each step from the pipescript is added as a dynamic script with the same content
            and combined variable spaces.
        """
        # Local imports
        from pizza.dscript import dscript, ScriptTemplate, lambdaScriptdata

        # verbosity
        printflag = self.printflag if printflag is None else printflag
        verbose = verbosity > 0 if verbosity is not None else (self.verbose if verbose is None else verbose)
        verbosity = 0 if not verbose else verbosity

        # Adjust name
        if name is None:
            if isinstance(self.name, str):
                name = self.name
            elif isinstance(self.name, list):
                name = (
                    self.name[0] if len(self.name) == 1 else self.name[0] + "..." + self.name[-1]
                )

        # Create the dscript container with the pipescript name as the userid
        outd = dscript(userid=name, verbose=self.verbose, **USER)

        # Initialize static merged definitions
        staticmerged_definitions = lambdaScriptdata()

        # Track used variables per step
        step_used_variables = []

        # Loop over each step in the pipescript
        for i, script in enumerate(self.listscript):
            # Merge STATIC, GLOBAL, and LOCAL variables for the current step
            static_vars = self.listUSER[i] # script.DEFINITIONS
            global_vars = script.DEFINITIONS # self.scripts[i].USER
            local_vars = script.USER # self.USER[i]
            refreshed_globalvars = static_vars + global_vars

            # Detect variables used in the current template
            used_variables = set(script.detect_variables())
            step_used_variables.append(used_variables)  # Track used variables for this step

            # Copy all current variables to local_static_updates and remove matching variables from staticmerged_definitions
            local_static_updates = lambdaScriptdata(**local_vars)

            for var, value in refreshed_globalvars.items():
                if var in staticmerged_definitions:
                    if (getattr(staticmerged_definitions, var) != value) and (var not in local_vars):
                        setattr(local_static_updates, var, value)
                else:
                    setattr(staticmerged_definitions, var, value)

           # Create the dynamic script for this step using the method in dscript
            key_name = i  # Use the index 'i' as the key in TEMPLATE
            content = script.TEMPLATE

            # Use the helper method in dscript to add this dynamic script
            outd.add_dynamic_script(
                key=key_name,
                content=content,
                definitions = lambdaScriptdata(**local_static_updates),
                verbose=self.verbose if verbose is None else verbose,
                userid=self.name[i],
                autorefresh=False # prevent the replacement by default values ${}
            )

            # Set eval=True only if variables are detected in the template
            if outd.TEMPLATE[key_name].detect_variables():
                outd.TEMPLATE[key_name].eval = True

        # Compute the union of all used variables across all steps
        global_used_variables = set().union(*step_used_variables)

        # Filter staticmerged_definitions to keep only variables that are used
        filtered_definitions = {
            var: value for var, value in staticmerged_definitions.items() if var in global_used_variables
        }

        # Assign the filtered definitions along with USER variables to outd.DEFINITIONS
        outd.DEFINITIONS = lambdaScriptdata(**filtered_definitions)

        # Clean the entries for empty templates
        outd.clean(verbose=verbose,behavior=clean)

        return outd



    def header(self, verbose=True,verbosity=None, style=4):
        """
        Generate a formatted header for the pipescript file.

        Parameters:
            verbosity (bool, optional): If specified, overrides the instance's `verbose` setting.
                                        Defaults to the instance's `verbose`.
            style (int from 1 to 6, optional): ASCII style to frame the header (default=4)

        Returns:
            str: A formatted string representing the pipescript object.
                 Returns an empty string if verbosity is False.

        The header includes:
            - Total number of scripts in the pipeline.
            - The verbosity setting.
            - The range of scripts from the first to the last script.
            - All enclosed within an ASCII frame that adjusts to the content.
        """
        verbose = verbosity > 0 if verbosity is not None else (self.verbose if verbose is None else verbose)
        verbosity = 0 if not verbose else verbosity
        if not verbosity:
            return ""

        # Prepare the header content
        lines = [
            f"PIPESCRIPT with {self.n} scripts | Verbosity: {verbosity}",
            "",
            f"From: <{str(self.scripts[0])}> To: <{str(self.scripts[-1])}>",
        ]

        # Use the shared method to format the header
        return frame_header(lines,style=style)



    def list_values(self, varname=None, what="all"):
        """
        Lists all occurrences and values of a specified variable or all variables across the pipeline scripts.

        Parameters:
        - varname (str, optional): The name of the variable to search for. If None, all variables are listed.
        - what (str or list/tuple, optional): Specifies the scopes to search in.
                                             Can be "static", "global", "local", "all",
                                             or a list/tuple of any combination of these.

        Returns:
        - VariableOccurrences: If varname is specified, returns a VariableOccurrences instance for that variable.
        - dict of VariableOccurrences: If varname is None, returns a dictionary mapping each variable name to its VariableOccurrences instance.
        """
        # Normalize 'what' to a list for uniform processing
        if isinstance(what, str):
            if what.lower() == "all":
                scopes = ["static", "global", "local"]
            else:
                scopes = [what.lower()]
        elif isinstance(what, (list, tuple)):
            scopes_lower = [s.lower() for s in what]
            if 'all' in scopes_lower:
                scopes = ["static", "global", "local"]
            else:
                scopes = scopes_lower
        else:
            raise ValueError("Parameter 'what' must be a string or a list/tuple of strings.")

        # Initialize data structures
        if varname:
            # Single variable case
            if len(scopes) == 1:
                data = []
            else:
                data = {}
                for scope in scopes:
                    data[scope] = []

            # Iterate over each script in the pipeline
            for i, script in enumerate(self.listscript):
                # Retrieve variables for each scope
                static_vars = self.listUSER[i] if i < len(self.listUSER) else {}
                global_vars = getattr(script, 'DEFINITIONS', {})
                local_vars = getattr(script, 'USER', {})

                scope_vars = {
                    "static": static_vars,
                    "global": global_vars,
                    "local": local_vars
                }

                # Check each requested scope
                for scope in scopes:
                    vars_dict = scope_vars.get(scope, {})
                    if varname in vars_dict.keys():
                        value = getattr(vars_dict,varname)
                        if len(scopes) == 1:
                            data.append((i, value))
                        else:
                            data[scope].append((i, value))

            # Return a VariableOccurrences instance for the specified variable
            return VariableOccurrences(data, variables=varname)

        else:
            # All variables case
            all_vars = set()

            # First, collect all variable names across specified scopes and scripts
            for i, script in enumerate(self.listscript):
                # Retrieve variables for each scope
                static_vars = self.listUSER[i] if i < len(self.listUSER) else {}
                global_vars = getattr(script, 'DEFINITIONS', {})
                local_vars = getattr(script, 'USER', {})

                scope_vars = {
                    "static": static_vars,
                    "global": global_vars,
                    "local": local_vars
                }

                for scope in scopes:
                    vars_dict = scope_vars.get(scope, {})
                    all_vars.update(vars_dict.keys())

            # Initialize a dictionary to hold VariableOccurrences for each variable
            variables_data = {}
            for var in all_vars:
                var_data = {}
                for scope in scopes:
                    var_data[scope] = []
                variables_data[var] = var_data

            # Iterate again to populate the data for each variable
            for i, script in enumerate(self.listscript):
                # Retrieve variables for each scope
                static_vars = self.listUSER[i] if i < len(self.listUSER) else {}
                global_vars = getattr(script, 'DEFINITIONS', {})
                local_vars = getattr(script, 'USER', {})

                scope_vars = {
                    "static": static_vars,
                    "global": global_vars,
                    "local": local_vars
                }

                for scope in scopes:
                    vars_dict = scope_vars.get(scope, {})
                    for var, value in vars_dict.items():
                        variables_data[var][scope].append((i, value))

            # Convert each variable's data into a VariableOccurrences instance
            variables_occurrences = {}
            for var, data in variables_data.items():
                variables_occurrences[var] = VariableOccurrences(data, variables=var)

            return variables_occurrences



    def list_multiple_values(self, varnames, what="all"):
        """
        Lists all occurrences and values of multiple variables across the pipeline scripts.

        Parameters:
        - varnames (list): A list of variable names to search for.
        - what (str or list/tuple): Specifies the scopes to search in.
                                     Can be "static", "global", "local", "all",
                                     or a list/tuple of any combination of these.

        Returns:
        - dict: A dictionary mapping each variable name to its VariableOccurrences object.
        """
        if not isinstance(varnames, (list, tuple)):
            raise ValueError("Parameter 'varnames' must be a list or tuple of strings.")

        return self.list_values(varname=varnames, what=what)



    def plot_multiple_value_distributions(self, varnames, what="all", separate_plots=True):
        """
        Plots the distribution of elements for multiple variables across specified scopes.

        Parameters:
        - varnames (list): A list of variable names to plot.
        - what (str or list/tuple): Specifies the scopes to include in the plot.
                                     Can be "static", "global", "local", "all",
                                     or a list/tuple of any combination of these.
        - separate_plots (bool): If True, plots each variable in a separate subplot.
                                 If False, combines all variables in a single plot for comparison.
        """
        if not isinstance(varnames, (list, tuple)):
            raise ValueError("Parameter 'varnames' must be a list or tuple of strings.")

        # Retrieve VariableOccurrences instances
        multiple_vars = self.list_multiple_values(varnames, what=what)

        if separate_plots:
            num_vars = len(multiple_vars)
            fig, axes = plt.subplots(num_vars, 1, figsize=(10, 5 * num_vars))
            if num_vars == 1:
                axes = [axes]  # Make it iterable

            for ax, (var, vo) in zip(axes, multiple_vars.items()):
                summary = vo.summarize()[var]
                for scope, details in summary.items():
                    elements = list(details.get('unique_elements_in_lists', []))
                    counts = [details['element_counts_in_lists'][elem] for elem in elements]
                    ax.bar(elements, counts, label=scope)
                ax.set_xlabel('Element')
                ax.set_ylabel('Count')
                ax.set_title(f"Distribution of elements in '{var}'")
                ax.legend()

            plt.tight_layout()
            plt.show()

        else:
            plt.figure(figsize=(12, 8))
            for var, vo in multiple_vars.items():
                summary = vo.summarize()[var]
                for scope, details in summary.items():
                    elements = list(details.get('unique_elements_in_lists', []))
                    counts = [details['element_counts_in_lists'][elem] for elem in elements]
                    plt.bar([f"{var}_{elem}" for elem in elements], counts, label=f"{var} - {scope}")

            plt.xlabel('Element')
            plt.ylabel('Count')
            plt.title("Distribution of elements in multiple variables")
            plt.legend()
            plt.xticks(rotation=45, ha='right')
            plt.tight_layout()
            plt.show()


    def generate_report(self, filename, varnames=None, scopes="all"):
        """
        Generates a comprehensive report for specified variables and writes it to a file.

        Parameters:
        - filename (str): Path to the output report file. Must end with .md, .txt, or .html.
        - varnames (str or list/tuple, optional): Variable name(s) to include in the report. Defaults to 'all'.
        - scopes (str or list/tuple, optional): 'all', a single scope string, or a list of scope strings. Defaults to 'all'.

        Raises:
        - ValueError: If 'filename' has an unsupported extension.
        - Exception: For other unforeseen errors.
        """
        # Validate filename extension
        _, ext = os.path.splitext(filename)
        ext = ext.lower()

        if ext not in ['.md', '.txt', '.html']:
            raise ValueError("Unsupported file extension. Supported extensions are .md, .txt, and .html.")

        # Determine format based on extension
        if ext in ['.md', '.txt']:
            export_format = 'markdown'
        elif ext == '.html':
            export_format = 'html'

        # Determine variables to include
        if varnames is None or (isinstance(varnames, (list, tuple)) and len(varnames) == 0):
            variables = 'all'
        else:
            variables = varnames  # Can be a string or a list/tuple

        # Retrieve VariableOccurrences instances
        if variables == 'all':
            variables_occurrences = self.list_values(varname=None, what=scopes)
        else:
            # Normalize varnames to a list
            if isinstance(variables, str):
                variables = [variables]
            elif isinstance(variables, (list, tuple)):
                variables = list(variables)
            else:
                raise ValueError("Parameter 'varnames' must be a string or a list/tuple of strings.")

            variables_occurrences = {}
            for var in variables:
                vo = self.list_values(varname=var, what=scopes)
                if vo and var in vo.variables:
                    variables_occurrences[var] = vo
                else:
                    print(f"Warning: Variable '{var}' not found in the specified scopes.")

        # Initialize report content
        report_content = ""
        timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        caller = "generate_report"

        # Add report header
        if export_format == 'markdown':
            report_content += f"# Comprehensive Variable Report\n\n"
            report_content += f"**Generated on {timestamp} by {caller}**\n\n"
        elif export_format == 'html':
            # Define CSS for HTML
            css = """
            <style>
                body {
                    font-family: Arial, sans-serif;
                    margin: 20px;
                }
                h1, h2, h3, h4, h5 {
                    color: #333;
                }
                table {
                    border-collapse: collapse;
                    width: 100%;
                    margin-bottom: 40px;
                }
                th, td {
                    border: 1px solid #ddd;
                    padding: 8px;
                    text-align: left;
                }
                th {
                    background-color: #4CAF50;
                    color: white;
                }
                tr:nth-child(even){background-color: #f2f2f2;}
                tr:hover {background-color: #ddd;}
            </style>
            """
            report_content += f"<!DOCTYPE html>\n<html>\n<head>\n<meta charset='UTF-8'>\n<title>Comprehensive Variable Report</title>\n{css}\n</head>\n<body>\n"
            report_content += f"<h1>Comprehensive Variable Report</h1>\n"
            report_content += f"<h2>Generated on {timestamp} by {caller}</h2>\n"

        # Assemble report content using VariableOccurrences.export()
        for var, vo in variables_occurrences.items():
            # Export content without headers and get as string
            var_content = vo.export(filename=filename, # use to identify the format based on its extension
                                    scopes=scopes,
                                    variables=var,
                                    include_headers=False,
                                    return_content=True)

            if export_format == 'markdown':
                # Add variable header
                report_content += f"## Variable: `{var}`\n\n"
                report_content += var_content + "\n\n"
                report_content += "---\n\n"  # Horizontal line between variables
            elif export_format == 'html':
                # Add variable header
                report_content += f"<h2>Variable: {var}</h2>\n"
                report_content += var_content + "\n<hr/>\n"  # Horizontal line between variables

        # Finalize HTML content
        if export_format == 'html':
            report_content += "</body>\n</html>"

        # Write report to file
        try:
            with open(filename, 'w', encoding='utf-8') as file:
                file.write(report_content)
            print(f"Report successfully generated at '{filename}'.")
        except Exception as e:
            raise Exception(f"Failed to write the report to '{filename}': {e}")



# %% Child classes of script sessions (to be derived)
# navigate with the outline tab through the different classes
#   globalsection()
#   initializesection()
#   geometrysection()
#   discretizationsection()
#   interactionsection()
#   integrationsection()
#   dumpsection()
#   statussection()
#   runsection()

# %% Global section template
class globalsection(script):
    """ LAMMPS script: global session """
    name = "global"
    description = name+" section"
    position = 0
    section = 1
    userid = "example"
    version = 0.1

    DEFINITIONS = scriptdata(
  outputfile= "$dump.mouthfeel_v5_long    # from the project of the same name",
        tsim= "500000                     # may be too long",
     outstep= 10
        )

    MATERIALS = scriptdata(
         rho_saliva= "1000 # mass density saliva",
            rho_obj= "1300 # mass density solid objects",
                 c0= "10.0 # speed of sound for saliva",
                  E= "5*${c0}*${c0}*${rho_saliva} # Young's modulus for solid objects",
           Etongue1= "10*${E} # Young's modulus for tongue",
           Etongue2= "2*${Etongue1} # Young's modulus for tongue",
                 nu= "0.3 # Poisson ratio for solid objects",
        sigma_yield= "0.1*${E} # plastic yield stress for solid objects",
     hardening_food= "0 # plastic hardening parameter for solid food",
   hardening_tongue= "1 # plastic hardening parameter for solid tongue",
  contact_stiffness= "2.5*${c0}^2*${rho_saliva} # contact force amplitude",
       contact_wall= "100*${contact_stiffness} # contact with wall (avoid interpenetration)",
                 q1= "1.0 # artificial viscosity",
                 q2= "0.0 # artificial viscosity",
                 Hg= "10 # Hourglass control coefficient for solid objects",
                 Cp= "1.0 # heat capacity -- not used here"
                  )

    DEFINITIONS += MATERIALS # append MATERIALS data

    TEMPLATE = """
# :GLOBAL SECTION:
#   avoid to set variables in LAMMPS script
#   use DEFINITIONS field to set properties.
#   If you need to define them, use the following syntax


    # ####################################################################################################
    # # GLOBAL
    # ####################################################################################################
     variable outputfile string "${outputfile}"
     variable tsim equal ${tsim}
     variable outstep equal ${outstep}

    # ####################################################################################################
    # # MATERIAL PARAMETERS
    # ####################################################################################################
    # variable        rho_saliva equal 1000 # mass density saliva
    # variable        rho_obj equal 1300 # mass density solid objects
    # variable        c0 equal 10.0 # speed of sound for saliva
    # variable        E equal 5*${c0}*${c0}*${rho_saliva} # Young's modulus for solid objects
    # variable        Etongue1 equal 10*${E} # Young's modulus for tongue
    # variable        Etongue2 equal 2*${Etongue1} # Young's modulus for tongue
    # variable        nu equal 0.3 # Poisson ratio for solid objects
    # variable        sigma_yield equal 0.1*${E} # plastic yield stress for solid objects
    # variable        hardening_food equal 0 # plastic hardening parameter for solid food
    # variable        hardening_tongue equal 1 # plastic hardening parameter for solid tongue
    # variable        contact_stiffness equal 2.5*${c0}^2*${rho_saliva} # contact force amplitude
    # variable        contact_wall equal 100*${contact_stiffness} # contact with wall (avoid interpenetration)
    # variable        q1 equal 1.0 # artificial viscosity
    # variable        q2 equal 0.0 # artificial viscosity
    # variable        Hg equal 10 # Hourglass control coefficient for solid objects
    # variable        Cp equal 1.0 # heat capacity -- not used here
    """

# %% Initialize section template
class initializesection(script):
    """ LAMMPS script: global session """
    name = "initialize"
    description = name+" section"
    position = 1
    section = 2
    userid = "example"
    version = 0.1

    DEFINITIONS = scriptdata(
               units= "$ si",
           dimension= 2,
            boundary= "$ sm sm p",
          atom_style= "$smd",
  neigh_modify_every= 5,
  neigh_modify_delay= 0,
         comm_modify= "$ vel yes",
              newton= "$ off",
         atom_modify= "$ map array",
          comm_style= "$ tiled"
        )

    TEMPLATE = """
# :INITIALIZE SECTION:
#   initialize styles, dimensions, boundaries and communivation

    ####################################################################################################
    # INITIALIZE LAMMPS
    ####################################################################################################
    units           ${units}
    dimension       ${dimension}
    boundary        ${boundary}
    atom_style      ${atom_style}
    neigh_modify    every ${neigh_modify_every} delay ${neigh_modify_delay} check yes
    comm_modify     ${comm_modify}
    newton          ${newton}
    atom_modify     ${atom_modify}
    comm_style      ${comm_style}
    """

# %% Geometry section template
class geometrysection(script):
    """ LAMMPS script: global session """
    name = "geometry"
    description = name+" section"
    position = 2
    section = 3
    userid = "example"
    version = 0.1

    DEFINITIONS = scriptdata(
         l0= 0.05,
       hgap= "0.25        # gap to prevent direct contact at t=0 (too much enery)",
  hsmallgap= "0.1   # gap to prevent direct contact at t=0 (too much enery)",
       hto1= "0.8         # height of to1 (the tongue to1, note 1 not l)",
       hto2= "0.5         # height of to2 (the tongue to2)",
       rsph= "0.3         # radius of spherical food particles",
       lpar= "0.6         # size of prismatic particles ",
    yfloor1= "${hgap}  # bottom position of to1, position of the first floor",
     yroof1= "${yfloor1}+${hto1} # bottom position of to1, position of the first floor",
   yfloor2a= "${yroof1}+${hsmallgap}  # position of the second floor / level a",
    yroof2a= "${yfloor2a}+${lpar}      # position of the second floor / level a",
   yfloor2b= "${yroof2a}+${hsmallgap} # position of the second floor / level b",
    yroof2b= "${yfloor2b}+${lpar}      # position of the second floor / level b",
   yfloor2c= "${yfloor2a}+${rsph}     # position of the second floor / level c",
    yroof2c= "${yfloor2c}+${rsph}      # position of the second floor / level c",
   yfloor2d= "${yroof2c}+${rsph}+${hsmallgap} # position of the second floor / level d",
    yroof2d= "${yfloor2d}+${rsph}      # position of the second floor / level d",
    yfloor3= 5.0,
     yroof3= "${yfloor3}+${hto2} # bottom position of to1",
   yfloor3a= "${yfloor3}-0.6",
    yroof3a= "${yfloor3}",
    crunchl= "${yfloor3}-${yfloor2a}-0.8",
    crunchp= 3,
    crunchw= "2*pi/${crunchp}",
    crunchd= "2*(sin((${crunchp}*${crunchw})/4)^2)/${crunchw}",
    crunchv= "${crunchl}/${crunchd}"
        )

    TEMPLATE = """
# :GEOMETRY SECTION:
#   Build geometry (very specific example)

    ####################################################################################################
    # CREATE INITIAL GEOMETRY
    # note there are 4 groups (create_box 5 box)
    # groupID 1 = saliva
    # groupID 2 = food
    # groupID 3 = mouth walls
    # groupID 4 = tongue alike (part1)
    # groupID 5 = also tongue but palate infact (part2)
    ####################################################################################################
    # create simulation box, a mouth, and a saliva column
    region          box block 0 12 0 8 -0.01 0.01 units box
    create_box      5 box
    region          saliva1 block 0.25 1.8 1.25 3.5 EDGE EDGE units box
    region          saliva2 block 10 11.65 1.25 4 EDGE EDGE units box
    region          mouth block 0.15 11.85 0.15 8 -0.01 0.01 units box side out # mouth
    lattice         sq ${l0}
    create_atoms    1 region saliva1
    create_atoms    1 region saliva2
    group           saliva type 1
    create_atoms    3 region mouth
    group           mouth type 3

    print "Crunch distance:${crunchl}"  # 3.65
    print "Crunch distance:${crunchv}"  # 0.1147


    # bottom part of the tongue: to1 (real tongue)
    # warning: all displacements are relative to the bottom part
    region          to1 block 1 11 ${yfloor1} ${yroof1} EDGE EDGE units box
    region          to2part1 block 0.5 11.5 ${yfloor3} ${yroof3} EDGE EDGE units box
    region          to2part2 block 5.5 6 ${yfloor3a} ${yroof3a} EDGE EDGE units box
    region          to2 union 2 to2part1 to2part2
    create_atoms    4 region to1
    create_atoms    5 region to2
    group           tongue1 type 4
    group           tongue2 type 5

    # create some solid objects to be pushed around
    region          pr1 prism 2 2.6 ${yfloor2a} ${yroof2a} EDGE EDGE 0.3 0 0 units box
    region          bl1 block 3 3.6 ${yfloor2a} ${yroof2a} EDGE EDGE units box
    region          sp1 sphere 4.3 ${yfloor2c} 0 ${rsph} units box
    region          sp2 sphere 5 ${yfloor2c} 0 ${rsph} units box
    region          sp3 sphere 5.7 ${yfloor2c} 0 ${rsph} units box
    region          sp4 sphere 6.4 ${yfloor2c} 0 ${rsph} units box
    region          sp5 sphere 7.1 ${yfloor2c} 0 ${rsph} units box
    region          sp6 sphere 6.05 ${yfloor2d} 0 ${rsph} units box
    region          br2 block 3 3.6 ${yfloor2b} ${yroof2b} EDGE EDGE units box

    # fill the regions with atoms (note that atoms = smoothed hydrodynamics particles)
    create_atoms    2 region pr1
    create_atoms    2 region bl1
    create_atoms    2 region sp1
    create_atoms    2 region sp2
    create_atoms    2 region sp3
    create_atoms    2 region sp4
    create_atoms    2 region sp5
    create_atoms    2 region sp6
    create_atoms    2 region br2

    # atoms of objects are grouped with two id
    # fix apply only to groups
    group           solidfoods type 2
    group           tlsph type 2

    # group heavy
    group           allheavy type 1:4


    """


# %% Discretization section template
class discretizationsection(script):
    """ LAMMPS script: discretization session """
    name = "discretization"
    description = name+" section"
    position = 3
    section = 4
    userid = "example"
    version = 0.1

    # inherit properties from geometrysection
    DEFINITIONS = geometrysection.DEFINITIONS + scriptdata(
              h= "2.5*${l0} # SPH kernel diameter",
        vol_one= "${l0}^2 # initial particle volume for 2d simulation",
     rho_saliva= 1000,
        rho_obj= 1300,
           skin= "${h} # Verlet list range",
  contact_scale= 1.5
        )

    TEMPLATE = """
# :DISCRETIZATION SECTION:
#   discretization

    ####################################################################################################
    # DISCRETIZATION PARAMETERS
    ####################################################################################################
    set             group all diameter ${h}
    set             group all smd/contact/radius ${l0}
    set             group all volume  ${vol_one}
    set             group all smd/mass/density ${rho_saliva}
    set             group solidfoods smd/mass/density ${rho_obj}
    set             group tongue1 smd/mass/density ${rho_obj}
    set             group tongue2 smd/mass/density ${rho_obj}
    neighbor        ${skin} bin

    """


# %% Boundary section template
class boundarysection(script):
    """ LAMMPS script: boundary session """
    name = "boundary"
    description = name+" section"
    position = 4
    section = 5
    userid = "example"
    version = 0.1

    # inherit properties from geometrysection
    DEFINITIONS = geometrysection.DEFINITIONS + scriptdata(
        gravity = -9.81,
        vector = "$ 0 1 0"
        )

    TEMPLATE = """
# :BOUNDARY SECTION:
#   boundary section

    ####################################################################################################
    # DEFINE BOUNDARY CONDITIONS
    #
    # note that the the particles constituting the mouth are simply not integrated in time,
    # thus these particles never move. This is equivalent to a fixed displacement boundary condition.
    ####################################################################################################
    fix             gfix allheavy gravity ${gravity} vector ${vector} # add gravity


    ####################################################################################################
    # moving top "tongue" (to2)
    ####################################################################################################
    variable vmouth equal -${crunchv}*sin(${crunchw}*time)
    fix             move_fix_tongue2 tongue2 smd/setvel 0 v_vmouth 0

    """

# %% Interactions section template
class interactionsection(script):
    """ LAMMPS script: interaction session """
    name = "interactions"
    description = name+" section"
    position = 5
    section = 6
    userid = "example"
    version = 0.1

    DEFINITIONS = globalsection.DEFINITIONS + \
                  geometrysection.DEFINITIONS + \
                  discretizationsection.DEFINITIONS

    TEMPLATE = """
# :INTERACTIONS SECTION:
#   Please use forcefield() to make a robust code

    ####################################################################################################
    # INTERACTION PHYSICS / MATERIAL MODEL
    # 3 different pair styles are used:
    #     - updated Lagrangian SPH for saliva
    #     - total Lagrangian SPH for solid objects
    #     - a repulsive Hertzian potential for contact forces between different physical bodies
    ####################################################################################################
    pair_style      hybrid/overlay smd/ulsph *DENSITY_CONTINUITY *VELOCITY_GRADIENT *NO_GRADIENT_CORRECTION &
                                   smd/tlsph smd/hertz ${contact_scale}
    pair_coeff      1 1 smd/ulsph *COMMON ${rho_saliva} ${c0} ${q1} ${Cp} 0 &
                    *EOS_TAIT 7.0 &
                    *END
    pair_coeff      2 2 smd/tlsph *COMMON ${rho_obj} ${E} ${nu} ${q1} ${q2} ${Hg} ${Cp} &
                    *STRENGTH_LINEAR_PLASTIC ${sigma_yield} ${hardening_food} &
                    *EOS_LINEAR &
                    *END
    pair_coeff      4 4 smd/tlsph *COMMON ${rho_obj} ${Etongue1} ${nu} ${q1} ${q2} ${Hg} ${Cp} &
                    *STRENGTH_LINEAR_PLASTIC ${sigma_yield} ${hardening_tongue} &
                    *EOS_LINEAR &
                    *END
    pair_coeff      5 5 smd/tlsph *COMMON ${rho_obj} ${Etongue2} ${nu} ${q1} ${q2} ${Hg} ${Cp} &
                    *STRENGTH_LINEAR_PLASTIC ${sigma_yield} ${hardening_tongue} &
                    *EOS_LINEAR &
                    *END

    pair_coeff      3 3 none   # wall-wall
    pair_coeff      1 2 smd/hertz ${contact_stiffness} # saliva-food
    pair_coeff      1 3 smd/hertz ${contact_wall} # saliva-wall
    pair_coeff      2 3 smd/hertz ${contact_wall} # food-wall
    pair_coeff      2 2 smd/hertz ${contact_stiffness} # food-food
    # add 4 (to1)
    pair_coeff      1 4 smd/hertz ${contact_stiffness} # saliva-tongue1
    pair_coeff      2 4 smd/hertz ${contact_stiffness} # food-tongue1
    pair_coeff      3 4 smd/hertz ${contact_wall} # wall-tongue1
    pair_coeff      4 4 smd/hertz ${contact_stiffness} # tongue1-tongue1
    # add 5 (to2)
    pair_coeff      1 5 smd/hertz ${contact_stiffness} # saliva-tongue2
    pair_coeff      2 5 smd/hertz ${contact_stiffness} # food-tongue2
    pair_coeff      3 5 smd/hertz ${contact_wall} # wall-tongue2
    pair_coeff      4 5 smd/hertz ${contact_stiffness} # tongue1-tongue2
    pair_coeff      5 5 smd/hertz ${contact_stiffness} # tongue2-tongue2

    """


# %% Time integration section template
class integrationsection(script):
    """ LAMMPS script: time integration session """
    name = "time integration"
    description = name+" section"
    position = 6
    section = 7
    userid = "example"
    version = 0.1

    DEFINITIONS = scriptdata(
              dt = 0.1,
   adjust_redius = "$ 1.01 10 15"
        )

    TEMPLATE = """
# :INTEGRATION SECTION:
#   Time integration conditions

    fix             dtfix tlsph smd/adjust_dt ${dt} # dynamically adjust time increment every step
    fix             integration_fix_water saliva smd/integrate_ulsph adjust_radius ${adjust_redius}
    fix             integration_fix_solids solidfoods smd/integrate_tlsph
    fix             integration_fix_tongue1 tongue1 smd/integrate_tlsph
    fix             integration_fix_tongue2 tongue2 smd/integrate_tlsph

    """


# %% Dump section template
class dumpsection(script):
    """ LAMMPS script: dump session """
    name = "dump"
    description = name+" section"
    position = 7
    section = 8
    userid = "example"
    version = 0.1

    DEFINITIONS = globalsection().DEFINITIONS

    TEMPLATE = """
# :DUMP SECTION:
#   Dump configuration

    ####################################################################################################
    # SPECIFY TRAJECTORY OUTPUT
    ####################################################################################################
    compute         eint all smd/internal/energy
    compute         contact_radius all smd/contact/radius
    compute         S solidfoods smd/tlsph/stress
    compute         nn saliva smd/ulsph/num/neighs
    compute         epl solidfoods smd/plastic/strain
    compute         vol all smd/vol
    compute         rho all smd/rho

    dump            dump_id all custom ${outstep} ${outputfile} id type x y &
                    fx fy vx vy c_eint c_contact_radius mol &
                    c_S[1] c_S[2] c_S[4] mass radius c_epl c_vol c_rho c_nn proc
    dump_modify     dump_id first yes

    """


# %% Status section template
class statussection(script):
    """ LAMMPS script: status session """
    name = "status"
    description = name+" section"
    position = 8
    section = 9
    userid = "example"
    version = 0.1

    DEFINITIONS = scriptdata(
        thermo = 100
        )

    TEMPLATE = """
# :STATUS SECTION:
#   Status configuration

    ####################################################################################################
    # STATUS OUTPUT
    ####################################################################################################
    compute         alleint all reduce sum c_eint
    variable        etot equal pe+ke+c_alleint+f_gfix # total energy of the system
    thermo          ${thermo}
    thermo_style    custom step ke pe v_etot c_alleint f_dtfix dt
    thermo_modify   lost ignore

    """


# %% Run section template
class runsection(script):
    """ LAMMPS script: run session """
    name = "run"
    description = name+" section"
    position = 9
    section = 10
    userid = "example"
    version = 0.1

    DEFINITIONS = globalsection.DEFINITIONS + scriptdata(
        balance = "$ 500 0.9 rcb"
        )

    TEMPLATE = """
# :RUN SECTION:
#   run configuration

    ####################################################################################################
    # RUN SIMULATION
    ####################################################################################################
    fix             balance_fix all balance ${balance} # load balancing for MPI
    run             ${tsim}
    """

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

    # example for debugging
    # from pizza.region import region
    # R = region(name="my region")
    # R.ellipsoid(0,0,0,1,1,1,name="E2",side="out",move=["left","${up}*3",None],up=0.1)
    # R.E2.VARIABLES.left = '"swiggle(%s,%s,%s)"%(${a},${b},${c})'
    # R.E2.VARIABLES.a="${b}-5"
    # R.E2.VARIABLES.b=5
    # R.E2.VARIABLES.c=100
    # code2 = R.E2.do()
    # p = R.E2.script
    # code = p.do(0)

    # example of scriptobject()
    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())

    collection = b1+b2+b3+b4
    grp_typ1 = collection.select(1)
    grpB = collection.group.B

    collection.interactions

    # main example of script()
    G = globalsection()
    print(G)
    c = initializesection()
    print(c)
    g = geometrysection()
    print(g)
    d = discretizationsection()
    print(d)
    b = boundarysection()
    print(b)
    i = interactionsection()
    print(i)
    t = integrationsection()
    print(t)
    d = dumpsection()
    print(d)
    s = statussection()
    print(s)
    r = runsection()
    print(r)

    # # all sections as a single script
    myscript = G+c+g+d+b+i+t+d+s+r
    p = pipescript()
    p | i
    p = collection | G
    p | i
    p[0]
    q = p | p
    q[0] = []
    p[0:1] = q[0:1]
    print("\n"*4,'='*80,'\n\n this is the full script\n\n','='*80,'\n')
    print(myscript.do())

    # pipe full demo
    p = G | c | g | d | b | i | t | d | s | r
    p.rename(["G","c","g","d","b","i","t","d","s","r"])
    cmd = p.do([0,1,4,7])
    sp = p.script([0,1,4,7])
    r = collection | p
    p[0:2]=p[0]*2

Functions

def frame_header(lines, padding=2, style=1, corner_symbols=None, horizontal_symbol=None, vertical_symbol=None, empty_line_symbol=None, line_fill_symbol=None, comment='#')

Format the header content into an ASCII framed box with customizable properties.

Parameters

lines (list or tuple): The lines to include in the header. - Empty strings "" are replaced with lines of line_fill_symbol. - None values are treated as empty lines.

padding (int, optional): Number of spaces to pad on each side of the content. Default is 2. style (int, optional): Style index (1 to 6) for predefined frame styles. Default is 1. corner_symbols (str or tuple, optional): Symbols for the corners (top-left, top-right, bottom-left, bottom-right). Can be a string (e.g., "+") for uniform corners. horizontal_symbol (str, optional): Symbol to use for horizontal lines. vertical_symbol (str, optional): Symbol to use for vertical lines. empty_line_symbol (str, optional): Symbol to use for empty lines inside the frame. line_fill_symbol (str, optional): Symbol to fill lines that replace empty strings. comment (str, optional): Comment symbol to prefix each line. Can be multiple characters. Default is "#".

Returns

str
The formatted header as a string.

Raises

ValueError
If the specified style is undefined or corner_symbols is invalid.
Expand source code
def frame_header(
    lines,
    padding=2,
    style=1,
    corner_symbols=None,  # Can be a string or a tuple
    horizontal_symbol=None,
    vertical_symbol=None,
    empty_line_symbol=None,
    line_fill_symbol=None,
    comment="#"
):
    """
    Format the header content into an ASCII framed box with customizable properties.

    Parameters:
        lines (list or tuple): The lines to include in the header.
            - Empty strings "" are replaced with lines of `line_fill_symbol`.
            - None values are treated as empty lines.

        padding (int, optional): Number of spaces to pad on each side of the content. Default is 2.
        style (int, optional): Style index (1 to 6) for predefined frame styles. Default is 1.
        corner_symbols (str or tuple, optional): Symbols for the corners (top-left, top-right, bottom-left, bottom-right).
                                                 Can be a string (e.g., "+") for uniform corners.
        horizontal_symbol (str, optional): Symbol to use for horizontal lines.
        vertical_symbol (str, optional): Symbol to use for vertical lines.
        empty_line_symbol (str, optional): Symbol to use for empty lines inside the frame.
        line_fill_symbol (str, optional): Symbol to fill lines that replace empty strings.
        comment (str, optional): Comment symbol to prefix each line. Can be multiple characters. Default is "#".

    Returns:
        str: The formatted header as a string.

    Raises:
        ValueError: If the specified style is undefined or `corner_symbols` is invalid.
    """
    # Predefined styles
    styles = {
        1: {
            "corner_symbols": ("+", "+", "+", "+"),
            "horizontal_symbol": "-",
            "vertical_symbol": "|",
            "empty_line_symbol": " ",
            "line_fill_symbol": "-"
        },
        2: {
            "corner_symbols": ("╔", "╗", "╚", "╝"),
            "horizontal_symbol": "═",
            "vertical_symbol": "║",
            "empty_line_symbol": " ",
            "line_fill_symbol": "═"
        },
        3: {
            "corner_symbols": (".", ".", "'", "'"),
            "horizontal_symbol": "-",
            "vertical_symbol": "|",
            "empty_line_symbol": " ",
            "line_fill_symbol": "-"
        },
        4: {
            "corner_symbols": ("#", "#", "#", "#"),
            "horizontal_symbol": "=",
            "vertical_symbol": "#",
            "empty_line_symbol": " ",
            "line_fill_symbol": "="
        },
        5: {
            "corner_symbols": ("┌", "┐", "└", "┘"),
            "horizontal_symbol": "─",
            "vertical_symbol": "│",
            "empty_line_symbol": " ",
            "line_fill_symbol": "─"
        },
        6: {
            "corner_symbols": (".", ".", ".", "."),
            "horizontal_symbol": ".",
            "vertical_symbol": ":",
            "empty_line_symbol": " ",
            "line_fill_symbol": "."
        }
    }

    # Validate style and set defaults
    if style not in styles:
        raise ValueError(f"Undefined style {style}. Valid styles are {list(styles.keys())}.")

    selected_style = styles[style]

    # Convert corner_symbols to a tuple of 4 values
    if isinstance(corner_symbols, str):
        corner_symbols = (corner_symbols,) * 4
    elif isinstance(corner_symbols, (list, tuple)) and len(corner_symbols) == 1:
        corner_symbols = tuple(corner_symbols * 4)
    elif isinstance(corner_symbols, (list, tuple)) and len(corner_symbols) == 2:
        corner_symbols = (corner_symbols[0], corner_symbols[1], corner_symbols[0], corner_symbols[1])
    elif corner_symbols is None:
        corner_symbols = selected_style["corner_symbols"]
    elif not isinstance(corner_symbols, (list, tuple)) or len(corner_symbols) != 4:
        raise ValueError("corner_symbols must be a string or a tuple/list of 1, 2, or 4 elements.")

    # Apply overrides or defaults
    horizontal_symbol = horizontal_symbol or selected_style["horizontal_symbol"]
    vertical_symbol = vertical_symbol or selected_style["vertical_symbol"]
    empty_line_symbol = empty_line_symbol or selected_style["empty_line_symbol"]
    line_fill_symbol = line_fill_symbol or selected_style["line_fill_symbol"]

    # Process lines: Replace "" with line_fill placeholders, None with empty lines
    processed_lines = []
    max_content_width = 0
    for line in lines:
        if line == "":
            processed_lines.append("<LINE_FILL>")
        elif line is None:
            processed_lines.append(None)
        else:
            processed_lines.append(line)
            max_content_width = max(max_content_width, len(line))

    # Adjust width for padding
    frame_width = max_content_width + padding * 2

    # Build the top border
    top_border = f"{corner_symbols[0]}{horizontal_symbol * frame_width}{corner_symbols[1]}"

    # Build content lines with vertical borders
    framed_lines = [top_border]
    for line in processed_lines:
        if line is None:
            empty_line = f"{vertical_symbol}{empty_line_symbol * frame_width}{vertical_symbol}"
            framed_lines.append(empty_line)
        elif line == "<LINE_FILL>":
            fill_line = f"{vertical_symbol}{line_fill_symbol * frame_width}{vertical_symbol}"
            framed_lines.append(fill_line)
        else:
            content = line.center(frame_width)
            framed_line = f"{vertical_symbol}{content}{vertical_symbol}"
            framed_lines.append(framed_line)

    # Build the bottom border
    bottom_border = f"{corner_symbols[2]}{horizontal_symbol * frame_width}{corner_symbols[3]}"
    framed_lines.append(bottom_border)

    # Ensure all lines start with the comment symbol
    commented_lines = [
        line if line.startswith(comment) else f"{comment} {line}" for line in framed_lines
    ]

    return "\n".join(commented_lines)+"\n"
def get_metadata()

Return a dictionary of explicitly defined metadata.

Expand source code
def get_metadata():
    """Return a dictionary of explicitly defined metadata."""
    # Define the desired metadata keys
    metadata_keys = [
        "__project__",
        "__author__",
        "__copyright__",
        "__credits__",
        "__license__",
        "__maintainer__",
        "__email__",
        "__version__",
    ]
    # Filter only the desired keys from the current module's globals
    return {key.strip("_"): globals()[key] for key in metadata_keys if key in globals()}
def get_tmp_location()
Expand source code
get_tmp_location = lambda: tempfile.gettempdir()
def is_scalar(val)

Determines if a value is scalar (not a list, dict, or tuple).

Expand source code
def is_scalar(val):
    """
    Determines if a value is scalar (not a list, dict, or tuple).
    """
    return not isinstance(val, (list, dict, tuple))
def make_hashable(val)

Recursively converts lists and dictionaries to tuples to make them hashable.

Expand source code
def make_hashable(val):
    """
    Recursively converts lists and dictionaries to tuples to make them hashable.
    """
    if isinstance(val, list):
        return tuple(make_hashable(item) for item in val)
    elif isinstance(val, dict):
        return tuple(sorted((k, make_hashable(v)) for k, v in val.items()))
    return val
def picker(L, indices)
Expand source code
def picker(L,indices): return [L[i] for i in indices if (i>=0 and i<len(L))]
def remove_comments(content, split_lines=False, emptylines=False, comment_chars='#', continuation_marker='\\\\', remove_continuation_marker=False)

Removes comments from a single or multi-line string, handling quotes, escaped characters, and line continuation.

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. emptylines : bool, optional (default: False) If True, empty lines will be preserved in the output. If False, empty lines will be removed from the output. comment_chars : str, optional (default: "#") A string containing characters to identify the start of a comment. Any of these characters will mark the beginning of a comment unless within quotes. continuation_marker : str or None, optional (default: "\") A string containing characters to indicate line continuation (use \ to specify). Any characters after the continuation marker are ignored as a comment. If set to None or an empty string, line continuation will not be processed. remove_continuation_marker : bool, optional (default: False) If True, the continuation marker itself is removed from the processed line, keeping only the characters before it. If False, the marker is retained as part of the line.

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, emptylines=False, comment_chars="#", continuation_marker="\\\\", remove_continuation_marker=False):
    """
    Removes comments from a single or multi-line string, handling quotes, escaped characters, and line continuation.

    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.
    emptylines : bool, optional (default: False)
        If True, empty lines will be preserved in the output. If False, empty lines
        will be removed from the output.
    comment_chars : str, optional (default: "#")
        A string containing characters to identify the start of a comment.
        Any of these characters will mark the beginning of a comment unless within quotes.
    continuation_marker : str or None, optional (default: "\\\\")
        A string containing characters to indicate line continuation (use `\\` to specify).
        Any characters after the continuation marker are ignored as a comment. If set to `None`
        or an empty string, line continuation will not be processed.
    remove_continuation_marker : bool, optional (default: False)
        If True, the continuation marker itself is removed from the processed line, keeping
        only the characters before it. If False, the marker is retained as part of the line.

    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 and handle line continuation within a single line while managing quotes and escapes."""
        in_single_quote = False
        in_double_quote = False
        escaped = False
        result = []

        i = 0
        while i < len(line):
            char = line[i]

            if escaped:
                result.append(char)
                escaped = False
                i += 1
                continue

            # Handle escape character within quoted strings
            if char == '\\' and (in_single_quote or in_double_quote):
                escaped = True
                result.append(char)
                i += 1
                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

            # Check for line continuation marker if it's set and outside of quotes
            if continuation_marker and not in_single_quote and not in_double_quote:
                # Check if the remaining part of the line matches the continuation marker
                if line[i:].startswith(continuation_marker):
                    # Optionally remove the continuation marker
                    if remove_continuation_marker:
                        result.append(line[:i].rstrip())  # Keep everything before the marker
                    else:
                        result.append(line[:i + len(continuation_marker)].rstrip())  # Include the marker itself
                    return ''.join(result).strip()

            # Check for comment start characters outside of quotes
            if char in comment_chars and not in_single_quote and not in_double_quote:
                break  # Stop processing the line when a comment is found

            result.append(char)
            i += 1

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

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

    # Process each line, considering the emptylines flag
    processed_lines = []
    for line in lines:
        stripped_line = line.strip()
        if not stripped_line and not emptylines:
            continue  # Skip empty lines if emptylines is False
        if any(stripped_line.startswith(c) for c in comment_chars):
            continue  # Skip lines that are pure comments
        processed_line = process_line(line)
        if processed_line or emptylines:  # Only add non-empty lines if emptylines is False
            processed_lines.append(processed_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
def span(vector, sep=' ', left='', right='')
Expand source code
def span(vector,sep=" ",left="",right=""):
    return left + (vector if isinstance(vector, str) else sep.join(map(str, vector))) + right if vector is not None else ""

Classes

class CallableScript (func)

A descriptor that allows the method Interactions to be accessed both as a property and as a callable function.

This class enables a method to behave like a property when accessed without parentheses, returning a function that can be called with default parameters. It also allows the method to be called directly with optional parameters, providing flexibility in usage.

Attributes:

func : function The original function that is decorated, which will be used for both property access and direct calls.

Methods:

get(self, instance, owner) Returns a lambda function to call the original function with default parameters when accessed as a property.

call(self, instance, printflag=False, verbosity=2) Allows the original function to be called directly with specified parameters.

Expand source code
class CallableScript:
    """
    A descriptor that allows the method Interactions to be accessed both as a property and as a callable function.

    This class enables a method to behave like a property when accessed without parentheses,
    returning a function that can be called with default parameters. It also allows the method
    to be called directly with optional parameters, providing flexibility in usage.

    Attributes:
    -----------
    func : function
        The original function that is decorated, which will be used for both property access
        and direct calls.

    Methods:
    --------
    __get__(self, instance, owner)
        Returns a lambda function to call the original function with default parameters
        when accessed as a property.

    __call__(self, instance, printflag=False, verbosity=2)
        Allows the original function to be called directly with specified parameters.
    """
    def __init__(self, func):
        self.func = func

    def __get__(self, instance, owner):
        # When accessed as a property, return a lambda that calls the original function
        return lambda printflag=False, verbosity=2, verbose=None: self.func(instance, printflag=printflag, verbosity=verbosity, verbose=verbose)

    def __call__(self, instance, printflag=False, verbosity=2, verbose=None):
        # Allow calling the function directly with specified parameters
        return self.func(instance, printflag=printflag, verbosity=verbosity)
class VariableOccurrences (data, variables=None)

The VariableOccurrences class encapsulates raw data and provides utility methods for analyzing variable occurrences across various scopes. It is designed to handle both single and multiple variables, supporting flexible data structures and analysis operations.

Class Methods:

  • __init__(data, variables=None): Initializes the class with raw data and optional variable names.
  • _determine_scopes(): Determines the unique scopes present across all variables.
  • get_raw_data(): Returns the raw data as provided during initialization.
  • get_steps_with_value(value, within_list=False): Retrieves steps where the variable equals the specified value.
  • get_all_values(): Retrieves all unique values of the variable(s).
  • get_all_elements_in_lists(): Retrieves all unique elements within list-type variable values.
  • get_usage_count(value, within_list=False): Counts occurrences of a specific value.
  • get_steps_with_value_in_scope(variable, scope, value, within_list=False): Retrieves steps where a variable equals a value within a specific scope.
  • summarize(): Summarizes the occurrences of variables, including counts and unique elements.
  • export(filename=None, scopes='all', variables='all', include_headers=True, return_content=False): Exports variable occurrences as Markdown, plain text, or HTML.

Attributes:

  • data: Dictionary containing the raw variable data.
  • Single variable: {'scope1': [...], 'scope2': [...]}.
  • Multiple variables: {'var1': {...}, 'var2': {...}, ...}.
  • variables: List of variable names managed by the instance.
  • scopes: List of unique scopes across all variables.

Usage:

The class is useful for tracking and analyzing variable values across different contexts, such as configuration files, programming environments, or simulation data. It supports advanced querying, summary generation, and export functionality.

Example:

Initialize with single variable

data = {'global': [(0, 1), (1, 2)], 'local': [(0, 3), (1, 4)]} vo = VariableOccurrences(data, variables="var1")

Initialize with multiple variables

data = { "var1": {"global": [(0, 1), (1, 2)], "local": [(0, 3), (1, 4)]}, "var2": {"global": [1, 2], "local": [3, 4]}, } vo = VariableOccurrences(data)

Query steps where a value is present

steps = vo.get_steps_with_value(2)

Export data to a Markdown file

vo.export("output.md")

Initializes the VariableOccurrences object.

Parameters: - data: - For single variable: dict with scopes as keys and lists of values or a single value. - For multiple variables: dict mapping variable names to their respective scope data. - variables (str or list/tuple, optional): - If single variable: string representing the variable name. - If multiple variables: list or tuple of variable names. - If None: assumes data is for multiple variables without specifying names.

Expand source code
class VariableOccurrences:
    """
    The `VariableOccurrences` class encapsulates raw data and provides utility methods for analyzing variable occurrences
    across various scopes. It is designed to handle both single and multiple variables, supporting flexible data structures
    and analysis operations.

    Class Methods:
    --------------
    - `__init__(data, variables=None)`: Initializes the class with raw data and optional variable names.
    - `_determine_scopes()`: Determines the unique scopes present across all variables.
    - `get_raw_data()`: Returns the raw data as provided during initialization.
    - `get_steps_with_value(value, within_list=False)`: Retrieves steps where the variable equals the specified value.
    - `get_all_values()`: Retrieves all unique values of the variable(s).
    - `get_all_elements_in_lists()`: Retrieves all unique elements within list-type variable values.
    - `get_usage_count(value, within_list=False)`: Counts occurrences of a specific value.
    - `get_steps_with_value_in_scope(variable, scope, value, within_list=False)`: Retrieves steps where a variable equals a value within a specific scope.
    - `summarize()`: Summarizes the occurrences of variables, including counts and unique elements.
    - `export(filename=None, scopes='all', variables='all', include_headers=True, return_content=False)`:
      Exports variable occurrences as Markdown, plain text, or HTML.

    Attributes:
    -----------
    - `data`: Dictionary containing the raw variable data.
      - Single variable: `{'scope1': [...], 'scope2': [...]}`.
      - Multiple variables: `{'var1': {...}, 'var2': {...}, ...}`.
    - `variables`: List of variable names managed by the instance.
    - `scopes`: List of unique scopes across all variables.

    Usage:
    ------
    The class is useful for tracking and analyzing variable values across different contexts, such as configuration files,
    programming environments, or simulation data. It supports advanced querying, summary generation, and export functionality.

    Example:
    --------
    # Initialize with single variable
    data = {'global': [(0, 1), (1, 2)], 'local': [(0, 3), (1, 4)]}
    vo = VariableOccurrences(data, variables="var1")

    # Initialize with multiple variables
    data = {
        "var1": {"global": [(0, 1), (1, 2)], "local": [(0, 3), (1, 4)]},
        "var2": {"global": [1, 2], "local": [3, 4]},
    }
    vo = VariableOccurrences(data)

    # Query steps where a value is present
    steps = vo.get_steps_with_value(2)

    # Export data to a Markdown file
    vo.export("output.md")
    """

    def __init__(self, data, variables=None):
        """
        Initializes the VariableOccurrences object.

        Parameters:
        - data:
            - For single variable: dict with scopes as keys and lists of values or a single value.
            - For multiple variables: dict mapping variable names to their respective scope data.
        - variables (str or list/tuple, optional):
            - If single variable: string representing the variable name.
            - If multiple variables: list or tuple of variable names.
            - If None: assumes data is for multiple variables without specifying names.
        """
        if variables is None:
            # Assume data is a dict mapping variable names to their scope data
            if not isinstance(data, dict):
                raise ValueError("For multiple variables, data must be a dict mapping variable names to their scope data.")
            self.variables = list(data.keys())
            self.data = data  # {var1: {scope1: [...], scope2: [...]}, var2: {...}, ...}
        elif isinstance(variables, str):
            # Single variable case
            self.variables = [variables]
            if not isinstance(data, dict):
                raise ValueError("For single variable, data must be a dict with scopes as keys and lists of values or single values as values.")
            self.data = {variables: data}  # {var: {scope1: [...], scope2: [...], ...}}
        elif isinstance(variables, (list, tuple)):
            # Multiple variables specified
            if not isinstance(data, dict):
                raise ValueError("For multiple variables, data must be a dict mapping variable names to their scope data.")
            if set(variables) > set(data.keys()):
                missing_vars = set(variables) - set(data.keys())
                raise ValueError(f"Data does not contain entries for variables: {', '.join(missing_vars)}")
            self.variables = list(variables)
            self.data = {var: data[var] for var in variables}  # {var1: {...}, var2: {...}, ...}
        else:
            raise ValueError("Parameter 'variables' must be a string, list, tuple, or None.")

        self.scopes = self._determine_scopes()

    def _determine_scopes(self):
        """Determines the unique scopes present across all variables."""
        scopes = set()
        for var in self.variables:
            var_scopes = self.data[var].keys()
            scopes.update(scope.lower() for scope in var_scopes)
        return sorted(list(scopes))

    def get_raw_data(self):
        """
        Returns the raw data.

        Returns:
        - The raw data as provided during initialization.
        """
        return self.data

    def get_steps_with_value(self, value, within_list=False):
        """
        Retrieves the steps where the variable equals the specified value.

        Parameters:
        - value: The value to search for.
        - within_list (bool): If True, searches for the value within list-type variable values.

        Returns:
        - If single variable:
            - A dict with scopes as keys and lists of step indices or values as values.
        - If multiple variables:
            - A dict mapping each variable name to their respective scope-step/value mappings.
        """
        result = {}
        for var in self.variables:
            var_result = {}
            for scope, occurrences in self.data[var].items():
                if is_scalar(occurrences):
                    # Scalar value
                    if within_list:
                        continue  # Cannot search within a scalar
                    if occurrences == value:
                        var_result[scope] = occurrences
                elif isinstance(occurrences, list):
                    # List of values or list of tuples
                    steps = []
                    for item in occurrences:
                        if isinstance(item, tuple) and len(item) == 2:
                            step, val = item
                            if within_list:
                                if isinstance(val, list) and value in val:
                                    steps.append(step)
                            else:
                                if val == value:
                                    steps.append(step)
                        else:
                            # List of values
                            if within_list:
                                if isinstance(item, list) and value in item:
                                    steps.append(item)
                            else:
                                if item == value:
                                    steps.append(item)
                    if steps:
                        var_result[scope] = steps
                else:
                    # Other types (e.g., dict), can be extended as needed
                    pass
            if var_result:
                result[var] = var_result
        return result

    def get_all_values(self):
        """
        Retrieves all unique values of the variable(s).

        Returns:
        - A dict mapping each variable to its set of unique values per scope.
        """
        result = {}
        for var in self.variables:
            var_unique = {}
            for scope, occurrences in self.data[var].items():
                unique_vals = set()
                if is_scalar(occurrences):
                    unique_vals.add(occurrences)
                elif isinstance(occurrences, list):
                    for item in occurrences:
                        if isinstance(item, tuple) and len(item) == 2:
                            _, val = item
                            hashable_val = make_hashable(val)
                            unique_vals.add(hashable_val)
                        else:
                            hashable_val = make_hashable(item)
                            unique_vals.add(hashable_val)
                var_unique[scope] = unique_vals
            result[var] = var_unique
        return result

    def get_all_elements_in_lists(self):
        """
        Retrieves all unique elements within list-type variable values.

        Returns:
        - A dict mapping each variable to its set of unique elements in lists per scope.
        """
        result = {}
        for var in self.variables:
            var_elements = {}
            for scope, occurrences in self.data[var].items():
                unique_elems = set()
                if is_scalar(occurrences):
                    if isinstance(occurrences, list):
                        unique_elems.update(occurrences)
                elif isinstance(occurrences, list):
                    for item in occurrences:
                        if isinstance(item, tuple) and len(item) == 2:
                            _, val = item
                            if isinstance(val, list):
                                unique_elems.update(val)
                        elif isinstance(item, list):
                            unique_elems.update(item)
                var_elements[scope] = unique_elems
            result[var] = var_elements
        return result

    def get_usage_count(self, value, within_list=False):
        """
        Counts how many times a specific value is used.

        Parameters:
        - value: The value to count.
        - within_list (bool): If True, counts occurrences within list-type variable values.

        Returns:
        - If single variable:
            - A dict with scopes as keys and integer counts or counts of values as values.
        - If multiple variables:
            - A dict mapping each variable name to their respective scope-count/value mappings.
        """
        result = {}
        for var in self.variables:
            var_count = {}
            for scope, occurrences in self.data[var].items():
                count = 0
                if is_scalar(occurrences):
                    if within_list:
                        if isinstance(occurrences, list) and value in occurrences:
                            count += 1
                    else:
                        if occurrences == value:
                            count += 1
                elif isinstance(occurrences, list):
                    for item in occurrences:
                        if isinstance(item, tuple) and len(item) == 2:
                            _, val = item
                            if within_list:
                                if isinstance(val, list) and value in val:
                                    count += 1
                            else:
                                if val == value:
                                    count += 1
                        else:
                            if within_list:
                                if isinstance(item, list) and value in item:
                                    count += 1
                            else:
                                if item == value:
                                    count += 1
                if count > 0:
                    var_count[scope] = count
            if var_count:
                result[var] = var_count
        return result

    def get_steps_with_value_in_scope(self, variable, scope, value, within_list=False):
        """
        Retrieves the steps within a specific scope where the variable equals the specified value.

        Parameters:
        - variable (str): The variable name.
        - scope (str): The scope to search within ("static", "global", "local").
        - value: The value to search for.
        - within_list (bool): If True, searches for the value within list-type variable values.

        Returns:
        - A list of step indices or values where the variable equals the value within the specified scope.
        """
        if variable not in self.variables:
            raise ValueError(f"Variable '{variable}' is not managed by this instance.")

        scope = scope.lower()
        occurrences = self.data[variable].get(scope, [])
        steps = []

        if is_scalar(occurrences):
            if within_list:
                if isinstance(occurrences, list) and value in occurrences:
                    steps.append(occurrences)
            else:
                if occurrences == value:
                    steps.append(occurrences)
        elif isinstance(occurrences, list):
            for item in occurrences:
                if isinstance(item, tuple) and len(item) == 2:
                    step, val = item
                    if within_list:
                        if isinstance(val, list) and value in val:
                            steps.append(step)
                    else:
                        if val == value:
                            steps.append(step)
                else:
                    if within_list:
                        if isinstance(item, list) and value in item:
                            steps.append(item)
                    else:
                        if item == value:
                            steps.append(item)
        return steps

    def summarize(self):
        """
        Provides a summary of the variable occurrences.

        Returns:
        - A dict mapping each variable to their respective summaries per scope.
        """
        summary = {}
        for var in self.variables:
            var_summary = {}
            for scope, occurrences in self.data[var].items():
                unique_vals = set()
                unique_elements = set()
                value_counts = defaultdict(int)
                element_counts = defaultdict(int)

                if is_scalar(occurrences):
                    # Scalar value
                    hashable_val = make_hashable(occurrences)
                    unique_vals.add(hashable_val)
                    value_counts[hashable_val] += 1
                    if isinstance(occurrences, list):
                        unique_elements.update(occurrences)
                        for elem in occurrences:
                            element_counts[elem] += 1
                elif isinstance(occurrences, list):
                    for item in occurrences:
                        if isinstance(item, tuple) and len(item) == 2:
                            # Tuple: (step, value)
                            step, val = item
                            hashable_val = make_hashable(val)
                            unique_vals.add(hashable_val)
                            value_counts[hashable_val] += 1
                            if isinstance(val, list):
                                unique_elements.update(val)
                                for elem in val:
                                    element_counts[elem] += 1
                            elif isinstance(val, dict):
                                # Handle nested dictionaries if necessary
                                for sub_val in val.values():
                                    if isinstance(sub_val, list):
                                        unique_elements.update(sub_val)
                                        for elem in sub_val:
                                            element_counts[elem] += 1
                        else:
                            # Direct value
                            hashable_val = make_hashable(item)
                            unique_vals.add(hashable_val)
                            value_counts[hashable_val] += 1
                            if isinstance(item, list):
                                unique_elements.update(item)
                                for elem in item:
                                    element_counts[elem] += 1
                            elif isinstance(item, dict):
                                for sub_val in item.values():
                                    if isinstance(sub_val, list):
                                        unique_elements.update(sub_val)
                                        for elem in sub_val:
                                            element_counts[elem] += 1
                else:
                    # Other types can be handled here if needed
                    pass

                var_summary[scope] = {
                    "total_occurrences": len(occurrences),
                    "unique_values": unique_vals,
                    "unique_elements_in_lists": unique_elements,
                    "value_counts": dict(value_counts),
                    "element_counts_in_lists": dict(element_counts)
                }
            summary[var] = var_summary
        return summary

    def export(self, filename=None, scopes='all', variables='all', include_headers=True, return_content=False):
        """
        Exports the variable occurrences to a file or returns the content as a string.

        Parameters:
        - filename (str, optional): Path to the output file. Must end with .md, .txt, or .html. Required if return_content is False.
        - scopes (str or list/tuple, optional): 'all', a single scope string, or a list of scope strings. Defaults to 'all'.
        - variables (str or list/tuple, optional): 'all', a single variable string, or a list of variable strings. Defaults to 'all'.
        - include_headers (bool, optional): If True, includes headers in the exported content. Defaults to True.
        - return_content (bool, optional): If True, returns the content as a string instead of writing to a file. Defaults to False.

        Returns:
        - str: The exported content as a string if return_content is True.
        - None: Writes to file if return_content is False.

        Raises:
        - ValueError: If 'filename' is not provided when return_content is False.
        - ValueError: If 'scopes' or 'variables' are of incorrect types.
        """
        # Determine file extension if filename is provided
        if filename:
            _, ext = os.path.splitext(filename)
            ext = ext.lower()

            if ext in ['.md', '.txt']:
                export_format = 'markdown'
            elif ext == '.html':
                export_format = 'html'
            else:
                raise ValueError("Unsupported file extension. Supported extensions are .md, .txt, and .html.")
        elif not return_content:
            raise ValueError("Filename must be provided if return_content is False.")

        # Determine scopes
        if isinstance(scopes, str):
            if scopes.lower() == 'all':
                selected_scopes = self.scopes
            else:
                selected_scopes = [scopes.lower()]
        elif isinstance(scopes, (list, tuple)):
            scopes_lower = [s.lower() for s in scopes]
            if 'all' in scopes_lower:
                selected_scopes = self.scopes
            else:
                selected_scopes = scopes_lower
        else:
            raise ValueError("Parameter 'scopes' must be a string or a list/tuple of strings.")

        # Determine variables
        if isinstance(variables, str):
            if variables.lower() == 'all':
                selected_variables = self.variables
            else:
                if variables not in self.variables:
                    print(f"Warning: Variable '{variables}' not managed by this instance.")
                    selected_variables = []
                else:
                    selected_variables = [variables]
        elif isinstance(variables, (list, tuple)):
            variables_lower = [v for v in variables]
            selected_variables = [v for v in variables_lower if v in self.variables]
            missing_vars = set(variables_lower) - set(selected_variables)
            if missing_vars:
                print(f"Warning: Variables '{', '.join(missing_vars)}' not managed by this instance.")
        else:
            raise ValueError("Parameter 'variables' must be a string or a list/tuple of strings.")

        # Generate content
        content = ""
        if include_headers:
            if export_format == 'markdown':
                content += f"## Variable: `{self.variables[0]}`\n\n" if len(self.variables) == 1 else ""
            elif export_format == 'html':
                content += f"<h2>Variable: {self.variables[0]}</h2>\n" if len(self.variables) == 1 else ""

        summary = self.summarize()
        for var in selected_variables:
            if include_headers:
                if export_format == 'markdown':
                    content += f"### Variable: `{var}`\n\n"
                elif export_format == 'html':
                    content += f"<h3>Variable: {var}</h3>\n"
            var_summary = summary[var]
            for scope, details in var_summary.items():
                if scope not in selected_scopes:
                    continue
                if include_headers or True:
                    if export_format == 'markdown':
                        content += f"#### Scope: {scope.capitalize()}\n\n"
                    elif export_format == 'html':
                        content += f"<h4>Scope: {scope.capitalize()}</h4>\n"
                # Add content based on format
                if export_format == 'markdown':
                    content += f"- **Total Occurrences**: {details['total_occurrences']}\n"
                    unique_vals_formatted = ', '.join(map(str, details['unique_values']))
                    content += f"- **Unique Values**: {unique_vals_formatted}\n"
                    if details['unique_elements_in_lists']:
                        unique_elems_formatted = ', '.join(map(str, details['unique_elements_in_lists']))
                        content += f"- **Unique Elements in Lists**: {unique_elems_formatted}\n\n"
                        # Element Counts Table
                        content += "**Element Counts in Lists:**\n\n"
                        content += "| Element | Count |\n"
                        content += "|---------|-------|\n"
                        for elem, count in details['element_counts_in_lists'].items():
                            content += f"| {elem} | {count} |\n"
                        content += "\n"
                elif export_format == 'html':
                    content += "<ul>"
                    content += f"<li><strong>Total Occurrences</strong>: {details['total_occurrences']}</li>"
                    unique_vals_formatted = ', '.join(map(str, details['unique_values']))
                    content += f"<li><strong>Unique Values</strong>: {unique_vals_formatted}</li>"
                    if details['unique_elements_in_lists']:
                        unique_elems_formatted = ', '.join(map(str, details['unique_elements_in_lists']))
                        content += f"<li><strong>Unique Elements in Lists</strong>: {unique_elems_formatted}</li>"
                    content += "</ul>\n"

                    if details['element_counts_in_lists']:
                        content += "<h5>Element Counts in Lists:</h5>\n"
                        content += "<table>\n<tr><th>Element</th><th>Count</th></tr>\n"
                        for elem, count in details['element_counts_in_lists'].items():
                            content += f"<tr><td>{elem}</td><td>{count}</td></tr>\n"
                        content += "</table>\n"

            # Add a horizontal line between variables
            if include_headers and len(selected_variables) > 1:
                if export_format == 'markdown':
                    content += "\n---\n\n"
                elif export_format == 'html':
                    content += "<hr/>\n"

        # Handle format-specific headers
        if return_content:
            if export_format == 'markdown':
                return content
            elif export_format == 'html':
                return f"<html><head><meta charset='UTF-8'><title>Variable Report</title></head><body>{content}</body></html>"
        else:
            if not filename:
                raise ValueError("Filename must be provided if return_content is False.")

            # Determine export format based on file extension
            if ext in ['.md', '.txt']:
                export_format = 'markdown'
            elif ext == '.html':
                export_format = 'html'
            else:
                raise ValueError("Unsupported file extension. Supported extensions are .md, .txt, and .html.")

            # Prepare full content for HTML
            if export_format == 'html' and not include_headers:
                full_content = f"<!DOCTYPE html>\n<html>\n<head>\n<meta charset='UTF-8'>\n<title>Variable Report</title>\n</head>\n<body>\n{content}\n</body>\n</html>"
            else:
                full_content = content

            # Write to file
            try:
                with open(filename, 'w', encoding='utf-8') as file:
                    file.write(full_content)
                print(f"Report successfully generated at '{filename}'.")
            except Exception as e:
                raise Exception(f"Failed to write the report to '{filename}': {e}")

Methods

def export(self, filename=None, scopes='all', variables='all', include_headers=True, return_content=False)

Exports the variable occurrences to a file or returns the content as a string.

Parameters: - filename (str, optional): Path to the output file. Must end with .md, .txt, or .html. Required if return_content is False. - scopes (str or list/tuple, optional): 'all', a single scope string, or a list of scope strings. Defaults to 'all'. - variables (str or list/tuple, optional): 'all', a single variable string, or a list of variable strings. Defaults to 'all'. - include_headers (bool, optional): If True, includes headers in the exported content. Defaults to True. - return_content (bool, optional): If True, returns the content as a string instead of writing to a file. Defaults to False.

Returns: - str: The exported content as a string if return_content is True. - None: Writes to file if return_content is False.

Raises: - ValueError: If 'filename' is not provided when return_content is False. - ValueError: If 'scopes' or 'variables' are of incorrect types.

Expand source code
def export(self, filename=None, scopes='all', variables='all', include_headers=True, return_content=False):
    """
    Exports the variable occurrences to a file or returns the content as a string.

    Parameters:
    - filename (str, optional): Path to the output file. Must end with .md, .txt, or .html. Required if return_content is False.
    - scopes (str or list/tuple, optional): 'all', a single scope string, or a list of scope strings. Defaults to 'all'.
    - variables (str or list/tuple, optional): 'all', a single variable string, or a list of variable strings. Defaults to 'all'.
    - include_headers (bool, optional): If True, includes headers in the exported content. Defaults to True.
    - return_content (bool, optional): If True, returns the content as a string instead of writing to a file. Defaults to False.

    Returns:
    - str: The exported content as a string if return_content is True.
    - None: Writes to file if return_content is False.

    Raises:
    - ValueError: If 'filename' is not provided when return_content is False.
    - ValueError: If 'scopes' or 'variables' are of incorrect types.
    """
    # Determine file extension if filename is provided
    if filename:
        _, ext = os.path.splitext(filename)
        ext = ext.lower()

        if ext in ['.md', '.txt']:
            export_format = 'markdown'
        elif ext == '.html':
            export_format = 'html'
        else:
            raise ValueError("Unsupported file extension. Supported extensions are .md, .txt, and .html.")
    elif not return_content:
        raise ValueError("Filename must be provided if return_content is False.")

    # Determine scopes
    if isinstance(scopes, str):
        if scopes.lower() == 'all':
            selected_scopes = self.scopes
        else:
            selected_scopes = [scopes.lower()]
    elif isinstance(scopes, (list, tuple)):
        scopes_lower = [s.lower() for s in scopes]
        if 'all' in scopes_lower:
            selected_scopes = self.scopes
        else:
            selected_scopes = scopes_lower
    else:
        raise ValueError("Parameter 'scopes' must be a string or a list/tuple of strings.")

    # Determine variables
    if isinstance(variables, str):
        if variables.lower() == 'all':
            selected_variables = self.variables
        else:
            if variables not in self.variables:
                print(f"Warning: Variable '{variables}' not managed by this instance.")
                selected_variables = []
            else:
                selected_variables = [variables]
    elif isinstance(variables, (list, tuple)):
        variables_lower = [v for v in variables]
        selected_variables = [v for v in variables_lower if v in self.variables]
        missing_vars = set(variables_lower) - set(selected_variables)
        if missing_vars:
            print(f"Warning: Variables '{', '.join(missing_vars)}' not managed by this instance.")
    else:
        raise ValueError("Parameter 'variables' must be a string or a list/tuple of strings.")

    # Generate content
    content = ""
    if include_headers:
        if export_format == 'markdown':
            content += f"## Variable: `{self.variables[0]}`\n\n" if len(self.variables) == 1 else ""
        elif export_format == 'html':
            content += f"<h2>Variable: {self.variables[0]}</h2>\n" if len(self.variables) == 1 else ""

    summary = self.summarize()
    for var in selected_variables:
        if include_headers:
            if export_format == 'markdown':
                content += f"### Variable: `{var}`\n\n"
            elif export_format == 'html':
                content += f"<h3>Variable: {var}</h3>\n"
        var_summary = summary[var]
        for scope, details in var_summary.items():
            if scope not in selected_scopes:
                continue
            if include_headers or True:
                if export_format == 'markdown':
                    content += f"#### Scope: {scope.capitalize()}\n\n"
                elif export_format == 'html':
                    content += f"<h4>Scope: {scope.capitalize()}</h4>\n"
            # Add content based on format
            if export_format == 'markdown':
                content += f"- **Total Occurrences**: {details['total_occurrences']}\n"
                unique_vals_formatted = ', '.join(map(str, details['unique_values']))
                content += f"- **Unique Values**: {unique_vals_formatted}\n"
                if details['unique_elements_in_lists']:
                    unique_elems_formatted = ', '.join(map(str, details['unique_elements_in_lists']))
                    content += f"- **Unique Elements in Lists**: {unique_elems_formatted}\n\n"
                    # Element Counts Table
                    content += "**Element Counts in Lists:**\n\n"
                    content += "| Element | Count |\n"
                    content += "|---------|-------|\n"
                    for elem, count in details['element_counts_in_lists'].items():
                        content += f"| {elem} | {count} |\n"
                    content += "\n"
            elif export_format == 'html':
                content += "<ul>"
                content += f"<li><strong>Total Occurrences</strong>: {details['total_occurrences']}</li>"
                unique_vals_formatted = ', '.join(map(str, details['unique_values']))
                content += f"<li><strong>Unique Values</strong>: {unique_vals_formatted}</li>"
                if details['unique_elements_in_lists']:
                    unique_elems_formatted = ', '.join(map(str, details['unique_elements_in_lists']))
                    content += f"<li><strong>Unique Elements in Lists</strong>: {unique_elems_formatted}</li>"
                content += "</ul>\n"

                if details['element_counts_in_lists']:
                    content += "<h5>Element Counts in Lists:</h5>\n"
                    content += "<table>\n<tr><th>Element</th><th>Count</th></tr>\n"
                    for elem, count in details['element_counts_in_lists'].items():
                        content += f"<tr><td>{elem}</td><td>{count}</td></tr>\n"
                    content += "</table>\n"

        # Add a horizontal line between variables
        if include_headers and len(selected_variables) > 1:
            if export_format == 'markdown':
                content += "\n---\n\n"
            elif export_format == 'html':
                content += "<hr/>\n"

    # Handle format-specific headers
    if return_content:
        if export_format == 'markdown':
            return content
        elif export_format == 'html':
            return f"<html><head><meta charset='UTF-8'><title>Variable Report</title></head><body>{content}</body></html>"
    else:
        if not filename:
            raise ValueError("Filename must be provided if return_content is False.")

        # Determine export format based on file extension
        if ext in ['.md', '.txt']:
            export_format = 'markdown'
        elif ext == '.html':
            export_format = 'html'
        else:
            raise ValueError("Unsupported file extension. Supported extensions are .md, .txt, and .html.")

        # Prepare full content for HTML
        if export_format == 'html' and not include_headers:
            full_content = f"<!DOCTYPE html>\n<html>\n<head>\n<meta charset='UTF-8'>\n<title>Variable Report</title>\n</head>\n<body>\n{content}\n</body>\n</html>"
        else:
            full_content = content

        # Write to file
        try:
            with open(filename, 'w', encoding='utf-8') as file:
                file.write(full_content)
            print(f"Report successfully generated at '{filename}'.")
        except Exception as e:
            raise Exception(f"Failed to write the report to '{filename}': {e}")
def get_all_elements_in_lists(self)

Retrieves all unique elements within list-type variable values.

Returns: - A dict mapping each variable to its set of unique elements in lists per scope.

Expand source code
def get_all_elements_in_lists(self):
    """
    Retrieves all unique elements within list-type variable values.

    Returns:
    - A dict mapping each variable to its set of unique elements in lists per scope.
    """
    result = {}
    for var in self.variables:
        var_elements = {}
        for scope, occurrences in self.data[var].items():
            unique_elems = set()
            if is_scalar(occurrences):
                if isinstance(occurrences, list):
                    unique_elems.update(occurrences)
            elif isinstance(occurrences, list):
                for item in occurrences:
                    if isinstance(item, tuple) and len(item) == 2:
                        _, val = item
                        if isinstance(val, list):
                            unique_elems.update(val)
                    elif isinstance(item, list):
                        unique_elems.update(item)
            var_elements[scope] = unique_elems
        result[var] = var_elements
    return result
def get_all_values(self)

Retrieves all unique values of the variable(s).

Returns: - A dict mapping each variable to its set of unique values per scope.

Expand source code
def get_all_values(self):
    """
    Retrieves all unique values of the variable(s).

    Returns:
    - A dict mapping each variable to its set of unique values per scope.
    """
    result = {}
    for var in self.variables:
        var_unique = {}
        for scope, occurrences in self.data[var].items():
            unique_vals = set()
            if is_scalar(occurrences):
                unique_vals.add(occurrences)
            elif isinstance(occurrences, list):
                for item in occurrences:
                    if isinstance(item, tuple) and len(item) == 2:
                        _, val = item
                        hashable_val = make_hashable(val)
                        unique_vals.add(hashable_val)
                    else:
                        hashable_val = make_hashable(item)
                        unique_vals.add(hashable_val)
            var_unique[scope] = unique_vals
        result[var] = var_unique
    return result
def get_raw_data(self)

Returns the raw data.

Returns: - The raw data as provided during initialization.

Expand source code
def get_raw_data(self):
    """
    Returns the raw data.

    Returns:
    - The raw data as provided during initialization.
    """
    return self.data
def get_steps_with_value(self, value, within_list=False)

Retrieves the steps where the variable equals the specified value.

Parameters: - value: The value to search for. - within_list (bool): If True, searches for the value within list-type variable values.

Returns: - If single variable: - A dict with scopes as keys and lists of step indices or values as values. - If multiple variables: - A dict mapping each variable name to their respective scope-step/value mappings.

Expand source code
def get_steps_with_value(self, value, within_list=False):
    """
    Retrieves the steps where the variable equals the specified value.

    Parameters:
    - value: The value to search for.
    - within_list (bool): If True, searches for the value within list-type variable values.

    Returns:
    - If single variable:
        - A dict with scopes as keys and lists of step indices or values as values.
    - If multiple variables:
        - A dict mapping each variable name to their respective scope-step/value mappings.
    """
    result = {}
    for var in self.variables:
        var_result = {}
        for scope, occurrences in self.data[var].items():
            if is_scalar(occurrences):
                # Scalar value
                if within_list:
                    continue  # Cannot search within a scalar
                if occurrences == value:
                    var_result[scope] = occurrences
            elif isinstance(occurrences, list):
                # List of values or list of tuples
                steps = []
                for item in occurrences:
                    if isinstance(item, tuple) and len(item) == 2:
                        step, val = item
                        if within_list:
                            if isinstance(val, list) and value in val:
                                steps.append(step)
                        else:
                            if val == value:
                                steps.append(step)
                    else:
                        # List of values
                        if within_list:
                            if isinstance(item, list) and value in item:
                                steps.append(item)
                        else:
                            if item == value:
                                steps.append(item)
                if steps:
                    var_result[scope] = steps
            else:
                # Other types (e.g., dict), can be extended as needed
                pass
        if var_result:
            result[var] = var_result
    return result
def get_steps_with_value_in_scope(self, variable, scope, value, within_list=False)

Retrieves the steps within a specific scope where the variable equals the specified value.

Parameters: - variable (str): The variable name. - scope (str): The scope to search within ("static", "global", "local"). - value: The value to search for. - within_list (bool): If True, searches for the value within list-type variable values.

Returns: - A list of step indices or values where the variable equals the value within the specified scope.

Expand source code
def get_steps_with_value_in_scope(self, variable, scope, value, within_list=False):
    """
    Retrieves the steps within a specific scope where the variable equals the specified value.

    Parameters:
    - variable (str): The variable name.
    - scope (str): The scope to search within ("static", "global", "local").
    - value: The value to search for.
    - within_list (bool): If True, searches for the value within list-type variable values.

    Returns:
    - A list of step indices or values where the variable equals the value within the specified scope.
    """
    if variable not in self.variables:
        raise ValueError(f"Variable '{variable}' is not managed by this instance.")

    scope = scope.lower()
    occurrences = self.data[variable].get(scope, [])
    steps = []

    if is_scalar(occurrences):
        if within_list:
            if isinstance(occurrences, list) and value in occurrences:
                steps.append(occurrences)
        else:
            if occurrences == value:
                steps.append(occurrences)
    elif isinstance(occurrences, list):
        for item in occurrences:
            if isinstance(item, tuple) and len(item) == 2:
                step, val = item
                if within_list:
                    if isinstance(val, list) and value in val:
                        steps.append(step)
                else:
                    if val == value:
                        steps.append(step)
            else:
                if within_list:
                    if isinstance(item, list) and value in item:
                        steps.append(item)
                else:
                    if item == value:
                        steps.append(item)
    return steps
def get_usage_count(self, value, within_list=False)

Counts how many times a specific value is used.

Parameters: - value: The value to count. - within_list (bool): If True, counts occurrences within list-type variable values.

Returns: - If single variable: - A dict with scopes as keys and integer counts or counts of values as values. - If multiple variables: - A dict mapping each variable name to their respective scope-count/value mappings.

Expand source code
def get_usage_count(self, value, within_list=False):
    """
    Counts how many times a specific value is used.

    Parameters:
    - value: The value to count.
    - within_list (bool): If True, counts occurrences within list-type variable values.

    Returns:
    - If single variable:
        - A dict with scopes as keys and integer counts or counts of values as values.
    - If multiple variables:
        - A dict mapping each variable name to their respective scope-count/value mappings.
    """
    result = {}
    for var in self.variables:
        var_count = {}
        for scope, occurrences in self.data[var].items():
            count = 0
            if is_scalar(occurrences):
                if within_list:
                    if isinstance(occurrences, list) and value in occurrences:
                        count += 1
                else:
                    if occurrences == value:
                        count += 1
            elif isinstance(occurrences, list):
                for item in occurrences:
                    if isinstance(item, tuple) and len(item) == 2:
                        _, val = item
                        if within_list:
                            if isinstance(val, list) and value in val:
                                count += 1
                        else:
                            if val == value:
                                count += 1
                    else:
                        if within_list:
                            if isinstance(item, list) and value in item:
                                count += 1
                        else:
                            if item == value:
                                count += 1
            if count > 0:
                var_count[scope] = count
        if var_count:
            result[var] = var_count
    return result
def summarize(self)

Provides a summary of the variable occurrences.

Returns: - A dict mapping each variable to their respective summaries per scope.

Expand source code
def summarize(self):
    """
    Provides a summary of the variable occurrences.

    Returns:
    - A dict mapping each variable to their respective summaries per scope.
    """
    summary = {}
    for var in self.variables:
        var_summary = {}
        for scope, occurrences in self.data[var].items():
            unique_vals = set()
            unique_elements = set()
            value_counts = defaultdict(int)
            element_counts = defaultdict(int)

            if is_scalar(occurrences):
                # Scalar value
                hashable_val = make_hashable(occurrences)
                unique_vals.add(hashable_val)
                value_counts[hashable_val] += 1
                if isinstance(occurrences, list):
                    unique_elements.update(occurrences)
                    for elem in occurrences:
                        element_counts[elem] += 1
            elif isinstance(occurrences, list):
                for item in occurrences:
                    if isinstance(item, tuple) and len(item) == 2:
                        # Tuple: (step, value)
                        step, val = item
                        hashable_val = make_hashable(val)
                        unique_vals.add(hashable_val)
                        value_counts[hashable_val] += 1
                        if isinstance(val, list):
                            unique_elements.update(val)
                            for elem in val:
                                element_counts[elem] += 1
                        elif isinstance(val, dict):
                            # Handle nested dictionaries if necessary
                            for sub_val in val.values():
                                if isinstance(sub_val, list):
                                    unique_elements.update(sub_val)
                                    for elem in sub_val:
                                        element_counts[elem] += 1
                    else:
                        # Direct value
                        hashable_val = make_hashable(item)
                        unique_vals.add(hashable_val)
                        value_counts[hashable_val] += 1
                        if isinstance(item, list):
                            unique_elements.update(item)
                            for elem in item:
                                element_counts[elem] += 1
                        elif isinstance(item, dict):
                            for sub_val in item.values():
                                if isinstance(sub_val, list):
                                    unique_elements.update(sub_val)
                                    for elem in sub_val:
                                        element_counts[elem] += 1
            else:
                # Other types can be handled here if needed
                pass

            var_summary[scope] = {
                "total_occurrences": len(occurrences),
                "unique_values": unique_vals,
                "unique_elements_in_lists": unique_elements,
                "value_counts": dict(value_counts),
                "element_counts_in_lists": dict(element_counts)
            }
        summary[var] = var_summary
    return summary
class boundarysection (persistentfile=True, persistentfolder=None, printflag=False, verbose=False, verbosity=None, **userdefinitions)

LAMMPS script: boundary session

constructor adding instance definitions stored in USER

Expand source code
class boundarysection(script):
    """ LAMMPS script: boundary session """
    name = "boundary"
    description = name+" section"
    position = 4
    section = 5
    userid = "example"
    version = 0.1

    # inherit properties from geometrysection
    DEFINITIONS = geometrysection.DEFINITIONS + scriptdata(
        gravity = -9.81,
        vector = "$ 0 1 0"
        )

    TEMPLATE = """
# :BOUNDARY SECTION:
#   boundary section

    ####################################################################################################
    # DEFINE BOUNDARY CONDITIONS
    #
    # note that the the particles constituting the mouth are simply not integrated in time,
    # thus these particles never move. This is equivalent to a fixed displacement boundary condition.
    ####################################################################################################
    fix             gfix allheavy gravity ${gravity} vector ${vector} # add gravity


    ####################################################################################################
    # moving top "tongue" (to2)
    ####################################################################################################
    variable vmouth equal -${crunchv}*sin(${crunchw}*time)
    fix             move_fix_tongue2 tongue2 smd/setvel 0 v_vmouth 0

    """

Ancestors

Class variables

var DEFINITIONS
var TEMPLATE
var description
var name
var position
var section
var userid
var version

Inherited members

class discretizationsection (persistentfile=True, persistentfolder=None, printflag=False, verbose=False, verbosity=None, **userdefinitions)

LAMMPS script: discretization session

constructor adding instance definitions stored in USER

Expand source code
class discretizationsection(script):
    """ LAMMPS script: discretization session """
    name = "discretization"
    description = name+" section"
    position = 3
    section = 4
    userid = "example"
    version = 0.1

    # inherit properties from geometrysection
    DEFINITIONS = geometrysection.DEFINITIONS + scriptdata(
              h= "2.5*${l0} # SPH kernel diameter",
        vol_one= "${l0}^2 # initial particle volume for 2d simulation",
     rho_saliva= 1000,
        rho_obj= 1300,
           skin= "${h} # Verlet list range",
  contact_scale= 1.5
        )

    TEMPLATE = """
# :DISCRETIZATION SECTION:
#   discretization

    ####################################################################################################
    # DISCRETIZATION PARAMETERS
    ####################################################################################################
    set             group all diameter ${h}
    set             group all smd/contact/radius ${l0}
    set             group all volume  ${vol_one}
    set             group all smd/mass/density ${rho_saliva}
    set             group solidfoods smd/mass/density ${rho_obj}
    set             group tongue1 smd/mass/density ${rho_obj}
    set             group tongue2 smd/mass/density ${rho_obj}
    neighbor        ${skin} bin

    """

Ancestors

Class variables

var DEFINITIONS
var TEMPLATE
var description
var name
var position
var section
var userid
var version

Inherited members

class dumpsection (persistentfile=True, persistentfolder=None, printflag=False, verbose=False, verbosity=None, **userdefinitions)

LAMMPS script: dump session

constructor adding instance definitions stored in USER

Expand source code
class dumpsection(script):
    """ LAMMPS script: dump session """
    name = "dump"
    description = name+" section"
    position = 7
    section = 8
    userid = "example"
    version = 0.1

    DEFINITIONS = globalsection().DEFINITIONS

    TEMPLATE = """
# :DUMP SECTION:
#   Dump configuration

    ####################################################################################################
    # SPECIFY TRAJECTORY OUTPUT
    ####################################################################################################
    compute         eint all smd/internal/energy
    compute         contact_radius all smd/contact/radius
    compute         S solidfoods smd/tlsph/stress
    compute         nn saliva smd/ulsph/num/neighs
    compute         epl solidfoods smd/plastic/strain
    compute         vol all smd/vol
    compute         rho all smd/rho

    dump            dump_id all custom ${outstep} ${outputfile} id type x y &
                    fx fy vx vy c_eint c_contact_radius mol &
                    c_S[1] c_S[2] c_S[4] mass radius c_epl c_vol c_rho c_nn proc
    dump_modify     dump_id first yes

    """

Ancestors

Class variables

var DEFINITIONS
var TEMPLATE
var description
var name
var position
var section
var userid
var version

Inherited members

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
  • pizza.forcefield.tlsphalone

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 geometrysection (persistentfile=True, persistentfolder=None, printflag=False, verbose=False, verbosity=None, **userdefinitions)

LAMMPS script: global session

constructor adding instance definitions stored in USER

Expand source code
class geometrysection(script):
    """ LAMMPS script: global session """
    name = "geometry"
    description = name+" section"
    position = 2
    section = 3
    userid = "example"
    version = 0.1

    DEFINITIONS = scriptdata(
         l0= 0.05,
       hgap= "0.25        # gap to prevent direct contact at t=0 (too much enery)",
  hsmallgap= "0.1   # gap to prevent direct contact at t=0 (too much enery)",
       hto1= "0.8         # height of to1 (the tongue to1, note 1 not l)",
       hto2= "0.5         # height of to2 (the tongue to2)",
       rsph= "0.3         # radius of spherical food particles",
       lpar= "0.6         # size of prismatic particles ",
    yfloor1= "${hgap}  # bottom position of to1, position of the first floor",
     yroof1= "${yfloor1}+${hto1} # bottom position of to1, position of the first floor",
   yfloor2a= "${yroof1}+${hsmallgap}  # position of the second floor / level a",
    yroof2a= "${yfloor2a}+${lpar}      # position of the second floor / level a",
   yfloor2b= "${yroof2a}+${hsmallgap} # position of the second floor / level b",
    yroof2b= "${yfloor2b}+${lpar}      # position of the second floor / level b",
   yfloor2c= "${yfloor2a}+${rsph}     # position of the second floor / level c",
    yroof2c= "${yfloor2c}+${rsph}      # position of the second floor / level c",
   yfloor2d= "${yroof2c}+${rsph}+${hsmallgap} # position of the second floor / level d",
    yroof2d= "${yfloor2d}+${rsph}      # position of the second floor / level d",
    yfloor3= 5.0,
     yroof3= "${yfloor3}+${hto2} # bottom position of to1",
   yfloor3a= "${yfloor3}-0.6",
    yroof3a= "${yfloor3}",
    crunchl= "${yfloor3}-${yfloor2a}-0.8",
    crunchp= 3,
    crunchw= "2*pi/${crunchp}",
    crunchd= "2*(sin((${crunchp}*${crunchw})/4)^2)/${crunchw}",
    crunchv= "${crunchl}/${crunchd}"
        )

    TEMPLATE = """
# :GEOMETRY SECTION:
#   Build geometry (very specific example)

    ####################################################################################################
    # CREATE INITIAL GEOMETRY
    # note there are 4 groups (create_box 5 box)
    # groupID 1 = saliva
    # groupID 2 = food
    # groupID 3 = mouth walls
    # groupID 4 = tongue alike (part1)
    # groupID 5 = also tongue but palate infact (part2)
    ####################################################################################################
    # create simulation box, a mouth, and a saliva column
    region          box block 0 12 0 8 -0.01 0.01 units box
    create_box      5 box
    region          saliva1 block 0.25 1.8 1.25 3.5 EDGE EDGE units box
    region          saliva2 block 10 11.65 1.25 4 EDGE EDGE units box
    region          mouth block 0.15 11.85 0.15 8 -0.01 0.01 units box side out # mouth
    lattice         sq ${l0}
    create_atoms    1 region saliva1
    create_atoms    1 region saliva2
    group           saliva type 1
    create_atoms    3 region mouth
    group           mouth type 3

    print "Crunch distance:${crunchl}"  # 3.65
    print "Crunch distance:${crunchv}"  # 0.1147


    # bottom part of the tongue: to1 (real tongue)
    # warning: all displacements are relative to the bottom part
    region          to1 block 1 11 ${yfloor1} ${yroof1} EDGE EDGE units box
    region          to2part1 block 0.5 11.5 ${yfloor3} ${yroof3} EDGE EDGE units box
    region          to2part2 block 5.5 6 ${yfloor3a} ${yroof3a} EDGE EDGE units box
    region          to2 union 2 to2part1 to2part2
    create_atoms    4 region to1
    create_atoms    5 region to2
    group           tongue1 type 4
    group           tongue2 type 5

    # create some solid objects to be pushed around
    region          pr1 prism 2 2.6 ${yfloor2a} ${yroof2a} EDGE EDGE 0.3 0 0 units box
    region          bl1 block 3 3.6 ${yfloor2a} ${yroof2a} EDGE EDGE units box
    region          sp1 sphere 4.3 ${yfloor2c} 0 ${rsph} units box
    region          sp2 sphere 5 ${yfloor2c} 0 ${rsph} units box
    region          sp3 sphere 5.7 ${yfloor2c} 0 ${rsph} units box
    region          sp4 sphere 6.4 ${yfloor2c} 0 ${rsph} units box
    region          sp5 sphere 7.1 ${yfloor2c} 0 ${rsph} units box
    region          sp6 sphere 6.05 ${yfloor2d} 0 ${rsph} units box
    region          br2 block 3 3.6 ${yfloor2b} ${yroof2b} EDGE EDGE units box

    # fill the regions with atoms (note that atoms = smoothed hydrodynamics particles)
    create_atoms    2 region pr1
    create_atoms    2 region bl1
    create_atoms    2 region sp1
    create_atoms    2 region sp2
    create_atoms    2 region sp3
    create_atoms    2 region sp4
    create_atoms    2 region sp5
    create_atoms    2 region sp6
    create_atoms    2 region br2

    # atoms of objects are grouped with two id
    # fix apply only to groups
    group           solidfoods type 2
    group           tlsph type 2

    # group heavy
    group           allheavy type 1:4


    """

Ancestors

Class variables

var DEFINITIONS
var TEMPLATE
var description
var name
var position
var section
var userid
var version

Inherited members

class globalsection (persistentfile=True, persistentfolder=None, printflag=False, verbose=False, verbosity=None, **userdefinitions)

LAMMPS script: global session

constructor adding instance definitions stored in USER

Expand source code
class globalsection(script):
    """ LAMMPS script: global session """
    name = "global"
    description = name+" section"
    position = 0
    section = 1
    userid = "example"
    version = 0.1

    DEFINITIONS = scriptdata(
  outputfile= "$dump.mouthfeel_v5_long    # from the project of the same name",
        tsim= "500000                     # may be too long",
     outstep= 10
        )

    MATERIALS = scriptdata(
         rho_saliva= "1000 # mass density saliva",
            rho_obj= "1300 # mass density solid objects",
                 c0= "10.0 # speed of sound for saliva",
                  E= "5*${c0}*${c0}*${rho_saliva} # Young's modulus for solid objects",
           Etongue1= "10*${E} # Young's modulus for tongue",
           Etongue2= "2*${Etongue1} # Young's modulus for tongue",
                 nu= "0.3 # Poisson ratio for solid objects",
        sigma_yield= "0.1*${E} # plastic yield stress for solid objects",
     hardening_food= "0 # plastic hardening parameter for solid food",
   hardening_tongue= "1 # plastic hardening parameter for solid tongue",
  contact_stiffness= "2.5*${c0}^2*${rho_saliva} # contact force amplitude",
       contact_wall= "100*${contact_stiffness} # contact with wall (avoid interpenetration)",
                 q1= "1.0 # artificial viscosity",
                 q2= "0.0 # artificial viscosity",
                 Hg= "10 # Hourglass control coefficient for solid objects",
                 Cp= "1.0 # heat capacity -- not used here"
                  )

    DEFINITIONS += MATERIALS # append MATERIALS data

    TEMPLATE = """
# :GLOBAL SECTION:
#   avoid to set variables in LAMMPS script
#   use DEFINITIONS field to set properties.
#   If you need to define them, use the following syntax


    # ####################################################################################################
    # # GLOBAL
    # ####################################################################################################
     variable outputfile string "${outputfile}"
     variable tsim equal ${tsim}
     variable outstep equal ${outstep}

    # ####################################################################################################
    # # MATERIAL PARAMETERS
    # ####################################################################################################
    # variable        rho_saliva equal 1000 # mass density saliva
    # variable        rho_obj equal 1300 # mass density solid objects
    # variable        c0 equal 10.0 # speed of sound for saliva
    # variable        E equal 5*${c0}*${c0}*${rho_saliva} # Young's modulus for solid objects
    # variable        Etongue1 equal 10*${E} # Young's modulus for tongue
    # variable        Etongue2 equal 2*${Etongue1} # Young's modulus for tongue
    # variable        nu equal 0.3 # Poisson ratio for solid objects
    # variable        sigma_yield equal 0.1*${E} # plastic yield stress for solid objects
    # variable        hardening_food equal 0 # plastic hardening parameter for solid food
    # variable        hardening_tongue equal 1 # plastic hardening parameter for solid tongue
    # variable        contact_stiffness equal 2.5*${c0}^2*${rho_saliva} # contact force amplitude
    # variable        contact_wall equal 100*${contact_stiffness} # contact with wall (avoid interpenetration)
    # variable        q1 equal 1.0 # artificial viscosity
    # variable        q2 equal 0.0 # artificial viscosity
    # variable        Hg equal 10 # Hourglass control coefficient for solid objects
    # variable        Cp equal 1.0 # heat capacity -- not used here
    """

Ancestors

Class variables

var DEFINITIONS
var MATERIALS
var TEMPLATE
var description
var name
var position
var section
var userid
var version

Inherited members

class initializesection (persistentfile=True, persistentfolder=None, printflag=False, verbose=False, verbosity=None, **userdefinitions)

LAMMPS script: global session

constructor adding instance definitions stored in USER

Expand source code
class initializesection(script):
    """ LAMMPS script: global session """
    name = "initialize"
    description = name+" section"
    position = 1
    section = 2
    userid = "example"
    version = 0.1

    DEFINITIONS = scriptdata(
               units= "$ si",
           dimension= 2,
            boundary= "$ sm sm p",
          atom_style= "$smd",
  neigh_modify_every= 5,
  neigh_modify_delay= 0,
         comm_modify= "$ vel yes",
              newton= "$ off",
         atom_modify= "$ map array",
          comm_style= "$ tiled"
        )

    TEMPLATE = """
# :INITIALIZE SECTION:
#   initialize styles, dimensions, boundaries and communivation

    ####################################################################################################
    # INITIALIZE LAMMPS
    ####################################################################################################
    units           ${units}
    dimension       ${dimension}
    boundary        ${boundary}
    atom_style      ${atom_style}
    neigh_modify    every ${neigh_modify_every} delay ${neigh_modify_delay} check yes
    comm_modify     ${comm_modify}
    newton          ${newton}
    atom_modify     ${atom_modify}
    comm_style      ${comm_style}
    """

Ancestors

Class variables

var DEFINITIONS
var TEMPLATE
var description
var name
var position
var section
var userid
var version

Inherited members

class integrationsection (persistentfile=True, persistentfolder=None, printflag=False, verbose=False, verbosity=None, **userdefinitions)

LAMMPS script: time integration session

constructor adding instance definitions stored in USER

Expand source code
class integrationsection(script):
    """ LAMMPS script: time integration session """
    name = "time integration"
    description = name+" section"
    position = 6
    section = 7
    userid = "example"
    version = 0.1

    DEFINITIONS = scriptdata(
              dt = 0.1,
   adjust_redius = "$ 1.01 10 15"
        )

    TEMPLATE = """
# :INTEGRATION SECTION:
#   Time integration conditions

    fix             dtfix tlsph smd/adjust_dt ${dt} # dynamically adjust time increment every step
    fix             integration_fix_water saliva smd/integrate_ulsph adjust_radius ${adjust_redius}
    fix             integration_fix_solids solidfoods smd/integrate_tlsph
    fix             integration_fix_tongue1 tongue1 smd/integrate_tlsph
    fix             integration_fix_tongue2 tongue2 smd/integrate_tlsph

    """

Ancestors

Class variables

var DEFINITIONS
var TEMPLATE
var description
var name
var position
var section
var userid
var version

Inherited members

class interactionsection (persistentfile=True, persistentfolder=None, printflag=False, verbose=False, verbosity=None, **userdefinitions)

LAMMPS script: interaction session

constructor adding instance definitions stored in USER

Expand source code
class interactionsection(script):
    """ LAMMPS script: interaction session """
    name = "interactions"
    description = name+" section"
    position = 5
    section = 6
    userid = "example"
    version = 0.1

    DEFINITIONS = globalsection.DEFINITIONS + \
                  geometrysection.DEFINITIONS + \
                  discretizationsection.DEFINITIONS

    TEMPLATE = """
# :INTERACTIONS SECTION:
#   Please use forcefield() to make a robust code

    ####################################################################################################
    # INTERACTION PHYSICS / MATERIAL MODEL
    # 3 different pair styles are used:
    #     - updated Lagrangian SPH for saliva
    #     - total Lagrangian SPH for solid objects
    #     - a repulsive Hertzian potential for contact forces between different physical bodies
    ####################################################################################################
    pair_style      hybrid/overlay smd/ulsph *DENSITY_CONTINUITY *VELOCITY_GRADIENT *NO_GRADIENT_CORRECTION &
                                   smd/tlsph smd/hertz ${contact_scale}
    pair_coeff      1 1 smd/ulsph *COMMON ${rho_saliva} ${c0} ${q1} ${Cp} 0 &
                    *EOS_TAIT 7.0 &
                    *END
    pair_coeff      2 2 smd/tlsph *COMMON ${rho_obj} ${E} ${nu} ${q1} ${q2} ${Hg} ${Cp} &
                    *STRENGTH_LINEAR_PLASTIC ${sigma_yield} ${hardening_food} &
                    *EOS_LINEAR &
                    *END
    pair_coeff      4 4 smd/tlsph *COMMON ${rho_obj} ${Etongue1} ${nu} ${q1} ${q2} ${Hg} ${Cp} &
                    *STRENGTH_LINEAR_PLASTIC ${sigma_yield} ${hardening_tongue} &
                    *EOS_LINEAR &
                    *END
    pair_coeff      5 5 smd/tlsph *COMMON ${rho_obj} ${Etongue2} ${nu} ${q1} ${q2} ${Hg} ${Cp} &
                    *STRENGTH_LINEAR_PLASTIC ${sigma_yield} ${hardening_tongue} &
                    *EOS_LINEAR &
                    *END

    pair_coeff      3 3 none   # wall-wall
    pair_coeff      1 2 smd/hertz ${contact_stiffness} # saliva-food
    pair_coeff      1 3 smd/hertz ${contact_wall} # saliva-wall
    pair_coeff      2 3 smd/hertz ${contact_wall} # food-wall
    pair_coeff      2 2 smd/hertz ${contact_stiffness} # food-food
    # add 4 (to1)
    pair_coeff      1 4 smd/hertz ${contact_stiffness} # saliva-tongue1
    pair_coeff      2 4 smd/hertz ${contact_stiffness} # food-tongue1
    pair_coeff      3 4 smd/hertz ${contact_wall} # wall-tongue1
    pair_coeff      4 4 smd/hertz ${contact_stiffness} # tongue1-tongue1
    # add 5 (to2)
    pair_coeff      1 5 smd/hertz ${contact_stiffness} # saliva-tongue2
    pair_coeff      2 5 smd/hertz ${contact_stiffness} # food-tongue2
    pair_coeff      3 5 smd/hertz ${contact_wall} # wall-tongue2
    pair_coeff      4 5 smd/hertz ${contact_stiffness} # tongue1-tongue2
    pair_coeff      5 5 smd/hertz ${contact_stiffness} # tongue2-tongue2

    """

Ancestors

Class variables

var DEFINITIONS
var TEMPLATE
var description
var name
var position
var section
var userid
var version

Inherited members

class none

SMD:TLSPH forcefield (updated Lagrangian)

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

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

Ancestors

  • pizza.forcefield.smd
  • pizza.forcefield.forcefield

Subclasses

  • pizza.forcefield.rigidwall

Class variables

var PAIR_DIAGCOEFF
var PAIR_OFFDIAGCOEFF
var description
var name
class param (sortdefinitions=False, debug=False, **kwargs)

Class: param

A class derived from struct that introduces dynamic evaluation of field values. The param class acts as a container for evaluated parameters, allowing expressions to depend on other fields. It supports advanced evaluation, sorting of dependencies, and text formatting.


Features

  • Inherits all functionalities of struct.
  • Supports dynamic evaluation of field expressions.
  • Automatically resolves dependencies between fields.
  • Includes utility methods for text formatting and evaluation.

Shorthands for p=param(...)

  • s = p.eval() returns the full evaluated structure
  • p.getval("field") returns the evaluation for the field "field"
  • s = p() returns the full evaluated structure as p.eval()
  • s = p("field1","field2"...) returns the evaluated substructure for fields "field1", "field2"

Examples

Basic Usage with Evaluation

s = param(a=1, b=2, c='${a} + ${b} # evaluate me if you can', d="$this is a string", e="1000 # this is my number")
s.eval()
# Output:
# --------
#      a: 1
#      b: 2
#      c: ${a} + ${b} # evaluate me if you can (= 3)
#      d: $this is a string (= this is a string)
#      e: 1000 # this is my number (= 1000)
# --------

s.a = 10
s.eval()
# Output:
# --------
#      a: 10
#      b: 2
#      c: ${a} + ${b} # evaluate me if you can (= 12)
#      d: $this is a string (= this is a string)
#      e: 1000 # this is my number (= 1000)
# --------

Handling Text Parameters

s = param()
s.mypath = "$/this/folder"
s.myfile = "$file"
s.myext = "$ext"
s.fullfile = "$${mypath}/${myfile}.${myext}"
s.eval()
# Output:
# --------
#    mypath: $/this/folder (= /this/folder)
#    myfile: $file (= file)
#     myext: $ext (= ext)
#  fullfile: $${mypath}/${myfile}.${myext} (= /this/folder/file.ext)
# --------

Text Evaluation and Formatting

Evaluate Strings

s = param(a=1, b=2)
result = s.eval("this is a string with ${a} and ${b}")
print(result)  # "this is a string with 1 and 2"

Prevent Evaluation

definitions = param(a=1, b="${a}*10+${a}", c="\${a}+10", d='\${myparam}')
text = definitions.formateval("this is my text ${a}, ${b}, \${myvar}=${c}+${d}")
print(text)  # "this is my text 1, 11, \${myvar}=\${a}+10+${myparam}"

Advanced Usage

Rearranging and Sorting Definitions

s = param(
    a=1,
    f="${e}/3",
    e="${a}*${c}",
    c="${a}+${b}",
    b=2,
    d="${c}*2"
)
s.sortdefinitions()
s.eval()
# Output:
# --------
#      a: 1
#      b: 2
#      c: ${a} + ${b} (= 3)
#      d: ${c} * 2 (= 6)
#      e: ${a} * ${c} (= 3)
#      f: ${e} / 3 (= 1.0)
# --------

Internal Evaluation and Recursion with !

p=param()
p.a = [0,1,2]
p.b = '![1,2,"test","${a[1]}"]'
p
# Output:
#  -------------:----------------------------------------
#          a: [0, 1, 2]
#          b: ![1,2,"test","${a[1]}"]
#           = [1, 2, 'test', '1']
#  -------------:----------------------------------------
# Out: parameter list (param object) with 2 definitions

Error Handling

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

Sorting unresolved definitions raises errors unless explicitly suppressed:

p.sortdefinitions(raiseerror=False)
# WARNING: unable to interpret 1/3 expressions in "definitions"

Utility Methods

Method Description
eval() Evaluate all field expressions.
formateval(string) Format and evaluate a string with field placeholders.
protect(string) Escape variable placeholders in a string.
sortdefinitions() Sort definitions to resolve dependencies.
escape(string) Protect escaped variables in a string.
safe_fstring(string) evaluate safely complex mathemical expressions.

Overloaded Methods and Operators

Supported Operators

  • +: Concatenation of two parameter lists, sorting definitions.
  • -: Subtraction of fields.
  • len(): Number of fields.
  • in: Check for field existence.

Notes

  • The paramauto class simplifies handling of partial definitions and inherits from param.
  • Use paramauto when definitions need to be stacked irrespective of execution order.

constructor

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

    A class derived from `struct` that introduces dynamic evaluation of field values.
    The `param` class acts as a container for evaluated parameters, allowing expressions
    to depend on other fields. It supports advanced evaluation, sorting of dependencies,
    and text formatting.

    ---

    ### Features
    - Inherits all functionalities of `struct`.
    - Supports dynamic evaluation of field expressions.
    - Automatically resolves dependencies between fields.
    - Includes utility methods for text formatting and evaluation.

    ### Shorthands for `p=param(...)`
    - `s = p.eval()` returns the full evaluated structure
    - `p.getval("field")` returns the evaluation for the field "field"
    - `s = p()` returns the full evaluated structure as `p.eval()`
    - `s = p("field1","field2"...)` returns the evaluated substructure for fields "field1", "field2"

    ---

    ### Examples

    #### Basic Usage with Evaluation
    ```python
    s = param(a=1, b=2, c='${a} + ${b} # evaluate me if you can', d="$this is a string", e="1000 # this is my number")
    s.eval()
    # Output:
    # --------
    #      a: 1
    #      b: 2
    #      c: ${a} + ${b} # evaluate me if you can (= 3)
    #      d: $this is a string (= this is a string)
    #      e: 1000 # this is my number (= 1000)
    # --------

    s.a = 10
    s.eval()
    # Output:
    # --------
    #      a: 10
    #      b: 2
    #      c: ${a} + ${b} # evaluate me if you can (= 12)
    #      d: $this is a string (= this is a string)
    #      e: 1000 # this is my number (= 1000)
    # --------
    ```

    #### Handling Text Parameters
    ```python
    s = param()
    s.mypath = "$/this/folder"
    s.myfile = "$file"
    s.myext = "$ext"
    s.fullfile = "$${mypath}/${myfile}.${myext}"
    s.eval()
    # Output:
    # --------
    #    mypath: $/this/folder (= /this/folder)
    #    myfile: $file (= file)
    #     myext: $ext (= ext)
    #  fullfile: $${mypath}/${myfile}.${myext} (= /this/folder/file.ext)
    # --------
    ```

    ---

    ### Text Evaluation and Formatting

    #### Evaluate Strings
    ```python
    s = param(a=1, b=2)
    result = s.eval("this is a string with ${a} and ${b}")
    print(result)  # "this is a string with 1 and 2"
    ```

    #### Prevent Evaluation
    ```python
    definitions = param(a=1, b="${a}*10+${a}", c="\\${a}+10", d='\\${myparam}')
    text = definitions.formateval("this is my text ${a}, ${b}, \\${myvar}=${c}+${d}")
    print(text)  # "this is my text 1, 11, \\${myvar}=\\${a}+10+${myparam}"
    ```

    ---

    ### Advanced Usage

    #### Rearranging and Sorting Definitions
    ```python
    s = param(
        a=1,
        f="${e}/3",
        e="${a}*${c}",
        c="${a}+${b}",
        b=2,
        d="${c}*2"
    )
    s.sortdefinitions()
    s.eval()
    # Output:
    # --------
    #      a: 1
    #      b: 2
    #      c: ${a} + ${b} (= 3)
    #      d: ${c} * 2 (= 6)
    #      e: ${a} * ${c} (= 3)
    #      f: ${e} / 3 (= 1.0)
    # --------
    ```

    #### Internal Evaluation and Recursion with !
    ```python
    p=param()
    p.a = [0,1,2]
    p.b = '![1,2,"test","${a[1]}"]'
    p
    # Output:
    #  -------------:----------------------------------------
    #          a: [0, 1, 2]
    #          b: ![1,2,"test","${a[1]}"]
    #           = [1, 2, 'test', '1']
    #  -------------:----------------------------------------
    # Out: parameter list (param object) with 2 definitions
    ```

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

    Sorting unresolved definitions raises errors unless explicitly suppressed:
    ```python
    p.sortdefinitions(raiseerror=False)
    # WARNING: unable to interpret 1/3 expressions in "definitions"
    ```

    ---

    ### Utility Methods
    | Method                 | Description                                             |
    |------------------------|---------------------------------------------------------|
    | `eval()`               | Evaluate all field expressions.                         |
    | `formateval(string)`   | Format and evaluate a string with field placeholders.   |
    | `protect(string)`      | Escape variable placeholders in a string.               |
    | `sortdefinitions()`    | Sort definitions to resolve dependencies.               |
    | `escape(string)`       | Protect escaped variables in a string.                  |
    | `safe_fstring(string)` | evaluate safely complex mathemical expressions.         |

    ---

    ### Overloaded Methods and Operators
    #### Supported Operators
    - `+`: Concatenation of two parameter lists, sorting definitions.
    - `-`: Subtraction of fields.
    - `len()`: Number of fields.
    - `in`: Check for field existence.

    ---

    ### Notes
    - The `paramauto` class simplifies handling of partial definitions and inherits from `param`.
    - Use `paramauto` when definitions need to be stacked irrespective of execution order.
    """

    # override
    _type = "param"
    _fulltype = "parameter list"
    _ftype = "definition"
    _evalfeature = True    # This class can be evaluated with .eval()
    _returnerror = True    # This class returns an error in the evaluation string (added on 2024-09-06)


    # magic constructor
    def __init__(self,_protection=False,_evaluation=True,
                 sortdefinitions=False,debug=False,**kwargs):
        """ constructor """
        super().__init__(debug=debug,**kwargs)
        self._protection = _protection
        self._evaluation = _evaluation
        self._needs_sorting = False # defers sorting
        if sortdefinitions: self.sortdefinitions()

    # escape definitions if needed
    @staticmethod
    def escape(s):
        """
            escape \\${} as ${{}} --> keep variable names
            convert ${} as {} --> prepare Python replacement

            Examples:
                escape("\\${a}")
                returns ('${{a}}', True)

                escape("  \\${abc} ${a} \\${bc}")
                returns ('  ${{abc}} {a} ${{bc}}', True)

                escape("${a}")
                Out[94]: ('{a}', False)

                escape("${tata}")
                returns ('{tata}', False)

        """
        if not isinstance(s,str):
            raise TypeError(f'the argument must be string not {type(s)}')
        se, start, found = "", 0, True
        while found:
            pos0 = s.find(r"\${",start)
            found = pos0>=0
            if found:
                pos1 = s.find("}",pos0)
                found = pos1>=0
                if found:
                    se += s[start:pos0].replace("${","{")+"${{"+s[pos0+3:pos1]+"}}"
                    start=pos1+1
        result = se+s[start:].replace("${","{")
        if isinstance(s,pstr): result = pstr(result)
        return result,start>0

    # protect variables in a string
    def protect(self,s=""):
        """ protect $variable as ${variable} """
        if isinstance(s,str):
            t = s.replace(r"\$","££") # && is a placeholder
            escape = t!=s
            for k in self.keyssorted():
                t = t.replace("$"+k,"${"+k+"}")
            if escape: t = t.replace("££",r"\$")
            if isinstance(s,pstr): t = pstr(t)
            return t, escape
        raise TypeError(f'the argument must be string not {type(s)}')


    # lines starting with # (hash) are interpreted as comments
    # ${variable} or {variable} are substituted by variable.value
    # any line starting with $ is assumed to be a string (no interpretation)
    # ^ is accepted in formula(replaced by **))
    def eval(self,s="",protection=False):
        """
            Eval method for structure such as MS.alias

                s = p.eval() or s = p.eval(string)

                where :
                    p is a param object
                    s is a structure with evaluated fields
                    string is only used to determine whether definitions have been forgotten

        """
        # handle deferred sorting
        if self._needs_sorting:
            self.sortdefinitions(raiseerror=False, silentmode=True)
        # the argument s is only used by formateval() for error management
        tmp = struct(debug=self._debug)
        # evaluator without context
        evaluator_nocontext = SafeEvaluator() # for global evaluation without context

        # main string evaluator
        def evalstr(value,key=""):
            # replace ${variable} (Bash, Lammps syntax) by {variable} (Python syntax)
            # use \${variable} to prevent replacement (espace with \)
            # Protect variables if required
            ispstr = isinstance(value,pstr)
            valuesafe = pstr.eval(value,ispstr=ispstr) # value.strip()
            if valuesafe=="${"+key+"}": # circular reference (it cannot be evaluated)
                return valuesafe
            if protection or self._protection:
                valuesafe, escape0 = self.protect(valuesafe)
            else:
                escape0 = False
            # replace ${var} by {var} once basic substitutions have been applied
            valuesafe_priorescape = tmp.numrepl(valuesafe) # minimal substitution
            valuesafe, escape = param.escape(valuesafe_priorescape)
            escape = escape or escape0
            # replace "^" (Matlab, Lammps exponent) by "**" (Python syntax)
            valuesafe = pstr.eval(valuesafe.replace("^","**"),ispstr=ispstr)
            # Remove all content after #
            # if the first character is '#', it is not comment (e.g. MarkDown titles)
            poscomment = valuesafe.find("#")
            if poscomment>0: valuesafe = valuesafe[0:poscomment].strip()
            # Matrix shorthand replacement
            # $[[1,2,${a}]]+$[[10,20,30]] --> np.array([[1,2,${a}]])+np.array([[10,20,30]])
            valuesafe = param.replace_matrix_shorthand(valuesafe)
            # Literal string starts with $ (no interpretation), ! (evaluation)
            if not self._evaluation:
                return pstr.eval(tmp.format(valuesafe,escape),ispstr=ispstr)
            elif valuesafe.startswith("!"): # <---------- FORECED LITERAL EVALUATION (error messages are returned)
                try:
                    #vtmp = ast.literal_eval(valuesafe[1:])
                    evaluator = SafeEvaluator(tmp)
                    vtmp = evaluate_with_placeholders(valuesafe[1:],evaluator,evaluator_nocontext)
                    if isinstance(vtmp,list):
                        for i,item in enumerate(vtmp):
                            if isinstance(item,str) and not is_literal_string(item):
                                try:
                                    vtmp[i] = tmp.format(item, raiseerror=False) # in case substitions/interpolations are needed
                                    try:
                                        vtmp[i] = evaluator_nocontext.evaluate(vtmp[i]) # full evaluation without context
                                    except Exception as othererr:
                                        if self._debug:
                                            print(f"DEBUG {key}: Error evaluating: {vtmp[i]}\n< {othererr} >")
                                except Exception as ve:
                                    vtmp[i] = f"Error in <{item}>: {ve.__class__.__name__} - {str(ve)}"
                    return vtmp
                except (SyntaxError, ValueError) as e:
                    return f"Error: {e.__class__.__name__} - {str(e)}"
            elif valuesafe.startswith("$") and not escape:
                return tmp.format(valuesafe[1:].lstrip()) # discard $
            elif valuesafe.startswith("%"):
                return tmp.format(valuesafe[1:].lstrip()) # discard %
            else: # string empty or which can be evaluated
                if valuesafe=="":
                    return valuesafe # empty content
                else:
                    if isinstance(value,pstr): # keep path
                        return pstr.topath(tmp.format(valuesafe,escape=escape))
                    elif escape:  # partial evaluation
                        return tmp.format(valuesafe,escape=True)
                    else: # full evaluation (if it fails the last string content is returned) <---------- FULL EVALUTION will be tried
                        try:
                            resstr = tmp.format(valuesafe,raiseerror=False)
                        except (KeyError,NameError) as nameerr:
                            try: # nested indexing (guess)
                                resstr = param.safe_fstring(
                                param.replace_matrix_shorthand(valuesafe_priorescape),tmp)
                                evaluator = SafeEvaluator(tmp)
                                reseval = evaluate_with_placeholders(resstr,
                                          evaluator,evaluator_nocontext,raiseerror=True)
                            except Exception as othererr:
                                if self._returnerror: # added on 2024-09-06
                                    strnameerr = str(nameerr).replace("'","")
                                    if self._debug:
                                        print(f'Key Error for "{key}" < {othererr} >')
                                    return '< undef %s "${%s}" >' % (self._ftype,strnameerr)
                                else:
                                    return value #we keep the original value
                            else:
                                return reseval
                        except SyntaxError as commonerr:
                            return "Syntax Error < %s >" % commonerr
                        except TypeError  as commonerr:
                            try: # nested indexing (guess)
                                resstr = param.safe_fstring(
                                param.replace_matrix_shorthand(valuesafe_priorescape),tmp)
                                evaluator = SafeEvaluator(tmp)
                                reseval = evaluate_with_placeholders(resstr,
                                          evaluator,evaluator_nocontext,raiseerror=True)
                            except Exception as othererr:
                                if self._debug:
                                    print(f'Type Error for "{key}" < {othererr} >')
                                return "Type Error < %s >" % commonerr
                            else:
                                return reseval
                        except (IndexError,AttributeError):
                            try:
                                resstr = param.safe_fstring(
                                    param.replace_matrix_shorthand(valuesafe_priorescape),tmp)
                            except Exception as fstrerr:
                                return "Index Error < %s >" % fstrerr
                            else:
                                try:
                                    # reseval = eval(resstr)
                                    # reseval = ast.literal_eval(resstr)
                                    # Use SafeEvaluator to evaluate the final expression
                                    evaluator = SafeEvaluator(tmp)
                                    reseval = evaluator.evaluate(resstr)
                                except Exception as othererr:
                                    #tmp.setattr(key,"Mathematical Error around/in ${}: < %s >" % othererr)
                                    if self._debug:
                                        print(f"DEBUG {key}: Error evaluating: {resstr}\n< {othererr} >")
                                    return resstr
                                else:
                                    return reseval
                        except ValueError as valerr: # forced evaluation within ${}
                            try:
                                evaluator = SafeEvaluator(tmp)
                                reseval = evaluate_with_placeholders(valuesafe_priorescape,evaluator,evaluator_nocontext,raiseerror=True)
                            except SyntaxError as synerror:
                                if self._debug:
                                    print(f"DEBUG {key}: Error evaluating: {valuesafe_priorescape}\n< {synerror} >")
                                return evaluate_with_placeholders(valuesafe_priorescape,evaluator,evaluator_nocontext,raiseerror=False)
                            except Exception as othererr:
                                if self._debug:
                                    print(f"DEBUG {key}: Error evaluating: {valuesafe_priorescape}\n< {othererr} >")
                                return "Error in ${}: < %s >" % valerr
                            else:
                                return reseval

                        except Exception as othererr:
                            return "Error in ${}: < %s >" % othererr
                        else:
                            try:
                                # reseval = eval(resstr)
                                evaluator = SafeEvaluator(tmp)
                                reseval = evaluate_with_placeholders(resstr,evaluator,evaluator_nocontext)
                            except Exception as othererr:
                                #tmp.setattr(key,"Eval Error < %s >" % othererr)
                                if self._debug:
                                    print(f"DEBUG {key}: Error evaluating: {resstr}\n< {othererr} >")
                                return resstr.replace("\n",",") # \n replaced by ,
                            else:
                                return reseval

        # evalstr() refactored for error management
        def safe_evalstr(x,key=""):
            xeval = evalstr(x,key)
            if isinstance(xeval,str):
                try:
                    evaluator = SafeEvaluator(tmp)
                    return evaluate_with_placeholders(xeval,evaluator,evaluator_nocontext)
                except Exception as e:
                    if self._debug:
                        print(f"DEBUG {key}: Error evaluating '{x}': {e}")
                    return xeval  # default fallback value
            else:
                return xeval

        # Evaluate all DEFINITIONS
        for key,value in self.items():
            # strings are assumed to be expressions on one single line
            if isinstance(value,str):
                tmp.setattr(key,evalstr(value,key))
            elif isinstance(value,_numeric_types): # already a number
                if isinstance(value,list):
                    valuelist = [safe_evalstr(x,key) if isinstance(x,str) else x for x in value]
                    tmp.setattr(key,valuelist)
                else:
                    tmp.setattr(key, value) # store the value with the key
            elif isinstance(value, dict):
                # For dictionaries, evaluate each entry using its own key (sub_key)
                new_dict = {}
                for sub_key, sub_value in value.items():
                    if isinstance(sub_value, str):
                        new_dict[sub_key] = safe_evalstr(sub_value,key)
                    elif isinstance(sub_value, list):
                        # If an entry is a list, apply safe_evalstr to each string element within it
                        new_dict[sub_key] = [safe_evalstr(x, sub_key) if isinstance(x, str) else x
                                             for x in sub_value]
                    else:
                        new_dict[sub_key] = sub_value
                tmp.setattr(key, new_dict)
            else: # unsupported types
                if s.find("{"+key+"}")>=0:
                    print(f'*** WARNING ***\n\tIn the {self._ftype}:"\n{s}\n"')
                else:
                    print(f'unable to interpret the "{key}" of type {type(value)}')
        return tmp

    # formateval obeys to following rules
    # lines starting with # (hash) are interpreted as comments
    def formateval(self,s,protection=False,fullevaluation=True):
        """
            format method with evaluation feature

                txt = p.formateval("this my text with ${variable1}, ${variable2} ")

                where:
                    p is a param object

                Example:
                    definitions = param(a=1,b="${a}",c="\\${a}")
                    text = definitions.formateval("this my text ${a}, ${b}, ${c}")
                    print(text)

        """
        tmp = self.eval(s,protection=protection)
        evaluator = SafeEvaluator(tmp) # used when fullevaluation=True
        evaluator_nocontext = SafeEvaluator() # for global evaluation without context
        # Do all replacements in s (keep comments)
        if len(tmp)==0:
            return s
        else:
            ispstr = isinstance(s,pstr)
            ssafe, escape = param.escape(s)
            slines = ssafe.split("\n")
            slines_priorescape = s.split("\n")
            for i in range(len(slines)):
                poscomment = slines[i].find("#")
                if poscomment>=0:
                    while (poscomment>0) and (slines[i][poscomment-1]==" "):
                        poscomment -= 1
                    comment = slines[i][poscomment:len(slines[i])]
                    slines[i]  = slines[i][0:poscomment]
                else:
                    comment = ""
                # Protect variables if required
                if protection or self._protection:
                    slines[i], escape2 = self.protect(slines[i])
                # conversion
                if ispstr:
                    slines[i] = pstr.eval(tmp.format(slines[i],escape=escape),ispstr=ispstr)
                else:
                    if fullevaluation:
                        try:
                            resstr =tmp.format(slines[i],escape=escape)
                        except:
                            resstr = param.safe_fstring(slines[i],tmp,varprefix="")
                        try:
                            #reseval = evaluator.evaluate(resstr)
                            reseval = evaluate_with_placeholders(slines_priorescape[i],evaluator,evaluator_nocontext,raiseerror=True)
                            slines[i] = str(reseval)+" "+comment if comment else str(reseval)
                        except:
                            slines[i] = resstr + comment
                    else:
                        slines[i] = tmp.format(slines[i],escape=escape)+comment
                # convert starting % into # to authorize replacement in comments
                if len(slines[i])>0:
                    if slines[i][0] == "%": slines[i]="#"+slines[i][1:]
            return "\n".join(slines)


    # return the value instead of formula
    def getval(self,key):
        """ returns the evaluated value """
        s = self.eval()
        return getattr(s,key)

    # override () for subindexing structure with key names
    def __call__(self, *keys):
        """
        Extract an evaluated 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
            An evaluated instance of class struct, containing
            only the specified keys with evaluated values.

        Usage:
        ------
        sub_struct = p('key1', 'key2', ...)
        """
        s = self.eval()
        if keys:
            return s(*keys)
        else:
            return s

    # returns the equivalent structure evaluated
    def tostruct(self,protection=False):
        """
            generate the evaluated structure
                tostruct(protection=False)
        """
        return self.eval(protection=protection)

    # returns the equivalent paramauto instance
    def toparamauto(self):
        """
            convert a param instance into a paramauto instance
                toparamauto()
        """
        return paramauto(**self)

    # returns the equivalent structure evaluated
    def tostatic(self):
        """ convert dynamic a param() object to a static struct() object.
            note: no interpretation
            note: use tostruct() to interpret them and convert it to struct
            note: tostatic().struct2param() makes it reversible
        """
        return struct.fromkeysvalues(self.keys(),self.values(),makeparam=False)

    # Matlab vector/list conversion
    @staticmethod
    def expand_ranges(text,numfmt=".4g"):
        """
        Expands MATLAB-style ranges in a string.

        Args:
            text: The input string containing ranges.
            numfmt: numeric format to be used for the string conversion (default=".4g")

        Returns:
            The string with ranges expanded, or the original string if no valid ranges are found
            or if expansion leads to more than 100 elements. Returns an error message if the input
            format is invalid.
        """
        def expand_range(match):
            try:
                parts = match.group(1).split(':')
                if len(parts) == 2:
                    start, stop = map(float, parts)
                    step = 1.0
                elif len(parts) == 3:
                    start, step, stop = map(float, parts)
                else:
                    return match.group(0)  # Return original if format is invalid
                if step == 0:
                    return "Error: <Step cannot be zero.>"
                if (stop - start) / step > 1e6:
                    return "Error: <Range is too large.>"
                if step > 0:
                    num_elements = int(np.floor((stop - start)/step)+1)
                else:
                     num_elements = int(np.floor((start - stop)/-step)+1)
                if num_elements > 100:
                    return match.group(0)  # Return original if too many elements
                expanded_range = np.arange(start, stop + np.sign(step)*1e-9, step) #adding a small number to include the stop in case of integer steps
                return '[' + ','.join(f'{x:{numfmt}}' for x in expanded_range) + ']'
            except ValueError:
                return "Error: <Invalid range format.>"
        pattern = r'(\b(?:-?\d+(?:\.\d*)?|-?\.\d+)(?::(?:-?\d+(?:\.\d*)?|-?\.\d+)){1,2})\b'
        expanded_text = re.sub(pattern, expand_range, text)
        #check for errors generated by the function
        if "Error:" in expanded_text:
            return expanded_text
        return expanded_text

    # Matlab syntax conversion
    @staticmethod
    def convert_matlab_like_arrays(text):
        """
        Converts Matlab-like array syntax (including hybrid notations) into
        a NumPy-esque list syntax in multiple passes.

        Steps:
          1) Convert 2D Matlab arrays (containing semicolons) into Python-like nested lists.
          2) Convert bracketed row vectors (no semicolons or nested brackets) into double-bracket format.
          3) Replace spaces with commas under specific conditions and remove duplicates.

        Args:
            text (str): Input string that may contain Matlab-like arrays.

        Returns:
            str: Transformed text with arrays converted to a Python/NumPy-like syntax.

        Examples:

            examples = [
                "[1, 2  ${var1}          ; 4, 5     ${var2}]",
                "[1,2,3]",
                "[1 2 ,  3]",
                "[1;2; 3]",
                "[[-0.5, 0.5;-0.5, 0.5],[ -0.5,  0.5; -0.5,  0.5]]",
                "[[1,2;3,4],[5,6; 7,8]]",
                "[1, 2, 3; 4, 5, 6]",  # Hybrid
                "[[Already, in, Python]]",  # Already Python-like?
                "Not an array"
            ]

            for ex in examples:
                converted = param.convert_matlab_like_arrays(ex)
                print(f"Matlab: {ex}\nNumPy : {converted}\n")

        """
        # --------------------------------------------------------------------------
        # Step 1: Detect innermost [ ... ; ... ] blocks and convert them
        # --------------------------------------------------------------------------
        def convert_matrices_with_semicolons(txt):
            """
            Repeatedly find the innermost bracket pair that contains a semicolon
            and convert it to a Python-style nested list, row by row.
            """
            # Pattern to find innermost [ ... ; ... ] without nested brackets
            pattern = r'\[[^\[\]]*?;[^\[\]]*?\]'
            while True:
                match = re.search(pattern, txt)
                if not match:
                    break  # No more [ ... ; ... ] blocks to convert
                inner_block = match.group(0)
                # Remove the outer brackets
                inner_content = inner_block[1:-1].strip()
                # Split into rows by semicolon
                rows = [row.strip() for row in inner_content.split(';')]
                converted_rows = []
                for row in rows:
                    # Replace multiple spaces with a single space
                    row_clean = re.sub(r'\s+', ' ', row)
                    # Split row by commas or spaces
                    row_elems = re.split(r'[,\s]+', row_clean)
                    row_elems = [elem for elem in row_elems if elem]  # Remove empty strings
                    # Join elements with commas and encapsulate in brackets
                    converted_rows.append("[" + ",".join(row_elems) + "]")
                # Join the row lists and encapsulate them in brackets
                replacement = "[" + ",".join(converted_rows) + "]"
                # Replace the original Matlab matrix with the Python list
                txt = txt[:match.start()] + replacement + txt[match.end():]
            return txt
        # --------------------------------------------------------------------------
        # Step 2: Convert row vectors without semicolons or nested brackets
        #         into double-bracket format, e.g. [1,2,3] -> [[1,2,3]]
        # --------------------------------------------------------------------------
        def convert_row_vectors(txt):
            """
            Convert [1,2,3] or [1 2 3] into [[1,2,3]] if the bracket does not contain
            semicolons, nor nested brackets. We do this iteratively, skipping any
            bracket blocks that don't qualify, rather than stopping.
            """
            # We only want bracket blocks that are NOT preceded by '[' or ','
            # do not contain semicolons or nested brackets
            # and are not followed by ']' or ','
            pattern = r"(?<!\[)(?<!,)\([^();]*\)(?!\s*\])(?!\s*\,)"
            startpos = 0
            while True:
                match = re.search(pattern, txt[startpos:])
                if not match:
                    break  # No more bracket blocks to check
                # Compute absolute positions in txt
                mstart = startpos + match.start()
                mend   = startpos + match.end()
                block  = txt[mstart:mend]
                # we need to be sure that [ ] are present around the block even if separated by spaces
                if mstart == 0 or mend == len(txt) - 1:
                    break
                if not (re.match(r"\[\s*$", txt[:mstart]) and re.match(r"^\s*\]", txt[mend+1:])):
                    break
                # Double-check that this bracket does not contain semicolons or nested brackets
                # If it does, we skip it (just advance the search) to avoid messing up matrices.
                # That is, we do not transform it into double brackets.
                if ';' in block or '[' in block[1:-1] or ']' in block[1:-1]:
                    # Move beyond this match and keep searching
                    startpos = mend
                    continue
                # It's a pure row vector (no semicolons, no nested brackets)
                new_block = "[" + block + "]"  # e.g. [1,2,3] -> [[1,2,3]]
                txt = txt[:mstart] + new_block + txt[mend:]
                # Update search position to avoid re-matching inside the newly inserted text
                startpos = mstart + len(new_block)
            return txt
        # --------------------------------------------------------------------------
        # Step 3: Replace spaces with commas under specific conditions and clean up
        # --------------------------------------------------------------------------
        def replace_spaces_safely(txt):
            """
            Replace spaces not preceded by ',', '[', or whitespace and followed by digit or '$' with commas.
            Then remove multiple consecutive commas and trailing commas before closing brackets.
            """
            # 1) Replace spaces with commas if not preceded by [,\s
            #    and followed by digit or $
            txt = re.sub(r'(?<![,\[\s])\s+(?=[\d\$])', ',', txt)
            # 2) Remove multiple consecutive commas
            txt = re.sub(r',+', ',', txt)
            # 3) Remove trailing commas before closing brackets
            txt = re.sub(r',+\]', ']', txt)
            return txt

        def replace_spaces_with_commas(txt):
            """
            Replaces spaces with commas only when they're within array shorthands and not preceded by a comma, opening bracket, or whitespace,
            and are followed by a digit or '$'. Also collapses multiple commas into one and strips leading/trailing commas.

            Parameters:
            ----------
            txt : str
                The text to process.

            Returns:
            -------
            str
                The processed text with appropriate commas.
            """
            # Replace spaces not preceded by ',', '[', or whitespace and followed by digit or '$' with commas
            txt = re.sub(r'(?<![,\[\s])\s+(?=[\d\$])', ',', txt)
            # Remove multiple consecutive commas
            txt = re.sub(r',+', ',', txt)
            # Strip leading and trailing commas
            txt = txt.strip(',')
            # Replace residual multiple consecutive spaces with a single space
            return re.sub(r'\s+', ' ', txt)

        # --------------------------------------------------------------------------
        # Apply Step 1: Convert matrices with semicolons
        # --------------------------------------------------------------------------
        text_cv = convert_matrices_with_semicolons(text)
        # --------------------------------------------------------------------------
        # Apply Step 2: Convert row vectors (no semicolons/nested brackets)
        # --------------------------------------------------------------------------
        text_cv = convert_row_vectors(text_cv)
        # --------------------------------------------------------------------------
        # Apply Step 3: Replace spaces with commas and clean up
        # --------------------------------------------------------------------------
        if text_cv != text:
            return replace_spaces_with_commas(text_cv) # old method: replace_spaces_safely(text_cv)
        else:
            return text



    @classmethod
    def replace_matrix_shorthand(cls,valuesafe):
        """
        Transforms custom shorthand notations for NumPy arrays within a string into valid NumPy array constructors.
        Supports up to 4-dimensional arrays and handles variable references.

        **Shorthand Patterns:**
        - **1D**: `$[1 2 3]` → `np.atleast_2d(np.array([1,2,3]))`
        - **2D**: `$[[1 2],[3 4]]` → `np.array([[1,2],[3,4]])`
        - **3D**: `$[[[1 2],[3 4]],[[5 6],[7 8]]]` → `np.array([[[1,2],[3,4]],[[5,6],[7,8]]])`
        - **4D**: `$[[[[1 2]]]]` → `np.array([[[[1,2]]]])`
        - **Variable References**: `@{var}` → `np.atleast_2d(np.array(${var}))`

        **Parameters:**
        ----------
        valuesafe : str
            The input string containing shorthand notations for NumPy arrays and variable references.

        **Returns:**
        -------
        str
            The transformed string with shorthands replaced by valid NumPy array constructors.

        **Raises:**
        -------
        ValueError
            If there are unmatched brackets in any shorthand.

        **Examples:**
        --------
        >>> # 1D shorthand
        >>> s = "$[1 2 3]"
        >>> param.replace_matrix_shorthand(s)
        'np.atleast_2d(np.array([1,2,3]))'

        >>> # 2D shorthand with mixed spacing
        >>> s = "$[[1, 2], [3 4]]"
        >>> param.replace_matrix_shorthand(s)
        'np.array([[1,2],[3,4]])'

        >>> # 3D array with partial spacing
        >>> s = "$[[[1  2], [3 4]], [[5 6], [7 8]]]"
        >>> param.replace_matrix_shorthand(s)
        'np.array([[[1,2],[3,4]],[[5,6],[7,8]]])'

        >>> # 4D array
        >>> s = "$[[[[1 2]]]]"
        >>> param.replace_matrix_shorthand(s)
        'np.array([[[[1,2]]]])'

        >>> # Combined with variable references
        >>> s = "@{a} + $[[${b}, 2],[ 3  4]]"
        >>> param.replace_matrix_shorthand(s)
        'np.atleast_2d(np.array(${a})) + np.array([[${b},2],[3,4]])'

        >>> # Complex ND array with scaling
        >>> s = '$[[[-0.5, -0.5],[-0.5, -0.5]],[[ 0.5,  0.5],[ 0.5,  0.5]]]*0.001'
        >>> param.replace_matrix_shorthand(s)
        'np.array([[[-0.5,-0.5],[-0.5,-0.5]],[[0.5,0.5],[0.5,0.5]]])*0.001'
        """

        numfmt = f".{cls._precision}g"

        def replace_spaces_with_commas(txt):
            """
            Replaces spaces with commas only when they're not preceded by a comma, opening bracket, or whitespace,
            and are followed by a digit or '$'. Also collapses multiple commas into one and strips leading/trailing commas.

            Parameters:
            ----------
            txt : str
                The text to process.

            Returns:
            -------
            str
                The processed text with appropriate commas.
            """
            # Replace spaces not preceded by ',', '[', or whitespace and followed by digit or '$' with commas
            txt = re.sub(r'(?<![,\[\s])\s+(?=[\d\$])', ',', txt)
            # Remove multiple consecutive commas
            txt = re.sub(r',+', ',', txt)
            return txt.strip(',')

        def build_pass_list(string):
            """
            Determines which dimensions (1D..4D) appear in the string by searching for:
             - 4D: $[[[[
             - 3D: $[[[
             - 2D: $[[
             - 1D: $[

            Returns a sorted list in descending order, e.g., [4, 3, 2, 1].

            Parameters:
            ----------
            string : str
                The input string to scan.

            Returns:
            -------
            list
                A list of integers representing the dimensions found, sorted descending.
            """
            dims_found = set()
            if re.search(r'\$\[\[\[\[', string):
                dims_found.add(4)
            if re.search(r'\$\[\[\[', string):
                dims_found.add(3)
            if re.search(r'\$\[\[', string):
                dims_found.add(2)
            if re.search(r'\$\[', string):
                dims_found.add(1)
            return sorted(dims_found, reverse=True)

        # Step 0: convert eventual Matlab syntax for row and column vectors into NumPy syntax
        valuesafe = param.expand_ranges(valuesafe,numfmt)  # expands start:stop and start:step:stop syntax
        valuesafe = param.convert_matlab_like_arrays(valuesafe) # vectors and matrices conversion
        # Step 1: Handle @{var} -> np.atleast_2d(np.array(${var}))
        valuesafe = re.sub(r'@\{([^\{\}]+)\}', r'np.atleast_2d(np.array(${\1}))', valuesafe)
        # Step 2: Build pass list from largest dimension to smallest
        pass_list = build_pass_list(valuesafe)
        # Step 3: Define patterns and replacements for each dimension
        dimension_patterns = {
            4: (r'\$\[\[\[\[(.*?)\]\]\]\]', 'np.array([[[[{content}]]]])'),   # 4D
            3: (r'\$\[\[\[(.*?)\]\]\]', 'np.array([[[{content}]]])'),         # 3D
            2: (r'\$\[\[(.*?)\]\]', 'np.array([[{content}]])'),               # 2D
            1: (r'\$\[(.*?)\]', 'np.atleast_2d(np.array([{content}]))')       # 1D
        }
        # Step 4: Iterate over each dimension and perform replacements
        for dim in pass_list:
            pattern, replacement_fmt = dimension_patterns[dim]
            # Find all non-overlapping matches for the current dimension
            matches = list(re.finditer(pattern, valuesafe))
            for match in matches:
                full_match = match.group(0)       # Entire matched shorthand
                inner_content = match.group(1)    # Content inside the brackets
                # Replace spaces with commas as per rules
                processed_content = replace_spaces_with_commas(inner_content.strip())
                # Create the replacement string
                replacement = replacement_fmt.format(content=processed_content)
                # Replace the shorthand in the string
                valuesafe = valuesafe.replace(full_match, replacement)
        # Step 5: Verify that all shorthands have been replaced by checking for remaining '$['
        if re.search(r'\$\[', valuesafe):
            raise ValueError("Unmatched or improperly formatted brackets detected in the input string.")
        return valuesafe



    # Safe fstring
    @staticmethod
    def safe_fstring(template, context,varprefix="$"):
        """Safely evaluate expressions in ${} using SafeEvaluator."""
        evaluator = SafeEvaluator(context)
        # Process template string in combination with safe_fstring()
        # it is required to have an output compatible with eval()

        def process_template(valuesafe):
            """
            Processes the input string by:
            1. Stripping leading and trailing whitespace.
            2. Removing comments (any text after '#' unless '#' is the first character).
            3. Replacing '^' with '**'.
            4. Replacing '{' with '${' if '{' is not preceded by '$'. <-- not applied anymore (brings confusion)

            Args:
                valuesafe (str): The input string to process.

            Returns:
                str: The processed string.
            """
            # Step 1: Strip leading and trailing whitespace
            valuesafe = valuesafe.strip()
            # Step 2: Remove comments
            # This regex removes '#' and everything after it if '#' is not the first character
            # (?<!^) is a negative lookbehind that ensures '#' is not at the start of the string
            valuesafe = re.sub(r'(?<!^)\#.*', '', valuesafe)
            # Step 3: Replace '^' with '**'
            valuesafe = re.sub(r'\^', '**', valuesafe)
            # Step 4: Replace '{' with '${' if '{' is not preceded by '$'
            # (?<!\$)\{ matches '{' not preceded by '$'
            # valuesafe = re.sub(r'(?<!\$)\{', '${', valuesafe)
            # Optional: Strip again to remove any trailing whitespace left after removing comments
            valuesafe = valuesafe.strip()
            return valuesafe

        # Adjusted display for NumPy arrays
        def serialize_result(result):
            """
            Serialize the result into a string that can be evaluated in Python.
            Handles NumPy arrays by converting them to lists with commas.
            Handles other iterable types appropriately.
            """
            if isinstance(result, np.ndarray):
                return str(result.tolist())
            elif isinstance(result, (list, tuple, dict)):
                return str(result)
            else:
                return str(result)
        # Regular expression to find ${expr} patterns
        escaped_varprefix = re.escape(varprefix)
        pattern = re.compile(escaped_varprefix+r'\{([^{}]+)\}')
        def replacer(match):
            expr = match.group(1)
            try:
                result = evaluator.evaluate(expr)
                serialized = serialize_result(result)
                return serialized
            except Exception as e:
                return f"<Error: {e}>"
        return pattern.sub(replacer, process_template(template))

Ancestors

  • pizza.private.mstruct.struct

Subclasses

  • pizza.private.mstruct.paramauto
  • pizza.script.scriptdata
  • scriptdata

Static methods

def convert_matlab_like_arrays(text)

Converts Matlab-like array syntax (including hybrid notations) into a NumPy-esque list syntax in multiple passes.

    Steps:
      1) Convert 2D Matlab arrays (containing semicolons) into Python-like nested lists.
      2) Convert bracketed row vectors (no semicolons or nested brackets) into double-bracket format.
      3) Replace spaces with commas under specific conditions and remove duplicates.

    Args:
        text (str): Input string that may contain Matlab-like arrays.

    Returns:
        str: Transformed text with arrays converted to a Python/NumPy-like syntax.

    Examples:

        examples = [
            "[1, 2  ${var1}          ; 4, 5     ${var2}]",
            "[1,2,3]",
            "[1 2 ,  3]",
            "[1;2; 3]",
            "[[-0.5, 0.5;-0.5, 0.5],[ -0.5,  0.5; -0.5,  0.5]]",
            "[[1,2;3,4],[5,6; 7,8]]",
            "[1, 2, 3; 4, 5, 6]",  # Hybrid
            "[[Already, in, Python]]",  # Already Python-like?
            "Not an array"
        ]

        for ex in examples:
            converted = param.convert_matlab_like_arrays(ex)
            print(f"Matlab: {ex}

NumPy : {converted} ")

Expand source code
@staticmethod
def convert_matlab_like_arrays(text):
    """
    Converts Matlab-like array syntax (including hybrid notations) into
    a NumPy-esque list syntax in multiple passes.

    Steps:
      1) Convert 2D Matlab arrays (containing semicolons) into Python-like nested lists.
      2) Convert bracketed row vectors (no semicolons or nested brackets) into double-bracket format.
      3) Replace spaces with commas under specific conditions and remove duplicates.

    Args:
        text (str): Input string that may contain Matlab-like arrays.

    Returns:
        str: Transformed text with arrays converted to a Python/NumPy-like syntax.

    Examples:

        examples = [
            "[1, 2  ${var1}          ; 4, 5     ${var2}]",
            "[1,2,3]",
            "[1 2 ,  3]",
            "[1;2; 3]",
            "[[-0.5, 0.5;-0.5, 0.5],[ -0.5,  0.5; -0.5,  0.5]]",
            "[[1,2;3,4],[5,6; 7,8]]",
            "[1, 2, 3; 4, 5, 6]",  # Hybrid
            "[[Already, in, Python]]",  # Already Python-like?
            "Not an array"
        ]

        for ex in examples:
            converted = param.convert_matlab_like_arrays(ex)
            print(f"Matlab: {ex}\nNumPy : {converted}\n")

    """
    # --------------------------------------------------------------------------
    # Step 1: Detect innermost [ ... ; ... ] blocks and convert them
    # --------------------------------------------------------------------------
    def convert_matrices_with_semicolons(txt):
        """
        Repeatedly find the innermost bracket pair that contains a semicolon
        and convert it to a Python-style nested list, row by row.
        """
        # Pattern to find innermost [ ... ; ... ] without nested brackets
        pattern = r'\[[^\[\]]*?;[^\[\]]*?\]'
        while True:
            match = re.search(pattern, txt)
            if not match:
                break  # No more [ ... ; ... ] blocks to convert
            inner_block = match.group(0)
            # Remove the outer brackets
            inner_content = inner_block[1:-1].strip()
            # Split into rows by semicolon
            rows = [row.strip() for row in inner_content.split(';')]
            converted_rows = []
            for row in rows:
                # Replace multiple spaces with a single space
                row_clean = re.sub(r'\s+', ' ', row)
                # Split row by commas or spaces
                row_elems = re.split(r'[,\s]+', row_clean)
                row_elems = [elem for elem in row_elems if elem]  # Remove empty strings
                # Join elements with commas and encapsulate in brackets
                converted_rows.append("[" + ",".join(row_elems) + "]")
            # Join the row lists and encapsulate them in brackets
            replacement = "[" + ",".join(converted_rows) + "]"
            # Replace the original Matlab matrix with the Python list
            txt = txt[:match.start()] + replacement + txt[match.end():]
        return txt
    # --------------------------------------------------------------------------
    # Step 2: Convert row vectors without semicolons or nested brackets
    #         into double-bracket format, e.g. [1,2,3] -> [[1,2,3]]
    # --------------------------------------------------------------------------
    def convert_row_vectors(txt):
        """
        Convert [1,2,3] or [1 2 3] into [[1,2,3]] if the bracket does not contain
        semicolons, nor nested brackets. We do this iteratively, skipping any
        bracket blocks that don't qualify, rather than stopping.
        """
        # We only want bracket blocks that are NOT preceded by '[' or ','
        # do not contain semicolons or nested brackets
        # and are not followed by ']' or ','
        pattern = r"(?<!\[)(?<!,)\([^();]*\)(?!\s*\])(?!\s*\,)"
        startpos = 0
        while True:
            match = re.search(pattern, txt[startpos:])
            if not match:
                break  # No more bracket blocks to check
            # Compute absolute positions in txt
            mstart = startpos + match.start()
            mend   = startpos + match.end()
            block  = txt[mstart:mend]
            # we need to be sure that [ ] are present around the block even if separated by spaces
            if mstart == 0 or mend == len(txt) - 1:
                break
            if not (re.match(r"\[\s*$", txt[:mstart]) and re.match(r"^\s*\]", txt[mend+1:])):
                break
            # Double-check that this bracket does not contain semicolons or nested brackets
            # If it does, we skip it (just advance the search) to avoid messing up matrices.
            # That is, we do not transform it into double brackets.
            if ';' in block or '[' in block[1:-1] or ']' in block[1:-1]:
                # Move beyond this match and keep searching
                startpos = mend
                continue
            # It's a pure row vector (no semicolons, no nested brackets)
            new_block = "[" + block + "]"  # e.g. [1,2,3] -> [[1,2,3]]
            txt = txt[:mstart] + new_block + txt[mend:]
            # Update search position to avoid re-matching inside the newly inserted text
            startpos = mstart + len(new_block)
        return txt
    # --------------------------------------------------------------------------
    # Step 3: Replace spaces with commas under specific conditions and clean up
    # --------------------------------------------------------------------------
    def replace_spaces_safely(txt):
        """
        Replace spaces not preceded by ',', '[', or whitespace and followed by digit or '$' with commas.
        Then remove multiple consecutive commas and trailing commas before closing brackets.
        """
        # 1) Replace spaces with commas if not preceded by [,\s
        #    and followed by digit or $
        txt = re.sub(r'(?<![,\[\s])\s+(?=[\d\$])', ',', txt)
        # 2) Remove multiple consecutive commas
        txt = re.sub(r',+', ',', txt)
        # 3) Remove trailing commas before closing brackets
        txt = re.sub(r',+\]', ']', txt)
        return txt

    def replace_spaces_with_commas(txt):
        """
        Replaces spaces with commas only when they're within array shorthands and not preceded by a comma, opening bracket, or whitespace,
        and are followed by a digit or '$'. Also collapses multiple commas into one and strips leading/trailing commas.

        Parameters:
        ----------
        txt : str
            The text to process.

        Returns:
        -------
        str
            The processed text with appropriate commas.
        """
        # Replace spaces not preceded by ',', '[', or whitespace and followed by digit or '$' with commas
        txt = re.sub(r'(?<![,\[\s])\s+(?=[\d\$])', ',', txt)
        # Remove multiple consecutive commas
        txt = re.sub(r',+', ',', txt)
        # Strip leading and trailing commas
        txt = txt.strip(',')
        # Replace residual multiple consecutive spaces with a single space
        return re.sub(r'\s+', ' ', txt)

    # --------------------------------------------------------------------------
    # Apply Step 1: Convert matrices with semicolons
    # --------------------------------------------------------------------------
    text_cv = convert_matrices_with_semicolons(text)
    # --------------------------------------------------------------------------
    # Apply Step 2: Convert row vectors (no semicolons/nested brackets)
    # --------------------------------------------------------------------------
    text_cv = convert_row_vectors(text_cv)
    # --------------------------------------------------------------------------
    # Apply Step 3: Replace spaces with commas and clean up
    # --------------------------------------------------------------------------
    if text_cv != text:
        return replace_spaces_with_commas(text_cv) # old method: replace_spaces_safely(text_cv)
    else:
        return text
def escape(s)

escape \${} as ${{}} –> keep variable names convert ${} as {} –> prepare Python replacement

Examples

escape("\${a}") returns ('${{a}}', True)

escape(" \${abc} ${a} \${bc}") returns (' ${{abc}} {a} ${{bc}}', True)

escape("${a}") Out[94]: ('{a}', False)

escape("${tata}") returns ('{tata}', False)

Expand source code
@staticmethod
def escape(s):
    """
        escape \\${} as ${{}} --> keep variable names
        convert ${} as {} --> prepare Python replacement

        Examples:
            escape("\\${a}")
            returns ('${{a}}', True)

            escape("  \\${abc} ${a} \\${bc}")
            returns ('  ${{abc}} {a} ${{bc}}', True)

            escape("${a}")
            Out[94]: ('{a}', False)

            escape("${tata}")
            returns ('{tata}', False)

    """
    if not isinstance(s,str):
        raise TypeError(f'the argument must be string not {type(s)}')
    se, start, found = "", 0, True
    while found:
        pos0 = s.find(r"\${",start)
        found = pos0>=0
        if found:
            pos1 = s.find("}",pos0)
            found = pos1>=0
            if found:
                se += s[start:pos0].replace("${","{")+"${{"+s[pos0+3:pos1]+"}}"
                start=pos1+1
    result = se+s[start:].replace("${","{")
    if isinstance(s,pstr): result = pstr(result)
    return result,start>0
def expand_ranges(text, numfmt='.4g')

Expands MATLAB-style ranges in a string.

Args

text
The input string containing ranges.
numfmt
numeric format to be used for the string conversion (default=".4g")

Returns

The string with ranges expanded, or the original string if no valid ranges are found or if expansion leads to more than 100 elements. Returns an error message if the input format is invalid.

Expand source code
@staticmethod
def expand_ranges(text,numfmt=".4g"):
    """
    Expands MATLAB-style ranges in a string.

    Args:
        text: The input string containing ranges.
        numfmt: numeric format to be used for the string conversion (default=".4g")

    Returns:
        The string with ranges expanded, or the original string if no valid ranges are found
        or if expansion leads to more than 100 elements. Returns an error message if the input
        format is invalid.
    """
    def expand_range(match):
        try:
            parts = match.group(1).split(':')
            if len(parts) == 2:
                start, stop = map(float, parts)
                step = 1.0
            elif len(parts) == 3:
                start, step, stop = map(float, parts)
            else:
                return match.group(0)  # Return original if format is invalid
            if step == 0:
                return "Error: <Step cannot be zero.>"
            if (stop - start) / step > 1e6:
                return "Error: <Range is too large.>"
            if step > 0:
                num_elements = int(np.floor((stop - start)/step)+1)
            else:
                 num_elements = int(np.floor((start - stop)/-step)+1)
            if num_elements > 100:
                return match.group(0)  # Return original if too many elements
            expanded_range = np.arange(start, stop + np.sign(step)*1e-9, step) #adding a small number to include the stop in case of integer steps
            return '[' + ','.join(f'{x:{numfmt}}' for x in expanded_range) + ']'
        except ValueError:
            return "Error: <Invalid range format.>"
    pattern = r'(\b(?:-?\d+(?:\.\d*)?|-?\.\d+)(?::(?:-?\d+(?:\.\d*)?|-?\.\d+)){1,2})\b'
    expanded_text = re.sub(pattern, expand_range, text)
    #check for errors generated by the function
    if "Error:" in expanded_text:
        return expanded_text
    return expanded_text
def replace_matrix_shorthand(valuesafe)

Transforms custom shorthand notations for NumPy arrays within a string into valid NumPy array constructors. Supports up to 4-dimensional arrays and handles variable references.

Shorthand Patterns: - 1D: $[1 2 3]np.atleast_2d(np.array([1,2,3])) - 2D: $[[1 2],[3 4]]np.array([[1,2],[3,4]]) - 3D: $[[[1 2],[3 4]],[[5 6],[7 8]]]np.array([[[1,2],[3,4]],[[5,6],[7,8]]]) - 4D: $[[[[1 2]]]]np.array([[[[1,2]]]]) - Variable References: @{var}np.atleast_2d(np.array(${var}))

Parameters:

valuesafe : str The input string containing shorthand notations for NumPy arrays and variable references.

Returns:

str The transformed string with shorthands replaced by valid NumPy array constructors.

Raises:

ValueError If there are unmatched brackets in any shorthand.

Examples:

>>> # 1D shorthand
>>> s = "$[1 2 3]"
>>> param.replace_matrix_shorthand(s)
'np.atleast_2d(np.array([1,2,3]))'
>>> # 2D shorthand with mixed spacing
>>> s = "$[[1, 2], [3 4]]"
>>> param.replace_matrix_shorthand(s)
'np.array([[1,2],[3,4]])'
>>> # 3D array with partial spacing
>>> s = "$[[[1  2], [3 4]], [[5 6], [7 8]]]"
>>> param.replace_matrix_shorthand(s)
'np.array([[[1,2],[3,4]],[[5,6],[7,8]]])'
>>> # 4D array
>>> s = "$[[[[1 2]]]]"
>>> param.replace_matrix_shorthand(s)
'np.array([[[[1,2]]]])'
>>> # Combined with variable references
>>> s = "@{a} + $[[${b}, 2],[ 3  4]]"
>>> param.replace_matrix_shorthand(s)
'np.atleast_2d(np.array(${a})) + np.array([[${b},2],[3,4]])'
>>> # Complex ND array with scaling
>>> s = '$[[[-0.5, -0.5],[-0.5, -0.5]],[[ 0.5,  0.5],[ 0.5,  0.5]]]*0.001'
>>> param.replace_matrix_shorthand(s)
'np.array([[[-0.5,-0.5],[-0.5,-0.5]],[[0.5,0.5],[0.5,0.5]]])*0.001'
Expand source code
@classmethod
def replace_matrix_shorthand(cls,valuesafe):
    """
    Transforms custom shorthand notations for NumPy arrays within a string into valid NumPy array constructors.
    Supports up to 4-dimensional arrays and handles variable references.

    **Shorthand Patterns:**
    - **1D**: `$[1 2 3]` → `np.atleast_2d(np.array([1,2,3]))`
    - **2D**: `$[[1 2],[3 4]]` → `np.array([[1,2],[3,4]])`
    - **3D**: `$[[[1 2],[3 4]],[[5 6],[7 8]]]` → `np.array([[[1,2],[3,4]],[[5,6],[7,8]]])`
    - **4D**: `$[[[[1 2]]]]` → `np.array([[[[1,2]]]])`
    - **Variable References**: `@{var}` → `np.atleast_2d(np.array(${var}))`

    **Parameters:**
    ----------
    valuesafe : str
        The input string containing shorthand notations for NumPy arrays and variable references.

    **Returns:**
    -------
    str
        The transformed string with shorthands replaced by valid NumPy array constructors.

    **Raises:**
    -------
    ValueError
        If there are unmatched brackets in any shorthand.

    **Examples:**
    --------
    >>> # 1D shorthand
    >>> s = "$[1 2 3]"
    >>> param.replace_matrix_shorthand(s)
    'np.atleast_2d(np.array([1,2,3]))'

    >>> # 2D shorthand with mixed spacing
    >>> s = "$[[1, 2], [3 4]]"
    >>> param.replace_matrix_shorthand(s)
    'np.array([[1,2],[3,4]])'

    >>> # 3D array with partial spacing
    >>> s = "$[[[1  2], [3 4]], [[5 6], [7 8]]]"
    >>> param.replace_matrix_shorthand(s)
    'np.array([[[1,2],[3,4]],[[5,6],[7,8]]])'

    >>> # 4D array
    >>> s = "$[[[[1 2]]]]"
    >>> param.replace_matrix_shorthand(s)
    'np.array([[[[1,2]]]])'

    >>> # Combined with variable references
    >>> s = "@{a} + $[[${b}, 2],[ 3  4]]"
    >>> param.replace_matrix_shorthand(s)
    'np.atleast_2d(np.array(${a})) + np.array([[${b},2],[3,4]])'

    >>> # Complex ND array with scaling
    >>> s = '$[[[-0.5, -0.5],[-0.5, -0.5]],[[ 0.5,  0.5],[ 0.5,  0.5]]]*0.001'
    >>> param.replace_matrix_shorthand(s)
    'np.array([[[-0.5,-0.5],[-0.5,-0.5]],[[0.5,0.5],[0.5,0.5]]])*0.001'
    """

    numfmt = f".{cls._precision}g"

    def replace_spaces_with_commas(txt):
        """
        Replaces spaces with commas only when they're not preceded by a comma, opening bracket, or whitespace,
        and are followed by a digit or '$'. Also collapses multiple commas into one and strips leading/trailing commas.

        Parameters:
        ----------
        txt : str
            The text to process.

        Returns:
        -------
        str
            The processed text with appropriate commas.
        """
        # Replace spaces not preceded by ',', '[', or whitespace and followed by digit or '$' with commas
        txt = re.sub(r'(?<![,\[\s])\s+(?=[\d\$])', ',', txt)
        # Remove multiple consecutive commas
        txt = re.sub(r',+', ',', txt)
        return txt.strip(',')

    def build_pass_list(string):
        """
        Determines which dimensions (1D..4D) appear in the string by searching for:
         - 4D: $[[[[
         - 3D: $[[[
         - 2D: $[[
         - 1D: $[

        Returns a sorted list in descending order, e.g., [4, 3, 2, 1].

        Parameters:
        ----------
        string : str
            The input string to scan.

        Returns:
        -------
        list
            A list of integers representing the dimensions found, sorted descending.
        """
        dims_found = set()
        if re.search(r'\$\[\[\[\[', string):
            dims_found.add(4)
        if re.search(r'\$\[\[\[', string):
            dims_found.add(3)
        if re.search(r'\$\[\[', string):
            dims_found.add(2)
        if re.search(r'\$\[', string):
            dims_found.add(1)
        return sorted(dims_found, reverse=True)

    # Step 0: convert eventual Matlab syntax for row and column vectors into NumPy syntax
    valuesafe = param.expand_ranges(valuesafe,numfmt)  # expands start:stop and start:step:stop syntax
    valuesafe = param.convert_matlab_like_arrays(valuesafe) # vectors and matrices conversion
    # Step 1: Handle @{var} -> np.atleast_2d(np.array(${var}))
    valuesafe = re.sub(r'@\{([^\{\}]+)\}', r'np.atleast_2d(np.array(${\1}))', valuesafe)
    # Step 2: Build pass list from largest dimension to smallest
    pass_list = build_pass_list(valuesafe)
    # Step 3: Define patterns and replacements for each dimension
    dimension_patterns = {
        4: (r'\$\[\[\[\[(.*?)\]\]\]\]', 'np.array([[[[{content}]]]])'),   # 4D
        3: (r'\$\[\[\[(.*?)\]\]\]', 'np.array([[[{content}]]])'),         # 3D
        2: (r'\$\[\[(.*?)\]\]', 'np.array([[{content}]])'),               # 2D
        1: (r'\$\[(.*?)\]', 'np.atleast_2d(np.array([{content}]))')       # 1D
    }
    # Step 4: Iterate over each dimension and perform replacements
    for dim in pass_list:
        pattern, replacement_fmt = dimension_patterns[dim]
        # Find all non-overlapping matches for the current dimension
        matches = list(re.finditer(pattern, valuesafe))
        for match in matches:
            full_match = match.group(0)       # Entire matched shorthand
            inner_content = match.group(1)    # Content inside the brackets
            # Replace spaces with commas as per rules
            processed_content = replace_spaces_with_commas(inner_content.strip())
            # Create the replacement string
            replacement = replacement_fmt.format(content=processed_content)
            # Replace the shorthand in the string
            valuesafe = valuesafe.replace(full_match, replacement)
    # Step 5: Verify that all shorthands have been replaced by checking for remaining '$['
    if re.search(r'\$\[', valuesafe):
        raise ValueError("Unmatched or improperly formatted brackets detected in the input string.")
    return valuesafe
def safe_fstring(template, context, varprefix='$')

Safely evaluate expressions in ${} using SafeEvaluator.

Expand source code
@staticmethod
def safe_fstring(template, context,varprefix="$"):
    """Safely evaluate expressions in ${} using SafeEvaluator."""
    evaluator = SafeEvaluator(context)
    # Process template string in combination with safe_fstring()
    # it is required to have an output compatible with eval()

    def process_template(valuesafe):
        """
        Processes the input string by:
        1. Stripping leading and trailing whitespace.
        2. Removing comments (any text after '#' unless '#' is the first character).
        3. Replacing '^' with '**'.
        4. Replacing '{' with '${' if '{' is not preceded by '$'. <-- not applied anymore (brings confusion)

        Args:
            valuesafe (str): The input string to process.

        Returns:
            str: The processed string.
        """
        # Step 1: Strip leading and trailing whitespace
        valuesafe = valuesafe.strip()
        # Step 2: Remove comments
        # This regex removes '#' and everything after it if '#' is not the first character
        # (?<!^) is a negative lookbehind that ensures '#' is not at the start of the string
        valuesafe = re.sub(r'(?<!^)\#.*', '', valuesafe)
        # Step 3: Replace '^' with '**'
        valuesafe = re.sub(r'\^', '**', valuesafe)
        # Step 4: Replace '{' with '${' if '{' is not preceded by '$'
        # (?<!\$)\{ matches '{' not preceded by '$'
        # valuesafe = re.sub(r'(?<!\$)\{', '${', valuesafe)
        # Optional: Strip again to remove any trailing whitespace left after removing comments
        valuesafe = valuesafe.strip()
        return valuesafe

    # Adjusted display for NumPy arrays
    def serialize_result(result):
        """
        Serialize the result into a string that can be evaluated in Python.
        Handles NumPy arrays by converting them to lists with commas.
        Handles other iterable types appropriately.
        """
        if isinstance(result, np.ndarray):
            return str(result.tolist())
        elif isinstance(result, (list, tuple, dict)):
            return str(result)
        else:
            return str(result)
    # Regular expression to find ${expr} patterns
    escaped_varprefix = re.escape(varprefix)
    pattern = re.compile(escaped_varprefix+r'\{([^{}]+)\}')
    def replacer(match):
        expr = match.group(1)
        try:
            result = evaluator.evaluate(expr)
            serialized = serialize_result(result)
            return serialized
        except Exception as e:
            return f"<Error: {e}>"
    return pattern.sub(replacer, process_template(template))

Methods

def eval(self, s='', protection=False)

Eval method for structure such as MS.alias

s = p.eval() or s = p.eval(string)

where :
    p is a param object
    s is a structure with evaluated fields
    string is only used to determine whether definitions have been forgotten
Expand source code
def eval(self,s="",protection=False):
    """
        Eval method for structure such as MS.alias

            s = p.eval() or s = p.eval(string)

            where :
                p is a param object
                s is a structure with evaluated fields
                string is only used to determine whether definitions have been forgotten

    """
    # handle deferred sorting
    if self._needs_sorting:
        self.sortdefinitions(raiseerror=False, silentmode=True)
    # the argument s is only used by formateval() for error management
    tmp = struct(debug=self._debug)
    # evaluator without context
    evaluator_nocontext = SafeEvaluator() # for global evaluation without context

    # main string evaluator
    def evalstr(value,key=""):
        # replace ${variable} (Bash, Lammps syntax) by {variable} (Python syntax)
        # use \${variable} to prevent replacement (espace with \)
        # Protect variables if required
        ispstr = isinstance(value,pstr)
        valuesafe = pstr.eval(value,ispstr=ispstr) # value.strip()
        if valuesafe=="${"+key+"}": # circular reference (it cannot be evaluated)
            return valuesafe
        if protection or self._protection:
            valuesafe, escape0 = self.protect(valuesafe)
        else:
            escape0 = False
        # replace ${var} by {var} once basic substitutions have been applied
        valuesafe_priorescape = tmp.numrepl(valuesafe) # minimal substitution
        valuesafe, escape = param.escape(valuesafe_priorescape)
        escape = escape or escape0
        # replace "^" (Matlab, Lammps exponent) by "**" (Python syntax)
        valuesafe = pstr.eval(valuesafe.replace("^","**"),ispstr=ispstr)
        # Remove all content after #
        # if the first character is '#', it is not comment (e.g. MarkDown titles)
        poscomment = valuesafe.find("#")
        if poscomment>0: valuesafe = valuesafe[0:poscomment].strip()
        # Matrix shorthand replacement
        # $[[1,2,${a}]]+$[[10,20,30]] --> np.array([[1,2,${a}]])+np.array([[10,20,30]])
        valuesafe = param.replace_matrix_shorthand(valuesafe)
        # Literal string starts with $ (no interpretation), ! (evaluation)
        if not self._evaluation:
            return pstr.eval(tmp.format(valuesafe,escape),ispstr=ispstr)
        elif valuesafe.startswith("!"): # <---------- FORECED LITERAL EVALUATION (error messages are returned)
            try:
                #vtmp = ast.literal_eval(valuesafe[1:])
                evaluator = SafeEvaluator(tmp)
                vtmp = evaluate_with_placeholders(valuesafe[1:],evaluator,evaluator_nocontext)
                if isinstance(vtmp,list):
                    for i,item in enumerate(vtmp):
                        if isinstance(item,str) and not is_literal_string(item):
                            try:
                                vtmp[i] = tmp.format(item, raiseerror=False) # in case substitions/interpolations are needed
                                try:
                                    vtmp[i] = evaluator_nocontext.evaluate(vtmp[i]) # full evaluation without context
                                except Exception as othererr:
                                    if self._debug:
                                        print(f"DEBUG {key}: Error evaluating: {vtmp[i]}\n< {othererr} >")
                            except Exception as ve:
                                vtmp[i] = f"Error in <{item}>: {ve.__class__.__name__} - {str(ve)}"
                return vtmp
            except (SyntaxError, ValueError) as e:
                return f"Error: {e.__class__.__name__} - {str(e)}"
        elif valuesafe.startswith("$") and not escape:
            return tmp.format(valuesafe[1:].lstrip()) # discard $
        elif valuesafe.startswith("%"):
            return tmp.format(valuesafe[1:].lstrip()) # discard %
        else: # string empty or which can be evaluated
            if valuesafe=="":
                return valuesafe # empty content
            else:
                if isinstance(value,pstr): # keep path
                    return pstr.topath(tmp.format(valuesafe,escape=escape))
                elif escape:  # partial evaluation
                    return tmp.format(valuesafe,escape=True)
                else: # full evaluation (if it fails the last string content is returned) <---------- FULL EVALUTION will be tried
                    try:
                        resstr = tmp.format(valuesafe,raiseerror=False)
                    except (KeyError,NameError) as nameerr:
                        try: # nested indexing (guess)
                            resstr = param.safe_fstring(
                            param.replace_matrix_shorthand(valuesafe_priorescape),tmp)
                            evaluator = SafeEvaluator(tmp)
                            reseval = evaluate_with_placeholders(resstr,
                                      evaluator,evaluator_nocontext,raiseerror=True)
                        except Exception as othererr:
                            if self._returnerror: # added on 2024-09-06
                                strnameerr = str(nameerr).replace("'","")
                                if self._debug:
                                    print(f'Key Error for "{key}" < {othererr} >')
                                return '< undef %s "${%s}" >' % (self._ftype,strnameerr)
                            else:
                                return value #we keep the original value
                        else:
                            return reseval
                    except SyntaxError as commonerr:
                        return "Syntax Error < %s >" % commonerr
                    except TypeError  as commonerr:
                        try: # nested indexing (guess)
                            resstr = param.safe_fstring(
                            param.replace_matrix_shorthand(valuesafe_priorescape),tmp)
                            evaluator = SafeEvaluator(tmp)
                            reseval = evaluate_with_placeholders(resstr,
                                      evaluator,evaluator_nocontext,raiseerror=True)
                        except Exception as othererr:
                            if self._debug:
                                print(f'Type Error for "{key}" < {othererr} >')
                            return "Type Error < %s >" % commonerr
                        else:
                            return reseval
                    except (IndexError,AttributeError):
                        try:
                            resstr = param.safe_fstring(
                                param.replace_matrix_shorthand(valuesafe_priorescape),tmp)
                        except Exception as fstrerr:
                            return "Index Error < %s >" % fstrerr
                        else:
                            try:
                                # reseval = eval(resstr)
                                # reseval = ast.literal_eval(resstr)
                                # Use SafeEvaluator to evaluate the final expression
                                evaluator = SafeEvaluator(tmp)
                                reseval = evaluator.evaluate(resstr)
                            except Exception as othererr:
                                #tmp.setattr(key,"Mathematical Error around/in ${}: < %s >" % othererr)
                                if self._debug:
                                    print(f"DEBUG {key}: Error evaluating: {resstr}\n< {othererr} >")
                                return resstr
                            else:
                                return reseval
                    except ValueError as valerr: # forced evaluation within ${}
                        try:
                            evaluator = SafeEvaluator(tmp)
                            reseval = evaluate_with_placeholders(valuesafe_priorescape,evaluator,evaluator_nocontext,raiseerror=True)
                        except SyntaxError as synerror:
                            if self._debug:
                                print(f"DEBUG {key}: Error evaluating: {valuesafe_priorescape}\n< {synerror} >")
                            return evaluate_with_placeholders(valuesafe_priorescape,evaluator,evaluator_nocontext,raiseerror=False)
                        except Exception as othererr:
                            if self._debug:
                                print(f"DEBUG {key}: Error evaluating: {valuesafe_priorescape}\n< {othererr} >")
                            return "Error in ${}: < %s >" % valerr
                        else:
                            return reseval

                    except Exception as othererr:
                        return "Error in ${}: < %s >" % othererr
                    else:
                        try:
                            # reseval = eval(resstr)
                            evaluator = SafeEvaluator(tmp)
                            reseval = evaluate_with_placeholders(resstr,evaluator,evaluator_nocontext)
                        except Exception as othererr:
                            #tmp.setattr(key,"Eval Error < %s >" % othererr)
                            if self._debug:
                                print(f"DEBUG {key}: Error evaluating: {resstr}\n< {othererr} >")
                            return resstr.replace("\n",",") # \n replaced by ,
                        else:
                            return reseval

    # evalstr() refactored for error management
    def safe_evalstr(x,key=""):
        xeval = evalstr(x,key)
        if isinstance(xeval,str):
            try:
                evaluator = SafeEvaluator(tmp)
                return evaluate_with_placeholders(xeval,evaluator,evaluator_nocontext)
            except Exception as e:
                if self._debug:
                    print(f"DEBUG {key}: Error evaluating '{x}': {e}")
                return xeval  # default fallback value
        else:
            return xeval

    # Evaluate all DEFINITIONS
    for key,value in self.items():
        # strings are assumed to be expressions on one single line
        if isinstance(value,str):
            tmp.setattr(key,evalstr(value,key))
        elif isinstance(value,_numeric_types): # already a number
            if isinstance(value,list):
                valuelist = [safe_evalstr(x,key) if isinstance(x,str) else x for x in value]
                tmp.setattr(key,valuelist)
            else:
                tmp.setattr(key, value) # store the value with the key
        elif isinstance(value, dict):
            # For dictionaries, evaluate each entry using its own key (sub_key)
            new_dict = {}
            for sub_key, sub_value in value.items():
                if isinstance(sub_value, str):
                    new_dict[sub_key] = safe_evalstr(sub_value,key)
                elif isinstance(sub_value, list):
                    # If an entry is a list, apply safe_evalstr to each string element within it
                    new_dict[sub_key] = [safe_evalstr(x, sub_key) if isinstance(x, str) else x
                                         for x in sub_value]
                else:
                    new_dict[sub_key] = sub_value
            tmp.setattr(key, new_dict)
        else: # unsupported types
            if s.find("{"+key+"}")>=0:
                print(f'*** WARNING ***\n\tIn the {self._ftype}:"\n{s}\n"')
            else:
                print(f'unable to interpret the "{key}" of type {type(value)}')
    return tmp
def formateval(self, s, protection=False, fullevaluation=True)

format method with evaluation feature

txt = p.formateval("this my text with ${variable1}, ${variable2} ")

where:
    p is a param object

Example:
    definitions = param(a=1,b="${a}",c="\${a}")
    text = definitions.formateval("this my text ${a}, ${b}, ${c}")
    print(text)
Expand source code
def formateval(self,s,protection=False,fullevaluation=True):
    """
        format method with evaluation feature

            txt = p.formateval("this my text with ${variable1}, ${variable2} ")

            where:
                p is a param object

            Example:
                definitions = param(a=1,b="${a}",c="\\${a}")
                text = definitions.formateval("this my text ${a}, ${b}, ${c}")
                print(text)

    """
    tmp = self.eval(s,protection=protection)
    evaluator = SafeEvaluator(tmp) # used when fullevaluation=True
    evaluator_nocontext = SafeEvaluator() # for global evaluation without context
    # Do all replacements in s (keep comments)
    if len(tmp)==0:
        return s
    else:
        ispstr = isinstance(s,pstr)
        ssafe, escape = param.escape(s)
        slines = ssafe.split("\n")
        slines_priorescape = s.split("\n")
        for i in range(len(slines)):
            poscomment = slines[i].find("#")
            if poscomment>=0:
                while (poscomment>0) and (slines[i][poscomment-1]==" "):
                    poscomment -= 1
                comment = slines[i][poscomment:len(slines[i])]
                slines[i]  = slines[i][0:poscomment]
            else:
                comment = ""
            # Protect variables if required
            if protection or self._protection:
                slines[i], escape2 = self.protect(slines[i])
            # conversion
            if ispstr:
                slines[i] = pstr.eval(tmp.format(slines[i],escape=escape),ispstr=ispstr)
            else:
                if fullevaluation:
                    try:
                        resstr =tmp.format(slines[i],escape=escape)
                    except:
                        resstr = param.safe_fstring(slines[i],tmp,varprefix="")
                    try:
                        #reseval = evaluator.evaluate(resstr)
                        reseval = evaluate_with_placeholders(slines_priorescape[i],evaluator,evaluator_nocontext,raiseerror=True)
                        slines[i] = str(reseval)+" "+comment if comment else str(reseval)
                    except:
                        slines[i] = resstr + comment
                else:
                    slines[i] = tmp.format(slines[i],escape=escape)+comment
            # convert starting % into # to authorize replacement in comments
            if len(slines[i])>0:
                if slines[i][0] == "%": slines[i]="#"+slines[i][1:]
        return "\n".join(slines)
def getval(self, key)

returns the evaluated value

Expand source code
def getval(self,key):
    """ returns the evaluated value """
    s = self.eval()
    return getattr(s,key)
def protect(self, s='')

protect $variable as ${variable}

Expand source code
def protect(self,s=""):
    """ protect $variable as ${variable} """
    if isinstance(s,str):
        t = s.replace(r"\$","££") # && is a placeholder
        escape = t!=s
        for k in self.keyssorted():
            t = t.replace("$"+k,"${"+k+"}")
        if escape: t = t.replace("££",r"\$")
        if isinstance(s,pstr): t = pstr(t)
        return t, escape
    raise TypeError(f'the argument must be string not {type(s)}')
def toparamauto(self)

convert a param instance into a paramauto instance toparamauto()

Expand source code
def toparamauto(self):
    """
        convert a param instance into a paramauto instance
            toparamauto()
    """
    return paramauto(**self)
def tostatic(self)

convert dynamic a param() object to a static struct() object. note: no interpretation note: use tostruct() to interpret them and convert it to struct note: tostatic().struct2param() makes it reversible

Expand source code
def tostatic(self):
    """ convert dynamic a param() object to a static struct() object.
        note: no interpretation
        note: use tostruct() to interpret them and convert it to struct
        note: tostatic().struct2param() makes it reversible
    """
    return struct.fromkeysvalues(self.keys(),self.values(),makeparam=False)
def tostruct(self, protection=False)

generate the evaluated structure tostruct(protection=False)

Expand source code
def tostruct(self,protection=False):
    """
        generate the evaluated structure
            tostruct(protection=False)
    """
    return self.eval(protection=protection)
class paramauto (sortdefinitions=False, debug=False, **kwargs)

Class: paramauto

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


Features

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

Examples

Automatic Dependency Sorting

Definitions are automatically sorted to resolve dependencies:

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

Handling Missing Definitions

Unresolved dependencies raise warnings but do not block execution:

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

Concatenation and Inheritance

Concatenating paramauto objects resolves definitions:

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

Utility Methods

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

Overloaded Operators

Supported Operators

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

Advanced Usage

Partial Definitions

The paramauto class simplifies handling of partially defined fields:

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

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

Notes

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

Examples

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


    Example:

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

constructor

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

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

    ---

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

    ---

    ### Examples

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

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

    ---

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

    ---

    ### Utility Methods

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

    ---

    ### Overloaded Operators

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

    ---

    ### Advanced Usage

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

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

    ---

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

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


            Example:

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

    """

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

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

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

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

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

Ancestors

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

Subclasses

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

Methods

def setattr(self, key, value)

set field and value

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

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

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

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

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

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

Parameters:

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

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

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

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

Ancestors

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

Subclasses

  • pizza.generic.genericdata
class pipescript (s=None, name=None, printflag=False, verbose=True, verbosity=None)

pipescript: A Class for Managing Script Pipelines

The pipescript class stores scripts in a pipeline where multiple scripts, script objects, or script object groups can be combined and executed sequentially. Scripts in the pipeline are executed using the pipe (|) operator, allowing for dynamic control over execution order, script concatenation, and variable management.

Key Features:

  • Pipeline Construction: Create pipelines of scripts, combining multiple script objects, script, scriptobject, or scriptobjectgroup instances. The pipe operator (|) is overloaded to concatenate scripts.
  • Sequential Execution: Execute all scripts in the pipeline in the order they were added, with support for reordering, selective execution, and clearing of individual steps.
  • User and Definition Spaces: Manage local and global user-defined variables (USER space) and static definitions for each script in the pipeline. Global definitions apply to all scripts in the pipeline, while local variables apply to specific steps.
  • Flexible Script Handling: Indexing, slicing, reordering, and renaming scripts in the pipeline are supported. Scripts can be accessed, replaced, and modified like array elements.

Practical Use Cases:

  • LAMMPS Script Automation: Automate the generation of multi-step simulation scripts for LAMMPS, combining different simulation setups into a single pipeline.
  • Script Management: Combine and manage multiple scripts, tracking user variables and ensuring that execution order can be adjusted easily.
  • Advanced Script Execution: Perform partial pipeline execution, reorder steps, or clear completed steps while maintaining the original pipeline structure.

Methods:

init(self, s=None): Initializes a new pipescript object, optionally starting with a script or script-like object (script, scriptobject, scriptobjectgroup).

setUSER(self, idx, key, value): Set a user-defined variable (USER) for the script at the specified index.

getUSER(self, idx, key): Get the value of a user-defined variable (USER) for the script at the specified index.

clear(self, idx=None): Clear the execution status of scripts in the pipeline, allowing them to be executed again.

do(self, idx=None, printflag=True, verbosity=2, verbose=None, forced=False): Execute the pipeline or a subset of the pipeline, generating a combined LAMMPS-compatible script.

script(self, idx=None, printflag=True, verbosity=2, verbose=None, forced=False): Generate the final LAMMPS script from the pipeline or a subset of the pipeline.

rename(self, name="", idx=None): Rename the scripts in the pipeline, assigning new names to specific indices or all scripts.

write(self, file, printflag=True, verbosity=2, verbose=None): Write the generated script to a file.

dscript(self, verbose=None, **USER) Convert the current pipescript into a dscript object

header(self, verbose=True,verbosity=None, style=4): Generate a formatted header for the pipescript file.

list_values(self, varname, what="all"): List all occurrences and values of a variable across the pipeline scripts.

list_multiple_values(self, varnames, what="all"): List all occurrences and values of multiple variables across the pipeline scripts.

plot_value_distribution(self, varname, what="all"): Plot the distribution of values for a given variable across specified scopes.

generate_report(self, filename, varnames=None, scopes="all"): Generate a comprehensive report for specified variables and writes it to a file.

Static Methods:

join(liste): Combine a list of script and pipescript objects into a single pipeline.

Additional Features:

  • Indexing and Slicing: Use array-like indexing (p[0], p[1:3]) to access and manipulate scripts in the pipeline.
  • Deep Copy Support: The pipeline supports deep copying, preserving the entire pipeline structure and its scripts.
  • Verbose and Print Options: Control verbosity and printing behavior for generated scripts, allowing for detailed output or minimal script generation.

Original Content:

The pipescript class supports a variety of pipeline operations, including: - Sequential execution with cmd = p.do(). - Reordering pipelines with p[[2, 0, 1]]. - Deleting steps with p[[0, 1]] = []. - Accessing local and global user space variables via p.USER[idx].var and p.scripts[idx].USER.var. - Managing static definitions for each script in the pipeline. - Example usage: p = pipescript() p | i p = G | c | g | d | b | i | t | d | s | r p.rename(["G", "c", "g", "d", "b", "i", "t", "d", "s", "r"]) cmd = p.do([0, 1, 4, 7]) sp = p.script([0, 1, 4, 7]) - Scripts in the pipeline are executed sequentially, and definitions propagate from left to right. The USER space and DEFINITIONS are managed separately for each script in the pipeline.

Overview

Pipescript class stores scripts in pipelines
    By assuming: s0, s1, s2... scripts, scriptobject or scriptobjectgroup
    p = s0 | s1 | s2 generates a pipe script

    Example of pipeline:
  ------------:----------------------------------------
  [-]  00: G with (0>>0>>19) DEFINITIONS
  [-]  01: c with (0>>0>>10) DEFINITIONS
  [-]  02: g with (0>>0>>26) DEFINITIONS
  [-]  03: d with (0>>0>>19) DEFINITIONS
  [-]  04: b with (0>>0>>28) DEFINITIONS
  [-]  05: i with (0>>0>>49) DEFINITIONS
  [-]  06: t with (0>>0>>2) DEFINITIONS
  [-]  07: d with (0>>0>>19) DEFINITIONS
  [-]  08: s with (0>>0>>1) DEFINITIONS
  [-]  09: r with (0>>0>>20) DEFINITIONS
  ------------:----------------------------------------
Out[35]: pipescript containing 11 scripts with 8 executed[*]

note: XX>>YY>>ZZ represents the number of stored variables
     and the direction of propagation (inheritance from left)
     XX: number of definitions in the pipeline USER space
     YY: number of definitions in the script instance (frozen in the pipeline)
     ZZ: number of definitions in the script (frozen space)

    pipelines are executed sequentially (i.e. parameters can be multivalued)
        cmd = p.do()
        fullscript = p.script()

    pipelines are indexed
        cmd = p[[0,2]].do()
        cmd = p[0:2].do()
        cmd = p.do([0,2])

    pipelines can be reordered
        q = p[[2,0,1]]

    steps can be deleted
        p[[0,1]] = []

    clear all executions with
        p.clear()
        p.clear(idx=1,2)

    local USER space can be accessed via
    (affects only the considered step)
        p.USER[0].a = 1
        p.USER[0].b = [1 2]
        p.USER[0].c = "$ hello world"

    global USER space can accessed via
    (affects all steps onward)
        p.scripts[0].USER.a = 10
        p.scripts[0].USER.b = [10 20]
        p.scripts[0].USER.c = "$ bye bye"

    static definitions
        p.scripts[0].DEFINITIONS

    steps can be renamed with the method rename()

    syntaxes are à la Matlab:
        p = pipescript()
        p | i
        p = collection | G
        p[0]
        q = p | p
        q[0] = []
        p[0:1] = q[0:1]
        p = G | c | g | d | b | i | t | d | s | r
        p.rename(["G","c","g","d","b","i","t","d","s","r"])
        cmd = p.do([0,1,4,7])
        sp = p.script([0,1,4,7])
        r = collection | p

    join joins a list (static method)
        p = pipescript.join([p1,p2,s3,s4])


    Pending: mechanism to store LAMMPS results (dump3) in the pipeline

constructor

Expand source code
class pipescript:
    """
    pipescript: A Class for Managing Script Pipelines

    The `pipescript` class stores scripts in a pipeline where multiple scripts,
    script objects, or script object groups can be combined and executed
    sequentially. Scripts in the pipeline are executed using the pipe (`|`) operator,
    allowing for dynamic control over execution order, script concatenation, and
    variable management.

    Key Features:
    -------------
    - **Pipeline Construction**: Create pipelines of scripts, combining multiple
      script objects, `script`, `scriptobject`, or `scriptobjectgroup` instances.
      The pipe operator (`|`) is overloaded to concatenate scripts.
    - **Sequential Execution**: Execute all scripts in the pipeline in the order
      they were added, with support for reordering, selective execution, and
      clearing of individual steps.
    - **User and Definition Spaces**: Manage local and global user-defined variables
      (`USER` space) and static definitions for each script in the pipeline.
      Global definitions apply to all scripts in the pipeline, while local variables
      apply to specific steps.
    - **Flexible Script Handling**: Indexing, slicing, reordering, and renaming
      scripts in the pipeline are supported. Scripts can be accessed, replaced,
      and modified like array elements.

    Practical Use Cases:
    --------------------
    - **LAMMPS Script Automation**: Automate the generation of multi-step simulation
      scripts for LAMMPS, combining different simulation setups into a single pipeline.
    - **Script Management**: Combine and manage multiple scripts, tracking user
      variables and ensuring that execution order can be adjusted easily.
    - **Advanced Script Execution**: Perform partial pipeline execution, reorder
      steps, or clear completed steps while maintaining the original pipeline structure.

    Methods:
    --------
    __init__(self, s=None):
        Initializes a new `pipescript` object, optionally starting with a script
        or script-like object (`script`, `scriptobject`, `scriptobjectgroup`).

    setUSER(self, idx, key, value):
        Set a user-defined variable (`USER`) for the script at the specified index.

    getUSER(self, idx, key):
        Get the value of a user-defined variable (`USER`) for the script at the
        specified index.

    clear(self, idx=None):
        Clear the execution status of scripts in the pipeline, allowing them to
        be executed again.

    do(self, idx=None, printflag=True, verbosity=2, verbose=None, forced=False):
        Execute the pipeline or a subset of the pipeline, generating a combined
        LAMMPS-compatible script.

    script(self, idx=None, printflag=True, verbosity=2, verbose=None, forced=False):
        Generate the final LAMMPS script from the pipeline or a subset of the pipeline.

    rename(self, name="", idx=None):
        Rename the scripts in the pipeline, assigning new names to specific
        indices or all scripts.

    write(self, file, printflag=True, verbosity=2, verbose=None):
        Write the generated script to a file.

    dscript(self, verbose=None, **USER)
        Convert the current pipescript into a dscript object

    header(self, verbose=True,verbosity=None, style=4):
        Generate a formatted header for the pipescript file.

    list_values(self, varname, what="all"):
        List all occurrences and values of a variable across the pipeline scripts.

    list_multiple_values(self, varnames, what="all"):
        List all occurrences and values of multiple variables across the pipeline scripts.

    plot_value_distribution(self, varname, what="all"):
        Plot the distribution of values for a given variable across specified scopes.

    generate_report(self, filename, varnames=None, scopes="all"):
        Generate a comprehensive report for specified variables and writes it to a file.


    Static Methods:
    ---------------
    join(liste):
        Combine a list of `script` and `pipescript` objects into a single pipeline.

    Additional Features:
    --------------------
    - **Indexing and Slicing**: Use array-like indexing (`p[0]`, `p[1:3]`) to access
      and manipulate scripts in the pipeline.
    - **Deep Copy Support**: The pipeline supports deep copying, preserving the
      entire pipeline structure and its scripts.
    - **Verbose and Print Options**: Control verbosity and printing behavior for
      generated scripts, allowing for detailed output or minimal script generation.

    Original Content:
    -----------------
    The `pipescript` class supports a variety of pipeline operations, including:
    - Sequential execution with `cmd = p.do()`.
    - Reordering pipelines with `p[[2, 0, 1]]`.
    - Deleting steps with `p[[0, 1]] = []`.
    - Accessing local and global user space variables via `p.USER[idx].var` and
      `p.scripts[idx].USER.var`.
    - Managing static definitions for each script in the pipeline.
    - Example usage:
      ```
      p = pipescript()
      p | i
      p = G | c | g | d | b | i | t | d | s | r
      p.rename(["G", "c", "g", "d", "b", "i", "t", "d", "s", "r"])
      cmd = p.do([0, 1, 4, 7])
      sp = p.script([0, 1, 4, 7])
      ```
    - Scripts in the pipeline are executed sequentially, and definitions propagate
      from left to right. The `USER` space and `DEFINITIONS` are managed separately
      for each script in the pipeline.

    OVERVIEW
    -----------------
        Pipescript class stores scripts in pipelines
            By assuming: s0, s1, s2... scripts, scriptobject or scriptobjectgroup
            p = s0 | s1 | s2 generates a pipe script

            Example of pipeline:
          ------------:----------------------------------------
          [-]  00: G with (0>>0>>19) DEFINITIONS
          [-]  01: c with (0>>0>>10) DEFINITIONS
          [-]  02: g with (0>>0>>26) DEFINITIONS
          [-]  03: d with (0>>0>>19) DEFINITIONS
          [-]  04: b with (0>>0>>28) DEFINITIONS
          [-]  05: i with (0>>0>>49) DEFINITIONS
          [-]  06: t with (0>>0>>2) DEFINITIONS
          [-]  07: d with (0>>0>>19) DEFINITIONS
          [-]  08: s with (0>>0>>1) DEFINITIONS
          [-]  09: r with (0>>0>>20) DEFINITIONS
          ------------:----------------------------------------
        Out[35]: pipescript containing 11 scripts with 8 executed[*]

        note: XX>>YY>>ZZ represents the number of stored variables
             and the direction of propagation (inheritance from left)
             XX: number of definitions in the pipeline USER space
             YY: number of definitions in the script instance (frozen in the pipeline)
             ZZ: number of definitions in the script (frozen space)

            pipelines are executed sequentially (i.e. parameters can be multivalued)
                cmd = p.do()
                fullscript = p.script()

            pipelines are indexed
                cmd = p[[0,2]].do()
                cmd = p[0:2].do()
                cmd = p.do([0,2])

            pipelines can be reordered
                q = p[[2,0,1]]

            steps can be deleted
                p[[0,1]] = []

            clear all executions with
                p.clear()
                p.clear(idx=1,2)

            local USER space can be accessed via
            (affects only the considered step)
                p.USER[0].a = 1
                p.USER[0].b = [1 2]
                p.USER[0].c = "$ hello world"

            global USER space can accessed via
            (affects all steps onward)
                p.scripts[0].USER.a = 10
                p.scripts[0].USER.b = [10 20]
                p.scripts[0].USER.c = "$ bye bye"

            static definitions
                p.scripts[0].DEFINITIONS

            steps can be renamed with the method rename()

            syntaxes are à la Matlab:
                p = pipescript()
                p | i
                p = collection | G
                p[0]
                q = p | p
                q[0] = []
                p[0:1] = q[0:1]
                p = G | c | g | d | b | i | t | d | s | r
                p.rename(["G","c","g","d","b","i","t","d","s","r"])
                cmd = p.do([0,1,4,7])
                sp = p.script([0,1,4,7])
                r = collection | p

            join joins a list (static method)
                p = pipescript.join([p1,p2,s3,s4])


            Pending: mechanism to store LAMMPS results (dump3) in the pipeline
    """

    def __init__(self,s=None, name=None, printflag=False, verbose=True, verbosity = None):
        """ constructor """
        self.globalscript = None
        self.listscript = []
        self.listUSER = []
        self.printflag = printflag
        self.verbose = verbose if verbosity is None else verbosity>0
        self.verbosity = 0 if not verbose else verbosity
        self.cmd = ""
        if isinstance(s,script):
            self.listscript = [duplicate(s)]
            self.listUSER = [scriptdata()]
        elif isinstance(s,scriptobject):
            self.listscript = [scriptobjectgroup(s).script]
            self.listUSER = [scriptdata()]
        elif isinstance(s,scriptobjectgroup):
            self.listscript = [s.script]
            self.listUSER = [scriptdata()]
        else:
            ValueError("the argument should be a scriptobject or scriptobjectgroup")
        if s != None:
            self.name = [str(s)]
            self.executed = [False]
        else:
            self.name = []
            self.executed = []

    def setUSER(self,idx,key,value):
        """
            setUSER sets USER variables
            setUSER(idx,varname,varvalue)
        """
        if isinstance(idx,int) and (idx>=0) and (idx<self.n):
            self.listUSER[idx].setattr(key,value)
        else:
            raise IndexError(f"the index in the pipe should be comprised between 0 and {len(self)-1}")

    def getUSER(self,idx,key):
        """
            getUSER get USER variable
            getUSER(idx,varname)
        """
        if isinstance(idx,int) and (idx>=0) and (idx<self.n):
            self.listUSER[idx].getattr(key)
        else:
            raise IndexError(f"the index in the pipe should be comprised between 0 and {len(self)-1}")

    @property
    def USER(self):
        """
            p.USER[idx].var returns the value of the USER variable var
            p.USER[idx].var = val assigns the value val to the USER variable var
        """
        return self.listUSER  # override listuser

    @property
    def scripts(self):
        """
            p.scripts[idx].USER.var returns the value of the USER variable var
            p.scripts[idx].USER.var = val assigns the value val to the USER variable var
        """
        return self.listscript # override listuser

    def __add__(self,s):
        """ overload + as pipe with copy """
        from pizza.dscript import dscript
        if isinstance(s,(pipescript,script)):
            dup = deepduplicate(self)
            return dup | s      # + or | are synonyms
        elif isinstance(s,scriptobject):
            return self + s.script(printflag=self.printflag,verbose=self.verbose,verbosity=self.verbosity)
        elif isinstance(s,dscript):
            return self + s.pipescript(printflag=s.printflag,verbose=s.verbose,verbosity=s.verbosity)
        else:
            raise TypeError(f"The operand should be a pipescript/script/dscript/scriptobjectgroup and not '{type(s).__name__}'")

    def __iadd__(self,s):
        """ overload += as pipe without copy """
        if isinstance(s,pipescript):
            return self | s      # + or | are synonyms
        else:
            raise TypeError(f"The operand should be a pipescript and not '{type(s).__name__}'")

    def __mul__(self,ntimes):
        """ overload * as multiple pipes with copy """
        if isinstance(self,pipescript):
            res = deepduplicate(self)
            if ntimes>1:
                for n in range(1,ntimes): res += self
            return res
        else:
            raise TypeError(f"The operand should be a pipescript and not '{type(s).__name__}'")



    def __or__(self, s):
        """ Overload | pipe operator in pipescript """
        leftarg = deepduplicate(self)  # Make a deep copy of the current object
        # Local import only when dscript type needs to be checked
        from pizza.dscript import dscript
        from pizza.group import group, groupcollection
        from pizza.region import region
        # Convert rightarg to pipescript if needed
        if isinstance(s, dscript):
            rightarg = s.pipescript(printflag=s.printflag,verbose=s.verbose,verbosity=s.verbosity)  # Convert the dscript object to a pipescript
            native = False
        elif isinstance(s,script):
            rightarg = pipescript(s,printflag=s.printflag,verbose=s.verbose,verbosity=s.verbosity)  # Convert the script-like object into a pipescript
            native = False
        elif isinstance(s,(scriptobject,scriptobjectgroup)):
            stmp = s.script(printflag=self.printflag,verbose=self.verbose,verbosity=self.verbosity)
            rightarg =  pipescript(stmp,printflag=self.printflag,verbose=self.verbose,verbosity=self.verbosity)  # Convert the script-like object into a pipescript
            native = False
        elif isinstance(s, group):
            stmp = s.script(printflag=s.printflag,verbose=s.verbose,verbosity=s.verbosity)
            rightarg = pipescript(stmp,printflag=s.printflag,verbose=s.verbose,verbosity=s.verbosity)  # Convert the script-like object into a pipescript
            native = False
        elif isinstance(s, groupcollection):
            stmp = s.script(printflag=self.printflag,verbose=self.verbose,verbosity=self.verbosity)
            rightarg = pipescript(stmp,printflag=self.printflag,verbose=self.verbose,verbosity=self.verbosity)  # Convert the script-like object into a pipescript
            native = False
        elif isinstance(s, region):
            rightarg = s.pipescript(printflag=self.printflag,verbose=self.verbose,verbosity=self.verbosity)  # Convert the script-like object into a pipescript
            native = False
        elif isinstance(s,pipescript):
            rightarg = s
            native = True
        else:
            raise TypeError(f"The operand should be a pipescript, dscript, script, scriptobject, scriptobjectgroup, group or groupcollection not {type(s)}")
        # Native piping
        if native:
            leftarg.listscript = leftarg.listscript + rightarg.listscript
            leftarg.listUSER = leftarg.listUSER + rightarg.listUSER
            leftarg.name = leftarg.name + rightarg.name
            for i in range(len(rightarg)):
                rightarg.executed[i] = False
            leftarg.executed = leftarg.executed + rightarg.executed
            return leftarg
        # Piping for non-native objects (dscript or script-like objects)
        else:
            # Loop through all items in rightarg and concatenate them
            for i in range(rightarg.n):
                leftarg.listscript.append(rightarg.listscript[i])
                leftarg.listUSER.append(rightarg.listUSER[i])
                leftarg.name.append(rightarg.name[i])
                leftarg.executed.append(False)
            return leftarg



    def __str__(self):
        """ string representation """
        return f"pipescript containing {self.n} scripts with {self.nrun} executed[*]"


    def __repr__(self):
        """ display method """
        line = "  "+"-"*12+":"+"-"*40
        if self.verbose:
            print("","Pipeline with %d scripts and" % self.n,
                  "D(STATIC:GLOBAL:LOCAL) DEFINITIONS",line,sep="\n")
        else:
            print(line)
        for i in range(len(self)):
            if self.executed[i]:
                state = "*"
            else:
                state = "-"
            print("%10s" % ("[%s]  %02d:" % (state,i)),
                  self.name[i],"with D(%2d:%2d:%2d)" % (
                       len(self.listscript[i].DEFINITIONS),
                       len(self.listscript[i].USER),
                       len(self.listUSER[i])                 )
                  )
        if self.verbose:
            print(line,"::: notes :::","p[i], p[i:j], p[[i,j]] copy pipeline segments",
                  "LOCAL: p.USER[i],p.USER[i].variable modify the user space of only p[i]",
                  "GLOBAL: p.scripts[i].USER.var to modify the user space from p[i] and onwards",
                  "STATIC: p.scripts[i].DEFINITIONS",
                  'p.rename(idx=range(2),name=["A","B"]), p.clear(idx=[0,3,4])',
                  "p.script(), p.script(idx=range(5)), p[0:5].script()","",sep="\n")
        else:
             print(line)
        return str(self)

    def __len__(self):
        """ len() method """
        return len(self.listscript)

    @property
    def n(self):
        """ number of scripts """
        return len(self)

    @property
    def nrun(self):
        """ number of scripts executed continuously from origin """
        n, nmax  = 0, len(self)
        while n<nmax and self.executed[n]: n+=1
        return n

    def __getitem__(self,idx):
        """ return the ith or slice element(s) of the pipe  """
        dup = deepduplicate(self)
        if isinstance(idx,slice):
            dup.listscript = dup.listscript[idx]
            dup.listUSER = dup.listUSER[idx]
            dup.name = dup.name[idx]
            dup.executed = dup.executed[idx]
        elif isinstance(idx,int):
            if idx<len(self):
                dup.listscript = dup.listscript[idx:idx+1]
                dup.listUSER = dup.listUSER[idx:idx+1]
                dup.name = dup.name[idx:idx+1]
                dup.executed = dup.executed[idx:idx+1]
            else:
                raise IndexError(f"the index in the pipe should be comprised between 0 and {len(self)-1}")
        elif isinstance(idx,list):
            dup.listscript = picker(dup.listscript,idx)
            dup.listUSER = picker(dup.listUSER,idx)
            dup.name = picker(dup.name,idx)
            dup.executed = picker(dup.executed,idx)
        else:
            raise IndexError("the index needs to be a slice or an integer")
        return dup

    def __setitem__(self,idx,s):
        """
            modify the ith element of the pipe
                p[4] = [] removes the 4th element
                p[4:7] = [] removes the elements from position 4 to 6
                p[2:4] = p[0:2] copy the elements 0 and 1 in positions 2 and 3
                p[[3,4]]=p[0]
        """
        if isinstance(s,(script,scriptobject,scriptobjectgroup)):
            dup = pipescript(s)
        elif isinstance(s,pipescript):
            dup = s
        elif s==[]:
            dup = []
        else:
            raise ValueError("the value must be a pipescript, script, scriptobject, scriptobjectgroup")
        if len(s)<1: # remove (delete)
            if isinstance(idx,slice) or idx<len(self):
                del self.listscript[idx]
                del self.listUSER[idx]
                del self.name[idx]
                del self.executed[idx]
            else:
                raise IndexError("the index must be a slice or an integer")
        elif len(s)==1: # scalar
            if isinstance(idx,int):
                if idx<len(self):
                    self.listscript[idx] = dup.listscript[0]
                    self.listUSER[idx] = dup.listUSER[0]
                    self.name[idx] = dup.name[0]
                    self.executed[idx] = False
                elif idx==len(self):
                    self.listscript.append(dup.listscript[0])
                    self.listUSER.append(dup.listUSER[0])
                    self.name.append(dup.name[0])
                    self.executed.append(False)
                else:
                    raise IndexError(f"the index must be ranged between 0 and {self.n}")
            elif isinstance(idx,list):
                for i in range(len(idx)):
                    self.__setitem__(idx[i], s) # call as a scalar
            elif isinstance(idx,slice):
                for i in range(*idx.indices(len(self)+1)):
                    self.__setitem__(i, s)
            else:
                raise IndexError("unrocognized index value, the index should be an integer or a slice")
        else: # many values
            if isinstance(idx,list): # list call à la Matlab
                if len(idx)==len(s):
                    for i in range(len(s)):
                        self.__setitem__(idx[i], s[i]) # call as a scalar
                else:
                    raise IndexError(f"the number of indices {len(list)} does not match the number of values {len(s)}")
            elif isinstance(idx,slice):
                ilist = list(range(*idx.indices(len(self)+len(s))))
                self.__setitem__(ilist, s) # call as a list
            else:
                raise IndexError("unrocognized index value, the index should be an integer or a slice")

    def rename(self,name="",idx=None):
        """
            rename scripts in the pipe
                p.rename(idx=[0,2,3],name=["A","B","C"])
        """
        if isinstance(name,list):
            if len(name)==len(self) and idx==None:
                self.name = name
            elif len(name) == len(idx):
                for i in range(len(idx)):
                    self.rename(name[i],idx[i])
            else:
                IndexError(f"the number of indices {len(idx)} does not match the number of names {len(name)}")
        elif idx !=None and idx<len(self) and name!="":
            self.name[idx] = name
        else:
            raise ValueError("provide a non empty name and valid index")

    def clear(self,idx=None):
        if len(self)>0:
            if idx==None:
                for i in range(len(self)):
                    self.clear(i)
            else:
                if isinstance(idx,(range,list)):
                    for i in idx:
                        self.clear(idx=i)
                elif isinstance(idx,int) and idx<len(self):
                    self.executed[idx] = False
                else:
                    raise IndexError(f"the index should be ranged between 0 and {self.n-1}")
            if not self.executed[0]:
                self.globalscript = None
                self.cmd = ""



    def do(self, idx=None, printflag=True, verbosity=2, verbose=None, forced=False):
        """
        Execute the pipeline or a part of the pipeline and generate the LAMMPS script.

        Parameters:
            idx (list, range, or int, optional): Specifies which steps of the pipeline to execute.
            printflag (bool, optional): Whether to print the script for each step. Default is True.
            verbosity (int, optional): Level of verbosity for the output.
            verbose (bool, optional): Override for verbosity. If False, sets verbosity to 0.
            forced (bool, optional): If True, forces the pipeline to regenerate all scripts.

        Returns:
            str: Combined LAMMPS script for the specified pipeline steps.

            Execute the pipeline or a part of the pipeline and generate the LAMMPS script.

            This method processes the pipeline of script objects, executing each step to generate
            a combined LAMMPS-compatible script. The execution can be done for the entire pipeline
            or for a specified range of indices. The generated script can include comments and
            metadata based on the verbosity level.


        Method Workflow:
            - The method first checks if there are any script objects in the pipeline.
              If the pipeline is empty, it returns a message indicating that there is nothing to execute.
            - It determines the start and stop indices for the range of steps to execute.
              If idx is not provided, it defaults to executing all steps from the last executed position.
            - If a specific index or list of indices is provided, it executes only those steps.
            - The pipeline steps are executed in order, combining the scripts using the
              >> operator for sequential execution.
            - The generated script includes comments indicating the current run step and pipeline range,
              based on the specified verbosity level.
            - The final combined script is returned as a string.

        Example Usage:
        --------------
            >>> p = pipescript()
            >>> # Execute the entire pipeline
            >>> full_script = p.do()
            >>> # Execute steps 0 and 2 only
            >>> partial_script = p.do([0, 2])
            >>> # Execute step 1 with minimal verbosity
            >>> minimal_script = p.do(idx=1, verbosity=0)

            Notes:
            - The method uses modular arithmetic to handle index wrapping, allowing
              for cyclic execution of pipeline steps.
            - If the pipeline is empty, the method returns the string "# empty pipe - nothing to do".
            - The globalscript is initialized or updated with each step's script,
              and the USER definitions are accumulated across the steps.
            - The command string self.cmd is updated with the generated script for
              each step in the specified range.

            Raises:
            - None: The method does not raise exceptions directly, but an empty pipeline will
                    result in the return of "# empty pipe - nothing to do".
        """
        verbosity = 0 if verbose is False else verbosity
        if len(self) == 0:
            return "# empty pipe - nothing to do"

        # Check if not all steps are executed or if there are gaps
        not_all_executed = not all(self.executed[:self.nrun])  # Check up to the last executed step

        # Determine pipeline range
        total_steps = len(self)
        if self.globalscript is None or forced or not_all_executed:
            start = 0
            self.cmd = ""
        else:
            start = self.nrun
            self.cmd = self.cmd.rstrip("\n") + "\n\n"

        if idx is None:
            idx = range(start, total_steps)
        if isinstance(idx, int):
            idx = [idx]
        if isinstance(idx, range):
            idx = list(idx)

        idx = [i % total_steps for i in idx]
        start, stop = min(idx), max(idx)

        # Prevent re-executing already completed steps
        if not forced:
            idx = [step for step in idx if not self.executed[step]]

        # Execute pipeline steps
        for step in idx:
            step_wrapped = step % total_steps

            # Combine scripts
            if step_wrapped == 0:
                self.globalscript = self.listscript[step_wrapped]
            else:
                self.globalscript = self.globalscript >> self.listscript[step_wrapped]

            # Step label
            step_name = f"<{self.name[step]}>"
            step_label = f"# [{step+1} of {total_steps} from {start}:{stop}] {step_name}"

            # Get script content for the step
            step_output = self.globalscript.do(printflag=printflag, verbose=verbosity > 1)

            # Add comments and content
            if step_output.strip():
                self.cmd += f"{step_label}\n{step_output.strip()}\n\n"
            elif verbosity > 0:
                self.cmd += f"{step_label} :: no content\n\n"

            # Update USER definitions
            self.globalscript.USER += self.listUSER[step]
            self.executed[step] = True

        # Clean up and finalize script
        self.cmd = self.cmd.replace("\\n", "\n").strip()  # Remove literal \\n and extra spaces
        self.cmd += "\n"  # Ensure trailing newline
        return remove_comments(self.cmd) if verbosity == 0 else self.cmd


    def do_legacy(self, idx=None, printflag=True, verbosity=2, verbose=None, forced=False):
        """
        Execute the pipeline or a part of the pipeline and generate the LAMMPS script.

        This method processes the pipeline of script objects, executing each step to generate
        a combined LAMMPS-compatible script. The execution can be done for the entire pipeline
        or for a specified range of indices. The generated script can include comments and
        metadata based on the verbosity level.

        Parameters:
        - idx (list, range, or int, optional): Specifies which steps of the pipeline to execute.
                                               If None, all steps from the current position to
                                               the end are executed. A list of indices can be
                                               provided to execute specific steps, or a single
                                               integer can be passed to execute a specific step.
                                               Default is None.
        - printflag (bool, optional): If True, the generated script for each step is printed
                                      to the console. Default is True.
        - verbosity (int, optional): Controls the level of detail in the generated script.
                                     - 0: Minimal output, no comments.
                                     - 1: Basic comments for run steps.
                                     - 2: Detailed comments with additional information.
                                     Default is 2.
        - forced (bool, optional): If True, all scripts are regenerated

        Returns:
        - str: The combined LAMMPS script generated from the specified steps of the pipeline.

        Method Workflow:
        - The method first checks if there are any script objects in the pipeline.
          If the pipeline is empty, it returns a message indicating that there is nothing to execute.
        - It determines the start and stop indices for the range of steps to execute.
          If idx is not provided, it defaults to executing all steps from the last executed position.
        - If a specific index or list of indices is provided, it executes only those steps.
        - The pipeline steps are executed in order, combining the scripts using the
          >> operator for sequential execution.
        - The generated script includes comments indicating the current run step and pipeline range,
          based on the specified verbosity level.
        - The final combined script is returned as a string.

        Example Usage:
        --------------
        >>> p = pipescript()
        >>> # Execute the entire pipeline
        >>> full_script = p.do()
        >>> # Execute steps 0 and 2 only
        >>> partial_script = p.do([0, 2])
        >>> # Execute step 1 with minimal verbosity
        >>> minimal_script = p.do(idx=1, verbosity=0)

        Notes:
        - The method uses modular arithmetic to handle index wrapping, allowing
          for cyclic execution of pipeline steps.
        - If the pipeline is empty, the method returns the string "# empty pipe - nothing to do".
        - The globalscript is initialized or updated with each step's script,
          and the USER definitions are accumulated across the steps.
        - The command string self.cmd is updated with the generated script for
          each step in the specified range.

        Raises:
        - None: The method does not raise exceptions directly, but an empty pipeline will
                result in the return of "# empty pipe - nothing to do".
        """

        verbosity = 0 if verbose is False else verbosity
        if len(self)>0:
            # ranges
            ntot = len(self)
            stop = ntot-1
            if (self.globalscript == None) or (self.globalscript == []) or not self.executed[0] or forced:
                start = 0
                self.cmd = ""
            else:
                start = self.nrun
            if start>stop: return self.cmd
            if idx is None: idx = range(start,stop+1)
            if isinstance(idx,range): idx = list(idx)
            if isinstance(idx,int): idx = [idx]
            start,stop = min(idx),max(idx)
            # do
            for i in idx:
                j = i % ntot
                if j==0:
                    self.globalscript = self.listscript[j]
                else:
                    self.globalscript = self.globalscript >> self.listscript[j]
                name = "  "+self.name[i]+"  "
                if verbosity>0:
                    self.cmd += "\n\n#\t --- run step [%d/%d] --- [%s]  %20s\n" % \
                            (j,ntot-1,name.center(50,"="),"pipeline [%d]-->[%d]" %(start,stop))
                else:
                    self.cmd +="\n"
                self.globalscript.USER = self.globalscript.USER + self.listUSER[j]
                self.cmd += self.globalscript.do(printflag=printflag,verbose=verbosity>1)
                self.executed[i] = True
            self.cmd = self.cmd.replace("\\n", "\n") # remove literal \\n if any (dscript.save add \\n)
            return remove_comments(self.cmd) if verbosity==0 else self.cmd
        else:
            return "# empty pipe - nothing to do"


    def script(self,idx=None, printflag=True, verbosity=2, verbose=None, forced=False, style=4):
        """
            script the pipeline or parts of the pipeline
                s = p.script()
                s = p.script([0,2])

        Parameters:
        - idx (list, range, or int, optional): Specifies which steps of the pipeline to execute.
                                               If None, all steps from the current position to
                                               the end are executed. A list of indices can be
                                               provided to execute specific steps, or a single
                                               integer can be passed to execute a specific step.
                                               Default is None.
        - printflag (bool, optional): If True, the generated script for each step is printed
                                      to the console. Default is True.
        - verbosity (int, optional): Controls the level of detail in the generated script.
                                     - 0: Minimal output, no comments.
                                     - 1: Basic comments for run steps.
                                     - 2: Detailed comments with additional information.
                                     Default is 2.
        - forced (bool, optional): If True, all scripts are regenerated
        - style (int, optional):
            Defines the ASCII frame style for the header.
            Valid values are integers from 1 to 6, corresponding to predefined styles:
                1. Basic box with `+`, `-`, and `|`
                2. Double-line frame with `╔`, `═`, and `║`
                3. Rounded corners with `.`, `'`, `-`, and `|`
                4. Thick outer frame with `#`, `=`, and `#`
                5. Box drawing characters with `┌`, `─`, and `│`
                6. Minimalist dotted frame with `.`, `:`, and `.`
            Default is `4` (thick outer frame).

        """
        printflag = self.printflag if printflag is None else printflag
        verbose = verbosity > 0 if verbosity is not None else (self.verbose if verbose is None else verbose)
        verbosity=0 if verbose is False else verbosity
        s = script(printflag=printflag, verbose=verbosity>0)
        s.name = "pipescript"
        s.description = "pipeline with %d scripts" % len(self)
        if len(self)>1:
            s.userid = self.name[0]+"->"+self.name[-1]
        elif len(self)==1:
            s.userid = self.name[0]
        else:
            s.userid = "empty pipeline"
        s.TEMPLATE = self.header(verbosity=verbosity, style=style) + "\n" +\
            self.do(idx, printflag=printflag, verbosity=verbosity, verbose=verbose, forced=forced)
        s.DEFINITIONS = duplicate(self.globalscript.DEFINITIONS)
        s.USER = duplicate(self.globalscript.USER)
        return s

    @staticmethod
    def join(liste):
        """
            join a combination scripts and pipescripts within a pipescript
                p = pipescript.join([s1,s2,p3,p4,p5...])
        """
        if not isinstance(liste,list):
            raise ValueError("the argument should be a list")
        ok = True
        for i in range(len(liste)):
            ok = ok and isinstance(liste[i],(script,pipescript))
            if not ok:
                raise ValueError(f"the entry [{i}] should be a script or pipescript")
        if len(liste)<1:
            return liste
        out = liste[0]
        for i in range(1,len(liste)):
            out = out | liste[i]
        return out

    # Note that it was not the original intent to copy pipescripts
    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, deepduplicate(v, memo))
        return copie

    # write file
    def write(self, file, printflag=False, verbosity=2, verbose=None, overwrite=False):
       """
       Write the combined script to a file.

       Parameters:
           file (str): The file path where the script will be saved.
           printflag (bool): Flag to enable/disable printing of details.
           verbosity (int): Level of verbosity for the script generation.
           verbose (bool or None): If True, enables verbose mode; if None, defaults to the instance's verbosity.
           overwrite (bool): Whether to overwrite the file if it already exists. Default is False.

        Returns:
            str: The full absolute path of the file written.

       Raises:
           FileExistsError: If the file already exists and overwrite is False.

       Notes:
           - This method combines the individual scripts within the `pipescript` object
             and saves the resulting script to the specified file.
           - If `overwrite` is False and the file exists, an error is raised.
           - If `verbose` is True and the file is overwritten, a warning is displayed.
       """
       # Generate the combined script
       myscript = self.script(printflag=printflag, verbosity=verbosity, verbose=verbose, forced=True)
       # Call the script's write method with the overwrite parameter
       return myscript.write(file, printflag=printflag, verbose=verbose, overwrite=overwrite)


    def dscript(self, name=None, printflag=None, verbose=None, verbosity=None, clean="fixing", **USER):
        """
        Convert the current pipescript object to a dscript object.

        This method merges the STATIC, GLOBAL, and LOCAL variable spaces from each step
        in the pipescript into a single dynamic script per step in the dscript.
        Each step in the pipescript is transformed into a dynamic script in the dscript,
        where variable spaces are combined using the following order:

        1. STATIC: Definitions specific to each script in the pipescript.
        2. GLOBAL: User variables shared across steps from a specific point onwards.
        3. LOCAL: User variables for each individual step.

        Parameters:
        -----------
        verbose : bool, optional
            Controls verbosity of the dynamic scripts in the resulting dscript object.
            If None, the verbosity setting of the pipescript will be used.

        clean : "fixing" or "removing"
            - 'removing': Completely remove the empty step from TEMPLATE.
            - 'fixing': Replace the content of the empty step with a comment.

        **USER : scriptobjectdata(), optional
            Additional user-defined variables that can override existing static variables
            in the dscript object or be added to it.

        Returns:
        --------
        outd : dscript
            A dscript object that contains all steps of the pipescript as dynamic scripts.
            Each step from the pipescript is added as a dynamic script with the same content
            and combined variable spaces.
        """
        # Local imports
        from pizza.dscript import dscript, ScriptTemplate, lambdaScriptdata

        # verbosity
        printflag = self.printflag if printflag is None else printflag
        verbose = verbosity > 0 if verbosity is not None else (self.verbose if verbose is None else verbose)
        verbosity = 0 if not verbose else verbosity

        # Adjust name
        if name is None:
            if isinstance(self.name, str):
                name = self.name
            elif isinstance(self.name, list):
                name = (
                    self.name[0] if len(self.name) == 1 else self.name[0] + "..." + self.name[-1]
                )

        # Create the dscript container with the pipescript name as the userid
        outd = dscript(userid=name, verbose=self.verbose, **USER)

        # Initialize static merged definitions
        staticmerged_definitions = lambdaScriptdata()

        # Track used variables per step
        step_used_variables = []

        # Loop over each step in the pipescript
        for i, script in enumerate(self.listscript):
            # Merge STATIC, GLOBAL, and LOCAL variables for the current step
            static_vars = self.listUSER[i] # script.DEFINITIONS
            global_vars = script.DEFINITIONS # self.scripts[i].USER
            local_vars = script.USER # self.USER[i]
            refreshed_globalvars = static_vars + global_vars

            # Detect variables used in the current template
            used_variables = set(script.detect_variables())
            step_used_variables.append(used_variables)  # Track used variables for this step

            # Copy all current variables to local_static_updates and remove matching variables from staticmerged_definitions
            local_static_updates = lambdaScriptdata(**local_vars)

            for var, value in refreshed_globalvars.items():
                if var in staticmerged_definitions:
                    if (getattr(staticmerged_definitions, var) != value) and (var not in local_vars):
                        setattr(local_static_updates, var, value)
                else:
                    setattr(staticmerged_definitions, var, value)

           # Create the dynamic script for this step using the method in dscript
            key_name = i  # Use the index 'i' as the key in TEMPLATE
            content = script.TEMPLATE

            # Use the helper method in dscript to add this dynamic script
            outd.add_dynamic_script(
                key=key_name,
                content=content,
                definitions = lambdaScriptdata(**local_static_updates),
                verbose=self.verbose if verbose is None else verbose,
                userid=self.name[i],
                autorefresh=False # prevent the replacement by default values ${}
            )

            # Set eval=True only if variables are detected in the template
            if outd.TEMPLATE[key_name].detect_variables():
                outd.TEMPLATE[key_name].eval = True

        # Compute the union of all used variables across all steps
        global_used_variables = set().union(*step_used_variables)

        # Filter staticmerged_definitions to keep only variables that are used
        filtered_definitions = {
            var: value for var, value in staticmerged_definitions.items() if var in global_used_variables
        }

        # Assign the filtered definitions along with USER variables to outd.DEFINITIONS
        outd.DEFINITIONS = lambdaScriptdata(**filtered_definitions)

        # Clean the entries for empty templates
        outd.clean(verbose=verbose,behavior=clean)

        return outd



    def header(self, verbose=True,verbosity=None, style=4):
        """
        Generate a formatted header for the pipescript file.

        Parameters:
            verbosity (bool, optional): If specified, overrides the instance's `verbose` setting.
                                        Defaults to the instance's `verbose`.
            style (int from 1 to 6, optional): ASCII style to frame the header (default=4)

        Returns:
            str: A formatted string representing the pipescript object.
                 Returns an empty string if verbosity is False.

        The header includes:
            - Total number of scripts in the pipeline.
            - The verbosity setting.
            - The range of scripts from the first to the last script.
            - All enclosed within an ASCII frame that adjusts to the content.
        """
        verbose = verbosity > 0 if verbosity is not None else (self.verbose if verbose is None else verbose)
        verbosity = 0 if not verbose else verbosity
        if not verbosity:
            return ""

        # Prepare the header content
        lines = [
            f"PIPESCRIPT with {self.n} scripts | Verbosity: {verbosity}",
            "",
            f"From: <{str(self.scripts[0])}> To: <{str(self.scripts[-1])}>",
        ]

        # Use the shared method to format the header
        return frame_header(lines,style=style)



    def list_values(self, varname=None, what="all"):
        """
        Lists all occurrences and values of a specified variable or all variables across the pipeline scripts.

        Parameters:
        - varname (str, optional): The name of the variable to search for. If None, all variables are listed.
        - what (str or list/tuple, optional): Specifies the scopes to search in.
                                             Can be "static", "global", "local", "all",
                                             or a list/tuple of any combination of these.

        Returns:
        - VariableOccurrences: If varname is specified, returns a VariableOccurrences instance for that variable.
        - dict of VariableOccurrences: If varname is None, returns a dictionary mapping each variable name to its VariableOccurrences instance.
        """
        # Normalize 'what' to a list for uniform processing
        if isinstance(what, str):
            if what.lower() == "all":
                scopes = ["static", "global", "local"]
            else:
                scopes = [what.lower()]
        elif isinstance(what, (list, tuple)):
            scopes_lower = [s.lower() for s in what]
            if 'all' in scopes_lower:
                scopes = ["static", "global", "local"]
            else:
                scopes = scopes_lower
        else:
            raise ValueError("Parameter 'what' must be a string or a list/tuple of strings.")

        # Initialize data structures
        if varname:
            # Single variable case
            if len(scopes) == 1:
                data = []
            else:
                data = {}
                for scope in scopes:
                    data[scope] = []

            # Iterate over each script in the pipeline
            for i, script in enumerate(self.listscript):
                # Retrieve variables for each scope
                static_vars = self.listUSER[i] if i < len(self.listUSER) else {}
                global_vars = getattr(script, 'DEFINITIONS', {})
                local_vars = getattr(script, 'USER', {})

                scope_vars = {
                    "static": static_vars,
                    "global": global_vars,
                    "local": local_vars
                }

                # Check each requested scope
                for scope in scopes:
                    vars_dict = scope_vars.get(scope, {})
                    if varname in vars_dict.keys():
                        value = getattr(vars_dict,varname)
                        if len(scopes) == 1:
                            data.append((i, value))
                        else:
                            data[scope].append((i, value))

            # Return a VariableOccurrences instance for the specified variable
            return VariableOccurrences(data, variables=varname)

        else:
            # All variables case
            all_vars = set()

            # First, collect all variable names across specified scopes and scripts
            for i, script in enumerate(self.listscript):
                # Retrieve variables for each scope
                static_vars = self.listUSER[i] if i < len(self.listUSER) else {}
                global_vars = getattr(script, 'DEFINITIONS', {})
                local_vars = getattr(script, 'USER', {})

                scope_vars = {
                    "static": static_vars,
                    "global": global_vars,
                    "local": local_vars
                }

                for scope in scopes:
                    vars_dict = scope_vars.get(scope, {})
                    all_vars.update(vars_dict.keys())

            # Initialize a dictionary to hold VariableOccurrences for each variable
            variables_data = {}
            for var in all_vars:
                var_data = {}
                for scope in scopes:
                    var_data[scope] = []
                variables_data[var] = var_data

            # Iterate again to populate the data for each variable
            for i, script in enumerate(self.listscript):
                # Retrieve variables for each scope
                static_vars = self.listUSER[i] if i < len(self.listUSER) else {}
                global_vars = getattr(script, 'DEFINITIONS', {})
                local_vars = getattr(script, 'USER', {})

                scope_vars = {
                    "static": static_vars,
                    "global": global_vars,
                    "local": local_vars
                }

                for scope in scopes:
                    vars_dict = scope_vars.get(scope, {})
                    for var, value in vars_dict.items():
                        variables_data[var][scope].append((i, value))

            # Convert each variable's data into a VariableOccurrences instance
            variables_occurrences = {}
            for var, data in variables_data.items():
                variables_occurrences[var] = VariableOccurrences(data, variables=var)

            return variables_occurrences



    def list_multiple_values(self, varnames, what="all"):
        """
        Lists all occurrences and values of multiple variables across the pipeline scripts.

        Parameters:
        - varnames (list): A list of variable names to search for.
        - what (str or list/tuple): Specifies the scopes to search in.
                                     Can be "static", "global", "local", "all",
                                     or a list/tuple of any combination of these.

        Returns:
        - dict: A dictionary mapping each variable name to its VariableOccurrences object.
        """
        if not isinstance(varnames, (list, tuple)):
            raise ValueError("Parameter 'varnames' must be a list or tuple of strings.")

        return self.list_values(varname=varnames, what=what)



    def plot_multiple_value_distributions(self, varnames, what="all", separate_plots=True):
        """
        Plots the distribution of elements for multiple variables across specified scopes.

        Parameters:
        - varnames (list): A list of variable names to plot.
        - what (str or list/tuple): Specifies the scopes to include in the plot.
                                     Can be "static", "global", "local", "all",
                                     or a list/tuple of any combination of these.
        - separate_plots (bool): If True, plots each variable in a separate subplot.
                                 If False, combines all variables in a single plot for comparison.
        """
        if not isinstance(varnames, (list, tuple)):
            raise ValueError("Parameter 'varnames' must be a list or tuple of strings.")

        # Retrieve VariableOccurrences instances
        multiple_vars = self.list_multiple_values(varnames, what=what)

        if separate_plots:
            num_vars = len(multiple_vars)
            fig, axes = plt.subplots(num_vars, 1, figsize=(10, 5 * num_vars))
            if num_vars == 1:
                axes = [axes]  # Make it iterable

            for ax, (var, vo) in zip(axes, multiple_vars.items()):
                summary = vo.summarize()[var]
                for scope, details in summary.items():
                    elements = list(details.get('unique_elements_in_lists', []))
                    counts = [details['element_counts_in_lists'][elem] for elem in elements]
                    ax.bar(elements, counts, label=scope)
                ax.set_xlabel('Element')
                ax.set_ylabel('Count')
                ax.set_title(f"Distribution of elements in '{var}'")
                ax.legend()

            plt.tight_layout()
            plt.show()

        else:
            plt.figure(figsize=(12, 8))
            for var, vo in multiple_vars.items():
                summary = vo.summarize()[var]
                for scope, details in summary.items():
                    elements = list(details.get('unique_elements_in_lists', []))
                    counts = [details['element_counts_in_lists'][elem] for elem in elements]
                    plt.bar([f"{var}_{elem}" for elem in elements], counts, label=f"{var} - {scope}")

            plt.xlabel('Element')
            plt.ylabel('Count')
            plt.title("Distribution of elements in multiple variables")
            plt.legend()
            plt.xticks(rotation=45, ha='right')
            plt.tight_layout()
            plt.show()


    def generate_report(self, filename, varnames=None, scopes="all"):
        """
        Generates a comprehensive report for specified variables and writes it to a file.

        Parameters:
        - filename (str): Path to the output report file. Must end with .md, .txt, or .html.
        - varnames (str or list/tuple, optional): Variable name(s) to include in the report. Defaults to 'all'.
        - scopes (str or list/tuple, optional): 'all', a single scope string, or a list of scope strings. Defaults to 'all'.

        Raises:
        - ValueError: If 'filename' has an unsupported extension.
        - Exception: For other unforeseen errors.
        """
        # Validate filename extension
        _, ext = os.path.splitext(filename)
        ext = ext.lower()

        if ext not in ['.md', '.txt', '.html']:
            raise ValueError("Unsupported file extension. Supported extensions are .md, .txt, and .html.")

        # Determine format based on extension
        if ext in ['.md', '.txt']:
            export_format = 'markdown'
        elif ext == '.html':
            export_format = 'html'

        # Determine variables to include
        if varnames is None or (isinstance(varnames, (list, tuple)) and len(varnames) == 0):
            variables = 'all'
        else:
            variables = varnames  # Can be a string or a list/tuple

        # Retrieve VariableOccurrences instances
        if variables == 'all':
            variables_occurrences = self.list_values(varname=None, what=scopes)
        else:
            # Normalize varnames to a list
            if isinstance(variables, str):
                variables = [variables]
            elif isinstance(variables, (list, tuple)):
                variables = list(variables)
            else:
                raise ValueError("Parameter 'varnames' must be a string or a list/tuple of strings.")

            variables_occurrences = {}
            for var in variables:
                vo = self.list_values(varname=var, what=scopes)
                if vo and var in vo.variables:
                    variables_occurrences[var] = vo
                else:
                    print(f"Warning: Variable '{var}' not found in the specified scopes.")

        # Initialize report content
        report_content = ""
        timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        caller = "generate_report"

        # Add report header
        if export_format == 'markdown':
            report_content += f"# Comprehensive Variable Report\n\n"
            report_content += f"**Generated on {timestamp} by {caller}**\n\n"
        elif export_format == 'html':
            # Define CSS for HTML
            css = """
            <style>
                body {
                    font-family: Arial, sans-serif;
                    margin: 20px;
                }
                h1, h2, h3, h4, h5 {
                    color: #333;
                }
                table {
                    border-collapse: collapse;
                    width: 100%;
                    margin-bottom: 40px;
                }
                th, td {
                    border: 1px solid #ddd;
                    padding: 8px;
                    text-align: left;
                }
                th {
                    background-color: #4CAF50;
                    color: white;
                }
                tr:nth-child(even){background-color: #f2f2f2;}
                tr:hover {background-color: #ddd;}
            </style>
            """
            report_content += f"<!DOCTYPE html>\n<html>\n<head>\n<meta charset='UTF-8'>\n<title>Comprehensive Variable Report</title>\n{css}\n</head>\n<body>\n"
            report_content += f"<h1>Comprehensive Variable Report</h1>\n"
            report_content += f"<h2>Generated on {timestamp} by {caller}</h2>\n"

        # Assemble report content using VariableOccurrences.export()
        for var, vo in variables_occurrences.items():
            # Export content without headers and get as string
            var_content = vo.export(filename=filename, # use to identify the format based on its extension
                                    scopes=scopes,
                                    variables=var,
                                    include_headers=False,
                                    return_content=True)

            if export_format == 'markdown':
                # Add variable header
                report_content += f"## Variable: `{var}`\n\n"
                report_content += var_content + "\n\n"
                report_content += "---\n\n"  # Horizontal line between variables
            elif export_format == 'html':
                # Add variable header
                report_content += f"<h2>Variable: {var}</h2>\n"
                report_content += var_content + "\n<hr/>\n"  # Horizontal line between variables

        # Finalize HTML content
        if export_format == 'html':
            report_content += "</body>\n</html>"

        # Write report to file
        try:
            with open(filename, 'w', encoding='utf-8') as file:
                file.write(report_content)
            print(f"Report successfully generated at '{filename}'.")
        except Exception as e:
            raise Exception(f"Failed to write the report to '{filename}': {e}")

Static methods

def join(liste)

join a combination scripts and pipescripts within a pipescript p = pipescript.join([s1,s2,p3,p4,p5…])

Expand source code
@staticmethod
def join(liste):
    """
        join a combination scripts and pipescripts within a pipescript
            p = pipescript.join([s1,s2,p3,p4,p5...])
    """
    if not isinstance(liste,list):
        raise ValueError("the argument should be a list")
    ok = True
    for i in range(len(liste)):
        ok = ok and isinstance(liste[i],(script,pipescript))
        if not ok:
            raise ValueError(f"the entry [{i}] should be a script or pipescript")
    if len(liste)<1:
        return liste
    out = liste[0]
    for i in range(1,len(liste)):
        out = out | liste[i]
    return out

Instance variables

var USER

p.USER[idx].var returns the value of the USER variable var p.USER[idx].var = val assigns the value val to the USER variable var

Expand source code
@property
def USER(self):
    """
        p.USER[idx].var returns the value of the USER variable var
        p.USER[idx].var = val assigns the value val to the USER variable var
    """
    return self.listUSER  # override listuser
var n

number of scripts

Expand source code
@property
def n(self):
    """ number of scripts """
    return len(self)
var nrun

number of scripts executed continuously from origin

Expand source code
@property
def nrun(self):
    """ number of scripts executed continuously from origin """
    n, nmax  = 0, len(self)
    while n<nmax and self.executed[n]: n+=1
    return n
var scripts

p.scripts[idx].USER.var returns the value of the USER variable var p.scripts[idx].USER.var = val assigns the value val to the USER variable var

Expand source code
@property
def scripts(self):
    """
        p.scripts[idx].USER.var returns the value of the USER variable var
        p.scripts[idx].USER.var = val assigns the value val to the USER variable var
    """
    return self.listscript # override listuser

Methods

def clear(self, idx=None)
Expand source code
def clear(self,idx=None):
    if len(self)>0:
        if idx==None:
            for i in range(len(self)):
                self.clear(i)
        else:
            if isinstance(idx,(range,list)):
                for i in idx:
                    self.clear(idx=i)
            elif isinstance(idx,int) and idx<len(self):
                self.executed[idx] = False
            else:
                raise IndexError(f"the index should be ranged between 0 and {self.n-1}")
        if not self.executed[0]:
            self.globalscript = None
            self.cmd = ""
def do(self, idx=None, printflag=True, verbosity=2, verbose=None, forced=False)

Execute the pipeline or a part of the pipeline and generate the LAMMPS script.

Parameters

idx (list, range, or int, optional): Specifies which steps of the pipeline to execute. printflag (bool, optional): Whether to print the script for each step. Default is True. verbosity (int, optional): Level of verbosity for the output. verbose (bool, optional): Override for verbosity. If False, sets verbosity to 0. forced (bool, optional): If True, forces the pipeline to regenerate all scripts.

Returns

str
Combined LAMMPS script for the specified pipeline steps.

Execute the pipeline or a part of the pipeline and generate the LAMMPS script.

This method processes the pipeline of script objects, executing each step to generate a combined LAMMPS-compatible script. The execution can be done for the entire pipeline or for a specified range of indices. The generated script can include comments and metadata based on the verbosity level. Method Workflow: - The method first checks if there are any script objects in the pipeline. If the pipeline is empty, it returns a message indicating that there is nothing to execute. - It determines the start and stop indices for the range of steps to execute. If idx is not provided, it defaults to executing all steps from the last executed position. - If a specific index or list of indices is provided, it executes only those steps. - The pipeline steps are executed in order, combining the scripts using the >> operator for sequential execution. - The generated script includes comments indicating the current run step and pipeline range, based on the specified verbosity level. - The final combined script is returned as a string.

Example Usage:

>>> p = pipescript()
>>> # Execute the entire pipeline
>>> full_script = p.do()
>>> # Execute steps 0 and 2 only
>>> partial_script = p.do([0, 2])
>>> # Execute step 1 with minimal verbosity
>>> minimal_script = p.do(idx=1, verbosity=0)

Notes:
- The method uses modular arithmetic to handle index wrapping, allowing
  for cyclic execution of pipeline steps.
- If the pipeline is empty, the method returns the string "# empty pipe - nothing to do".
- The globalscript is initialized or updated with each step's script,
  and the USER definitions are accumulated across the steps.
- The command string self.cmd is updated with the generated script for
  each step in the specified range.

Raises:
- None: The method does not raise exceptions directly, but an empty pipeline will
        result in the return of "# empty pipe - nothing to do".
Expand source code
def do(self, idx=None, printflag=True, verbosity=2, verbose=None, forced=False):
    """
    Execute the pipeline or a part of the pipeline and generate the LAMMPS script.

    Parameters:
        idx (list, range, or int, optional): Specifies which steps of the pipeline to execute.
        printflag (bool, optional): Whether to print the script for each step. Default is True.
        verbosity (int, optional): Level of verbosity for the output.
        verbose (bool, optional): Override for verbosity. If False, sets verbosity to 0.
        forced (bool, optional): If True, forces the pipeline to regenerate all scripts.

    Returns:
        str: Combined LAMMPS script for the specified pipeline steps.

        Execute the pipeline or a part of the pipeline and generate the LAMMPS script.

        This method processes the pipeline of script objects, executing each step to generate
        a combined LAMMPS-compatible script. The execution can be done for the entire pipeline
        or for a specified range of indices. The generated script can include comments and
        metadata based on the verbosity level.


    Method Workflow:
        - The method first checks if there are any script objects in the pipeline.
          If the pipeline is empty, it returns a message indicating that there is nothing to execute.
        - It determines the start and stop indices for the range of steps to execute.
          If idx is not provided, it defaults to executing all steps from the last executed position.
        - If a specific index or list of indices is provided, it executes only those steps.
        - The pipeline steps are executed in order, combining the scripts using the
          >> operator for sequential execution.
        - The generated script includes comments indicating the current run step and pipeline range,
          based on the specified verbosity level.
        - The final combined script is returned as a string.

    Example Usage:
    --------------
        >>> p = pipescript()
        >>> # Execute the entire pipeline
        >>> full_script = p.do()
        >>> # Execute steps 0 and 2 only
        >>> partial_script = p.do([0, 2])
        >>> # Execute step 1 with minimal verbosity
        >>> minimal_script = p.do(idx=1, verbosity=0)

        Notes:
        - The method uses modular arithmetic to handle index wrapping, allowing
          for cyclic execution of pipeline steps.
        - If the pipeline is empty, the method returns the string "# empty pipe - nothing to do".
        - The globalscript is initialized or updated with each step's script,
          and the USER definitions are accumulated across the steps.
        - The command string self.cmd is updated with the generated script for
          each step in the specified range.

        Raises:
        - None: The method does not raise exceptions directly, but an empty pipeline will
                result in the return of "# empty pipe - nothing to do".
    """
    verbosity = 0 if verbose is False else verbosity
    if len(self) == 0:
        return "# empty pipe - nothing to do"

    # Check if not all steps are executed or if there are gaps
    not_all_executed = not all(self.executed[:self.nrun])  # Check up to the last executed step

    # Determine pipeline range
    total_steps = len(self)
    if self.globalscript is None or forced or not_all_executed:
        start = 0
        self.cmd = ""
    else:
        start = self.nrun
        self.cmd = self.cmd.rstrip("\n") + "\n\n"

    if idx is None:
        idx = range(start, total_steps)
    if isinstance(idx, int):
        idx = [idx]
    if isinstance(idx, range):
        idx = list(idx)

    idx = [i % total_steps for i in idx]
    start, stop = min(idx), max(idx)

    # Prevent re-executing already completed steps
    if not forced:
        idx = [step for step in idx if not self.executed[step]]

    # Execute pipeline steps
    for step in idx:
        step_wrapped = step % total_steps

        # Combine scripts
        if step_wrapped == 0:
            self.globalscript = self.listscript[step_wrapped]
        else:
            self.globalscript = self.globalscript >> self.listscript[step_wrapped]

        # Step label
        step_name = f"<{self.name[step]}>"
        step_label = f"# [{step+1} of {total_steps} from {start}:{stop}] {step_name}"

        # Get script content for the step
        step_output = self.globalscript.do(printflag=printflag, verbose=verbosity > 1)

        # Add comments and content
        if step_output.strip():
            self.cmd += f"{step_label}\n{step_output.strip()}\n\n"
        elif verbosity > 0:
            self.cmd += f"{step_label} :: no content\n\n"

        # Update USER definitions
        self.globalscript.USER += self.listUSER[step]
        self.executed[step] = True

    # Clean up and finalize script
    self.cmd = self.cmd.replace("\\n", "\n").strip()  # Remove literal \\n and extra spaces
    self.cmd += "\n"  # Ensure trailing newline
    return remove_comments(self.cmd) if verbosity == 0 else self.cmd
def do_legacy(self, idx=None, printflag=True, verbosity=2, verbose=None, forced=False)

Execute the pipeline or a part of the pipeline and generate the LAMMPS script.

This method processes the pipeline of script objects, executing each step to generate a combined LAMMPS-compatible script. The execution can be done for the entire pipeline or for a specified range of indices. The generated script can include comments and metadata based on the verbosity level.

Parameters: - idx (list, range, or int, optional): Specifies which steps of the pipeline to execute. If None, all steps from the current position to the end are executed. A list of indices can be provided to execute specific steps, or a single integer can be passed to execute a specific step. Default is None. - printflag (bool, optional): If True, the generated script for each step is printed to the console. Default is True. - verbosity (int, optional): Controls the level of detail in the generated script. - 0: Minimal output, no comments. - 1: Basic comments for run steps. - 2: Detailed comments with additional information. Default is 2. - forced (bool, optional): If True, all scripts are regenerated

Returns: - str: The combined LAMMPS script generated from the specified steps of the pipeline.

Method Workflow: - The method first checks if there are any script objects in the pipeline. If the pipeline is empty, it returns a message indicating that there is nothing to execute. - It determines the start and stop indices for the range of steps to execute. If idx is not provided, it defaults to executing all steps from the last executed position. - If a specific index or list of indices is provided, it executes only those steps. - The pipeline steps are executed in order, combining the scripts using the

operator for sequential execution. - The generated script includes comments indicating the current run step and pipeline range, based on the specified verbosity level. - The final combined script is returned as a string.

Example Usage:

>>> p = pipescript()
>>> # Execute the entire pipeline
>>> full_script = p.do()
>>> # Execute steps 0 and 2 only
>>> partial_script = p.do([0, 2])
>>> # Execute step 1 with minimal verbosity
>>> minimal_script = p.do(idx=1, verbosity=0)

Notes: - The method uses modular arithmetic to handle index wrapping, allowing for cyclic execution of pipeline steps. - If the pipeline is empty, the method returns the string "# empty pipe - nothing to do". - The globalscript is initialized or updated with each step's script, and the USER definitions are accumulated across the steps. - The command string self.cmd is updated with the generated script for each step in the specified range.

Raises: - None: The method does not raise exceptions directly, but an empty pipeline will result in the return of "# empty pipe - nothing to do".

Expand source code
def do_legacy(self, idx=None, printflag=True, verbosity=2, verbose=None, forced=False):
    """
    Execute the pipeline or a part of the pipeline and generate the LAMMPS script.

    This method processes the pipeline of script objects, executing each step to generate
    a combined LAMMPS-compatible script. The execution can be done for the entire pipeline
    or for a specified range of indices. The generated script can include comments and
    metadata based on the verbosity level.

    Parameters:
    - idx (list, range, or int, optional): Specifies which steps of the pipeline to execute.
                                           If None, all steps from the current position to
                                           the end are executed. A list of indices can be
                                           provided to execute specific steps, or a single
                                           integer can be passed to execute a specific step.
                                           Default is None.
    - printflag (bool, optional): If True, the generated script for each step is printed
                                  to the console. Default is True.
    - verbosity (int, optional): Controls the level of detail in the generated script.
                                 - 0: Minimal output, no comments.
                                 - 1: Basic comments for run steps.
                                 - 2: Detailed comments with additional information.
                                 Default is 2.
    - forced (bool, optional): If True, all scripts are regenerated

    Returns:
    - str: The combined LAMMPS script generated from the specified steps of the pipeline.

    Method Workflow:
    - The method first checks if there are any script objects in the pipeline.
      If the pipeline is empty, it returns a message indicating that there is nothing to execute.
    - It determines the start and stop indices for the range of steps to execute.
      If idx is not provided, it defaults to executing all steps from the last executed position.
    - If a specific index or list of indices is provided, it executes only those steps.
    - The pipeline steps are executed in order, combining the scripts using the
      >> operator for sequential execution.
    - The generated script includes comments indicating the current run step and pipeline range,
      based on the specified verbosity level.
    - The final combined script is returned as a string.

    Example Usage:
    --------------
    >>> p = pipescript()
    >>> # Execute the entire pipeline
    >>> full_script = p.do()
    >>> # Execute steps 0 and 2 only
    >>> partial_script = p.do([0, 2])
    >>> # Execute step 1 with minimal verbosity
    >>> minimal_script = p.do(idx=1, verbosity=0)

    Notes:
    - The method uses modular arithmetic to handle index wrapping, allowing
      for cyclic execution of pipeline steps.
    - If the pipeline is empty, the method returns the string "# empty pipe - nothing to do".
    - The globalscript is initialized or updated with each step's script,
      and the USER definitions are accumulated across the steps.
    - The command string self.cmd is updated with the generated script for
      each step in the specified range.

    Raises:
    - None: The method does not raise exceptions directly, but an empty pipeline will
            result in the return of "# empty pipe - nothing to do".
    """

    verbosity = 0 if verbose is False else verbosity
    if len(self)>0:
        # ranges
        ntot = len(self)
        stop = ntot-1
        if (self.globalscript == None) or (self.globalscript == []) or not self.executed[0] or forced:
            start = 0
            self.cmd = ""
        else:
            start = self.nrun
        if start>stop: return self.cmd
        if idx is None: idx = range(start,stop+1)
        if isinstance(idx,range): idx = list(idx)
        if isinstance(idx,int): idx = [idx]
        start,stop = min(idx),max(idx)
        # do
        for i in idx:
            j = i % ntot
            if j==0:
                self.globalscript = self.listscript[j]
            else:
                self.globalscript = self.globalscript >> self.listscript[j]
            name = "  "+self.name[i]+"  "
            if verbosity>0:
                self.cmd += "\n\n#\t --- run step [%d/%d] --- [%s]  %20s\n" % \
                        (j,ntot-1,name.center(50,"="),"pipeline [%d]-->[%d]" %(start,stop))
            else:
                self.cmd +="\n"
            self.globalscript.USER = self.globalscript.USER + self.listUSER[j]
            self.cmd += self.globalscript.do(printflag=printflag,verbose=verbosity>1)
            self.executed[i] = True
        self.cmd = self.cmd.replace("\\n", "\n") # remove literal \\n if any (dscript.save add \\n)
        return remove_comments(self.cmd) if verbosity==0 else self.cmd
    else:
        return "# empty pipe - nothing to do"
def dscript(self, name=None, printflag=None, verbose=None, verbosity=None, clean='fixing', **USER)

Convert the current pipescript object to a dscript object.

This method merges the STATIC, GLOBAL, and LOCAL variable spaces from each step in the pipescript into a single dynamic script per step in the dscript. Each step in the pipescript is transformed into a dynamic script in the dscript, where variable spaces are combined using the following order:

  1. STATIC: Definitions specific to each script in the pipescript.
  2. GLOBAL: User variables shared across steps from a specific point onwards.
  3. LOCAL: User variables for each individual step.

Parameters:

verbose : bool, optional Controls verbosity of the dynamic scripts in the resulting dscript object. If None, the verbosity setting of the pipescript will be used.

clean : "fixing" or "removing" - 'removing': Completely remove the empty step from TEMPLATE. - 'fixing': Replace the content of the empty step with a comment.

**USER : scriptobjectdata(), optional Additional user-defined variables that can override existing static variables in the dscript object or be added to it.

Returns:

outd : dscript A dscript object that contains all steps of the pipescript as dynamic scripts. Each step from the pipescript is added as a dynamic script with the same content and combined variable spaces.

Expand source code
def dscript(self, name=None, printflag=None, verbose=None, verbosity=None, clean="fixing", **USER):
    """
    Convert the current pipescript object to a dscript object.

    This method merges the STATIC, GLOBAL, and LOCAL variable spaces from each step
    in the pipescript into a single dynamic script per step in the dscript.
    Each step in the pipescript is transformed into a dynamic script in the dscript,
    where variable spaces are combined using the following order:

    1. STATIC: Definitions specific to each script in the pipescript.
    2. GLOBAL: User variables shared across steps from a specific point onwards.
    3. LOCAL: User variables for each individual step.

    Parameters:
    -----------
    verbose : bool, optional
        Controls verbosity of the dynamic scripts in the resulting dscript object.
        If None, the verbosity setting of the pipescript will be used.

    clean : "fixing" or "removing"
        - 'removing': Completely remove the empty step from TEMPLATE.
        - 'fixing': Replace the content of the empty step with a comment.

    **USER : scriptobjectdata(), optional
        Additional user-defined variables that can override existing static variables
        in the dscript object or be added to it.

    Returns:
    --------
    outd : dscript
        A dscript object that contains all steps of the pipescript as dynamic scripts.
        Each step from the pipescript is added as a dynamic script with the same content
        and combined variable spaces.
    """
    # Local imports
    from pizza.dscript import dscript, ScriptTemplate, lambdaScriptdata

    # verbosity
    printflag = self.printflag if printflag is None else printflag
    verbose = verbosity > 0 if verbosity is not None else (self.verbose if verbose is None else verbose)
    verbosity = 0 if not verbose else verbosity

    # Adjust name
    if name is None:
        if isinstance(self.name, str):
            name = self.name
        elif isinstance(self.name, list):
            name = (
                self.name[0] if len(self.name) == 1 else self.name[0] + "..." + self.name[-1]
            )

    # Create the dscript container with the pipescript name as the userid
    outd = dscript(userid=name, verbose=self.verbose, **USER)

    # Initialize static merged definitions
    staticmerged_definitions = lambdaScriptdata()

    # Track used variables per step
    step_used_variables = []

    # Loop over each step in the pipescript
    for i, script in enumerate(self.listscript):
        # Merge STATIC, GLOBAL, and LOCAL variables for the current step
        static_vars = self.listUSER[i] # script.DEFINITIONS
        global_vars = script.DEFINITIONS # self.scripts[i].USER
        local_vars = script.USER # self.USER[i]
        refreshed_globalvars = static_vars + global_vars

        # Detect variables used in the current template
        used_variables = set(script.detect_variables())
        step_used_variables.append(used_variables)  # Track used variables for this step

        # Copy all current variables to local_static_updates and remove matching variables from staticmerged_definitions
        local_static_updates = lambdaScriptdata(**local_vars)

        for var, value in refreshed_globalvars.items():
            if var in staticmerged_definitions:
                if (getattr(staticmerged_definitions, var) != value) and (var not in local_vars):
                    setattr(local_static_updates, var, value)
            else:
                setattr(staticmerged_definitions, var, value)

       # Create the dynamic script for this step using the method in dscript
        key_name = i  # Use the index 'i' as the key in TEMPLATE
        content = script.TEMPLATE

        # Use the helper method in dscript to add this dynamic script
        outd.add_dynamic_script(
            key=key_name,
            content=content,
            definitions = lambdaScriptdata(**local_static_updates),
            verbose=self.verbose if verbose is None else verbose,
            userid=self.name[i],
            autorefresh=False # prevent the replacement by default values ${}
        )

        # Set eval=True only if variables are detected in the template
        if outd.TEMPLATE[key_name].detect_variables():
            outd.TEMPLATE[key_name].eval = True

    # Compute the union of all used variables across all steps
    global_used_variables = set().union(*step_used_variables)

    # Filter staticmerged_definitions to keep only variables that are used
    filtered_definitions = {
        var: value for var, value in staticmerged_definitions.items() if var in global_used_variables
    }

    # Assign the filtered definitions along with USER variables to outd.DEFINITIONS
    outd.DEFINITIONS = lambdaScriptdata(**filtered_definitions)

    # Clean the entries for empty templates
    outd.clean(verbose=verbose,behavior=clean)

    return outd
def generate_report(self, filename, varnames=None, scopes='all')

Generates a comprehensive report for specified variables and writes it to a file.

Parameters: - filename (str): Path to the output report file. Must end with .md, .txt, or .html. - varnames (str or list/tuple, optional): Variable name(s) to include in the report. Defaults to 'all'. - scopes (str or list/tuple, optional): 'all', a single scope string, or a list of scope strings. Defaults to 'all'.

Raises: - ValueError: If 'filename' has an unsupported extension. - Exception: For other unforeseen errors.

Expand source code
def generate_report(self, filename, varnames=None, scopes="all"):
    """
    Generates a comprehensive report for specified variables and writes it to a file.

    Parameters:
    - filename (str): Path to the output report file. Must end with .md, .txt, or .html.
    - varnames (str or list/tuple, optional): Variable name(s) to include in the report. Defaults to 'all'.
    - scopes (str or list/tuple, optional): 'all', a single scope string, or a list of scope strings. Defaults to 'all'.

    Raises:
    - ValueError: If 'filename' has an unsupported extension.
    - Exception: For other unforeseen errors.
    """
    # Validate filename extension
    _, ext = os.path.splitext(filename)
    ext = ext.lower()

    if ext not in ['.md', '.txt', '.html']:
        raise ValueError("Unsupported file extension. Supported extensions are .md, .txt, and .html.")

    # Determine format based on extension
    if ext in ['.md', '.txt']:
        export_format = 'markdown'
    elif ext == '.html':
        export_format = 'html'

    # Determine variables to include
    if varnames is None or (isinstance(varnames, (list, tuple)) and len(varnames) == 0):
        variables = 'all'
    else:
        variables = varnames  # Can be a string or a list/tuple

    # Retrieve VariableOccurrences instances
    if variables == 'all':
        variables_occurrences = self.list_values(varname=None, what=scopes)
    else:
        # Normalize varnames to a list
        if isinstance(variables, str):
            variables = [variables]
        elif isinstance(variables, (list, tuple)):
            variables = list(variables)
        else:
            raise ValueError("Parameter 'varnames' must be a string or a list/tuple of strings.")

        variables_occurrences = {}
        for var in variables:
            vo = self.list_values(varname=var, what=scopes)
            if vo and var in vo.variables:
                variables_occurrences[var] = vo
            else:
                print(f"Warning: Variable '{var}' not found in the specified scopes.")

    # Initialize report content
    report_content = ""
    timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    caller = "generate_report"

    # Add report header
    if export_format == 'markdown':
        report_content += f"# Comprehensive Variable Report\n\n"
        report_content += f"**Generated on {timestamp} by {caller}**\n\n"
    elif export_format == 'html':
        # Define CSS for HTML
        css = """
        <style>
            body {
                font-family: Arial, sans-serif;
                margin: 20px;
            }
            h1, h2, h3, h4, h5 {
                color: #333;
            }
            table {
                border-collapse: collapse;
                width: 100%;
                margin-bottom: 40px;
            }
            th, td {
                border: 1px solid #ddd;
                padding: 8px;
                text-align: left;
            }
            th {
                background-color: #4CAF50;
                color: white;
            }
            tr:nth-child(even){background-color: #f2f2f2;}
            tr:hover {background-color: #ddd;}
        </style>
        """
        report_content += f"<!DOCTYPE html>\n<html>\n<head>\n<meta charset='UTF-8'>\n<title>Comprehensive Variable Report</title>\n{css}\n</head>\n<body>\n"
        report_content += f"<h1>Comprehensive Variable Report</h1>\n"
        report_content += f"<h2>Generated on {timestamp} by {caller}</h2>\n"

    # Assemble report content using VariableOccurrences.export()
    for var, vo in variables_occurrences.items():
        # Export content without headers and get as string
        var_content = vo.export(filename=filename, # use to identify the format based on its extension
                                scopes=scopes,
                                variables=var,
                                include_headers=False,
                                return_content=True)

        if export_format == 'markdown':
            # Add variable header
            report_content += f"## Variable: `{var}`\n\n"
            report_content += var_content + "\n\n"
            report_content += "---\n\n"  # Horizontal line between variables
        elif export_format == 'html':
            # Add variable header
            report_content += f"<h2>Variable: {var}</h2>\n"
            report_content += var_content + "\n<hr/>\n"  # Horizontal line between variables

    # Finalize HTML content
    if export_format == 'html':
        report_content += "</body>\n</html>"

    # Write report to file
    try:
        with open(filename, 'w', encoding='utf-8') as file:
            file.write(report_content)
        print(f"Report successfully generated at '{filename}'.")
    except Exception as e:
        raise Exception(f"Failed to write the report to '{filename}': {e}")
def getUSER(self, idx, key)

getUSER get USER variable getUSER(idx,varname)

Expand source code
def getUSER(self,idx,key):
    """
        getUSER get USER variable
        getUSER(idx,varname)
    """
    if isinstance(idx,int) and (idx>=0) and (idx<self.n):
        self.listUSER[idx].getattr(key)
    else:
        raise IndexError(f"the index in the pipe should be comprised between 0 and {len(self)-1}")
def header(self, verbose=True, verbosity=None, style=4)

Generate a formatted header for the pipescript file.

Parameters

verbosity (bool, optional): If specified, overrides the instance's verbose setting. Defaults to the instance's verbose. style (int from 1 to 6, optional): ASCII style to frame the header (default=4)

Returns

str
A formatted string representing the pipescript object. Returns an empty string if verbosity is False.

The header includes: - Total number of scripts in the pipeline. - The verbosity setting. - The range of scripts from the first to the last script. - All enclosed within an ASCII frame that adjusts to the content.

Expand source code
def header(self, verbose=True,verbosity=None, style=4):
    """
    Generate a formatted header for the pipescript file.

    Parameters:
        verbosity (bool, optional): If specified, overrides the instance's `verbose` setting.
                                    Defaults to the instance's `verbose`.
        style (int from 1 to 6, optional): ASCII style to frame the header (default=4)

    Returns:
        str: A formatted string representing the pipescript object.
             Returns an empty string if verbosity is False.

    The header includes:
        - Total number of scripts in the pipeline.
        - The verbosity setting.
        - The range of scripts from the first to the last script.
        - All enclosed within an ASCII frame that adjusts to the content.
    """
    verbose = verbosity > 0 if verbosity is not None else (self.verbose if verbose is None else verbose)
    verbosity = 0 if not verbose else verbosity
    if not verbosity:
        return ""

    # Prepare the header content
    lines = [
        f"PIPESCRIPT with {self.n} scripts | Verbosity: {verbosity}",
        "",
        f"From: <{str(self.scripts[0])}> To: <{str(self.scripts[-1])}>",
    ]

    # Use the shared method to format the header
    return frame_header(lines,style=style)
def list_multiple_values(self, varnames, what='all')

Lists all occurrences and values of multiple variables across the pipeline scripts.

Parameters: - varnames (list): A list of variable names to search for. - what (str or list/tuple): Specifies the scopes to search in. Can be "static", "global", "local", "all", or a list/tuple of any combination of these.

Returns: - dict: A dictionary mapping each variable name to its VariableOccurrences object.

Expand source code
def list_multiple_values(self, varnames, what="all"):
    """
    Lists all occurrences and values of multiple variables across the pipeline scripts.

    Parameters:
    - varnames (list): A list of variable names to search for.
    - what (str or list/tuple): Specifies the scopes to search in.
                                 Can be "static", "global", "local", "all",
                                 or a list/tuple of any combination of these.

    Returns:
    - dict: A dictionary mapping each variable name to its VariableOccurrences object.
    """
    if not isinstance(varnames, (list, tuple)):
        raise ValueError("Parameter 'varnames' must be a list or tuple of strings.")

    return self.list_values(varname=varnames, what=what)
def list_values(self, varname=None, what='all')

Lists all occurrences and values of a specified variable or all variables across the pipeline scripts.

Parameters: - varname (str, optional): The name of the variable to search for. If None, all variables are listed. - what (str or list/tuple, optional): Specifies the scopes to search in. Can be "static", "global", "local", "all", or a list/tuple of any combination of these.

Returns: - VariableOccurrences: If varname is specified, returns a VariableOccurrences instance for that variable. - dict of VariableOccurrences: If varname is None, returns a dictionary mapping each variable name to its VariableOccurrences instance.

Expand source code
def list_values(self, varname=None, what="all"):
    """
    Lists all occurrences and values of a specified variable or all variables across the pipeline scripts.

    Parameters:
    - varname (str, optional): The name of the variable to search for. If None, all variables are listed.
    - what (str or list/tuple, optional): Specifies the scopes to search in.
                                         Can be "static", "global", "local", "all",
                                         or a list/tuple of any combination of these.

    Returns:
    - VariableOccurrences: If varname is specified, returns a VariableOccurrences instance for that variable.
    - dict of VariableOccurrences: If varname is None, returns a dictionary mapping each variable name to its VariableOccurrences instance.
    """
    # Normalize 'what' to a list for uniform processing
    if isinstance(what, str):
        if what.lower() == "all":
            scopes = ["static", "global", "local"]
        else:
            scopes = [what.lower()]
    elif isinstance(what, (list, tuple)):
        scopes_lower = [s.lower() for s in what]
        if 'all' in scopes_lower:
            scopes = ["static", "global", "local"]
        else:
            scopes = scopes_lower
    else:
        raise ValueError("Parameter 'what' must be a string or a list/tuple of strings.")

    # Initialize data structures
    if varname:
        # Single variable case
        if len(scopes) == 1:
            data = []
        else:
            data = {}
            for scope in scopes:
                data[scope] = []

        # Iterate over each script in the pipeline
        for i, script in enumerate(self.listscript):
            # Retrieve variables for each scope
            static_vars = self.listUSER[i] if i < len(self.listUSER) else {}
            global_vars = getattr(script, 'DEFINITIONS', {})
            local_vars = getattr(script, 'USER', {})

            scope_vars = {
                "static": static_vars,
                "global": global_vars,
                "local": local_vars
            }

            # Check each requested scope
            for scope in scopes:
                vars_dict = scope_vars.get(scope, {})
                if varname in vars_dict.keys():
                    value = getattr(vars_dict,varname)
                    if len(scopes) == 1:
                        data.append((i, value))
                    else:
                        data[scope].append((i, value))

        # Return a VariableOccurrences instance for the specified variable
        return VariableOccurrences(data, variables=varname)

    else:
        # All variables case
        all_vars = set()

        # First, collect all variable names across specified scopes and scripts
        for i, script in enumerate(self.listscript):
            # Retrieve variables for each scope
            static_vars = self.listUSER[i] if i < len(self.listUSER) else {}
            global_vars = getattr(script, 'DEFINITIONS', {})
            local_vars = getattr(script, 'USER', {})

            scope_vars = {
                "static": static_vars,
                "global": global_vars,
                "local": local_vars
            }

            for scope in scopes:
                vars_dict = scope_vars.get(scope, {})
                all_vars.update(vars_dict.keys())

        # Initialize a dictionary to hold VariableOccurrences for each variable
        variables_data = {}
        for var in all_vars:
            var_data = {}
            for scope in scopes:
                var_data[scope] = []
            variables_data[var] = var_data

        # Iterate again to populate the data for each variable
        for i, script in enumerate(self.listscript):
            # Retrieve variables for each scope
            static_vars = self.listUSER[i] if i < len(self.listUSER) else {}
            global_vars = getattr(script, 'DEFINITIONS', {})
            local_vars = getattr(script, 'USER', {})

            scope_vars = {
                "static": static_vars,
                "global": global_vars,
                "local": local_vars
            }

            for scope in scopes:
                vars_dict = scope_vars.get(scope, {})
                for var, value in vars_dict.items():
                    variables_data[var][scope].append((i, value))

        # Convert each variable's data into a VariableOccurrences instance
        variables_occurrences = {}
        for var, data in variables_data.items():
            variables_occurrences[var] = VariableOccurrences(data, variables=var)

        return variables_occurrences
def plot_multiple_value_distributions(self, varnames, what='all', separate_plots=True)

Plots the distribution of elements for multiple variables across specified scopes.

Parameters: - varnames (list): A list of variable names to plot. - what (str or list/tuple): Specifies the scopes to include in the plot. Can be "static", "global", "local", "all", or a list/tuple of any combination of these. - separate_plots (bool): If True, plots each variable in a separate subplot. If False, combines all variables in a single plot for comparison.

Expand source code
def plot_multiple_value_distributions(self, varnames, what="all", separate_plots=True):
    """
    Plots the distribution of elements for multiple variables across specified scopes.

    Parameters:
    - varnames (list): A list of variable names to plot.
    - what (str or list/tuple): Specifies the scopes to include in the plot.
                                 Can be "static", "global", "local", "all",
                                 or a list/tuple of any combination of these.
    - separate_plots (bool): If True, plots each variable in a separate subplot.
                             If False, combines all variables in a single plot for comparison.
    """
    if not isinstance(varnames, (list, tuple)):
        raise ValueError("Parameter 'varnames' must be a list or tuple of strings.")

    # Retrieve VariableOccurrences instances
    multiple_vars = self.list_multiple_values(varnames, what=what)

    if separate_plots:
        num_vars = len(multiple_vars)
        fig, axes = plt.subplots(num_vars, 1, figsize=(10, 5 * num_vars))
        if num_vars == 1:
            axes = [axes]  # Make it iterable

        for ax, (var, vo) in zip(axes, multiple_vars.items()):
            summary = vo.summarize()[var]
            for scope, details in summary.items():
                elements = list(details.get('unique_elements_in_lists', []))
                counts = [details['element_counts_in_lists'][elem] for elem in elements]
                ax.bar(elements, counts, label=scope)
            ax.set_xlabel('Element')
            ax.set_ylabel('Count')
            ax.set_title(f"Distribution of elements in '{var}'")
            ax.legend()

        plt.tight_layout()
        plt.show()

    else:
        plt.figure(figsize=(12, 8))
        for var, vo in multiple_vars.items():
            summary = vo.summarize()[var]
            for scope, details in summary.items():
                elements = list(details.get('unique_elements_in_lists', []))
                counts = [details['element_counts_in_lists'][elem] for elem in elements]
                plt.bar([f"{var}_{elem}" for elem in elements], counts, label=f"{var} - {scope}")

        plt.xlabel('Element')
        plt.ylabel('Count')
        plt.title("Distribution of elements in multiple variables")
        plt.legend()
        plt.xticks(rotation=45, ha='right')
        plt.tight_layout()
        plt.show()
def rename(self, name='', idx=None)

rename scripts in the pipe p.rename(idx=[0,2,3],name=["A","B","C"])

Expand source code
def rename(self,name="",idx=None):
    """
        rename scripts in the pipe
            p.rename(idx=[0,2,3],name=["A","B","C"])
    """
    if isinstance(name,list):
        if len(name)==len(self) and idx==None:
            self.name = name
        elif len(name) == len(idx):
            for i in range(len(idx)):
                self.rename(name[i],idx[i])
        else:
            IndexError(f"the number of indices {len(idx)} does not match the number of names {len(name)}")
    elif idx !=None and idx<len(self) and name!="":
        self.name[idx] = name
    else:
        raise ValueError("provide a non empty name and valid index")
def script(self, idx=None, printflag=True, verbosity=2, verbose=None, forced=False, style=4)

script the pipeline or parts of the pipeline s = p.script() s = p.script([0,2])

Parameters: - idx (list, range, or int, optional): Specifies which steps of the pipeline to execute. If None, all steps from the current position to the end are executed. A list of indices can be provided to execute specific steps, or a single integer can be passed to execute a specific step. Default is None. - printflag (bool, optional): If True, the generated script for each step is printed to the console. Default is True. - verbosity (int, optional): Controls the level of detail in the generated script. - 0: Minimal output, no comments. - 1: Basic comments for run steps. - 2: Detailed comments with additional information. Default is 2. - forced (bool, optional): If True, all scripts are regenerated - style (int, optional): Defines the ASCII frame style for the header. Valid values are integers from 1 to 6, corresponding to predefined styles: 1. Basic box with +, -, and | 2. Double-line frame with , , and 3. Rounded corners with ., ', -, and | 4. Thick outer frame with #, =, and # 5. Box drawing characters with , , and 6. Minimalist dotted frame with ., :, and . Default is 4 (thick outer frame).

Expand source code
def script(self,idx=None, printflag=True, verbosity=2, verbose=None, forced=False, style=4):
    """
        script the pipeline or parts of the pipeline
            s = p.script()
            s = p.script([0,2])

    Parameters:
    - idx (list, range, or int, optional): Specifies which steps of the pipeline to execute.
                                           If None, all steps from the current position to
                                           the end are executed. A list of indices can be
                                           provided to execute specific steps, or a single
                                           integer can be passed to execute a specific step.
                                           Default is None.
    - printflag (bool, optional): If True, the generated script for each step is printed
                                  to the console. Default is True.
    - verbosity (int, optional): Controls the level of detail in the generated script.
                                 - 0: Minimal output, no comments.
                                 - 1: Basic comments for run steps.
                                 - 2: Detailed comments with additional information.
                                 Default is 2.
    - forced (bool, optional): If True, all scripts are regenerated
    - style (int, optional):
        Defines the ASCII frame style for the header.
        Valid values are integers from 1 to 6, corresponding to predefined styles:
            1. Basic box with `+`, `-`, and `|`
            2. Double-line frame with `╔`, `═`, and `║`
            3. Rounded corners with `.`, `'`, `-`, and `|`
            4. Thick outer frame with `#`, `=`, and `#`
            5. Box drawing characters with `┌`, `─`, and `│`
            6. Minimalist dotted frame with `.`, `:`, and `.`
        Default is `4` (thick outer frame).

    """
    printflag = self.printflag if printflag is None else printflag
    verbose = verbosity > 0 if verbosity is not None else (self.verbose if verbose is None else verbose)
    verbosity=0 if verbose is False else verbosity
    s = script(printflag=printflag, verbose=verbosity>0)
    s.name = "pipescript"
    s.description = "pipeline with %d scripts" % len(self)
    if len(self)>1:
        s.userid = self.name[0]+"->"+self.name[-1]
    elif len(self)==1:
        s.userid = self.name[0]
    else:
        s.userid = "empty pipeline"
    s.TEMPLATE = self.header(verbosity=verbosity, style=style) + "\n" +\
        self.do(idx, printflag=printflag, verbosity=verbosity, verbose=verbose, forced=forced)
    s.DEFINITIONS = duplicate(self.globalscript.DEFINITIONS)
    s.USER = duplicate(self.globalscript.USER)
    return s
def setUSER(self, idx, key, value)

setUSER sets USER variables setUSER(idx,varname,varvalue)

Expand source code
def setUSER(self,idx,key,value):
    """
        setUSER sets USER variables
        setUSER(idx,varname,varvalue)
    """
    if isinstance(idx,int) and (idx>=0) and (idx<self.n):
        self.listUSER[idx].setattr(key,value)
    else:
        raise IndexError(f"the index in the pipe should be comprised between 0 and {len(self)-1}")
def write(self, file, printflag=False, verbosity=2, verbose=None, overwrite=False)

Write the combined script to a file.

Parameters

file (str): The file path where the script will be saved. printflag (bool): Flag to enable/disable printing of details. verbosity (int): Level of verbosity for the script generation. verbose (bool or None): If True, enables verbose mode; if None, defaults to the instance's verbosity. overwrite (bool): Whether to overwrite the file if it already exists. Default is False.

Returns: str: The full absolute path of the file written.

Raises

FileExistsError
If the file already exists and overwrite is False.

Notes

  • This method combines the individual scripts within the pipescript object and saves the resulting script to the specified file.
  • If overwrite is False and the file exists, an error is raised.
  • If verbose is True and the file is overwritten, a warning is displayed.
Expand source code
def write(self, file, printflag=False, verbosity=2, verbose=None, overwrite=False):
   """
   Write the combined script to a file.

   Parameters:
       file (str): The file path where the script will be saved.
       printflag (bool): Flag to enable/disable printing of details.
       verbosity (int): Level of verbosity for the script generation.
       verbose (bool or None): If True, enables verbose mode; if None, defaults to the instance's verbosity.
       overwrite (bool): Whether to overwrite the file if it already exists. Default is False.

    Returns:
        str: The full absolute path of the file written.

   Raises:
       FileExistsError: If the file already exists and overwrite is False.

   Notes:
       - This method combines the individual scripts within the `pipescript` object
         and saves the resulting script to the specified file.
       - If `overwrite` is False and the file exists, an error is raised.
       - If `verbose` is True and the file is overwritten, a warning is displayed.
   """
   # Generate the combined script
   myscript = self.script(printflag=printflag, verbosity=verbosity, verbose=verbose, forced=True)
   # Call the script's write method with the overwrite parameter
   return myscript.write(file, printflag=printflag, verbose=verbose, overwrite=overwrite)
class rigidwall (beadtype=1, userid=None, USER=forcefield (FF object) with 0 parameters)

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

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

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

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

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

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

Ancestors

  • pizza.forcefield.none
  • pizza.forcefield.smd
  • pizza.forcefield.forcefield

Class variables

var description
var name
var userid
var version
class runsection (persistentfile=True, persistentfolder=None, printflag=False, verbose=False, verbosity=None, **userdefinitions)

LAMMPS script: run session

constructor adding instance definitions stored in USER

Expand source code
class runsection(script):
    """ LAMMPS script: run session """
    name = "run"
    description = name+" section"
    position = 9
    section = 10
    userid = "example"
    version = 0.1

    DEFINITIONS = globalsection.DEFINITIONS + scriptdata(
        balance = "$ 500 0.9 rcb"
        )

    TEMPLATE = """
# :RUN SECTION:
#   run configuration

    ####################################################################################################
    # RUN SIMULATION
    ####################################################################################################
    fix             balance_fix all balance ${balance} # load balancing for MPI
    run             ${tsim}
    """

Ancestors

Class variables

var DEFINITIONS
var TEMPLATE
var description
var name
var position
var section
var userid
var version

Inherited members

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

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

override any property with USER.property = value

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

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

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

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

Ancestors

  • pizza.forcefield.tlsph
  • pizza.forcefield.smd
  • pizza.forcefield.forcefield

Class variables

var description
var name
var userid
var version
class script (persistentfile=True, persistentfolder=None, printflag=False, verbose=False, verbosity=None, **userdefinitions)

script: A Core Class for Flexible LAMMPS Script Generation

The script class provides a flexible framework for generating dynamic LAMMPS script sections. It supports various LAMMPS sections such as "GLOBAL", "INITIALIZE", "GEOMETRY", "INTERACTIONS", and more, while allowing users to define custom sections with variable definitions, templates, and dynamic evaluation of script content.

Key Features:

  • Dynamic Script Generation: Easily define and manage script sections, using templates and definitions to dynamically generate LAMMPS-compatible scripts.
  • Script Concatenation: Combine multiple script sections while managing variable precedence and ensuring that definitions propagate as expected.
  • Flexible Variable Management: Separate DEFINITIONS for static variables and USER for user-defined variables, with clear rules for inheritance and precedence.
  • Operators for Advanced Script Handling: Use +, &, >>, |, and ** operators for script merging, static execution, right-shifting of definitions, and more.
  • Pipeline Support: Integrate scripts into pipelines, with full support for staged execution, variable inheritance, and reordering of script sections.

Practical Use Cases:

  • LAMMPS Automation: Automate the generation of complex LAMMPS scripts by defining reusable script sections with variables and templates.
  • Multi-Step Simulations: Manage multi-step simulations by splitting large scripts into smaller, manageable sections and combining them as needed.
  • Advanced Script Control: Dynamically modify script behavior by overriding variables or using advanced operators to concatenate, pipe, or merge scripts.

Methods:

init(self, persistentfile=True, persistentfolder=None, printflag=False, verbose=False, **userdefinitions): Initializes a new script object, with optional user-defined variables passed as userdefinitions.

do(self, printflag=None, verbose=None): Generates the LAMMPS script based on the current configuration, evaluating templates and definitions to produce the final output.

script(self, idx=None, printflag=True, verbosity=2, verbose=None, forced=False): Generate the final LAMMPS script from the pipeline or a subset of the pipeline.

add(self, s): Overloads the + operator to concatenate script objects, merging definitions and templates while maintaining variable precedence.

and(self, s): Overloads the & operator for static execution, combining the generated scripts of two script objects without merging their definitions.

mul(self, ntimes): Overloads the * operator to repeat the script ntimes, returning a new script object with repeated sections.

pow(self, ntimes): Overloads the ** operator to concatenate the script with itself ntimes, similar to the & operator, but repeated.

or(self, pipe): Overloads the pipe (|) operator to integrate the script into a pipeline, returning a pipescript object.

write(self, file, printflag=True, verbose=False): Writes the generated script to a file, including headers with metadata.

tmpwrite(self): Writes the script to a temporary file, creating both a full version and a clean version without comments.

printheader(txt, align="^", width=80, filler="~"): Static method to print formatted headers, useful for organizing output.

copy(self): Creates a shallow copy of the script object.

deepcopy(self, memo): Creates a deep copy of the script object, duplicating all internal variables.

Additional Features:

  • Customizable Templates: Use string templates with variable placeholders (e.g., ${value}) to dynamically generate script lines.
  • Static and User-Defined Variables: Manage global DEFINITIONS for static variables and USER variables for dynamic, user-defined settings.
  • Advanced Operators: Leverage a range of operators (+, >>, |, &) to manipulate script content, inherit definitions, and control variable precedence.
  • Verbose Output: Control verbosity to include detailed comments and debugging information in generated scripts.

Original Content:

The script class supports LAMMPS section generation and variable management with features such as: - Dynamic Evaluation of Scripts: Definitions and templates are evaluated at runtime, allowing for flexible and reusable scripts. - Inheritance of Definitions: Variable definitions can be inherited from previous sections, allowing for modular script construction. - Precedence Rules for Variables: When scripts are concatenated, definitions from the left take precedence, ensuring that the first defined values are preserved. - Instance and Global Variables: Instance variables are set via the USER object, while global variables (shared across instances) are managed in DEFINITIONS. - Script Pipelines: Scripts can be integrated into pipelines for sequential execution and dynamic variable propagation. - Flexible Output Formats: Lists are expanded into space-separated strings, while tuples are expanded with commas, making the output more readable.

Example Usage:

from pizza.script import script, scriptdata

class example_section(script):
    DEFINITIONS = scriptdata(
        X = 10,
        Y = 20,
        result = "${X} + ${Y}"
    )
    TEMPLATE = "${result} = ${X} + ${Y}"

s1 = example_section()
s1.USER.X = 5
s1.do()

The output for s1.do() will be:

25 = 5 + 20

With additional sections, scripts can be concatenated and executed as a single entity, with inheritance of variables and customizable behavior.

--------------------------------------
   OVERVIEW ANDE DETAILED FEATURES
--------------------------------------

The class script enables to generate dynamically LAMMPS sections
"NONE","GLOBAL","INITIALIZE","GEOMETRY","DISCRETIZATION",
"BOUNDARY","INTERACTIONS","INTEGRATION","DUMP","STATUS","RUN"


# %% This the typical construction for a class
class XXXXsection(script):
    "" " LAMMPS script: XXXX session "" "
    name = "XXXXXX"
    description = name+" section"
    position = 0
    section = 0
    userid = "example"
    version = 0.1

    DEFINITIONS = scriptdata(
         value= 1,
    expression= "${value+1}",
          text= "$my text"
        )

    TEMPLATE = "" "
# :UNDEF SECTION:
#   to be defined
LAMMPS code with ${value}, ${expression}, ${text}
    "" "

DEFINTIONS can be inherited from a previous section
DEFINITIONS = previousection.DEFINTIONS + scriptdata(
         value= 1,
    expression= "${value+1}",
          text= "$my text"
    )


Recommandation: Split a large script into a small classes or actions
An example of use could be:
    move1 = translation(displacement=10)+rotation(angle=30)
    move2 = shear(rate=0.1)+rotation(angle=20)
    bigmove = move1+move2+move1
    script = bigmove.do() generates the script

NOTE1: Use the print() and the method do() to get the script interpreted

NOTE2: DEFINITIONS can be pretified using DEFINITIONS.generator()

NOTE3: Variables can extracted from a template using TEMPLATE.scan()

NOTE4: Scripts can be joined (from top down to bottom).
The first definitions keep higher precedence. Please do not use
a variable twice with different contents.

myscript = s1 + s2 + s3 will propagate the definitions
without overwritting previous values). myscript will be
defined as s1 (same name, position, userid, etc.)

myscript += s appends the script section s to myscript

NOTE5: rules of precedence when script are concatenated
The attributes from the class (name, description...) are kept from the left
The values of the right overwrite all DEFINITIONS

NOTE6: user variables (instance variables) can set with USER or at the construction
myclass_instance = myclass(myvariable = myvalue)
myclass_instance.USER.myvariable = myvalue

NOTE7: how to change variables for all instances at once?
In the example below, let x is a global variable (instance independent)
and y a local variable (instance dependent)
instance1 = myclass(y=1) --> y=1 in instance1
instance2 = myclass(y=2) --> y=2 in instance2
instance3.USER.y=3 --> y=3 in instance3
instance1.DEFINITIONS.x = 10 --> x=10 in all instances (1,2,3)

If x is also defined in the USER section, its value will be used
Setting instance3.USER.x = 30 will assign x=30 only in instance3

NOTE8: if a the script is used with different values for a same parameter
use the operator & to concatenate the results instead of the script
example: load(file="myfile1") & load(file="myfile2) & load(file="myfile3")+...

NOTE9: lists (e.g., [1,2,'a',3] are expanded ("1 2 a 3")
       tuples (e.g. (1,2)) are expanded ("1,2")
       It is easier to read ["lost","ignore"] than "$ lost ignore"

NOTE 10: New operators >> and || extend properties
    + merge all scripts but overwrite definitions
    & execute statically script content
    >> pass only DEFINITIONS but not TEMPLATE to the right
    | pipe execution such as in Bash, the result is a pipeline

NOTE 11: Scripts in pipelines are very flexible, they support
full indexing à la Matlab, including staged executions
    method do(idx) generates the script corresponding to indices idx
    method script(idx) generates the corresponding script object

--------------------------[ FULL EXAMPLE ]-----------------------------

# Import the class
from pizza.script import *

# Override the class globalsection
class scriptexample(globalsection):
    description = "demonstrate commutativity of additions"
    verbose = True

    DEFINITIONS = scriptdata(
        X = 10,
        Y = 20,
        R1 = "${X}+${Y}",
        R2 = "${Y}+${X}"
        )
    TEMPLATE = "" "
    # Property of the addition
    ${R1} = ${X} + ${Y}
    ${R2} = ${Y} + ${X}
 "" "

# derived from scriptexample, X and Y are reused
class scriptexample2(scriptexample):
    description = "demonstrate commutativity of multiplications"
    verbose = True
    DEFINITIONS = scriptexample.DEFINITIONS + scriptdata(
        R3 = "${X} * ${Y}",
        R4 = "${Y} * ${X}",
        )
    TEMPLATE = "" "
    # Property of the multiplication
    ${R3} = ${X} * ${Y}
    ${R4} = ${Y} * ${X}
 "" "

# call the first class and override the values X and Y
s1 = scriptexample()
s1.USER.X = 1  # method 1 of override
s1.USER.Y = 2
s1.do()
# call the second class and override the values X and Y
s2 = scriptexample2(X=1000,Y=2000) # method 2
s2.do()
# Merge the two scripts
s = s1+s2
print("this is my full script")
s.description
s.do()

# The result for s1 is
    3 = 1 + 2
    3 = 2 + 1
# The result for s2 is
    2000000 = 1000 * 2000
    2000000 = 2000 * 1000
# The result for s=s1+s2 is
    # Property of the addition
    3000 = 1000 + 2000
    3000 = 2000 + 1000
    # Property of the multiplication
    2000000 = 1000 * 2000
    2000000 = 2000 * 1000

constructor adding instance definitions stored in USER

Expand source code
class script:
    """
    script: A Core Class for Flexible LAMMPS Script Generation

    The `script` class provides a flexible framework for generating dynamic LAMMPS
    script sections. It supports various LAMMPS sections such as "GLOBAL", "INITIALIZE",
    "GEOMETRY", "INTERACTIONS", and more, while allowing users to define custom sections
    with variable definitions, templates, and dynamic evaluation of script content.

    Key Features:
    -------------
    - **Dynamic Script Generation**: Easily define and manage script sections,
      using templates and definitions to dynamically generate LAMMPS-compatible scripts.
    - **Script Concatenation**: Combine multiple script sections while managing
      variable precedence and ensuring that definitions propagate as expected.
    - **Flexible Variable Management**: Separate `DEFINITIONS` for static variables and
      `USER` for user-defined variables, with clear rules for inheritance and precedence.
    - **Operators for Advanced Script Handling**: Use `+`, `&`, `>>`, `|`, and `**` operators
      for script merging, static execution, right-shifting of definitions, and more.
    - **Pipeline Support**: Integrate scripts into pipelines, with full support for
      staged execution, variable inheritance, and reordering of script sections.

    Practical Use Cases:
    --------------------
    - **LAMMPS Automation**: Automate the generation of complex LAMMPS scripts by defining
      reusable script sections with variables and templates.
    - **Multi-Step Simulations**: Manage multi-step simulations by splitting large scripts
      into smaller, manageable sections and combining them as needed.
    - **Advanced Script Control**: Dynamically modify script behavior by overriding variables
      or using advanced operators to concatenate, pipe, or merge scripts.

    Methods:
    --------
    __init__(self, persistentfile=True, persistentfolder=None, printflag=False, verbose=False, **userdefinitions):
        Initializes a new `script` object, with optional user-defined variables
        passed as `userdefinitions`.

    do(self, printflag=None, verbose=None):
        Generates the LAMMPS script based on the current configuration, evaluating
        templates and definitions to produce the final output.

    script(self, idx=None, printflag=True, verbosity=2, verbose=None, forced=False):
        Generate the final LAMMPS script from the pipeline or a subset of the pipeline.

    add(self, s):
        Overloads the `+` operator to concatenate script objects, merging definitions
        and templates while maintaining variable precedence.

    and(self, s):
        Overloads the `&` operator for static execution, combining the generated scripts
        of two script objects without merging their definitions.

    __mul__(self, ntimes):
        Overloads the `*` operator to repeat the script `ntimes`, returning a new script
        object with repeated sections.

    __pow__(self, ntimes):
        Overloads the `**` operator to concatenate the script with itself `ntimes`,
        similar to the `&` operator, but repeated.

    __or__(self, pipe):
        Overloads the pipe (`|`) operator to integrate the script into a pipeline,
        returning a `pipescript` object.

    write(self, file, printflag=True, verbose=False):
        Writes the generated script to a file, including headers with metadata.

    tmpwrite(self):
        Writes the script to a temporary file, creating both a full version and a clean
        version without comments.

    printheader(txt, align="^", width=80, filler="~"):
        Static method to print formatted headers, useful for organizing output.

    __copy__(self):
        Creates a shallow copy of the script object.

    __deepcopy__(self, memo):
        Creates a deep copy of the script object, duplicating all internal variables.

    Additional Features:
    --------------------
    - **Customizable Templates**: Use string templates with variable placeholders
      (e.g., `${value}`) to dynamically generate script lines.
    - **Static and User-Defined Variables**: Manage global `DEFINITIONS` for static
      variables and `USER` variables for dynamic, user-defined settings.
    - **Advanced Operators**: Leverage a range of operators (`+`, `>>`, `|`, `&`) to
      manipulate script content, inherit definitions, and control variable precedence.
    - **Verbose Output**: Control verbosity to include detailed comments and debugging
      information in generated scripts.

    Original Content:
    -----------------
    The `script` class supports LAMMPS section generation and variable management with
    features such as:
    - **Dynamic Evaluation of Scripts**: Definitions and templates are evaluated at runtime,
      allowing for flexible and reusable scripts.
    - **Inheritance of Definitions**: Variable definitions can be inherited from previous
      sections, allowing for modular script construction.
    - **Precedence Rules for Variables**: When scripts are concatenated, definitions from
      the left take precedence, ensuring that the first defined values are preserved.
    - **Instance and Global Variables**: Instance variables are set via the `USER` object,
      while global variables (shared across instances) are managed in `DEFINITIONS`.
    - **Script Pipelines**: Scripts can be integrated into pipelines for sequential execution
      and dynamic variable propagation.
    - **Flexible Output Formats**: Lists are expanded into space-separated strings, while
      tuples are expanded with commas, making the output more readable.

    Example Usage:
    --------------
    ```
    from pizza.script import script, scriptdata

    class example_section(script):
        DEFINITIONS = scriptdata(
            X = 10,
            Y = 20,
            result = "${X} + ${Y}"
        )
        TEMPLATE = "${result} = ${X} + ${Y}"

    s1 = example_section()
    s1.USER.X = 5
    s1.do()
    ```

    The output for `s1.do()` will be:
    ```
    25 = 5 + 20
    ```

    With additional sections, scripts can be concatenated and executed as a single
    entity, with inheritance of variables and customizable behavior.


        --------------------------------------
           OVERVIEW ANDE DETAILED FEATURES
        --------------------------------------

        The class script enables to generate dynamically LAMMPS sections
        "NONE","GLOBAL","INITIALIZE","GEOMETRY","DISCRETIZATION",
        "BOUNDARY","INTERACTIONS","INTEGRATION","DUMP","STATUS","RUN"


        # %% This the typical construction for a class
        class XXXXsection(script):
            "" " LAMMPS script: XXXX session "" "
            name = "XXXXXX"
            description = name+" section"
            position = 0
            section = 0
            userid = "example"
            version = 0.1

            DEFINITIONS = scriptdata(
                 value= 1,
            expression= "${value+1}",
                  text= "$my text"
                )

            TEMPLATE = "" "
        # :UNDEF SECTION:
        #   to be defined
        LAMMPS code with ${value}, ${expression}, ${text}
            "" "

        DEFINTIONS can be inherited from a previous section
        DEFINITIONS = previousection.DEFINTIONS + scriptdata(
                 value= 1,
            expression= "${value+1}",
                  text= "$my text"
            )


        Recommandation: Split a large script into a small classes or actions
        An example of use could be:
            move1 = translation(displacement=10)+rotation(angle=30)
            move2 = shear(rate=0.1)+rotation(angle=20)
            bigmove = move1+move2+move1
            script = bigmove.do() generates the script

        NOTE1: Use the print() and the method do() to get the script interpreted

        NOTE2: DEFINITIONS can be pretified using DEFINITIONS.generator()

        NOTE3: Variables can extracted from a template using TEMPLATE.scan()

        NOTE4: Scripts can be joined (from top down to bottom).
        The first definitions keep higher precedence. Please do not use
        a variable twice with different contents.

        myscript = s1 + s2 + s3 will propagate the definitions
        without overwritting previous values). myscript will be
        defined as s1 (same name, position, userid, etc.)

        myscript += s appends the script section s to myscript

        NOTE5: rules of precedence when script are concatenated
        The attributes from the class (name, description...) are kept from the left
        The values of the right overwrite all DEFINITIONS

        NOTE6: user variables (instance variables) can set with USER or at the construction
        myclass_instance = myclass(myvariable = myvalue)
        myclass_instance.USER.myvariable = myvalue

        NOTE7: how to change variables for all instances at once?
        In the example below, let x is a global variable (instance independent)
        and y a local variable (instance dependent)
        instance1 = myclass(y=1) --> y=1 in instance1
        instance2 = myclass(y=2) --> y=2 in instance2
        instance3.USER.y=3 --> y=3 in instance3
        instance1.DEFINITIONS.x = 10 --> x=10 in all instances (1,2,3)

        If x is also defined in the USER section, its value will be used
        Setting instance3.USER.x = 30 will assign x=30 only in instance3

        NOTE8: if a the script is used with different values for a same parameter
        use the operator & to concatenate the results instead of the script
        example: load(file="myfile1") & load(file="myfile2) & load(file="myfile3")+...

        NOTE9: lists (e.g., [1,2,'a',3] are expanded ("1 2 a 3")
               tuples (e.g. (1,2)) are expanded ("1,2")
               It is easier to read ["lost","ignore"] than "$ lost ignore"

        NOTE 10: New operators >> and || extend properties
            + merge all scripts but overwrite definitions
            & execute statically script content
            >> pass only DEFINITIONS but not TEMPLATE to the right
            | pipe execution such as in Bash, the result is a pipeline

        NOTE 11: Scripts in pipelines are very flexible, they support
        full indexing à la Matlab, including staged executions
            method do(idx) generates the script corresponding to indices idx
            method script(idx) generates the corresponding script object

        --------------------------[ FULL EXAMPLE ]-----------------------------

        # Import the class
        from pizza.script import *

        # Override the class globalsection
        class scriptexample(globalsection):
            description = "demonstrate commutativity of additions"
            verbose = True

            DEFINITIONS = scriptdata(
                X = 10,
                Y = 20,
                R1 = "${X}+${Y}",
                R2 = "${Y}+${X}"
                )
            TEMPLATE = "" "
            # Property of the addition
            ${R1} = ${X} + ${Y}
            ${R2} = ${Y} + ${X}
         "" "

        # derived from scriptexample, X and Y are reused
        class scriptexample2(scriptexample):
            description = "demonstrate commutativity of multiplications"
            verbose = True
            DEFINITIONS = scriptexample.DEFINITIONS + scriptdata(
                R3 = "${X} * ${Y}",
                R4 = "${Y} * ${X}",
                )
            TEMPLATE = "" "
            # Property of the multiplication
            ${R3} = ${X} * ${Y}
            ${R4} = ${Y} * ${X}
         "" "

        # call the first class and override the values X and Y
        s1 = scriptexample()
        s1.USER.X = 1  # method 1 of override
        s1.USER.Y = 2
        s1.do()
        # call the second class and override the values X and Y
        s2 = scriptexample2(X=1000,Y=2000) # method 2
        s2.do()
        # Merge the two scripts
        s = s1+s2
        print("this is my full script")
        s.description
        s.do()

        # The result for s1 is
            3 = 1 + 2
            3 = 2 + 1
        # The result for s2 is
            2000000 = 1000 * 2000
            2000000 = 2000 * 1000
        # The result for s=s1+s2 is
            # Property of the addition
            3000 = 1000 + 2000
            3000 = 2000 + 1000
            # Property of the multiplication
            2000000 = 1000 * 2000
            2000000 = 2000 * 1000

    """

    # metadata
    metadata = get_metadata()               # retrieve all metadata

    type = "script"                         # type (class name)
    name = "empty script"                   # name
    description = "it is an empty script"   # description
    position = 0                            # 0 = root
    section = 0                             # section (0=undef)
    userid = "undefined"                    # user name
    version = metadata["version"]           # version
    license = metadata["license"]
    email = metadata["email"]               # email

    verbose = False                         # set it to True to force verbosity
    _contact = ("INRAE\SAYFOOD\olivier.vitrac@agroparistech.fr",
                "INRAE\SAYFOOD\william.jenkinson@agroparistech.fr",
                "INRAE\SAYFOOD\han.chen@inrae.fr")

    SECTIONS = ["NONE","GLOBAL","INITIALIZE","GEOMETRY","DISCRETIZATION",
                "BOUNDARY","INTERACTIONS","INTEGRATION","DUMP","STATUS","RUN"]

    # Main class variables
    # These definitions are for instances
    DEFINITIONS = scriptdata()
    TEMPLATE = """
        # empty LAMMPS script
    """

    # constructor
    def __init__(self,persistentfile=True,
                 persistentfolder = None,
                 printflag = False,
                 verbose = False,
                 verbosity = None,
                 **userdefinitions):
        """ constructor adding instance definitions stored in USER """
        if persistentfolder is None: persistentfolder = get_tmp_location()
        self.persistentfile = persistentfile
        self.persistentfolder = persistentfolder
        self.printflag = printflag
        self.verbose = verbose if verbosity is None else verbosity>0
        self.verbosity = 0 if not verbose else verbosity
        self.USER = scriptdata(**userdefinitions)

    # 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)))

    # String representation
    def __str__(self):
        """ string representation """
        return f"{self.type}:{self.name}:{self.userid}"

    # Display/representation method
    def __repr__(self):
        """ disp method """
        stamp = str(self)
        self.printheader(f"{stamp} | version={self.version}",filler="/")
        self.printheader("POSITION & DESCRIPTION",filler="-",align=">")
        print(f"     position: {self.position}")
        print(f"         role: {self.role} (section={self.section})")
        print(f"  description: {self.description}")
        self.printheader("DEFINITIONS",filler="-",align=">")
        if len(self.DEFINITIONS)<15:
            self.DEFINITIONS.__repr__()
        else:
            print("too many definitions: ",self.DEFINITIONS)
        if self.verbose:
            self.printheader("USER",filler="-",align=">")
            self.USER.__repr__()
            self.printheader("TEMPLATE",filler="-",align=">")
            print(self.TEMPLATE)
            self.printheader("SCRIPT",filler="-",align=">")
        print(self.do(printflag=False))
        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))}

    # Generate the script
    def do(self,printflag=None,verbose=None):
        """
        Generate the LAMMPS script based on the current configuration.

        This method generates a LAMMPS-compatible script from the templates and definitions
        stored in the `script` object. The generated script can be displayed, returned,
        and optionally include comments for debugging or clarity.

        Parameters:
        -----------
        - printflag (bool, optional): If True, the generated script is printed to the console.
                                      Default is True.
        - verbose (bool, optional): If True, comments and additional information are included
                                    in the generated script. If False, comments are removed.
                                    Default is True.

        Returns:
        ---------
        - str: The generated LAMMPS script.

        Method Behavior:
        - The method first collects all key-value pairs from `DEFINITIONS` and `USER` objects,
          which store the configuration data for the script.
        - Lists and tuples in the collected data are formatted into a readable string with proper
          separators (space for lists, comma for tuples) and prefixed with a '%' to indicate comments.
        - The generated command template is formatted and evaluated using the collected data.
        - If `verbose` is set to False, comments in the generated script are removed.
        - The script is then printed if `printflag` is True.
        - Finally, the formatted script is returned as a string.


        Example Usage:
        --------------
        >>> s = script()
        >>> s.do(printflag=True, verbose=True)
        units           si
        dimension       3
        boundary        f f f
        # Additional script commands...

        >>> s.do(printflag=False, verbose=False)
        'units si\ndimension 3\nboundary f f f\n# Additional script commands...'

        Notes:
        - Comments are indicated in the script with '%' or '#'.
        - The [position {self.position}:{self.userid}] marker is inserted for tracking
          script sections or modifications.


        Known Issues for indexed variables
        ----------------------------------
        List and tupples used indexed in templates, such as varlist[1], are not converted into strings so that
        each element can be considered as a variable and accessed as ${varlist[1]}. If varlist is also used in
        full, such as ${varlist}. Then it is preferable to define varlist as a string:
            "![v1,v2,...]" where v1,v2,v3 can be int, float, static str or evaluable str (i.e., subjected to nested
             evaluation).
        The control character ! in lists, such as in var=![v1,v2,v3] preserves the Pythonic definition by do()
        at later stages during the evaluation so that its content can be indexed.

        """
        printflag = self.printflag if printflag is None else printflag
        verbose = self.verbose if verbose is None else verbose
        inputs = self.DEFINITIONS + self.USER
        usedvariables = self.detect_variables(with_index=False,only_indexed=False)
        variables_used_with_index = self.detect_variables(with_index=False,only_indexed=True)
        usedvariables_withoutindex = [ var for var in usedvariables if var not in variables_used_with_index ]
        for k in inputs.keys():
            if k in usedvariables_withoutindex:
                if isinstance(inputs.getattr(k),list):
                    inputs.setattr(k,"% "+span(inputs.getattr(k)))
                elif isinstance(inputs.getattr(k),tuple):
                    inputs.setattr(k,"% "+span(inputs.getattr(k),sep=","))
        cmd = inputs.formateval(self.TEMPLATE)
        cmd = cmd.replace("[comment]",f"[position {self.position}:{self.userid}]")
        if not verbose: cmd=remove_comments(cmd)
        if printflag: print(cmd)
        return cmd

    # Return the role of the script (based on section)
    @property
    def role(self):
        """ convert section index into a role (section name) """
        if self.section in range(len(self.SECTIONS)):
            return self.SECTIONS[self.section]
        else:
            return ""

    # override +
    def __add__(self,s):
        """ overload addition operator """
        from pizza.dscript import dscript
        from pizza.group import group, groupcollection
        from pizza.region import region
        if isinstance(s,script):
            dup = duplicate(self)
            dup.DEFINITIONS = dup.DEFINITIONS + s.DEFINITIONS
            dup.USER = dup.USER + s.USER
            dup.TEMPLATE = "\n".join([dup.TEMPLATE,s.TEMPLATE])
            return dup
        elif isinstance(s,pipescript):
            return pipescript(self, printflag=s.printflag,verbose=s.verbose,verbosity=s.verbosity) | s
        elif isinstance(s,dscript):
            return self + s.script(printflag=s.printflag,verbose=s.verbose,verbosity=s.verbosity)
        elif isinstance(s, scriptobjectgroup):
            return self + s.script(printflag=self.printflag,verbose=self.verbose,verbosity=self.verbosity)
        elif isinstance(s, group):
            return self + s.script(printflag=s.printflag,verbose=s.verbose,verbosity=s.verbosity)
        elif isinstance(s, groupcollection):
            return self + s.script(printflag=self.printflag,verbose=self.verbose,verbosity=self.verbosity)
        elif isinstance(s,region):
            return self + s.script(printflag=s.printflag,verbose=s.verbose,verbosity=s.verbosity)
        raise TypeError(f"the second operand in + must a script, pipescript, scriptobjectgroup,\n group, groupcollection or region object not {type(s)}")

    # override +=
    def _iadd__(self,s):
        """ overload addition operator """
        if isinstance(s,script):
            self.DEFINITIONS = self.DEFINITIONS + s.DEFINITIONS
            self.USER = self.USER + s.USER
            self.TEMPLATE = "\n".join([self.TEMPLATE,s.TEMPLATE])
        else:
            raise TypeError("the second operand must a script object")

    # override >>
    def __rshift__(self,s):
        """ overload right  shift operator (keep only the last template) """
        if isinstance(s,script):
            dup = duplicate(self)
            dup.DEFINITIONS = dup.DEFINITIONS + s.DEFINITIONS
            dup.USER = dup.USER + s.USER
            dup.TEMPLATE = s.TEMPLATE
            return dup
        else:
            raise TypeError(f"the second operand in >> must a script object not {type(s)}")

    # override &
    def __and__(self,s):
        """ overload and operator """
        if isinstance(s,script):
            dup = duplicate(self)
            dup.TEMPLATE = "\n".join([self.do(printflag=False,verbose=False),s.do(printflag=False,verbose=False)])
            return dup
        raise TypeError(f"the second operand in & must a script object not {type(s)}")

    # override *
    def __mul__(self,ntimes):
        """ overload * operator """
        if isinstance(ntimes, int) and ntimes>0:
           res = duplicate(self)
           if ntimes>1:
               for n in range(1,ntimes): res += self
           return res
        raise ValueError("multiplicator should be a strictly positive integer")

    # override **
    def __pow__(self,ntimes):
        """ overload ** operator """
        if isinstance(ntimes, int) and ntimes>0:
           res = duplicate(self)
           if ntimes>1:
               for n in range(1,ntimes): res = res & self
           return res
        raise ValueError("multiplicator should be a strictly positive integer")

    # pipe scripts
    def __or__(self,pipe):
        """ overload | or for pipe """
        from pizza.dscript import dscript
        from pizza.group import group, groupcollection
        from pizza.region import region
        if isinstance(pipe, dscript):
            rightarg = pipe.pipescript(printflag=pipe.printflag,verbose=pipe.verbose,verbosity=pipe.verbosity)
        elif isinstance(pipe,group):
            rightarg = pipe.script(printflag=pipe.printflag,verbose=pipe.verbose,verbosity=pipe.verbosity)
        elif isinstance(pipe,groupcollection):
            rightarg = pipe.script(printflag=self.printflag,verbose=self.verbose,verbosity=self.verbosity)
        elif isinstance(pipe,region):
            rightarg = pipe.pscript(printflag=self.printflag,verbose=self.verbose,verbosity=self.verbosity)
        else:
            rightarg = pipe
        if isinstance(rightarg,(pipescript,script,scriptobject,scriptobjectgroup)):
            return pipescript(self,printflag=self.printflag,verbose=self.verbose,verbosity=self.verbosity) | rightarg
        else:
            raise ValueError(f"the argument in | must a pipescript, a scriptobject or a scriptobjectgroup not {type(s)}")


    def header(self, verbose=True, verbosity=None, style=2):
        """
        Generate a formatted header for the script file.

        Parameters:
            verbosity (bool, optional): If specified, overrides the instance's `verbose` setting.
                                        Defaults to the instance's `verbose`.
            style (int from 1 to 6, optional): ASCII style to frame the header (default=2)

        Returns:
            str: A formatted string representing the script's metadata and initialization details.
                 Returns an empty string if verbosity is False.

        The header includes:
            - Script version, license, and contact email.
            - User ID and the number of initialized definitions.
            - Current system user, hostname, and working directory.
            - Persistent filename and folder path.
            - Timestamp of the header generation.
        """
        verbose = verbosity > 0 if verbosity is not None else (self.verbose if verbose is None else verbose)
        verbosity = 0 if not verbose else verbosity
        if not verbose:
            return ""
        # Prepare the header content
        lines = [
            f"PIZZA.SCRIPT FILE v{script.version} | License: {script.license} | Email: {script.email}",
            "",
            f"<{str(self)}>",
            f"Initialized with {len(self.USER)} definitions | Verbosity: {verbosity}",
            f"Persistent file: \"{self.persistentfile}\" | Folder: \"{self.persistentfolder}\"",
            "",
            f"Generated on: {getpass.getuser()}@{socket.gethostname()}:{os.getcwd()}",
            f"{datetime.datetime.now().strftime('%A, %B %d, %Y at %H:%M:%S')}",
        ]
        # Use the shared method to format the header
        return frame_header(lines,style=style)


    # write file
    def write(self, file, printflag=True, verbose=False, overwrite=False, style=2):
        """
        Write the script to a file.

        Parameters:
            - file (str): The file path where the script will be saved.
            - printflag (bool): Flag to enable/disable printing of details.
            - verbose (bool): Flag to enable/disable verbose mode.
            - overwrite (bool): Whether to overwrite the file if it already exists.
            - style (int, optional):
                Defines the ASCII frame style for the header.
                Valid values are integers from 1 to 6, corresponding to predefined styles:
                    1. Basic box with `+`, `-`, and `|`
                    2. Double-line frame with `╔`, `═`, and `║`
                    3. Rounded corners with `.`, `'`, `-`, and `|`
                    4. Thick outer frame with `#`, `=`, and `#`
                    5. Box drawing characters with `┌`, `─`, and `│`
                    6. Minimalist dotted frame with `.`, `:`, and `.`
                Default is `2` (frame with rounded corners).

        Returns:
            str: The full absolute path of the file written.

        Raises:
            FileExistsError: If the file already exists and overwrite is False.
        """
        # Resolve full path
        full_path = os.path.abspath(file)
        if os.path.exists(full_path) and not overwrite:
            raise FileExistsError(f"The file '{full_path}' already exists. Use overwrite=True to overwrite it.")
        if os.path.exists(full_path) and overwrite and verbose:
            print(f"Warning: Overwriting the existing file '{full_path}'.")
        # Generate the script and write to the file
        cmd = self.do(printflag=printflag, verbose=verbose)
        with open(full_path, "w") as f:
            print(self.header(verbosity=verbose, style=style), "\n", file=f)
            print(cmd, file=f)
        # Return the full path of the written file
        return full_path

    def tmpwrite(self, verbose=False, style=1):
        """
        Write the script to a temporary file and create optional persistent copies.

        Parameters:
            verbose (bool, optional): Controls verbosity during script generation. Defaults to False.

        The method:
            - Creates a temporary file for the script, with platform-specific behavior:
                - On Windows (`os.name == 'nt'`), the file is not automatically deleted.
                - On other systems, the file is temporary and deleted upon closure.
            - Writes a header and the script content into the temporary file.
            - Optionally creates a persistent copy in the `self.persistentfolder` directory:
                - `script.preview.<suffix>`: A persistent copy of the temporary file.
                - `script.preview.clean.<suffix>`: A clean copy with comments and empty lines removed.
            - Handles cleanup and exceptions gracefully to avoid leaving orphaned files.
            - style (int, optional):
                Defines the ASCII frame style for the header.
                Valid values are integers from 1 to 6, corresponding to predefined styles:
                    1. Basic box with `+`, `-`, and `|`
                    2. Double-line frame with `╔`, `═`, and `║`
                    3. Rounded corners with `.`, `'`, `-`, and `|`
                    4. Thick outer frame with `#`, `=`, and `#`
                    5. Box drawing characters with `┌`, `─`, and `│`
                    6. Minimalist dotted frame with `.`, `:`, and `.`
                Default is `1` (basic box).

        Returns:
            TemporaryFile: The temporary file handle (non-Windows systems only).
            None: On Windows, the file is closed and not returned.

        Raises:
            Exception: If there is an error creating or writing to the temporary file.
        """
        try:
            # OS-specific temporary file behavior
            if os.name == 'nt':  # Windows
                ftmp = tempfile.NamedTemporaryFile(mode="w+b", prefix="script_", suffix=".txt", delete=False)
            else:  # Other platforms
                ftmp = tempfile.NamedTemporaryFile(mode="w+b", prefix="script_", suffix=".txt")

            # Generate header and content
            header = (
                f"# TEMPORARY PIZZA.SCRIPT FILE\n"
                f"# {'-' * 40}\n"
                f"{self.header(verbosity=verbose, style=style)}"
            )
            content = (
                header
                + "\n# This is a temporary file (it will be deleted automatically)\n\n"
                + self.do(printflag=False, verbose=verbose)
            )

            # Write content to the temporary file
            ftmp.write(BOM_UTF8 + content.encode('utf-8'))
            ftmp.seek(0)  # Reset file pointer to the beginning

        except Exception as e:
            # Handle errors gracefully
            ftmp.close()
            os.remove(ftmp.name)  # Clean up the temporary file
            raise Exception(f"Failed to write to or handle the temporary file: {e}") from None

        print("\nTemporary File Header:\n", header, "\n")
        print("A temporary file has been generated here:\n", ftmp.name)

        # Persistent copy creation
        if self.persistentfile:
            ftmpname = os.path.basename(ftmp.name)
            fcopyname = os.path.join(self.persistentfolder, f"script.preview.{ftmpname.rsplit('_', 1)[1]}")
            copyfile(ftmp.name, fcopyname)
            print("A persistent copy has been created here:\n", fcopyname)

            # Create a clean copy without empty lines or comments
            with open(ftmp.name, "r") as f:
                lines = f.readlines()
            bom_utf8_str = BOM_UTF8.decode("utf-8")
            clean_lines = [
                line for line in lines
                if line.strip() and not line.lstrip().startswith("#") and not line.startswith(bom_utf8_str)
            ]
            fcleanname = os.path.join(self.persistentfolder, f"script.preview.clean.{ftmpname.rsplit('_', 1)[1]}")
            with open(fcleanname, "w") as f:
                f.writelines(clean_lines)
            print("A clean copy has been created here:\n", fcleanname)

            # Handle file closure for Windows
            if os.name == 'nt':
                ftmp.close()
                return None
            else:
                return ftmp


    # Note that it was not the original intent to copy scripts
    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, deepduplicate(v, memo))
        return copie


    def detect_variables(self, with_index=False, only_indexed=False):
        """
        Detects variables in the content of the template using an extended pattern
        to include indexed variables (e.g., ${var[i]}) if `with_index` is True.

        Parameters:
        -----------
        with_index : bool, optional
            If True, include indexed variables with their indices (e.g., ${var[i]}). Default is False.
        only_indexed : bool, optional
            If True, target only variables that are used with an index (e.g., ${var[i]}). Default is False.

        Returns:
        --------
        list
            A list of unique variable names detected in the content based on the flags.
        """
        # Regular expression to match variables with optional indexing
        variable_pattern = re.compile(r'\$\{(\w+)(\[\w+\])?\}')
        # Ensure TEMPLATE is iterable (split string into lines if needed)
        if isinstance(self.TEMPLATE, str):
            lines = self.TEMPLATE.splitlines()  # Split string into lines
        elif isinstance(self.TEMPLATE, list):
            lines = self.TEMPLATE
        else:
            raise TypeError("TEMPLATE must be a string or a list of strings.")
        # Detect variables from all lines
        detected_vars = set()
        for line in lines:
            matches = variable_pattern.findall(line)
            for match in matches:
                variable_name = match[0]  # Base variable name
                index = match[1]          # Optional index (e.g., '[i]')
                if only_indexed and not index:
                    continue  # Skip non-indexed variables if targeting only indexed ones
                if with_index and index:
                    detected_vars.add(f"{variable_name}{index}")  # Include the full indexed variable
                elif not with_index:
                    detected_vars.add(variable_name)  # Include only the base variable

        # Return the list of unique variables
        return list(detected_vars)

Subclasses

Class variables

var DEFINITIONS
var SECTIONS
var TEMPLATE
var description
var email
var license
var metadata
var name
var position
var section
var type
var userid
var verbose
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)))

Instance variables

var role

convert section index into a role (section name)

Expand source code
@property
def role(self):
    """ convert section index into a role (section name) """
    if self.section in range(len(self.SECTIONS)):
        return self.SECTIONS[self.section]
    else:
        return ""

Methods

def detect_variables(self, with_index=False, only_indexed=False)

Detects variables in the content of the template using an extended pattern to include indexed variables (e.g., ${var[i]}) if with_index is True.

Parameters:

with_index : bool, optional If True, include indexed variables with their indices (e.g., ${var[i]}). Default is False. only_indexed : bool, optional If True, target only variables that are used with an index (e.g., ${var[i]}). Default is False.

Returns:

list A list of unique variable names detected in the content based on the flags.

Expand source code
def detect_variables(self, with_index=False, only_indexed=False):
    """
    Detects variables in the content of the template using an extended pattern
    to include indexed variables (e.g., ${var[i]}) if `with_index` is True.

    Parameters:
    -----------
    with_index : bool, optional
        If True, include indexed variables with their indices (e.g., ${var[i]}). Default is False.
    only_indexed : bool, optional
        If True, target only variables that are used with an index (e.g., ${var[i]}). Default is False.

    Returns:
    --------
    list
        A list of unique variable names detected in the content based on the flags.
    """
    # Regular expression to match variables with optional indexing
    variable_pattern = re.compile(r'\$\{(\w+)(\[\w+\])?\}')
    # Ensure TEMPLATE is iterable (split string into lines if needed)
    if isinstance(self.TEMPLATE, str):
        lines = self.TEMPLATE.splitlines()  # Split string into lines
    elif isinstance(self.TEMPLATE, list):
        lines = self.TEMPLATE
    else:
        raise TypeError("TEMPLATE must be a string or a list of strings.")
    # Detect variables from all lines
    detected_vars = set()
    for line in lines:
        matches = variable_pattern.findall(line)
        for match in matches:
            variable_name = match[0]  # Base variable name
            index = match[1]          # Optional index (e.g., '[i]')
            if only_indexed and not index:
                continue  # Skip non-indexed variables if targeting only indexed ones
            if with_index and index:
                detected_vars.add(f"{variable_name}{index}")  # Include the full indexed variable
            elif not with_index:
                detected_vars.add(variable_name)  # Include only the base variable

    # Return the list of unique variables
    return list(detected_vars)
def do(self, printflag=None, verbose=None)

Generate the LAMMPS script based on the current configuration.

    This method generates a LAMMPS-compatible script from the templates and definitions
    stored in the <code><a title="script.script" href="#script.script">script</a></code> object. The generated script can be displayed, returned,
    and optionally include comments for debugging or clarity.

    Parameters:
    -----------
    - printflag (bool, optional): If True, the generated script is printed to the console.
                                  Default is True.
    - verbose (bool, optional): If True, comments and additional information are included
                                in the generated script. If False, comments are removed.
                                Default is True.

    Returns:
    ---------
    - str: The generated LAMMPS script.

    Method Behavior:
    - The method first collects all key-value pairs from <code>DEFINITIONS</code> and <code>USER</code> objects,
      which store the configuration data for the script.
    - Lists and tuples in the collected data are formatted into a readable string with proper
      separators (space for lists, comma for tuples) and prefixed with a '%' to indicate comments.
    - The generated command template is formatted and evaluated using the collected data.
    - If <code>verbose</code> is set to False, comments in the generated script are removed.
    - The script is then printed if <code>printflag</code> is True.
    - Finally, the formatted script is returned as a string.


    Example Usage:
    --------------
    >>> s = script()
    >>> s.do(printflag=True, verbose=True)
    units           si
    dimension       3
    boundary        f f f
    # Additional script commands...

    >>> s.do(printflag=False, verbose=False)
    'units si

dimension 3 boundary f f f

Additional script commands…'

    Notes:
    - Comments are indicated in the script with '%' or '#'.
    - The [position {self.position}:{self.userid}] marker is inserted for tracking
      script sections or modifications.


    Known Issues for indexed variables
    ----------------------------------
    List and tupples used indexed in templates, such as varlist[1], are not converted into strings so that
    each element can be considered as a variable and accessed as ${varlist[1]}. If varlist is also used in
    full, such as ${varlist}. Then it is preferable to define varlist as a string:
        "![v1,v2,...]" where v1,v2,v3 can be int, float, static str or evaluable str (i.e., subjected to nested
         evaluation).
    The control character ! in lists, such as in var=![v1,v2,v3] preserves the Pythonic definition by do()
    at later stages during the evaluation so that its content can be indexed.
Expand source code
def do(self,printflag=None,verbose=None):
    """
    Generate the LAMMPS script based on the current configuration.

    This method generates a LAMMPS-compatible script from the templates and definitions
    stored in the `script` object. The generated script can be displayed, returned,
    and optionally include comments for debugging or clarity.

    Parameters:
    -----------
    - printflag (bool, optional): If True, the generated script is printed to the console.
                                  Default is True.
    - verbose (bool, optional): If True, comments and additional information are included
                                in the generated script. If False, comments are removed.
                                Default is True.

    Returns:
    ---------
    - str: The generated LAMMPS script.

    Method Behavior:
    - The method first collects all key-value pairs from `DEFINITIONS` and `USER` objects,
      which store the configuration data for the script.
    - Lists and tuples in the collected data are formatted into a readable string with proper
      separators (space for lists, comma for tuples) and prefixed with a '%' to indicate comments.
    - The generated command template is formatted and evaluated using the collected data.
    - If `verbose` is set to False, comments in the generated script are removed.
    - The script is then printed if `printflag` is True.
    - Finally, the formatted script is returned as a string.


    Example Usage:
    --------------
    >>> s = script()
    >>> s.do(printflag=True, verbose=True)
    units           si
    dimension       3
    boundary        f f f
    # Additional script commands...

    >>> s.do(printflag=False, verbose=False)
    'units si\ndimension 3\nboundary f f f\n# Additional script commands...'

    Notes:
    - Comments are indicated in the script with '%' or '#'.
    - The [position {self.position}:{self.userid}] marker is inserted for tracking
      script sections or modifications.


    Known Issues for indexed variables
    ----------------------------------
    List and tupples used indexed in templates, such as varlist[1], are not converted into strings so that
    each element can be considered as a variable and accessed as ${varlist[1]}. If varlist is also used in
    full, such as ${varlist}. Then it is preferable to define varlist as a string:
        "![v1,v2,...]" where v1,v2,v3 can be int, float, static str or evaluable str (i.e., subjected to nested
         evaluation).
    The control character ! in lists, such as in var=![v1,v2,v3] preserves the Pythonic definition by do()
    at later stages during the evaluation so that its content can be indexed.

    """
    printflag = self.printflag if printflag is None else printflag
    verbose = self.verbose if verbose is None else verbose
    inputs = self.DEFINITIONS + self.USER
    usedvariables = self.detect_variables(with_index=False,only_indexed=False)
    variables_used_with_index = self.detect_variables(with_index=False,only_indexed=True)
    usedvariables_withoutindex = [ var for var in usedvariables if var not in variables_used_with_index ]
    for k in inputs.keys():
        if k in usedvariables_withoutindex:
            if isinstance(inputs.getattr(k),list):
                inputs.setattr(k,"% "+span(inputs.getattr(k)))
            elif isinstance(inputs.getattr(k),tuple):
                inputs.setattr(k,"% "+span(inputs.getattr(k),sep=","))
    cmd = inputs.formateval(self.TEMPLATE)
    cmd = cmd.replace("[comment]",f"[position {self.position}:{self.userid}]")
    if not verbose: cmd=remove_comments(cmd)
    if printflag: print(cmd)
    return cmd
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 header(self, verbose=True, verbosity=None, style=2)

Generate a formatted header for the script file.

Parameters

verbosity (bool, optional): If specified, overrides the instance's verbose setting. Defaults to the instance's verbose. style (int from 1 to 6, optional): ASCII style to frame the header (default=2)

Returns

str
A formatted string representing the script's metadata and initialization details. Returns an empty string if verbosity is False.

The header includes: - Script version, license, and contact email. - User ID and the number of initialized definitions. - Current system user, hostname, and working directory. - Persistent filename and folder path. - Timestamp of the header generation.

Expand source code
def header(self, verbose=True, verbosity=None, style=2):
    """
    Generate a formatted header for the script file.

    Parameters:
        verbosity (bool, optional): If specified, overrides the instance's `verbose` setting.
                                    Defaults to the instance's `verbose`.
        style (int from 1 to 6, optional): ASCII style to frame the header (default=2)

    Returns:
        str: A formatted string representing the script's metadata and initialization details.
             Returns an empty string if verbosity is False.

    The header includes:
        - Script version, license, and contact email.
        - User ID and the number of initialized definitions.
        - Current system user, hostname, and working directory.
        - Persistent filename and folder path.
        - Timestamp of the header generation.
    """
    verbose = verbosity > 0 if verbosity is not None else (self.verbose if verbose is None else verbose)
    verbosity = 0 if not verbose else verbosity
    if not verbose:
        return ""
    # Prepare the header content
    lines = [
        f"PIZZA.SCRIPT FILE v{script.version} | License: {script.license} | Email: {script.email}",
        "",
        f"<{str(self)}>",
        f"Initialized with {len(self.USER)} definitions | Verbosity: {verbosity}",
        f"Persistent file: \"{self.persistentfile}\" | Folder: \"{self.persistentfolder}\"",
        "",
        f"Generated on: {getpass.getuser()}@{socket.gethostname()}:{os.getcwd()}",
        f"{datetime.datetime.now().strftime('%A, %B %d, %Y at %H:%M:%S')}",
    ]
    # Use the shared method to format the header
    return frame_header(lines,style=style)
def tmpwrite(self, verbose=False, style=1)

Write the script to a temporary file and create optional persistent copies.

Parameters

verbose (bool, optional): Controls verbosity during script generation. Defaults to False.

The method: - Creates a temporary file for the script, with platform-specific behavior: - On Windows (os.name == 'nt'), the file is not automatically deleted. - On other systems, the file is temporary and deleted upon closure. - Writes a header and the script content into the temporary file. - Optionally creates a persistent copy in the self.persistentfolder directory: - script.preview.<suffix>: A persistent copy of the temporary file. - script.preview.clean.<suffix>: A clean copy with comments and empty lines removed. - Handles cleanup and exceptions gracefully to avoid leaving orphaned files. - style (int, optional): Defines the ASCII frame style for the header. Valid values are integers from 1 to 6, corresponding to predefined styles: 1. Basic box with +, -, and | 2. Double-line frame with , , and 3. Rounded corners with ., ', -, and | 4. Thick outer frame with #, =, and # 5. Box drawing characters with , , and 6. Minimalist dotted frame with ., :, and . Default is 1 (basic box).

Returns

TemporaryFile
The temporary file handle (non-Windows systems only).
None
On Windows, the file is closed and not returned.

Raises

Exception
If there is an error creating or writing to the temporary file.
Expand source code
def tmpwrite(self, verbose=False, style=1):
    """
    Write the script to a temporary file and create optional persistent copies.

    Parameters:
        verbose (bool, optional): Controls verbosity during script generation. Defaults to False.

    The method:
        - Creates a temporary file for the script, with platform-specific behavior:
            - On Windows (`os.name == 'nt'`), the file is not automatically deleted.
            - On other systems, the file is temporary and deleted upon closure.
        - Writes a header and the script content into the temporary file.
        - Optionally creates a persistent copy in the `self.persistentfolder` directory:
            - `script.preview.<suffix>`: A persistent copy of the temporary file.
            - `script.preview.clean.<suffix>`: A clean copy with comments and empty lines removed.
        - Handles cleanup and exceptions gracefully to avoid leaving orphaned files.
        - style (int, optional):
            Defines the ASCII frame style for the header.
            Valid values are integers from 1 to 6, corresponding to predefined styles:
                1. Basic box with `+`, `-`, and `|`
                2. Double-line frame with `╔`, `═`, and `║`
                3. Rounded corners with `.`, `'`, `-`, and `|`
                4. Thick outer frame with `#`, `=`, and `#`
                5. Box drawing characters with `┌`, `─`, and `│`
                6. Minimalist dotted frame with `.`, `:`, and `.`
            Default is `1` (basic box).

    Returns:
        TemporaryFile: The temporary file handle (non-Windows systems only).
        None: On Windows, the file is closed and not returned.

    Raises:
        Exception: If there is an error creating or writing to the temporary file.
    """
    try:
        # OS-specific temporary file behavior
        if os.name == 'nt':  # Windows
            ftmp = tempfile.NamedTemporaryFile(mode="w+b", prefix="script_", suffix=".txt", delete=False)
        else:  # Other platforms
            ftmp = tempfile.NamedTemporaryFile(mode="w+b", prefix="script_", suffix=".txt")

        # Generate header and content
        header = (
            f"# TEMPORARY PIZZA.SCRIPT FILE\n"
            f"# {'-' * 40}\n"
            f"{self.header(verbosity=verbose, style=style)}"
        )
        content = (
            header
            + "\n# This is a temporary file (it will be deleted automatically)\n\n"
            + self.do(printflag=False, verbose=verbose)
        )

        # Write content to the temporary file
        ftmp.write(BOM_UTF8 + content.encode('utf-8'))
        ftmp.seek(0)  # Reset file pointer to the beginning

    except Exception as e:
        # Handle errors gracefully
        ftmp.close()
        os.remove(ftmp.name)  # Clean up the temporary file
        raise Exception(f"Failed to write to or handle the temporary file: {e}") from None

    print("\nTemporary File Header:\n", header, "\n")
    print("A temporary file has been generated here:\n", ftmp.name)

    # Persistent copy creation
    if self.persistentfile:
        ftmpname = os.path.basename(ftmp.name)
        fcopyname = os.path.join(self.persistentfolder, f"script.preview.{ftmpname.rsplit('_', 1)[1]}")
        copyfile(ftmp.name, fcopyname)
        print("A persistent copy has been created here:\n", fcopyname)

        # Create a clean copy without empty lines or comments
        with open(ftmp.name, "r") as f:
            lines = f.readlines()
        bom_utf8_str = BOM_UTF8.decode("utf-8")
        clean_lines = [
            line for line in lines
            if line.strip() and not line.lstrip().startswith("#") and not line.startswith(bom_utf8_str)
        ]
        fcleanname = os.path.join(self.persistentfolder, f"script.preview.clean.{ftmpname.rsplit('_', 1)[1]}")
        with open(fcleanname, "w") as f:
            f.writelines(clean_lines)
        print("A clean copy has been created here:\n", fcleanname)

        # Handle file closure for Windows
        if os.name == 'nt':
            ftmp.close()
            return None
        else:
            return ftmp
def write(self, file, printflag=True, verbose=False, overwrite=False, style=2)

Write the script to a file.

Parameters

  • file (str): The file path where the script will be saved.
  • printflag (bool): Flag to enable/disable printing of details.
  • verbose (bool): Flag to enable/disable verbose mode.
  • overwrite (bool): Whether to overwrite the file if it already exists.
  • style (int, optional): Defines the ASCII frame style for the header. Valid values are integers from 1 to 6, corresponding to predefined styles: 1. Basic box with +, -, and | 2. Double-line frame with , , and 3. Rounded corners with ., ', -, and | 4. Thick outer frame with #, =, and # 5. Box drawing characters with , , and 6. Minimalist dotted frame with ., :, and . Default is 2 (frame with rounded corners).

Returns

str
The full absolute path of the file written.

Raises

FileExistsError
If the file already exists and overwrite is False.
Expand source code
def write(self, file, printflag=True, verbose=False, overwrite=False, style=2):
    """
    Write the script to a file.

    Parameters:
        - file (str): The file path where the script will be saved.
        - printflag (bool): Flag to enable/disable printing of details.
        - verbose (bool): Flag to enable/disable verbose mode.
        - overwrite (bool): Whether to overwrite the file if it already exists.
        - style (int, optional):
            Defines the ASCII frame style for the header.
            Valid values are integers from 1 to 6, corresponding to predefined styles:
                1. Basic box with `+`, `-`, and `|`
                2. Double-line frame with `╔`, `═`, and `║`
                3. Rounded corners with `.`, `'`, `-`, and `|`
                4. Thick outer frame with `#`, `=`, and `#`
                5. Box drawing characters with `┌`, `─`, and `│`
                6. Minimalist dotted frame with `.`, `:`, and `.`
            Default is `2` (frame with rounded corners).

    Returns:
        str: The full absolute path of the file written.

    Raises:
        FileExistsError: If the file already exists and overwrite is False.
    """
    # Resolve full path
    full_path = os.path.abspath(file)
    if os.path.exists(full_path) and not overwrite:
        raise FileExistsError(f"The file '{full_path}' already exists. Use overwrite=True to overwrite it.")
    if os.path.exists(full_path) and overwrite and verbose:
        print(f"Warning: Overwriting the existing file '{full_path}'.")
    # Generate the script and write to the file
    cmd = self.do(printflag=printflag, verbose=verbose)
    with open(full_path, "w") as f:
        print(self.header(verbosity=verbose, style=style), "\n", file=f)
        print(cmd, file=f)
    # Return the full path of the written file
    return full_path
class scriptdata (sortdefinitions=False, debug=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, use debug=True to report eval errors

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 scriptobjectgroup (*SOgroup)

scriptobjectgroup: A Class for Managing Groups of Script Objects in LAMMPS

The scriptobjectgroup class is designed to represent a group of scriptobject instances, such as beads or atoms in a simulation. This class allows users to group objects together based on their properties (e.g., beadtype, name), and provides tools to generate scripts that define interactions, groups, and forcefields for these objects in LAMMPS.

Key Features:

  • Group Management: Objects can be combined into a group, where each beadtype occurs once. The class ensures that objects are uniquely identified by their beadtype and name.
  • Dynamic Properties: The group’s properties (e.g., beadtype, name, groupname) are dynamically calculated, ensuring that the group reflects the current state of the objects.
  • Script Generation: Provides methods to generate scripts based on the group's objects, including interaction forcefields and group definitions.
  • Interaction Accumulation: Automatically accumulates and updates all forcefield interactions for the objects in the group.

Practical Use Cases:

  • LAMMPS Group Definitions: Define groups of objects for use in LAMMPS simulations, based on properties like beadtype and groupname.
  • Forcefield Management: Automatically manage and update interaction forcefields for objects in the group.
  • Script Generation: Generate LAMMPS-compatible scripts that include group definitions, input file handling, and interaction forcefields.

Methods:

init(self, *SOgroup): Initializes a new scriptobjectgroup with one or more scriptobject instances.

str(self): Returns a string representation of the scriptobjectgroup, showing the number of objects in the group and their beadtypes.

add(self, SOgroup): Combines two scriptobjectgroup instances or a scriptobject with an existing group, ensuring that beadtype values are unique.

or(self, pipe): Overloads the pipe (|) operator to integrate the group into a pipeline.

select(self, beadtype=None): Selects and returns a subset of the group based on the specified beadtype.

script(self, printflag=False, verbosity=2, verbose=None): Generates a script based on the current collection of objects, including input file handling, group definitions, and interaction forcefields.

interactions(self, printflag=False, verbosity=2, verbose=None): Updates and accumulates all forcefields for the objects in the group.

group_generator(self, name=None): Generates and returns a group object, based on the existing group structure.

Properties:

  • list : Converts the group into a sorted list of objects.
  • zip : Returns a sorted list of tuples containing beadtype, name, group, and filename for each object.
  • n : Returns the number of objects in the group.
  • beadtype : Returns a list of the beadtypes for all objects in the group.
  • name : Returns a list of the names for all objects in the group.
  • groupname : Returns a list of all group names (synonyms).
  • filename : Returns a dictionary mapping filenames to the objects that use them.
  • str : Returns a string representation of the group's beadtypes.
  • min : Returns the minimum beadtype in the group.
  • max : Returns the maximum beadtype in the group.
  • minmax : Returns a tuple of the minimum and maximum beadtypes in the group.
  • forcefield : Returns the interaction forcefields for the group.

Original Content:

The scriptobjectgroup class enables the collection and management of multiple scriptobject instances, providing the following functionalities: - Group Creation: Groups are automatically formed by combining individual objects using the + operator. Each beadtype occurs only once in the group, and errors are raised if an object with the same name or beadtype already exists. - Dynamic Properties: Properties such as beadtype, name, groupname, and filename are dynamically calculated, reflecting the current state of the objects. - Forcefield Handling: Forcefields are automatically managed for the objects in the group, including diagonal and off-diagonal terms for pair interactions. - Script Generation: Scripts are generated to define the interactions, groups, and input file handling for LAMMPS.

Example Usage:

from pizza.scriptobject import scriptobject, scriptobjectgroup, rigidwall, solidfood, water

# Define some script objects
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 objects into a group
collection = b1 + b2 + b3 + b4

# Select a subset of objects and generate a script
grp_typ1 = collection.select(1)
grpB = collection.group.B
script12 = collection.select([1, 2]).script()

Output:

script object group with 4 objects (1 2 3 4)
script

OVERVIEW:

class of script object group
    script object groups are built from script objects OBJ1, OBJ2,..
    GRP = scriptobjectgroup(OBJ1,OBJ2,...)
    GRP = OBJ1+OBJ2+...

note: each beadtype occurs once in the group (if not an error message is generated)

List of methods
    struct() converts data as structure
    select([1,2,4]) selects objects with matching beadtypes

List of properties (dynamically calculated)
    converted data: list, str, zip, beadtype, name, groupname, group, filename
    numeric: len, min, max, minmax
    forcefield related: interactions, forcefield
    script: generate the script (load,group,forcefield)

Full syntax (toy example)

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())

note: beadtype are incremented during the collection (effect of order)

    # generate a collection, select a typ 1 and a subgroup, generate the script for 1,2

    collection = b1+b2+b3+b4
    grp_typ1 = collection.select(1)
    grpB = collection.group.B
    script12 = collection.select([1,2]).script

note: collection.group.B returns a strcture with 6 fields
-----------:----------------------------------------
    groupid: 2 <-- automatic group numbering
groupidname: B <-- group name
  groupname: ['A', 'B', 'C', 'D', 'E'] <--- snonyms
   beadtype: [1, 2, 3] <-- beads belonging to B
       name: ['bead 1', 'bead 2', 'bead 3'] <-- their names
        str: group B 1 2 3 <-- LAMMPS syntax
-----------:----------------------------------------

SOG constructor

Expand source code
class scriptobjectgroup(struct):
    """
    scriptobjectgroup: A Class for Managing Groups of Script Objects in LAMMPS

    The `scriptobjectgroup` class is designed to represent a group of `scriptobject` instances,
    such as beads or atoms in a simulation. This class allows users to group objects together
    based on their properties (e.g., beadtype, name), and provides tools to generate scripts
    that define interactions, groups, and forcefields for these objects in LAMMPS.

    Key Features:
    -------------
    - **Group Management**: Objects can be combined into a group, where each `beadtype` occurs
      once. The class ensures that objects are uniquely identified by their `beadtype` and `name`.
    - **Dynamic Properties**: The group’s properties (e.g., `beadtype`, `name`, `groupname`)
      are dynamically calculated, ensuring that the group reflects the current state of the objects.
    - **Script Generation**: Provides methods to generate scripts based on the group's objects,
      including interaction forcefields and group definitions.
    - **Interaction Accumulation**: Automatically accumulates and updates all forcefield
      interactions for the objects in the group.

    Practical Use Cases:
    --------------------
    - **LAMMPS Group Definitions**: Define groups of objects for use in LAMMPS simulations,
      based on properties like `beadtype` and `groupname`.
    - **Forcefield Management**: Automatically manage and update interaction forcefields for
      objects in the group.
    - **Script Generation**: Generate LAMMPS-compatible scripts that include group definitions,
      input file handling, and interaction forcefields.

    Methods:
    --------
    __init__(self, *SOgroup):
        Initializes a new `scriptobjectgroup` with one or more `scriptobject` instances.

    __str__(self):
        Returns a string representation of the `scriptobjectgroup`, showing the number of objects
        in the group and their `beadtypes`.

    __add__(self, SOgroup):
        Combines two `scriptobjectgroup` instances or a `scriptobject` with an existing group,
        ensuring that `beadtype` values are unique.

    __or__(self, pipe):
        Overloads the pipe (`|`) operator to integrate the group into a pipeline.

    select(self, beadtype=None):
        Selects and returns a subset of the group based on the specified `beadtype`.

    script(self, printflag=False, verbosity=2, verbose=None):
        Generates a script based on the current collection of objects, including input file
        handling, group definitions, and interaction forcefields.

    interactions(self, printflag=False, verbosity=2, verbose=None):
        Updates and accumulates all forcefields for the objects in the group.

    group_generator(self, name=None):
        Generates and returns a `group` object, based on the existing group structure.

    Properties:
    -----------
    - list : Converts the group into a sorted list of objects.
    - zip : Returns a sorted list of tuples containing `beadtype`, `name`, `group`, and `filename`
      for each object.
    - n : Returns the number of objects in the group.
    - beadtype : Returns a list of the `beadtypes` for all objects in the group.
    - name : Returns a list of the `names` for all objects in the group.
    - groupname : Returns a list of all group names (synonyms).
    - filename : Returns a dictionary mapping filenames to the objects that use them.
    - str : Returns a string representation of the group's `beadtypes`.
    - min : Returns the minimum `beadtype` in the group.
    - max : Returns the maximum `beadtype` in the group.
    - minmax : Returns a tuple of the minimum and maximum `beadtypes` in the group.
    - forcefield : Returns the interaction forcefields for the group.

    Original Content:
    -----------------
    The `scriptobjectgroup` class enables the collection and management of multiple
    `scriptobject` instances, providing the following functionalities:
    - **Group Creation**: Groups are automatically formed by combining individual objects
      using the `+` operator. Each `beadtype` occurs only once in the group, and errors are
      raised if an object with the same `name` or `beadtype` already exists.
    - **Dynamic Properties**: Properties such as `beadtype`, `name`, `groupname`, and `filename`
      are dynamically calculated, reflecting the current state of the objects.
    - **Forcefield Handling**: Forcefields are automatically managed for the objects in the group,
      including diagonal and off-diagonal terms for pair interactions.
    - **Script Generation**: Scripts are generated to define the interactions, groups, and
      input file handling for LAMMPS.

    Example Usage:
    --------------
    ```
    from pizza.scriptobject import scriptobject, scriptobjectgroup, rigidwall, solidfood, water

    # Define some script objects
    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 objects into a group
    collection = b1 + b2 + b3 + b4

    # Select a subset of objects and generate a script
    grp_typ1 = collection.select(1)
    grpB = collection.group.B
    script12 = collection.select([1, 2]).script()
    ```

    Output:
    ```
    script object group with 4 objects (1 2 3 4)
    script
    ```

    OVERVIEW:
    --------------


        class of script object group
            script object groups are built from script objects OBJ1, OBJ2,..
            GRP = scriptobjectgroup(OBJ1,OBJ2,...)
            GRP = OBJ1+OBJ2+...

        note: each beadtype occurs once in the group (if not an error message is generated)

        List of methods
            struct() converts data as structure
            select([1,2,4]) selects objects with matching beadtypes

        List of properties (dynamically calculated)
            converted data: list, str, zip, beadtype, name, groupname, group, filename
            numeric: len, min, max, minmax
            forcefield related: interactions, forcefield
            script: generate the script (load,group,forcefield)

        Full syntax (toy example)

    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())

        note: beadtype are incremented during the collection (effect of order)

            # generate a collection, select a typ 1 and a subgroup, generate the script for 1,2

            collection = b1+b2+b3+b4
            grp_typ1 = collection.select(1)
            grpB = collection.group.B
            script12 = collection.select([1,2]).script

        note: collection.group.B returns a strcture with 6 fields
        -----------:----------------------------------------
            groupid: 2 <-- automatic group numbering
        groupidname: B <-- group name
          groupname: ['A', 'B', 'C', 'D', 'E'] <--- snonyms
           beadtype: [1, 2, 3] <-- beads belonging to B
               name: ['bead 1', 'bead 2', 'bead 3'] <-- their names
                str: group B 1 2 3 <-- LAMMPS syntax
        -----------:----------------------------------------

    """
    _type = "SOG"
    _fulltype = "script object group"
    _ftype = "object"
    _propertyasattribute = True

    def __init__(self,*SOgroup):
        """ SOG constructor """
        super(scriptobjectgroup,self).__init__()
        beadtypemax = 0
        names = []
        for k in range(len(SOgroup)):
            if isinstance(SOgroup[k],scriptobject):
                if SOgroup[k].beadtype<beadtypemax or SOgroup[k].beadtype==None:
                    beadtypemax +=1
                    SOgroup[k].beadtype = beadtypemax
                if SOgroup[k].name not in names:
                    self.setattr(SOgroup[k].name,SOgroup[k])
                    beadtypemax = SOgroup[k].beadtype
                else:
                    raise ValueError('the script object "%s" already exists' % SOgroup[k].name)
                names.append(SOgroup[k].name)
            else:
                raise ValueError("the argument #%d is not a script object")

    def __str__(self):
        """ string representation """
        return f"{self._fulltype} with {len(self)} {self._ftype}s ({span(self.beadtype)})"

    def __add__(self, SOgroup):
        """ overload + """
        beadlist = self.beadtype
        dup = duplicate(self)
        if isinstance(SOgroup,scriptobject):
            if SOgroup.name not in self.keys():
                if SOgroup.beadtype in beadlist and \
                  (SOgroup.beadtype==None or SOgroup.beadtype==self.min):
                      SOgroup.beadtype = self.max+1
                if SOgroup.beadtype not in beadlist:
                    dup.setattr(SOgroup.name, SOgroup)
                    beadlist.append(SOgroup.beadtype)
                    return dup
                else:
                    raise ValueError('%s (beadtype=%d) is already in use, same beadtype' \
                                     % (SOgroup.name,SOgroup.beadtype))
            else:
                raise ValueError('the object "%s" is already in the list' % SOgroup.name)
        elif isinstance(SOgroup,scriptobjectgroup):
            for k in SOgroup.keys():
                if k not in dup.keys():
                    if SOgroup.getattr(k).beadtype not in beadlist:
                        dup.setattr(k,SOgroup.getattr(k))
                        beadlist.append(SOgroup.getattr(k).beadtype)
                    else:
                        raise ValueError('%s (beadtype=%d) is already in use, same beadtype' \
                                         % (k,SOgroup.getattr(k).beadtype))
                else:
                    raise ValueError('the object "%s" is already in the list' % k)
            return dup
        else:
            raise ValueError("the argument #%d is not a script object or a script object group")

    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")

    @property
    def list(self):
        """ convert into a list """
        return sorted(self)

    @property
    def zip(self):
        """ zip beadtypes and names """
        return sorted( \
            [(self.getattr(k).beadtype,self.getattr(k).name,self.getattr(k).group,self.getattr(k).filename) \
            for k in self.keys()])

    @property
    def n(self):
        """ returns the number of bead types """
        return len(self)

    @property
    def beadtype(self):
        """ returns the beads in the group """
        return [x for x,_,_,_ in self.zip]

    @property
    def name(self):
        """ "return the list of names """
        return [x for _,x,_,_ in self.zip]

    @property
    def groupname(self):
        """ "return the list of groupnames """
        grp = []
        for _,_,glist,_ in self.zip:
            for g in glist:
                if g not in grp: grp.append(g)
        return grp

    @property
    def filename(self):
        """ "return the list of names as a dictionary """
        files = {}
        for _,n,_,fn in self.zip:
            if fn != "":
                if fn not in files:
                    files[fn] = [n]
                else:
                    files[fn].append(n)
        return files

    @property
    def str(self):
        return span(self.beadtype)

    def struct(self,groupid=1,groupidname="undef"):
        """ create a group with name """
        return struct(
                groupid = groupid,
            groupidname = groupidname,
              groupname = self.groupname, # meaning is synonyms
               beadtype = self.beadtype,
                   name = self.name,
                    str = "group %s %s" % (groupidname, span(self.beadtype))
               )

    @property
    def minmax(self):
        """ returns the min,max of beadtype """
        return self.min,self.max

    @property
    def min(self):
        """ returns the min of beadtype """
        return min(self.beadtype)

    @property
    def max(self):
        """ returns the max of beadtype """
        return max(self.beadtype)

    def select(self,beadtype=None):
        """ select bead from a keep beadlist """
        if beadtype==None: beadtype = list(range(self.min,self.max+1))
        if not isinstance(beadtype,(list,tuple)): beadtype = [beadtype]
        dup = scriptobjectgroup()
        for b,n,_,_ in self.zip:
            if b in beadtype:
                dup = dup + self.getattr(n)
                dup.getattr(n).USER = self.getattr(n).USER
                dup.getattr(n).forcefield = self.getattr(n).forcefield
        return dup

    @property
    def group(self):
        """ build groups from group (groupname contains synonyms) """
        groupdef = struct()
        gid = 0
        bng = self.zip
        for g in self.groupname:
            gid +=1
            b =[x for x,_,gx,_ in bng if g in gx]
            groupdef.setattr(g,self.select(b).struct(groupid = gid, groupidname = g))
        return groupdef

    @CallableScript
    def interactions(self, printflag=False, verbosity=2, verbose=None):
        """ update and accumulate all forcefields """
        verbosity = 0 if verbose is False else verbosity
        FF = []
        for b in self.beadtype:
            selection = deepduplicate(self.select(b)[0])
            selection.forcefield.beadtype = selection.beadtype
            selection.forcefield.userid = selection.name
            FF.append(selection.forcefield)
        # initialize interactions with pair_style
        TEMPLATE = "\n# ===== [ BEGIN FORCEFIELD SECTION ] "+"="*80 if verbosity>0 else ""
        TEMPLATE = FF[0].pair_style(verbose=verbosity>0)
        # pair diagonal terms
        for i in range(len(FF)):
            TEMPLATE += FF[i].pair_diagcoeff(verbose=verbosity>0)
        # pair off-diagonal terms
        for j in range(1,len(FF)):
            for i in range(0,j):
                TEMPLATE += FF[i].pair_offdiagcoeff(o=FF[j],verbose=verbosity>0)
        # end
        TEMPLATE += "\n# ===== [ END FORCEFIELD SECTION ] "+"="*82+"\n"  if verbosity>0 else ""
        return FF,TEMPLATE

    @property
    def forcefield(self):
        """ interaction forcefields """
        FF,_ = self.interactions
        return FF

    @CallableScript
    def script(self, printflag=False, verbosity=None, verbose=None):
        """
            Generate a script based on the current collection of script objects

            Parameters:
            -----------
            printflag : bool, optional, default=False
                If True, prints the generated script.
            verbosity (int, optional): Controls the level of detail in the generated script.
                - 0: Minimal output, no comments.
                - 1: Basic comments for run steps.
                - 2: Detailed comments with additional information.
                Default is 2

            Returns:
            --------
            script
                The generated script describing the interactions between script objects.
        """
        printflag = self.printflag if printflag is None else printflag
        verbose = verbosity > 0 if verbosity is not None else (self.verbose if verbose is None else verbose)
        verbosity = 0 if verbose is False else verbosity
        TEMPFILES = ""
        isfirst = True
        files_added = False
        if self.filename:
            for fn, cfn in self.filename.items():
                if fn and cfn:
                    if not files_added:
                        files_added = True
                        TEMPFILES += "\n# ===== [ BEGIN INPUT FILES SECTION ] " + "=" * 79 + "\n" if verbosity>0 else ""
                    TEMPFILES += span(cfn, sep=", ", left="\n# load files for objects: ", right="\n") if verbosity>1 else ""
                    if isfirst:
                        isfirst = False
                        TEMPFILES += f"\tread_data {fn}\n"  # First file, no append
                    else:
                        TEMPFILES += f"\tread_data {fn} add append\n"  # Subsequent files, append
        # define groups
        TEMPGRP = "\n# ===== [ BEGIN GROUP SECTION ] "+"="*85 + "\n" if verbosity>0 else ""
        for g in self.group:
            TEMPGRP += f'\n\t#\tDefinition of group {g.groupid}:{g.groupidname}\n' if verbosity>1 else ""
            TEMPGRP += f'\t#\t={span(g.name,sep=", ")}\n' if verbosity>1 else ""
            TEMPGRP += f'\t#\tSimilar groups: {span(g.groupname,sep=", ")}\n' if verbosity>1 else ""
            TEMPGRP += f'\tgroup \t {g.groupidname} \ttype \t {span(g.beadtype)}\n'
        TEMPGRP += "\n# ===== [ END GROUP SECTION ] "+"="*87+"\n\n" if verbosity>0 else ""
        # define interactions
        _,TEMPFF = self.interactions(printflag=printflag, verbosity=verbosity)
        # chain strings into a script
        tscript = script(printflag=False,verbose=verbosity>1)
        tscript.name = "scriptobject script"        # name
        tscript.description = str(self)             # description
        tscript.userid = "scriptobject"             # user name
        tscript.TEMPLATE = TEMPFILES+TEMPGRP+TEMPFF
        if verbosity==0:
            tscript.TEMPLATE = remove_comments(tscript.TEMPLATE)
        if printflag:
            repr(tscript)
        return tscript

    def group_generator(self, name=None):
        """
        Generate and return a group object.

        This method creates a new `group` object, optionally with a specified name.
        If no name is provided, it generates a default name based on the current
        instance's `name` attribute, formatted with the `span` function. The method
        then iterates through the existing groups in `self.group`, adding each group
        to the new `group` object based on its `groupidname` and `beadtype`.

        Parameters:
        -----------
        name : str, optional
            The name for the generated group object. If not provided, a default name
            is generated based on the current instance's `name`.

        Returns:
        --------
        group
            A newly created `group` object with criteria set based on the existing groups.
        """
        from pizza.group import group
        # Use the provided name or generate a default name using the span function
        G = group(name=name if name is not None else span(self.name, ",", "[", "]"))
        # Add criteria for each group in self.group
        for g in self.group:
            G.add_group_criteria(g.groupidname, type=g.beadtype)
        return G


    def mass(self, name=None, default_mass="${mass}", printflag=False, verbosity=2, verbose=True):
        """
        Generates LAMMPS mass commands for each unique beadtype in the collection.

        The method iterates through all `scriptobjectgroup` instances in the collection,
        collects unique beadtypes, and ensures that each beadtype has a consistent mass.
        If a beadtype has `mass=None`, it assigns a default mass as specified by `default_mass`.

        ### Parameters:
            name (str, optional):
                The name to assign to the resulting `script` object. Defaults to a generated name.
            default_mass (str, int, or float, optional):
                The default mass value to assign when a beadtype's mass is `None`.
                Can be a string, integer, or floating-point value. Defaults to `"${mass}"`.
            printflag (bool, optional):
                If `True`, prints the representation of the resulting `script` object. Defaults to `False`.
            verbosity (int, optional):
                The verbosity level for logging or debugging. Higher values indicate more detailed output.
                Defaults to `2`.
            verbose (bool, optional):
                If `True`, includes a comment header in the output. Overrides `verbosity` when `False`.
                Defaults to `True`.

        ### Returns:
            script: A `script` object containing the mass commands for each beadtype, formatted as follows:
                     ```
                     mass 1 1.0
                     mass 2 ${mass}
                     mass 3 2.5
                     ```
                     The `TEMPLATE` attribute of the `script` object holds the formatted mass commands as a single string.

        ### Raises:
            ValueError: If a beadtype has inconsistent mass values across different `scriptobjectgroup` instances.

        ### Example:
            ```python
            # Create scriptobjectgroup instances
            obj1 = scriptobjectgroup(beadtype=1, group=["all", "A"], mass=1.0)
            obj2 = scriptobjectgroup(beadtype=2, group=["all", "B", "C"])
            obj3 = scriptobjectgroup(beadtype=3, group="C", mass=2.5)

            # Initialize a script group with the scriptobjectgroup instances
            G = scriptobjectgroup([obj1, obj2, obj3])

            # Generate mass commands
            M = G.mass()
            print(M.do())
            ```

            **Output:**
            ```
            # <script:group:mass> definitions for 3 beads
            mass 1 1.0
            mass 2 ${mass}
            mass 3 2.5
            ```
        """
        verbosity = 0 if verbose is False else verbosity
        beadtype_mass = {}
        for iobj in range(0,len(self)):
            obj = self[iobj]
            bt = obj.beadtype
            mass = obj.mass if obj.mass is not None else default_mass
            if bt in beadtype_mass:
                if beadtype_mass[bt] != mass:
                    raise ValueError(
                        f"Inconsistent masses for beadtype {bt}: {beadtype_mass[bt]} vs {mass}"
                    )
            else:
                beadtype_mass[bt] = mass
        # Sort beadtypes for consistent ordering
        sorted_beadtypes = sorted(beadtype_mass.keys())
        # Generate mass commands
        lines = [f"mass {bt} {beadtype_mass[bt]}" for bt in sorted_beadtypes]
        # return a script object
        nameid = f"<script:group:{self.name}:mass>"
        description = f"{nameid} definitions for {len(self)} beads"
        if verbose:
            lines.insert(0, "# "+description)
        mscript = script(printflag=False,verbose=verbosity>1)
        mscript.name = nameid if name is None else name
        mscript.description = description
        mscript.userid = "scriptobject"             # user name
        mscript.TEMPLATE = "\n".join(lines)
        mscript.DEFINITIONS.mass = default_mass
        if printflag:
            repr(mscript)
        return mscript

Ancestors

  • pizza.private.mstruct.struct

Instance variables

var beadtype

returns the beads in the group

Expand source code
@property
def beadtype(self):
    """ returns the beads in the group """
    return [x for x,_,_,_ in self.zip]
var filename

"return the list of names as a dictionary

Expand source code
@property
def filename(self):
    """ "return the list of names as a dictionary """
    files = {}
    for _,n,_,fn in self.zip:
        if fn != "":
            if fn not in files:
                files[fn] = [n]
            else:
                files[fn].append(n)
    return files
var forcefield

interaction forcefields

Expand source code
@property
def forcefield(self):
    """ interaction forcefields """
    FF,_ = self.interactions
    return FF
var group

build groups from group (groupname contains synonyms)

Expand source code
@property
def group(self):
    """ build groups from group (groupname contains synonyms) """
    groupdef = struct()
    gid = 0
    bng = self.zip
    for g in self.groupname:
        gid +=1
        b =[x for x,_,gx,_ in bng if g in gx]
        groupdef.setattr(g,self.select(b).struct(groupid = gid, groupidname = g))
    return groupdef
var groupname

"return the list of groupnames

Expand source code
@property
def groupname(self):
    """ "return the list of groupnames """
    grp = []
    for _,_,glist,_ in self.zip:
        for g in glist:
            if g not in grp: grp.append(g)
    return grp
var list

convert into a list

Expand source code
@property
def list(self):
    """ convert into a list """
    return sorted(self)
var max

returns the max of beadtype

Expand source code
@property
def max(self):
    """ returns the max of beadtype """
    return max(self.beadtype)
var min

returns the min of beadtype

Expand source code
@property
def min(self):
    """ returns the min of beadtype """
    return min(self.beadtype)
var minmax

returns the min,max of beadtype

Expand source code
@property
def minmax(self):
    """ returns the min,max of beadtype """
    return self.min,self.max
var n

returns the number of bead types

Expand source code
@property
def n(self):
    """ returns the number of bead types """
    return len(self)
var name

"return the list of names

Expand source code
@property
def name(self):
    """ "return the list of names """
    return [x for _,x,_,_ in self.zip]
var str
Expand source code
@property
def str(self):
    return span(self.beadtype)
var zip

zip beadtypes and names

Expand source code
@property
def zip(self):
    """ zip beadtypes and names """
    return sorted( \
        [(self.getattr(k).beadtype,self.getattr(k).name,self.getattr(k).group,self.getattr(k).filename) \
        for k in self.keys()])

Methods

def group_generator(self, name=None)

Generate and return a group object.

This method creates a new group object, optionally with a specified name. If no name is provided, it generates a default name based on the current instance's name attribute, formatted with the span() function. The method then iterates through the existing groups in self.group, adding each group to the new group object based on its groupidname and beadtype.

Parameters:

name : str, optional The name for the generated group object. If not provided, a default name is generated based on the current instance's name.

Returns:

group A newly created group object with criteria set based on the existing groups.

Expand source code
def group_generator(self, name=None):
    """
    Generate and return a group object.

    This method creates a new `group` object, optionally with a specified name.
    If no name is provided, it generates a default name based on the current
    instance's `name` attribute, formatted with the `span` function. The method
    then iterates through the existing groups in `self.group`, adding each group
    to the new `group` object based on its `groupidname` and `beadtype`.

    Parameters:
    -----------
    name : str, optional
        The name for the generated group object. If not provided, a default name
        is generated based on the current instance's `name`.

    Returns:
    --------
    group
        A newly created `group` object with criteria set based on the existing groups.
    """
    from pizza.group import group
    # Use the provided name or generate a default name using the span function
    G = group(name=name if name is not None else span(self.name, ",", "[", "]"))
    # Add criteria for each group in self.group
    for g in self.group:
        G.add_group_criteria(g.groupidname, type=g.beadtype)
    return G
def interactions(printflag=False, verbosity=2, verbose=None)
Expand source code
return lambda printflag=False, verbosity=2, verbose=None: self.func(instance, printflag=printflag, verbosity=verbosity, verbose=verbose)
def mass(self, name=None, default_mass='${mass}', printflag=False, verbosity=2, verbose=True)

Generates LAMMPS mass commands for each unique beadtype in the collection.

The method iterates through all scriptobjectgroup instances in the collection, collects unique beadtypes, and ensures that each beadtype has a consistent mass. If a beadtype has mass=None, it assigns a default mass as specified by default_mass.

Parameters:

name (str, optional):
    The name to assign to the resulting <code><a title="script.script" href="#script.script">script</a></code> object. Defaults to a generated name.
default_mass (str, int, or float, optional):
    The default mass value to assign when a beadtype's mass is <code>None</code>.
    Can be a string, integer, or floating-point value. Defaults to `"${mass}"`.
printflag (bool, optional):
    If <code>True</code>, prints the representation of the resulting <code><a title="script.script" href="#script.script">script</a></code> object. Defaults to <code>False</code>.
verbosity (int, optional):
    The verbosity level for logging or debugging. Higher values indicate more detailed output.
    Defaults to <code>2</code>.
verbose (bool, optional):
    If <code>True</code>, includes a comment header in the output. Overrides <code>verbosity</code> when <code>False</code>.
    Defaults to <code>True</code>.

Returns:

script: A <code><a title="script.script" href="#script.script">script</a></code> object containing the mass commands for each beadtype, formatted as follows:
         ```
         mass 1 1.0
         mass 2 ${mass}
         mass 3 2.5
         ```
         The <code>TEMPLATE</code> attribute of the <code><a title="script.script" href="#script.script">script</a></code> object holds the formatted mass commands as a single string.

Raises:

ValueError: If a beadtype has inconsistent mass values across different <code><a title="script.scriptobjectgroup" href="#script.scriptobjectgroup">scriptobjectgroup</a></code> instances.

Example:

```python
# Create scriptobjectgroup instances
obj1 = scriptobjectgroup(beadtype=1, group=["all", "A"], mass=1.0)
obj2 = scriptobjectgroup(beadtype=2, group=["all", "B", "C"])
obj3 = scriptobjectgroup(beadtype=3, group="C", mass=2.5)

# Initialize a script group with the scriptobjectgroup instances
G = scriptobjectgroup([obj1, obj2, obj3])

# Generate mass commands
M = G.mass()
print(M.do())
```

**Output:**
```
# <script:group:mass> definitions for 3 beads
mass 1 1.0
mass 2 ${mass}
mass 3 2.5
```
Expand source code
def mass(self, name=None, default_mass="${mass}", printflag=False, verbosity=2, verbose=True):
    """
    Generates LAMMPS mass commands for each unique beadtype in the collection.

    The method iterates through all `scriptobjectgroup` instances in the collection,
    collects unique beadtypes, and ensures that each beadtype has a consistent mass.
    If a beadtype has `mass=None`, it assigns a default mass as specified by `default_mass`.

    ### Parameters:
        name (str, optional):
            The name to assign to the resulting `script` object. Defaults to a generated name.
        default_mass (str, int, or float, optional):
            The default mass value to assign when a beadtype's mass is `None`.
            Can be a string, integer, or floating-point value. Defaults to `"${mass}"`.
        printflag (bool, optional):
            If `True`, prints the representation of the resulting `script` object. Defaults to `False`.
        verbosity (int, optional):
            The verbosity level for logging or debugging. Higher values indicate more detailed output.
            Defaults to `2`.
        verbose (bool, optional):
            If `True`, includes a comment header in the output. Overrides `verbosity` when `False`.
            Defaults to `True`.

    ### Returns:
        script: A `script` object containing the mass commands for each beadtype, formatted as follows:
                 ```
                 mass 1 1.0
                 mass 2 ${mass}
                 mass 3 2.5
                 ```
                 The `TEMPLATE` attribute of the `script` object holds the formatted mass commands as a single string.

    ### Raises:
        ValueError: If a beadtype has inconsistent mass values across different `scriptobjectgroup` instances.

    ### Example:
        ```python
        # Create scriptobjectgroup instances
        obj1 = scriptobjectgroup(beadtype=1, group=["all", "A"], mass=1.0)
        obj2 = scriptobjectgroup(beadtype=2, group=["all", "B", "C"])
        obj3 = scriptobjectgroup(beadtype=3, group="C", mass=2.5)

        # Initialize a script group with the scriptobjectgroup instances
        G = scriptobjectgroup([obj1, obj2, obj3])

        # Generate mass commands
        M = G.mass()
        print(M.do())
        ```

        **Output:**
        ```
        # <script:group:mass> definitions for 3 beads
        mass 1 1.0
        mass 2 ${mass}
        mass 3 2.5
        ```
    """
    verbosity = 0 if verbose is False else verbosity
    beadtype_mass = {}
    for iobj in range(0,len(self)):
        obj = self[iobj]
        bt = obj.beadtype
        mass = obj.mass if obj.mass is not None else default_mass
        if bt in beadtype_mass:
            if beadtype_mass[bt] != mass:
                raise ValueError(
                    f"Inconsistent masses for beadtype {bt}: {beadtype_mass[bt]} vs {mass}"
                )
        else:
            beadtype_mass[bt] = mass
    # Sort beadtypes for consistent ordering
    sorted_beadtypes = sorted(beadtype_mass.keys())
    # Generate mass commands
    lines = [f"mass {bt} {beadtype_mass[bt]}" for bt in sorted_beadtypes]
    # return a script object
    nameid = f"<script:group:{self.name}:mass>"
    description = f"{nameid} definitions for {len(self)} beads"
    if verbose:
        lines.insert(0, "# "+description)
    mscript = script(printflag=False,verbose=verbosity>1)
    mscript.name = nameid if name is None else name
    mscript.description = description
    mscript.userid = "scriptobject"             # user name
    mscript.TEMPLATE = "\n".join(lines)
    mscript.DEFINITIONS.mass = default_mass
    if printflag:
        repr(mscript)
    return mscript
def script(printflag=False, verbosity=2, verbose=None)
Expand source code
return lambda printflag=False, verbosity=2, verbose=None: self.func(instance, printflag=printflag, verbosity=verbosity, verbose=verbose)
def select(self, beadtype=None)

select bead from a keep beadlist

Expand source code
def select(self,beadtype=None):
    """ select bead from a keep beadlist """
    if beadtype==None: beadtype = list(range(self.min,self.max+1))
    if not isinstance(beadtype,(list,tuple)): beadtype = [beadtype]
    dup = scriptobjectgroup()
    for b,n,_,_ in self.zip:
        if b in beadtype:
            dup = dup + self.getattr(n)
            dup.getattr(n).USER = self.getattr(n).USER
            dup.getattr(n).forcefield = self.getattr(n).forcefield
    return dup
def struct(self, groupid=1, groupidname='undef')

create a group with name

Expand source code
def struct(self,groupid=1,groupidname="undef"):
    """ create a group with name """
    return struct(
            groupid = groupid,
        groupidname = groupidname,
          groupname = self.groupname, # meaning is synonyms
           beadtype = self.beadtype,
               name = self.name,
                str = "group %s %s" % (groupidname, span(self.beadtype))
           )
class smd

SMD forcefield

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

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

Ancestors

  • pizza.forcefield.forcefield

Subclasses

  • pizza.forcefield.none
  • pizza.forcefield.tlsph
  • pizza.forcefield.ulsph
  • pizza.forcefield.ulsphalone

Class variables

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

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

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

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

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

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

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

Ancestors

  • pizza.forcefield.tlsph
  • pizza.forcefield.smd
  • pizza.forcefield.forcefield

Class variables

var description
var name
var userid
var version
class statussection (persistentfile=True, persistentfolder=None, printflag=False, verbose=False, verbosity=None, **userdefinitions)

LAMMPS script: status session

constructor adding instance definitions stored in USER

Expand source code
class statussection(script):
    """ LAMMPS script: status session """
    name = "status"
    description = name+" section"
    position = 8
    section = 9
    userid = "example"
    version = 0.1

    DEFINITIONS = scriptdata(
        thermo = 100
        )

    TEMPLATE = """
# :STATUS SECTION:
#   Status configuration

    ####################################################################################################
    # STATUS OUTPUT
    ####################################################################################################
    compute         alleint all reduce sum c_eint
    variable        etot equal pe+ke+c_alleint+f_gfix # total energy of the system
    thermo          ${thermo}
    thermo_style    custom step ke pe v_etot c_alleint f_dtfix dt
    thermo_modify   lost ignore

    """

Ancestors

Class variables

var DEFINITIONS
var TEMPLATE
var description
var name
var position
var section
var userid
var version

Inherited members

class struct (debug=False, **kwargs)

Class: struct

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


Features

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

Examples

Basic Usage

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

Using param for Evaluation

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

Concatenation and Subtraction

Fields from the right-most structure overwrite existing values.

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

Practical Shorthands

Constructing a Structure from Keys

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

Building a Structure from Variables in a String

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

Indexing and Iteration

Structures can be indexed or sliced like lists.

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

Dynamic Dependency Management

struct provides control over dependencies, sorting, and evaluation.

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

For dynamic evaluation, use param:

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

Overloaded Methods and Operators

Supported Operators

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

Method Overview

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

Dynamic Properties

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

constructor, use debug=True to report eval errors

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

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

    ---

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

    ---

    ### Examples

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

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

    ---

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

    ---

    ### Practical Shorthands

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

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

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

    ---

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

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

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

    ---

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

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

    ---

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

    ---
    """

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

          paramauto > param > struct

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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


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

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


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

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

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

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

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

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


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

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


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

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

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


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

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

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

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


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

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

        Protected attributes defined in _excludedattr are not updated.

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


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

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

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

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

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

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

        return sub_struct


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


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

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

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

        max_display = 10  # Maximum number of elements to display

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

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

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

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

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

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

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

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

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

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

        return formatted


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

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

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

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

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

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

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

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

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

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

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

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

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

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

            s = s1 << s2

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

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

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

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

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

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

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

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

Subclasses

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

Static methods

def dict2struct(dico, makeparam=False)

create a structure from a dictionary

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

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

Args

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

Returns

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

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

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

    max_display = 10  # Maximum number of elements to display

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

isstrexpression(string) returns true if s contains an expression

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

read the equivalent structure read(filename)

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

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

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

scan(string) scan a string for variables

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

Instance variables

var isempty

isempty is set to True for an empty structure

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

same structure with True if it is an expression

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

Methods

def check(self, default)

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

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

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

clear() delete all fields while preserving the original class

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

display method

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

optimize display

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

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

Args

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

Note

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

Returns

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

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

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

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

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

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

returns a structure from keys

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

Generate Python code of the equivalent structure.

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

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

Parameters

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

Returns

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

return all elements as iterable key, value

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

return the fields

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

sort keys by length()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

initialization

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

set field and value

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

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

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

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

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

create a dictionary from the current structure

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

convert an object struct() to param()

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

Update multiple fields at once, while protecting certain attributes.

Parameters:

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

Protected attributes defined in _excludedattr are not updated.

Usage:

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

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

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

    Protected attributes defined in _excludedattr are not updated.

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

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

Parameters:

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

Returns:

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

Raises:

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

Example:

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

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

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

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

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

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

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

return the values

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

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

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

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

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

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

zip keys and values

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

SMD:TLSPH forcefield (total Lagrangian)

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

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

Ancestors

  • pizza.forcefield.smd
  • pizza.forcefield.forcefield

Subclasses

  • pizza.forcefield.saltTLSPH
  • pizza.forcefield.solidfood

Class variables

var PAIR_DIAGCOEFF
var PAIR_OFFDIAGCOEFF
var description
var name
class tlsphalone

SMD:TLSPH forcefield (total Lagrangian)

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

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

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

Ancestors

  • pizza.forcefield.forcefield

Class variables

var PAIR_DIAGCOEFF
var PAIR_OFFDIAGCOEFF
var PAIR_STYLE
var description
var name
class ulsph

SMD:ULSPH forcefield (updated Lagrangian)

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

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

Ancestors

  • pizza.forcefield.smd
  • pizza.forcefield.forcefield

Subclasses

  • pizza.forcefield.water

Class variables

var PAIR_DIAGCOEFF
var PAIR_OFFDIAGCOEFF
var description
var name
class ulsphalone

SMD:ULSPH forcefield (updated Lagrangian)

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

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

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

Ancestors

  • pizza.forcefield.smd
  • pizza.forcefield.forcefield

Class variables

var PAIR_DIAGCOEFF
var PAIR_OFFDIAGCOEFF
var PAIR_STYLE
var description
var name
class water (beadtype=1, userid=None, USER=forcefield (FF object) with 0 parameters)

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

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

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

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

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

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

Ancestors

  • pizza.forcefield.ulsph
  • pizza.forcefield.smd
  • pizza.forcefield.forcefield

Class variables

var description
var name
var userid
var version