Module dscript

================================================================================ PIZZA.DSCRIPT Module Documentation ================================================================================

The PIZZA.DSCRIPT module is a versatile tool for dynamically generating and managing pizza.scripts, especially in conjunction with LAMMPS (Large-scale Atomic/Molecular Massively Parallel Simulator). This module provides powerful features to create complex, parameterized scripts, manage multiple sections, and flexibly concatenate scripts to form pipelines. It offers advanced control over script execution and generation.

Overview:

  • pizza.script: Use this to define reusable codelets or scriptlets stored in maintainable libraries.
  • pizza.dscript: Use this to dynamically generate codelets or scriptlets directly in your code, allowing for flexible, runtime script generation.

The output of pizza.dscript.script() is a complete script instance, similar to a standard pizza.script. These scripts can be managed like any pizza.script, allowing you to combine them using the + or | operators and integrate them into pizza.pipescripts for building complex, multi-step workflows.

Key Features:

  1. Dynamic Script Generation:
  2. pizza.dscript allows for the generation of scripts dynamically within your code. This means that codelets and scriptlets can be constructed at runtime, offering flexibility in handling complex scenarios.

  3. Combining Scripts:

  4. The generated scripts can be combined using operators like + or |, enabling you to merge multiple script sections seamlessly. This is especially useful for creating modular scripts that can be reused across different contexts or experiments.

  5. Saving and Loading Scripts:

  6. pizza.dscript.save() and pizza.dscript.load() enable you to save and load complex scripts as text templates, independent of both Python and LAMMPS. This functionality allows for the deployment of sophisticated scripts without requiring the full script class libraries (commonly referred to as "workshops" in Pizza3).

  7. Leveraging AI for Rapid Template Generation:

  8. You can even leverage Large Language Models (LLMs) to generate templates quickly. This feature allows for automation and faster script generation, particularly useful for deploying templated workflows or codelets.

  9. Non-linear Execution:

  10. It's important to note that pizza.scripts and pizza.dscripts are not directly equivalent to LAMMPS code. They can be executed statically and non-linearly, without needing LAMMPS to be involved. This makes the framework particularly useful when managing large LAMMPS simulations that span multiple submodules.

Applications:

The PIZZA.DSCRIPT module is particularly useful when you need to: - Dynamically create and manage LAMMPS script sections. - Merge, manipulate, and execute multiple script sections with predefined or user-defined variables. - Generate scripts with conditional sections and custom execution logic, enabling the handling of complex simulations and workflows.

The modular nature of pizza.dscript makes it well-suited for scenarios where you need to reuse various submodules of LAMMPS code or mix them with pure Python logic for more advanced control.

Key Classes:

  • lambdaScriptdata: Holds parameters and definitions for script execution. This class encapsulates the data and logic needed for the dynamic generation of script elements.

  • lambdaScript: Wraps a dscript object to generate a pizza.script instance from its contents. This class is essential for converting dynamically generated dscript objects into reusable, executable script instances.

  • dscript: Manages and stores multiple script lines/items as ScriptTemplate objects, supporting dynamic execution and the concatenation of script sections. The dscript class allows for flexible script construction and execution, making it a core component for generating dynamic LAMMPS scripts.

Notes:

  • Script Composition: Generated scripts can span several submodules, allowing for the reuse of script components in different contexts. This feature enables a modular approach to script generation.
  • Execution Flexibility: Since pizza.dscript scripts are not bound to LAMMPS directly, they can be executed independently, making them highly flexible for preprocessing, debugging, or running custom logic within a LAMMPS-related workflow.

Important Distinction between PIZZA.DSCRIPT and PIZZA.SCRIPT: (with possible evolution in the future)

  • PIZZA.DSCRIPT stores the DEFINITIONS in lambdaScriptdata objects based on pizza.private.mstruct.paramauto The practical consequence is that all variables should be within {} such as ${variable}. The definitions will be redordered to authorize execution
  • PIZZA.SCRIPT stores the DEFINITIONS in Scriptdata objects based on pizza.private.mstruct.param The variables can be $myvar or ${myvar}. The definition order is important.

Practical Usage:

  • Scripts can be combined dynamically.
  • Variables in script templates are handled automatically, allowing flexible script execution.
  • Concatenate multiple scripts, with automatic merging and non-overwriting of variables.

The concept of TEMPLATING is managed via shorthands

from dscript import dscript

    ```python
    # Create a dscript instance
    S = dscript(name="MyScript")

    # Define script sections
    S[0] = "instruction 1"
    S[1] = "${var1} + ${var2}"

    # Set definitions
    S.DEFINITIONS.var1 = "1"
    S.DEFINITIONS.var2 = "2"

    # Enable evaluation
    S[1].eval = True

    # Generate the script
    result = S.do()
    print(result)

    # convert it into pizza.script
    s=S.script()
    rs = s.do()
    print(rs)
    ```

Chat GPT teaching instructions: Copy and paste these instructions to teach chatGPT how to convert in LAMMPS code in pizza.dscript() -----------------------------[ start here ]--------------------------------------------------

DSCRIPT SAVE FILE Format Instructions:

Each DSCRIPT file begins with the line # DSCRIPT SAVE FILE. The file is divided into sections: Global Parameters, Definitions, Template, and Attributes. 1. Global Parameters Section: Enclosed in {} and contains key-value pairs where values can be integers, floats, strings, booleans, or lists. Example:

# GLOBAL PARAMETERS
{
    SECTIONS = ['SECTION1', 'SECTION2'],
    section = 0,
    position = 0,
    role = "dscript instance",
    description = "A description",
    userid = "dscript",
    version = 0.1,
    verbose = False
}
  1. Definitions Section: Defines variables as key-value pairs, which can reference other variables using ${}. Example:
# DEFINITIONS (number of definitions=X)
var1 = value1
var2 = "${var1}"
  1. Template Section: Contains key-value pairs for blocks of script content. Single-line content is written as key: value. Multi-line content is enclosed in square brackets []. Example:
# TEMPLATE (number of lines=X)
block1: command using ${var1}
block2: [
    multi-line command 1
    multi-line command 2
]
  1. Attributes Section: Optional attributes are attached to each block as key-value pairs inside curly braces {}. Example:
# ATTRIBUTES (number of lines with explicit attributes=X)
block1: {facultative=True, eval=True}

Definitions can be dynamically substituted into the templates using ${} notation, and the parser should handle both single-line and multi-line templates.

-----------------------------[ end here ]--------------------------------------------------



================================================================================
Production Example: Dynamic LAMMPS Script Generation Using `dscript` and `script`
================================================================================

This example illustrates how to dynamically generate and manage LAMMPS scripts
using the `dscript` and `script` classes. The goal is to demonstrate the flexibility
of these classes in handling script sections, overriding parameters, and generating
a script with conditions and dynamic content.

Overview:
---------
    In this example, we:
    - Define global `DEFINITIONS` that hold script parameters.
    - Create a script template with multiple lines/items, each identified by a unique key.
    - Add conditions to script lines/items to control their inclusion based on the state of variables.
    - Overwrite `DEFINITIONS` at runtime to customize the script's behavior.
    - Generate and execute the script using the `do()` method.

Key Classes Used:
-----------------
    - `dscript`: Manages multiple script lines and their dynamic execution.
    - `lamdaScript`: Wraps a `dscript` object to generate a script instance from its contents.

Practical Steps:
----------------
    1. Initialize a `dscript` object and define global variables (DEFINITIONS).
    2. Create script lines/items using keys to identify each line.
    3. Apply conditions to script lines/items to control their execution.
    4. Overwrite or add new variables to `DEFINITIONS` at runtime.
    5. Generate the final script using `lamdaScript` and execute it.

Example:
--------
    # Initialize the dscript object
    R = dscript(name="ProductionExample")

    # Define global variables (DEFINITIONS)
    R.DEFINITIONS.dimension = 3
    R.DEFINITIONS.units = "$si"
    R.DEFINITIONS.boundary = ["sm","sm","sm"]
    R.DEFINITIONS.atom_style = "$smd"
    R.DEFINITIONS.atom_modify = ["map","array"]
    R.DEFINITIONS.comm_modify = ["vel","yes"]
    R.DEFINITIONS.neigh_modify = ["every",10,"delay",0,"check","yes"]
    R.DEFINITIONS.newton = "$off"

    # Define the script template, with each line identified by a key
    R[0]        = "% ${comment}"
    R["dim"]    = "dimension    ${dimension}"  # Line identified as 'dim'
    R["unit"]   = "units        ${units}"      # Line identified as 'unit'
    R["bound"]  = "boundary     ${boundary}"
    R["astyle"] = "atom_style   ${atom_style}"
    R["amod"]   = "atom_modify  ${atom_modify}"
    R["cmod"]   = "comm_modify  ${comm_modify}"
    R["nmod"]   = "neigh_modify ${neigh_modify}"
    R["newton"] = "newton       ${newton}"

    # Apply a condition to the 'astyle' line; it will only be included if ${atom_style} is defined
    R["astyle"].condition = "${atom_style}"

    # Revise the DEFINITIONS, making ${atom_style} undefined
    R.DEFINITIONS.atom_style = ""

    # Generate a script instance, overwriting the 'units' variable and adding a comment
    sR = R.script(units="$lj",  # Use "$" to prevent immediate evaluation
                  comment="$my first dynamic script")

    # Execute the script to get the final content
    ssR = sR.do()

    # Print the generated script as text
    print(ssR)

    # Save your script
    R.save("myscript.txt")

    # Load again your script
    Rcopy = dsave.load("myscript.txt")

More Compact Example:
---------------------
    # Initialization
    R2 = dscript(name="ProductionExample2")
    # Define global variables (DEFINITIONS) for the script
    R2.DEFINITIONS.dimension = 3
    R2.DEFINITIONS.units = "$si"
    R2.DEFINITIONS.boundary = ["sm", "sm", "sm"]
    R2.DEFINITIONS.atom_modify = ["map", "array"]
    R2.DEFINITIONS.comm_modify = ["vel", "yes"]
    R2.DEFINITIONS.neigh_modify = ["every", 10, "delay", 0, "check", "yes"]
    R2.DEFINITIONS.newton = "$off"
    R2.DEFINITIONS.atom_style = "$smd"
    # Define the script template with a multiple line syntax
    R2["code"] = "" "
        % ${comment}
        dimension    ${dimension}
        units        ${units}
        boundary     ${boundary}
        atom_style   ${atom_style}
        atom_modify  ${atom_modify}
        comm_modify  ${comm_modify}
        neigh_modify ${neigh_modify}
        newton       ${newton}
    "" "
    # Generate a script instance, overwriting the 'units' variable and adding a comment
    sR2 = R2.script(comment="$my first compact dscript")
    ssR2 = sR2.do()
    # Print the generated script
    print(ssR2)

Expected Output:
----------------
    Depending on the conditions and overwritten variables, the output script should
    reflect the updated `DEFINITIONS` and the conditionally included lines.

For instance:
    - The line for `atom_style` will be omitted because `atom_style` is undefined.
    - The script will include the custom units and comment specified at runtime.


Some Comments:
--------------
    pizza.dscript does not not require the definition of modules, submodules and so on.
    For comparison, pizza.script requires managing directly classes

    Usage Example
    -------------
    ```python
    from pizza.script import script

    class scriptexample(script):
        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}
        "" "

    s1 = scriptexample()
    s1.do()
    ```


Load and Save features:
-----------------------
use dscript.load() and dscript.save() methods


DSCRIPT SAVE FILE Syntax:
--------------------------

A DSCRIPT SAVE FILE is a text-based representation of script instances that are dynamically loaded or saved. It consists of key sections that define global parameters, variables (definitions), script templates, and attributes. The file format is structured and flexible, allowing both minimal and extended configurations depending on the level of detail required.

### Minimal DSCRIPT File
A minimal DSCRIPT SAVE FILE includes the template section, which defines the script's core structure. The template assigns content to variables, which can then be dynamically evaluated and modified during script execution.

Example of a minimal DSCRIPT SAVE FILE:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# DSCRIPT SAVE FILE

0: % ${comment}
dim: dimension    ${dimension}
unit: units        ${units}
bound: boundary     ${boundary}
astyle: atom_style   ${atom_style}
amod: atom_modify  ${atom_modify}
cmod: comm_modify  ${comm_modify}
nmod: neigh_modify ${neigh_modify}
newton: newton       ${newton}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Example of a compact DSCRIPT SAVE FILE:
    Note the position of the []
    Use % to keep comments
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# DSCRIPT SAVE FILE

mytemplate: [
    % ${comment}
    dimension    ${dimension}
    units        ${units}
    boundary     ${boundary}
    atom_style   ${atom_style}
    atom_modify  ${atom_modify}
    comm_modify  ${comm_modify}
    neigh_modify ${neigh_modify}
    newton       ${newton}
    ]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

- **Required header**: The file must start with the line `# DSCRIPT SAVE FILE`. This is mandatory and serves to authenticate the file as a valid DSCRIPT file.
- **Template section**: Each line contains a variable key followed by the content. Variables (e.g., `${dimension}`) are placeholders that will be replaced during script execution. The format is `key: content`, where `key` is a unique identifier and `content` represents the script logic.

### Extended DSCRIPT File
An extended DSCRIPT file includes additional sections such as global parameters, definitions, templates, and attributes. These provide more control and flexibility over script behavior and configuration.

    Example of an extended DSCRIPT SAVE FILE:
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # DSCRIPT SAVE FILE
    # generated on 20XX-XX-XX on user@localhost

    #   name = "ProductionExample"
    #   path = "/your/path/ProductionExample.txt"

    # GLOBAL PARAMETERS
    {
        SECTIONS = ['DYNAMIC'],
        section = 0,
        position = 0,
        role = 'dscript instance',
        description = 'dynamic script',
        userid = 'dscript',
        version = 0.1,
        verbose = False
    }

    # DEFINITIONS (number of definitions=9)
    dimension=3
    units=$si
    boundary=['sm', 'sm', 'sm']
    atom_style=""
    atom_modify=['map', 'array']
    comm_modify=['vel', 'yes']
    neigh_modify=['every', 10, 'delay', 0, 'check', 'yes']
    newton=$off
    comment=${comment}

    # TEMPLATE (number of lines=9)
    0: % ${comment}
    dim: dimension    ${dimension}
    unit: units        ${units}
    bound: boundary     ${boundary}
    astyle: atom_style   ${atom_style}
    amod: atom_modify  ${atom_modify}
    cmod: comm_modify  ${comm_modify}
    nmod: neigh_modify ${neigh_modify}
    newton: newton       ${newton}

    # ATTRIBUTES (number of lines with explicit attributes=9)
    0:{facultative=False, eval=False, readonly=False, condition=None, condeval=False, detectvar=True}
    dim:{facultative=False, eval=False, readonly=False, condition=None, condeval=False, detectvar=True}
    unit:{facultative=False, eval=False, readonly=False, condition=None, condeval=False, detectvar=True}
    bound:{facultative=False, eval=False, readonly=False, condition=None, condeval=False, detectvar=True}
    astyle:{facultative=False, eval=False, readonly=False, condition='${atom_style}', condeval=False, detectvar=True}
    amod:{facultative=False, eval=False, readonly=False, condition=None, condeval=False, detectvar=True}
    cmod:{facultative=False, eval=False, readonly=False, condition=None, condeval=False, detectvar=True}
    nmod:{facultative=False, eval=False, readonly=False, condition=None, condeval=False, detectvar=True}
    newton:{facultative=False, eval=False, readonly=False, condition=None, condeval=False, detectvar=True}
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

### Detailed Explanation of "DSCRIPT SAVE FILE" Sections:
1. **Global Parameters**:
   - Defined between `{...}` and describe the overall configuration of the script.
   - Common parameters include `SECTIONS`, `section`, `position`, `role`, `description`, `userid`, `version`, and `verbose`.
   - These parameters help control the behavior and structure of the script instance.

2. **Definitions**:
   - The `DEFINITIONS` section lists key-value pairs, where each key is a variable, and its value can be any valid Python data type (e.g., integers, strings, lists).
   - Example: `dimension=3`, `units=$si`, `boundary=['sm', 'sm', 'sm']`.
   - Variables starting with a `$` (e.g., `$si`) are typically placeholders or dynamic variables.

3. **Template**:
   - The `TEMPLATE` section defines the actual content of the script. Each line is in the format `key: content`, where `key` is a unique identifier, and `content` includes placeholders for variables (e.g., `${dimension}`).
   - Example: `dim: dimension    ${dimension}`.
   - The template is where variables from the `DEFINITIONS` section are substituted dynamically.

4. **Attributes**:
   - The `ATTRIBUTES` section defines the properties of each template entry. Each line associates a key with a dictionary of attributes (e.g., `facultative`, `eval`, `readonly`).
   - Example: `astyle:{facultative=False, eval=False, readonly=False, condition='${atom_style}', condeval=False, detectvar=True}`.
   - The attributes control how each template line behaves (e.g., whether it's evaluated, whether it depends on a condition, etc.).

### Adding Comments:
- Comments are added by starting a line with `#`. Comments can appear anywhere in the file, and they will be ignored during the loading process.
- Example:
    ```
    # This is a comment
    dim: dimension    ${dimension}  # Another comment
    ```
- Comments are typically used for documentation purposes within the DSCRIPT file, such as describing sections or explaining template logic.

### Imperative Components:
- **Header**: The line `# DSCRIPT SAVE FILE` must be present at the beginning of the file. It authenticates the file as a valid DSCRIPT file.
- **Template**: At least one template entry must be defined, as the template represents the main script content.

### Accessory Components:
- **Variable Substitution**: Variables (e.g., `${dimension}`) must be defined in the `DEFINITIONS` section to be substituted dynamically in the template content.
- **Attributes (Optional)**: Attributes are optional but provide more control over the template behavior when specified.
readonly=False, condition=None, condeval=False, detectvar=True}.

### Flexible structure
- **Global Parameters** can be defined anywhere but in a single block
- **Template** and **Attributes** lines can be mixed together.


Production example (using last features)
------------------
```python
mydscriptfile = '''
            # GLOBAL DEFINITIONS (number of definitions=4)
            dumpfile = $dump.LAMMPS
            dumpdt = 50
            thermodt = 100
            runtime = 5000


            # TEMPLATES (number of items=24)

            # LOCAL DEFINITIONS for key '0'
            dimension = 3
            units = $si
            boundary = ['f', 'f', 'f']
            atom_style = $smd
            atom_modify = ['map', 'array']
            comm_modify = ['vel', 'yes']
            neigh_modify = ['every', 10, 'delay', 0, 'check', 'yes']
            newton = $off
            name = $SimulationBox

            0: [
                % --------------[ Initialization Header (helper) for "${name}"   ]--------------
                # set a parameter to None or "" to remove the definition
                dimension    ${dimension}
                units        ${units}
                boundary     ${boundary}
                atom_style   ${atom_style}
                atom_modify  ${atom_modify}
                comm_modify  ${comm_modify}
                neigh_modify ${neigh_modify}
                newton       ${newton}
                # ------------------------------------------
             ]

            # LOCAL DEFINITIONS for key '1'
            lattice_style = $sc
            lattice_scale = 0.0008271
            lattice_spacing = [0.0008271, 0.0008271, 0.0008271]

            1: [
                % --------------[ LatticeHeader 'helper' for "${name}"   ]--------------
                lattice ${lattice_style} ${lattice_scale} spacing ${lattice_spacing}
                # ------------------------------------------
             ]

            # LOCAL DEFINITIONS for key '2'
            xmin = -0.03
            xmax = 0.03
            ymin = -0.01
            ymax = 0.01
            zmin = -0.03
            zmax = 0.03
            nbeads = 3

            2: [
                % --------------[ Box Header 'helper' for "${name}"   ]--------------
                region box block ${xmin} ${xmax} ${ymin} ${ymax} ${zmin} ${zmax}
                create_box      ${nbeads} box
                # ------------------------------------------
             ]

            # LOCAL DEFINITIONS for key '3'
            ID = $LowerCylinder
            style = $cylinder

            3: % variables to be used for ${ID} ${style}             ]

            # LOCAL DEFINITIONS for key '4'
            ID = $CentralCylinder

            4: % variables to be used for ${ID} ${style}

            # LOCAL DEFINITIONS for key '5'
            ID = $UpperCylinder

            5: % variables to be used for ${ID} ${style}

            # LOCAL DEFINITIONS for key '6'
            args = ['z', 0.0, 0.0, 36.27130939426913, 0.0, 6.045218232378189]
            side = ""
            move = ""
            rotate = ""
            open = ""
            ID = $LowerCylinder
            units = ""

            6: [
                % Create region ${ID} ${style} args ...  (URL: https://docs.lammps.org/region.html)
                # keywords: side, units, move, rotate, open
                # values: in|out, lattice|box, v_x v_y v_z, v_theta Px Py Pz Rx Ry Rz, integer
                region ${ID} ${style} ${args} ${side}${units}${move}${rotate}${open}
             ]

            # LOCAL DEFINITIONS for key '7'
            ID = $CentralCylinder
            args = ['z', 0.0, 0.0, 36.27130939426913, 6.045218232378189, 18.135654697134566]

            7: [
                % Create region ${ID} ${style} args ...  (URL: https://docs.lammps.org/region.html)
                # keywords: side, units, move, rotate, open
                # values: in|out, lattice|box, v_x v_y v_z, v_theta Px Py Pz Rx Ry Rz, integer
                region ${ID} ${style} ${args} ${side}${units}${move}${rotate}${open}
             ]

            # LOCAL DEFINITIONS for key '8'
            ID = $UpperCylinder
            args = ['z', 0.0, 0.0, 36.27130939426913, 18.135654697134566, 24.180872929512756]

            8: [
                % Create region ${ID} ${style} args ...  (URL: https://docs.lammps.org/region.html)
                # keywords: side, units, move, rotate, open
                # values: in|out, lattice|box, v_x v_y v_z, v_theta Px Py Pz Rx Ry Rz, integer
                region ${ID} ${style} ${args} ${side}${units}${move}${rotate}${open}
             ]

            # LOCAL DEFINITIONS for key '9'
            ID = $LowerCylinder
            beadtype = 1

            9: [
                % Create atoms of type ${beadtype} for ${ID} ${style} (https://docs.lammps.org/create_atoms.html)
                create_atoms ${beadtype} region ${ID}
             ]

            # LOCAL DEFINITIONS for key '10'
            ID = $CentralCylinder
            beadtype = 2

            10: [
                % Create atoms of type ${beadtype} for ${ID} ${style} (https://docs.lammps.org/create_atoms.html)
                create_atoms ${beadtype} region ${ID}
             ]

            # LOCAL DEFINITIONS for key '11'
            ID = $UpperCylinder
            beadtype = 3

            11: [
                % Create atoms of type ${beadtype} for ${ID} ${style} (https://docs.lammps.org/create_atoms.html)
                create_atoms ${beadtype} region ${ID}
             ]

            12: [
                # ===== [ BEGIN GROUP SECTION ] =====================================================================================
                group    lower  type     1
                group    solid  type     1 2 3
                group    fixed  type     1
                group    middle         type     2
                group    movable        type     2 3
                group    upper  type     3

                # ===== [ END GROUP SECTION ] =======================================================================================


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

                # [1:b1 x 1:b1] Diagonal pair coefficient tlsph
                pair_coeff      1 1 smd/tlsph *COMMON 1000 10000.0 0.3 1.0 2.0 10.0 1000.0 &
                *STRENGTH_LINEAR_PLASTIC 1000.0 0 &
                *EOS_LINEAR &
                *END

                # [2:b2 x 2:b2] Diagonal pair coefficient tlsph
                pair_coeff      2 2 smd/tlsph *COMMON 1000 5000.0 0.3 1.0 2.0 10.0 1000.0 &
                *STRENGTH_LINEAR_PLASTIC 500.0 0 &
                *EOS_LINEAR &
                *END

                # [3:b3 x 3:b3] Diagonal pair coefficient tlsph
                pair_coeff      3 3 smd/tlsph *COMMON 1000 40000.0 0.3 1.0 2.0 10.0 1000.0 &
                *STRENGTH_LINEAR_PLASTIC 4000.0 0 &
                *EOS_LINEAR &
                *END

                # [1:b1 x 2:b2] Off-diagonal pair coefficient (generic)
                pair_coeff      1 2 smd/hertz 250.0000000000001

                # [1:b1 x 3:b3] Off-diagonal pair coefficient (generic)
                pair_coeff      1 3 smd/hertz 250.0000000000001

                # [2:b2 x 3:b3] Off-diagonal pair coefficient (generic)
                pair_coeff      2 3 smd/hertz 125.00000000000003

                # ===== [ END FORCEFIELD SECTION ] ==================================================================================
             ]

            13: [
                group all union lower middle upper
                group external subtract all middle
             ]

            14: velocity all set 0.0 0.0 0.0 units box
            15: fix fix_lower lower setforce 0.0 0.0 0.0
            16: fix move_upper upper move wiggle 0.0 0.0 ${amplitude} ${period} units box
            17: fix dtfix tlsph smd/adjust_dt ${dt}
            18: fix integration_fix tlsph smd/integrate_tlsph

            19: [
                compute S all smd/tlsph_stress
                compute E all smd/tlsph_strain
                compute nn all smd/tlsph_num_neighs
             ]

            20: [
                dump dump_id all custom ${dumpdt} ${dumpfile} id type x y z vx vy vz &
                c_S[1] c_S[2] c_S[4] c_nn &
                c_E[1] c_E[2] c_E[4] &
                vx vy vz
             ]

            21: dump_modify dump_id first yes

            22: [
                thermo ${thermodt}
                thermo_style custom step dt f_dtfix v_strain
             ]

            23: run ${runtime}
        '''
mydscript = dscript.parsesyntax(mydscriptfile,verbose=False,authentification=False)
mydscript[-1].definitions.runtime = 1000 # change local definitions of $runtime at the last step
print(mydscript.do(verbose=True))

Note that mydscript[-1].runtime = 1000 would have created the attribute runtime. Use definitions instead.

print(repr(mydscript[-1])) gives all details

Template Content | id:23 (1 line, 31 defs) <— all variables have be created also locally

run ${runtime}

Detected Variable (1 / +1 / -0)

[+] runtime <— this runtime is a variable

Template Attributes (7 attributes)

[ ] facultativ [x] eval [ ] readonly [ ] condition [ ] condeval [x] detectvar [x] runtime <— this runtime is an attribute

Dependencies

  • Python 3.x
  • LAMMPS
  • Pizza3.pizza

Installation

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

License

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

Contact

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

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

"""
================================================================================
PIZZA.DSCRIPT Module Documentation
================================================================================

The **PIZZA.DSCRIPT** module is a versatile tool for dynamically generating and managing
**pizza.scripts**, especially in conjunction with **LAMMPS** (Large-scale Atomic/Molecular
Massively Parallel Simulator). This module provides powerful features to create complex,
parameterized scripts, manage multiple sections, and flexibly concatenate scripts to form
pipelines. It offers advanced control over script execution and generation.

Overview:
---------
- **pizza.script**: Use this to define reusable codelets or scriptlets stored in maintainable libraries.
- **pizza.dscript**: Use this to dynamically generate codelets or scriptlets directly in your code,
   allowing for flexible, runtime script generation.

The output of `pizza.dscript.script()` is a complete script instance, similar to a standard pizza.script.
These scripts can be managed like any pizza.script, allowing you to combine them using the `+` or `|`
operators and integrate them into **pizza.pipescripts** for building complex, multi-step workflows.

Key Features:
-------------
1. **Dynamic Script Generation**:
   - `pizza.dscript` allows for the generation of scripts dynamically within your code. This means that codelets
     and scriptlets can be constructed at runtime, offering flexibility in handling complex scenarios.

2. **Combining Scripts**:
   - The generated scripts can be combined using operators like `+` or `|`, enabling you to merge multiple
     script sections seamlessly. This is especially useful for creating modular scripts that can be
     reused across different contexts or experiments.

3. **Saving and Loading Scripts**:
   - `pizza.dscript.save()` and `pizza.dscript.load()` enable you to save and load complex scripts as text templates,
     independent of both Python and LAMMPS. This functionality allows for the deployment of sophisticated scripts
     without requiring the full script class libraries (commonly referred to as "workshops" in Pizza3).

4. **Leveraging AI for Rapid Template Generation**:
   - You can even leverage Large Language Models (LLMs) to generate templates quickly. This feature allows for
     automation and faster script generation, particularly useful for deploying templated workflows or codelets.

5. **Non-linear Execution**:
   - It's important to note that **pizza.scripts** and **pizza.dscripts** are not directly equivalent to LAMMPS code.
     They can be executed statically and non-linearly, without needing LAMMPS to be involved. This makes the
     framework particularly useful when managing large LAMMPS simulations that span multiple submodules.

Applications:
-------------
The **PIZZA.DSCRIPT** module is particularly useful when you need to:
- **Dynamically create and manage LAMMPS script sections**.
- **Merge, manipulate, and execute multiple script sections** with predefined or user-defined variables.
- **Generate scripts with conditional sections and custom execution logic**, enabling the handling of complex simulations
  and workflows.

The modular nature of `pizza.dscript` makes it well-suited for scenarios where you need to reuse various submodules of
LAMMPS code or mix them with pure Python logic for more advanced control.

Key Classes:
------------
- **`lambdaScriptdata`**: Holds parameters and definitions for script execution. This class encapsulates the data and
  logic needed for the dynamic generation of script elements.

- **`lambdaScript`**: Wraps a `dscript` object to generate a `pizza.script instance` from its contents. This class
  is essential for converting dynamically generated `dscript` objects into reusable, executable script instances.

- **`dscript`**: Manages and stores multiple script lines/items as `ScriptTemplate` objects, supporting dynamic execution
  and the concatenation of script sections. The `dscript` class allows for flexible script construction and execution,
  making it a core component for generating dynamic LAMMPS scripts.

Notes:
------
- **Script Composition**: Generated scripts can span several submodules, allowing for the reuse of script components
  in different contexts. This feature enables a modular approach to script generation.
- **Execution Flexibility**: Since `pizza.dscript` scripts are not bound to LAMMPS directly, they can be executed
  independently, making them highly flexible for preprocessing, debugging, or running custom logic within a
  LAMMPS-related workflow.

Important Distinction between **PIZZA.DSCRIPT** and **PIZZA.SCRIPT**: (with possible evolution in the future)
---------------------------------------------------------------------
- **PIZZA.DSCRIPT** stores the DEFINITIONS in lambdaScriptdata objects based on pizza.private.mstruct.paramauto
  The practical consequence is that all variables should be within {} such as ${variable}.
  The definitions will be redordered to authorize execution
- **PIZZA.SCRIPT** stores the DEFINITIONS in Scriptdata objects based on pizza.private.mstruct.param
  The variables can be $myvar or ${myvar}. The definition order is important.


Practical Usage:
----------------
- Scripts can be combined dynamically.
- Variables in script templates are handled automatically, allowing flexible script execution.
- Concatenate multiple scripts, with automatic merging and non-overwriting of variables.

The concept of TEMPLATING is managed via shorthands

from dscript import dscript

        ```python
        # Create a dscript instance
        S = dscript(name="MyScript")

        # Define script sections
        S[0] = "instruction 1"
        S[1] = "${var1} + ${var2}"

        # Set definitions
        S.DEFINITIONS.var1 = "1"
        S.DEFINITIONS.var2 = "2"

        # Enable evaluation
        S[1].eval = True

        # Generate the script
        result = S.do()
        print(result)

        # convert it into pizza.script
        s=S.script()
        rs = s.do()
        print(rs)
        ```


Chat GPT teaching instructions:
Copy and paste these instructions to teach chatGPT how to convert in LAMMPS code in pizza.dscript()
-----------------------------[ start here ]--------------------------------------------------
# DSCRIPT SAVE FILE Format Instructions:
Each DSCRIPT file begins with the line `# DSCRIPT SAVE FILE`. The file is divided into sections: Global Parameters, Definitions, Template, and Attributes.
1. Global Parameters Section: Enclosed in `{}` and contains key-value pairs where values can be integers, floats, strings, booleans, or lists. Example:
```
# GLOBAL PARAMETERS
{
    SECTIONS = ['SECTION1', 'SECTION2'],
    section = 0,
    position = 0,
    role = "dscript instance",
    description = "A description",
    userid = "dscript",
    version = 0.1,
    verbose = False
}
```
2. Definitions Section: Defines variables as key-value pairs, which can reference other variables using `${}`. Example:
```
# DEFINITIONS (number of definitions=X)
var1 = value1
var2 = "${var1}"
```
3. Template Section: Contains key-value pairs for blocks of script content. Single-line content is written as `key: value`. Multi-line content is enclosed in square brackets `[]`. Example:
```
# TEMPLATE (number of lines=X)
block1: command using ${var1}
block2: [
    multi-line command 1
    multi-line command 2
]
```
4. Attributes Section: Optional attributes are attached to each block as key-value pairs inside curly braces `{}`. Example:
```
# ATTRIBUTES (number of lines with explicit attributes=X)
block1: {facultative=True, eval=True}
```
Definitions can be dynamically substituted into the templates using `${}` notation, and the parser should handle both single-line and multi-line templates.
```
-----------------------------[ end here ]--------------------------------------------------



================================================================================
Production Example: Dynamic LAMMPS Script Generation Using `dscript` and `script`
================================================================================

This example illustrates how to dynamically generate and manage LAMMPS scripts
using the `dscript` and `script` classes. The goal is to demonstrate the flexibility
of these classes in handling script sections, overriding parameters, and generating
a script with conditions and dynamic content.

Overview:
---------
    In this example, we:
    - Define global `DEFINITIONS` that hold script parameters.
    - Create a script template with multiple lines/items, each identified by a unique key.
    - Add conditions to script lines/items to control their inclusion based on the state of variables.
    - Overwrite `DEFINITIONS` at runtime to customize the script's behavior.
    - Generate and execute the script using the `do()` method.

Key Classes Used:
-----------------
    - `dscript`: Manages multiple script lines and their dynamic execution.
    - `lamdaScript`: Wraps a `dscript` object to generate a script instance from its contents.

Practical Steps:
----------------
    1. Initialize a `dscript` object and define global variables (DEFINITIONS).
    2. Create script lines/items using keys to identify each line.
    3. Apply conditions to script lines/items to control their execution.
    4. Overwrite or add new variables to `DEFINITIONS` at runtime.
    5. Generate the final script using `lamdaScript` and execute it.

Example:
--------
    # Initialize the dscript object
    R = dscript(name="ProductionExample")

    # Define global variables (DEFINITIONS)
    R.DEFINITIONS.dimension = 3
    R.DEFINITIONS.units = "$si"
    R.DEFINITIONS.boundary = ["sm","sm","sm"]
    R.DEFINITIONS.atom_style = "$smd"
    R.DEFINITIONS.atom_modify = ["map","array"]
    R.DEFINITIONS.comm_modify = ["vel","yes"]
    R.DEFINITIONS.neigh_modify = ["every",10,"delay",0,"check","yes"]
    R.DEFINITIONS.newton = "$off"

    # Define the script template, with each line identified by a key
    R[0]        = "% ${comment}"
    R["dim"]    = "dimension    ${dimension}"  # Line identified as 'dim'
    R["unit"]   = "units        ${units}"      # Line identified as 'unit'
    R["bound"]  = "boundary     ${boundary}"
    R["astyle"] = "atom_style   ${atom_style}"
    R["amod"]   = "atom_modify  ${atom_modify}"
    R["cmod"]   = "comm_modify  ${comm_modify}"
    R["nmod"]   = "neigh_modify ${neigh_modify}"
    R["newton"] = "newton       ${newton}"

    # Apply a condition to the 'astyle' line; it will only be included if ${atom_style} is defined
    R["astyle"].condition = "${atom_style}"

    # Revise the DEFINITIONS, making ${atom_style} undefined
    R.DEFINITIONS.atom_style = ""

    # Generate a script instance, overwriting the 'units' variable and adding a comment
    sR = R.script(units="$lj",  # Use "$" to prevent immediate evaluation
                  comment="$my first dynamic script")

    # Execute the script to get the final content
    ssR = sR.do()

    # Print the generated script as text
    print(ssR)

    # Save your script
    R.save("myscript.txt")

    # Load again your script
    Rcopy = dsave.load("myscript.txt")

More Compact Example:
---------------------
    # Initialization
    R2 = dscript(name="ProductionExample2")
    # Define global variables (DEFINITIONS) for the script
    R2.DEFINITIONS.dimension = 3
    R2.DEFINITIONS.units = "$si"
    R2.DEFINITIONS.boundary = ["sm", "sm", "sm"]
    R2.DEFINITIONS.atom_modify = ["map", "array"]
    R2.DEFINITIONS.comm_modify = ["vel", "yes"]
    R2.DEFINITIONS.neigh_modify = ["every", 10, "delay", 0, "check", "yes"]
    R2.DEFINITIONS.newton = "$off"
    R2.DEFINITIONS.atom_style = "$smd"
    # Define the script template with a multiple line syntax
    R2["code"] = "" "
        % ${comment}
        dimension    ${dimension}
        units        ${units}
        boundary     ${boundary}
        atom_style   ${atom_style}
        atom_modify  ${atom_modify}
        comm_modify  ${comm_modify}
        neigh_modify ${neigh_modify}
        newton       ${newton}
    "" "
    # Generate a script instance, overwriting the 'units' variable and adding a comment
    sR2 = R2.script(comment="$my first compact dscript")
    ssR2 = sR2.do()
    # Print the generated script
    print(ssR2)

Expected Output:
----------------
    Depending on the conditions and overwritten variables, the output script should
    reflect the updated `DEFINITIONS` and the conditionally included lines.

For instance:
    - The line for `atom_style` will be omitted because `atom_style` is undefined.
    - The script will include the custom units and comment specified at runtime.


Some Comments:
--------------
    pizza.dscript does not not require the definition of modules, submodules and so on.
    For comparison, pizza.script requires managing directly classes

    Usage Example
    -------------
    ```python
    from pizza.script import script

    class scriptexample(script):
        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}
        "" "

    s1 = scriptexample()
    s1.do()
    ```


Load and Save features:
-----------------------
use dscript.load() and dscript.save() methods


DSCRIPT SAVE FILE Syntax:
--------------------------

A DSCRIPT SAVE FILE is a text-based representation of script instances that are dynamically loaded or saved. It consists of key sections that define global parameters, variables (definitions), script templates, and attributes. The file format is structured and flexible, allowing both minimal and extended configurations depending on the level of detail required.

### Minimal DSCRIPT File
A minimal DSCRIPT SAVE FILE includes the template section, which defines the script's core structure. The template assigns content to variables, which can then be dynamically evaluated and modified during script execution.

Example of a minimal DSCRIPT SAVE FILE:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# DSCRIPT SAVE FILE

0: % ${comment}
dim: dimension    ${dimension}
unit: units        ${units}
bound: boundary     ${boundary}
astyle: atom_style   ${atom_style}
amod: atom_modify  ${atom_modify}
cmod: comm_modify  ${comm_modify}
nmod: neigh_modify ${neigh_modify}
newton: newton       ${newton}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Example of a compact DSCRIPT SAVE FILE:
    Note the position of the []
    Use % to keep comments
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# DSCRIPT SAVE FILE

mytemplate: [
    % ${comment}
    dimension    ${dimension}
    units        ${units}
    boundary     ${boundary}
    atom_style   ${atom_style}
    atom_modify  ${atom_modify}
    comm_modify  ${comm_modify}
    neigh_modify ${neigh_modify}
    newton       ${newton}
    ]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

- **Required header**: The file must start with the line `# DSCRIPT SAVE FILE`. This is mandatory and serves to authenticate the file as a valid DSCRIPT file.
- **Template section**: Each line contains a variable key followed by the content. Variables (e.g., `${dimension}`) are placeholders that will be replaced during script execution. The format is `key: content`, where `key` is a unique identifier and `content` represents the script logic.

### Extended DSCRIPT File
An extended DSCRIPT file includes additional sections such as global parameters, definitions, templates, and attributes. These provide more control and flexibility over script behavior and configuration.

    Example of an extended DSCRIPT SAVE FILE:
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # DSCRIPT SAVE FILE
    # generated on 20XX-XX-XX on user@localhost

    #   name = "ProductionExample"
    #   path = "/your/path/ProductionExample.txt"

    # GLOBAL PARAMETERS
    {
        SECTIONS = ['DYNAMIC'],
        section = 0,
        position = 0,
        role = 'dscript instance',
        description = 'dynamic script',
        userid = 'dscript',
        version = 0.1,
        verbose = False
    }

    # DEFINITIONS (number of definitions=9)
    dimension=3
    units=$si
    boundary=['sm', 'sm', 'sm']
    atom_style=""
    atom_modify=['map', 'array']
    comm_modify=['vel', 'yes']
    neigh_modify=['every', 10, 'delay', 0, 'check', 'yes']
    newton=$off
    comment=${comment}

    # TEMPLATE (number of lines=9)
    0: % ${comment}
    dim: dimension    ${dimension}
    unit: units        ${units}
    bound: boundary     ${boundary}
    astyle: atom_style   ${atom_style}
    amod: atom_modify  ${atom_modify}
    cmod: comm_modify  ${comm_modify}
    nmod: neigh_modify ${neigh_modify}
    newton: newton       ${newton}

    # ATTRIBUTES (number of lines with explicit attributes=9)
    0:{facultative=False, eval=False, readonly=False, condition=None, condeval=False, detectvar=True}
    dim:{facultative=False, eval=False, readonly=False, condition=None, condeval=False, detectvar=True}
    unit:{facultative=False, eval=False, readonly=False, condition=None, condeval=False, detectvar=True}
    bound:{facultative=False, eval=False, readonly=False, condition=None, condeval=False, detectvar=True}
    astyle:{facultative=False, eval=False, readonly=False, condition='${atom_style}', condeval=False, detectvar=True}
    amod:{facultative=False, eval=False, readonly=False, condition=None, condeval=False, detectvar=True}
    cmod:{facultative=False, eval=False, readonly=False, condition=None, condeval=False, detectvar=True}
    nmod:{facultative=False, eval=False, readonly=False, condition=None, condeval=False, detectvar=True}
    newton:{facultative=False, eval=False, readonly=False, condition=None, condeval=False, detectvar=True}
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

### Detailed Explanation of "DSCRIPT SAVE FILE" Sections:
1. **Global Parameters**:
   - Defined between `{...}` and describe the overall configuration of the script.
   - Common parameters include `SECTIONS`, `section`, `position`, `role`, `description`, `userid`, `version`, and `verbose`.
   - These parameters help control the behavior and structure of the script instance.

2. **Definitions**:
   - The `DEFINITIONS` section lists key-value pairs, where each key is a variable, and its value can be any valid Python data type (e.g., integers, strings, lists).
   - Example: `dimension=3`, `units=$si`, `boundary=['sm', 'sm', 'sm']`.
   - Variables starting with a `$` (e.g., `$si`) are typically placeholders or dynamic variables.

3. **Template**:
   - The `TEMPLATE` section defines the actual content of the script. Each line is in the format `key: content`, where `key` is a unique identifier, and `content` includes placeholders for variables (e.g., `${dimension}`).
   - Example: `dim: dimension    ${dimension}`.
   - The template is where variables from the `DEFINITIONS` section are substituted dynamically.

4. **Attributes**:
   - The `ATTRIBUTES` section defines the properties of each template entry. Each line associates a key with a dictionary of attributes (e.g., `facultative`, `eval`, `readonly`).
   - Example: `astyle:{facultative=False, eval=False, readonly=False, condition='${atom_style}', condeval=False, detectvar=True}`.
   - The attributes control how each template line behaves (e.g., whether it's evaluated, whether it depends on a condition, etc.).

### Adding Comments:
- Comments are added by starting a line with `#`. Comments can appear anywhere in the file, and they will be ignored during the loading process.
- Example:
    ```
    # This is a comment
    dim: dimension    ${dimension}  # Another comment
    ```
- Comments are typically used for documentation purposes within the DSCRIPT file, such as describing sections or explaining template logic.

### Imperative Components:
- **Header**: The line `# DSCRIPT SAVE FILE` must be present at the beginning of the file. It authenticates the file as a valid DSCRIPT file.
- **Template**: At least one template entry must be defined, as the template represents the main script content.

### Accessory Components:
- **Variable Substitution**: Variables (e.g., `${dimension}`) must be defined in the `DEFINITIONS` section to be substituted dynamically in the template content.
- **Attributes (Optional)**: Attributes are optional but provide more control over the template behavior when specified.
readonly=False, condition=None, condeval=False, detectvar=True}.

### Flexible structure
- **Global Parameters** can be defined anywhere but in a single block
- **Template** and **Attributes** lines can be mixed together.


Production example (using last features)
------------------
```python
mydscriptfile = '''
            # GLOBAL DEFINITIONS (number of definitions=4)
            dumpfile = $dump.LAMMPS
            dumpdt = 50
            thermodt = 100
            runtime = 5000


            # TEMPLATES (number of items=24)

            # LOCAL DEFINITIONS for key '0'
            dimension = 3
            units = $si
            boundary = ['f', 'f', 'f']
            atom_style = $smd
            atom_modify = ['map', 'array']
            comm_modify = ['vel', 'yes']
            neigh_modify = ['every', 10, 'delay', 0, 'check', 'yes']
            newton = $off
            name = $SimulationBox

            0: [
                % --------------[ Initialization Header (helper) for "${name}"   ]--------------
                # set a parameter to None or "" to remove the definition
                dimension    ${dimension}
                units        ${units}
                boundary     ${boundary}
                atom_style   ${atom_style}
                atom_modify  ${atom_modify}
                comm_modify  ${comm_modify}
                neigh_modify ${neigh_modify}
                newton       ${newton}
                # ------------------------------------------
             ]

            # LOCAL DEFINITIONS for key '1'
            lattice_style = $sc
            lattice_scale = 0.0008271
            lattice_spacing = [0.0008271, 0.0008271, 0.0008271]

            1: [
                % --------------[ LatticeHeader 'helper' for "${name}"   ]--------------
                lattice ${lattice_style} ${lattice_scale} spacing ${lattice_spacing}
                # ------------------------------------------
             ]

            # LOCAL DEFINITIONS for key '2'
            xmin = -0.03
            xmax = 0.03
            ymin = -0.01
            ymax = 0.01
            zmin = -0.03
            zmax = 0.03
            nbeads = 3

            2: [
                % --------------[ Box Header 'helper' for "${name}"   ]--------------
                region box block ${xmin} ${xmax} ${ymin} ${ymax} ${zmin} ${zmax}
                create_box      ${nbeads} box
                # ------------------------------------------
             ]

            # LOCAL DEFINITIONS for key '3'
            ID = $LowerCylinder
            style = $cylinder

            3: % variables to be used for ${ID} ${style}             ]

            # LOCAL DEFINITIONS for key '4'
            ID = $CentralCylinder

            4: % variables to be used for ${ID} ${style}

            # LOCAL DEFINITIONS for key '5'
            ID = $UpperCylinder

            5: % variables to be used for ${ID} ${style}

            # LOCAL DEFINITIONS for key '6'
            args = ['z', 0.0, 0.0, 36.27130939426913, 0.0, 6.045218232378189]
            side = ""
            move = ""
            rotate = ""
            open = ""
            ID = $LowerCylinder
            units = ""

            6: [
                % Create region ${ID} ${style} args ...  (URL: https://docs.lammps.org/region.html)
                # keywords: side, units, move, rotate, open
                # values: in|out, lattice|box, v_x v_y v_z, v_theta Px Py Pz Rx Ry Rz, integer
                region ${ID} ${style} ${args} ${side}${units}${move}${rotate}${open}
             ]

            # LOCAL DEFINITIONS for key '7'
            ID = $CentralCylinder
            args = ['z', 0.0, 0.0, 36.27130939426913, 6.045218232378189, 18.135654697134566]

            7: [
                % Create region ${ID} ${style} args ...  (URL: https://docs.lammps.org/region.html)
                # keywords: side, units, move, rotate, open
                # values: in|out, lattice|box, v_x v_y v_z, v_theta Px Py Pz Rx Ry Rz, integer
                region ${ID} ${style} ${args} ${side}${units}${move}${rotate}${open}
             ]

            # LOCAL DEFINITIONS for key '8'
            ID = $UpperCylinder
            args = ['z', 0.0, 0.0, 36.27130939426913, 18.135654697134566, 24.180872929512756]

            8: [
                % Create region ${ID} ${style} args ...  (URL: https://docs.lammps.org/region.html)
                # keywords: side, units, move, rotate, open
                # values: in|out, lattice|box, v_x v_y v_z, v_theta Px Py Pz Rx Ry Rz, integer
                region ${ID} ${style} ${args} ${side}${units}${move}${rotate}${open}
             ]

            # LOCAL DEFINITIONS for key '9'
            ID = $LowerCylinder
            beadtype = 1

            9: [
                % Create atoms of type ${beadtype} for ${ID} ${style} (https://docs.lammps.org/create_atoms.html)
                create_atoms ${beadtype} region ${ID}
             ]

            # LOCAL DEFINITIONS for key '10'
            ID = $CentralCylinder
            beadtype = 2

            10: [
                % Create atoms of type ${beadtype} for ${ID} ${style} (https://docs.lammps.org/create_atoms.html)
                create_atoms ${beadtype} region ${ID}
             ]

            # LOCAL DEFINITIONS for key '11'
            ID = $UpperCylinder
            beadtype = 3

            11: [
                % Create atoms of type ${beadtype} for ${ID} ${style} (https://docs.lammps.org/create_atoms.html)
                create_atoms ${beadtype} region ${ID}
             ]

            12: [
                # ===== [ BEGIN GROUP SECTION ] =====================================================================================
                group    lower  type     1
                group    solid  type     1 2 3
                group    fixed  type     1
                group    middle         type     2
                group    movable        type     2 3
                group    upper  type     3

                # ===== [ END GROUP SECTION ] =======================================================================================


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

                # [1:b1 x 1:b1] Diagonal pair coefficient tlsph
                pair_coeff      1 1 smd/tlsph *COMMON 1000 10000.0 0.3 1.0 2.0 10.0 1000.0 &
                *STRENGTH_LINEAR_PLASTIC 1000.0 0 &
                *EOS_LINEAR &
                *END

                # [2:b2 x 2:b2] Diagonal pair coefficient tlsph
                pair_coeff      2 2 smd/tlsph *COMMON 1000 5000.0 0.3 1.0 2.0 10.0 1000.0 &
                *STRENGTH_LINEAR_PLASTIC 500.0 0 &
                *EOS_LINEAR &
                *END

                # [3:b3 x 3:b3] Diagonal pair coefficient tlsph
                pair_coeff      3 3 smd/tlsph *COMMON 1000 40000.0 0.3 1.0 2.0 10.0 1000.0 &
                *STRENGTH_LINEAR_PLASTIC 4000.0 0 &
                *EOS_LINEAR &
                *END

                # [1:b1 x 2:b2] Off-diagonal pair coefficient (generic)
                pair_coeff      1 2 smd/hertz 250.0000000000001

                # [1:b1 x 3:b3] Off-diagonal pair coefficient (generic)
                pair_coeff      1 3 smd/hertz 250.0000000000001

                # [2:b2 x 3:b3] Off-diagonal pair coefficient (generic)
                pair_coeff      2 3 smd/hertz 125.00000000000003

                # ===== [ END FORCEFIELD SECTION ] ==================================================================================
             ]

            13: [
                group all union lower middle upper
                group external subtract all middle
             ]

            14: velocity all set 0.0 0.0 0.0 units box
            15: fix fix_lower lower setforce 0.0 0.0 0.0
            16: fix move_upper upper move wiggle 0.0 0.0 ${amplitude} ${period} units box
            17: fix dtfix tlsph smd/adjust_dt ${dt}
            18: fix integration_fix tlsph smd/integrate_tlsph

            19: [
                compute S all smd/tlsph_stress
                compute E all smd/tlsph_strain
                compute nn all smd/tlsph_num_neighs
             ]

            20: [
                dump dump_id all custom ${dumpdt} ${dumpfile} id type x y z vx vy vz &
                c_S[1] c_S[2] c_S[4] c_nn &
                c_E[1] c_E[2] c_E[4] &
                vx vy vz
             ]

            21: dump_modify dump_id first yes

            22: [
                thermo ${thermodt}
                thermo_style custom step dt f_dtfix v_strain
             ]

            23: run ${runtime}
        '''
mydscript = dscript.parsesyntax(mydscriptfile,verbose=False,authentification=False)
mydscript[-1].definitions.runtime = 1000 # change local definitions of $runtime at the last step
print(mydscript.do(verbose=True))
```

Note that mydscript[-1].runtime = 1000 would have created the attribute runtime. Use definitions instead.

print(repr(mydscript[-1])) gives all details
--------------------------------------------------
Template Content | id:23        (1 line, 31 defs)   <--- all variables have be created also locally
--------------------------------------------------
run ${runtime}
--------------------------------------------------
Detected Variable                  (1 / +1 / -0)
--------------------------------------------------
[+] runtime                                         <--- this runtime is a variable
--------------------------------------------------
Template Attributes                (7 attributes)
--------------------------------------------------
[ ] facultativ   [x] eval         [ ] readonly
[ ] condition    [ ] condeval     [x] detectvar
[x] runtime                                        <--- this runtime is an attribute


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

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

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

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

"""

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



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

# Revision history
# 2024-09-02 alpha version (compatibility issues with pizza.script)
# 2024-09-03 release candidate (fully compatible with pizza.script)
# 2024-09-04 load() and save() methods, improved documentation
# 2024-09-05 several fixes, add dscript.write(), dscript.parsesyntax()
# 2024-09-06 finalization of the block syntax between [], and its examples + documentation
# 2024-10-07 fix example
# 2024-10-09 remove_comments moved to script (to prevent circular reference)
# 2024-10-14 finalization of the integration with scripts
# 2024-10-17 fix __add__, improve repr()
# 2024-10-18 add add_dynamic_script() helper for adding a step to a dscript
# 2024-10-19 better file management
# 2024-10-22 sophistication of ScriptTemplate to recognize defined variables at construction and to parse flags/conditions
# 2024-10-24 management of locl definitions in dscript.save() and dscript.parsesyntax()
# 2024-10-27 caching ScriptTemplate checks, improved dscript.save() methods
# 2024-10-28 less redundancy beyween local and global variables in dscript.save()
# 2024-10-07 new parsesyntax accepting [ ] at any position, improved doc, parsesyntax_legacy holds the old parser method
# 2024-10-08 fix single line templates
# 2024-10-12 several improvements and sophistication in the definition and execution of templates from dscript
# 2024-12-01 standarize scripting features, automatically call script/pscript methods
# 2024-12-02 add dscript.header() and implement it in dscript.save() and dscript.write()
# 2024-12-03 improve the logics of dscript.parse(), dscript.save() on real cases
# 2024-12-04 preparation for major release
# 2024-12-09 get-metadata() use globals()

# Dependencies
import os, getpass, socket, time, datetime
import re, string, random, copy, hashlib
from pizza.private.mstruct import paramauto
from pizza.script import script, scriptdata, pipescript, remove_comments, span, frame_header

__all__ = ['ScriptTemplate', 'autoname', 'dscript', 'frame_header', 'get_metadata', 'lambdaScriptdata', 'lamdaScript', 'paramauto', 'pipescript', 'remove_comments', 'script', 'scriptdata', 'span']


# %% Private Functions

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


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


# %% Low-level Classes (wrappers for pizza.script)

class lambdaScriptdata(paramauto):
    """
    Class to manage lambda script parameters.

    This class holds definitions and variables used within the script templates.
    These parameters are typically set up as global variables that can be accessed
    by script sections for evaluation and substitution.

    Example Usage:
    --------------
    definitions = lambdaScriptdata(var1=10, var2="value")

    Attributes:
    -----------
    _type : str
        The type of the data ("LSD" by default).
    _fulltype : str
        Full type description of the lambda script data.
    _ftype : str
        Short description of the type (parameter definition).

    Methods:
    --------
    This class inherits methods from the `paramauto` class, which allows automatic
    handling of parameters and script data.
    """

    _type = "LSD"
    _fulltype = "Lambda Script Parameters"
    _ftype = "parameter definition"

    def __init__(self, _protection=False, _evaluation=True, sortdefinitions=False, **kwargs):
        """
        Constructor for lambdaScriptdata. 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


class lamdaScript(script):

    """
    lamdaScript
    ===========

    The `lamdaScript` class is a specialized subclass of `script` that acts as a wrapper
    for generating script objects from `dscript` instances. It facilitates the creation
    of scripts with persistent storage options and user-defined configurations.

    Attributes
    ----------
    name : str
        The name of the script.
    SECTIONS : list
        Inherited list of script sections from the `script` class.
    position : int
        The position index of the script section.
    role : str
        The role of the script section, derived from its position.
    description : str
        A brief description of the script.
    userid : str
        The user ID associated with the script.
    version : float
        The version of the script.
    verbose : bool
        Flag to enable verbose output.
    DEFINITIONS : scriptdata
        Definitions of variables used in the script.
    USER : lambdaScriptdata
        User-defined variables specific to `lamdaScript`.
    TEMPLATE : dict
        A dictionary of script templates.

    Methods
    -------
    do()
        Generates the complete LAMMPS script by processing all script sections.

    Special Methods
    ----------------
    __contains__(key)
        Allows checking if a specific section exists using the `in` keyword.
    __str__()
        Returns the string representation of the script.

    Usage Example
    -------------
    ```python
    from dscript import lamdaScript, dscript

    # Create an existing dscript instance
    existing_dscript = dscript(name="ExistingScript")
    existing_dscript.role = "Custom Role"

    # Create a lamdaScript instance based on the existing dscript
    ls = lamdaScript(existing_dscript)
    print(ls.role)  # Outputs: "Custom Role"
    ```
    """
    name = ""

    def __init__(self, dscriptobj, persistentfile=True, persistentfolder=None,
                 printflag = False, verbose = True, softrun = True,
                 **userdefinitions):
        """
            Initialize a new `lambdaScript` instance.

            This constructor creates a `lambdaScript` object based on an existing `dscriptobj`,
            providing options for persistent storage, verbose output, and user-defined configurations.
            A `lambdaScript` represents an anonymous or temporary script that can either preserve the
            original script structure or partially evaluate variable definitions based on the `softrun` flag.

            Parameters
            ----------
            dscriptobj : dscript
                An existing `dscript` object to base the new instance on.
            persistentfile : bool, optional
                If `True`, the script will be saved to a persistent file. Defaults to `True`.
            persistentfolder : str or None, optional
                The folder where the persistent file will be saved. If `None`, a temporary location is used.
                Defaults to `None`.
            printflag : bool, optional
                If `True`, enables printing of the script details during execution. Defaults to `False`.
            verbose : bool, optional
                If `True`, provides detailed output during script initialization and execution. Defaults to `True`.
            softrun : bool, optional
                Determines whether a pre-execution run is carried out to partially evaluate and substitute variables.
                - If `True` (default), the variable definitions and original script content are preserved without full
                  evaluation, meaning no substitution is carried out on the `dscript`'s templates or definitions.
                - If `False`, performs an initial evaluation phase that substitutes available variables and captures
                  the local definitions before creating the `lambdaScript` object.
            **userdefinitions
                Additional user-defined variables and configurations to be included in the `lambdaScript`.

            Raises
            ------
            TypeError
                If `dscriptobj` is not an instance of the `dscript` class.

            Example
            -------
            ```python
            existing_dscript = dscript(name="ExistingScript")
            ls = lambdaScript(existing_dscript, var3="3", softrun=False)
            ```
        """
        if not isinstance(dscriptobj, dscript):
            raise TypeError(f"The 'dscriptobj' object must be of class dscript not {type(dscriptobj).__name__}.")
        verbose = dscriptobj.verbose if verbose is None else dscriptobj.verbose
        super().__init__(persistentfile=persistentfile, persistentfolder=persistentfolder, printflag=printflag, verbose=verbose, **userdefinitions)
        self.name = dscriptobj.name
        self.SECTIONS = dscriptobj.SECTIONS
        self.section = dscriptobj.section
        self.position = dscriptobj.position
        self._role = dscriptobj.role  # Initialize an internal storage for the role
        self.description = dscriptobj.description
        self.userid = dscriptobj.userid
        self.version= dscriptobj.version
        self.verbose = verbose
        self.printflag = printflag
        self.DEFINITIONS = dscriptobj.DEFINITIONS
        if softrun:
            self.TEMPLATE,localdefinitions = dscriptobj.do(softrun=softrun,return_definitions=True)
            self.USER = localdefinitions + lambdaScriptdata(**self.USER)
        else:
            self.TEMPLATE = dscriptobj.do(softrun=softrun)
            self.USER = lambdaScriptdata(**self.USER)

    @property
    def role(self):
        """Override the role property to include a setter."""
        # If _role is set, return it; otherwise, use the inherited logic
        if self._role is not None:
            return self._role
        elif self.section in range(len(self.SECTIONS)):
            return self.SECTIONS[self.section]
        else:
            return ""

    @role.setter
    def role(self, value):
        """Allow setting the role."""
        self._role = value


# %% Main Classes
class ScriptTemplate:
    """
     The `ScriptTemplate` class provides a mechanism to store, process, and dynamically substitute
     variables within script content. This class supports handling flags and conditions, allowing
     for flexible and conditional execution of parts of the script.

     Attributes:
     -----------
     default_attributes : dict (class-level)
         A dictionary containing the default flags for each `ScriptTemplate` object. These attributes
         are applied when initializing the object or when no flags are specified in the content.
         Flags include:
         - facultative (bool): If True, the script line is optional and may be discarded if certain
             conditions are not met. (default: False)
         - eval (bool): If True, the content is evaluated using `formateval`, which allows for variable
             substitution during execution. (default: False)
         - readonly (bool): If True, the content cannot be modified after initialization. (default: False)
         - condition (str or None): A string that specifies a condition for executing the content.
             If the condition is not met, the script line is skipped. (default: None)
         - condeval (bool): If True, the `condition` attribute is evaluated dynamically using variables.
             (default: False)
         - detectvar (bool): If True, any variables in the content (e.g., `${varname}`) are automatically
             detected and registered in the definitions. (default: True)

     Methods:
     --------
     __init__(self, content="", definitions=lambdaScriptdata(), userid=None, verbose=False, **kwargs):
         Initializes the `ScriptTemplate` object with script content, optional variable definitions,
         and configurable attributes. Flags like `facultative`, `eval`, `readonly`, `condition`,
         and others are set via content prefixes or passed directly as keyword arguments.

     parse_content(cls, content, verbose=False):
         A class method that parses content to detect special flags and condition tags, returning the
         cleaned content and a dictionary of attributes (flags).

         The method processes the following flags and tags:
         - `!` : Sets `eval=True`, which forces evaluation of the content.
         - `?` : Sets `facultative=True`, marking the content as optional.
         - `^` : Sets `readonly=True`, preventing modifications to the content.
         - `~` : Sets `detectvar=False`, disabling automatic variable detection.
         - `[if:condition]` : Defines a condition for executing the script. If the condition is met,
           the script will be executed. Conditions can include `;eval` to trigger evaluation.

         Parameters:
         -----------
         content : str
             The script content to be parsed. It may contain lines starting with special characters
             that set specific attributes for the script template.

         verbose : bool (default: False)
             If True, preserves comments and does not remove comment lines during parsing.

         Returns:
         --------
         cleaned_content : str
             The cleaned script content, with all flags and conditions removed.

         attributes : dict
             A dictionary of parsed attributes (flags) including:
             - facultative (bool): Indicates if the content is optional.
             - eval (bool): Specifies if the content should be evaluated for variable substitution.
             - readonly (bool): Prevents modifications to the content.
             - condition (str or None): Defines the condition for executing the script.
             - condeval (bool): If True, dynamically evaluates the condition.
             - detectvar (bool): If True, detects and registers variables in the content.

     detect_variables(self):
         Detects variables in the content using the pattern `${varname}`. It returns a list of
         variable names found in the script.

     refreshvar(self):
         Ensures that any detected variables are added to the `definitions` if they are missing.

     __setattr__(self, name, value):
         Sets the value of an attribute. The method performs validation on specific attributes
         (e.g., `eval`, `readonly`, `facultative`, etc.) to ensure the correct data types are assigned.

     __getattr__(self, name):
         Retrieves the value of an attribute. If the attribute is not set, it returns the default
         value from `default_attributes`.


    Example 1:
    ----------
    # Create a ScriptTemplate object with content and optional definitions
    line = ScriptTemplate("dimension    ${dimension}")

    # You can also pass in global definitions if needed
    global_definitions = lambdaScriptdata(dimension=3)
    line_with_defs = ScriptTemplate("dimension    ${dimension}",
                                    definitions=global_definitions)

    After initialization, you can modify the line's attributes or use it as
    part of a larger script managed by a `dscript` object.


     Example 2:
     ----------
     # Example of using flags and conditions in content

     line = ScriptTemplate("!velocity all set 0.0 0.0 0.0 units box")
     # This will automatically set `eval=True` due to the '!' flag.

     line_with_condition = ScriptTemplate("[if: ${condition};eval] fix upper move wiggle 0.0 0.0 1.0 1.0")
     # This content will only execute if `${condition}` is True, and `eval` will be applied to substitute variables.

    """

    # Class-level attribute for default flags
    default_attributes = {
        'facultative': False,
        'eval': False,
        'readonly': False,
        'condition': None,
        'condeval': False,
        'detectvar': True
    }

    def __init__(self, content="", definitions=lambdaScriptdata(), autorefresh=True, userid=None, verbose=False, comment_chars="#%", **kwargs):
        """
        Initializes a new `ScriptTemplate` object.

        The constructor sets up a new script template with content, optional variable
        definitions, and a set of configurable attributes. This template is capable
        of dynamically substituting variables using a provided `lambdaScriptdata`
        object or internal definitions. You can also manage attributes like evaluation,
        facultative execution, and variable detection.

        Parameters:
        -----------
        content : str or list of str
            The content of the script template, which can be a single string or a list of strings.
            If a single string is provided, it will automatically be converted to a list of lines.
            This ensures consistent handling of multi-line content.

        definitions : lambdaScriptdata, optional
            A reference to a `lambdaScriptdata` object that contains global variable
            definitions. These definitions will be used to substitute variables within the content.
            If `definitions` is not provided, variable substitution will rely on local
            or inline definitions.

        verbose : flag (default value=True)
            If True the comments are preserved (applied when str is a string).

        autorefresh : flag (default=False)
            If True, new variables are automatically detected when the content is changed

        **kwargs :
            Additional keyword arguments to set specific attributes for the script line.
            These attributes control the behavior of the script template during evaluation
            and execution. Any keyword argument passed here will update the default
            attribute values.

                Default attributes (with default values):
            - facultative (False):
                If True, the script line is optional and may be discarded if certain conditions are not met.
            - eval (False):
                If True, the content will be evaluated using `formateval`, allowing variable
                substitution during the execution.
            - readonly (False):
                If True, the content of the script cannot be modified after initialization.
            - condition (None):
                An optional condition that controls whether the content will be executed.
                If the condition is not met, the script line will not be executed.
            - condeval (False):
                If True, the `condition` attribute will be evaluated, allowing conditional
                logic based on variable values.
            - detectvar (True):
                If True, the content will automatically detect and register any variables
                (such as `${varname}`) within the `definitions` for substitution.
            - 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.
        """


        # Initialize `_CACHE` with separate entries for each method
        self._CACHE = {
            'variables': {'result': None},
            'check_variables': {'result': None}
        }
        # Constructor
        self._autorefresh = autorefresh
        self._content = content # # Store initial content
        self.definitions = lambdaScriptdata(**definitions)  # Reference to the DEFINITIONS object
        # Initialize attributes with default values
        self.attributes = self.default_attributes.copy()
        # Convert single string content to a list for consistent processing
        if content == "" or content is None:
            content = ""  # Set to empty string if None
        elif isinstance(content, str):
            # Interpret the content and extract any attribute flags (e.g. !, ?, etc.)
            content, self.attributes = self.parse_content(content, verbose=verbose)
            # Convert content to a list of strings
            if verbose:
                content = content.split('\n')  # All comments are preserved during construction
            else:
                content = remove_comments(content, split_lines=True,comment_chars=comment_chars)  # Split string by newlines into list of strings
        elif not isinstance(content, list) or not all(isinstance(item, str) for item in content):
            raise TypeError("The 'content' attribute must be a string or a list of strings.")
        # Assign content to the object
        self.content = content
        # Update attributes with any additional keyword arguments passed in
        self.attributes.update(kwargs)
        # Detect variables in the content
        detected_variables = self.detect_variables()
        # Automatically set `eval=True` if any detected variables are defined in `definitions`
        if detected_variables:
            for var in detected_variables:
                if var in self.definitions:
                    self.attributes['eval'] = True
                    break  # No need to continue checking once we know eval should be set
        # Assign userid (optional)
        self.userid = userid if userid is not None else autoname(3)


    def _calculate_content_hash(self, content):
        """Generate hash for content."""
        return hashlib.md5("\n".join(content).encode()).hexdigest() if isinstance(content, list) else hashlib.md5(content.encode()).hexdigest()

    @property
    def content(self):
        return self._content

    @content.setter
    def content(self, new_content):
        """Set content and reinitialize cache if the content has changed."""
        new_content_hash = self._calculate_content_hash(new_content)
        if new_content_hash != self._content_hash:
            self._content = new_content
            self._content_hash = new_content_hash
            self._invalidate_cache()  # Reset cache only if content changes


    def _update_content(self, value):
        """Helper to set _content and _content_hash, refreshing cache as necessary."""
        # Check if content modification is allowed based on readonly attribute
        if getattr(self, 'attributes', {}).get('readonly', False):
            raise AttributeError("Cannot modify content. It is read-only.")
        # Validate and process content as either a string or list of strings
        if isinstance(value, str):
            value = remove_comments(value, split_lines=True)
        elif not isinstance(value, list) or not all(isinstance(item, str) for item in value):
            raise TypeError("The 'content' attribute must be a string or a list of strings.")
        # Set _content and calculate hash
        super().__setattr__('_content', value)
        new_hash = hash(tuple(value))
        # If hash changes, reset cache
        if new_hash != getattr(self, '_content_hash', None):
            super().__setattr__('_content_hash', new_hash)
            self._invalidate_cache()  # Invalidate cache due to content change
            if self._autorefresh:
                self.refreshvar()  # Refresh variables based on new content

    def _invalidate_cache(self):
        """Reset all cache entries."""
        # Check if _CACHE is initialized
        if not hasattr(self, '_CACHE'):
            self._CACHE = {
                'variables': {'result': None},
                'check_variables': {'result': None}
            }
        for entry in self._CACHE.values():
            entry['result'] = None


    @classmethod
    def parse_content(cls, content, verbose=False):
        """
        Parse the content string and return:
        1. Cleaned content (without flags or condition tags).
        2. A dictionary of attributes (flags) initialized with default values.

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

        Returns:
        --------
        cleaned_content : str
            The cleaned content with all flag prefixes and condition tags removed.
        attributes : dict
            A dictionary of attributes (flags) set based on the content prefixes.
        """
        attributes = cls.default_attributes.copy()

        # Handle empty content
        if not content.strip():  # Empty string or whitespace-only
            return "", attributes

        idx = 0
        cleaned_content = []

        # If content is a single string, split it into lines
        if isinstance(content, str):
            content = content.split('\n')

        # Remove leading and trailing empty lines
        while content and content[0].strip() == '':
            content.pop(0)
        while content and content[-1].strip() == '':
            content.pop()

        # Process each remaining line to detect flags and conditions
        for line in content:
            idx = 0
            line = line.strip()  # Trim any leading/trailing whitespace

            # Parse flags and conditions only at the beginning of the line
            while idx < len(line):
                ch = line[idx]
                if ch == '!':
                    attributes['eval'] = True
                    idx += 1
                elif ch == '?':
                    attributes['facultative'] = True
                    idx += 1
                elif ch == '^':
                    attributes['readonly'] = True
                    idx += 1
                elif ch == '~':
                    attributes['detectvar'] = False
                    idx += 1
                elif line.startswith('[if:', idx):
                    # Parse condition
                    end_idx = line.find(']', idx)
                    if end_idx == -1:
                        raise ValueError("Unclosed '[if:]' tag in content")
                    cond_str = line[idx + 4:end_idx]
                    if ';eval' in cond_str:
                        attributes['condeval'] = True
                        cond_str = cond_str.replace(';eval', '')
                    attributes['condition'] = cond_str.strip()
                    idx = end_idx + 1
                else:
                    break  # No more flags or conditions, process the rest of the line

                # Skip any whitespace after flags or conditions
                while idx < len(line) and line[idx] in (' ', '\t'):
                    idx += 1

            # Append the cleaned line (without flags or condition tags)
            cleaned_content.append(line[idx:].strip())

        # Join the cleaned lines back into a single string
        cleaned_content = '\n'.join(cleaned_content)

        return cleaned_content, attributes



    def __str__(self):
        num_attrs = len(self.attributes)  # All attributes count
        return f"1 line/block, {num_attrs} attributes"


    def __repr__(self):
      # Template content section
      total_lines = len(self.content)
      total_variables = len(self.definitions)
      line_word = "lines" if total_lines > 1 else "line"
      variable_word = "defs" if total_variables > 1 else "def"
      content_label = "Template Content"
      content_label += "" if self.userid == "" else f" | id:{self.userid}"
      available_space = 50 - len(content_label) - 1
      repr_str = "-" * 50 + "\n"
      repr_str += f"{content_label}{('(' + str(total_lines) + ' ' + line_word + ', ' + str(total_variables) + ' ' + variable_word + ')').rjust(available_space)}\n"
      repr_str += "-" * 50 + "\n"

      if total_lines < 1:
          repr_str += "< empty content >\n"
      elif total_lines <= 12:
          # If content has 12 or fewer lines, display all lines
          for line in self.content:
              truncated_line = (line[:18] + '[...]' + line[-18:]) if len(line) > 40 else line
              repr_str += f"{truncated_line:<50}\n"
      else:
          # Display first three lines, middle three lines, and last three lines
          for line in self.content[:3]:
              truncated_line = (line[:18] + '[...]' + line[-18:]) if len(line) > 40 else line
              repr_str += f"{truncated_line:<50}\n"
          repr_str += "\t\t[...]\n"
          mid_start = total_lines // 2 - 1
          mid_end = mid_start + 3
          for line in self.content[mid_start:mid_end]:
              truncated_line = (line[:18] + '[...]' + line[-18:]) if len(line) > 40 else line
              repr_str += f"{truncated_line:<50}\n"
          repr_str += "\t\t[...]\n"
          for line in self.content[-3:]:
              truncated_line = (line[:18] + '[...]' + line[-18:]) if len(line) > 40 else line
              repr_str += f"{truncated_line:<50}\n"

      # Detected Variables section in 3-column format
      detected_variables = self.detect_variables()
      if detected_variables:
          repr_str += "-" * 50 + "\n"
          # Count the number of defined and missing variables
          defined_variables = set(self.definitions.keys()) if self.definitions else set()
          variable_word = 'Variables' if len(detected_variables) > 1 else 'Variable'
          detected_set = set(detected_variables)
          missing_variables = detected_set - defined_variables
          defined_count = len(detected_set & defined_variables)
          missing_count = len(missing_variables)
          total_variables = len(detected_set)
          repr_str += f"Detected {variable_word}{('(' + str(total_variables) + ' / +' + str(defined_count) + ' / -' + str(missing_count) + ')').rjust(50 - len('Detected Variables') - 1)}\n"
          repr_str += "-" * 50 + "\n"

          # Display variables with status:
          # [+] for defined variables with values other than "${varname}"
          # [-] for defined variables with default values "${varname}"
          # [ ] for undefined variables
          for i in range(0, len(detected_variables), 3):
              var_set = detected_variables[i:i+3]
              line = ""
              for var in var_set:
                  var_name = (var[:10] + ' ') if len(var) > 10 else var.ljust(11)
                  if var in defined_variables:
                      # Check if it's set to its default value (i.e., "${varname}")
                      if self.definitions[var] == f"${{{var}}}":
                          flag = '[-]'
                      else:
                          flag = '[+]'
                  else:
                      flag = '[ ]'
                  line += f"{flag} {var_name}  "
              repr_str += f"{line}\n"

      # Attribute section in 3 columns
      repr_str += "-" * 50 + "\n"
      repr_str += f"Template Attributes{('(' + str(len(self.attributes)) + ' attributes)').rjust(50 - len('Template Attributes') - 1)}\n"
      repr_str += "-" * 50 + "\n"

      # Create a compact three-column layout for attributes with checkboxes
      attr_items = [(attr, '[x]' if value else '[ ]') for attr, value in self.attributes.items() if attr != 'definitions']
      for i in range(0, len(attr_items), 3):
          attr_set = attr_items[i:i+3]
          line = ""
          for attr, value in attr_set:
              attr_name = (attr[:10] + ' ') if len(attr) > 10 else attr.ljust(11)
              line += f"{value} {attr_name}  "
          repr_str += f"{line}\n"

      return repr_str


    def __setattr__(self, name, value):
        # Handle specific attributes with validation
        if name in ['facultative', 'eval', 'readonly']:
            if not isinstance(value, bool):
                raise TypeError(f"The '{name}' attribute must be a Boolean, not {type(value).__name__}.")
        elif name == 'condition':
            if not isinstance(value, str) and value is not None:
                raise TypeError(f"The 'condition' attribute must be a string or None, not {type(value).__name__}.")
        elif name == 'content':
            # Use helper to manage content updates and cache invalidation
            self._update_content(value)
            return  # Exit to avoid further processing since _update_content handles the setting
        elif name == 'definitions':
            if not isinstance(value, (lambdaScriptdata, scriptdata)) and value is not None:
                raise TypeError(f"The 'definitions' must be a lambdaScriptdata or scriptdata, not {type(value).__name__}.")

        # Set attributes directly for key fields and avoid recursion
        if name in ['userid', 'attributes', 'definitions', '_content', '_content_hash', '_autorefresh']:
            super().__setattr__(name, value)
        else:
            # Ensure 'attributes' is initialized before updating
            if not hasattr(self, 'attributes') or self.attributes is None:
                self.attributes = {}
            self.attributes[name] = value


    def __getattr__(self, name):
        """
        Handles attribute retrieval, checking the following in order:
        1. If 'name' is in default_attributes, return the value from attributes if it exists,
           otherwise return the default value from default_attributes.
        2. If 'name' is 'content', return the content (or an empty string if content is not set).
        3. If 'name' exists in the attributes dictionary, return its value.
        4. If attributes itself exists in __dict__, return the value from attributes if 'name' is found.
        5. If all previous checks fail, raise an AttributeError indicating that 'name' is not found.
        """
        # Ensure '_CACHE' is always accessible without a KeyError
        if name == '_CACHE':
            # Initialize _CACHE if it does not exist
            if '_CACHE' not in self.__dict__:
                self.__dict__['_CACHE'] = {
                    'variables': {'result': None},
                    'check_variables': {'result': None}
                }
            return self.__dict__['_CACHE']  # Access _CACHE directly
        # Step 1: Check if 'name' is a valid attribute in default_attributes
        if name in self.default_attributes:
            # Directly access __dict__ to avoid recursive lookup
            attributes = self.__dict__.get('attributes', {})
            return attributes.get(name, self.default_attributes[name])
        # Step 2: Special case for 'content'
        if name == 'content':
            # Directly access __dict__ to avoid recursion
            return self.__dict__.get('_content', "")
        if name == "_autorefresh":
            return self.__dict__.get('_autorefresh', True)
        # Step 3: Check if 'name' exists in 'attributes' and return its value directly
        attributes = self.__dict__.get('attributes', {})
        if name in attributes:
            return attributes[name]
        # Step 4: Check if 'attributes' exists in __dict__ and retrieve 'name' if present
        if 'attributes' in self.__dict__ and name in self.__dict__['attributes']:
            return self.__dict__['attributes'][name]
        # Step 5: If none of the above conditions are met, raise an AttributeError
        raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")




    def do(self, protected=True, softrun=False, USER=lambdaScriptdata()):
        """
        Executes or prepares the script template content based on its attributes and the `softrun` flag.

        Parameters
        ----------
        protected : bool, optional
            If `True` (default), variable evaluation uses a protected environment, safeguarding global definitions
            and system attributes from modification during execution.
        softrun : bool, optional
            Determines if the script is evaluated in a preliminary mode:
            - If `True`, returns the script content without full evaluation, allowing for a preview or
              an initial capture of local definitions without substituting variables.
            - If `False` (default), processes the script with full evaluation, applying all substitutions
              defined in `definitions`.
        USER : lambdaScriptdata, optional
            A `lambdaScriptdata` instance with user-provided definitions, which supplement or override
            template-level definitions during execution.

        Returns
        -------
        str
            The processed or original script content as a single string.
            - Returns an empty string if `facultative` is set to `True`, or if `condition` is `False`
              and does not meet any specified evaluation requirements.

        Notes
        -----
        - `facultative`: If `True`, the method returns an empty string, effectively skipping execution of the content.
        - `condition`: If specified, this attribute is evaluated to decide whether the content should be processed
          (default `True` if `condition` is `None`). If `condeval` is `True`, the condition itself undergoes
          evaluation using Python's `eval`.
        - `eval`: If `True` and `softrun` is `False`, performs evaluation for variable substitution on the
          content lines, applying transformations based on both `definitions` and `USER` definitions if provided.
          This allows variables to be dynamically substituted within the script content.

        Processing Workflow
        -------------------
        1. **Facultative Check**: If the `facultative` attribute is `True`, immediately returns an empty string.
        2. **Condition Check**: If a `condition` is specified, it is evaluated:
           - If `condeval` is `True`, the condition undergoes evaluation (using `eval`).
           - If the evaluated `condition` is `False`, returns an empty string, skipping execution.
        3. **Execution Based on `softrun`**:
           - If `softrun` is `True`, returns the original content without variable substitution, providing a preview.
           - If `softrun` is `False`, evaluates the content lines based on `definitions` and `USER` if applicable.
        4. **Variable Formatting**: During evaluation, lists and tuples are formatted into strings with prefixed comments,
           enhancing readability and handling complex data structures directly in the script.

        Example
        -------
        >>> template = ScriptTemplate(
        ...     content=["variable x equal ${var1}", "print 'Value of x is ${var1}'"],
        ...     definitions=lambdaScriptdata(var1=10)
        ... )
        >>> template.do()
        "variable x equal 10\nprint 'Value of x is 10'"

        """

        # If 'facultative' is set to True, return an empty string immediately
        if self.attributes.get("facultative", False):
            return ""

        # Evaluate condition if present
        cond = True
        if self.attributes.get("condition") is not None:
            condition_expr = self.definitions.formateval(self.attributes["condition"], protected)
            cond = eval(condition_expr) if self.attributes.get("condeval", False) else condition_expr

        # Process based on softrun flag
        if softrun:
            return "\n".join(self.content) if cond else ""

        # Perform full processing when softrun is False
        if cond:
            if self.attributes.get("eval", False):
                #return "\n".join([self.definitions.formateval(line, protected) for line in self.content])
                inputs = self.definitions + USER
                for k in inputs.keys():
                    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=","))
                return "\n".join([inputs.formateval(line, protected) for line in self.content])

            else:
                return "\n".join(self.content)

        return ""



    def detect_variables(self):
        """
        Detects variables in the content of the template using the pattern r'\$\{(\w+)\}'.

        Returns:
        --------
        list
            A list of unique variable names detected in the content.
        """
        # Check cache first
        cache_entry = self._CACHE['variables']
        if cache_entry['result'] is not None:
            return cache_entry['result']
        # Detect variables if cache miss or content changed
        variable_pattern = re.compile(r'\$\{(\w+)\}')
        detected_vars = {variable for line in self.content for variable in variable_pattern.findall(line)}
        # Cache the result and return output
        cache_entry['result'] = list(detected_vars)
        return cache_entry['result']



    def refreshvar(self,globaldefinitions = lambdaScriptdata()):
        """
        Detects variables in the content and adds them to definitions if needed.
        This method ensures that variables like ${varname} are correctly detected
        and added to the definitions if they are missing.

        use globaldefinitions to add a list of global variables/definitions

        """
        if self.attributes["detectvar"] and isinstance(self.content, list) and self.definitions:
            variables = self.detect_variables()
            for varname in variables:
                if (varname not in self.definitions) and (varname not in globaldefinitions):
                    self.definitions.setattr(varname, "${" + varname + "}")


    def check_variables(self, verbose=True, seteval=True):
        """
        Checks for undefined variables in the ScriptTemplate instance.

        Parameters:
        -----------
        verbose : bool, optional, default=True
            If True, prints information about variables for the template.
            Shows [-] if the variable is set to its default value, [+] if it is defined, and [ ] if it is undefined.

        seteval : bool, optional, default=True
            If True, sets the `eval` attribute to True if at least one variable is defined or set to its default value.

        Returns:
        --------
        out : dict
            A dictionary with lists of default variables, set variables, and undefined variables:
            - "defaultvalues": Variables set to their default value (${varname}).
            - "setvalues": Variables defined with a specific value.
            - "undefined": Variables that are undefined.
        """
        # Check cache first
        cache_entry = self._CACHE['check_variables']
        if cache_entry['result'] is not None:
            return cache_entry['result']
        # Main
        out = {"defaultvalues": [], "setvalues": [], "undefined": []}
        detected_vars = self.detect_variables()
        defined_vars = set(self.definitions.keys()) if self.definitions else set()
        set_values, default_values, undefined_vars = [], [], []
        if verbose:
            print(f"\nTEMPLATE {self.userid} variables:")
        for var in detected_vars:
            if var in defined_vars:
                if self.definitions[var] == f"${{{var}}}":  # Check for default value
                    default_values.append(var)
                    if verbose:
                        print(f"[-] {var}")  # Variable is set to its default value
                else:
                    set_values.append(var)
                    if verbose:
                        print(f"[+] {var}")  # Variable is defined with a specific value
            else:
                undefined_vars.append(var)
                if verbose:
                    print(f"[ ] {var}")  # Variable is not defined
        # If seteval is True, set eval to True if at least one variable is defined or set to its default
        if seteval and (set_values or default_values):
            self.attributes['eval'] = True  # Set eval in the attributes dictionary
        # Update the output dictionary
        out["defaultvalues"].extend(default_values)
        out["setvalues"].extend(set_values)
        out["undefined"].extend(undefined_vars)
        # update Cache and return output
        cache_entry['result'] = out
        return out


    def is_variable_defined(self, var_name):
        """
        Checks if a specified variable is defined (either as a default value or a set value).

        Parameters:
        -----------
        var_name : str
            The name of the variable to check.

        Returns:
        --------
        bool
            True if the variable is defined (either as a default or a set value), False if not.

        Raises:
        -------
        ValueError
            If `var_name` is invalid or undefined in the template.
        """
        if not isinstance(var_name, str):
            raise ValueError("Variable name must be a string.")

        variable_status = self.check_variables(verbose=False, seteval=False)

        # Check if the variable is in default values or set values
        if var_name in variable_status["defaultvalues"] or var_name in variable_status["setvalues"]:
            return True
        else:
            raise ValueError(f"Variable '{var_name}' is undefined in the template.")


    def is_variable_set_value_only(self, var_name):
        """
        Checks if a specified variable is defined and set to a specific (non-default) value.

        Parameters:
        -----------
        var_name : str
            The name of the variable to check.

        Returns:
        --------
        bool
            True if the variable is defined with a set (non-default) value, False if not.

        Raises:
        -------
        ValueError
            If `var_name` is invalid or not defined in the template.
        """
        if not isinstance(var_name, str):
            raise ValueError("Variable name must be a string.")

        variable_status = self.check_variables(verbose=False, seteval=False)

        # Check if the variable is in set values only (not default values)
        if var_name in variable_status["setvalues"]:
            return True
        elif var_name in variable_status["defaultvalues"]:
            return False  # Defined but only at its default value
        else:
            raise ValueError(f"Variable '{var_name}' is undefined in the template.")


class dscript:
    """
    dscript: A Dynamic Script Management Class

    The `dscript` class is designed to manage and dynamically generate multiple
    lines/items of a script, typically for use with LAMMPS or similar simulation tools.
    Each line in the script is represented as a `ScriptTemplate` object, and the
    class provides tools to easily manipulate, concatenate, and execute these
    script lines/items.

    Key Features:
    -------------
    - **Dynamic Script Generation**: Define and manage script lines/items dynamically,
      with variables that can be substituted at runtime.
    - **Conditional Execution**: Add conditions to script lines/items so they are only
      included if certain criteria are met.
    - **Script Concatenation**: Combine multiple script objects while maintaining
      control over variable precedence and script structure.
    - **User-Friendly Access**: Easily access and manipulate script lines/items using
      familiar Python constructs like indexing and iteration.

    Practical Use Cases:
    --------------------
    - **Custom LAMMPS Scripts**: Generate complex simulation scripts with varying
      parameters based on dynamic conditions.
    - **Automation**: Automate the creation of scripts for batch processing,
      simulations, or other repetitive tasks.
    - **Script Management**: Manage and version-control different script sections
      and configurations easily.

    Methods:
    --------
    __init__(self, name=None):
        Initializes a new `dscript` object with an optional name.

    __getitem__(self, key):
        Retrieves a script line by its key. If a list of keys is provided,
        returns a new `dscript` object with lines/items reordered accordingly.

    __setitem__(self, key, value):
        Adds or updates a script line. If the value is an empty list, the
        corresponding script line is removed.

    __delitem__(self, key):
        Deletes a script line by its key.

    __contains__(self, key):
        Checks if a key exists in the script. Allows usage of `in` keyword.

    __iter__(self):
        Returns an iterator over the script lines/items, allowing for easy iteration
        through all lines/items in the `TEMPLATE`.

    __len__(self):
        Returns the number of script lines/items currently stored in the `TEMPLATE`.

    keys(self):
        Returns the keys of the `TEMPLATE` dictionary.

    values(self):
        Returns the `ScriptTemplate` objects stored as values in the `TEMPLATE`.

    items(self):
        Returns the keys and `ScriptTemplate` objects from the `TEMPLATE` as pairs.

    __str__(self):
        Returns a human-readable summary of the script, including the number
        of lines/items and total attributes. Shortcut: `str(S)`.

    __repr__(self):
        Provides a detailed string representation of the entire `dscript` object,
        including all script lines/items and their attributes. Useful for debugging.

    reorder(self, order):
        Reorders the script lines/items based on a given list of indices, creating a
        new `dscript` object with the reordered lines/items.

    get_content_by_index(self, index, do=True, protected=True):
        Returns the processed content of the script line at the specified index,
        with variables substituted based on the definitions and conditions applied.

    get_attributes_by_index(self, index):
        Returns the attributes of the script line at the specified index.

    add_dynamic_script(self, key, content="", definitions=None, verbose=None, **USER):
        Add a dynamic script step to the `dscript` object.

    createEmptyVariables(self, vars):
        Creates new variables in `DEFINITIONS` if they do not already exist.
        Accepts a single variable name or a list of variable names.

    do(self, printflag=None, verbose=None):
        Executes all script lines/items in the `TEMPLATE`, concatenating the results,
        and handling variable substitution. Returns the full script as a string.

    script(self, **userdefinitions):
        Generates a `lamdaScript` object from the current `dscript` object,
        applying any additional user definitions provided.

    pipescript(self, printflag=None, verbose=None, **USER):
        Returns a `pipescript` object by combining script objects for all keys
        in the `TEMPLATE`. Each key in `TEMPLATE` is handled separately, and
        the resulting scripts are combined using the `|` operator.

    save(self, filename=None, foldername=None, overwrite=False):
        Saves the current script instance to a text file in a structured format.
        Includes metadata, global parameters, definitions, templates, and attributes.

    write(scriptcontent, filename=None, foldername=None, overwrite=False):
        Writes the provided script content to a specified file in a given folder,
        with a header added if necessary, ensuring the correct file format.

    load(cls, filename, foldername=None, numerickeys=True):
        Loads a script instance from a text file, restoring the content, definitions,
        templates, and attributes. Handles parsing and variable substitution based on
        the structure of the file.

    parsesyntax(cls, content, numerickeys=True):
        Parses a script instance from a string input, restoring the content, definitions,
        templates, and attributes. Handles parsing and variable substitution based on the
        structure of the provided string, ensuring the correct format and key conversions
        when necessary.

    Example:
    --------
    # Create a dscript object
    R = dscript(name="MyScript")

    # Define global variables (DEFINITIONS)
    R.DEFINITIONS.dimension = 3
    R.DEFINITIONS.units = "$si"

    # Add script lines
    R[0] = "dimension    ${dimension}"
    R[1] = "units        ${units}"

    # Generate and print the script
    sR = R.script()
    print(sR.do())

    Attributes:
    -----------
    name : str
        The name of the script, useful for identification.
    TEMPLATE : dict
        A dictionary storing script lines/items, with keys to identify each line.
    DEFINITIONS : lambdaScriptdata
        Stores the variables and parameters used within the script lines/items.
    """

    # Class variable to list attributes that should not be treated as TEMPLATE entries
    construction_attributes = {'name', 'SECTIONS', 'section', 'position', 'role', 'description',
                               'userid', 'verbose', 'printflag', 'DEFINITIONS', 'TEMPLATE',
                               'version','license','email'
                               }

    def __init__(self,  name=None,
                        SECTIONS = ["DYNAMIC"],
                        section = 0,
                        position = None,
                        role = "dscript instance",
                        description = "dynamic script",
                        userid = "dscript",
                        version = None,
                        license = None,
                        email = None,
                        printflag = False,
                        verbose = False,
                        verbosity = None,
                        **userdefinitions
                        ):
        """
        Initializes a new `dscript` object.

        The constructor sets up a new `dscript` object, which allows you to
        define and manage a script composed of multiple lines/items. Each line is
        stored in the `TEMPLATE` dictionary, and variables used in the script
        are stored in `DEFINITIONS`.

        Parameters:
        -----------
        name : str, optional
            The name of the script. If no name is provided, a random name will
            be generated automatically. The name is useful for identifying the
            script, especially when managing multiple scripts.

        Example:
        --------
        # Create a dscript object with a specific name
        R = dscript(name="ExampleScript")

        # Or create a dscript object with a random name
        R = dscript()

        After initialization, you can start adding script lines/items and defining variables.
        """

        if name is None:
            self.name = autoname()
        else:
            self.name = name

        if (version is None) or (license is None) or (email is None):
            metadata = get_metadata()               # retrieve all metadata
            version = metadata["version"] if version is None else version
            license = metadata["license"] if license is None else license
            email = metadata["email"] if email is None else email
        self.SECTIONS = SECTIONS if isinstance(SECTIONS,(list,tuple)) else [SECTIONS]
        self.section = section
        self.position = position if position is not None else 0
        self.role = role
        self.description = description
        self.userid = userid
        self.version = version
        self.license = license
        self.email = email
        self.printflag = printflag
        self.verbose = verbose if verbosity is None else verbosity>0
        self.verbosity = 0 if not verbose else verbosity
        self.DEFINITIONS = lambdaScriptdata(**userdefinitions)
        self.TEMPLATE = {}

    def __getattr__(self, attr):
        # During construction phase, we only access the predefined attributes
        if 'TEMPLATE' not in self.__dict__:
            if attr in self.__dict__:
                return self.__dict__[attr]
            raise AttributeError(f"'dscript' object has no attribute '{attr}'")

        # If TEMPLATE is initialized and attr is in TEMPLATE, return the corresponding ScriptTemplate entry
        if attr in self.TEMPLATE:
            return self.TEMPLATE[attr]

        # Fall back to internal __dict__ attributes if not in TEMPLATE
        if attr in self.__dict__:
            return self.__dict__[attr]

        raise AttributeError(f"'dscript' object has no attribute '{attr}'")


    def __setattr__(self, attr, value):
        # Handle internal attributes during the construction phase
        if 'TEMPLATE' not in self.__dict__:
            self.__dict__[attr] = value
            return

        # Handle construction attributes separately (name, TEMPLATE, USER)
        if attr in self.construction_attributes:
            self.__dict__[attr] = value
        # If TEMPLATE exists, and the attribute is intended for it, update TEMPLATE
        elif 'TEMPLATE' in self.__dict__:
            # Convert the value to a ScriptTemplate if needed, and update the template
            if attr in self.TEMPLATE:
                # Modify the existing ScriptTemplate object for this attribute
                if isinstance(value, str):
                    self.TEMPLATE[attr].content = value
                elif isinstance(value, dict):
                    self.TEMPLATE[attr].attributes.update(value)
            else:
                # Create a new entry if it does not exist
                self.TEMPLATE[attr] = ScriptTemplate(content=value,definitions=self.DEFINITIONS,verbose=self.verbose,userid=attr)
        else:
            # Default to internal attributes
            self.__dict__[attr] = value


    def __getitem__(self, key):
        """
        Implements index-based retrieval, slicing, or reordering for dscript objects.

        Parameters:
        -----------
        key : int, slice, list, or str
            - If `key` is an int, returns the corresponding template at that index.
              Supports negative indices to retrieve templates from the end.
            - If `key` is a slice, returns a new `dscript` object containing the templates in the specified range.
            - If `key` is a list, reorders the TEMPLATE based on the list of indices or keys.
            - If `key` is a string, treats it as a key and returns the corresponding template.

        Returns:
        --------
        dscript or ScriptTemplate : Depending on the type of key, returns either a new dscript object (for slicing or reordering)
                                    or a ScriptTemplate object (for direct key access).
        """
        # Handle list-based reordering
        if isinstance(key, list):
            return self.reorder(key)

        # Handle slicing
        elif isinstance(key, slice):
            new_dscript = dscript(name=f"{self.name}_slice_{key.start}_{key.stop}")
            keys = list(self.TEMPLATE.keys())
            for k in keys[key]:
                new_dscript.TEMPLATE[k] = self.TEMPLATE[k]
            return new_dscript

        # Handle integer indexing with support for negative indices
        elif isinstance(key, int):
            keys = list(self.TEMPLATE.keys())
            if key in self.TEMPLATE:  # Check if the integer exists as a key
                return self.TEMPLATE[key]
            if key < 0:  # Support negative indices
                key += len(keys)
            if key < 0 or key >= len(keys):  # Check for index out of range
                raise IndexError(f"Index {key - len(keys)} is out of range")
            # Return the template corresponding to the integer index
            return self.TEMPLATE[keys[key]]

        # Handle key-based access (string keys)
        elif isinstance(key, str):
            if key in self.TEMPLATE:
                return self.TEMPLATE[key]
            raise KeyError(f"Key '{key}' does not exist in TEMPLATE.")


    def __setitem__(self, key, value):
        if (value == []) or (value is None):
            # If the value is an empty list, delete the corresponding key
            del self.TEMPLATE[key]
        else:
            # Otherwise, set the key to the new ScriptTemplate
            self.TEMPLATE[key] = ScriptTemplate(value, definitions=self.DEFINITIONS, verbose=self.verbose, userid=key)

    def __delitem__(self, key):
        del self.TEMPLATE[key]

    def __iter__(self):
        return iter(self.TEMPLATE.items())

    # def keys(self):
    #     return self.TEMPLATE.keys()

    # def values(self):
    #     return (s.content for s in self.TEMPLATE.values())

    def __contains__(self, key):
        return key in self.TEMPLATE

    def __len__(self):
        return len(self.TEMPLATE)

    def items(self):
        return ((key, s.content) for key, s in self.TEMPLATE.items())

    def __str__(self):
        num_TEMPLATE = len(self.TEMPLATE)
        total_attributes = sum(len(s.attributes) for s in self.TEMPLATE.values())
        return f"{num_TEMPLATE} TEMPLATE, {total_attributes} attributes"

    def __repr__(self):
        """Representation of dscript object with additional properties."""
        repr_str = f"dscript object ({self.name})\n"
        # Add description, role, and version at the beginning
        repr_str += f"id: {self.userid}\n" if self.userid else ""
        repr_str += f"Descr: {self.description}\n" if self.description else ""
        repr_str += f"Role: {self.role} (v. {self.version})\n"
        repr_str += f'SECTIONS {span(self.SECTIONS,",","[","]")} | index: {self.section} | position: {self.position}\n'
        repr_str += f"\n\n\twith {len(self.TEMPLATE)} TEMPLATE"
        repr_str += "s" if len(self.TEMPLATE)>1 else ""
        repr_str += f" (with {len(self.DEFINITIONS)} DEFINITIONS)\n\n"
        # Add TEMPLATE information
        c = 0
        for k, s in self.TEMPLATE.items():
            head = f"|  idx: {c} |  key: {k}  |"
            dashes = (50 - len(head)) // 2
            repr_str += "-" * 50 + "\n"
            repr_str += f"{'<' * dashes}{head}{'>' * dashes}\n{repr(s)}\n"
            c += 1
        return repr_str

    def keys(self):
        """Return the keys of the TEMPLATE."""
        return self.TEMPLATE.keys()

    def values(self):
        """Return the ScriptTemplate objects in TEMPLATE."""
        return self.TEMPLATE.values()

    def reorder(self, order):
        """Reorder the TEMPLATE lines according to a list of indices."""
        # Get the original items as a list of (key, value) pairs
        original_items = list(self.TEMPLATE.items())
        # Create a new dictionary with reordered scripts, preserving original keys
        new_scripts = {original_items[i][0]: original_items[i][1] for i in order}
        # Create a new dscript object with reordered scripts
        reordered_script = dscript()
        reordered_script.TEMPLATE = new_scripts
        return reordered_script


    def get_content_by_index(self, index, do=True, protected=True):
        """
        Returns the content of the ScriptTemplate at the specified index.

        Parameters:
        -----------
        index : int
            The index of the template in the TEMPLATE dictionary.
        do : bool, optional (default=True)
            If True, the content will be processed based on conditions and evaluation flags.
        protected : bool, optional (default=True)
            Controls whether variable evaluation is protected (e.g., prevents overwriting certain definitions).

        Returns:
        --------
        str or list of str
            The content of the template after processing, or an empty string if conditions or evaluation flags block it.
        """
        key = list(self.TEMPLATE.keys())[index]
        s = self.TEMPLATE[key].content
        att = self.TEMPLATE[key].attributes
        # Return an empty string if the facultative attribute is True and do is True
        if att["facultative"] and do:
            return ""
        # Evaluate the condition (if any)
        if att["condition"] is not None:
            cond = eval(self.DEFINITIONS.formateval(att["condition"], protected))
        else:
            cond = True
        # If the condition is met, process the content
        if cond:
            # Apply formateval only if the eval attribute is True and do is True
            if att["eval"] and do:
                if isinstance(s, list):
                    # Apply formateval to each item in the list if s is a list
                    return [self.DEFINITIONS.formateval(line, protected) for line in s]
                else:
                    # Apply formateval to the single string content
                    return self.DEFINITIONS.formateval(s, protected)
            else:
                return s  # Return the raw content if no evaluation is needed
        elif do:
            return ""  # Return an empty string if the condition is not met and do is True
        else:
            return s  # Return the raw content if do is False


    def get_attributes_by_index(self, index):
        """ Returns the attributes of the ScriptTemplate at the specified index."""
        key = list(self.TEMPLATE.keys())[index]
        return self.TEMPLATE[key].attributes


    def __add__(self, other):
        """
        Concatenates two dscript objects, creating a new dscript object that combines
        the TEMPLATE and DEFINITIONS of both. This operation avoids deep copying
        of definitions by creating a new lambdaScriptdata instance from the definitions.

        Parameters:
        -----------
        other : dscript
            The other dscript object to concatenate with the current one.

        Returns:
        --------
        result : dscript
            A new dscript object with the concatenated TEMPLATE and merged DEFINITIONS.

        Raises:
        -------
        TypeError:
            If the other object is not an instance of dscript, script, pipescript
        """
        if isinstance(other, dscript):
            # Step 1: Merge global DEFINITIONS from both self and other
            result = dscript(name=self.name+"+"+other.name,**(self.DEFINITIONS + other.DEFINITIONS))
            # Step 2: Start by copying the TEMPLATE from self (no deepcopy for performance reasons)
            result.TEMPLATE = self.TEMPLATE.copy()
            # Step 3: Ensure that the local definitions for `self.TEMPLATE` are properly copied
            for key, value in self.TEMPLATE.items():
                result.TEMPLATE[key].definitions = lambdaScriptdata(**self.TEMPLATE[key].definitions)
            # Step 4: Track the next available index if keys need to be created
            next_index = len(result.TEMPLATE)
            # Step 5: Add items from the other dscript object, updating or generating new keys as needed
            for key, value in other.TEMPLATE.items():
                if key in result.TEMPLATE:
                    # If key already exists in result, assign a new unique index
                    while next_index in result.TEMPLATE:
                        next_index += 1
                    new_key = next_index
                else:
                    # Use the original key if it doesn't already exist
                    new_key = key
                # Copy the TEMPLATE's content and definitions from `other`
                result.TEMPLATE[new_key] = value
                # Merge the local TEMPLATE definitions from `other`
                result.TEMPLATE[new_key].definitions = lambdaScriptdata(**other.TEMPLATE[key].definitions)
            return result
        elif isinstance(other,script):
            return self.script() + script
        elif isinstance(other,pipescript):
            return self.pipescript() | script
        else:
            raise TypeError(f"Cannot concatenate 'dscript' with '{type(other).__name__}'")


    def __call__(self, *keys):
        """
        Extracts subobjects from the dscript based on the provided keys.

        Parameters:
        -----------
        *keys : one or more keys that correspond to the `TEMPLATE` entries.

        Returns:
        --------
        A new `dscript` object that contains only the selected script lines/items, along with
        the relevant definitions and attributes from the original object.
        """
        # Create a new dscript object to store the extracted sub-objects
        result = dscript(name=f"{self.name}_subobject")
        # Copy the TEMPLATE entries corresponding to the provided keys
        for key in keys:
            if key in self.TEMPLATE:
                result.TEMPLATE[key] = self.TEMPLATE[key]
            else:
                raise KeyError(f"Key '{key}' not found in TEMPLATE.")
        # Copy the DEFINITIONS from the current object
        result.DEFINITIONS = copy.deepcopy(self.DEFINITIONS)
        # Copy other relevant attributes
        result.SECTIONS = self.SECTIONS[:]
        result.section = self.section
        result.position = self.position
        result.role = self.role
        result.description = self.description
        result.userid = self.userid
        result.version = self.version
        result.verbose = self.verbose
        result.printflag = self.printflag

        return result

    def createEmptyVariables(self, vars):
        """
        Creates empty variables in DEFINITIONS if they don't already exist.

        Parameters:
        -----------
        vars : str or list of str
            The variable name or list of variable names to be created in DEFINITIONS.
        """
        if isinstance(vars, str):
            vars = [vars]  # Convert single variable name to list for uniform processing
        for varname in vars:
            if varname not in self.DEFINITIONS:
                self.DEFINITIONS.setattr(varname,"${" + varname + "}")


    def do(self, printflag=None, verbose=None, softrun=False, return_definitions=False,comment_chars="#%", **USER):
        """
        Executes or previews all `ScriptTemplate` instances in `TEMPLATE`, concatenating their processed content.
        Allows for optional headers and footers based on verbosity settings, and offers a preliminary preview mode with `softrun`.
        Accumulates definitions across all templates if `return_definitions=True`.

        Parameters
        ----------
        printflag : bool, optional
            If `True`, enables print output during execution. Defaults to the instance's print flag if `None`.
        verbose : bool, optional
            If `True`, includes headers and footers in the output, providing additional detail.
            Defaults to the instance's verbosity setting if `None`.
        softrun : bool, optional
            If `True`, executes the script in a preliminary mode:
            - Bypasses full variable substitution for a preview of the content, useful for validating structure.
            - If `False` (default), performs full processing, including variable substitutions and evaluations.
        return_definitions : bool, optional
            If `True`, returns a tuple where the second element contains accumulated definitions from all templates.
            If `False` (default), returns only the concatenated 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.
        **USER : keyword arguments
            Allows for the provision of additional user-defined definitions, where each keyword represents a
            definition key and the associated value represents the definition's content. These definitions
            can override or supplement template-level definitions during execution.

        Returns
        -------
        str or tuple
            - If `return_definitions=False`, returns the concatenated output of all `ScriptTemplate` instances,
              with optional headers, footers, and execution summary based on verbosity.
            - If `return_definitions=True`, returns a tuple of (`output`, `accumulated_definitions`), where
              `accumulated_definitions` contains all definitions used across templates.

        Notes
        -----
        - Each `ScriptTemplate` in `TEMPLATE` is processed individually using its own `do()` method.
        - The `softrun` mode provides a preliminary content preview without full variable substitution,
          helpful for inspecting the script structure or gathering local definitions.
        - When `verbose` is enabled, the method includes detailed headers, footers, and a summary of processed and
          ignored items, providing insight into the script's construction and variable usage.
        - Accumulated definitions from each `ScriptTemplate` are combined if `return_definitions=True`, which can be
          useful for tracking all variables and definitions applied across the templates.

        Example
        -------
        >>> dscript_instance = dscript(name="ExampleScript")
        >>> dscript_instance.TEMPLATE[0] = ScriptTemplate(
        ...     content=["units ${units}", "boundary ${boundary}"],
        ...     definitions=lambdaScriptdata(units="lj", boundary="p p p"),
        ...     attributes={'eval': True}
        ... )
        >>> dscript_instance.do(verbose=True, units="real")
        # Output:
        # --------------
        # TEMPLATE "ExampleScript"
        # --------------
        units real
        boundary p p p
        # ---> Total items: 2 - Ignored items: 0
        """

        printflag = self.printflag if printflag is None else printflag
        verbose = self.verbose if verbose is None else verbose
        header = f"# --------------[ TEMPLATE \"{self.name}\" ]--------------" if verbose else ""
        footer = "# --------------------------------------------" if verbose else ""

        # Initialize output, counters, and optional definitions accumulator
        output = [header]
        non_empty_lines = 0
        ignored_lines = 0
        accumulated_definitions = lambdaScriptdata() if return_definitions else None

        for key, template in self.TEMPLATE.items():
            # Process each template with softrun if enabled, otherwise use full processing
            result = template.do(softrun=softrun,USER=lambdaScriptdata(**USER))
            if result:
                # Apply comment removal based on verbosity
                final_result = result if verbose else remove_comments(result,comment_chars=comment_chars)
                if final_result or verbose:
                    output.append(final_result)
                    non_empty_lines += 1
                else:
                    ignored_lines += 1
                # Accumulate definitions if return_definitions is enabled
                if return_definitions:
                    accumulated_definitions += template.definitions
            else:
                ignored_lines += 1

        # Add footer summary if verbose
        nel_word = 'items' if non_empty_lines > 1 else 'item'
        il_word = 'items' if ignored_lines > 1 else 'item'
        footer += f"\n# ---> Total {nel_word}: {non_empty_lines} - Ignored {il_word}: {ignored_lines}" if verbose else ""
        output.append(footer)

        # Concatenate output and determine return type based on return_definitions
        output_content = "\n".join(output)
        return (output_content, accumulated_definitions) if return_definitions else output_content



    def script(self,printflag=None, verbose=None, verbosity=None, **USER):
        """
        returns the corresponding script
        """
        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
        return lamdaScript(self,persistentfile=True, persistentfolder=None,
                           printflag=printflag, verbose=verbose,
                           **USER)

    def pipescript(self, *keys, printflag=None, verbose=None, verbosity=None, **USER):
        """
        Returns a pipescript object by combining script objects corresponding to the given keys.

        Parameters:
        -----------
        *keys : one or more keys that correspond to the `TEMPLATE` entries.
        printflag : bool, optional
            Whether to enable printing of additional information.
        verbose : bool, optional
            Whether to run in verbose mode for debugging or detailed output.
        **USER : dict, optional
            Additional user-defined variables to pass into the script.

        Returns:
        --------
        A `pipescript` object that combines the script objects generated from the selected
        dscript subobjects.
        """
        # Start with an empty pipescript
        # combined_pipescript = None
        # # Iterate over the provided keys to extract corresponding subobjects
        # for key in keys:
        #     # Extract the dscript subobject for the given key
        #     sub_dscript = self(key)
        #     # Convert the dscript subobject to a script object, passing USER, printflag, and verbose
        #     script_obj = sub_dscript.script(printflag=printflag, verbose=verbose, **USER)
        #     # Combine script objects into a pipescript object
        #     if combined_pipescript is None:
        #         combined_pipescript = pipescript(script_obj)  # Initialize pipescript
        #     else:
        #         combined_pipescript = combined_pipescript | script_obj  # Use pipe operator
        # if combined_pipescript is None:
        #     ValueError('The conversion to pipescript from {type{self}} falled')
        # return combined_pipescript
        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

        # Loop over all keys in TEMPLATE and combine them
        combined_pipescript = None
        for key in self.keys():
            # Create a new dscript object with only the current key in TEMPLATE
            focused_dscript = dscript(name=f"{self.name}:{key}")
            focused_dscript.TEMPLATE[key] = self.TEMPLATE[key]
            focused_dscript.TEMPLATE[key].definitions = scriptdata(**self.TEMPLATE[key].definitions)
            focused_dscript.DEFINITIONS = scriptdata(**self.DEFINITIONS)
            focused_dscript.SECTIONS = self.SECTIONS[:]
            focused_dscript.section = self.section
            focused_dscript.position = self.position
            focused_dscript.role = self.role
            focused_dscript.description = self.description
            focused_dscript.userid = self.userid
            focused_dscript.version = self.version
            focused_dscript.verbose = verbose
            focused_dscript.printflag = printflag

            # Convert the focused dscript object to a script object
            script_obj = focused_dscript.script(printflag=printflag, verbose=verbose, **USER)

            # Combine the script objects into a pipescript object using the pipe operator
            if combined_pipescript is None:
                combined_pipescript = pipescript(script_obj)  # Initialize pipescript
            else:
                combined_pipescript = combined_pipescript | pipescript(script_obj)  # Use pipe operator

        if combined_pipescript is None:
            ValueError('The conversion to pipescript from {type{self}} falled')
        return combined_pipescript


    @staticmethod
    def header(name=None, verbose=True, verbosity=None, style=2, filepath=None, version=None, license=None, email=None):
        """
        Generate a formatted header for the DSCRIPT file.

        ### Parameters:
            name (str, optional): The name of the script. If None, "Unnamed" is used.
            verbose (bool, optional): Whether to include the header. Default is True.
            verbosity (int, optional): Verbosity level. Overrides `verbose` if specified.
            style (int, optional): ASCII style for the header (default=2).
            filepath (str, optional): Full path to the file being saved. If None, the line mentioning the file path is excluded.
            version (str, optional): DSCRIPT version. If None, it is omitted from the header.
            license (str, optional): License type. If None, it is omitted from the header.
            email (str, optional): Contact email. If None, it is omitted from the header.

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

        ### The header includes:
            - DSCRIPT version, license, and contact email, if provided.
            - The name of the script.
            - Filepath, if provided.
            - Information on where and when the script was generated.

        ### Notes:
            - If `verbosity` is specified, it overrides `verbose`.
            - Omits metadata lines if `version`, `license`, or `email` are not provided.
        """
        # Resolve verbosity
        verbose = verbosity > 0 if verbosity is not None else verbose
        if not verbose:
            return ""
        # Validate inputs
        if name is None:
            name = "Unnamed"
        # Prepare metadata line
        metadata = []
        if version:
            metadata.append(f"v{version}")
        if license:
            metadata.append(f"License: {license}")
        if email:
            metadata.append(f"Email: {email}")
        metadata_line = " | ".join(metadata)
        # Prepare the framed header content
        lines = []
        if metadata_line:
            lines.append(f"PIZZA.DSCRIPT FILE {metadata_line}")
        lines += [
            "",
            f"Name: {name}",
        ]
        # Add the filepath line if filepath is not None
        if filepath:
            lines.append(f"Path: {filepath}")
        lines += [
            "",
            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 frame_header function to format the framed content
        return frame_header(lines, style=style)



    # Generator -- added on 2024-10-17
    # -----------
    def generator(self):
        """
        Returns
        -------
        STR
            generated code corresponding to dscript (using dscript syntax/language).

        """
        return self.save(generatoronly=True)


    # Save Method -- added on 2024-09-04
    # -----------
    def save(self, filename=None, foldername=None, overwrite=False, generatoronly=False, onlyusedvariables=True):
        """
        Save the current script instance to a text file.

        Parameters
        ----------
        filename : str, optional
            The name of the file to save the script to. If not provided, `self.name` is used.
            The extension ".txt" is automatically appended if not included.

        foldername : str, optional
            The directory where the file will be saved. If not provided, it defaults to the system's
            temporary directory. If the filename does not include a full path, this folder will be used.

        overwrite : bool, default=True
            Whether to overwrite the file if it already exists. If set to False, an exception is raised
            if the file exists.

        generatoronly : bool, default=False
            If True, the method returns the generated content string without saving to a file.

        onlyusedvariables : bool, default=True
            If True, local definitions are only saved if they are used within the template content.
            If False, all local definitions are saved, regardless of whether they are referenced in
            the template.

        Raises
        ------
        FileExistsError
            If the file already exists and `overwrite` is set to False.

        Notes
        -----
        - The script is saved in a plain text format, and each section (global parameters, definitions,
          template, and attributes) is written in a structured format with appropriate comments.
        - If `self.name` is used as the filename, it must be a valid string that can serve as a file name.
        - The file structure follows the format:
            # DSCRIPT SAVE FILE
            # generated on YYYY-MM-DD on user@hostname

            # GLOBAL PARAMETERS
            { ... }

            # DEFINITIONS (number of definitions=...)
            key=value

            # TEMPLATES (number of items=...)
            key: template_content

            # ATTRIBUTES (number of items with explicit attributes=...)
            key:{attr1=value1, attr2=value2, ...}
        """
        # At the beginning of the save method
        start_time = time.time()  # Start the timer

        if not generatoronly:
            # Use self.name if filename is not provided
            if filename is None:
                filename = span(self.name, sep="\n")

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

            # Construct the full path
            if foldername in [None, ""]:  # Handle cases where foldername is None or an empty string
                filepath = os.path.abspath(filename)
            else:
                filepath = os.path.join(foldername, filename)

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

        # Header with current date, username, and host
        header = "# DSCRIPT SAVE FILE\n"
        header += "\n"*2
        if generatoronly:
            header += dscript.header(verbose=True,filepath='dynamic code generation (no file)',
                                     name = self.name, version=self.version, license=self.license, email=self.email)
        else:
            header += dscript.header(verbose=True,filepath=filepath,
                                     name = self.name, version=self.version, license=self.license, email=self.email)
        header += "\n"*2

        # Global parameters in strict Python syntax
        global_params = "# GLOBAL PARAMETERS (8 parameters)\n"
        global_params += "{\n"
        global_params += f"    SECTIONS = {self.SECTIONS},\n"
        global_params += f"    section = {self.section},\n"
        global_params += f"    position = {self.position},\n"
        global_params += f"    role = {self.role!r},\n"
        global_params += f"    description = {self.description!r},\n"
        global_params += f"    userid = {self.userid!r},\n"
        global_params += f"    version = {self.version},\n"
        global_params += f"    verbose = {self.verbose}\n"
        global_params += "}\n"

        # Initialize definitions with self.DEFINITIONS
        allvars = self.DEFINITIONS

        # Temporary dictionary to track global variable information
        global_var_info = {}

        # Loop over each template item to detect and record variable usage and overrides
        for template_index, (key, script_template) in enumerate(self.TEMPLATE.items()):
            # Detect variables used in this template
            used_variables = script_template.detect_variables()

            # Loop over each template item to detect and record variable usage and overrides
            for template_index, (key, script_template) in enumerate(self.TEMPLATE.items()):
                # Detect variables used in this template
                used_variables = script_template.detect_variables()

                # Check each variable used in this template
                for var in used_variables:
                    # Get global and local values for the variable
                    global_value = getattr(allvars, var, None)
                    local_value = getattr(script_template.definitions, var, None)
                    is_global = var in allvars  # Check if the variable originates in global space
                    is_default = is_global and (
                        global_value is None or global_value == "" or global_value == f"${{{var}}}"
                    )

                    # If the variable is not yet tracked, initialize its info
                    if var not in global_var_info:
                        global_var_info[var] = {
                            "value": global_value,       # Initial value from allvars if exists
                            "updatedvalue": global_value,# Initial value from allvars if exists
                            "is_default": is_default,    # Check if it’s set to a default value
                            "first_def": None,           # First definition (to be updated later)
                            "first_use": template_index, # First time the variable is used
                            "first_val": global_value,
                            "override_index": template_index if local_value is not None else None,  # Set override if defined locally
                            "is_global": is_global       # Track if the variable originates as global
                        }
                    else:
                        # Update `override_index` if the variable is defined locally and its value changes
                        if local_value is not None:
                            # Check if the local value differs from the tracked value in global_var_info
                            current_value = global_var_info[var]["value"]
                            if current_value != local_value:
                                global_var_info[var]["override_index"] = template_index
                                global_var_info[var]["updatedvalue"] = local_value  # Update the tracked value


            # Second loop: Update `first_def` for all variables
            for template_index, (key, script_template) in enumerate(self.TEMPLATE.items()):
                local_definitions = script_template.definitions.keys()
                for var in local_definitions:
                    if var in global_var_info and global_var_info[var]["first_def"] is None:
                        global_var_info[var]["first_def"] = template_index
                        global_var_info[var]["first_val"] = getattr(script_template.definitions, var)


        # Filter global definitions based on usage, overrides, and first_def
        filtered_globals = {
            var: info for var, info in global_var_info.items()
            if (info["is_global"] or info["is_default"]) and (info["override_index"] is None)
            }


        # Generate the definitions output based on filtered globals
        definitions = f"\n# GLOBAL DEFINITIONS (number of definitions={len(filtered_globals)})\n"
        for var, info in filtered_globals.items():
            if info["is_default"] and (info["first_def"]>info["first_use"] if info["first_def"] else True):
                definitions += f"{var} = ${{{var}}}  # value assumed to be defined outside this DSCRIPT file\n"
            else:
                value = info["first_val"] #info["value"]
                if value in ["", None]:
                    definitions += f'{var} = ""\n'
                elif isinstance(value, str):
                    safe_value = value.replace('\\', '\\\\').replace('\n', '\\n')
                    definitions += f"{var} = {safe_value}\n"
                else:
                    definitions += f"{var} = {value}\n"

        # Template (number of lines/items)
        printsinglecontent = False
        template = f"\n# TEMPLATES (number of items={len(self.TEMPLATE)})\n"
        for key, script_template in self.TEMPLATE.items():
            # Get local template definitions and detected variables
            template_vars = script_template.definitions
            used_variables = script_template.detect_variables()
            islocal = False
            # Temporary dictionary to accumulate variables to add to allvars
            valid_local_vars = lambdaScriptdata()
            # Write template-specific definitions only if they meet the updated conditions
            for var in template_vars.keys():
                # Conditions for adding a variable to the local template and to `allvars`
                if (var in used_variables or not onlyusedvariables) and (
                    script_template.is_variable_set_value_only(var) and
                    (var not in allvars or getattr(template_vars, var) != getattr(allvars, var))
                ):
                    # Start local definitions section if this is the first local variable for the template
                    if not islocal:
                        template += f"\n# LOCAL DEFINITIONS for key '{key}'\n"
                        islocal = True
                    # Retrieve and process the variable value
                    value = getattr(template_vars, var)
                    if value in ["", None]:
                        template += f'{var} = ""\n'  # Set empty or None values as ""
                    elif isinstance(value, str):
                        safe_value = value.replace('\\', '\\\\').replace('\n', '\\n')
                        template += f"{var} = {safe_value}\n"
                    else:
                        template += f"{var} = {value}\n"
                    # Add the variable to valid_local_vars for selective update of allvars
                    valid_local_vars.setattr(var, value)
            # Update allvars only with filtered, valid local variables
            allvars += valid_local_vars

            # Write the template content
            if isinstance(script_template.content, list):
                if len(script_template.content) == 1:
                    # Single-line template saved as a single line
                    content_str = script_template.content[0].strip()
                    template += "" if printsinglecontent else "\n"
                    template += f"{key}: {content_str}\n"
                    printsinglecontent = True
                else:
                    content_str = '\n    '.join(script_template.content)
                    template += f"\n{key}: [\n    {content_str}\n ]\n"
                    printsinglecontent = False
            else:
                template += "" if printsinglecontent else "\n"
                template += f"{key}: {script_template.content}\n"
                printsinglecontent = True

        # Attributes (number of lines/items with explicit attributes)
        attributes = f"# ATTRIBUTES (number of items with explicit attributes={len(self.TEMPLATE)})\n"
        for key, script_template in self.TEMPLATE.items():
            attr_str = ", ".join(f"{attr_name}={repr(attr_value)}"
                                 for attr_name, attr_value in script_template.attributes.items())
            attributes += f"{key}:{{{attr_str}}}\n"

        # Combine all sections into one content
        content = header + "\n" + global_params + "\n" + definitions + "\n" + template + "\n" + attributes + "\n"


        # Append footer information to the content
        non_empty_lines = sum(1 for line in content.splitlines() if line.strip())  # Count non-empty lines
        execution_time = time.time() - start_time  # Calculate the execution time in seconds
        # Prepare the footer content
        footer_lines = [
            ["Non-empty lines", str(non_empty_lines)],
            ["Execution time (seconds)", f"{execution_time:.4f}"],
        ]
        # Format footer into tabular style
        footer_content = [
            f"{row[0]:<25} {row[1]:<15}" for row in footer_lines
        ]
        # Use frame_header to format footer
        footer = frame_header(
            lines=["DSCRIPT SAVE FILE generator"] + footer_content,
            style=1
        )
        # Append footer to the content
        content += f"\n{footer}"

        if generatoronly:
            return content
        else:
            # Write the content to the file
            with open(filepath, 'w') as f:
                f.write(content)
            print(f"\nScript saved to {filepath}")
            return filepath



    # Write Method -- added on 2024-09-05
    # ------------
    @staticmethod
    @staticmethod
    def write(scriptcontent, filename=None, foldername=None, overwrite=False):
        """
        Writes the provided script content to a specified file in a given folder, with a header if necessary.

        Parameters
        ----------
        scriptcontent : str
            The content to be written to the file.

        filename : str, optional
            The name of the file. If not provided, a random name will be generated.
            The extension `.txt` will be appended if not already present.

        foldername : str, optional
            The folder where the file will be saved. If not provided, the current working directory is used.

        overwrite : bool, optional
            If False (default), raises a `FileExistsError` if the file already exists. If True, the file will be overwritten if it exists.

        Returns
        -------
        str
            The full path to the written file.

        Raises
        ------
        FileExistsError
            If the file already exists and `overwrite` is set to False.

        Notes
        -----
        - A header is prepended to the content if it does not already exist, using the `header` method.
        - The header includes metadata such as the current date, username, hostname, and file details.
        """
        # Generate a random name if filename is not provided
        if filename is None:
            filename = autoname(8)  # Generates a random name of 8 letters

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

        # Handle foldername and relative paths
        if foldername is None or foldername == "":
            # If foldername is empty or None, use current working directory for relative paths
            if not os.path.isabs(filename):
                filepath = os.path.join(os.getcwd(), filename)
            else:
                filepath = filename  # If filename is absolute, use it directly
        else:
            # If foldername is provided and filename is not absolute, use foldername
            if not os.path.isabs(filename):
                filepath = os.path.join(foldername, filename)
            else:
                filepath = filename

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

        # Count total and non-empty lines in the content
        total_lines = len(scriptcontent.splitlines())
        non_empty_lines = sum(1 for line in scriptcontent.splitlines() if line.strip())

        # Prepare header if not already present
        if not scriptcontent.startswith("# DSCRIPT SAVE FILE"):
            fname = os.path.basename(filepath)  # Extracts the filename (e.g., "myscript.txt")
            name, _ = os.path.splitext(fname)   # Removes the extension, e.g., "myscript"
            metadata = get_metadata()           # retrieve all metadata (statically)
            header = dscript.header(name=name, verbosity=True,style=1,filepath=filepath,
                    version = metadata["version"], license = metadata["license"], email = metadata["email"])
            # Add line count information to the header
            footer = frame_header(
                lines=[
                    f"Total lines written: {total_lines}",
                    f"Non-empty lines: {non_empty_lines}"
                ],
                style=1
            )
            scriptcontent = header + "\n" + scriptcontent + "\n" + footer

        # Write the content to the file
        with open(filepath, 'w') as file:
            file.write(scriptcontent)
        return filepath



    # Load Method and its Parsing Rules -- added on 2024-09-04
    # ---------------------------------
    @classmethod
    def load(cls, filename, foldername=None, numerickeys=True):
        """
        Load a script instance from a text file.

        Parameters
        ----------
        filename : str
            The name of the file to load the script from. If the filename does not end with ".txt",
            the extension is automatically appended.

        foldername : str, optional
            The directory where the file is located. If not provided, it defaults to the system's
            temporary directory. If the filename does not include a full path, this folder will be used.

        numerickeys : bool, default=True
            If True, numeric string keys in the template section are automatically converted into integers.
            For example, the key "0" would be converted into the integer 0.

        Returns
        -------
        dscript
            A new `dscript` instance populated with the content of the loaded file.

        Raises
        ------
        ValueError
            If the file does not start with the correct DSCRIPT header or the file format is invalid.

        FileNotFoundError
            If the specified file does not exist.

        Notes
        -----
        - The file is expected to follow the same structured format as the one produced by the `save()` method.
        - The method processes global parameters, definitions, template lines/items, and attributes. If the file
          includes numeric keys as strings (e.g., "0", "1"), they can be automatically converted into integers
          if `numerickeys=True`.
        - The script structure is dynamically rebuilt, and each section (global parameters, definitions,
          template, and attributes) is correctly parsed and assigned to the corresponding parts of the `dscript`
          instance.
        """

        # Step 0 validate filepath
        if not filename.endswith('.txt'):
            filename += '.txt'

        # Handle foldername and relative paths
        if foldername is None or foldername == "":
            # If the foldername is empty or None, use current working directory for relative paths
            if not os.path.isabs(filename):
                filepath = os.path.join(os.getcwd(), filename)
            else:
                filepath = filename  # If filename is absolute, use it directly
        else:
            # If foldername is provided and filename is not absolute, use foldername
            if not os.path.isabs(filename):
                filepath = os.path.join(foldername, filename)
            else:
                filepath = filename

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

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

        # Call parsesyntax to parse the file content
        fname = os.path.basename(filepath)  # Extracts the filename (e.g., "myscript.txt")
        name, _ = os.path.splitext(fname)   # Removes the extension, e.g., "myscript
        return cls.parsesyntax(content, name, numerickeys)



    # Load Method and its Parsing Rules -- added on 2024-09-04
    # ---------------------------------
    @classmethod
    def parsesyntax(cls, content, name=None, numerickeys=True, verbose=False, authentification=True,
                    comment_chars="#%",continuation_marker="..."):
        """
        Parse a DSCRIPT script from a string content.

        Parameters
        ----------
        content : str
            The string content of the DSCRIPT script to be parsed.

        name : str, optional
            The name of the dscript project. If `None`, a random name is generated.

        numerickeys : bool, default=True
            If `True`, numeric string keys in the template section are automatically converted into integers.

        verbose : bool, default=False
            If `True`, the parser will output warnings for unrecognized lines outside of blocks.

        authentification : bool, default=True
            If `True`, the parser is expected that the first non empty line is # DSCRIPT SAVE FILE

        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, optional (default: "...")
            A string containing characters to indicate line continuation
            Any characters after the continuation marker are considered comment and are theorefore ignored



        Returns
        -------
        dscript
            A new `dscript` instance populated with the content of the loaded file.

        Raises
        ------
        ValueError
            If content does not start with the correct DSCRIPT header or the file format is invalid.

        Notes
        -----
        **DSCRIPT SAVE FILE FORMAT**

        This script syntax is designed for creating dynamic and customizable input files, where variables, templates,
        and control attributes can be defined in a flexible manner.

        **Mandatory First Line:**

        Every DSCRIPT file must begin with the following line:

        ```plaintext
        # DSCRIPT SAVE FILE
        ```

        **Structure Overview:**

        1. **Global Parameters Section (Optional):**

            - This section defines global script settings, enclosed within curly braces `{}`.
            - Properties include:
                - `SECTIONS`: List of section names to be considered (e.g., `["DYNAMIC"]`).
                - `section`: Current section index (e.g., `0`).
                - `position`: Current script position in the order.
                - `role`: Defines the role of the script instance (e.g., `"dscript instance"`).
                - `description`: A short description of the script (e.g., `"dynamic script"`).
                - `userid`: Identifier for the user (e.g., `"dscript"`).
                - `version`: Script version (e.g., `0.1`).
                - `verbose`: Verbosity flag, typically a boolean (e.g., `False`).

            **Example:**

            ```plaintext
            {
                SECTIONS = ['INITIALIZATION', 'SIMULATION']  # Global script parameters
            }
            ```

        2. **Definitions Section:**

            - Variables are defined in Python-like syntax, allowing for dynamic variable substitution.
            - Variables can be numbers, strings, or lists, and they can include placeholders using `$`
              to delay execution or substitution.

            **Example:**

            ```plaintext
            d = 3                               # Define a number
            periodic = "$p"                     # '$' prevents immediate evaluation of 'p'
            units = "$metal"                    # '$' prevents immediate evaluation of 'metal'
            dimension = "${d}"                  # Variable substitution
            boundary = ['p', 'p', 'p']          # List with a mix of variables and values
            atom_style = "$atomic"              # String variable with delayed evaluation
            ```

        3. **Templates Section:**

            - This section provides a mapping between keys and their corresponding commands or instructions.
            - Each template can reference variables defined in the **Definitions** section or elsewhere, typically using the `${variable}` syntax.
            - **Syntax Variations**:

                Templates can be defined in several ways, including without blocks, with single- or multi-line blocks, and with ellipsis (`...`) as a line continuation marker.

                - **Single-line Template Without Block**:
                    ```plaintext
                    KEY: INSTRUCTION
                    ```
                    - `KEY` is the identifier for the template (numeric or alphanumeric).
                    - `INSTRUCTION` is the command or template text, which may reference variables.

                - **Single-line Template With Block**:
                    ```plaintext
                    KEY: [INSTRUCTION]
                    ```
                    - Uses square brackets (`[ ]`) around the `INSTRUCTION`, indicating that all instructions are part of the block.

                - **Multi-line Template With Block**:
                    ```plaintext
                    KEY: [
                        INSTRUCTION1
                        INSTRUCTION2
                        ...
                        ]
                    ```
                    - Begins with `KEY: [` and ends with a standalone `]` on a new line.
                    - Instructions within the block can span multiple lines, and ellipses (`...`) at the end of a line are used to indicate that the line continues, ignoring any content following the ellipsis as comments.
                    - Comments following ellipses are removed after parsing and do not become part of the block, preserving only the instructions.

                - **Multi-line Template With Continuation Marker (Ellipsis)**:
                    - For templates with complex code containing square brackets (`[ ]`), the ellipsis (`...`) can be used to prevent `]` from prematurely closing the block. The ellipsis will keep the line open across multiple lines, allowing brackets in the instructions.

                    **Example:**
                    ```plaintext
                    example1: command ${value}       # Single-line template without block
                    example2: [command ${value}]     # Single-line template with block

                    # Multi-line template with block
                    example3: [
                        command1 ${var1}
                        command2 ${var2} ...   # Line continues after ellipsis
                        command3 ${var3} ...   # Additional instruction continues
                        ]

                    # Multi-line template with ellipsis (handling square brackets)
                    example4: [
                        A[0][1] ...            # Ellipsis allows [ ] within instructions
                        B[2][3] ...            # Another instruction in the block
                        ]
                    ```

            - **Key Points**:
                - **Blocks** allow grouping of multiple instructions for a single key, enclosed in square brackets.
                - **Ellipsis (`...`)** at the end of a line keeps the line open, preventing premature closing by `]`, especially useful if the template code includes square brackets (`[ ]`).
                - **Comments** placed after the ellipsis are removed after parsing and are not part of the final block content.

            This flexibility supports both simple and complex template structures, allowing instructions to be grouped logically while keeping code and comments distinct.


        4. **Attributes Section:**

            - Each template line can have customizable attributes to control behavior and conditions.
            - Default attributes include:
                - `facultative`: If `True`, the line is optional and can be removed if needed.
                - `eval`: If `True`, the line will be evaluated with Python's `eval()` function.
                - `readonly`: If `True`, the line cannot be modified later in the script.
                - `condition`: An expression that must be satisfied for the line to be included.
                - `condeval`: If `True`, the condition will be evaluated using `eval()`.
                - `detectvar`: If `True`, this creates variables in the **Definitions** section if they do not exist.

            **Example:**

            ```plaintext
            units: {facultative=False, eval=False, readonly=False, condition="${units}", condeval=False, detectvar=True}
            dim: {facultative=False, eval=False, readonly=False, condition=None, condeval=False, detectvar=True}
            ```

        **Note on Multiple Definitions**

        This example demonstrates how variables defined in the **Definitions** section are handled for each template.
        Each template retains its own snapshot of the variable definitions at the time it is created, ensuring that templates
        can use different values for the same variable if redefined.

        **Example:**

        ```plaintext
        # DSCRIPT SAVE FILE

        # Definitions
        var = 10

        # Template key1
        key1: Template content with ${var}

        # Definitions
        var = 20

        # Template key2
        key2: Template content with ${var}

        # Template key3
        key3:[
            this is an undefined variable ${var31}
            this is another undefined variable ${var32}
            this variable is defined  ${var}
        ]
        ```

        **Parsing and Usage:**

        ```python
        # Parse content using parsesyntax()
        ds = dscript.parsesyntax(content)

        # Accessing templates and their variables
        print(ds.TEMPLATE['key1'].text)  # Output: Template content with 10
        print(ds.TEMPLATE['key2'].text)  # Output: Template content with 20
        ```

        **Handling Undefined Variables:**

        Variables like `${var31}` and `${var32}` in `key3` are undefined. The parser will handle them based on your substitution logic or raise an error if they are required.

        **Important Notes:**

        - The parser processes the script sequentially. Definitions must appear before the templates that use them.
        - Templates capture the variable definitions at the time they are parsed. Redefining a variable affects only subsequent templates.
        - Comments outside of blocks are allowed and ignored by the parser.
        - Content within templates is treated as-is, allowing for any syntax required by the target system (e.g., LAMMPS commands).


    **Advanced Example**

        Here's a more advanced example demonstrating the use of global definitions, local definitions, templates, and how to parse and render the template content.

        ```python
        content = '''
            # GLOBAL DEFINITIONS
            dumpfile = $dump.LAMMPS
            dumpdt = 50
            thermodt = 100
            runtime = 5000

            # LOCAL DEFINITIONS for step '0'
            dimension = 3
            units = $si
            boundary = ['f', 'f', 'f']
            atom_style = $smd
            atom_modify = ['map', 'array']
            comm_modify = ['vel', 'yes']
            neigh_modify = ['every', 10, 'delay', 0, 'check', 'yes']
            newton = $off
            name = $SimulationBox

            # This is a comment line outside of blocks
            # ------------------------------------------

            0: [    % --------------[ Initialization Header (helper) for "${name}" ]--------------
                # set a parameter to None or "" to remove the definition
                dimension    ${dimension}
                units        ${units}
                boundary     ${boundary}
                atom_style   ${atom_style}
                atom_modify  ${atom_modify}
                comm_modify  ${comm_modify}
                neigh_modify ${neigh_modify}
                newton       ${newton}
                # ------------------------------------------
             ]
        '''
        # Parse the content
        ds = dscript.parsesyntax(content, verbose=True, authentification=False)

        # Access and print the rendered template
        print("Template 0 content:")
        print(ds.TEMPLATE[0].do())
        ```

        **Explanation:**

        - **Global Definitions:** Define variables that are accessible throughout the script.
        - **Local Definitions for Step '0':** Define variables specific to a particular step or template.
        - **Template Block:** Identified by `0: [ ... ]`, it contains the content where variables will be substituted.
        - **Comments:** Lines starting with `#` are comments and are ignored by the parser outside of template blocks.

        **Expected Output:**

        ```
        Template 0 content:
        # --------------[ Initialization Header (helper) for "SimulationBox" ]--------------
        # set a parameter to None or "" to remove the definition
        dimension    3
        units        si
        boundary     ['f', 'f', 'f']
        atom_style   smd
        atom_modify  ['map', 'array']
        comm_modify  ['vel', 'yes']
        neigh_modify ['every', 10, 'delay', 0, 'check', 'yes']
        newton       off
        # ------------------------------------------
        ```

        **Notes:**

        - The `do()` method renders the template, substituting variables with their defined values.
        - Variables like `${dimension}` are replaced with their corresponding values defined in the local or global definitions.
        - The parser handles comments and blank lines appropriately, ensuring they don't interfere with the parsing logic.


        """
        # Split the content into lines
        lines = content.splitlines()
        if not lines:
            raise ValueError("File/Content is empty or only contains blank lines.")

        # Initialize containers
        global_params = {}
        GLOBALdefinitions = lambdaScriptdata()
        LOCALdefinitions = lambdaScriptdata()
        template = {}
        attributes = {}

        # State variables
        inside_global_params = False
        global_params_content = ""
        inside_template_block = False
        current_template_key = None
        current_template_content = []
        current_var_value = lambdaScriptdata()

        # Initialize line number
        line_number = 0
        last_successful_line = 0

        # Step 1: Authenticate the file
        if authentification:
            auth_line_found = False
            max_header_lines = 10
            header_end_idx = -1
            for idx, line in enumerate(lines[:max_header_lines]):
                stripped_line = line.strip()
                if not stripped_line:
                    continue
                if stripped_line.startswith("# DSCRIPT SAVE FILE"):
                    auth_line_found = True
                    header_end_idx = idx
                    break
                elif stripped_line.startswith("#") or stripped_line.startswith("%"):
                    continue
                else:
                    raise ValueError(f"Unexpected content before authentication line (# DSCRIPT SAVE FILE) at line {idx + 1}:\n{line}")
            if not auth_line_found:
                raise ValueError("File/Content is not a valid DSCRIPT file.")

            # Remove header lines
            lines = lines[header_end_idx + 1:]
            line_number = header_end_idx + 1
            last_successful_line = line_number - 1
        else:
            line_number = 0
            last_successful_line = 0

        # Process each line
        for idx, line in enumerate(lines):
            line_number += 1
            line_content = line.rstrip('\n')

            # Determine if we're inside a template block
            if inside_template_block:
                # Extract the code with its eventual continuation_marker
                code_line = remove_comments(
                        line_content,
                        comment_chars=comment_chars,
                        continuation_marker=continuation_marker,
                        remove_continuation_marker=False,
                        ).rstrip()

                # Check if line should continue
                if code_line.endswith(continuation_marker):
                    # Append line up to the continuation marker
                    endofline_index = line_content.rindex(continuation_marker)
                    trimmed_content = line_content[:endofline_index].rstrip()
                    if trimmed_content:
                        current_template_content.append(trimmed_content)
                    continue
                elif code_line.endswith("]"):  # End of multi-line block
                    closing_index = code_line.rindex(']')
                    trimmed_content = code_line[:closing_index].rstrip()

                    # Append any valid content before `]`, if non-empty
                    if trimmed_content:
                        current_template_content.append(trimmed_content)

                    # End of template block
                    content = '\n'.join(current_template_content)
                    template[current_template_key] = ScriptTemplate(
                        content=content,
                        autorefresh=False,
                        definitions=LOCALdefinitions,
                        verbose=verbose,
                        userid=current_template_key)
                    # Refresh variables definitions
                    template[current_template_key].refreshvar(globaldefinitions=GLOBALdefinitions)
                    LOCALdefinitions = lambdaScriptdata()
                    # Reset state for next block
                    inside_template_block = False
                    current_template_key = None
                    current_template_content = []
                    last_successful_line = line_number
                    continue
                else:
                    # Append the entire original line content if not ending with `...` or `]`
                    current_template_content.append(line_content)
                continue

            # Not inside a template block
            stripped_no_comments = remove_comments(line_content)

            # Ignore empty lines after removing comments
            if not stripped_no_comments.strip():
                continue

            # If the original line is a comment line, skip it
            if line_content.strip().startswith("#") or line_content.strip().startswith("%"):
                continue

            stripped = stripped_no_comments.strip()

            # Handle start of a new template block
            template_block_match = re.match(r'^(\w+)\s*:\s*\[', stripped)
            if template_block_match:
                current_template_key = template_block_match.group(1)
                if inside_template_block:
                    # Collect error context
                    context_start = max(0, last_successful_line - 3)
                    context_end = min(len(lines), line_number + 2)
                    error_context_lines = lines[context_start:context_end]
                    error_context = ""
                    for i, error_line in enumerate(error_context_lines):
                        line_num = context_start + i + 1
                        indicator = ">" if line_num == line_number else "*" if line_num == last_successful_line else " "
                        error_context += f"{indicator} {line_num}: {error_line}\n"

                    raise ValueError(
                        f"Template block '{current_template_key}' starting at line {last_successful_line} (*) was not properly closed before starting a new one at line {line_number} (>).\n\n"
                        f"Error context:\n{error_context}"
                    )
                else:
                    inside_template_block = True
                    idx_open_bracket = line_content.index('[')
                    remainder = line_content[idx_open_bracket + 1:].strip()
                    if remainder:
                        remainder_code = remove_comments(remainder, comment_chars=comment_chars).rstrip()
                        if remainder_code.endswith("]"):
                            closing_index = remainder_code.rindex(']')
                            content_line = remainder_code[:closing_index].strip()
                            if content_line:
                                current_template_content.append(content_line)
                            content = '\n'.join(current_template_content)
                            template[current_template_key] = ScriptTemplate(
                                content=content,
                                autorefresh=False,
                                definitions=LOCALdefinitions,
                                verbose=verbose,
                                userid=current_template_key)
                            template[current_template_key].refreshvar(globaldefinitions=GLOBALdefinitions)
                            LOCALdefinitions = lambdaScriptdata()
                            inside_template_block = False
                            current_template_key = None
                            current_template_content = []
                            last_successful_line = line_number
                            continue
                        else:
                            current_template_content.append(remainder)
                    last_successful_line = line_number
                continue

            # Handle start of global parameters
            if stripped.startswith('{') and not inside_global_params:
                if '}' in stripped:
                    global_params_content = stripped
                    cls._parse_global_params(global_params_content.strip(), global_params)
                    global_params_content = ""
                    last_successful_line = line_number
                else:
                    inside_global_params = True
                    global_params_content = stripped
                continue

            # Handle global parameters inside {...}
            if inside_global_params:
                global_params_content += ' ' + stripped
                if '}' in stripped:
                    inside_global_params = False
                    cls._parse_global_params(global_params_content.strip(), global_params)
                    global_params_content = ""
                    last_successful_line = line_number
                continue

            # Handle attributes
            attribute_match = re.match(r'^(\w+)\s*:\s*\{(.+)\}', stripped)
            if attribute_match:
                key, attr_content = attribute_match.groups()
                attributes[key] = {}
                cls._parse_attributes(attributes[key], attr_content.strip())
                last_successful_line = line_number
                continue

            # Handle definitions
            definition_match = re.match(r'^(\w+)\s*=\s*(.+)', stripped)
            if definition_match:
                key, value = definition_match.groups()
                convertedvalue = cls._convert_value(value)
                if key in GLOBALdefinitions:
                    if (GLOBALdefinitions.getattr(key) != convertedvalue) or \
                        (getattr(current_var_value, key) != convertedvalue):
                        LOCALdefinitions.setattr(key, convertedvalue)
                else:
                    GLOBALdefinitions.setattr(key, convertedvalue)
                last_successful_line = line_number
                setattr(current_var_value, key, convertedvalue)
                continue

            # Handle single-line templates
            template_match = re.match(r'^(\w+)\s*:\s*(.+)', stripped)
            if template_match:
                key, content = template_match.groups()
                template[key] = ScriptTemplate(
                    content = content.strip(),
                    autorefresh = False,
                    definitions=LOCALdefinitions,
                    verbose=verbose,
                    userid=key)
                template[key].refreshvar(globaldefinitions=GLOBALdefinitions)
                LOCALdefinitions = lambdaScriptdata()
                last_successful_line = line_number
                continue

            # Unrecognized line
            if verbose:
                print(f"Warning: Unrecognized line at {line_number}: {line_content}")
            last_successful_line = line_number
            continue

        # At the end, check if any template block was left unclosed
        if inside_template_block:
            # Collect error context
            context_start = max(0, last_successful_line - 3)
            context_end = min(len(lines), last_successful_line + 3)
            error_context_lines = lines[context_start:context_end]
            error_context = ""
            for i, error_line in enumerate(error_context_lines):
                line_num = context_start + i
                indicator = ">" if line_num == last_successful_line else " "
                error_context += f"{indicator} {line_num}: {error_line}\n"

            raise ValueError(
                f"Template block '{current_template_key}' starting at line {last_successful_line} was not properly closed.\n\n"
                f"Error context:\n{error_context}"
            )

        # Apply attributes to templates
        for key in attributes:
            if key in template:
                for attr_name, attr_value in attributes[key].items():
                    setattr(template[key], attr_name, attr_value)
                template[key]._autorefresh = True # restore the default behavior for the end-user
            else:
                raise ValueError(f"Attributes found for undefined template key: {key}")

        # Create and return new instance
        if name is None:
            name = autoname(8)
        instance = cls(
            name=name,
            SECTIONS=global_params.get('SECTIONS', ['DYNAMIC']),
            section=global_params.get('section', 0),
            position=global_params.get('position', 0),
            role=global_params.get('role', 'dscript instance'),
            description=global_params.get('description', 'dynamic script'),
            userid=global_params.get('userid', 'dscript'),
            version=global_params.get('version', 0.1),
            verbose=global_params.get('verbose', False)
        )

        # Convert numeric string keys to integers if numerickeys is True
        if numerickeys:
            numeric_template = {}
            for key, value in template.items():
                if key.isdigit():
                    numeric_template[int(key)] = value
                else:
                    numeric_template[key] = value
            template = numeric_template

        # Set definitions and template
        instance.DEFINITIONS = GLOBALdefinitions
        instance.TEMPLATE = template

        # Refresh variables
        instance.set_all_variables()

        # Check variables
        instance.check_all_variables(verbose=False)

        return instance




    @classmethod
    def parsesyntax_legacy(cls, content, name=None, numerickeys=True):
        """
        Parse a script from a string content.
        [ ------------------------------------------------------]
        [ Legacy parsesyntax method for backward compatibility. ]
        [ ------------------------------------------------------]

        Parameters
        ----------
        content : str
            The string content of the script to be parsed.

        name : str
            The name of the dscript project (if None, it is set randomly)

        numerickeys : bool, default=True
            If True, numeric string keys in the template section are automatically converted into integers.

        Returns
        -------
        dscript
            A new `dscript` instance populated with the content of the loaded file.

        Raises
        ------
        ValueError
            If content does not start with the correct DSCRIPT header or the file format is invalid.

        Notes
        -----
        - The file is expected to follow the same structured format as the one produced by the `save()` method.
        - The method processes global parameters, definitions, template lines/items, and attributes. If the file
          includes numeric keys as strings (e.g., "0", "1"), they can be automatically converted into integers
          if `numerickeys=True`.
        - The script structure is dynamically rebuilt, and each section (global parameters, definitions,
          template, and attributes) is correctly parsed and assigned to the corresponding parts of the `dscript`
          instance.


        PIZZA.DSCRIPT SAVE FILE FORMAT
        -------------------------------
        This script syntax is designed for creating dynamic and customizable input files, where variables, templates,
        and control attributes can be defined in a flexible manner.

        ### Mandatory First Line:
        Every DSCRIPT file must begin with the following line:
            # DSCRIPT SAVE FILE

        ### Structure Overview:

        1. **Global Parameters Section (Optional):**
            - This section defines global script settings, enclosed within curly braces `{ }`.
            - Properties include:
                - `SECTIONS`: List of section names to be considered (e.g., `["DYNAMIC"]`).
                - `section`: Current section index (e.g., `0`).
                - `position`: Current script position in the order.
                - `role`: Defines the role of the script instance (e.g., `"dscript instance"`).
                - `description`: A short description of the script (e.g., `"dynamic script"`).
                - `userid`: Identifier for the user (e.g., `"dscript"`).
                - `version`: Script version (e.g., `0.1`).
                - `verbose`: Verbosity flag, typically a boolean (e.g., `False`).

            Example:
            ```
            {
                SECTIONS = ['INITIALIZATION', 'SIMULATION']  # Global script parameters
            }
            ```

        2. **Definitions Section:**
            - Variables are defined in Python-like syntax, allowing for dynamic variable substitution.
            - Variables can be numbers, strings, or lists, and they can include placeholders using `$`
              to delay execution or substitution.

            Example:
            ```
            d = 3                               # Define a number
            periodic = "$p"                     # '$' prevents immediate evaluation of 'p'
            units = "$metal"                    # '$' prevents immediate evaluation of 'metal'
            dimension = "${d}"                  # Variable substitution
            boundary = ['p', 'p', 'p']  # List with a mix of variables and values
            atom_style = "$atomic"              # String variable with delayed evaluation
            ```

        3. **Templates Section:**
            - This section provides a mapping between keys and their corresponding commands or instructions.
            - The templates reference variables defined in the **Definitions** section or elsewhere.
            - Syntax:
                ```
                KEY: INSTRUCTION
                ```
                where:
                - `KEY` can be numeric or alphanumeric.
                - `INSTRUCTION` represents a command template, often referring to variables using `${variable}` notation.

            Example:
            ```
            units: units ${units}               # Template uses the 'units' variable
            dim: dimension ${dimension}         # Template for setting the dimension
            bound: boundary ${boundary}         # Template for boundary settings
            lattice: lattice ${lattice}         # Lattice template
            ```

        4. **Attributes Section:**
            - Each template line can have customizable attributes to control behavior and conditions.
            - Default attributes include:
                - `facultative`: If `True`, the line is optional and can be removed if needed.
                - `eval`: If `True`, the line will be evaluated with Python's `eval()` function.
                - `readonly`: If `True`, the line cannot be modified later in the script.
                - `condition`: An expression that must be satisfied for the line to be included.
                - `condeval`: If `True`, the condition will be evaluated using `eval()`.
                - `detectvar`: If `True`, this creates variables in the **Definitions** section if they do not exist.

            Example:
            ```
            units: {facultative=False, eval=False, readonly=False, condition="${units}", condeval=False, detectvar=True}
            dim: {facultative=False, eval=False, readonly=False, condition=None, condeval=False, detectvar=True}
            ```

        Note on multiple definitions
        -----------------------------
        This example demonstrates how variables defined in the `Definitions` section are handled for each template.
        Each template retains its own snapshot of the variable definitions at the time it is created, ensuring that templates
        can use different values for the same variable if redefined.

        content = "" "
        # DSCRIPT SAVE FILE

        # Definitions
        var = 10

        # Template ky1
        key1: Template content with ${var}

        # Definitions
        var = 20

        # Template key2
        key2: Template content with ${var}

        # Template key3
        key3:[
            this is an underfined variable ${var31}
            this is an another underfined variable ${var32}
            this variables is defined  ${var}
            ]

        "" "

        # Parse content using parsesyntax()
        ds = dscript.parsesyntax(content)

        # Key1 should use the first definition of 'var' (10)
        print(ds.key1.definitions.var)  # Output: Template content with 10

        # Key2 should use the updated definition of 'var' (20)
        print(ds.key2.definitions.var)  # Output: Template content with 10


        """

        # Split the content into lines
        lines = content.splitlines()
        lines = [line for line in lines if line.strip()]  # Remove blank or empty lines
        # Raise an error if no content is left after removing blank lines
        if not lines:
            raise ValueError("File/Content is empty or only contains blank lines.")

        # Initialize containers for global parameters, definitions, templates, and attributes
        global_params = {}
        definitions = lambdaScriptdata()
        template = {}
        attributes = {}

        # State variables to handle multi-line global parameters and attributes
        inside_global_params = False
        inside_attributes = False
        current_attr_key = None  # Ensure this is properly initialized
        global_params_content = ""
        inside_template_block = False  # Track if we are inside a multi-line template
        current_template_key = None    # Track the current template key
        current_template_content = []  # Store lines for the current template content

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

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

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

            # Remove trailing comments
            stripped = remove_comments(stripped)

            # Step 3: Handle global parameters inside {...}
            if stripped.startswith("{"):
                # Found the opening {, start accumulating global parameters
                inside_global_params = True
                # Remove the opening { and accumulate the remaining content
                global_params_content = stripped[stripped.index('{') + 1:].strip()

                # Check if the closing } is also on the same line
                if '}' in global_params_content:
                    global_params_content = global_params_content[:global_params_content.index('}')].strip()
                    inside_global_params = False  # We found the closing } on the same line
                    # Now parse the global parameters block
                    cls._parse_global_params(global_params_content.strip(), global_params)
                    global_params_content = ""  # Reset for the next block
                continue

            if inside_global_params:
                # Accumulate content until the closing } is found
                if stripped.endswith("}"):
                    # Found the closing }, accumulate and process the entire block
                    global_params_content += " " + stripped[:stripped.index('}')].strip()
                    inside_global_params = False  # Finished reading global parameters block

                    # Now parse the entire global parameters block
                    cls._parse_global_params(global_params_content.strip(), global_params)
                    global_params_content = ""  # Reset for the next block if necessary
                else:
                    # Continue accumulating if } is not found
                    global_params_content += " " + stripped
                continue

            # Step 4: Detect the start of a multi-line template block inside [...]
            if not inside_template_block:
                template_match = re.match(r'(\w+)\s*:\s*\[', stripped)
                if template_match:
                    current_template_key = template_match.group(1)  # Capture the key
                    inside_template_block = True
                    current_template_content = []  # Reset content list
                    continue

            # If inside a template block, accumulate lines until we find the closing ]
            if inside_template_block:
                if stripped == "]":
                    # End of the template block, join the content and store it
                    template[current_template_key] = ScriptTemplate(
                        current_template_content,
                        definitions=lambdaScriptdata(**definitions),  # Clone current global definitions
                        verbose=True,
                        userid=current_template_key
                        )
                    template[current_template_key].refreshvar()
                    inside_template_block = False
                    current_template_key = None
                    current_template_content = []
                else:
                    # Accumulate the current line (without surrounding spaces)
                    current_template_content.append(stripped)
                continue

            # Step 5: Handle attributes inside {...}
            if inside_attributes and stripped.endswith("}"):
                # Finish processing attributes for the current key
                cls._parse_attributes(attributes[current_attr_key], stripped[:-1])  # Remove trailing }
                inside_attributes = False
                current_attr_key = None
                continue

            if inside_attributes:
                # Continue accumulating attributes
                cls._parse_attributes(attributes[current_attr_key], stripped)
                continue

            # Step 6: Determine if the line is a definition, template, or attribute
            definition_match = re.match(r'(\w+)\s*=\s*(.+)', stripped)
            template_match = re.match(r'(\w+)\s*:\s*(?!\s*\{.*\}\s*$)(.+)', stripped) # template_match = re.match(r'(\w+)\s*:\s*(?!\{)(.+)', stripped)
            attribute_match = re.match(r'(\w+)\s*:\s*\{\s*(.+)\s*\}', stripped)       # attribute_match = re.match(r'(\w+)\s*:\s*\{(.+)\}', stripped)

            if definition_match:
                # Line is a definition (key=value)
                key, value = definition_match.groups()
                definitions.setattr(key,cls._convert_value(value))

            elif template_match and not inside_template_block:
                # Line is a template (key: content)
                key, content = template_match.groups()
                template[key] = ScriptTemplate(
                    content,
                    definitions=lambdaScriptdata(**definitions),  # Clone current definitions
                    verbose=True,
                    userid=current_template_key)
                template[key].refreshvar()

            elif attribute_match:
                # Line is an attribute (key:{attributes...})
                current_attr_key, attr_content = attribute_match.groups()
                attributes[current_attr_key] = {}
                cls._parse_attributes(attributes[current_attr_key], attr_content)
                inside_attributes = not stripped.endswith("}")

        # Step 7: Validation and Reconstruction
        # Make sure there are no attributes without a template entry
        for key in attributes:
            if key not in template:
                raise ValueError(f"Attributes found for undefined template key: {key}")
            # Apply attributes to the corresponding template object
            for attr_name, attr_value in attributes[key].items():
                setattr(template[key], attr_name, attr_value)

        # Step 7: Create and return a new dscript instance
        if name is None:
            name = autoname(8)
        instance = cls(
            name = name,
            SECTIONS=global_params.get('SECTIONS', ['DYNAMIC']),
            section=global_params.get('section', 0),
            position=global_params.get('position', 0),
            role=global_params.get('role', 'dscript instance'),
            description=global_params.get('description', 'dynamic script'),
            userid=global_params.get('userid', 'dscript'),
            version=global_params.get('version', 0.1),
            verbose=global_params.get('verbose', False)
        )


        # Convert numeric string keys to integers if numerickeys is True
        if numerickeys:
            numeric_template = {}
            for key, value in template.items():
                # Check if the key is a numeric string
                if key.isdigit():
                    numeric_template[int(key)] = value
                else:
                    numeric_template[key] = value
            template = numeric_template

        # Set definitions and template
        instance.DEFINITIONS = definitions
        instance.TEMPLATE = template

        # Refresh variables (ensure that variables are detected and added to definitions)
        instance.set_all_variables()

        # Check eval
        instance.check_all_variables(verbose=False)

        # return the new instance
        return instance


    @classmethod
    def _parse_global_params(cls, content, global_params):
        """
        Parses global parameters from the accumulated content enclosed in `{}`.

        ### Parameters:
            content (str): The content string containing global parameters.
            global_params (dict): A dictionary to populate with parsed parameters.

        ### Raises:
            ValueError: If invalid lines or key-value pairs are encountered.
        """
        # Remove braces from the content
        content = content.strip().strip("{}")

        # Split the content into lines by commas
        lines = re.split(r',(?![^(){}\[\]]*[\)\}\]])', content.strip())

        for line in lines:
            line = line.strip()
            # Match key-value pairs
            match = re.match(r'([\w_]+)\s*=\s*(.+)', line)
            if match:
                key, value = match.groups()
                key = key.strip()
                value = value.strip()
                # Convert the value to the appropriate Python type and store it
                global_params[key] = cls._convert_value(value)
            else:
                raise ValueError(f"Invalid parameter line: '{line}'")


    @classmethod
    def _parse_attributes(cls, attr_dict, content):
        """Parses attributes from the content inside {attribute=value,...}."""
        attr_pairs = re.findall(r'(\w+)\s*=\s*([^,]+)', content)
        for attr_name, attr_value in attr_pairs:
            attr_dict[attr_name] = cls._convert_value(attr_value)

    @classmethod
    def _convert_value(cls, value):
        """Converts a string representation of a value to the appropriate Python type."""
        value = value.strip()
        # Boolean and None conversion
        if value.lower() == 'true':
            return True
        elif value.lower() == 'false':
            return False
        elif value.lower() == 'none':
            return None
        # Handle quoted strings
        if (value.startswith('"') and value.endswith('"')) or (value.startswith("'") and value.endswith("'")):
            return value[1:-1]
        # Handle lists (Python syntax inside the file)
        if value.startswith('[') and value.endswith(']'):
            return eval(value)  # Using eval to parse lists safely in this controlled scenario
        # Handle numbers
        try:
            if '.' in value:
                return float(value)
            return int(value)
        except ValueError:
            # Return the value as-is if it doesn't match other types
            return value

    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, copy.deepcopy(v, memo))
        return copie

    def detect_all_variables(self):
        """
        Detects all variables across all templates in the dscript object.

        This method iterates through all ScriptTemplate objects in the dscript and
        collects variables from each template using the detect_variables method.

        Returns:
        --------
        list
            A sorted list of unique variables detected in all templates.
        """
        all_variables = set()  # Use a set to avoid duplicates
        # Iterate through all templates in the dscript object
        for template_key, template in self.TEMPLATE.items():
            # Ensure the template is a ScriptTemplate and has the detect_variables method
            if isinstance(template, ScriptTemplate):
                detected_vars = template.detect_variables()
                all_variables.update(detected_vars)  # Add the detected variables to the set
        return sorted(all_variables)  # Return a sorted list of unique variables

    def add_dynamic_script(self, key, content="", userid=None, definitions=None, verbose=None, **USER):
        """
        Add a dynamic script step to the dscript object.

        Parameters:
        -----------
        key : str
            The key for the dynamic script (usually an index or step identifier).
        content : str or list of str, optional
            The content (template) of the script step.
        definitions : lambdaScriptdata, optional
            The merged variable space (STATIC + GLOBAL + LOCAL).
        verbose : bool, optional
            If None, self.verbose will be used. Controls verbosity of the template.
        USER : dict
            Additional user variables that override the definitions for this step.
        """
        if definitions is None:
            definitions = lambdaScriptdata()
        if verbose is None:
            verbose = self.verbose
        # Create a new ScriptTemplate and add it to the TEMPLATE
        self.TEMPLATE[key] = ScriptTemplate(
            content=content,
            definitions=self.DEFINITIONS+definitions,
            verbose=verbose,
            userid = key if userid is None else userid,
            **USER
        )



    def check_all_variables(self, verbose=True, seteval=True, output=False):
        """
        Checks for undefined variables for each TEMPLATE key in the dscript object.

        Parameters:
        -----------
        verbose : bool, optional, default=True
            If True, prints information about variables for each TEMPLATE key.
            Shows [-] if the variable is set to its default value, [+] if it is defined, and [ ] if it is undefined.

        seteval : bool, optional, default=True
            If True, sets the `eval` attribute to True if at least one variable is defined or set to its default value.

        output : bool, optional, default=False
            If True, returns a dictionary with lists of default variables, set variables, and undefined variables.

        Returns:
        --------
        out : dict, optional
            If `output=True`, returns a dictionary with the following structure:
            - "defaultvalues": List of variables set to their default value (${varname}).
            - "setvalues": List of variables defined with values other than their default.
            - "undefined": List of variables that are undefined.
        """
        out = {"defaultvalues": [], "setvalues": [], "undefined": []}

        for key in self.TEMPLATE:
            template = self.TEMPLATE[key]
            # Call the check_variables method of ScriptTemplate for each TEMPLATE key
            result = template.check_variables(verbose=verbose, seteval=seteval)

            # Update the output dictionary if needed
            out["defaultvalues"].extend(result["defaultvalues"])
            out["setvalues"].extend(result["setvalues"])
            out["undefined"].extend(result["undefined"])

        if output:
            return out


    def set_all_variables(self):
        """
        Ensures that all variables in the templates are added to the global definitions
        with default values if they are not already defined.
        """
        for key, script_template in self.TEMPLATE.items():
            # Check and update the global definitions with template-specific variables
            for var in script_template.detect_variables():
                if var not in self.DEFINITIONS:
                    # Add undefined variables with their default value
                    self.DEFINITIONS.setattr(var, f"${{{var}}}")  # Set default as ${varname}


# %% debug section - generic code to test methods (press F5)
# ===================================================
# main()
# ===================================================
# for debugging purposes (code called as a script)
# the code is called from here
# ===================================================
if __name__ == '__main__':

    # Usage example
    # -------------
    # Initialize a dscript object
    S = dscript()

    # Add script lines/items with placeholders for variables
    S[3] = "instruction .... with substitution rules ${v1}+${var2}"
    S['alpha'] = "another script template ${v3}"

    # Set a custom attribute for a specific line
    S[3].attribute1 = True

    # Reorder script lines/items
    T = S[[1,0]]

    # Define global variables in DEFINITIONS
    S.DEFINITIONS.a = 1
    S.DEFINITIONS.b = 2

    # Update a script line and enable evaluation of its content
    S[0] = "$a+$b"
    S[0].eval = True

    # Set a line as mandatory (not facultative)
    S[3].facultative = False

    # Access and print the content of specific script lines/items
    print(S[3])       # Outputs: instruction .... with substitution rules ${v1}+${var2}
    print(S['alpha']) # Outputs: another script template ${v3}

    # Access and print custom attributes
    print(S[3].attribute1)  # Outputs: True

    # Iterate through all script lines, printing their keys and content
    for key, content in S.items():
        print(f"Key: {key}, Content: {content}")

    # Retrieve and evaluate the content of a script line by its index
    S.get_content_by_index(0, False)  # Retrieve without evaluation
    S.get_content_by_index(0)         # Retrieve with evaluation

    # Access attributes of a specific script line by index
    S.get_attributes_by_index(0)

    # Apply conditions to the execution of a script line
    S[0].condition = "$a>1"
    S[0].condeval = True
    S.get_content_by_index(2)  # Condition not met, may result in an empty string

    # Update the condition and evaluate again
    S[0].condition = "$a>0"
    S[0].do()  # Executes and evaluates the line content

    # Create multiple variables in DEFINITIONS if they don't already exist
    S.createEmptyVariables(["a", "b", "c", "d", "e"])

    # Access and print all current definitions
    S.DEFINITIONS


    # =====================================================
    # Production Example: LAMMPS Header Initialization
    #   closely related to pizza.region.LammpsHeaderInit
    # =====================================================
    # Initialize a dscript object with a custom name
    R = dscript(name="ProductionExample")

    # Define global variables (DEFINITIONS) for the script
    R.DEFINITIONS.dimension = 3
    R.DEFINITIONS.units = "$si"
    R.DEFINITIONS.boundary = ["sm", "sm", "sm"]
    R.DEFINITIONS.atom_style = "$smd"
    R.DEFINITIONS.atom_modify = ["map", "array"]
    R.DEFINITIONS.comm_modify = ["vel", "yes"]
    R.DEFINITIONS.neigh_modify = ["every", 10, "delay", 0, "check", "yes"]
    R.DEFINITIONS.newton = "$off"

    # Define the script template, associating each line with a key
    R[0]        = "% ${comment}"               # line/item can be identied by numbers/names
    R["dim"]    = "dimension    ${dimension}"  # line/item identified as 'dim'
    R["unit"]   = "units        ${units}"      # line/item identified as 'unit'
    R["bound"]  = "boundary     ${boundary}"
    R["astyle"] = "atom_style   ${atom_style}"
    R["amod"]   = "atom_modify  ${atom_modify}"
    R["cmod"]   = "comm_modify  ${comm_modify}"
    R["nmod"]   = "neigh_modify ${neigh_modify}"
    R["newton"] = "newton       ${newton}"

    # Apply a condition to the 'astyle' line
    # it will only be included if ${atom_style} is defined
    R["astyle"].condition = "${atom_style}"

    # Update DEFINITIONS to unset the atom_style variable
    R.DEFINITIONS.atom_style = ""

    # Generate a script instance, overwriting the 'units' variable and adding a comment
    sR = R.script(units="$lj",  # Use "$" to prevent immediate evaluation
                  comment="$my first dynamic script")

    # Execute the script to generate the final content
    ssR = sR.do()

    # Print the generated script
    print(ssR)

    # Save the current script
    R.save(overwrite=True)

    # Load again the same script and show the script
    T = dscript.load(R.name)
    print(repr(T))
    ssT = R.script(units="$lj",  # Use "$" to prevent immediate evaluation
                  comment="$my second dynamic script").do()
    print(ssT)

    # ========================================================
    # DSCRIPT SAVE FILE: Example: LAMMPS generic code
    #   This example is intended to illustrate the syntax
    #   Note that the script below does not include the header.
    #   It will be added with the write method
    # ========================================================

    # The script is defined here within a string
    # note that the first line should be: # DSCRIPT SAVE FILE
    myscript = """# DSCRIPT SAVE FILE
# Global Parameters:
# ------------------
# Define general settings for the script class.
# This section is not mandatory.
# Properties are defined within { }
# They include
#       SECTIONS = ["DYNAMIC"] # the considered section names
#       section = 0            # the current section index,
#       position = 0           # the script  position order,
#       role = "dscript instance",
#       description = "dynamic script",
#       userid = "dscript",
#       version = 0.1,
#       verbose = False
{ # a line starting with { indicates the begining of the section
    SECTIONS = ['INITIALIZATION', 'SIMULATION'], # this is a comment
    section=0, position=0 } # note the closing } is mandatory

# DEFINITIONS (Define general settings for the script.)
# -----------
# All variables are defined in a Python way

d = 3                        # d is a number and equals 3
units = "$lj"                # $ is used to block immediate execution in a string
periodic = "$p"              # $ is used to block immediate execution in a string
dimension = "${d}"           # d is a variable
boundary = ["p", "p", "p"]   # this a list (Python syntax)
atom_style = "$atomic"
lattice = ["fcc", 3.52]       # this a list (Python syntax)
region = ["box", "block", 0, 10, 0, 10, 0, 10] # this a list (Python syntax)
create_box = [1, "box"]
create_atoms = [1, "box"]
mass = 1.0
pair_style = ["lj/cut", 2.5]
pair_coeff = [1, 1, 1.0, 1.0, 2.5]
velocity = ["all", "create", 300.0, 12345]
fix = [1, "all", "nve"]
run = 1000
timestep = 0.001
thermo = 100

# TEMPLATE:
# ---------
# Provide a template for how these parameters should be formatted or used in the script.
#  The general syntax is:
#      KEY: INSTRUCTION
#      KEY can be numeric, alphanumeric
#      INSTRUCTION can be any LAMMPS command involving variables defined in the DEFINITION section or elsewhere
units: units ${units}     # key = units, template = units ${units}
dim: dimension ${dimension}
bound: boundary ${boundary}
astyle: atom_style ${atom_style}
lattice: lattice ${lattice}
region: region ${region}
create_box: create_box ${create_box}
create_atoms: create_atoms ${create_atoms}
mass: mass ${mass}
pair_style: pair_style ${pair_style}
pair_coeff: pair_coeff ${pair_coeff}
velocity: velocity ${velocity}
fix: fix ${fix}
run: run ${run}
timestep: timestep ${timestep}
thermo: thermo ${thermo}

# Attributes:
# -----------
# Each template line can have attributes (here default attributes, but the user can add more)
# Default value between ()
#   facultative = True or (False) (remove the template line if True)
#   eval = True or (False) (evaluate the template line with eval() if True)
#   readonly True or (False) (prevent any subsequent modification() if True)
#   condition = any expression with variables
#   condeval = True or (False) (evaluate the condition with eval() if True)
#   detectvar = (True) or False (create variables in DEFINITIONS if True)
units: {facultative=False, eval=False, readonly=False, condition="${units}", condeval=False, detectvar=True}
dim: {facultative=False, eval=False, readonly=False, condition=None, condeval=False, detectvar=True}
    """

    # write myscript to disk
    myscriptfile = dscript.write(myscript)
    print(f"DSCRIPT SAVE FILE: {myscriptfile}")

    # load the script as a dscript object
    myS = dscript.load(myscriptfile)

    # generate the corresponding script
    myS.units.do()
    smyS = myS.script()

    # Ececute the script and print it
    ssmyS = smyS.do()
    print(ssmyS)

    # The conversion of a string into a script
    # can be mediated via dscript.parsesyntax()
    # without using a temporary file
    mytemplate = dscript.parsesyntax(myscript).script()
    mytemplatetxt = mytemplate.do()
    print(mytemplatetxt)


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#       Beyond this line, the previous examples are tested with the new compact syntax
#       enabling to define a template-block with a single key/tag.
#       The intent is to accelerate scripting and readability.
#
#       IMPORTANT
#           In DSCRIPT SAVE FILE, a block uses a new syntax between square brackets "[]"
#               # TEMPLATE (number of items=1)
#               code: [
#               % ${comment}
#               dimension    ${dimension}
#               units        ${units}
#               boundary     ${boundary}
#               atom_style   ${atom_style}
#               atom_modify  ${atom_modify}
#               comm_modify  ${comm_modify}
#               neigh_modify ${neigh_modify}
#               newton       ${newton}
#     ]
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
#
    # =====================================================
    # Production Example version 2: compact version
    #   using multiple lines/items template
    # =====================================================

    R2 = dscript(name="ProductionExample2")

    # Define global variables (DEFINITIONS) for the script
    R2.DEFINITIONS.dimension = 3
    R2.DEFINITIONS.units = "$si"
    R2.DEFINITIONS.boundary = ["sm", "sm", "sm"]
    R2.DEFINITIONS.atom_modify = ["map", "array"]
    R2.DEFINITIONS.comm_modify = ["vel", "yes"]
    R2.DEFINITIONS.neigh_modify = ["every", 10, "delay", 0, "check", "yes"]
    R2.DEFINITIONS.newton = "$off"

    # Define the script template, associating each line with a key
    R2["code"] = """
    % ${comment}               # this comment will be preserved as it starts with %
    dimension    ${dimension}  # this comment will be deleted
    units        ${units}
    boundary     ${boundary}
    atom_style   ${atom_style}
    atom_modify  ${atom_modify}
    comm_modify  ${comm_modify}
    neigh_modify ${neigh_modify}
    newton       ${newton}
    """

    # Add missing defintion
    R2.DEFINITIONS.atom_style = "$smd"

    # Generate a script instance, overwriting the 'units' variable and adding a comment
    sR2 = R2.script(units="$lj",  # Use "$" to prevent immediate evaluation
                   comment="$my first dynamic script")
    ssR2 = sR2.do()

    # Print the generated script
    print(ssR2)

    # Save the current script
    R2.save(overwrite=True)

    # Load again the same script and show the script
    T2 = dscript.load(R2.name)
    print(repr(T2))
    ssT2 = R2.script(units="$lj",  # Use "$" to prevent immediate evaluation
                  comment="$my second dynamic script").do()
    print(ssT2)

    # ========================================================
    # DSCRIPT SAVE FILE: compact version
    # ========================================================

    # The script is defined here within a string
    # note that the first line should be: # DSCRIPT SAVE FILE

    myscript2 = """# DSCRIPT SAVE FILE

# Global Parameters:
# ------------------
{ # a line starting with { indicates the begining of the section
    SECTIONS = ['INITIALIZATION', 'SIMULATION'], # this is a comment
    section=0, position=0 } # note the closing } is mandatory

# DEFINITIONS (Define general settings for the script.)
# -----------
d = 3                        # d is a number and equals 3
units = "$lj"                # $ is used to block immediate execution in a string
periodic = "$p"              # $ is used to block immediate execution in a string
dimension = "${d}"           # d is a variable
boundary = ["p", "p", "p"]   # this a list (Python syntax)
atom_style = "$atomic"
lattice = ["fcc", 3.52]       # this a list (Python syntax)
region = ["box", "block", 0, 10, 0, 10, 0, 10] # this a list (Python syntax)
create_box = [1, "box"]
create_atoms = [1, "box"]
mass = 1.0
pair_style = ["lj/cut", 2.5]
pair_coeff = [1, 1, 1.0, 1.0, 2.5]
velocity = ["all", "create", 300.0, 12345]
fix = [1, "all", "nve"]
run = 1000
timestep = 0.001
thermo = 100

# TEMPLATE:
# ---------
mytemplate1: [
    # you can add comments inside templates
    units ${units}     # whereever you need
    dimension ${dimension}
    boundary ${boundary}
    atom_style ${atom_style}
    lattice ${lattice}
    region ${region}
    create_box ${create_box}
    create_atoms ${create_atoms}
    mass ${mass}
    pair_style ${pair_style}
    pair_coeff ${pair_coeff}
    ]

mytemplate2: [
    velocity ${velocity}
    fix ${fix}
    run ${run}
    timestep ${timestep}
    thermo ${thermo}
    ]

# Attributes:
# -----------
mytemplate1: {facultative=False, eval=False, readonly=False, condition="${units}", condeval=False, detectvar=True}
mytemplate2: {facultative=False, eval=False, readonly=False, condition="", condeval=False, detectvar=True}
    """

    # write myscript to disk
    myscriptfile2 = dscript.write(myscript2)
    print(f"DSCRIPT SAVE FILE: {myscriptfile2}")

    # load the script as a dscript object
    myS2 = dscript.load(myscriptfile2)

    # generate the corresponding script
    smyS2 = myS2.script()

    # Ececute the script and print it
    ssmyS2 = smyS2.do()
    print(ssmyS2)

    # The conversion of a string into a script
    # can be mediated via dscript.parsesyntax()
    # without using a temporary file
    mytemplate2 = dscript.parsesyntax(myscript2).script()
    mytemplatetxt2 = mytemplate2.do()
    print(mytemplatetxt2)


    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    #                                        ADVANCED EXAMPLE
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        ################################################################################
        # This Python script demonstrates the conversion of a LAMMPS input script
        # for a TLSPH (Total Lagrangian Smoothed Particle Hydrodynamics) simulation
        # into a `dscript` object. The `dscript` format allows for flexible manipulation
        # of variables, templates, and simulation parameters using Python.
        #
        # Example use case:
        # - The script simulates elongation of a 2D strip of a linear elastic material
        #   by pulling its ends apart, using LAMMPS USER.SMD package.
        # - The units are set to GPa / mm / ms, and material properties such as
        #   Young’s modulus, Poisson’s ratio, and density are defined.
        # - The geometry, boundary conditions, material model, and output settings
        #   are set up dynamically.
        #
        # Sections in the script:
        # 1. **INITIALIZE**: Initializes the LAMMPS environment and sets simulation settings.
        # 2. **CREATE_GEOMETRY**: Defines the initial particle geometry and region.
        # 3. **DISCRETIZATION**: Defines parameters for discretization and particle properties.
        # 4. **BOUNDARY_CONDITIONS**: Sets velocity conditions to pull the strip's top and bottom edges.
        # 5. **PHYSICS**: Specifies the interaction physics and material model using the USER.SMD package.
        # 6. **OUTPUT**: Configures stress, strain, and neighbor computations for output.
        # 7. **STATUS_OUTPUT**: Defines how stress and strain are calculated and output.
        # 8. **RUN**: Executes the simulation for a specified number of steps.
        #
        # The main variables such as Young’s modulus (E), Poisson’s ratio (nu), and density (rho)
        # are added to the `DEFINITIONS` section for dynamic use in the script.
        #
        # The `TEMPLATE` section organizes each block of the LAMMPS script under different keys
        # (e.g., "initialize", "create", "discretization"), allowing easy manipulation or modification
        # of individual parts of the script through Python code.
        #
        # This approach facilitates parameter sweeps, automatic adjustments of simulation inputs,
        # and easy reconfiguration of simulation settings, making it suitable for high-throughput
        # or iterative simulations in LAMMPS.
        #
        # The script can be saved, loaded, or executed in Python as a `dscript` object, providing
        # a robust tool for dynamic LAMMPS input file generation and manipulation.
        ################################################################################
    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


    # This flexible approach enables dynamic manipulation of simulation parameters.
    # The full TLSPH template is defined as a multi-line script in DSCRIPT format.
    # The following template was automatically generated by ChatGPT based on the
    # original LAMMPS TLSPH simulation script for elongating a 2D strip of linear
    # elastic material by pulling its ends apart.
    #
    # Key variables (such as Young's modulus, Poisson's ratio, and mass density)
    # have been added to the DEFINITIONS section for dynamic substitution in the
    # template.
    #
    # Read the synopsis of this module to learn how to instruct ChatGPT to generate
    # such templates.

    TLSPH_template = """# DSCRIPT SAVE FILE
# TENSILE SUMULATION
####################################################################################################
#
# TLSPH example: elongate a 2d strip of a linear elastic material py pulling its ends apart
#
# unit sytem: GPa / mm / ms
#
####################################################################################################
# Source:
# GLOBAL PARAMETERS
{
    SECTIONS = ['INITIALIZE', 'CREATE_GEOMETRY', 'DISCRETIZATION', 'BOUNDARY_CONDITIONS', 'PHYSICS', 'OUTPUT', 'RUN'],
    section = 0,
    position = 0,
    role = "dscript instance",
    description = "Advanced example based on ChatGPT translation",
    userid = "ChatGPT",
    version = 1.0,
    verbose = False
}

# DEFINITIONS (number of definitions=12)
E=1.0              # Young's modulus
nu=0.3             # Poisson ratio
rho=1.0            # Initial mass density
q1=0.06            # Artificial viscosity linear coefficient
q2=0.0             # Artificial viscosity quadratic coefficient
hg=10.0            # Hourglass control coefficient
cp=1.0             # Heat capacity
l0=1.0             # Lattice spacing
h=2.01 * ${l0}     # SPH smoothing kernel radius
vol_one=${l0}**2   # Volume of one particle (unit thickness)
vel0=0.005         # Pull velocity
skin=${h}          # Verlet list range

# TEMPLATE (number of lines=8)
initialize: [
    dimension 2
    units si
    boundary sm sm p
    atom_style smd
    atom_modify map array
    comm_modify vel yes
    neigh_modify every 10 delay 0 check yes
    newton off ]

# set region dimensions
boxlength = 10  # variables can be defined and changed any time (only the last definition is retained)
boxdepth =  0.1 # variables can be defined and changed any time (only the last definition is retained)

create: [
    lattice sq ${l0}
    region box block ${boxlength} ${boxlength} ${boxlength} ${boxlength} ${boxdepth} ${boxdepth} units box
    create_box 1 box
    create_atoms 1 box
    group tlsph type 1 ]

discretization: [
    neighbor ${skin} bin
    set group all volume ${vol_one}
    set group all smd_mass_density ${rho}
    set group all diameter ${h} ]

boundary_conditions: [
    region top block EDGE EDGE 9.0 EDGE EDGE EDGE units box
    region bot block EDGE EDGE EDGE 9.1 EDGE EDGE units box
    group top region top
    group bot region bot
    variable vel_up equal ${vel0} * (1.0 exp(0.01 * time))
    variable vel_down equal v_vel_up
    fix veltop_fix top smd/setvelocity 0 v_vel_up 0
    fix velbot_fix bot smd/setvelocity 0 v_vel_down 0 ]

physics: [
    pair_style smd/tlsph
    pair_coeff 1 1 *COMMON ${rho} ${E} ${nu} ${q1} ${q2} ${hg} ${cp} &
    *STRENGTH_LINEAR &
    *EOS_LINEAR &
    *END ]

output: [
    compute S all smd/tlsph_stress
    compute E all smd/tlsph_strain
    compute nn all smd/tlsph_num_neighs
    dump dump_id all custom 10 dump.LAMMPS id type x y z vx vy vz &
    c_S[1] c_S[2] c_S[4] c_nn &
    c_E[1] c_E[2] c_E[4] &
    vx vy vz
    dump_modify dump_id first yes ]

# add filename
outputfilename = "$stress_strain.dat" # variables can be defined and changed any time

status_output: [
    variable stress equal 0.5 * (f_velbot_fix[2] - f_veltop_fix[2]) / 20
    variable length equal xcm(top,y) - xcm(bot,y)
    variable strain equal (v_length - ${length}) / ${length}
    fix stress_curve all print 10 "${strain} ${stress}" file ${outputfilename} screen no
    thermo 100
    thermo_style custom step dt f_dtfix v_strain ]

# add runtime
runtime = 2000  # variables can be defined and changed any time

# single liner template
run_simulation: run ${runtime}

# change runtime
runtime = 2500  # variables can be defined and changed any time (only the last definition is retained)


# ATTRIBUTES (number of lines with explicit attributes=0)

    """

    TLSPH = dscript.parsesyntax(TLSPH_template)  # this is a dscript instance
    TLSPH_script = TLSPH.script()                # this is a script instance
    TLSPH.code = TLSPH_script.do()               # this is the corresponding LAMMPS code
    print(TLSPH.code)
    # Note that some definitions are missing since they are calculated by LAMMPS during the simulation
    # It includes: stress, strain, length
    repr(TLSPH.DEFINITIONS)

Functions

def autoname(numChars=8)

generate automatically names

Expand source code
def autoname(numChars=8):
    """ generate automatically names """
    return ''.join(random.choices(string.ascii_letters, k=numChars))  # Generates a random name of numChars letters
def 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 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 ScriptTemplate (content='', definitions=Lambda Script Parameters (LSD object) with 0 parameter definitions, autorefresh=True, userid=None, verbose=False, comment_chars='#%', **kwargs)

The ScriptTemplate class provides a mechanism to store, process, and dynamically substitute variables within script content. This class supports handling flags and conditions, allowing for flexible and conditional execution of parts of the script.

Attributes:


default_attributes : dict (class-level) A dictionary containing the default flags for each ScriptTemplate object. These attributes are applied when initializing the object or when no flags are specified in the content. Flags include: - facultative (bool): If True, the script line is optional and may be discarded if certain conditions are not met. (default: False) - eval (bool): If True, the content is evaluated using formateval, which allows for variable substitution during execution. (default: False) - readonly (bool): If True, the content cannot be modified after initialization. (default: False) - condition (str or None): A string that specifies a condition for executing the content. If the condition is not met, the script line is skipped. (default: None) - condeval (bool): If True, the condition attribute is evaluated dynamically using variables. (default: False) - detectvar (bool): If True, any variables in the content (e.g., ${varname}) are automatically detected and registered in the definitions. (default: True)

Methods:


init(self, content="", definitions=lambdaScriptdata(), userid=None, verbose=False, **kwargs): Initializes the ScriptTemplate object with script content, optional variable definitions, and configurable attributes. Flags like facultative, eval, readonly, condition, and others are set via content prefixes or passed directly as keyword arguments.

parse_content(cls, content, verbose=False): A class method that parses content to detect special flags and condition tags, returning the cleaned content and a dictionary of attributes (flags).

 The method processes the following flags and tags:
 - `!` : Sets `eval=True`, which forces evaluation of the content.
 - `?` : Sets `facultative=True`, marking the content as optional.
 - `^` : Sets `readonly=True`, preventing modifications to the content.
 - `~` : Sets `detectvar=False`, disabling automatic variable detection.
 - `[if:condition]` : Defines a condition for executing the script. If the condition is met,
   the script will be executed. Conditions can include `;eval` to trigger evaluation.

 Parameters:
 -----------
 content : str
     The script content to be parsed. It may contain lines starting with special characters
     that set specific attributes for the script template.

 verbose : bool (default: False)
     If True, preserves comments and does not remove comment lines during parsing.

 Returns:
 --------
 cleaned_content : str
     The cleaned script content, with all flags and conditions removed.

 attributes : dict
     A dictionary of parsed attributes (flags) including:
     - facultative (bool): Indicates if the content is optional.
     - eval (bool): Specifies if the content should be evaluated for variable substitution.
     - readonly (bool): Prevents modifications to the content.
     - condition (str or None): Defines the condition for executing the script.
     - condeval (bool): If True, dynamically evaluates the condition.
     - detectvar (bool): If True, detects and registers variables in the content.

detect_variables(self): Detects variables in the content using the pattern ${varname}. It returns a list of variable names found in the script.

refreshvar(self): Ensures that any detected variables are added to the definitions if they are missing.

setattr(self, name, value): Sets the value of an attribute. The method performs validation on specific attributes (e.g., eval, readonly, facultative, etc.) to ensure the correct data types are assigned.

getattr(self, name): Retrieves the value of an attribute. If the attribute is not set, it returns the default value from default_attributes.

Example 1:

Create a ScriptTemplate object with content and optional definitions

line = ScriptTemplate("dimension ${dimension}")

You can also pass in global definitions if needed

global_definitions = lambdaScriptdata(dimension=3) line_with_defs = ScriptTemplate("dimension ${dimension}", definitions=global_definitions)

After initialization, you can modify the line's attributes or use it as part of a larger script managed by a dscript object.

Example 2:


# Example of using flags and conditions in content

line = ScriptTemplate("!velocity all set 0.0 0.0 0.0 units box") # This will automatically set eval=True due to the '!' flag.

line_with_condition = ScriptTemplate("[if: ${condition};eval] fix upper move wiggle 0.0 0.0 1.0 1.0") # This content will only execute if ${condition} is True, and eval will be applied to substitute variables.

Initializes a new ScriptTemplate object.

The constructor sets up a new script template with content, optional variable definitions, and a set of configurable attributes. This template is capable of dynamically substituting variables using a provided lambdaScriptdata object or internal definitions. You can also manage attributes like evaluation, facultative execution, and variable detection.

Parameters:

content : str or list of str The content of the script template, which can be a single string or a list of strings. If a single string is provided, it will automatically be converted to a list of lines. This ensures consistent handling of multi-line content.

definitions : lambdaScriptdata, optional A reference to a lambdaScriptdata object that contains global variable definitions. These definitions will be used to substitute variables within the content. If definitions is not provided, variable substitution will rely on local or inline definitions.

verbose : flag (default value=True) If True the comments are preserved (applied when str is a string).

autorefresh : flag (default=False) If True, new variables are automatically detected when the content is changed

**kwargs : Additional keyword arguments to set specific attributes for the script line. These attributes control the behavior of the script template during evaluation and execution. Any keyword argument passed here will update the default attribute values.

    Default attributes (with default values):
- facultative (False):
    If True, the script line is optional and may be discarded if certain conditions are not met.
- eval (False):
    If True, the content will be evaluated using <code>formateval</code>, allowing variable
    substitution during the execution.
- readonly (False):
    If True, the content of the script cannot be modified after initialization.
- condition (None):
    An optional condition that controls whether the content will be executed.
    If the condition is not met, the script line will not be executed.
- condeval (False):
    If True, the <code>condition</code> attribute will be evaluated, allowing conditional
    logic based on variable values.
- detectvar (True):
    If True, the content will automatically detect and register any variables
    (such as `${varname}`) within the <code>definitions</code> for substitution.
- 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.
Expand source code
class ScriptTemplate:
    """
     The `ScriptTemplate` class provides a mechanism to store, process, and dynamically substitute
     variables within script content. This class supports handling flags and conditions, allowing
     for flexible and conditional execution of parts of the script.

     Attributes:
     -----------
     default_attributes : dict (class-level)
         A dictionary containing the default flags for each `ScriptTemplate` object. These attributes
         are applied when initializing the object or when no flags are specified in the content.
         Flags include:
         - facultative (bool): If True, the script line is optional and may be discarded if certain
             conditions are not met. (default: False)
         - eval (bool): If True, the content is evaluated using `formateval`, which allows for variable
             substitution during execution. (default: False)
         - readonly (bool): If True, the content cannot be modified after initialization. (default: False)
         - condition (str or None): A string that specifies a condition for executing the content.
             If the condition is not met, the script line is skipped. (default: None)
         - condeval (bool): If True, the `condition` attribute is evaluated dynamically using variables.
             (default: False)
         - detectvar (bool): If True, any variables in the content (e.g., `${varname}`) are automatically
             detected and registered in the definitions. (default: True)

     Methods:
     --------
     __init__(self, content="", definitions=lambdaScriptdata(), userid=None, verbose=False, **kwargs):
         Initializes the `ScriptTemplate` object with script content, optional variable definitions,
         and configurable attributes. Flags like `facultative`, `eval`, `readonly`, `condition`,
         and others are set via content prefixes or passed directly as keyword arguments.

     parse_content(cls, content, verbose=False):
         A class method that parses content to detect special flags and condition tags, returning the
         cleaned content and a dictionary of attributes (flags).

         The method processes the following flags and tags:
         - `!` : Sets `eval=True`, which forces evaluation of the content.
         - `?` : Sets `facultative=True`, marking the content as optional.
         - `^` : Sets `readonly=True`, preventing modifications to the content.
         - `~` : Sets `detectvar=False`, disabling automatic variable detection.
         - `[if:condition]` : Defines a condition for executing the script. If the condition is met,
           the script will be executed. Conditions can include `;eval` to trigger evaluation.

         Parameters:
         -----------
         content : str
             The script content to be parsed. It may contain lines starting with special characters
             that set specific attributes for the script template.

         verbose : bool (default: False)
             If True, preserves comments and does not remove comment lines during parsing.

         Returns:
         --------
         cleaned_content : str
             The cleaned script content, with all flags and conditions removed.

         attributes : dict
             A dictionary of parsed attributes (flags) including:
             - facultative (bool): Indicates if the content is optional.
             - eval (bool): Specifies if the content should be evaluated for variable substitution.
             - readonly (bool): Prevents modifications to the content.
             - condition (str or None): Defines the condition for executing the script.
             - condeval (bool): If True, dynamically evaluates the condition.
             - detectvar (bool): If True, detects and registers variables in the content.

     detect_variables(self):
         Detects variables in the content using the pattern `${varname}`. It returns a list of
         variable names found in the script.

     refreshvar(self):
         Ensures that any detected variables are added to the `definitions` if they are missing.

     __setattr__(self, name, value):
         Sets the value of an attribute. The method performs validation on specific attributes
         (e.g., `eval`, `readonly`, `facultative`, etc.) to ensure the correct data types are assigned.

     __getattr__(self, name):
         Retrieves the value of an attribute. If the attribute is not set, it returns the default
         value from `default_attributes`.


    Example 1:
    ----------
    # Create a ScriptTemplate object with content and optional definitions
    line = ScriptTemplate("dimension    ${dimension}")

    # You can also pass in global definitions if needed
    global_definitions = lambdaScriptdata(dimension=3)
    line_with_defs = ScriptTemplate("dimension    ${dimension}",
                                    definitions=global_definitions)

    After initialization, you can modify the line's attributes or use it as
    part of a larger script managed by a `dscript` object.


     Example 2:
     ----------
     # Example of using flags and conditions in content

     line = ScriptTemplate("!velocity all set 0.0 0.0 0.0 units box")
     # This will automatically set `eval=True` due to the '!' flag.

     line_with_condition = ScriptTemplate("[if: ${condition};eval] fix upper move wiggle 0.0 0.0 1.0 1.0")
     # This content will only execute if `${condition}` is True, and `eval` will be applied to substitute variables.

    """

    # Class-level attribute for default flags
    default_attributes = {
        'facultative': False,
        'eval': False,
        'readonly': False,
        'condition': None,
        'condeval': False,
        'detectvar': True
    }

    def __init__(self, content="", definitions=lambdaScriptdata(), autorefresh=True, userid=None, verbose=False, comment_chars="#%", **kwargs):
        """
        Initializes a new `ScriptTemplate` object.

        The constructor sets up a new script template with content, optional variable
        definitions, and a set of configurable attributes. This template is capable
        of dynamically substituting variables using a provided `lambdaScriptdata`
        object or internal definitions. You can also manage attributes like evaluation,
        facultative execution, and variable detection.

        Parameters:
        -----------
        content : str or list of str
            The content of the script template, which can be a single string or a list of strings.
            If a single string is provided, it will automatically be converted to a list of lines.
            This ensures consistent handling of multi-line content.

        definitions : lambdaScriptdata, optional
            A reference to a `lambdaScriptdata` object that contains global variable
            definitions. These definitions will be used to substitute variables within the content.
            If `definitions` is not provided, variable substitution will rely on local
            or inline definitions.

        verbose : flag (default value=True)
            If True the comments are preserved (applied when str is a string).

        autorefresh : flag (default=False)
            If True, new variables are automatically detected when the content is changed

        **kwargs :
            Additional keyword arguments to set specific attributes for the script line.
            These attributes control the behavior of the script template during evaluation
            and execution. Any keyword argument passed here will update the default
            attribute values.

                Default attributes (with default values):
            - facultative (False):
                If True, the script line is optional and may be discarded if certain conditions are not met.
            - eval (False):
                If True, the content will be evaluated using `formateval`, allowing variable
                substitution during the execution.
            - readonly (False):
                If True, the content of the script cannot be modified after initialization.
            - condition (None):
                An optional condition that controls whether the content will be executed.
                If the condition is not met, the script line will not be executed.
            - condeval (False):
                If True, the `condition` attribute will be evaluated, allowing conditional
                logic based on variable values.
            - detectvar (True):
                If True, the content will automatically detect and register any variables
                (such as `${varname}`) within the `definitions` for substitution.
            - 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.
        """


        # Initialize `_CACHE` with separate entries for each method
        self._CACHE = {
            'variables': {'result': None},
            'check_variables': {'result': None}
        }
        # Constructor
        self._autorefresh = autorefresh
        self._content = content # # Store initial content
        self.definitions = lambdaScriptdata(**definitions)  # Reference to the DEFINITIONS object
        # Initialize attributes with default values
        self.attributes = self.default_attributes.copy()
        # Convert single string content to a list for consistent processing
        if content == "" or content is None:
            content = ""  # Set to empty string if None
        elif isinstance(content, str):
            # Interpret the content and extract any attribute flags (e.g. !, ?, etc.)
            content, self.attributes = self.parse_content(content, verbose=verbose)
            # Convert content to a list of strings
            if verbose:
                content = content.split('\n')  # All comments are preserved during construction
            else:
                content = remove_comments(content, split_lines=True,comment_chars=comment_chars)  # Split string by newlines into list of strings
        elif not isinstance(content, list) or not all(isinstance(item, str) for item in content):
            raise TypeError("The 'content' attribute must be a string or a list of strings.")
        # Assign content to the object
        self.content = content
        # Update attributes with any additional keyword arguments passed in
        self.attributes.update(kwargs)
        # Detect variables in the content
        detected_variables = self.detect_variables()
        # Automatically set `eval=True` if any detected variables are defined in `definitions`
        if detected_variables:
            for var in detected_variables:
                if var in self.definitions:
                    self.attributes['eval'] = True
                    break  # No need to continue checking once we know eval should be set
        # Assign userid (optional)
        self.userid = userid if userid is not None else autoname(3)


    def _calculate_content_hash(self, content):
        """Generate hash for content."""
        return hashlib.md5("\n".join(content).encode()).hexdigest() if isinstance(content, list) else hashlib.md5(content.encode()).hexdigest()

    @property
    def content(self):
        return self._content

    @content.setter
    def content(self, new_content):
        """Set content and reinitialize cache if the content has changed."""
        new_content_hash = self._calculate_content_hash(new_content)
        if new_content_hash != self._content_hash:
            self._content = new_content
            self._content_hash = new_content_hash
            self._invalidate_cache()  # Reset cache only if content changes


    def _update_content(self, value):
        """Helper to set _content and _content_hash, refreshing cache as necessary."""
        # Check if content modification is allowed based on readonly attribute
        if getattr(self, 'attributes', {}).get('readonly', False):
            raise AttributeError("Cannot modify content. It is read-only.")
        # Validate and process content as either a string or list of strings
        if isinstance(value, str):
            value = remove_comments(value, split_lines=True)
        elif not isinstance(value, list) or not all(isinstance(item, str) for item in value):
            raise TypeError("The 'content' attribute must be a string or a list of strings.")
        # Set _content and calculate hash
        super().__setattr__('_content', value)
        new_hash = hash(tuple(value))
        # If hash changes, reset cache
        if new_hash != getattr(self, '_content_hash', None):
            super().__setattr__('_content_hash', new_hash)
            self._invalidate_cache()  # Invalidate cache due to content change
            if self._autorefresh:
                self.refreshvar()  # Refresh variables based on new content

    def _invalidate_cache(self):
        """Reset all cache entries."""
        # Check if _CACHE is initialized
        if not hasattr(self, '_CACHE'):
            self._CACHE = {
                'variables': {'result': None},
                'check_variables': {'result': None}
            }
        for entry in self._CACHE.values():
            entry['result'] = None


    @classmethod
    def parse_content(cls, content, verbose=False):
        """
        Parse the content string and return:
        1. Cleaned content (without flags or condition tags).
        2. A dictionary of attributes (flags) initialized with default values.

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

        Returns:
        --------
        cleaned_content : str
            The cleaned content with all flag prefixes and condition tags removed.
        attributes : dict
            A dictionary of attributes (flags) set based on the content prefixes.
        """
        attributes = cls.default_attributes.copy()

        # Handle empty content
        if not content.strip():  # Empty string or whitespace-only
            return "", attributes

        idx = 0
        cleaned_content = []

        # If content is a single string, split it into lines
        if isinstance(content, str):
            content = content.split('\n')

        # Remove leading and trailing empty lines
        while content and content[0].strip() == '':
            content.pop(0)
        while content and content[-1].strip() == '':
            content.pop()

        # Process each remaining line to detect flags and conditions
        for line in content:
            idx = 0
            line = line.strip()  # Trim any leading/trailing whitespace

            # Parse flags and conditions only at the beginning of the line
            while idx < len(line):
                ch = line[idx]
                if ch == '!':
                    attributes['eval'] = True
                    idx += 1
                elif ch == '?':
                    attributes['facultative'] = True
                    idx += 1
                elif ch == '^':
                    attributes['readonly'] = True
                    idx += 1
                elif ch == '~':
                    attributes['detectvar'] = False
                    idx += 1
                elif line.startswith('[if:', idx):
                    # Parse condition
                    end_idx = line.find(']', idx)
                    if end_idx == -1:
                        raise ValueError("Unclosed '[if:]' tag in content")
                    cond_str = line[idx + 4:end_idx]
                    if ';eval' in cond_str:
                        attributes['condeval'] = True
                        cond_str = cond_str.replace(';eval', '')
                    attributes['condition'] = cond_str.strip()
                    idx = end_idx + 1
                else:
                    break  # No more flags or conditions, process the rest of the line

                # Skip any whitespace after flags or conditions
                while idx < len(line) and line[idx] in (' ', '\t'):
                    idx += 1

            # Append the cleaned line (without flags or condition tags)
            cleaned_content.append(line[idx:].strip())

        # Join the cleaned lines back into a single string
        cleaned_content = '\n'.join(cleaned_content)

        return cleaned_content, attributes



    def __str__(self):
        num_attrs = len(self.attributes)  # All attributes count
        return f"1 line/block, {num_attrs} attributes"


    def __repr__(self):
      # Template content section
      total_lines = len(self.content)
      total_variables = len(self.definitions)
      line_word = "lines" if total_lines > 1 else "line"
      variable_word = "defs" if total_variables > 1 else "def"
      content_label = "Template Content"
      content_label += "" if self.userid == "" else f" | id:{self.userid}"
      available_space = 50 - len(content_label) - 1
      repr_str = "-" * 50 + "\n"
      repr_str += f"{content_label}{('(' + str(total_lines) + ' ' + line_word + ', ' + str(total_variables) + ' ' + variable_word + ')').rjust(available_space)}\n"
      repr_str += "-" * 50 + "\n"

      if total_lines < 1:
          repr_str += "< empty content >\n"
      elif total_lines <= 12:
          # If content has 12 or fewer lines, display all lines
          for line in self.content:
              truncated_line = (line[:18] + '[...]' + line[-18:]) if len(line) > 40 else line
              repr_str += f"{truncated_line:<50}\n"
      else:
          # Display first three lines, middle three lines, and last three lines
          for line in self.content[:3]:
              truncated_line = (line[:18] + '[...]' + line[-18:]) if len(line) > 40 else line
              repr_str += f"{truncated_line:<50}\n"
          repr_str += "\t\t[...]\n"
          mid_start = total_lines // 2 - 1
          mid_end = mid_start + 3
          for line in self.content[mid_start:mid_end]:
              truncated_line = (line[:18] + '[...]' + line[-18:]) if len(line) > 40 else line
              repr_str += f"{truncated_line:<50}\n"
          repr_str += "\t\t[...]\n"
          for line in self.content[-3:]:
              truncated_line = (line[:18] + '[...]' + line[-18:]) if len(line) > 40 else line
              repr_str += f"{truncated_line:<50}\n"

      # Detected Variables section in 3-column format
      detected_variables = self.detect_variables()
      if detected_variables:
          repr_str += "-" * 50 + "\n"
          # Count the number of defined and missing variables
          defined_variables = set(self.definitions.keys()) if self.definitions else set()
          variable_word = 'Variables' if len(detected_variables) > 1 else 'Variable'
          detected_set = set(detected_variables)
          missing_variables = detected_set - defined_variables
          defined_count = len(detected_set & defined_variables)
          missing_count = len(missing_variables)
          total_variables = len(detected_set)
          repr_str += f"Detected {variable_word}{('(' + str(total_variables) + ' / +' + str(defined_count) + ' / -' + str(missing_count) + ')').rjust(50 - len('Detected Variables') - 1)}\n"
          repr_str += "-" * 50 + "\n"

          # Display variables with status:
          # [+] for defined variables with values other than "${varname}"
          # [-] for defined variables with default values "${varname}"
          # [ ] for undefined variables
          for i in range(0, len(detected_variables), 3):
              var_set = detected_variables[i:i+3]
              line = ""
              for var in var_set:
                  var_name = (var[:10] + ' ') if len(var) > 10 else var.ljust(11)
                  if var in defined_variables:
                      # Check if it's set to its default value (i.e., "${varname}")
                      if self.definitions[var] == f"${{{var}}}":
                          flag = '[-]'
                      else:
                          flag = '[+]'
                  else:
                      flag = '[ ]'
                  line += f"{flag} {var_name}  "
              repr_str += f"{line}\n"

      # Attribute section in 3 columns
      repr_str += "-" * 50 + "\n"
      repr_str += f"Template Attributes{('(' + str(len(self.attributes)) + ' attributes)').rjust(50 - len('Template Attributes') - 1)}\n"
      repr_str += "-" * 50 + "\n"

      # Create a compact three-column layout for attributes with checkboxes
      attr_items = [(attr, '[x]' if value else '[ ]') for attr, value in self.attributes.items() if attr != 'definitions']
      for i in range(0, len(attr_items), 3):
          attr_set = attr_items[i:i+3]
          line = ""
          for attr, value in attr_set:
              attr_name = (attr[:10] + ' ') if len(attr) > 10 else attr.ljust(11)
              line += f"{value} {attr_name}  "
          repr_str += f"{line}\n"

      return repr_str


    def __setattr__(self, name, value):
        # Handle specific attributes with validation
        if name in ['facultative', 'eval', 'readonly']:
            if not isinstance(value, bool):
                raise TypeError(f"The '{name}' attribute must be a Boolean, not {type(value).__name__}.")
        elif name == 'condition':
            if not isinstance(value, str) and value is not None:
                raise TypeError(f"The 'condition' attribute must be a string or None, not {type(value).__name__}.")
        elif name == 'content':
            # Use helper to manage content updates and cache invalidation
            self._update_content(value)
            return  # Exit to avoid further processing since _update_content handles the setting
        elif name == 'definitions':
            if not isinstance(value, (lambdaScriptdata, scriptdata)) and value is not None:
                raise TypeError(f"The 'definitions' must be a lambdaScriptdata or scriptdata, not {type(value).__name__}.")

        # Set attributes directly for key fields and avoid recursion
        if name in ['userid', 'attributes', 'definitions', '_content', '_content_hash', '_autorefresh']:
            super().__setattr__(name, value)
        else:
            # Ensure 'attributes' is initialized before updating
            if not hasattr(self, 'attributes') or self.attributes is None:
                self.attributes = {}
            self.attributes[name] = value


    def __getattr__(self, name):
        """
        Handles attribute retrieval, checking the following in order:
        1. If 'name' is in default_attributes, return the value from attributes if it exists,
           otherwise return the default value from default_attributes.
        2. If 'name' is 'content', return the content (or an empty string if content is not set).
        3. If 'name' exists in the attributes dictionary, return its value.
        4. If attributes itself exists in __dict__, return the value from attributes if 'name' is found.
        5. If all previous checks fail, raise an AttributeError indicating that 'name' is not found.
        """
        # Ensure '_CACHE' is always accessible without a KeyError
        if name == '_CACHE':
            # Initialize _CACHE if it does not exist
            if '_CACHE' not in self.__dict__:
                self.__dict__['_CACHE'] = {
                    'variables': {'result': None},
                    'check_variables': {'result': None}
                }
            return self.__dict__['_CACHE']  # Access _CACHE directly
        # Step 1: Check if 'name' is a valid attribute in default_attributes
        if name in self.default_attributes:
            # Directly access __dict__ to avoid recursive lookup
            attributes = self.__dict__.get('attributes', {})
            return attributes.get(name, self.default_attributes[name])
        # Step 2: Special case for 'content'
        if name == 'content':
            # Directly access __dict__ to avoid recursion
            return self.__dict__.get('_content', "")
        if name == "_autorefresh":
            return self.__dict__.get('_autorefresh', True)
        # Step 3: Check if 'name' exists in 'attributes' and return its value directly
        attributes = self.__dict__.get('attributes', {})
        if name in attributes:
            return attributes[name]
        # Step 4: Check if 'attributes' exists in __dict__ and retrieve 'name' if present
        if 'attributes' in self.__dict__ and name in self.__dict__['attributes']:
            return self.__dict__['attributes'][name]
        # Step 5: If none of the above conditions are met, raise an AttributeError
        raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")




    def do(self, protected=True, softrun=False, USER=lambdaScriptdata()):
        """
        Executes or prepares the script template content based on its attributes and the `softrun` flag.

        Parameters
        ----------
        protected : bool, optional
            If `True` (default), variable evaluation uses a protected environment, safeguarding global definitions
            and system attributes from modification during execution.
        softrun : bool, optional
            Determines if the script is evaluated in a preliminary mode:
            - If `True`, returns the script content without full evaluation, allowing for a preview or
              an initial capture of local definitions without substituting variables.
            - If `False` (default), processes the script with full evaluation, applying all substitutions
              defined in `definitions`.
        USER : lambdaScriptdata, optional
            A `lambdaScriptdata` instance with user-provided definitions, which supplement or override
            template-level definitions during execution.

        Returns
        -------
        str
            The processed or original script content as a single string.
            - Returns an empty string if `facultative` is set to `True`, or if `condition` is `False`
              and does not meet any specified evaluation requirements.

        Notes
        -----
        - `facultative`: If `True`, the method returns an empty string, effectively skipping execution of the content.
        - `condition`: If specified, this attribute is evaluated to decide whether the content should be processed
          (default `True` if `condition` is `None`). If `condeval` is `True`, the condition itself undergoes
          evaluation using Python's `eval`.
        - `eval`: If `True` and `softrun` is `False`, performs evaluation for variable substitution on the
          content lines, applying transformations based on both `definitions` and `USER` definitions if provided.
          This allows variables to be dynamically substituted within the script content.

        Processing Workflow
        -------------------
        1. **Facultative Check**: If the `facultative` attribute is `True`, immediately returns an empty string.
        2. **Condition Check**: If a `condition` is specified, it is evaluated:
           - If `condeval` is `True`, the condition undergoes evaluation (using `eval`).
           - If the evaluated `condition` is `False`, returns an empty string, skipping execution.
        3. **Execution Based on `softrun`**:
           - If `softrun` is `True`, returns the original content without variable substitution, providing a preview.
           - If `softrun` is `False`, evaluates the content lines based on `definitions` and `USER` if applicable.
        4. **Variable Formatting**: During evaluation, lists and tuples are formatted into strings with prefixed comments,
           enhancing readability and handling complex data structures directly in the script.

        Example
        -------
        >>> template = ScriptTemplate(
        ...     content=["variable x equal ${var1}", "print 'Value of x is ${var1}'"],
        ...     definitions=lambdaScriptdata(var1=10)
        ... )
        >>> template.do()
        "variable x equal 10\nprint 'Value of x is 10'"

        """

        # If 'facultative' is set to True, return an empty string immediately
        if self.attributes.get("facultative", False):
            return ""

        # Evaluate condition if present
        cond = True
        if self.attributes.get("condition") is not None:
            condition_expr = self.definitions.formateval(self.attributes["condition"], protected)
            cond = eval(condition_expr) if self.attributes.get("condeval", False) else condition_expr

        # Process based on softrun flag
        if softrun:
            return "\n".join(self.content) if cond else ""

        # Perform full processing when softrun is False
        if cond:
            if self.attributes.get("eval", False):
                #return "\n".join([self.definitions.formateval(line, protected) for line in self.content])
                inputs = self.definitions + USER
                for k in inputs.keys():
                    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=","))
                return "\n".join([inputs.formateval(line, protected) for line in self.content])

            else:
                return "\n".join(self.content)

        return ""



    def detect_variables(self):
        """
        Detects variables in the content of the template using the pattern r'\$\{(\w+)\}'.

        Returns:
        --------
        list
            A list of unique variable names detected in the content.
        """
        # Check cache first
        cache_entry = self._CACHE['variables']
        if cache_entry['result'] is not None:
            return cache_entry['result']
        # Detect variables if cache miss or content changed
        variable_pattern = re.compile(r'\$\{(\w+)\}')
        detected_vars = {variable for line in self.content for variable in variable_pattern.findall(line)}
        # Cache the result and return output
        cache_entry['result'] = list(detected_vars)
        return cache_entry['result']



    def refreshvar(self,globaldefinitions = lambdaScriptdata()):
        """
        Detects variables in the content and adds them to definitions if needed.
        This method ensures that variables like ${varname} are correctly detected
        and added to the definitions if they are missing.

        use globaldefinitions to add a list of global variables/definitions

        """
        if self.attributes["detectvar"] and isinstance(self.content, list) and self.definitions:
            variables = self.detect_variables()
            for varname in variables:
                if (varname not in self.definitions) and (varname not in globaldefinitions):
                    self.definitions.setattr(varname, "${" + varname + "}")


    def check_variables(self, verbose=True, seteval=True):
        """
        Checks for undefined variables in the ScriptTemplate instance.

        Parameters:
        -----------
        verbose : bool, optional, default=True
            If True, prints information about variables for the template.
            Shows [-] if the variable is set to its default value, [+] if it is defined, and [ ] if it is undefined.

        seteval : bool, optional, default=True
            If True, sets the `eval` attribute to True if at least one variable is defined or set to its default value.

        Returns:
        --------
        out : dict
            A dictionary with lists of default variables, set variables, and undefined variables:
            - "defaultvalues": Variables set to their default value (${varname}).
            - "setvalues": Variables defined with a specific value.
            - "undefined": Variables that are undefined.
        """
        # Check cache first
        cache_entry = self._CACHE['check_variables']
        if cache_entry['result'] is not None:
            return cache_entry['result']
        # Main
        out = {"defaultvalues": [], "setvalues": [], "undefined": []}
        detected_vars = self.detect_variables()
        defined_vars = set(self.definitions.keys()) if self.definitions else set()
        set_values, default_values, undefined_vars = [], [], []
        if verbose:
            print(f"\nTEMPLATE {self.userid} variables:")
        for var in detected_vars:
            if var in defined_vars:
                if self.definitions[var] == f"${{{var}}}":  # Check for default value
                    default_values.append(var)
                    if verbose:
                        print(f"[-] {var}")  # Variable is set to its default value
                else:
                    set_values.append(var)
                    if verbose:
                        print(f"[+] {var}")  # Variable is defined with a specific value
            else:
                undefined_vars.append(var)
                if verbose:
                    print(f"[ ] {var}")  # Variable is not defined
        # If seteval is True, set eval to True if at least one variable is defined or set to its default
        if seteval and (set_values or default_values):
            self.attributes['eval'] = True  # Set eval in the attributes dictionary
        # Update the output dictionary
        out["defaultvalues"].extend(default_values)
        out["setvalues"].extend(set_values)
        out["undefined"].extend(undefined_vars)
        # update Cache and return output
        cache_entry['result'] = out
        return out


    def is_variable_defined(self, var_name):
        """
        Checks if a specified variable is defined (either as a default value or a set value).

        Parameters:
        -----------
        var_name : str
            The name of the variable to check.

        Returns:
        --------
        bool
            True if the variable is defined (either as a default or a set value), False if not.

        Raises:
        -------
        ValueError
            If `var_name` is invalid or undefined in the template.
        """
        if not isinstance(var_name, str):
            raise ValueError("Variable name must be a string.")

        variable_status = self.check_variables(verbose=False, seteval=False)

        # Check if the variable is in default values or set values
        if var_name in variable_status["defaultvalues"] or var_name in variable_status["setvalues"]:
            return True
        else:
            raise ValueError(f"Variable '{var_name}' is undefined in the template.")


    def is_variable_set_value_only(self, var_name):
        """
        Checks if a specified variable is defined and set to a specific (non-default) value.

        Parameters:
        -----------
        var_name : str
            The name of the variable to check.

        Returns:
        --------
        bool
            True if the variable is defined with a set (non-default) value, False if not.

        Raises:
        -------
        ValueError
            If `var_name` is invalid or not defined in the template.
        """
        if not isinstance(var_name, str):
            raise ValueError("Variable name must be a string.")

        variable_status = self.check_variables(verbose=False, seteval=False)

        # Check if the variable is in set values only (not default values)
        if var_name in variable_status["setvalues"]:
            return True
        elif var_name in variable_status["defaultvalues"]:
            return False  # Defined but only at its default value
        else:
            raise ValueError(f"Variable '{var_name}' is undefined in the template.")

Class variables

var default_attributes

Static methods

def parse_content(content, verbose=False)

Parse the content string and return: 1. Cleaned content (without flags or condition tags). 2. A dictionary of attributes (flags) initialized with default values.

Parameters:

content : str The content string to be parsed.

Returns:

cleaned_content : str The cleaned content with all flag prefixes and condition tags removed. attributes : dict A dictionary of attributes (flags) set based on the content prefixes.

Expand source code
@classmethod
def parse_content(cls, content, verbose=False):
    """
    Parse the content string and return:
    1. Cleaned content (without flags or condition tags).
    2. A dictionary of attributes (flags) initialized with default values.

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

    Returns:
    --------
    cleaned_content : str
        The cleaned content with all flag prefixes and condition tags removed.
    attributes : dict
        A dictionary of attributes (flags) set based on the content prefixes.
    """
    attributes = cls.default_attributes.copy()

    # Handle empty content
    if not content.strip():  # Empty string or whitespace-only
        return "", attributes

    idx = 0
    cleaned_content = []

    # If content is a single string, split it into lines
    if isinstance(content, str):
        content = content.split('\n')

    # Remove leading and trailing empty lines
    while content and content[0].strip() == '':
        content.pop(0)
    while content and content[-1].strip() == '':
        content.pop()

    # Process each remaining line to detect flags and conditions
    for line in content:
        idx = 0
        line = line.strip()  # Trim any leading/trailing whitespace

        # Parse flags and conditions only at the beginning of the line
        while idx < len(line):
            ch = line[idx]
            if ch == '!':
                attributes['eval'] = True
                idx += 1
            elif ch == '?':
                attributes['facultative'] = True
                idx += 1
            elif ch == '^':
                attributes['readonly'] = True
                idx += 1
            elif ch == '~':
                attributes['detectvar'] = False
                idx += 1
            elif line.startswith('[if:', idx):
                # Parse condition
                end_idx = line.find(']', idx)
                if end_idx == -1:
                    raise ValueError("Unclosed '[if:]' tag in content")
                cond_str = line[idx + 4:end_idx]
                if ';eval' in cond_str:
                    attributes['condeval'] = True
                    cond_str = cond_str.replace(';eval', '')
                attributes['condition'] = cond_str.strip()
                idx = end_idx + 1
            else:
                break  # No more flags or conditions, process the rest of the line

            # Skip any whitespace after flags or conditions
            while idx < len(line) and line[idx] in (' ', '\t'):
                idx += 1

        # Append the cleaned line (without flags or condition tags)
        cleaned_content.append(line[idx:].strip())

    # Join the cleaned lines back into a single string
    cleaned_content = '\n'.join(cleaned_content)

    return cleaned_content, attributes

Instance variables

var content
Expand source code
@property
def content(self):
    return self._content

Methods

def check_variables(self, verbose=True, seteval=True)

Checks for undefined variables in the ScriptTemplate instance.

Parameters:

verbose : bool, optional, default=True If True, prints information about variables for the template. Shows [-] if the variable is set to its default value, [+] if it is defined, and [ ] if it is undefined.

seteval : bool, optional, default=True If True, sets the eval attribute to True if at least one variable is defined or set to its default value.

Returns:

out : dict A dictionary with lists of default variables, set variables, and undefined variables: - "defaultvalues": Variables set to their default value (${varname}). - "setvalues": Variables defined with a specific value. - "undefined": Variables that are undefined.

Expand source code
def check_variables(self, verbose=True, seteval=True):
    """
    Checks for undefined variables in the ScriptTemplate instance.

    Parameters:
    -----------
    verbose : bool, optional, default=True
        If True, prints information about variables for the template.
        Shows [-] if the variable is set to its default value, [+] if it is defined, and [ ] if it is undefined.

    seteval : bool, optional, default=True
        If True, sets the `eval` attribute to True if at least one variable is defined or set to its default value.

    Returns:
    --------
    out : dict
        A dictionary with lists of default variables, set variables, and undefined variables:
        - "defaultvalues": Variables set to their default value (${varname}).
        - "setvalues": Variables defined with a specific value.
        - "undefined": Variables that are undefined.
    """
    # Check cache first
    cache_entry = self._CACHE['check_variables']
    if cache_entry['result'] is not None:
        return cache_entry['result']
    # Main
    out = {"defaultvalues": [], "setvalues": [], "undefined": []}
    detected_vars = self.detect_variables()
    defined_vars = set(self.definitions.keys()) if self.definitions else set()
    set_values, default_values, undefined_vars = [], [], []
    if verbose:
        print(f"\nTEMPLATE {self.userid} variables:")
    for var in detected_vars:
        if var in defined_vars:
            if self.definitions[var] == f"${{{var}}}":  # Check for default value
                default_values.append(var)
                if verbose:
                    print(f"[-] {var}")  # Variable is set to its default value
            else:
                set_values.append(var)
                if verbose:
                    print(f"[+] {var}")  # Variable is defined with a specific value
        else:
            undefined_vars.append(var)
            if verbose:
                print(f"[ ] {var}")  # Variable is not defined
    # If seteval is True, set eval to True if at least one variable is defined or set to its default
    if seteval and (set_values or default_values):
        self.attributes['eval'] = True  # Set eval in the attributes dictionary
    # Update the output dictionary
    out["defaultvalues"].extend(default_values)
    out["setvalues"].extend(set_values)
    out["undefined"].extend(undefined_vars)
    # update Cache and return output
    cache_entry['result'] = out
    return out
def detect_variables(self)

Detects variables in the content of the template using the pattern r'\${(\w+)}'.

Returns:

list A list of unique variable names detected in the content.

Expand source code
def detect_variables(self):
    """
    Detects variables in the content of the template using the pattern r'\$\{(\w+)\}'.

    Returns:
    --------
    list
        A list of unique variable names detected in the content.
    """
    # Check cache first
    cache_entry = self._CACHE['variables']
    if cache_entry['result'] is not None:
        return cache_entry['result']
    # Detect variables if cache miss or content changed
    variable_pattern = re.compile(r'\$\{(\w+)\}')
    detected_vars = {variable for line in self.content for variable in variable_pattern.findall(line)}
    # Cache the result and return output
    cache_entry['result'] = list(detected_vars)
    return cache_entry['result']
def do(self, protected=True, softrun=False, USER=Lambda Script Parameters (LSD object) with 0 parameter definitions)

Executes or prepares the script template content based on its attributes and the softrun flag.

    Parameters
    ----------
    protected : bool, optional
        If <code>True</code> (default), variable evaluation uses a protected environment, safeguarding global definitions
        and system attributes from modification during execution.
    softrun : bool, optional
        Determines if the script is evaluated in a preliminary mode:
        - If <code>True</code>, returns the script content without full evaluation, allowing for a preview or
          an initial capture of local definitions without substituting variables.
        - If <code>False</code> (default), processes the script with full evaluation, applying all substitutions
          defined in <code>definitions</code>.
    USER : lambdaScriptdata, optional
        A <code><a title="dscript.lambdaScriptdata" href="#dscript.lambdaScriptdata">lambdaScriptdata</a></code> instance with user-provided definitions, which supplement or override
        template-level definitions during execution.

    Returns
    -------
    str
        The processed or original script content as a single string.
        - Returns an empty string if <code>facultative</code> is set to <code>True</code>, or if <code>condition</code> is <code>False</code>
          and does not meet any specified evaluation requirements.

    Notes
    -----
    - <code>facultative</code>: If <code>True</code>, the method returns an empty string, effectively skipping execution of the content.
    - <code>condition</code>: If specified, this attribute is evaluated to decide whether the content should be processed
      (default <code>True</code> if <code>condition</code> is <code>None</code>). If <code>condeval</code> is <code>True</code>, the condition itself undergoes
      evaluation using Python's <code>eval</code>.
    - <code>eval</code>: If <code>True</code> and <code>softrun</code> is <code>False</code>, performs evaluation for variable substitution on the
      content lines, applying transformations based on both <code>definitions</code> and <code>USER</code> definitions if provided.
      This allows variables to be dynamically substituted within the script content.

    Processing Workflow
    -------------------
    1. **Facultative Check**: If the <code>facultative</code> attribute is <code>True</code>, immediately returns an empty string.
    2. **Condition Check**: If a <code>condition</code> is specified, it is evaluated:
       - If <code>condeval</code> is <code>True</code>, the condition undergoes evaluation (using <code>eval</code>).
       - If the evaluated <code>condition</code> is <code>False</code>, returns an empty string, skipping execution.
    3. **Execution Based on <code>softrun</code>**:
       - If <code>softrun</code> is <code>True</code>, returns the original content without variable substitution, providing a preview.
       - If <code>softrun</code> is <code>False</code>, evaluates the content lines based on <code>definitions</code> and <code>USER</code> if applicable.
    4. **Variable Formatting**: During evaluation, lists and tuples are formatted into strings with prefixed comments,
       enhancing readability and handling complex data structures directly in the script.

    Example
    -------
    >>> template = ScriptTemplate(
    ...     content=["variable x equal ${var1}", "print 'Value of x is ${var1}'"],
    ...     definitions=lambdaScriptdata(var1=10)
    ... )
    >>> template.do()
    "variable x equal 10

print 'Value of x is 10'"

Expand source code
def do(self, protected=True, softrun=False, USER=lambdaScriptdata()):
    """
    Executes or prepares the script template content based on its attributes and the `softrun` flag.

    Parameters
    ----------
    protected : bool, optional
        If `True` (default), variable evaluation uses a protected environment, safeguarding global definitions
        and system attributes from modification during execution.
    softrun : bool, optional
        Determines if the script is evaluated in a preliminary mode:
        - If `True`, returns the script content without full evaluation, allowing for a preview or
          an initial capture of local definitions without substituting variables.
        - If `False` (default), processes the script with full evaluation, applying all substitutions
          defined in `definitions`.
    USER : lambdaScriptdata, optional
        A `lambdaScriptdata` instance with user-provided definitions, which supplement or override
        template-level definitions during execution.

    Returns
    -------
    str
        The processed or original script content as a single string.
        - Returns an empty string if `facultative` is set to `True`, or if `condition` is `False`
          and does not meet any specified evaluation requirements.

    Notes
    -----
    - `facultative`: If `True`, the method returns an empty string, effectively skipping execution of the content.
    - `condition`: If specified, this attribute is evaluated to decide whether the content should be processed
      (default `True` if `condition` is `None`). If `condeval` is `True`, the condition itself undergoes
      evaluation using Python's `eval`.
    - `eval`: If `True` and `softrun` is `False`, performs evaluation for variable substitution on the
      content lines, applying transformations based on both `definitions` and `USER` definitions if provided.
      This allows variables to be dynamically substituted within the script content.

    Processing Workflow
    -------------------
    1. **Facultative Check**: If the `facultative` attribute is `True`, immediately returns an empty string.
    2. **Condition Check**: If a `condition` is specified, it is evaluated:
       - If `condeval` is `True`, the condition undergoes evaluation (using `eval`).
       - If the evaluated `condition` is `False`, returns an empty string, skipping execution.
    3. **Execution Based on `softrun`**:
       - If `softrun` is `True`, returns the original content without variable substitution, providing a preview.
       - If `softrun` is `False`, evaluates the content lines based on `definitions` and `USER` if applicable.
    4. **Variable Formatting**: During evaluation, lists and tuples are formatted into strings with prefixed comments,
       enhancing readability and handling complex data structures directly in the script.

    Example
    -------
    >>> template = ScriptTemplate(
    ...     content=["variable x equal ${var1}", "print 'Value of x is ${var1}'"],
    ...     definitions=lambdaScriptdata(var1=10)
    ... )
    >>> template.do()
    "variable x equal 10\nprint 'Value of x is 10'"

    """

    # If 'facultative' is set to True, return an empty string immediately
    if self.attributes.get("facultative", False):
        return ""

    # Evaluate condition if present
    cond = True
    if self.attributes.get("condition") is not None:
        condition_expr = self.definitions.formateval(self.attributes["condition"], protected)
        cond = eval(condition_expr) if self.attributes.get("condeval", False) else condition_expr

    # Process based on softrun flag
    if softrun:
        return "\n".join(self.content) if cond else ""

    # Perform full processing when softrun is False
    if cond:
        if self.attributes.get("eval", False):
            #return "\n".join([self.definitions.formateval(line, protected) for line in self.content])
            inputs = self.definitions + USER
            for k in inputs.keys():
                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=","))
            return "\n".join([inputs.formateval(line, protected) for line in self.content])

        else:
            return "\n".join(self.content)

    return ""
def is_variable_defined(self, var_name)

Checks if a specified variable is defined (either as a default value or a set value).

Parameters:

var_name : str The name of the variable to check.

Returns:

bool True if the variable is defined (either as a default or a set value), False if not.

Raises:

ValueError If var_name is invalid or undefined in the template.

Expand source code
def is_variable_defined(self, var_name):
    """
    Checks if a specified variable is defined (either as a default value or a set value).

    Parameters:
    -----------
    var_name : str
        The name of the variable to check.

    Returns:
    --------
    bool
        True if the variable is defined (either as a default or a set value), False if not.

    Raises:
    -------
    ValueError
        If `var_name` is invalid or undefined in the template.
    """
    if not isinstance(var_name, str):
        raise ValueError("Variable name must be a string.")

    variable_status = self.check_variables(verbose=False, seteval=False)

    # Check if the variable is in default values or set values
    if var_name in variable_status["defaultvalues"] or var_name in variable_status["setvalues"]:
        return True
    else:
        raise ValueError(f"Variable '{var_name}' is undefined in the template.")
def is_variable_set_value_only(self, var_name)

Checks if a specified variable is defined and set to a specific (non-default) value.

Parameters:

var_name : str The name of the variable to check.

Returns:

bool True if the variable is defined with a set (non-default) value, False if not.

Raises:

ValueError If var_name is invalid or not defined in the template.

Expand source code
def is_variable_set_value_only(self, var_name):
    """
    Checks if a specified variable is defined and set to a specific (non-default) value.

    Parameters:
    -----------
    var_name : str
        The name of the variable to check.

    Returns:
    --------
    bool
        True if the variable is defined with a set (non-default) value, False if not.

    Raises:
    -------
    ValueError
        If `var_name` is invalid or not defined in the template.
    """
    if not isinstance(var_name, str):
        raise ValueError("Variable name must be a string.")

    variable_status = self.check_variables(verbose=False, seteval=False)

    # Check if the variable is in set values only (not default values)
    if var_name in variable_status["setvalues"]:
        return True
    elif var_name in variable_status["defaultvalues"]:
        return False  # Defined but only at its default value
    else:
        raise ValueError(f"Variable '{var_name}' is undefined in the template.")
def refreshvar(self, globaldefinitions=Lambda Script Parameters (LSD object) with 0 parameter definitions)

Detects variables in the content and adds them to definitions if needed. This method ensures that variables like ${varname} are correctly detected and added to the definitions if they are missing.

use globaldefinitions to add a list of global variables/definitions

Expand source code
def refreshvar(self,globaldefinitions = lambdaScriptdata()):
    """
    Detects variables in the content and adds them to definitions if needed.
    This method ensures that variables like ${varname} are correctly detected
    and added to the definitions if they are missing.

    use globaldefinitions to add a list of global variables/definitions

    """
    if self.attributes["detectvar"] and isinstance(self.content, list) and self.definitions:
        variables = self.detect_variables()
        for varname in variables:
            if (varname not in self.definitions) and (varname not in globaldefinitions):
                self.definitions.setattr(varname, "${" + varname + "}")
class dscript (name=None, SECTIONS=['DYNAMIC'], section=0, position=None, role='dscript instance', description='dynamic script', userid='dscript', version=None, license=None, email=None, printflag=False, verbose=False, verbosity=None, **userdefinitions)

dscript: A Dynamic Script Management Class

The dscript class is designed to manage and dynamically generate multiple lines/items of a script, typically for use with LAMMPS or similar simulation tools. Each line in the script is represented as a ScriptTemplate object, and the class provides tools to easily manipulate, concatenate, and execute these script lines/items.

Key Features:

  • Dynamic Script Generation: Define and manage script lines/items dynamically, with variables that can be substituted at runtime.
  • Conditional Execution: Add conditions to script lines/items so they are only included if certain criteria are met.
  • Script Concatenation: Combine multiple script objects while maintaining control over variable precedence and script structure.
  • User-Friendly Access: Easily access and manipulate script lines/items using familiar Python constructs like indexing and iteration.

Practical Use Cases:

  • Custom LAMMPS Scripts: Generate complex simulation scripts with varying parameters based on dynamic conditions.
  • Automation: Automate the creation of scripts for batch processing, simulations, or other repetitive tasks.
  • Script Management: Manage and version-control different script sections and configurations easily.

Methods:

init(self, name=None): Initializes a new dscript object with an optional name.

getitem(self, key): Retrieves a script line by its key. If a list of keys is provided, returns a new dscript object with lines/items reordered accordingly.

setitem(self, key, value): Adds or updates a script line. If the value is an empty list, the corresponding script line is removed.

delitem(self, key): Deletes a script line by its key.

contains(self, key): Checks if a key exists in the script. Allows usage of in keyword.

iter(self): Returns an iterator over the script lines/items, allowing for easy iteration through all lines/items in the TEMPLATE.

len(self): Returns the number of script lines/items currently stored in the TEMPLATE.

keys(self): Returns the keys of the TEMPLATE dictionary.

values(self): Returns the ScriptTemplate objects stored as values in the TEMPLATE.

items(self): Returns the keys and ScriptTemplate objects from the TEMPLATE as pairs.

str(self): Returns a human-readable summary of the script, including the number of lines/items and total attributes. Shortcut: str(S).

repr(self): Provides a detailed string representation of the entire dscript object, including all script lines/items and their attributes. Useful for debugging.

reorder(self, order): Reorders the script lines/items based on a given list of indices, creating a new dscript object with the reordered lines/items.

get_content_by_index(self, index, do=True, protected=True): Returns the processed content of the script line at the specified index, with variables substituted based on the definitions and conditions applied.

get_attributes_by_index(self, index): Returns the attributes of the script line at the specified index.

add_dynamic_script(self, key, content="", definitions=None, verbose=None, **USER): Add a dynamic script step to the dscript object.

createEmptyVariables(self, vars): Creates new variables in DEFINITIONS if they do not already exist. Accepts a single variable name or a list of variable names.

do(self, printflag=None, verbose=None): Executes all script lines/items in the TEMPLATE, concatenating the results, and handling variable substitution. Returns the full script as a string.

script(self, **userdefinitions): Generates a lamdaScript object from the current dscript object, applying any additional user definitions provided.

pipescript(self, printflag=None, verbose=None, **USER): Returns a pipescript object by combining script objects for all keys in the TEMPLATE. Each key in TEMPLATE is handled separately, and the resulting scripts are combined using the | operator.

save(self, filename=None, foldername=None, overwrite=False): Saves the current script instance to a text file in a structured format. Includes metadata, global parameters, definitions, templates, and attributes.

write(scriptcontent, filename=None, foldername=None, overwrite=False): Writes the provided script content to a specified file in a given folder, with a header added if necessary, ensuring the correct file format.

load(cls, filename, foldername=None, numerickeys=True): Loads a script instance from a text file, restoring the content, definitions, templates, and attributes. Handles parsing and variable substitution based on the structure of the file.

parsesyntax(cls, content, numerickeys=True): Parses a script instance from a string input, restoring the content, definitions, templates, and attributes. Handles parsing and variable substitution based on the structure of the provided string, ensuring the correct format and key conversions when necessary.

Example:

Create a dscript object

R = dscript(name="MyScript")

Define global variables (DEFINITIONS)

R.DEFINITIONS.dimension = 3 R.DEFINITIONS.units = "$si"

Add script lines

R[0] = "dimension ${dimension}" R[1] = "units ${units}"

Generate and print the script

sR = R.script() print(sR.do())

Attributes:

name : str The name of the script, useful for identification. TEMPLATE : dict A dictionary storing script lines/items, with keys to identify each line. DEFINITIONS : lambdaScriptdata Stores the variables and parameters used within the script lines/items.

Initializes a new dscript object.

The constructor sets up a new dscript object, which allows you to define and manage a script composed of multiple lines/items. Each line is stored in the TEMPLATE dictionary, and variables used in the script are stored in DEFINITIONS.

Parameters:

name : str, optional The name of the script. If no name is provided, a random name will be generated automatically. The name is useful for identifying the script, especially when managing multiple scripts.

Example:

Create a dscript object with a specific name

R = dscript(name="ExampleScript")

Or create a dscript object with a random name

R = dscript()

After initialization, you can start adding script lines/items and defining variables.

Expand source code
class dscript:
    """
    dscript: A Dynamic Script Management Class

    The `dscript` class is designed to manage and dynamically generate multiple
    lines/items of a script, typically for use with LAMMPS or similar simulation tools.
    Each line in the script is represented as a `ScriptTemplate` object, and the
    class provides tools to easily manipulate, concatenate, and execute these
    script lines/items.

    Key Features:
    -------------
    - **Dynamic Script Generation**: Define and manage script lines/items dynamically,
      with variables that can be substituted at runtime.
    - **Conditional Execution**: Add conditions to script lines/items so they are only
      included if certain criteria are met.
    - **Script Concatenation**: Combine multiple script objects while maintaining
      control over variable precedence and script structure.
    - **User-Friendly Access**: Easily access and manipulate script lines/items using
      familiar Python constructs like indexing and iteration.

    Practical Use Cases:
    --------------------
    - **Custom LAMMPS Scripts**: Generate complex simulation scripts with varying
      parameters based on dynamic conditions.
    - **Automation**: Automate the creation of scripts for batch processing,
      simulations, or other repetitive tasks.
    - **Script Management**: Manage and version-control different script sections
      and configurations easily.

    Methods:
    --------
    __init__(self, name=None):
        Initializes a new `dscript` object with an optional name.

    __getitem__(self, key):
        Retrieves a script line by its key. If a list of keys is provided,
        returns a new `dscript` object with lines/items reordered accordingly.

    __setitem__(self, key, value):
        Adds or updates a script line. If the value is an empty list, the
        corresponding script line is removed.

    __delitem__(self, key):
        Deletes a script line by its key.

    __contains__(self, key):
        Checks if a key exists in the script. Allows usage of `in` keyword.

    __iter__(self):
        Returns an iterator over the script lines/items, allowing for easy iteration
        through all lines/items in the `TEMPLATE`.

    __len__(self):
        Returns the number of script lines/items currently stored in the `TEMPLATE`.

    keys(self):
        Returns the keys of the `TEMPLATE` dictionary.

    values(self):
        Returns the `ScriptTemplate` objects stored as values in the `TEMPLATE`.

    items(self):
        Returns the keys and `ScriptTemplate` objects from the `TEMPLATE` as pairs.

    __str__(self):
        Returns a human-readable summary of the script, including the number
        of lines/items and total attributes. Shortcut: `str(S)`.

    __repr__(self):
        Provides a detailed string representation of the entire `dscript` object,
        including all script lines/items and their attributes. Useful for debugging.

    reorder(self, order):
        Reorders the script lines/items based on a given list of indices, creating a
        new `dscript` object with the reordered lines/items.

    get_content_by_index(self, index, do=True, protected=True):
        Returns the processed content of the script line at the specified index,
        with variables substituted based on the definitions and conditions applied.

    get_attributes_by_index(self, index):
        Returns the attributes of the script line at the specified index.

    add_dynamic_script(self, key, content="", definitions=None, verbose=None, **USER):
        Add a dynamic script step to the `dscript` object.

    createEmptyVariables(self, vars):
        Creates new variables in `DEFINITIONS` if they do not already exist.
        Accepts a single variable name or a list of variable names.

    do(self, printflag=None, verbose=None):
        Executes all script lines/items in the `TEMPLATE`, concatenating the results,
        and handling variable substitution. Returns the full script as a string.

    script(self, **userdefinitions):
        Generates a `lamdaScript` object from the current `dscript` object,
        applying any additional user definitions provided.

    pipescript(self, printflag=None, verbose=None, **USER):
        Returns a `pipescript` object by combining script objects for all keys
        in the `TEMPLATE`. Each key in `TEMPLATE` is handled separately, and
        the resulting scripts are combined using the `|` operator.

    save(self, filename=None, foldername=None, overwrite=False):
        Saves the current script instance to a text file in a structured format.
        Includes metadata, global parameters, definitions, templates, and attributes.

    write(scriptcontent, filename=None, foldername=None, overwrite=False):
        Writes the provided script content to a specified file in a given folder,
        with a header added if necessary, ensuring the correct file format.

    load(cls, filename, foldername=None, numerickeys=True):
        Loads a script instance from a text file, restoring the content, definitions,
        templates, and attributes. Handles parsing and variable substitution based on
        the structure of the file.

    parsesyntax(cls, content, numerickeys=True):
        Parses a script instance from a string input, restoring the content, definitions,
        templates, and attributes. Handles parsing and variable substitution based on the
        structure of the provided string, ensuring the correct format and key conversions
        when necessary.

    Example:
    --------
    # Create a dscript object
    R = dscript(name="MyScript")

    # Define global variables (DEFINITIONS)
    R.DEFINITIONS.dimension = 3
    R.DEFINITIONS.units = "$si"

    # Add script lines
    R[0] = "dimension    ${dimension}"
    R[1] = "units        ${units}"

    # Generate and print the script
    sR = R.script()
    print(sR.do())

    Attributes:
    -----------
    name : str
        The name of the script, useful for identification.
    TEMPLATE : dict
        A dictionary storing script lines/items, with keys to identify each line.
    DEFINITIONS : lambdaScriptdata
        Stores the variables and parameters used within the script lines/items.
    """

    # Class variable to list attributes that should not be treated as TEMPLATE entries
    construction_attributes = {'name', 'SECTIONS', 'section', 'position', 'role', 'description',
                               'userid', 'verbose', 'printflag', 'DEFINITIONS', 'TEMPLATE',
                               'version','license','email'
                               }

    def __init__(self,  name=None,
                        SECTIONS = ["DYNAMIC"],
                        section = 0,
                        position = None,
                        role = "dscript instance",
                        description = "dynamic script",
                        userid = "dscript",
                        version = None,
                        license = None,
                        email = None,
                        printflag = False,
                        verbose = False,
                        verbosity = None,
                        **userdefinitions
                        ):
        """
        Initializes a new `dscript` object.

        The constructor sets up a new `dscript` object, which allows you to
        define and manage a script composed of multiple lines/items. Each line is
        stored in the `TEMPLATE` dictionary, and variables used in the script
        are stored in `DEFINITIONS`.

        Parameters:
        -----------
        name : str, optional
            The name of the script. If no name is provided, a random name will
            be generated automatically. The name is useful for identifying the
            script, especially when managing multiple scripts.

        Example:
        --------
        # Create a dscript object with a specific name
        R = dscript(name="ExampleScript")

        # Or create a dscript object with a random name
        R = dscript()

        After initialization, you can start adding script lines/items and defining variables.
        """

        if name is None:
            self.name = autoname()
        else:
            self.name = name

        if (version is None) or (license is None) or (email is None):
            metadata = get_metadata()               # retrieve all metadata
            version = metadata["version"] if version is None else version
            license = metadata["license"] if license is None else license
            email = metadata["email"] if email is None else email
        self.SECTIONS = SECTIONS if isinstance(SECTIONS,(list,tuple)) else [SECTIONS]
        self.section = section
        self.position = position if position is not None else 0
        self.role = role
        self.description = description
        self.userid = userid
        self.version = version
        self.license = license
        self.email = email
        self.printflag = printflag
        self.verbose = verbose if verbosity is None else verbosity>0
        self.verbosity = 0 if not verbose else verbosity
        self.DEFINITIONS = lambdaScriptdata(**userdefinitions)
        self.TEMPLATE = {}

    def __getattr__(self, attr):
        # During construction phase, we only access the predefined attributes
        if 'TEMPLATE' not in self.__dict__:
            if attr in self.__dict__:
                return self.__dict__[attr]
            raise AttributeError(f"'dscript' object has no attribute '{attr}'")

        # If TEMPLATE is initialized and attr is in TEMPLATE, return the corresponding ScriptTemplate entry
        if attr in self.TEMPLATE:
            return self.TEMPLATE[attr]

        # Fall back to internal __dict__ attributes if not in TEMPLATE
        if attr in self.__dict__:
            return self.__dict__[attr]

        raise AttributeError(f"'dscript' object has no attribute '{attr}'")


    def __setattr__(self, attr, value):
        # Handle internal attributes during the construction phase
        if 'TEMPLATE' not in self.__dict__:
            self.__dict__[attr] = value
            return

        # Handle construction attributes separately (name, TEMPLATE, USER)
        if attr in self.construction_attributes:
            self.__dict__[attr] = value
        # If TEMPLATE exists, and the attribute is intended for it, update TEMPLATE
        elif 'TEMPLATE' in self.__dict__:
            # Convert the value to a ScriptTemplate if needed, and update the template
            if attr in self.TEMPLATE:
                # Modify the existing ScriptTemplate object for this attribute
                if isinstance(value, str):
                    self.TEMPLATE[attr].content = value
                elif isinstance(value, dict):
                    self.TEMPLATE[attr].attributes.update(value)
            else:
                # Create a new entry if it does not exist
                self.TEMPLATE[attr] = ScriptTemplate(content=value,definitions=self.DEFINITIONS,verbose=self.verbose,userid=attr)
        else:
            # Default to internal attributes
            self.__dict__[attr] = value


    def __getitem__(self, key):
        """
        Implements index-based retrieval, slicing, or reordering for dscript objects.

        Parameters:
        -----------
        key : int, slice, list, or str
            - If `key` is an int, returns the corresponding template at that index.
              Supports negative indices to retrieve templates from the end.
            - If `key` is a slice, returns a new `dscript` object containing the templates in the specified range.
            - If `key` is a list, reorders the TEMPLATE based on the list of indices or keys.
            - If `key` is a string, treats it as a key and returns the corresponding template.

        Returns:
        --------
        dscript or ScriptTemplate : Depending on the type of key, returns either a new dscript object (for slicing or reordering)
                                    or a ScriptTemplate object (for direct key access).
        """
        # Handle list-based reordering
        if isinstance(key, list):
            return self.reorder(key)

        # Handle slicing
        elif isinstance(key, slice):
            new_dscript = dscript(name=f"{self.name}_slice_{key.start}_{key.stop}")
            keys = list(self.TEMPLATE.keys())
            for k in keys[key]:
                new_dscript.TEMPLATE[k] = self.TEMPLATE[k]
            return new_dscript

        # Handle integer indexing with support for negative indices
        elif isinstance(key, int):
            keys = list(self.TEMPLATE.keys())
            if key in self.TEMPLATE:  # Check if the integer exists as a key
                return self.TEMPLATE[key]
            if key < 0:  # Support negative indices
                key += len(keys)
            if key < 0 or key >= len(keys):  # Check for index out of range
                raise IndexError(f"Index {key - len(keys)} is out of range")
            # Return the template corresponding to the integer index
            return self.TEMPLATE[keys[key]]

        # Handle key-based access (string keys)
        elif isinstance(key, str):
            if key in self.TEMPLATE:
                return self.TEMPLATE[key]
            raise KeyError(f"Key '{key}' does not exist in TEMPLATE.")


    def __setitem__(self, key, value):
        if (value == []) or (value is None):
            # If the value is an empty list, delete the corresponding key
            del self.TEMPLATE[key]
        else:
            # Otherwise, set the key to the new ScriptTemplate
            self.TEMPLATE[key] = ScriptTemplate(value, definitions=self.DEFINITIONS, verbose=self.verbose, userid=key)

    def __delitem__(self, key):
        del self.TEMPLATE[key]

    def __iter__(self):
        return iter(self.TEMPLATE.items())

    # def keys(self):
    #     return self.TEMPLATE.keys()

    # def values(self):
    #     return (s.content for s in self.TEMPLATE.values())

    def __contains__(self, key):
        return key in self.TEMPLATE

    def __len__(self):
        return len(self.TEMPLATE)

    def items(self):
        return ((key, s.content) for key, s in self.TEMPLATE.items())

    def __str__(self):
        num_TEMPLATE = len(self.TEMPLATE)
        total_attributes = sum(len(s.attributes) for s in self.TEMPLATE.values())
        return f"{num_TEMPLATE} TEMPLATE, {total_attributes} attributes"

    def __repr__(self):
        """Representation of dscript object with additional properties."""
        repr_str = f"dscript object ({self.name})\n"
        # Add description, role, and version at the beginning
        repr_str += f"id: {self.userid}\n" if self.userid else ""
        repr_str += f"Descr: {self.description}\n" if self.description else ""
        repr_str += f"Role: {self.role} (v. {self.version})\n"
        repr_str += f'SECTIONS {span(self.SECTIONS,",","[","]")} | index: {self.section} | position: {self.position}\n'
        repr_str += f"\n\n\twith {len(self.TEMPLATE)} TEMPLATE"
        repr_str += "s" if len(self.TEMPLATE)>1 else ""
        repr_str += f" (with {len(self.DEFINITIONS)} DEFINITIONS)\n\n"
        # Add TEMPLATE information
        c = 0
        for k, s in self.TEMPLATE.items():
            head = f"|  idx: {c} |  key: {k}  |"
            dashes = (50 - len(head)) // 2
            repr_str += "-" * 50 + "\n"
            repr_str += f"{'<' * dashes}{head}{'>' * dashes}\n{repr(s)}\n"
            c += 1
        return repr_str

    def keys(self):
        """Return the keys of the TEMPLATE."""
        return self.TEMPLATE.keys()

    def values(self):
        """Return the ScriptTemplate objects in TEMPLATE."""
        return self.TEMPLATE.values()

    def reorder(self, order):
        """Reorder the TEMPLATE lines according to a list of indices."""
        # Get the original items as a list of (key, value) pairs
        original_items = list(self.TEMPLATE.items())
        # Create a new dictionary with reordered scripts, preserving original keys
        new_scripts = {original_items[i][0]: original_items[i][1] for i in order}
        # Create a new dscript object with reordered scripts
        reordered_script = dscript()
        reordered_script.TEMPLATE = new_scripts
        return reordered_script


    def get_content_by_index(self, index, do=True, protected=True):
        """
        Returns the content of the ScriptTemplate at the specified index.

        Parameters:
        -----------
        index : int
            The index of the template in the TEMPLATE dictionary.
        do : bool, optional (default=True)
            If True, the content will be processed based on conditions and evaluation flags.
        protected : bool, optional (default=True)
            Controls whether variable evaluation is protected (e.g., prevents overwriting certain definitions).

        Returns:
        --------
        str or list of str
            The content of the template after processing, or an empty string if conditions or evaluation flags block it.
        """
        key = list(self.TEMPLATE.keys())[index]
        s = self.TEMPLATE[key].content
        att = self.TEMPLATE[key].attributes
        # Return an empty string if the facultative attribute is True and do is True
        if att["facultative"] and do:
            return ""
        # Evaluate the condition (if any)
        if att["condition"] is not None:
            cond = eval(self.DEFINITIONS.formateval(att["condition"], protected))
        else:
            cond = True
        # If the condition is met, process the content
        if cond:
            # Apply formateval only if the eval attribute is True and do is True
            if att["eval"] and do:
                if isinstance(s, list):
                    # Apply formateval to each item in the list if s is a list
                    return [self.DEFINITIONS.formateval(line, protected) for line in s]
                else:
                    # Apply formateval to the single string content
                    return self.DEFINITIONS.formateval(s, protected)
            else:
                return s  # Return the raw content if no evaluation is needed
        elif do:
            return ""  # Return an empty string if the condition is not met and do is True
        else:
            return s  # Return the raw content if do is False


    def get_attributes_by_index(self, index):
        """ Returns the attributes of the ScriptTemplate at the specified index."""
        key = list(self.TEMPLATE.keys())[index]
        return self.TEMPLATE[key].attributes


    def __add__(self, other):
        """
        Concatenates two dscript objects, creating a new dscript object that combines
        the TEMPLATE and DEFINITIONS of both. This operation avoids deep copying
        of definitions by creating a new lambdaScriptdata instance from the definitions.

        Parameters:
        -----------
        other : dscript
            The other dscript object to concatenate with the current one.

        Returns:
        --------
        result : dscript
            A new dscript object with the concatenated TEMPLATE and merged DEFINITIONS.

        Raises:
        -------
        TypeError:
            If the other object is not an instance of dscript, script, pipescript
        """
        if isinstance(other, dscript):
            # Step 1: Merge global DEFINITIONS from both self and other
            result = dscript(name=self.name+"+"+other.name,**(self.DEFINITIONS + other.DEFINITIONS))
            # Step 2: Start by copying the TEMPLATE from self (no deepcopy for performance reasons)
            result.TEMPLATE = self.TEMPLATE.copy()
            # Step 3: Ensure that the local definitions for `self.TEMPLATE` are properly copied
            for key, value in self.TEMPLATE.items():
                result.TEMPLATE[key].definitions = lambdaScriptdata(**self.TEMPLATE[key].definitions)
            # Step 4: Track the next available index if keys need to be created
            next_index = len(result.TEMPLATE)
            # Step 5: Add items from the other dscript object, updating or generating new keys as needed
            for key, value in other.TEMPLATE.items():
                if key in result.TEMPLATE:
                    # If key already exists in result, assign a new unique index
                    while next_index in result.TEMPLATE:
                        next_index += 1
                    new_key = next_index
                else:
                    # Use the original key if it doesn't already exist
                    new_key = key
                # Copy the TEMPLATE's content and definitions from `other`
                result.TEMPLATE[new_key] = value
                # Merge the local TEMPLATE definitions from `other`
                result.TEMPLATE[new_key].definitions = lambdaScriptdata(**other.TEMPLATE[key].definitions)
            return result
        elif isinstance(other,script):
            return self.script() + script
        elif isinstance(other,pipescript):
            return self.pipescript() | script
        else:
            raise TypeError(f"Cannot concatenate 'dscript' with '{type(other).__name__}'")


    def __call__(self, *keys):
        """
        Extracts subobjects from the dscript based on the provided keys.

        Parameters:
        -----------
        *keys : one or more keys that correspond to the `TEMPLATE` entries.

        Returns:
        --------
        A new `dscript` object that contains only the selected script lines/items, along with
        the relevant definitions and attributes from the original object.
        """
        # Create a new dscript object to store the extracted sub-objects
        result = dscript(name=f"{self.name}_subobject")
        # Copy the TEMPLATE entries corresponding to the provided keys
        for key in keys:
            if key in self.TEMPLATE:
                result.TEMPLATE[key] = self.TEMPLATE[key]
            else:
                raise KeyError(f"Key '{key}' not found in TEMPLATE.")
        # Copy the DEFINITIONS from the current object
        result.DEFINITIONS = copy.deepcopy(self.DEFINITIONS)
        # Copy other relevant attributes
        result.SECTIONS = self.SECTIONS[:]
        result.section = self.section
        result.position = self.position
        result.role = self.role
        result.description = self.description
        result.userid = self.userid
        result.version = self.version
        result.verbose = self.verbose
        result.printflag = self.printflag

        return result

    def createEmptyVariables(self, vars):
        """
        Creates empty variables in DEFINITIONS if they don't already exist.

        Parameters:
        -----------
        vars : str or list of str
            The variable name or list of variable names to be created in DEFINITIONS.
        """
        if isinstance(vars, str):
            vars = [vars]  # Convert single variable name to list for uniform processing
        for varname in vars:
            if varname not in self.DEFINITIONS:
                self.DEFINITIONS.setattr(varname,"${" + varname + "}")


    def do(self, printflag=None, verbose=None, softrun=False, return_definitions=False,comment_chars="#%", **USER):
        """
        Executes or previews all `ScriptTemplate` instances in `TEMPLATE`, concatenating their processed content.
        Allows for optional headers and footers based on verbosity settings, and offers a preliminary preview mode with `softrun`.
        Accumulates definitions across all templates if `return_definitions=True`.

        Parameters
        ----------
        printflag : bool, optional
            If `True`, enables print output during execution. Defaults to the instance's print flag if `None`.
        verbose : bool, optional
            If `True`, includes headers and footers in the output, providing additional detail.
            Defaults to the instance's verbosity setting if `None`.
        softrun : bool, optional
            If `True`, executes the script in a preliminary mode:
            - Bypasses full variable substitution for a preview of the content, useful for validating structure.
            - If `False` (default), performs full processing, including variable substitutions and evaluations.
        return_definitions : bool, optional
            If `True`, returns a tuple where the second element contains accumulated definitions from all templates.
            If `False` (default), returns only the concatenated 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.
        **USER : keyword arguments
            Allows for the provision of additional user-defined definitions, where each keyword represents a
            definition key and the associated value represents the definition's content. These definitions
            can override or supplement template-level definitions during execution.

        Returns
        -------
        str or tuple
            - If `return_definitions=False`, returns the concatenated output of all `ScriptTemplate` instances,
              with optional headers, footers, and execution summary based on verbosity.
            - If `return_definitions=True`, returns a tuple of (`output`, `accumulated_definitions`), where
              `accumulated_definitions` contains all definitions used across templates.

        Notes
        -----
        - Each `ScriptTemplate` in `TEMPLATE` is processed individually using its own `do()` method.
        - The `softrun` mode provides a preliminary content preview without full variable substitution,
          helpful for inspecting the script structure or gathering local definitions.
        - When `verbose` is enabled, the method includes detailed headers, footers, and a summary of processed and
          ignored items, providing insight into the script's construction and variable usage.
        - Accumulated definitions from each `ScriptTemplate` are combined if `return_definitions=True`, which can be
          useful for tracking all variables and definitions applied across the templates.

        Example
        -------
        >>> dscript_instance = dscript(name="ExampleScript")
        >>> dscript_instance.TEMPLATE[0] = ScriptTemplate(
        ...     content=["units ${units}", "boundary ${boundary}"],
        ...     definitions=lambdaScriptdata(units="lj", boundary="p p p"),
        ...     attributes={'eval': True}
        ... )
        >>> dscript_instance.do(verbose=True, units="real")
        # Output:
        # --------------
        # TEMPLATE "ExampleScript"
        # --------------
        units real
        boundary p p p
        # ---> Total items: 2 - Ignored items: 0
        """

        printflag = self.printflag if printflag is None else printflag
        verbose = self.verbose if verbose is None else verbose
        header = f"# --------------[ TEMPLATE \"{self.name}\" ]--------------" if verbose else ""
        footer = "# --------------------------------------------" if verbose else ""

        # Initialize output, counters, and optional definitions accumulator
        output = [header]
        non_empty_lines = 0
        ignored_lines = 0
        accumulated_definitions = lambdaScriptdata() if return_definitions else None

        for key, template in self.TEMPLATE.items():
            # Process each template with softrun if enabled, otherwise use full processing
            result = template.do(softrun=softrun,USER=lambdaScriptdata(**USER))
            if result:
                # Apply comment removal based on verbosity
                final_result = result if verbose else remove_comments(result,comment_chars=comment_chars)
                if final_result or verbose:
                    output.append(final_result)
                    non_empty_lines += 1
                else:
                    ignored_lines += 1
                # Accumulate definitions if return_definitions is enabled
                if return_definitions:
                    accumulated_definitions += template.definitions
            else:
                ignored_lines += 1

        # Add footer summary if verbose
        nel_word = 'items' if non_empty_lines > 1 else 'item'
        il_word = 'items' if ignored_lines > 1 else 'item'
        footer += f"\n# ---> Total {nel_word}: {non_empty_lines} - Ignored {il_word}: {ignored_lines}" if verbose else ""
        output.append(footer)

        # Concatenate output and determine return type based on return_definitions
        output_content = "\n".join(output)
        return (output_content, accumulated_definitions) if return_definitions else output_content



    def script(self,printflag=None, verbose=None, verbosity=None, **USER):
        """
        returns the corresponding script
        """
        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
        return lamdaScript(self,persistentfile=True, persistentfolder=None,
                           printflag=printflag, verbose=verbose,
                           **USER)

    def pipescript(self, *keys, printflag=None, verbose=None, verbosity=None, **USER):
        """
        Returns a pipescript object by combining script objects corresponding to the given keys.

        Parameters:
        -----------
        *keys : one or more keys that correspond to the `TEMPLATE` entries.
        printflag : bool, optional
            Whether to enable printing of additional information.
        verbose : bool, optional
            Whether to run in verbose mode for debugging or detailed output.
        **USER : dict, optional
            Additional user-defined variables to pass into the script.

        Returns:
        --------
        A `pipescript` object that combines the script objects generated from the selected
        dscript subobjects.
        """
        # Start with an empty pipescript
        # combined_pipescript = None
        # # Iterate over the provided keys to extract corresponding subobjects
        # for key in keys:
        #     # Extract the dscript subobject for the given key
        #     sub_dscript = self(key)
        #     # Convert the dscript subobject to a script object, passing USER, printflag, and verbose
        #     script_obj = sub_dscript.script(printflag=printflag, verbose=verbose, **USER)
        #     # Combine script objects into a pipescript object
        #     if combined_pipescript is None:
        #         combined_pipescript = pipescript(script_obj)  # Initialize pipescript
        #     else:
        #         combined_pipescript = combined_pipescript | script_obj  # Use pipe operator
        # if combined_pipescript is None:
        #     ValueError('The conversion to pipescript from {type{self}} falled')
        # return combined_pipescript
        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

        # Loop over all keys in TEMPLATE and combine them
        combined_pipescript = None
        for key in self.keys():
            # Create a new dscript object with only the current key in TEMPLATE
            focused_dscript = dscript(name=f"{self.name}:{key}")
            focused_dscript.TEMPLATE[key] = self.TEMPLATE[key]
            focused_dscript.TEMPLATE[key].definitions = scriptdata(**self.TEMPLATE[key].definitions)
            focused_dscript.DEFINITIONS = scriptdata(**self.DEFINITIONS)
            focused_dscript.SECTIONS = self.SECTIONS[:]
            focused_dscript.section = self.section
            focused_dscript.position = self.position
            focused_dscript.role = self.role
            focused_dscript.description = self.description
            focused_dscript.userid = self.userid
            focused_dscript.version = self.version
            focused_dscript.verbose = verbose
            focused_dscript.printflag = printflag

            # Convert the focused dscript object to a script object
            script_obj = focused_dscript.script(printflag=printflag, verbose=verbose, **USER)

            # Combine the script objects into a pipescript object using the pipe operator
            if combined_pipescript is None:
                combined_pipescript = pipescript(script_obj)  # Initialize pipescript
            else:
                combined_pipescript = combined_pipescript | pipescript(script_obj)  # Use pipe operator

        if combined_pipescript is None:
            ValueError('The conversion to pipescript from {type{self}} falled')
        return combined_pipescript


    @staticmethod
    def header(name=None, verbose=True, verbosity=None, style=2, filepath=None, version=None, license=None, email=None):
        """
        Generate a formatted header for the DSCRIPT file.

        ### Parameters:
            name (str, optional): The name of the script. If None, "Unnamed" is used.
            verbose (bool, optional): Whether to include the header. Default is True.
            verbosity (int, optional): Verbosity level. Overrides `verbose` if specified.
            style (int, optional): ASCII style for the header (default=2).
            filepath (str, optional): Full path to the file being saved. If None, the line mentioning the file path is excluded.
            version (str, optional): DSCRIPT version. If None, it is omitted from the header.
            license (str, optional): License type. If None, it is omitted from the header.
            email (str, optional): Contact email. If None, it is omitted from the header.

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

        ### The header includes:
            - DSCRIPT version, license, and contact email, if provided.
            - The name of the script.
            - Filepath, if provided.
            - Information on where and when the script was generated.

        ### Notes:
            - If `verbosity` is specified, it overrides `verbose`.
            - Omits metadata lines if `version`, `license`, or `email` are not provided.
        """
        # Resolve verbosity
        verbose = verbosity > 0 if verbosity is not None else verbose
        if not verbose:
            return ""
        # Validate inputs
        if name is None:
            name = "Unnamed"
        # Prepare metadata line
        metadata = []
        if version:
            metadata.append(f"v{version}")
        if license:
            metadata.append(f"License: {license}")
        if email:
            metadata.append(f"Email: {email}")
        metadata_line = " | ".join(metadata)
        # Prepare the framed header content
        lines = []
        if metadata_line:
            lines.append(f"PIZZA.DSCRIPT FILE {metadata_line}")
        lines += [
            "",
            f"Name: {name}",
        ]
        # Add the filepath line if filepath is not None
        if filepath:
            lines.append(f"Path: {filepath}")
        lines += [
            "",
            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 frame_header function to format the framed content
        return frame_header(lines, style=style)



    # Generator -- added on 2024-10-17
    # -----------
    def generator(self):
        """
        Returns
        -------
        STR
            generated code corresponding to dscript (using dscript syntax/language).

        """
        return self.save(generatoronly=True)


    # Save Method -- added on 2024-09-04
    # -----------
    def save(self, filename=None, foldername=None, overwrite=False, generatoronly=False, onlyusedvariables=True):
        """
        Save the current script instance to a text file.

        Parameters
        ----------
        filename : str, optional
            The name of the file to save the script to. If not provided, `self.name` is used.
            The extension ".txt" is automatically appended if not included.

        foldername : str, optional
            The directory where the file will be saved. If not provided, it defaults to the system's
            temporary directory. If the filename does not include a full path, this folder will be used.

        overwrite : bool, default=True
            Whether to overwrite the file if it already exists. If set to False, an exception is raised
            if the file exists.

        generatoronly : bool, default=False
            If True, the method returns the generated content string without saving to a file.

        onlyusedvariables : bool, default=True
            If True, local definitions are only saved if they are used within the template content.
            If False, all local definitions are saved, regardless of whether they are referenced in
            the template.

        Raises
        ------
        FileExistsError
            If the file already exists and `overwrite` is set to False.

        Notes
        -----
        - The script is saved in a plain text format, and each section (global parameters, definitions,
          template, and attributes) is written in a structured format with appropriate comments.
        - If `self.name` is used as the filename, it must be a valid string that can serve as a file name.
        - The file structure follows the format:
            # DSCRIPT SAVE FILE
            # generated on YYYY-MM-DD on user@hostname

            # GLOBAL PARAMETERS
            { ... }

            # DEFINITIONS (number of definitions=...)
            key=value

            # TEMPLATES (number of items=...)
            key: template_content

            # ATTRIBUTES (number of items with explicit attributes=...)
            key:{attr1=value1, attr2=value2, ...}
        """
        # At the beginning of the save method
        start_time = time.time()  # Start the timer

        if not generatoronly:
            # Use self.name if filename is not provided
            if filename is None:
                filename = span(self.name, sep="\n")

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

            # Construct the full path
            if foldername in [None, ""]:  # Handle cases where foldername is None or an empty string
                filepath = os.path.abspath(filename)
            else:
                filepath = os.path.join(foldername, filename)

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

        # Header with current date, username, and host
        header = "# DSCRIPT SAVE FILE\n"
        header += "\n"*2
        if generatoronly:
            header += dscript.header(verbose=True,filepath='dynamic code generation (no file)',
                                     name = self.name, version=self.version, license=self.license, email=self.email)
        else:
            header += dscript.header(verbose=True,filepath=filepath,
                                     name = self.name, version=self.version, license=self.license, email=self.email)
        header += "\n"*2

        # Global parameters in strict Python syntax
        global_params = "# GLOBAL PARAMETERS (8 parameters)\n"
        global_params += "{\n"
        global_params += f"    SECTIONS = {self.SECTIONS},\n"
        global_params += f"    section = {self.section},\n"
        global_params += f"    position = {self.position},\n"
        global_params += f"    role = {self.role!r},\n"
        global_params += f"    description = {self.description!r},\n"
        global_params += f"    userid = {self.userid!r},\n"
        global_params += f"    version = {self.version},\n"
        global_params += f"    verbose = {self.verbose}\n"
        global_params += "}\n"

        # Initialize definitions with self.DEFINITIONS
        allvars = self.DEFINITIONS

        # Temporary dictionary to track global variable information
        global_var_info = {}

        # Loop over each template item to detect and record variable usage and overrides
        for template_index, (key, script_template) in enumerate(self.TEMPLATE.items()):
            # Detect variables used in this template
            used_variables = script_template.detect_variables()

            # Loop over each template item to detect and record variable usage and overrides
            for template_index, (key, script_template) in enumerate(self.TEMPLATE.items()):
                # Detect variables used in this template
                used_variables = script_template.detect_variables()

                # Check each variable used in this template
                for var in used_variables:
                    # Get global and local values for the variable
                    global_value = getattr(allvars, var, None)
                    local_value = getattr(script_template.definitions, var, None)
                    is_global = var in allvars  # Check if the variable originates in global space
                    is_default = is_global and (
                        global_value is None or global_value == "" or global_value == f"${{{var}}}"
                    )

                    # If the variable is not yet tracked, initialize its info
                    if var not in global_var_info:
                        global_var_info[var] = {
                            "value": global_value,       # Initial value from allvars if exists
                            "updatedvalue": global_value,# Initial value from allvars if exists
                            "is_default": is_default,    # Check if it’s set to a default value
                            "first_def": None,           # First definition (to be updated later)
                            "first_use": template_index, # First time the variable is used
                            "first_val": global_value,
                            "override_index": template_index if local_value is not None else None,  # Set override if defined locally
                            "is_global": is_global       # Track if the variable originates as global
                        }
                    else:
                        # Update `override_index` if the variable is defined locally and its value changes
                        if local_value is not None:
                            # Check if the local value differs from the tracked value in global_var_info
                            current_value = global_var_info[var]["value"]
                            if current_value != local_value:
                                global_var_info[var]["override_index"] = template_index
                                global_var_info[var]["updatedvalue"] = local_value  # Update the tracked value


            # Second loop: Update `first_def` for all variables
            for template_index, (key, script_template) in enumerate(self.TEMPLATE.items()):
                local_definitions = script_template.definitions.keys()
                for var in local_definitions:
                    if var in global_var_info and global_var_info[var]["first_def"] is None:
                        global_var_info[var]["first_def"] = template_index
                        global_var_info[var]["first_val"] = getattr(script_template.definitions, var)


        # Filter global definitions based on usage, overrides, and first_def
        filtered_globals = {
            var: info for var, info in global_var_info.items()
            if (info["is_global"] or info["is_default"]) and (info["override_index"] is None)
            }


        # Generate the definitions output based on filtered globals
        definitions = f"\n# GLOBAL DEFINITIONS (number of definitions={len(filtered_globals)})\n"
        for var, info in filtered_globals.items():
            if info["is_default"] and (info["first_def"]>info["first_use"] if info["first_def"] else True):
                definitions += f"{var} = ${{{var}}}  # value assumed to be defined outside this DSCRIPT file\n"
            else:
                value = info["first_val"] #info["value"]
                if value in ["", None]:
                    definitions += f'{var} = ""\n'
                elif isinstance(value, str):
                    safe_value = value.replace('\\', '\\\\').replace('\n', '\\n')
                    definitions += f"{var} = {safe_value}\n"
                else:
                    definitions += f"{var} = {value}\n"

        # Template (number of lines/items)
        printsinglecontent = False
        template = f"\n# TEMPLATES (number of items={len(self.TEMPLATE)})\n"
        for key, script_template in self.TEMPLATE.items():
            # Get local template definitions and detected variables
            template_vars = script_template.definitions
            used_variables = script_template.detect_variables()
            islocal = False
            # Temporary dictionary to accumulate variables to add to allvars
            valid_local_vars = lambdaScriptdata()
            # Write template-specific definitions only if they meet the updated conditions
            for var in template_vars.keys():
                # Conditions for adding a variable to the local template and to `allvars`
                if (var in used_variables or not onlyusedvariables) and (
                    script_template.is_variable_set_value_only(var) and
                    (var not in allvars or getattr(template_vars, var) != getattr(allvars, var))
                ):
                    # Start local definitions section if this is the first local variable for the template
                    if not islocal:
                        template += f"\n# LOCAL DEFINITIONS for key '{key}'\n"
                        islocal = True
                    # Retrieve and process the variable value
                    value = getattr(template_vars, var)
                    if value in ["", None]:
                        template += f'{var} = ""\n'  # Set empty or None values as ""
                    elif isinstance(value, str):
                        safe_value = value.replace('\\', '\\\\').replace('\n', '\\n')
                        template += f"{var} = {safe_value}\n"
                    else:
                        template += f"{var} = {value}\n"
                    # Add the variable to valid_local_vars for selective update of allvars
                    valid_local_vars.setattr(var, value)
            # Update allvars only with filtered, valid local variables
            allvars += valid_local_vars

            # Write the template content
            if isinstance(script_template.content, list):
                if len(script_template.content) == 1:
                    # Single-line template saved as a single line
                    content_str = script_template.content[0].strip()
                    template += "" if printsinglecontent else "\n"
                    template += f"{key}: {content_str}\n"
                    printsinglecontent = True
                else:
                    content_str = '\n    '.join(script_template.content)
                    template += f"\n{key}: [\n    {content_str}\n ]\n"
                    printsinglecontent = False
            else:
                template += "" if printsinglecontent else "\n"
                template += f"{key}: {script_template.content}\n"
                printsinglecontent = True

        # Attributes (number of lines/items with explicit attributes)
        attributes = f"# ATTRIBUTES (number of items with explicit attributes={len(self.TEMPLATE)})\n"
        for key, script_template in self.TEMPLATE.items():
            attr_str = ", ".join(f"{attr_name}={repr(attr_value)}"
                                 for attr_name, attr_value in script_template.attributes.items())
            attributes += f"{key}:{{{attr_str}}}\n"

        # Combine all sections into one content
        content = header + "\n" + global_params + "\n" + definitions + "\n" + template + "\n" + attributes + "\n"


        # Append footer information to the content
        non_empty_lines = sum(1 for line in content.splitlines() if line.strip())  # Count non-empty lines
        execution_time = time.time() - start_time  # Calculate the execution time in seconds
        # Prepare the footer content
        footer_lines = [
            ["Non-empty lines", str(non_empty_lines)],
            ["Execution time (seconds)", f"{execution_time:.4f}"],
        ]
        # Format footer into tabular style
        footer_content = [
            f"{row[0]:<25} {row[1]:<15}" for row in footer_lines
        ]
        # Use frame_header to format footer
        footer = frame_header(
            lines=["DSCRIPT SAVE FILE generator"] + footer_content,
            style=1
        )
        # Append footer to the content
        content += f"\n{footer}"

        if generatoronly:
            return content
        else:
            # Write the content to the file
            with open(filepath, 'w') as f:
                f.write(content)
            print(f"\nScript saved to {filepath}")
            return filepath



    # Write Method -- added on 2024-09-05
    # ------------
    @staticmethod
    @staticmethod
    def write(scriptcontent, filename=None, foldername=None, overwrite=False):
        """
        Writes the provided script content to a specified file in a given folder, with a header if necessary.

        Parameters
        ----------
        scriptcontent : str
            The content to be written to the file.

        filename : str, optional
            The name of the file. If not provided, a random name will be generated.
            The extension `.txt` will be appended if not already present.

        foldername : str, optional
            The folder where the file will be saved. If not provided, the current working directory is used.

        overwrite : bool, optional
            If False (default), raises a `FileExistsError` if the file already exists. If True, the file will be overwritten if it exists.

        Returns
        -------
        str
            The full path to the written file.

        Raises
        ------
        FileExistsError
            If the file already exists and `overwrite` is set to False.

        Notes
        -----
        - A header is prepended to the content if it does not already exist, using the `header` method.
        - The header includes metadata such as the current date, username, hostname, and file details.
        """
        # Generate a random name if filename is not provided
        if filename is None:
            filename = autoname(8)  # Generates a random name of 8 letters

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

        # Handle foldername and relative paths
        if foldername is None or foldername == "":
            # If foldername is empty or None, use current working directory for relative paths
            if not os.path.isabs(filename):
                filepath = os.path.join(os.getcwd(), filename)
            else:
                filepath = filename  # If filename is absolute, use it directly
        else:
            # If foldername is provided and filename is not absolute, use foldername
            if not os.path.isabs(filename):
                filepath = os.path.join(foldername, filename)
            else:
                filepath = filename

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

        # Count total and non-empty lines in the content
        total_lines = len(scriptcontent.splitlines())
        non_empty_lines = sum(1 for line in scriptcontent.splitlines() if line.strip())

        # Prepare header if not already present
        if not scriptcontent.startswith("# DSCRIPT SAVE FILE"):
            fname = os.path.basename(filepath)  # Extracts the filename (e.g., "myscript.txt")
            name, _ = os.path.splitext(fname)   # Removes the extension, e.g., "myscript"
            metadata = get_metadata()           # retrieve all metadata (statically)
            header = dscript.header(name=name, verbosity=True,style=1,filepath=filepath,
                    version = metadata["version"], license = metadata["license"], email = metadata["email"])
            # Add line count information to the header
            footer = frame_header(
                lines=[
                    f"Total lines written: {total_lines}",
                    f"Non-empty lines: {non_empty_lines}"
                ],
                style=1
            )
            scriptcontent = header + "\n" + scriptcontent + "\n" + footer

        # Write the content to the file
        with open(filepath, 'w') as file:
            file.write(scriptcontent)
        return filepath



    # Load Method and its Parsing Rules -- added on 2024-09-04
    # ---------------------------------
    @classmethod
    def load(cls, filename, foldername=None, numerickeys=True):
        """
        Load a script instance from a text file.

        Parameters
        ----------
        filename : str
            The name of the file to load the script from. If the filename does not end with ".txt",
            the extension is automatically appended.

        foldername : str, optional
            The directory where the file is located. If not provided, it defaults to the system's
            temporary directory. If the filename does not include a full path, this folder will be used.

        numerickeys : bool, default=True
            If True, numeric string keys in the template section are automatically converted into integers.
            For example, the key "0" would be converted into the integer 0.

        Returns
        -------
        dscript
            A new `dscript` instance populated with the content of the loaded file.

        Raises
        ------
        ValueError
            If the file does not start with the correct DSCRIPT header or the file format is invalid.

        FileNotFoundError
            If the specified file does not exist.

        Notes
        -----
        - The file is expected to follow the same structured format as the one produced by the `save()` method.
        - The method processes global parameters, definitions, template lines/items, and attributes. If the file
          includes numeric keys as strings (e.g., "0", "1"), they can be automatically converted into integers
          if `numerickeys=True`.
        - The script structure is dynamically rebuilt, and each section (global parameters, definitions,
          template, and attributes) is correctly parsed and assigned to the corresponding parts of the `dscript`
          instance.
        """

        # Step 0 validate filepath
        if not filename.endswith('.txt'):
            filename += '.txt'

        # Handle foldername and relative paths
        if foldername is None or foldername == "":
            # If the foldername is empty or None, use current working directory for relative paths
            if not os.path.isabs(filename):
                filepath = os.path.join(os.getcwd(), filename)
            else:
                filepath = filename  # If filename is absolute, use it directly
        else:
            # If foldername is provided and filename is not absolute, use foldername
            if not os.path.isabs(filename):
                filepath = os.path.join(foldername, filename)
            else:
                filepath = filename

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

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

        # Call parsesyntax to parse the file content
        fname = os.path.basename(filepath)  # Extracts the filename (e.g., "myscript.txt")
        name, _ = os.path.splitext(fname)   # Removes the extension, e.g., "myscript
        return cls.parsesyntax(content, name, numerickeys)



    # Load Method and its Parsing Rules -- added on 2024-09-04
    # ---------------------------------
    @classmethod
    def parsesyntax(cls, content, name=None, numerickeys=True, verbose=False, authentification=True,
                    comment_chars="#%",continuation_marker="..."):
        """
        Parse a DSCRIPT script from a string content.

        Parameters
        ----------
        content : str
            The string content of the DSCRIPT script to be parsed.

        name : str, optional
            The name of the dscript project. If `None`, a random name is generated.

        numerickeys : bool, default=True
            If `True`, numeric string keys in the template section are automatically converted into integers.

        verbose : bool, default=False
            If `True`, the parser will output warnings for unrecognized lines outside of blocks.

        authentification : bool, default=True
            If `True`, the parser is expected that the first non empty line is # DSCRIPT SAVE FILE

        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, optional (default: "...")
            A string containing characters to indicate line continuation
            Any characters after the continuation marker are considered comment and are theorefore ignored



        Returns
        -------
        dscript
            A new `dscript` instance populated with the content of the loaded file.

        Raises
        ------
        ValueError
            If content does not start with the correct DSCRIPT header or the file format is invalid.

        Notes
        -----
        **DSCRIPT SAVE FILE FORMAT**

        This script syntax is designed for creating dynamic and customizable input files, where variables, templates,
        and control attributes can be defined in a flexible manner.

        **Mandatory First Line:**

        Every DSCRIPT file must begin with the following line:

        ```plaintext
        # DSCRIPT SAVE FILE
        ```

        **Structure Overview:**

        1. **Global Parameters Section (Optional):**

            - This section defines global script settings, enclosed within curly braces `{}`.
            - Properties include:
                - `SECTIONS`: List of section names to be considered (e.g., `["DYNAMIC"]`).
                - `section`: Current section index (e.g., `0`).
                - `position`: Current script position in the order.
                - `role`: Defines the role of the script instance (e.g., `"dscript instance"`).
                - `description`: A short description of the script (e.g., `"dynamic script"`).
                - `userid`: Identifier for the user (e.g., `"dscript"`).
                - `version`: Script version (e.g., `0.1`).
                - `verbose`: Verbosity flag, typically a boolean (e.g., `False`).

            **Example:**

            ```plaintext
            {
                SECTIONS = ['INITIALIZATION', 'SIMULATION']  # Global script parameters
            }
            ```

        2. **Definitions Section:**

            - Variables are defined in Python-like syntax, allowing for dynamic variable substitution.
            - Variables can be numbers, strings, or lists, and they can include placeholders using `$`
              to delay execution or substitution.

            **Example:**

            ```plaintext
            d = 3                               # Define a number
            periodic = "$p"                     # '$' prevents immediate evaluation of 'p'
            units = "$metal"                    # '$' prevents immediate evaluation of 'metal'
            dimension = "${d}"                  # Variable substitution
            boundary = ['p', 'p', 'p']          # List with a mix of variables and values
            atom_style = "$atomic"              # String variable with delayed evaluation
            ```

        3. **Templates Section:**

            - This section provides a mapping between keys and their corresponding commands or instructions.
            - Each template can reference variables defined in the **Definitions** section or elsewhere, typically using the `${variable}` syntax.
            - **Syntax Variations**:

                Templates can be defined in several ways, including without blocks, with single- or multi-line blocks, and with ellipsis (`...`) as a line continuation marker.

                - **Single-line Template Without Block**:
                    ```plaintext
                    KEY: INSTRUCTION
                    ```
                    - `KEY` is the identifier for the template (numeric or alphanumeric).
                    - `INSTRUCTION` is the command or template text, which may reference variables.

                - **Single-line Template With Block**:
                    ```plaintext
                    KEY: [INSTRUCTION]
                    ```
                    - Uses square brackets (`[ ]`) around the `INSTRUCTION`, indicating that all instructions are part of the block.

                - **Multi-line Template With Block**:
                    ```plaintext
                    KEY: [
                        INSTRUCTION1
                        INSTRUCTION2
                        ...
                        ]
                    ```
                    - Begins with `KEY: [` and ends with a standalone `]` on a new line.
                    - Instructions within the block can span multiple lines, and ellipses (`...`) at the end of a line are used to indicate that the line continues, ignoring any content following the ellipsis as comments.
                    - Comments following ellipses are removed after parsing and do not become part of the block, preserving only the instructions.

                - **Multi-line Template With Continuation Marker (Ellipsis)**:
                    - For templates with complex code containing square brackets (`[ ]`), the ellipsis (`...`) can be used to prevent `]` from prematurely closing the block. The ellipsis will keep the line open across multiple lines, allowing brackets in the instructions.

                    **Example:**
                    ```plaintext
                    example1: command ${value}       # Single-line template without block
                    example2: [command ${value}]     # Single-line template with block

                    # Multi-line template with block
                    example3: [
                        command1 ${var1}
                        command2 ${var2} ...   # Line continues after ellipsis
                        command3 ${var3} ...   # Additional instruction continues
                        ]

                    # Multi-line template with ellipsis (handling square brackets)
                    example4: [
                        A[0][1] ...            # Ellipsis allows [ ] within instructions
                        B[2][3] ...            # Another instruction in the block
                        ]
                    ```

            - **Key Points**:
                - **Blocks** allow grouping of multiple instructions for a single key, enclosed in square brackets.
                - **Ellipsis (`...`)** at the end of a line keeps the line open, preventing premature closing by `]`, especially useful if the template code includes square brackets (`[ ]`).
                - **Comments** placed after the ellipsis are removed after parsing and are not part of the final block content.

            This flexibility supports both simple and complex template structures, allowing instructions to be grouped logically while keeping code and comments distinct.


        4. **Attributes Section:**

            - Each template line can have customizable attributes to control behavior and conditions.
            - Default attributes include:
                - `facultative`: If `True`, the line is optional and can be removed if needed.
                - `eval`: If `True`, the line will be evaluated with Python's `eval()` function.
                - `readonly`: If `True`, the line cannot be modified later in the script.
                - `condition`: An expression that must be satisfied for the line to be included.
                - `condeval`: If `True`, the condition will be evaluated using `eval()`.
                - `detectvar`: If `True`, this creates variables in the **Definitions** section if they do not exist.

            **Example:**

            ```plaintext
            units: {facultative=False, eval=False, readonly=False, condition="${units}", condeval=False, detectvar=True}
            dim: {facultative=False, eval=False, readonly=False, condition=None, condeval=False, detectvar=True}
            ```

        **Note on Multiple Definitions**

        This example demonstrates how variables defined in the **Definitions** section are handled for each template.
        Each template retains its own snapshot of the variable definitions at the time it is created, ensuring that templates
        can use different values for the same variable if redefined.

        **Example:**

        ```plaintext
        # DSCRIPT SAVE FILE

        # Definitions
        var = 10

        # Template key1
        key1: Template content with ${var}

        # Definitions
        var = 20

        # Template key2
        key2: Template content with ${var}

        # Template key3
        key3:[
            this is an undefined variable ${var31}
            this is another undefined variable ${var32}
            this variable is defined  ${var}
        ]
        ```

        **Parsing and Usage:**

        ```python
        # Parse content using parsesyntax()
        ds = dscript.parsesyntax(content)

        # Accessing templates and their variables
        print(ds.TEMPLATE['key1'].text)  # Output: Template content with 10
        print(ds.TEMPLATE['key2'].text)  # Output: Template content with 20
        ```

        **Handling Undefined Variables:**

        Variables like `${var31}` and `${var32}` in `key3` are undefined. The parser will handle them based on your substitution logic or raise an error if they are required.

        **Important Notes:**

        - The parser processes the script sequentially. Definitions must appear before the templates that use them.
        - Templates capture the variable definitions at the time they are parsed. Redefining a variable affects only subsequent templates.
        - Comments outside of blocks are allowed and ignored by the parser.
        - Content within templates is treated as-is, allowing for any syntax required by the target system (e.g., LAMMPS commands).


    **Advanced Example**

        Here's a more advanced example demonstrating the use of global definitions, local definitions, templates, and how to parse and render the template content.

        ```python
        content = '''
            # GLOBAL DEFINITIONS
            dumpfile = $dump.LAMMPS
            dumpdt = 50
            thermodt = 100
            runtime = 5000

            # LOCAL DEFINITIONS for step '0'
            dimension = 3
            units = $si
            boundary = ['f', 'f', 'f']
            atom_style = $smd
            atom_modify = ['map', 'array']
            comm_modify = ['vel', 'yes']
            neigh_modify = ['every', 10, 'delay', 0, 'check', 'yes']
            newton = $off
            name = $SimulationBox

            # This is a comment line outside of blocks
            # ------------------------------------------

            0: [    % --------------[ Initialization Header (helper) for "${name}" ]--------------
                # set a parameter to None or "" to remove the definition
                dimension    ${dimension}
                units        ${units}
                boundary     ${boundary}
                atom_style   ${atom_style}
                atom_modify  ${atom_modify}
                comm_modify  ${comm_modify}
                neigh_modify ${neigh_modify}
                newton       ${newton}
                # ------------------------------------------
             ]
        '''
        # Parse the content
        ds = dscript.parsesyntax(content, verbose=True, authentification=False)

        # Access and print the rendered template
        print("Template 0 content:")
        print(ds.TEMPLATE[0].do())
        ```

        **Explanation:**

        - **Global Definitions:** Define variables that are accessible throughout the script.
        - **Local Definitions for Step '0':** Define variables specific to a particular step or template.
        - **Template Block:** Identified by `0: [ ... ]`, it contains the content where variables will be substituted.
        - **Comments:** Lines starting with `#` are comments and are ignored by the parser outside of template blocks.

        **Expected Output:**

        ```
        Template 0 content:
        # --------------[ Initialization Header (helper) for "SimulationBox" ]--------------
        # set a parameter to None or "" to remove the definition
        dimension    3
        units        si
        boundary     ['f', 'f', 'f']
        atom_style   smd
        atom_modify  ['map', 'array']
        comm_modify  ['vel', 'yes']
        neigh_modify ['every', 10, 'delay', 0, 'check', 'yes']
        newton       off
        # ------------------------------------------
        ```

        **Notes:**

        - The `do()` method renders the template, substituting variables with their defined values.
        - Variables like `${dimension}` are replaced with their corresponding values defined in the local or global definitions.
        - The parser handles comments and blank lines appropriately, ensuring they don't interfere with the parsing logic.


        """
        # Split the content into lines
        lines = content.splitlines()
        if not lines:
            raise ValueError("File/Content is empty or only contains blank lines.")

        # Initialize containers
        global_params = {}
        GLOBALdefinitions = lambdaScriptdata()
        LOCALdefinitions = lambdaScriptdata()
        template = {}
        attributes = {}

        # State variables
        inside_global_params = False
        global_params_content = ""
        inside_template_block = False
        current_template_key = None
        current_template_content = []
        current_var_value = lambdaScriptdata()

        # Initialize line number
        line_number = 0
        last_successful_line = 0

        # Step 1: Authenticate the file
        if authentification:
            auth_line_found = False
            max_header_lines = 10
            header_end_idx = -1
            for idx, line in enumerate(lines[:max_header_lines]):
                stripped_line = line.strip()
                if not stripped_line:
                    continue
                if stripped_line.startswith("# DSCRIPT SAVE FILE"):
                    auth_line_found = True
                    header_end_idx = idx
                    break
                elif stripped_line.startswith("#") or stripped_line.startswith("%"):
                    continue
                else:
                    raise ValueError(f"Unexpected content before authentication line (# DSCRIPT SAVE FILE) at line {idx + 1}:\n{line}")
            if not auth_line_found:
                raise ValueError("File/Content is not a valid DSCRIPT file.")

            # Remove header lines
            lines = lines[header_end_idx + 1:]
            line_number = header_end_idx + 1
            last_successful_line = line_number - 1
        else:
            line_number = 0
            last_successful_line = 0

        # Process each line
        for idx, line in enumerate(lines):
            line_number += 1
            line_content = line.rstrip('\n')

            # Determine if we're inside a template block
            if inside_template_block:
                # Extract the code with its eventual continuation_marker
                code_line = remove_comments(
                        line_content,
                        comment_chars=comment_chars,
                        continuation_marker=continuation_marker,
                        remove_continuation_marker=False,
                        ).rstrip()

                # Check if line should continue
                if code_line.endswith(continuation_marker):
                    # Append line up to the continuation marker
                    endofline_index = line_content.rindex(continuation_marker)
                    trimmed_content = line_content[:endofline_index].rstrip()
                    if trimmed_content:
                        current_template_content.append(trimmed_content)
                    continue
                elif code_line.endswith("]"):  # End of multi-line block
                    closing_index = code_line.rindex(']')
                    trimmed_content = code_line[:closing_index].rstrip()

                    # Append any valid content before `]`, if non-empty
                    if trimmed_content:
                        current_template_content.append(trimmed_content)

                    # End of template block
                    content = '\n'.join(current_template_content)
                    template[current_template_key] = ScriptTemplate(
                        content=content,
                        autorefresh=False,
                        definitions=LOCALdefinitions,
                        verbose=verbose,
                        userid=current_template_key)
                    # Refresh variables definitions
                    template[current_template_key].refreshvar(globaldefinitions=GLOBALdefinitions)
                    LOCALdefinitions = lambdaScriptdata()
                    # Reset state for next block
                    inside_template_block = False
                    current_template_key = None
                    current_template_content = []
                    last_successful_line = line_number
                    continue
                else:
                    # Append the entire original line content if not ending with `...` or `]`
                    current_template_content.append(line_content)
                continue

            # Not inside a template block
            stripped_no_comments = remove_comments(line_content)

            # Ignore empty lines after removing comments
            if not stripped_no_comments.strip():
                continue

            # If the original line is a comment line, skip it
            if line_content.strip().startswith("#") or line_content.strip().startswith("%"):
                continue

            stripped = stripped_no_comments.strip()

            # Handle start of a new template block
            template_block_match = re.match(r'^(\w+)\s*:\s*\[', stripped)
            if template_block_match:
                current_template_key = template_block_match.group(1)
                if inside_template_block:
                    # Collect error context
                    context_start = max(0, last_successful_line - 3)
                    context_end = min(len(lines), line_number + 2)
                    error_context_lines = lines[context_start:context_end]
                    error_context = ""
                    for i, error_line in enumerate(error_context_lines):
                        line_num = context_start + i + 1
                        indicator = ">" if line_num == line_number else "*" if line_num == last_successful_line else " "
                        error_context += f"{indicator} {line_num}: {error_line}\n"

                    raise ValueError(
                        f"Template block '{current_template_key}' starting at line {last_successful_line} (*) was not properly closed before starting a new one at line {line_number} (>).\n\n"
                        f"Error context:\n{error_context}"
                    )
                else:
                    inside_template_block = True
                    idx_open_bracket = line_content.index('[')
                    remainder = line_content[idx_open_bracket + 1:].strip()
                    if remainder:
                        remainder_code = remove_comments(remainder, comment_chars=comment_chars).rstrip()
                        if remainder_code.endswith("]"):
                            closing_index = remainder_code.rindex(']')
                            content_line = remainder_code[:closing_index].strip()
                            if content_line:
                                current_template_content.append(content_line)
                            content = '\n'.join(current_template_content)
                            template[current_template_key] = ScriptTemplate(
                                content=content,
                                autorefresh=False,
                                definitions=LOCALdefinitions,
                                verbose=verbose,
                                userid=current_template_key)
                            template[current_template_key].refreshvar(globaldefinitions=GLOBALdefinitions)
                            LOCALdefinitions = lambdaScriptdata()
                            inside_template_block = False
                            current_template_key = None
                            current_template_content = []
                            last_successful_line = line_number
                            continue
                        else:
                            current_template_content.append(remainder)
                    last_successful_line = line_number
                continue

            # Handle start of global parameters
            if stripped.startswith('{') and not inside_global_params:
                if '}' in stripped:
                    global_params_content = stripped
                    cls._parse_global_params(global_params_content.strip(), global_params)
                    global_params_content = ""
                    last_successful_line = line_number
                else:
                    inside_global_params = True
                    global_params_content = stripped
                continue

            # Handle global parameters inside {...}
            if inside_global_params:
                global_params_content += ' ' + stripped
                if '}' in stripped:
                    inside_global_params = False
                    cls._parse_global_params(global_params_content.strip(), global_params)
                    global_params_content = ""
                    last_successful_line = line_number
                continue

            # Handle attributes
            attribute_match = re.match(r'^(\w+)\s*:\s*\{(.+)\}', stripped)
            if attribute_match:
                key, attr_content = attribute_match.groups()
                attributes[key] = {}
                cls._parse_attributes(attributes[key], attr_content.strip())
                last_successful_line = line_number
                continue

            # Handle definitions
            definition_match = re.match(r'^(\w+)\s*=\s*(.+)', stripped)
            if definition_match:
                key, value = definition_match.groups()
                convertedvalue = cls._convert_value(value)
                if key in GLOBALdefinitions:
                    if (GLOBALdefinitions.getattr(key) != convertedvalue) or \
                        (getattr(current_var_value, key) != convertedvalue):
                        LOCALdefinitions.setattr(key, convertedvalue)
                else:
                    GLOBALdefinitions.setattr(key, convertedvalue)
                last_successful_line = line_number
                setattr(current_var_value, key, convertedvalue)
                continue

            # Handle single-line templates
            template_match = re.match(r'^(\w+)\s*:\s*(.+)', stripped)
            if template_match:
                key, content = template_match.groups()
                template[key] = ScriptTemplate(
                    content = content.strip(),
                    autorefresh = False,
                    definitions=LOCALdefinitions,
                    verbose=verbose,
                    userid=key)
                template[key].refreshvar(globaldefinitions=GLOBALdefinitions)
                LOCALdefinitions = lambdaScriptdata()
                last_successful_line = line_number
                continue

            # Unrecognized line
            if verbose:
                print(f"Warning: Unrecognized line at {line_number}: {line_content}")
            last_successful_line = line_number
            continue

        # At the end, check if any template block was left unclosed
        if inside_template_block:
            # Collect error context
            context_start = max(0, last_successful_line - 3)
            context_end = min(len(lines), last_successful_line + 3)
            error_context_lines = lines[context_start:context_end]
            error_context = ""
            for i, error_line in enumerate(error_context_lines):
                line_num = context_start + i
                indicator = ">" if line_num == last_successful_line else " "
                error_context += f"{indicator} {line_num}: {error_line}\n"

            raise ValueError(
                f"Template block '{current_template_key}' starting at line {last_successful_line} was not properly closed.\n\n"
                f"Error context:\n{error_context}"
            )

        # Apply attributes to templates
        for key in attributes:
            if key in template:
                for attr_name, attr_value in attributes[key].items():
                    setattr(template[key], attr_name, attr_value)
                template[key]._autorefresh = True # restore the default behavior for the end-user
            else:
                raise ValueError(f"Attributes found for undefined template key: {key}")

        # Create and return new instance
        if name is None:
            name = autoname(8)
        instance = cls(
            name=name,
            SECTIONS=global_params.get('SECTIONS', ['DYNAMIC']),
            section=global_params.get('section', 0),
            position=global_params.get('position', 0),
            role=global_params.get('role', 'dscript instance'),
            description=global_params.get('description', 'dynamic script'),
            userid=global_params.get('userid', 'dscript'),
            version=global_params.get('version', 0.1),
            verbose=global_params.get('verbose', False)
        )

        # Convert numeric string keys to integers if numerickeys is True
        if numerickeys:
            numeric_template = {}
            for key, value in template.items():
                if key.isdigit():
                    numeric_template[int(key)] = value
                else:
                    numeric_template[key] = value
            template = numeric_template

        # Set definitions and template
        instance.DEFINITIONS = GLOBALdefinitions
        instance.TEMPLATE = template

        # Refresh variables
        instance.set_all_variables()

        # Check variables
        instance.check_all_variables(verbose=False)

        return instance




    @classmethod
    def parsesyntax_legacy(cls, content, name=None, numerickeys=True):
        """
        Parse a script from a string content.
        [ ------------------------------------------------------]
        [ Legacy parsesyntax method for backward compatibility. ]
        [ ------------------------------------------------------]

        Parameters
        ----------
        content : str
            The string content of the script to be parsed.

        name : str
            The name of the dscript project (if None, it is set randomly)

        numerickeys : bool, default=True
            If True, numeric string keys in the template section are automatically converted into integers.

        Returns
        -------
        dscript
            A new `dscript` instance populated with the content of the loaded file.

        Raises
        ------
        ValueError
            If content does not start with the correct DSCRIPT header or the file format is invalid.

        Notes
        -----
        - The file is expected to follow the same structured format as the one produced by the `save()` method.
        - The method processes global parameters, definitions, template lines/items, and attributes. If the file
          includes numeric keys as strings (e.g., "0", "1"), they can be automatically converted into integers
          if `numerickeys=True`.
        - The script structure is dynamically rebuilt, and each section (global parameters, definitions,
          template, and attributes) is correctly parsed and assigned to the corresponding parts of the `dscript`
          instance.


        PIZZA.DSCRIPT SAVE FILE FORMAT
        -------------------------------
        This script syntax is designed for creating dynamic and customizable input files, where variables, templates,
        and control attributes can be defined in a flexible manner.

        ### Mandatory First Line:
        Every DSCRIPT file must begin with the following line:
            # DSCRIPT SAVE FILE

        ### Structure Overview:

        1. **Global Parameters Section (Optional):**
            - This section defines global script settings, enclosed within curly braces `{ }`.
            - Properties include:
                - `SECTIONS`: List of section names to be considered (e.g., `["DYNAMIC"]`).
                - `section`: Current section index (e.g., `0`).
                - `position`: Current script position in the order.
                - `role`: Defines the role of the script instance (e.g., `"dscript instance"`).
                - `description`: A short description of the script (e.g., `"dynamic script"`).
                - `userid`: Identifier for the user (e.g., `"dscript"`).
                - `version`: Script version (e.g., `0.1`).
                - `verbose`: Verbosity flag, typically a boolean (e.g., `False`).

            Example:
            ```
            {
                SECTIONS = ['INITIALIZATION', 'SIMULATION']  # Global script parameters
            }
            ```

        2. **Definitions Section:**
            - Variables are defined in Python-like syntax, allowing for dynamic variable substitution.
            - Variables can be numbers, strings, or lists, and they can include placeholders using `$`
              to delay execution or substitution.

            Example:
            ```
            d = 3                               # Define a number
            periodic = "$p"                     # '$' prevents immediate evaluation of 'p'
            units = "$metal"                    # '$' prevents immediate evaluation of 'metal'
            dimension = "${d}"                  # Variable substitution
            boundary = ['p', 'p', 'p']  # List with a mix of variables and values
            atom_style = "$atomic"              # String variable with delayed evaluation
            ```

        3. **Templates Section:**
            - This section provides a mapping between keys and their corresponding commands or instructions.
            - The templates reference variables defined in the **Definitions** section or elsewhere.
            - Syntax:
                ```
                KEY: INSTRUCTION
                ```
                where:
                - `KEY` can be numeric or alphanumeric.
                - `INSTRUCTION` represents a command template, often referring to variables using `${variable}` notation.

            Example:
            ```
            units: units ${units}               # Template uses the 'units' variable
            dim: dimension ${dimension}         # Template for setting the dimension
            bound: boundary ${boundary}         # Template for boundary settings
            lattice: lattice ${lattice}         # Lattice template
            ```

        4. **Attributes Section:**
            - Each template line can have customizable attributes to control behavior and conditions.
            - Default attributes include:
                - `facultative`: If `True`, the line is optional and can be removed if needed.
                - `eval`: If `True`, the line will be evaluated with Python's `eval()` function.
                - `readonly`: If `True`, the line cannot be modified later in the script.
                - `condition`: An expression that must be satisfied for the line to be included.
                - `condeval`: If `True`, the condition will be evaluated using `eval()`.
                - `detectvar`: If `True`, this creates variables in the **Definitions** section if they do not exist.

            Example:
            ```
            units: {facultative=False, eval=False, readonly=False, condition="${units}", condeval=False, detectvar=True}
            dim: {facultative=False, eval=False, readonly=False, condition=None, condeval=False, detectvar=True}
            ```

        Note on multiple definitions
        -----------------------------
        This example demonstrates how variables defined in the `Definitions` section are handled for each template.
        Each template retains its own snapshot of the variable definitions at the time it is created, ensuring that templates
        can use different values for the same variable if redefined.

        content = "" "
        # DSCRIPT SAVE FILE

        # Definitions
        var = 10

        # Template ky1
        key1: Template content with ${var}

        # Definitions
        var = 20

        # Template key2
        key2: Template content with ${var}

        # Template key3
        key3:[
            this is an underfined variable ${var31}
            this is an another underfined variable ${var32}
            this variables is defined  ${var}
            ]

        "" "

        # Parse content using parsesyntax()
        ds = dscript.parsesyntax(content)

        # Key1 should use the first definition of 'var' (10)
        print(ds.key1.definitions.var)  # Output: Template content with 10

        # Key2 should use the updated definition of 'var' (20)
        print(ds.key2.definitions.var)  # Output: Template content with 10


        """

        # Split the content into lines
        lines = content.splitlines()
        lines = [line for line in lines if line.strip()]  # Remove blank or empty lines
        # Raise an error if no content is left after removing blank lines
        if not lines:
            raise ValueError("File/Content is empty or only contains blank lines.")

        # Initialize containers for global parameters, definitions, templates, and attributes
        global_params = {}
        definitions = lambdaScriptdata()
        template = {}
        attributes = {}

        # State variables to handle multi-line global parameters and attributes
        inside_global_params = False
        inside_attributes = False
        current_attr_key = None  # Ensure this is properly initialized
        global_params_content = ""
        inside_template_block = False  # Track if we are inside a multi-line template
        current_template_key = None    # Track the current template key
        current_template_content = []  # Store lines for the current template content

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

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

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

            # Remove trailing comments
            stripped = remove_comments(stripped)

            # Step 3: Handle global parameters inside {...}
            if stripped.startswith("{"):
                # Found the opening {, start accumulating global parameters
                inside_global_params = True
                # Remove the opening { and accumulate the remaining content
                global_params_content = stripped[stripped.index('{') + 1:].strip()

                # Check if the closing } is also on the same line
                if '}' in global_params_content:
                    global_params_content = global_params_content[:global_params_content.index('}')].strip()
                    inside_global_params = False  # We found the closing } on the same line
                    # Now parse the global parameters block
                    cls._parse_global_params(global_params_content.strip(), global_params)
                    global_params_content = ""  # Reset for the next block
                continue

            if inside_global_params:
                # Accumulate content until the closing } is found
                if stripped.endswith("}"):
                    # Found the closing }, accumulate and process the entire block
                    global_params_content += " " + stripped[:stripped.index('}')].strip()
                    inside_global_params = False  # Finished reading global parameters block

                    # Now parse the entire global parameters block
                    cls._parse_global_params(global_params_content.strip(), global_params)
                    global_params_content = ""  # Reset for the next block if necessary
                else:
                    # Continue accumulating if } is not found
                    global_params_content += " " + stripped
                continue

            # Step 4: Detect the start of a multi-line template block inside [...]
            if not inside_template_block:
                template_match = re.match(r'(\w+)\s*:\s*\[', stripped)
                if template_match:
                    current_template_key = template_match.group(1)  # Capture the key
                    inside_template_block = True
                    current_template_content = []  # Reset content list
                    continue

            # If inside a template block, accumulate lines until we find the closing ]
            if inside_template_block:
                if stripped == "]":
                    # End of the template block, join the content and store it
                    template[current_template_key] = ScriptTemplate(
                        current_template_content,
                        definitions=lambdaScriptdata(**definitions),  # Clone current global definitions
                        verbose=True,
                        userid=current_template_key
                        )
                    template[current_template_key].refreshvar()
                    inside_template_block = False
                    current_template_key = None
                    current_template_content = []
                else:
                    # Accumulate the current line (without surrounding spaces)
                    current_template_content.append(stripped)
                continue

            # Step 5: Handle attributes inside {...}
            if inside_attributes and stripped.endswith("}"):
                # Finish processing attributes for the current key
                cls._parse_attributes(attributes[current_attr_key], stripped[:-1])  # Remove trailing }
                inside_attributes = False
                current_attr_key = None
                continue

            if inside_attributes:
                # Continue accumulating attributes
                cls._parse_attributes(attributes[current_attr_key], stripped)
                continue

            # Step 6: Determine if the line is a definition, template, or attribute
            definition_match = re.match(r'(\w+)\s*=\s*(.+)', stripped)
            template_match = re.match(r'(\w+)\s*:\s*(?!\s*\{.*\}\s*$)(.+)', stripped) # template_match = re.match(r'(\w+)\s*:\s*(?!\{)(.+)', stripped)
            attribute_match = re.match(r'(\w+)\s*:\s*\{\s*(.+)\s*\}', stripped)       # attribute_match = re.match(r'(\w+)\s*:\s*\{(.+)\}', stripped)

            if definition_match:
                # Line is a definition (key=value)
                key, value = definition_match.groups()
                definitions.setattr(key,cls._convert_value(value))

            elif template_match and not inside_template_block:
                # Line is a template (key: content)
                key, content = template_match.groups()
                template[key] = ScriptTemplate(
                    content,
                    definitions=lambdaScriptdata(**definitions),  # Clone current definitions
                    verbose=True,
                    userid=current_template_key)
                template[key].refreshvar()

            elif attribute_match:
                # Line is an attribute (key:{attributes...})
                current_attr_key, attr_content = attribute_match.groups()
                attributes[current_attr_key] = {}
                cls._parse_attributes(attributes[current_attr_key], attr_content)
                inside_attributes = not stripped.endswith("}")

        # Step 7: Validation and Reconstruction
        # Make sure there are no attributes without a template entry
        for key in attributes:
            if key not in template:
                raise ValueError(f"Attributes found for undefined template key: {key}")
            # Apply attributes to the corresponding template object
            for attr_name, attr_value in attributes[key].items():
                setattr(template[key], attr_name, attr_value)

        # Step 7: Create and return a new dscript instance
        if name is None:
            name = autoname(8)
        instance = cls(
            name = name,
            SECTIONS=global_params.get('SECTIONS', ['DYNAMIC']),
            section=global_params.get('section', 0),
            position=global_params.get('position', 0),
            role=global_params.get('role', 'dscript instance'),
            description=global_params.get('description', 'dynamic script'),
            userid=global_params.get('userid', 'dscript'),
            version=global_params.get('version', 0.1),
            verbose=global_params.get('verbose', False)
        )


        # Convert numeric string keys to integers if numerickeys is True
        if numerickeys:
            numeric_template = {}
            for key, value in template.items():
                # Check if the key is a numeric string
                if key.isdigit():
                    numeric_template[int(key)] = value
                else:
                    numeric_template[key] = value
            template = numeric_template

        # Set definitions and template
        instance.DEFINITIONS = definitions
        instance.TEMPLATE = template

        # Refresh variables (ensure that variables are detected and added to definitions)
        instance.set_all_variables()

        # Check eval
        instance.check_all_variables(verbose=False)

        # return the new instance
        return instance


    @classmethod
    def _parse_global_params(cls, content, global_params):
        """
        Parses global parameters from the accumulated content enclosed in `{}`.

        ### Parameters:
            content (str): The content string containing global parameters.
            global_params (dict): A dictionary to populate with parsed parameters.

        ### Raises:
            ValueError: If invalid lines or key-value pairs are encountered.
        """
        # Remove braces from the content
        content = content.strip().strip("{}")

        # Split the content into lines by commas
        lines = re.split(r',(?![^(){}\[\]]*[\)\}\]])', content.strip())

        for line in lines:
            line = line.strip()
            # Match key-value pairs
            match = re.match(r'([\w_]+)\s*=\s*(.+)', line)
            if match:
                key, value = match.groups()
                key = key.strip()
                value = value.strip()
                # Convert the value to the appropriate Python type and store it
                global_params[key] = cls._convert_value(value)
            else:
                raise ValueError(f"Invalid parameter line: '{line}'")


    @classmethod
    def _parse_attributes(cls, attr_dict, content):
        """Parses attributes from the content inside {attribute=value,...}."""
        attr_pairs = re.findall(r'(\w+)\s*=\s*([^,]+)', content)
        for attr_name, attr_value in attr_pairs:
            attr_dict[attr_name] = cls._convert_value(attr_value)

    @classmethod
    def _convert_value(cls, value):
        """Converts a string representation of a value to the appropriate Python type."""
        value = value.strip()
        # Boolean and None conversion
        if value.lower() == 'true':
            return True
        elif value.lower() == 'false':
            return False
        elif value.lower() == 'none':
            return None
        # Handle quoted strings
        if (value.startswith('"') and value.endswith('"')) or (value.startswith("'") and value.endswith("'")):
            return value[1:-1]
        # Handle lists (Python syntax inside the file)
        if value.startswith('[') and value.endswith(']'):
            return eval(value)  # Using eval to parse lists safely in this controlled scenario
        # Handle numbers
        try:
            if '.' in value:
                return float(value)
            return int(value)
        except ValueError:
            # Return the value as-is if it doesn't match other types
            return value

    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, copy.deepcopy(v, memo))
        return copie

    def detect_all_variables(self):
        """
        Detects all variables across all templates in the dscript object.

        This method iterates through all ScriptTemplate objects in the dscript and
        collects variables from each template using the detect_variables method.

        Returns:
        --------
        list
            A sorted list of unique variables detected in all templates.
        """
        all_variables = set()  # Use a set to avoid duplicates
        # Iterate through all templates in the dscript object
        for template_key, template in self.TEMPLATE.items():
            # Ensure the template is a ScriptTemplate and has the detect_variables method
            if isinstance(template, ScriptTemplate):
                detected_vars = template.detect_variables()
                all_variables.update(detected_vars)  # Add the detected variables to the set
        return sorted(all_variables)  # Return a sorted list of unique variables

    def add_dynamic_script(self, key, content="", userid=None, definitions=None, verbose=None, **USER):
        """
        Add a dynamic script step to the dscript object.

        Parameters:
        -----------
        key : str
            The key for the dynamic script (usually an index or step identifier).
        content : str or list of str, optional
            The content (template) of the script step.
        definitions : lambdaScriptdata, optional
            The merged variable space (STATIC + GLOBAL + LOCAL).
        verbose : bool, optional
            If None, self.verbose will be used. Controls verbosity of the template.
        USER : dict
            Additional user variables that override the definitions for this step.
        """
        if definitions is None:
            definitions = lambdaScriptdata()
        if verbose is None:
            verbose = self.verbose
        # Create a new ScriptTemplate and add it to the TEMPLATE
        self.TEMPLATE[key] = ScriptTemplate(
            content=content,
            definitions=self.DEFINITIONS+definitions,
            verbose=verbose,
            userid = key if userid is None else userid,
            **USER
        )



    def check_all_variables(self, verbose=True, seteval=True, output=False):
        """
        Checks for undefined variables for each TEMPLATE key in the dscript object.

        Parameters:
        -----------
        verbose : bool, optional, default=True
            If True, prints information about variables for each TEMPLATE key.
            Shows [-] if the variable is set to its default value, [+] if it is defined, and [ ] if it is undefined.

        seteval : bool, optional, default=True
            If True, sets the `eval` attribute to True if at least one variable is defined or set to its default value.

        output : bool, optional, default=False
            If True, returns a dictionary with lists of default variables, set variables, and undefined variables.

        Returns:
        --------
        out : dict, optional
            If `output=True`, returns a dictionary with the following structure:
            - "defaultvalues": List of variables set to their default value (${varname}).
            - "setvalues": List of variables defined with values other than their default.
            - "undefined": List of variables that are undefined.
        """
        out = {"defaultvalues": [], "setvalues": [], "undefined": []}

        for key in self.TEMPLATE:
            template = self.TEMPLATE[key]
            # Call the check_variables method of ScriptTemplate for each TEMPLATE key
            result = template.check_variables(verbose=verbose, seteval=seteval)

            # Update the output dictionary if needed
            out["defaultvalues"].extend(result["defaultvalues"])
            out["setvalues"].extend(result["setvalues"])
            out["undefined"].extend(result["undefined"])

        if output:
            return out


    def set_all_variables(self):
        """
        Ensures that all variables in the templates are added to the global definitions
        with default values if they are not already defined.
        """
        for key, script_template in self.TEMPLATE.items():
            # Check and update the global definitions with template-specific variables
            for var in script_template.detect_variables():
                if var not in self.DEFINITIONS:
                    # Add undefined variables with their default value
                    self.DEFINITIONS.setattr(var, f"${{{var}}}")  # Set default as ${varname}

Class variables

var construction_attributes

Static methods

def header(name=None, verbose=True, verbosity=None, style=2, filepath=None, version=None, license=None, email=None)

Generate a formatted header for the DSCRIPT file.

Parameters:

name (str, optional): The name of the script. If None, "Unnamed" is used.
verbose (bool, optional): Whether to include the header. Default is True.
verbosity (int, optional): Verbosity level. Overrides <code>verbose</code> if specified.
style (int, optional): ASCII style for the header (default=2).
filepath (str, optional): Full path to the file being saved. If None, the line mentioning the file path is excluded.
version (str, optional): DSCRIPT version. If None, it is omitted from the header.
license (str, optional): License type. If None, it is omitted from the header.
email (str, optional): Contact email. If None, it is omitted from the header.

Returns:

str: A formatted string representing the script's metadata and initialization details.
     Returns an empty string if <code>verbose</code> is False.

The header includes:

- DSCRIPT version, license, and contact email, if provided.
- The name of the script.
- Filepath, if provided.
- Information on where and when the script was generated.

Notes:

- If <code>verbosity</code> is specified, it overrides <code>verbose</code>.
- Omits metadata lines if <code>version</code>, <code>license</code>, or <code>email</code> are not provided.
Expand source code
@staticmethod
def header(name=None, verbose=True, verbosity=None, style=2, filepath=None, version=None, license=None, email=None):
    """
    Generate a formatted header for the DSCRIPT file.

    ### Parameters:
        name (str, optional): The name of the script. If None, "Unnamed" is used.
        verbose (bool, optional): Whether to include the header. Default is True.
        verbosity (int, optional): Verbosity level. Overrides `verbose` if specified.
        style (int, optional): ASCII style for the header (default=2).
        filepath (str, optional): Full path to the file being saved. If None, the line mentioning the file path is excluded.
        version (str, optional): DSCRIPT version. If None, it is omitted from the header.
        license (str, optional): License type. If None, it is omitted from the header.
        email (str, optional): Contact email. If None, it is omitted from the header.

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

    ### The header includes:
        - DSCRIPT version, license, and contact email, if provided.
        - The name of the script.
        - Filepath, if provided.
        - Information on where and when the script was generated.

    ### Notes:
        - If `verbosity` is specified, it overrides `verbose`.
        - Omits metadata lines if `version`, `license`, or `email` are not provided.
    """
    # Resolve verbosity
    verbose = verbosity > 0 if verbosity is not None else verbose
    if not verbose:
        return ""
    # Validate inputs
    if name is None:
        name = "Unnamed"
    # Prepare metadata line
    metadata = []
    if version:
        metadata.append(f"v{version}")
    if license:
        metadata.append(f"License: {license}")
    if email:
        metadata.append(f"Email: {email}")
    metadata_line = " | ".join(metadata)
    # Prepare the framed header content
    lines = []
    if metadata_line:
        lines.append(f"PIZZA.DSCRIPT FILE {metadata_line}")
    lines += [
        "",
        f"Name: {name}",
    ]
    # Add the filepath line if filepath is not None
    if filepath:
        lines.append(f"Path: {filepath}")
    lines += [
        "",
        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 frame_header function to format the framed content
    return frame_header(lines, style=style)
def load(filename, foldername=None, numerickeys=True)

Load a script instance from a text file.

Parameters

filename : str
The name of the file to load the script from. If the filename does not end with ".txt", the extension is automatically appended.
foldername : str, optional
The directory where the file is located. If not provided, it defaults to the system's temporary directory. If the filename does not include a full path, this folder will be used.
numerickeys : bool, default=True
If True, numeric string keys in the template section are automatically converted into integers. For example, the key "0" would be converted into the integer 0.

Returns

dscript
A new dscript instance populated with the content of the loaded file.

Raises

ValueError
If the file does not start with the correct DSCRIPT header or the file format is invalid.
FileNotFoundError
If the specified file does not exist.

Notes

  • The file is expected to follow the same structured format as the one produced by the save() method.
  • The method processes global parameters, definitions, template lines/items, and attributes. If the file includes numeric keys as strings (e.g., "0", "1"), they can be automatically converted into integers if numerickeys=True.
  • The script structure is dynamically rebuilt, and each section (global parameters, definitions, template, and attributes) is correctly parsed and assigned to the corresponding parts of the dscript instance.
Expand source code
@classmethod
def load(cls, filename, foldername=None, numerickeys=True):
    """
    Load a script instance from a text file.

    Parameters
    ----------
    filename : str
        The name of the file to load the script from. If the filename does not end with ".txt",
        the extension is automatically appended.

    foldername : str, optional
        The directory where the file is located. If not provided, it defaults to the system's
        temporary directory. If the filename does not include a full path, this folder will be used.

    numerickeys : bool, default=True
        If True, numeric string keys in the template section are automatically converted into integers.
        For example, the key "0" would be converted into the integer 0.

    Returns
    -------
    dscript
        A new `dscript` instance populated with the content of the loaded file.

    Raises
    ------
    ValueError
        If the file does not start with the correct DSCRIPT header or the file format is invalid.

    FileNotFoundError
        If the specified file does not exist.

    Notes
    -----
    - The file is expected to follow the same structured format as the one produced by the `save()` method.
    - The method processes global parameters, definitions, template lines/items, and attributes. If the file
      includes numeric keys as strings (e.g., "0", "1"), they can be automatically converted into integers
      if `numerickeys=True`.
    - The script structure is dynamically rebuilt, and each section (global parameters, definitions,
      template, and attributes) is correctly parsed and assigned to the corresponding parts of the `dscript`
      instance.
    """

    # Step 0 validate filepath
    if not filename.endswith('.txt'):
        filename += '.txt'

    # Handle foldername and relative paths
    if foldername is None or foldername == "":
        # If the foldername is empty or None, use current working directory for relative paths
        if not os.path.isabs(filename):
            filepath = os.path.join(os.getcwd(), filename)
        else:
            filepath = filename  # If filename is absolute, use it directly
    else:
        # If foldername is provided and filename is not absolute, use foldername
        if not os.path.isabs(filename):
            filepath = os.path.join(foldername, filename)
        else:
            filepath = filename

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

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

    # Call parsesyntax to parse the file content
    fname = os.path.basename(filepath)  # Extracts the filename (e.g., "myscript.txt")
    name, _ = os.path.splitext(fname)   # Removes the extension, e.g., "myscript
    return cls.parsesyntax(content, name, numerickeys)
def parsesyntax(content, name=None, numerickeys=True, verbose=False, authentification=True, comment_chars='#%', continuation_marker='...')

Parse a DSCRIPT script from a string content.

Parameters
----------
content : str
    The string content of the DSCRIPT script to be parsed.

name : str, optional
    The name of the dscript project. If <code>None</code>, a random name is generated.

numerickeys : bool, default=True
    If <code>True</code>, numeric string keys in the template section are automatically converted into integers.

verbose : bool, default=False
    If <code>True</code>, the parser will output warnings for unrecognized lines outside of blocks.

authentification : bool, default=True
    If <code>True</code>, the parser is expected that the first non empty line is # DSCRIPT SAVE FILE

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, optional (default: "...")
    A string containing characters to indicate line continuation
    Any characters after the continuation marker are considered comment and are theorefore ignored



Returns
-------
dscript
    A new <code><a title="dscript.dscript" href="#dscript.dscript">dscript</a></code> instance populated with the content of the loaded file.

Raises
------
ValueError
    If content does not start with the correct DSCRIPT header or the file format is invalid.

Notes
-----
**DSCRIPT SAVE FILE FORMAT**

This script syntax is designed for creating dynamic and customizable input files, where variables, templates,
and control attributes can be defined in a flexible manner.

**Mandatory First Line:**

Every DSCRIPT file must begin with the following line:

```plaintext
# DSCRIPT SAVE FILE
```

**Structure Overview:**

1. **Global Parameters Section (Optional):**

    - This section defines global script settings, enclosed within curly braces `{}`.
    - Properties include:
        - <code>SECTIONS</code>: List of section names to be considered (e.g., `["DYNAMIC"]`).
        - <code>section</code>: Current section index (e.g., <code>0</code>).
        - <code>position</code>: Current script position in the order.
        - <code>role</code>: Defines the role of the script instance (e.g., `"dscript instance"`).
        - <code>description</code>: A short description of the script (e.g., `"dynamic script"`).
        - <code>userid</code>: Identifier for the user (e.g., `"dscript"`).
        - <code>version</code>: Script version (e.g., <code>0.1</code>).
        - <code>verbose</code>: Verbosity flag, typically a boolean (e.g., <code>False</code>).

    **Example:**

    ```plaintext
    {
        SECTIONS = ['INITIALIZATION', 'SIMULATION']  # Global script parameters
    }
    ```

2. **Definitions Section:**

    - Variables are defined in Python-like syntax, allowing for dynamic variable substitution.
    - Variables can be numbers, strings, or lists, and they can include placeholders using `$`
      to delay execution or substitution.

    **Example:**

    ```plaintext
    d = 3                               # Define a number
    periodic = "$p"                     # '$' prevents immediate evaluation of 'p'
    units = "$metal"                    # '$' prevents immediate evaluation of 'metal'
    dimension = "${d}"                  # Variable substitution
    boundary = ['p', 'p', 'p']          # List with a mix of variables and values
    atom_style = "$atomic"              # String variable with delayed evaluation
    ```

3. **Templates Section:**

    - This section provides a mapping between keys and their corresponding commands or instructions.
    - Each template can reference variables defined in the **Definitions** section or elsewhere, typically using the `${variable}` syntax.
    - **Syntax Variations**:

        Templates can be defined in several ways, including without blocks, with single- or multi-line blocks, and with ellipsis (<code>...</code>) as a line continuation marker.

        - **Single-line Template Without Block**:
            ```plaintext
            KEY: INSTRUCTION
            ```
            - <code>KEY</code> is the identifier for the template (numeric or alphanumeric).
            - <code>INSTRUCTION</code> is the command or template text, which may reference variables.

        - **Single-line Template With Block**:
            ```plaintext
            KEY: [INSTRUCTION]
            ```
            - Uses square brackets (<code>\[ ]</code>) around the <code>INSTRUCTION</code>, indicating that all instructions are part of the block.

        - **Multi-line Template With Block**:
            ```plaintext
            KEY: [
                INSTRUCTION1
                INSTRUCTION2
                ...
                ]
            ```
            - Begins with `KEY: [` and ends with a standalone <code>]</code> on a new line.
            - Instructions within the block can span multiple lines, and ellipses (<code>...</code>) at the end of a line are used to indicate that the line continues, ignoring any content following the ellipsis as comments.
            - Comments following ellipses are removed after parsing and do not become part of the block, preserving only the instructions.

        - **Multi-line Template With Continuation Marker (Ellipsis)**:
            - For templates with complex code containing square brackets (<code>\[ ]</code>), the ellipsis (<code>...</code>) can be used to prevent <code>]</code> from prematurely closing the block. The ellipsis will keep the line open across multiple lines, allowing brackets in the instructions.

            **Example:**
            ```plaintext
            example1: command ${value}       # Single-line template without block
            example2: [command ${value}]     # Single-line template with block

            # Multi-line template with block
            example3: [
                command1 ${var1}
                command2 ${var2} ...   # Line continues after ellipsis
                command3 ${var3} ...   # Additional instruction continues
                ]

            # Multi-line template with ellipsis (handling square brackets)
            example4: [
                A[0][1] ...            # Ellipsis allows [ ] within instructions
                B[2][3] ...            # Another instruction in the block
                ]
            ```

    - **Key Points**:
        - **Blocks** allow grouping of multiple instructions for a single key, enclosed in square brackets.
        - **Ellipsis (<code>...</code>)** at the end of a line keeps the line open, preventing premature closing by <code>]</code>, especially useful if the template code includes square brackets (<code>\[ ]</code>).
        - **Comments** placed after the ellipsis are removed after parsing and are not part of the final block content.

    This flexibility supports both simple and complex template structures, allowing instructions to be grouped logically while keeping code and comments distinct.


4. **Attributes Section:**

    - Each template line can have customizable attributes to control behavior and conditions.
    - Default attributes include:
        - <code>facultative</code>: If <code>True</code>, the line is optional and can be removed if needed.
        - <code>eval</code>: If <code>True</code>, the line will be evaluated with Python's <code>eval()</code> function.
        - <code>readonly</code>: If <code>True</code>, the line cannot be modified later in the script.
        - <code>condition</code>: An expression that must be satisfied for the line to be included.
        - <code>condeval</code>: If <code>True</code>, the condition will be evaluated using <code>eval()</code>.
        - <code>detectvar</code>: If <code>True</code>, this creates variables in the **Definitions** section if they do not exist.

    **Example:**

    ```plaintext
    units: {facultative=False, eval=False, readonly=False, condition="${units}", condeval=False, detectvar=True}
    dim: {facultative=False, eval=False, readonly=False, condition=None, condeval=False, detectvar=True}
    ```

**Note on Multiple Definitions**

This example demonstrates how variables defined in the **Definitions** section are handled for each template.
Each template retains its own snapshot of the variable definitions at the time it is created, ensuring that templates
can use different values for the same variable if redefined.

**Example:**

```plaintext
# DSCRIPT SAVE FILE

# Definitions
var = 10

# Template key1
key1: Template content with ${var}

# Definitions
var = 20

# Template key2
key2: Template content with ${var}

# Template key3
key3:[
    this is an undefined variable ${var31}
    this is another undefined variable ${var32}
    this variable is defined  ${var}
]
```

**Parsing and Usage:**

```python
# Parse content using parsesyntax()
ds = dscript.parsesyntax(content)

# Accessing templates and their variables
print(ds.TEMPLATE['key1'].text)  # Output: Template content with 10
print(ds.TEMPLATE['key2'].text)  # Output: Template content with 20
```

**Handling Undefined Variables:**

Variables like `${var31}` and `${var32}` in <code>key3</code> are undefined. The parser will handle them based on your substitution logic or raise an error if they are required.

**Important Notes:**

- The parser processes the script sequentially. Definitions must appear before the templates that use them.
- Templates capture the variable definitions at the time they are parsed. Redefining a variable affects only subsequent templates.
- Comments outside of blocks are allowed and ignored by the parser.
- Content within templates is treated as-is, allowing for any syntax required by the target system (e.g., LAMMPS commands).

Advanced Example

Here's a more advanced example demonstrating the use of global definitions, local definitions, templates, and how to parse and render the template content.

```python
content = '''
    # GLOBAL DEFINITIONS
    dumpfile = $dump.LAMMPS
    dumpdt = 50
    thermodt = 100
    runtime = 5000

    # LOCAL DEFINITIONS for step '0'
    dimension = 3
    units = $si
    boundary = ['f', 'f', 'f']
    atom_style = $smd
    atom_modify = ['map', 'array']
    comm_modify = ['vel', 'yes']
    neigh_modify = ['every', 10, 'delay', 0, 'check', 'yes']
    newton = $off
    name = $SimulationBox

    # This is a comment line outside of blocks
    # ------------------------------------------

    0: [    % --------------[ Initialization Header (helper) for "${name}" ]--------------
        # set a parameter to None or "" to remove the definition
        dimension    ${dimension}
        units        ${units}
        boundary     ${boundary}
        atom_style   ${atom_style}
        atom_modify  ${atom_modify}
        comm_modify  ${comm_modify}
        neigh_modify ${neigh_modify}
        newton       ${newton}
        # ------------------------------------------
     ]
'''
# Parse the content
ds = dscript.parsesyntax(content, verbose=True, authentification=False)

# Access and print the rendered template
print("Template 0 content:")
print(ds.TEMPLATE[0].do())
```

**Explanation:**

- **Global Definitions:** Define variables that are accessible throughout the script.
- **Local Definitions for Step '0':** Define variables specific to a particular step or template.
- **Template Block:** Identified by `0: [ ... ]`, it contains the content where variables will be substituted.
- **Comments:** Lines starting with `#` are comments and are ignored by the parser outside of template blocks.

**Expected Output:**

```
Template 0 content:
# --------------[ Initialization Header (helper) for "SimulationBox" ]--------------
# set a parameter to None or "" to remove the definition
dimension    3
units        si
boundary     ['f', 'f', 'f']
atom_style   smd
atom_modify  ['map', 'array']
comm_modify  ['vel', 'yes']
neigh_modify ['every', 10, 'delay', 0, 'check', 'yes']
newton       off
# ------------------------------------------
```

**Notes:**

- The <code>do()</code> method renders the template, substituting variables with their defined values.
- Variables like `${dimension}` are replaced with their corresponding values defined in the local or global definitions.
- The parser handles comments and blank lines appropriately, ensuring they don't interfere with the parsing logic.
Expand source code
@classmethod
def parsesyntax(cls, content, name=None, numerickeys=True, verbose=False, authentification=True,
                comment_chars="#%",continuation_marker="..."):
    """
    Parse a DSCRIPT script from a string content.

    Parameters
    ----------
    content : str
        The string content of the DSCRIPT script to be parsed.

    name : str, optional
        The name of the dscript project. If `None`, a random name is generated.

    numerickeys : bool, default=True
        If `True`, numeric string keys in the template section are automatically converted into integers.

    verbose : bool, default=False
        If `True`, the parser will output warnings for unrecognized lines outside of blocks.

    authentification : bool, default=True
        If `True`, the parser is expected that the first non empty line is # DSCRIPT SAVE FILE

    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, optional (default: "...")
        A string containing characters to indicate line continuation
        Any characters after the continuation marker are considered comment and are theorefore ignored



    Returns
    -------
    dscript
        A new `dscript` instance populated with the content of the loaded file.

    Raises
    ------
    ValueError
        If content does not start with the correct DSCRIPT header or the file format is invalid.

    Notes
    -----
    **DSCRIPT SAVE FILE FORMAT**

    This script syntax is designed for creating dynamic and customizable input files, where variables, templates,
    and control attributes can be defined in a flexible manner.

    **Mandatory First Line:**

    Every DSCRIPT file must begin with the following line:

    ```plaintext
    # DSCRIPT SAVE FILE
    ```

    **Structure Overview:**

    1. **Global Parameters Section (Optional):**

        - This section defines global script settings, enclosed within curly braces `{}`.
        - Properties include:
            - `SECTIONS`: List of section names to be considered (e.g., `["DYNAMIC"]`).
            - `section`: Current section index (e.g., `0`).
            - `position`: Current script position in the order.
            - `role`: Defines the role of the script instance (e.g., `"dscript instance"`).
            - `description`: A short description of the script (e.g., `"dynamic script"`).
            - `userid`: Identifier for the user (e.g., `"dscript"`).
            - `version`: Script version (e.g., `0.1`).
            - `verbose`: Verbosity flag, typically a boolean (e.g., `False`).

        **Example:**

        ```plaintext
        {
            SECTIONS = ['INITIALIZATION', 'SIMULATION']  # Global script parameters
        }
        ```

    2. **Definitions Section:**

        - Variables are defined in Python-like syntax, allowing for dynamic variable substitution.
        - Variables can be numbers, strings, or lists, and they can include placeholders using `$`
          to delay execution or substitution.

        **Example:**

        ```plaintext
        d = 3                               # Define a number
        periodic = "$p"                     # '$' prevents immediate evaluation of 'p'
        units = "$metal"                    # '$' prevents immediate evaluation of 'metal'
        dimension = "${d}"                  # Variable substitution
        boundary = ['p', 'p', 'p']          # List with a mix of variables and values
        atom_style = "$atomic"              # String variable with delayed evaluation
        ```

    3. **Templates Section:**

        - This section provides a mapping between keys and their corresponding commands or instructions.
        - Each template can reference variables defined in the **Definitions** section or elsewhere, typically using the `${variable}` syntax.
        - **Syntax Variations**:

            Templates can be defined in several ways, including without blocks, with single- or multi-line blocks, and with ellipsis (`...`) as a line continuation marker.

            - **Single-line Template Without Block**:
                ```plaintext
                KEY: INSTRUCTION
                ```
                - `KEY` is the identifier for the template (numeric or alphanumeric).
                - `INSTRUCTION` is the command or template text, which may reference variables.

            - **Single-line Template With Block**:
                ```plaintext
                KEY: [INSTRUCTION]
                ```
                - Uses square brackets (`[ ]`) around the `INSTRUCTION`, indicating that all instructions are part of the block.

            - **Multi-line Template With Block**:
                ```plaintext
                KEY: [
                    INSTRUCTION1
                    INSTRUCTION2
                    ...
                    ]
                ```
                - Begins with `KEY: [` and ends with a standalone `]` on a new line.
                - Instructions within the block can span multiple lines, and ellipses (`...`) at the end of a line are used to indicate that the line continues, ignoring any content following the ellipsis as comments.
                - Comments following ellipses are removed after parsing and do not become part of the block, preserving only the instructions.

            - **Multi-line Template With Continuation Marker (Ellipsis)**:
                - For templates with complex code containing square brackets (`[ ]`), the ellipsis (`...`) can be used to prevent `]` from prematurely closing the block. The ellipsis will keep the line open across multiple lines, allowing brackets in the instructions.

                **Example:**
                ```plaintext
                example1: command ${value}       # Single-line template without block
                example2: [command ${value}]     # Single-line template with block

                # Multi-line template with block
                example3: [
                    command1 ${var1}
                    command2 ${var2} ...   # Line continues after ellipsis
                    command3 ${var3} ...   # Additional instruction continues
                    ]

                # Multi-line template with ellipsis (handling square brackets)
                example4: [
                    A[0][1] ...            # Ellipsis allows [ ] within instructions
                    B[2][3] ...            # Another instruction in the block
                    ]
                ```

        - **Key Points**:
            - **Blocks** allow grouping of multiple instructions for a single key, enclosed in square brackets.
            - **Ellipsis (`...`)** at the end of a line keeps the line open, preventing premature closing by `]`, especially useful if the template code includes square brackets (`[ ]`).
            - **Comments** placed after the ellipsis are removed after parsing and are not part of the final block content.

        This flexibility supports both simple and complex template structures, allowing instructions to be grouped logically while keeping code and comments distinct.


    4. **Attributes Section:**

        - Each template line can have customizable attributes to control behavior and conditions.
        - Default attributes include:
            - `facultative`: If `True`, the line is optional and can be removed if needed.
            - `eval`: If `True`, the line will be evaluated with Python's `eval()` function.
            - `readonly`: If `True`, the line cannot be modified later in the script.
            - `condition`: An expression that must be satisfied for the line to be included.
            - `condeval`: If `True`, the condition will be evaluated using `eval()`.
            - `detectvar`: If `True`, this creates variables in the **Definitions** section if they do not exist.

        **Example:**

        ```plaintext
        units: {facultative=False, eval=False, readonly=False, condition="${units}", condeval=False, detectvar=True}
        dim: {facultative=False, eval=False, readonly=False, condition=None, condeval=False, detectvar=True}
        ```

    **Note on Multiple Definitions**

    This example demonstrates how variables defined in the **Definitions** section are handled for each template.
    Each template retains its own snapshot of the variable definitions at the time it is created, ensuring that templates
    can use different values for the same variable if redefined.

    **Example:**

    ```plaintext
    # DSCRIPT SAVE FILE

    # Definitions
    var = 10

    # Template key1
    key1: Template content with ${var}

    # Definitions
    var = 20

    # Template key2
    key2: Template content with ${var}

    # Template key3
    key3:[
        this is an undefined variable ${var31}
        this is another undefined variable ${var32}
        this variable is defined  ${var}
    ]
    ```

    **Parsing and Usage:**

    ```python
    # Parse content using parsesyntax()
    ds = dscript.parsesyntax(content)

    # Accessing templates and their variables
    print(ds.TEMPLATE['key1'].text)  # Output: Template content with 10
    print(ds.TEMPLATE['key2'].text)  # Output: Template content with 20
    ```

    **Handling Undefined Variables:**

    Variables like `${var31}` and `${var32}` in `key3` are undefined. The parser will handle them based on your substitution logic or raise an error if they are required.

    **Important Notes:**

    - The parser processes the script sequentially. Definitions must appear before the templates that use them.
    - Templates capture the variable definitions at the time they are parsed. Redefining a variable affects only subsequent templates.
    - Comments outside of blocks are allowed and ignored by the parser.
    - Content within templates is treated as-is, allowing for any syntax required by the target system (e.g., LAMMPS commands).


**Advanced Example**

    Here's a more advanced example demonstrating the use of global definitions, local definitions, templates, and how to parse and render the template content.

    ```python
    content = '''
        # GLOBAL DEFINITIONS
        dumpfile = $dump.LAMMPS
        dumpdt = 50
        thermodt = 100
        runtime = 5000

        # LOCAL DEFINITIONS for step '0'
        dimension = 3
        units = $si
        boundary = ['f', 'f', 'f']
        atom_style = $smd
        atom_modify = ['map', 'array']
        comm_modify = ['vel', 'yes']
        neigh_modify = ['every', 10, 'delay', 0, 'check', 'yes']
        newton = $off
        name = $SimulationBox

        # This is a comment line outside of blocks
        # ------------------------------------------

        0: [    % --------------[ Initialization Header (helper) for "${name}" ]--------------
            # set a parameter to None or "" to remove the definition
            dimension    ${dimension}
            units        ${units}
            boundary     ${boundary}
            atom_style   ${atom_style}
            atom_modify  ${atom_modify}
            comm_modify  ${comm_modify}
            neigh_modify ${neigh_modify}
            newton       ${newton}
            # ------------------------------------------
         ]
    '''
    # Parse the content
    ds = dscript.parsesyntax(content, verbose=True, authentification=False)

    # Access and print the rendered template
    print("Template 0 content:")
    print(ds.TEMPLATE[0].do())
    ```

    **Explanation:**

    - **Global Definitions:** Define variables that are accessible throughout the script.
    - **Local Definitions for Step '0':** Define variables specific to a particular step or template.
    - **Template Block:** Identified by `0: [ ... ]`, it contains the content where variables will be substituted.
    - **Comments:** Lines starting with `#` are comments and are ignored by the parser outside of template blocks.

    **Expected Output:**

    ```
    Template 0 content:
    # --------------[ Initialization Header (helper) for "SimulationBox" ]--------------
    # set a parameter to None or "" to remove the definition
    dimension    3
    units        si
    boundary     ['f', 'f', 'f']
    atom_style   smd
    atom_modify  ['map', 'array']
    comm_modify  ['vel', 'yes']
    neigh_modify ['every', 10, 'delay', 0, 'check', 'yes']
    newton       off
    # ------------------------------------------
    ```

    **Notes:**

    - The `do()` method renders the template, substituting variables with their defined values.
    - Variables like `${dimension}` are replaced with their corresponding values defined in the local or global definitions.
    - The parser handles comments and blank lines appropriately, ensuring they don't interfere with the parsing logic.


    """
    # Split the content into lines
    lines = content.splitlines()
    if not lines:
        raise ValueError("File/Content is empty or only contains blank lines.")

    # Initialize containers
    global_params = {}
    GLOBALdefinitions = lambdaScriptdata()
    LOCALdefinitions = lambdaScriptdata()
    template = {}
    attributes = {}

    # State variables
    inside_global_params = False
    global_params_content = ""
    inside_template_block = False
    current_template_key = None
    current_template_content = []
    current_var_value = lambdaScriptdata()

    # Initialize line number
    line_number = 0
    last_successful_line = 0

    # Step 1: Authenticate the file
    if authentification:
        auth_line_found = False
        max_header_lines = 10
        header_end_idx = -1
        for idx, line in enumerate(lines[:max_header_lines]):
            stripped_line = line.strip()
            if not stripped_line:
                continue
            if stripped_line.startswith("# DSCRIPT SAVE FILE"):
                auth_line_found = True
                header_end_idx = idx
                break
            elif stripped_line.startswith("#") or stripped_line.startswith("%"):
                continue
            else:
                raise ValueError(f"Unexpected content before authentication line (# DSCRIPT SAVE FILE) at line {idx + 1}:\n{line}")
        if not auth_line_found:
            raise ValueError("File/Content is not a valid DSCRIPT file.")

        # Remove header lines
        lines = lines[header_end_idx + 1:]
        line_number = header_end_idx + 1
        last_successful_line = line_number - 1
    else:
        line_number = 0
        last_successful_line = 0

    # Process each line
    for idx, line in enumerate(lines):
        line_number += 1
        line_content = line.rstrip('\n')

        # Determine if we're inside a template block
        if inside_template_block:
            # Extract the code with its eventual continuation_marker
            code_line = remove_comments(
                    line_content,
                    comment_chars=comment_chars,
                    continuation_marker=continuation_marker,
                    remove_continuation_marker=False,
                    ).rstrip()

            # Check if line should continue
            if code_line.endswith(continuation_marker):
                # Append line up to the continuation marker
                endofline_index = line_content.rindex(continuation_marker)
                trimmed_content = line_content[:endofline_index].rstrip()
                if trimmed_content:
                    current_template_content.append(trimmed_content)
                continue
            elif code_line.endswith("]"):  # End of multi-line block
                closing_index = code_line.rindex(']')
                trimmed_content = code_line[:closing_index].rstrip()

                # Append any valid content before `]`, if non-empty
                if trimmed_content:
                    current_template_content.append(trimmed_content)

                # End of template block
                content = '\n'.join(current_template_content)
                template[current_template_key] = ScriptTemplate(
                    content=content,
                    autorefresh=False,
                    definitions=LOCALdefinitions,
                    verbose=verbose,
                    userid=current_template_key)
                # Refresh variables definitions
                template[current_template_key].refreshvar(globaldefinitions=GLOBALdefinitions)
                LOCALdefinitions = lambdaScriptdata()
                # Reset state for next block
                inside_template_block = False
                current_template_key = None
                current_template_content = []
                last_successful_line = line_number
                continue
            else:
                # Append the entire original line content if not ending with `...` or `]`
                current_template_content.append(line_content)
            continue

        # Not inside a template block
        stripped_no_comments = remove_comments(line_content)

        # Ignore empty lines after removing comments
        if not stripped_no_comments.strip():
            continue

        # If the original line is a comment line, skip it
        if line_content.strip().startswith("#") or line_content.strip().startswith("%"):
            continue

        stripped = stripped_no_comments.strip()

        # Handle start of a new template block
        template_block_match = re.match(r'^(\w+)\s*:\s*\[', stripped)
        if template_block_match:
            current_template_key = template_block_match.group(1)
            if inside_template_block:
                # Collect error context
                context_start = max(0, last_successful_line - 3)
                context_end = min(len(lines), line_number + 2)
                error_context_lines = lines[context_start:context_end]
                error_context = ""
                for i, error_line in enumerate(error_context_lines):
                    line_num = context_start + i + 1
                    indicator = ">" if line_num == line_number else "*" if line_num == last_successful_line else " "
                    error_context += f"{indicator} {line_num}: {error_line}\n"

                raise ValueError(
                    f"Template block '{current_template_key}' starting at line {last_successful_line} (*) was not properly closed before starting a new one at line {line_number} (>).\n\n"
                    f"Error context:\n{error_context}"
                )
            else:
                inside_template_block = True
                idx_open_bracket = line_content.index('[')
                remainder = line_content[idx_open_bracket + 1:].strip()
                if remainder:
                    remainder_code = remove_comments(remainder, comment_chars=comment_chars).rstrip()
                    if remainder_code.endswith("]"):
                        closing_index = remainder_code.rindex(']')
                        content_line = remainder_code[:closing_index].strip()
                        if content_line:
                            current_template_content.append(content_line)
                        content = '\n'.join(current_template_content)
                        template[current_template_key] = ScriptTemplate(
                            content=content,
                            autorefresh=False,
                            definitions=LOCALdefinitions,
                            verbose=verbose,
                            userid=current_template_key)
                        template[current_template_key].refreshvar(globaldefinitions=GLOBALdefinitions)
                        LOCALdefinitions = lambdaScriptdata()
                        inside_template_block = False
                        current_template_key = None
                        current_template_content = []
                        last_successful_line = line_number
                        continue
                    else:
                        current_template_content.append(remainder)
                last_successful_line = line_number
            continue

        # Handle start of global parameters
        if stripped.startswith('{') and not inside_global_params:
            if '}' in stripped:
                global_params_content = stripped
                cls._parse_global_params(global_params_content.strip(), global_params)
                global_params_content = ""
                last_successful_line = line_number
            else:
                inside_global_params = True
                global_params_content = stripped
            continue

        # Handle global parameters inside {...}
        if inside_global_params:
            global_params_content += ' ' + stripped
            if '}' in stripped:
                inside_global_params = False
                cls._parse_global_params(global_params_content.strip(), global_params)
                global_params_content = ""
                last_successful_line = line_number
            continue

        # Handle attributes
        attribute_match = re.match(r'^(\w+)\s*:\s*\{(.+)\}', stripped)
        if attribute_match:
            key, attr_content = attribute_match.groups()
            attributes[key] = {}
            cls._parse_attributes(attributes[key], attr_content.strip())
            last_successful_line = line_number
            continue

        # Handle definitions
        definition_match = re.match(r'^(\w+)\s*=\s*(.+)', stripped)
        if definition_match:
            key, value = definition_match.groups()
            convertedvalue = cls._convert_value(value)
            if key in GLOBALdefinitions:
                if (GLOBALdefinitions.getattr(key) != convertedvalue) or \
                    (getattr(current_var_value, key) != convertedvalue):
                    LOCALdefinitions.setattr(key, convertedvalue)
            else:
                GLOBALdefinitions.setattr(key, convertedvalue)
            last_successful_line = line_number
            setattr(current_var_value, key, convertedvalue)
            continue

        # Handle single-line templates
        template_match = re.match(r'^(\w+)\s*:\s*(.+)', stripped)
        if template_match:
            key, content = template_match.groups()
            template[key] = ScriptTemplate(
                content = content.strip(),
                autorefresh = False,
                definitions=LOCALdefinitions,
                verbose=verbose,
                userid=key)
            template[key].refreshvar(globaldefinitions=GLOBALdefinitions)
            LOCALdefinitions = lambdaScriptdata()
            last_successful_line = line_number
            continue

        # Unrecognized line
        if verbose:
            print(f"Warning: Unrecognized line at {line_number}: {line_content}")
        last_successful_line = line_number
        continue

    # At the end, check if any template block was left unclosed
    if inside_template_block:
        # Collect error context
        context_start = max(0, last_successful_line - 3)
        context_end = min(len(lines), last_successful_line + 3)
        error_context_lines = lines[context_start:context_end]
        error_context = ""
        for i, error_line in enumerate(error_context_lines):
            line_num = context_start + i
            indicator = ">" if line_num == last_successful_line else " "
            error_context += f"{indicator} {line_num}: {error_line}\n"

        raise ValueError(
            f"Template block '{current_template_key}' starting at line {last_successful_line} was not properly closed.\n\n"
            f"Error context:\n{error_context}"
        )

    # Apply attributes to templates
    for key in attributes:
        if key in template:
            for attr_name, attr_value in attributes[key].items():
                setattr(template[key], attr_name, attr_value)
            template[key]._autorefresh = True # restore the default behavior for the end-user
        else:
            raise ValueError(f"Attributes found for undefined template key: {key}")

    # Create and return new instance
    if name is None:
        name = autoname(8)
    instance = cls(
        name=name,
        SECTIONS=global_params.get('SECTIONS', ['DYNAMIC']),
        section=global_params.get('section', 0),
        position=global_params.get('position', 0),
        role=global_params.get('role', 'dscript instance'),
        description=global_params.get('description', 'dynamic script'),
        userid=global_params.get('userid', 'dscript'),
        version=global_params.get('version', 0.1),
        verbose=global_params.get('verbose', False)
    )

    # Convert numeric string keys to integers if numerickeys is True
    if numerickeys:
        numeric_template = {}
        for key, value in template.items():
            if key.isdigit():
                numeric_template[int(key)] = value
            else:
                numeric_template[key] = value
        template = numeric_template

    # Set definitions and template
    instance.DEFINITIONS = GLOBALdefinitions
    instance.TEMPLATE = template

    # Refresh variables
    instance.set_all_variables()

    # Check variables
    instance.check_all_variables(verbose=False)

    return instance
def parsesyntax_legacy(content, name=None, numerickeys=True)

Parse a script from a string content. [ ------------------------------------------------------] [ Legacy parsesyntax method for backward compatibility. ] [ ------------------------------------------------------]

Parameters

content : str
The string content of the script to be parsed.
name : str
The name of the dscript project (if None, it is set randomly)
numerickeys : bool, default=True
If True, numeric string keys in the template section are automatically converted into integers.

Returns

dscript
A new dscript instance populated with the content of the loaded file.

Raises

ValueError
If content does not start with the correct DSCRIPT header or the file format is invalid.

Notes

  • The file is expected to follow the same structured format as the one produced by the save() method.
  • The method processes global parameters, definitions, template lines/items, and attributes. If the file includes numeric keys as strings (e.g., "0", "1"), they can be automatically converted into integers if numerickeys=True.
  • The script structure is dynamically rebuilt, and each section (global parameters, definitions, template, and attributes) is correctly parsed and assigned to the corresponding parts of the dscript instance.

PIZZA.DSCRIPT SAVE FILE FORMAT

This script syntax is designed for creating dynamic and customizable input files, where variables, templates, and control attributes can be defined in a flexible manner.

Mandatory First Line:

Every DSCRIPT file must begin with the following line: # DSCRIPT SAVE FILE

Structure Overview:

  1. Global Parameters Section (Optional):

    • This section defines global script settings, enclosed within curly braces { }.
    • Properties include:
      • SECTIONS: List of section names to be considered (e.g., ["DYNAMIC"]).
      • section: Current section index (e.g., 0).
      • position: Current script position in the order.
      • role: Defines the role of the script instance (e.g., "dscript instance").
      • description: A short description of the script (e.g., "dynamic script").
      • userid: Identifier for the user (e.g., "dscript").
      • version: Script version (e.g., 0.1).
      • verbose: Verbosity flag, typically a boolean (e.g., False).

    Example: { SECTIONS = ['INITIALIZATION', 'SIMULATION'] # Global script parameters }

  2. Definitions Section:

    • Variables are defined in Python-like syntax, allowing for dynamic variable substitution.
    • Variables can be numbers, strings, or lists, and they can include placeholders using $ to delay execution or substitution.

    Example: d = 3 # Define a number periodic = "$p" # '$' prevents immediate evaluation of 'p' units = "$metal" # '$' prevents immediate evaluation of 'metal' dimension = "${d}" # Variable substitution boundary = ['p', 'p', 'p'] # List with a mix of variables and values atom_style = "$atomic" # String variable with delayed evaluation

  3. Templates Section:

    • This section provides a mapping between keys and their corresponding commands or instructions.
    • The templates reference variables defined in the Definitions section or elsewhere.
    • Syntax: KEY: INSTRUCTION where:
      • KEY can be numeric or alphanumeric.
      • INSTRUCTION represents a command template, often referring to variables using ${variable} notation.

    Example: units: units ${units} # Template uses the 'units' variable dim: dimension ${dimension} # Template for setting the dimension bound: boundary ${boundary} # Template for boundary settings lattice: lattice ${lattice} # Lattice template

  4. Attributes Section:

    • Each template line can have customizable attributes to control behavior and conditions.
    • Default attributes include:
      • facultative: If True, the line is optional and can be removed if needed.
      • eval: If True, the line will be evaluated with Python's eval() function.
      • readonly: If True, the line cannot be modified later in the script.
      • condition: An expression that must be satisfied for the line to be included.
      • condeval: If True, the condition will be evaluated using eval().
      • detectvar: If True, this creates variables in the Definitions section if they do not exist.

    Example: units: {facultative=False, eval=False, readonly=False, condition="${units}", condeval=False, detectvar=True} dim: {facultative=False, eval=False, readonly=False, condition=None, condeval=False, detectvar=True}

Note On Multiple Definitions

This example demonstrates how variables defined in the Definitions section are handled for each template. Each template retains its own snapshot of the variable definitions at the time it is created, ensuring that templates can use different values for the same variable if redefined.

content = "" "

DSCRIPT SAVE FILE

Definitions

var = 10

Template ky1

key1: Template content with ${var}

Definitions

var = 20

Template key2

key2: Template content with ${var}

Template key3

key3:[ this is an underfined variable ${var31} this is an another underfined variable ${var32} this variables is defined ${var} ]

"" "

Parse content using parsesyntax()

ds = dscript.parsesyntax(content)

Key1 should use the first definition of 'var' (10)

print(ds.key1.definitions.var) # Output: Template content with 10

Key2 should use the updated definition of 'var' (20)

print(ds.key2.definitions.var) # Output: Template content with 10

Expand source code
@classmethod
def parsesyntax_legacy(cls, content, name=None, numerickeys=True):
    """
    Parse a script from a string content.
    [ ------------------------------------------------------]
    [ Legacy parsesyntax method for backward compatibility. ]
    [ ------------------------------------------------------]

    Parameters
    ----------
    content : str
        The string content of the script to be parsed.

    name : str
        The name of the dscript project (if None, it is set randomly)

    numerickeys : bool, default=True
        If True, numeric string keys in the template section are automatically converted into integers.

    Returns
    -------
    dscript
        A new `dscript` instance populated with the content of the loaded file.

    Raises
    ------
    ValueError
        If content does not start with the correct DSCRIPT header or the file format is invalid.

    Notes
    -----
    - The file is expected to follow the same structured format as the one produced by the `save()` method.
    - The method processes global parameters, definitions, template lines/items, and attributes. If the file
      includes numeric keys as strings (e.g., "0", "1"), they can be automatically converted into integers
      if `numerickeys=True`.
    - The script structure is dynamically rebuilt, and each section (global parameters, definitions,
      template, and attributes) is correctly parsed and assigned to the corresponding parts of the `dscript`
      instance.


    PIZZA.DSCRIPT SAVE FILE FORMAT
    -------------------------------
    This script syntax is designed for creating dynamic and customizable input files, where variables, templates,
    and control attributes can be defined in a flexible manner.

    ### Mandatory First Line:
    Every DSCRIPT file must begin with the following line:
        # DSCRIPT SAVE FILE

    ### Structure Overview:

    1. **Global Parameters Section (Optional):**
        - This section defines global script settings, enclosed within curly braces `{ }`.
        - Properties include:
            - `SECTIONS`: List of section names to be considered (e.g., `["DYNAMIC"]`).
            - `section`: Current section index (e.g., `0`).
            - `position`: Current script position in the order.
            - `role`: Defines the role of the script instance (e.g., `"dscript instance"`).
            - `description`: A short description of the script (e.g., `"dynamic script"`).
            - `userid`: Identifier for the user (e.g., `"dscript"`).
            - `version`: Script version (e.g., `0.1`).
            - `verbose`: Verbosity flag, typically a boolean (e.g., `False`).

        Example:
        ```
        {
            SECTIONS = ['INITIALIZATION', 'SIMULATION']  # Global script parameters
        }
        ```

    2. **Definitions Section:**
        - Variables are defined in Python-like syntax, allowing for dynamic variable substitution.
        - Variables can be numbers, strings, or lists, and they can include placeholders using `$`
          to delay execution or substitution.

        Example:
        ```
        d = 3                               # Define a number
        periodic = "$p"                     # '$' prevents immediate evaluation of 'p'
        units = "$metal"                    # '$' prevents immediate evaluation of 'metal'
        dimension = "${d}"                  # Variable substitution
        boundary = ['p', 'p', 'p']  # List with a mix of variables and values
        atom_style = "$atomic"              # String variable with delayed evaluation
        ```

    3. **Templates Section:**
        - This section provides a mapping between keys and their corresponding commands or instructions.
        - The templates reference variables defined in the **Definitions** section or elsewhere.
        - Syntax:
            ```
            KEY: INSTRUCTION
            ```
            where:
            - `KEY` can be numeric or alphanumeric.
            - `INSTRUCTION` represents a command template, often referring to variables using `${variable}` notation.

        Example:
        ```
        units: units ${units}               # Template uses the 'units' variable
        dim: dimension ${dimension}         # Template for setting the dimension
        bound: boundary ${boundary}         # Template for boundary settings
        lattice: lattice ${lattice}         # Lattice template
        ```

    4. **Attributes Section:**
        - Each template line can have customizable attributes to control behavior and conditions.
        - Default attributes include:
            - `facultative`: If `True`, the line is optional and can be removed if needed.
            - `eval`: If `True`, the line will be evaluated with Python's `eval()` function.
            - `readonly`: If `True`, the line cannot be modified later in the script.
            - `condition`: An expression that must be satisfied for the line to be included.
            - `condeval`: If `True`, the condition will be evaluated using `eval()`.
            - `detectvar`: If `True`, this creates variables in the **Definitions** section if they do not exist.

        Example:
        ```
        units: {facultative=False, eval=False, readonly=False, condition="${units}", condeval=False, detectvar=True}
        dim: {facultative=False, eval=False, readonly=False, condition=None, condeval=False, detectvar=True}
        ```

    Note on multiple definitions
    -----------------------------
    This example demonstrates how variables defined in the `Definitions` section are handled for each template.
    Each template retains its own snapshot of the variable definitions at the time it is created, ensuring that templates
    can use different values for the same variable if redefined.

    content = "" "
    # DSCRIPT SAVE FILE

    # Definitions
    var = 10

    # Template ky1
    key1: Template content with ${var}

    # Definitions
    var = 20

    # Template key2
    key2: Template content with ${var}

    # Template key3
    key3:[
        this is an underfined variable ${var31}
        this is an another underfined variable ${var32}
        this variables is defined  ${var}
        ]

    "" "

    # Parse content using parsesyntax()
    ds = dscript.parsesyntax(content)

    # Key1 should use the first definition of 'var' (10)
    print(ds.key1.definitions.var)  # Output: Template content with 10

    # Key2 should use the updated definition of 'var' (20)
    print(ds.key2.definitions.var)  # Output: Template content with 10


    """

    # Split the content into lines
    lines = content.splitlines()
    lines = [line for line in lines if line.strip()]  # Remove blank or empty lines
    # Raise an error if no content is left after removing blank lines
    if not lines:
        raise ValueError("File/Content is empty or only contains blank lines.")

    # Initialize containers for global parameters, definitions, templates, and attributes
    global_params = {}
    definitions = lambdaScriptdata()
    template = {}
    attributes = {}

    # State variables to handle multi-line global parameters and attributes
    inside_global_params = False
    inside_attributes = False
    current_attr_key = None  # Ensure this is properly initialized
    global_params_content = ""
    inside_template_block = False  # Track if we are inside a multi-line template
    current_template_key = None    # Track the current template key
    current_template_content = []  # Store lines for the current template content

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

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

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

        # Remove trailing comments
        stripped = remove_comments(stripped)

        # Step 3: Handle global parameters inside {...}
        if stripped.startswith("{"):
            # Found the opening {, start accumulating global parameters
            inside_global_params = True
            # Remove the opening { and accumulate the remaining content
            global_params_content = stripped[stripped.index('{') + 1:].strip()

            # Check if the closing } is also on the same line
            if '}' in global_params_content:
                global_params_content = global_params_content[:global_params_content.index('}')].strip()
                inside_global_params = False  # We found the closing } on the same line
                # Now parse the global parameters block
                cls._parse_global_params(global_params_content.strip(), global_params)
                global_params_content = ""  # Reset for the next block
            continue

        if inside_global_params:
            # Accumulate content until the closing } is found
            if stripped.endswith("}"):
                # Found the closing }, accumulate and process the entire block
                global_params_content += " " + stripped[:stripped.index('}')].strip()
                inside_global_params = False  # Finished reading global parameters block

                # Now parse the entire global parameters block
                cls._parse_global_params(global_params_content.strip(), global_params)
                global_params_content = ""  # Reset for the next block if necessary
            else:
                # Continue accumulating if } is not found
                global_params_content += " " + stripped
            continue

        # Step 4: Detect the start of a multi-line template block inside [...]
        if not inside_template_block:
            template_match = re.match(r'(\w+)\s*:\s*\[', stripped)
            if template_match:
                current_template_key = template_match.group(1)  # Capture the key
                inside_template_block = True
                current_template_content = []  # Reset content list
                continue

        # If inside a template block, accumulate lines until we find the closing ]
        if inside_template_block:
            if stripped == "]":
                # End of the template block, join the content and store it
                template[current_template_key] = ScriptTemplate(
                    current_template_content,
                    definitions=lambdaScriptdata(**definitions),  # Clone current global definitions
                    verbose=True,
                    userid=current_template_key
                    )
                template[current_template_key].refreshvar()
                inside_template_block = False
                current_template_key = None
                current_template_content = []
            else:
                # Accumulate the current line (without surrounding spaces)
                current_template_content.append(stripped)
            continue

        # Step 5: Handle attributes inside {...}
        if inside_attributes and stripped.endswith("}"):
            # Finish processing attributes for the current key
            cls._parse_attributes(attributes[current_attr_key], stripped[:-1])  # Remove trailing }
            inside_attributes = False
            current_attr_key = None
            continue

        if inside_attributes:
            # Continue accumulating attributes
            cls._parse_attributes(attributes[current_attr_key], stripped)
            continue

        # Step 6: Determine if the line is a definition, template, or attribute
        definition_match = re.match(r'(\w+)\s*=\s*(.+)', stripped)
        template_match = re.match(r'(\w+)\s*:\s*(?!\s*\{.*\}\s*$)(.+)', stripped) # template_match = re.match(r'(\w+)\s*:\s*(?!\{)(.+)', stripped)
        attribute_match = re.match(r'(\w+)\s*:\s*\{\s*(.+)\s*\}', stripped)       # attribute_match = re.match(r'(\w+)\s*:\s*\{(.+)\}', stripped)

        if definition_match:
            # Line is a definition (key=value)
            key, value = definition_match.groups()
            definitions.setattr(key,cls._convert_value(value))

        elif template_match and not inside_template_block:
            # Line is a template (key: content)
            key, content = template_match.groups()
            template[key] = ScriptTemplate(
                content,
                definitions=lambdaScriptdata(**definitions),  # Clone current definitions
                verbose=True,
                userid=current_template_key)
            template[key].refreshvar()

        elif attribute_match:
            # Line is an attribute (key:{attributes...})
            current_attr_key, attr_content = attribute_match.groups()
            attributes[current_attr_key] = {}
            cls._parse_attributes(attributes[current_attr_key], attr_content)
            inside_attributes = not stripped.endswith("}")

    # Step 7: Validation and Reconstruction
    # Make sure there are no attributes without a template entry
    for key in attributes:
        if key not in template:
            raise ValueError(f"Attributes found for undefined template key: {key}")
        # Apply attributes to the corresponding template object
        for attr_name, attr_value in attributes[key].items():
            setattr(template[key], attr_name, attr_value)

    # Step 7: Create and return a new dscript instance
    if name is None:
        name = autoname(8)
    instance = cls(
        name = name,
        SECTIONS=global_params.get('SECTIONS', ['DYNAMIC']),
        section=global_params.get('section', 0),
        position=global_params.get('position', 0),
        role=global_params.get('role', 'dscript instance'),
        description=global_params.get('description', 'dynamic script'),
        userid=global_params.get('userid', 'dscript'),
        version=global_params.get('version', 0.1),
        verbose=global_params.get('verbose', False)
    )


    # Convert numeric string keys to integers if numerickeys is True
    if numerickeys:
        numeric_template = {}
        for key, value in template.items():
            # Check if the key is a numeric string
            if key.isdigit():
                numeric_template[int(key)] = value
            else:
                numeric_template[key] = value
        template = numeric_template

    # Set definitions and template
    instance.DEFINITIONS = definitions
    instance.TEMPLATE = template

    # Refresh variables (ensure that variables are detected and added to definitions)
    instance.set_all_variables()

    # Check eval
    instance.check_all_variables(verbose=False)

    # return the new instance
    return instance
def write(scriptcontent, filename=None, foldername=None, overwrite=False)

Writes the provided script content to a specified file in a given folder, with a header if necessary.

Parameters

scriptcontent : str
The content to be written to the file.
filename : str, optional
The name of the file. If not provided, a random name will be generated. The extension .txt will be appended if not already present.
foldername : str, optional
The folder where the file will be saved. If not provided, the current working directory is used.
overwrite : bool, optional
If False (default), raises a FileExistsError if the file already exists. If True, the file will be overwritten if it exists.

Returns

str
The full path to the written file.

Raises

FileExistsError
If the file already exists and overwrite is set to False.

Notes

  • A header is prepended to the content if it does not already exist, using the header method.
  • The header includes metadata such as the current date, username, hostname, and file details.
Expand source code
@staticmethod
@staticmethod
def write(scriptcontent, filename=None, foldername=None, overwrite=False):
    """
    Writes the provided script content to a specified file in a given folder, with a header if necessary.

    Parameters
    ----------
    scriptcontent : str
        The content to be written to the file.

    filename : str, optional
        The name of the file. If not provided, a random name will be generated.
        The extension `.txt` will be appended if not already present.

    foldername : str, optional
        The folder where the file will be saved. If not provided, the current working directory is used.

    overwrite : bool, optional
        If False (default), raises a `FileExistsError` if the file already exists. If True, the file will be overwritten if it exists.

    Returns
    -------
    str
        The full path to the written file.

    Raises
    ------
    FileExistsError
        If the file already exists and `overwrite` is set to False.

    Notes
    -----
    - A header is prepended to the content if it does not already exist, using the `header` method.
    - The header includes metadata such as the current date, username, hostname, and file details.
    """
    # Generate a random name if filename is not provided
    if filename is None:
        filename = autoname(8)  # Generates a random name of 8 letters

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

    # Handle foldername and relative paths
    if foldername is None or foldername == "":
        # If foldername is empty or None, use current working directory for relative paths
        if not os.path.isabs(filename):
            filepath = os.path.join(os.getcwd(), filename)
        else:
            filepath = filename  # If filename is absolute, use it directly
    else:
        # If foldername is provided and filename is not absolute, use foldername
        if not os.path.isabs(filename):
            filepath = os.path.join(foldername, filename)
        else:
            filepath = filename

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

    # Count total and non-empty lines in the content
    total_lines = len(scriptcontent.splitlines())
    non_empty_lines = sum(1 for line in scriptcontent.splitlines() if line.strip())

    # Prepare header if not already present
    if not scriptcontent.startswith("# DSCRIPT SAVE FILE"):
        fname = os.path.basename(filepath)  # Extracts the filename (e.g., "myscript.txt")
        name, _ = os.path.splitext(fname)   # Removes the extension, e.g., "myscript"
        metadata = get_metadata()           # retrieve all metadata (statically)
        header = dscript.header(name=name, verbosity=True,style=1,filepath=filepath,
                version = metadata["version"], license = metadata["license"], email = metadata["email"])
        # Add line count information to the header
        footer = frame_header(
            lines=[
                f"Total lines written: {total_lines}",
                f"Non-empty lines: {non_empty_lines}"
            ],
            style=1
        )
        scriptcontent = header + "\n" + scriptcontent + "\n" + footer

    # Write the content to the file
    with open(filepath, 'w') as file:
        file.write(scriptcontent)
    return filepath

Methods

def add_dynamic_script(self, key, content='', userid=None, definitions=None, verbose=None, **USER)

Add a dynamic script step to the dscript object.

Parameters:

key : str The key for the dynamic script (usually an index or step identifier). content : str or list of str, optional The content (template) of the script step. definitions : lambdaScriptdata, optional The merged variable space (STATIC + GLOBAL + LOCAL). verbose : bool, optional If None, self.verbose will be used. Controls verbosity of the template. USER : dict Additional user variables that override the definitions for this step.

Expand source code
def add_dynamic_script(self, key, content="", userid=None, definitions=None, verbose=None, **USER):
    """
    Add a dynamic script step to the dscript object.

    Parameters:
    -----------
    key : str
        The key for the dynamic script (usually an index or step identifier).
    content : str or list of str, optional
        The content (template) of the script step.
    definitions : lambdaScriptdata, optional
        The merged variable space (STATIC + GLOBAL + LOCAL).
    verbose : bool, optional
        If None, self.verbose will be used. Controls verbosity of the template.
    USER : dict
        Additional user variables that override the definitions for this step.
    """
    if definitions is None:
        definitions = lambdaScriptdata()
    if verbose is None:
        verbose = self.verbose
    # Create a new ScriptTemplate and add it to the TEMPLATE
    self.TEMPLATE[key] = ScriptTemplate(
        content=content,
        definitions=self.DEFINITIONS+definitions,
        verbose=verbose,
        userid = key if userid is None else userid,
        **USER
    )
def check_all_variables(self, verbose=True, seteval=True, output=False)

Checks for undefined variables for each TEMPLATE key in the dscript object.

Parameters:

verbose : bool, optional, default=True If True, prints information about variables for each TEMPLATE key. Shows [-] if the variable is set to its default value, [+] if it is defined, and [ ] if it is undefined.

seteval : bool, optional, default=True If True, sets the eval attribute to True if at least one variable is defined or set to its default value.

output : bool, optional, default=False If True, returns a dictionary with lists of default variables, set variables, and undefined variables.

Returns:

out : dict, optional If output=True, returns a dictionary with the following structure: - "defaultvalues": List of variables set to their default value (${varname}). - "setvalues": List of variables defined with values other than their default. - "undefined": List of variables that are undefined.

Expand source code
def check_all_variables(self, verbose=True, seteval=True, output=False):
    """
    Checks for undefined variables for each TEMPLATE key in the dscript object.

    Parameters:
    -----------
    verbose : bool, optional, default=True
        If True, prints information about variables for each TEMPLATE key.
        Shows [-] if the variable is set to its default value, [+] if it is defined, and [ ] if it is undefined.

    seteval : bool, optional, default=True
        If True, sets the `eval` attribute to True if at least one variable is defined or set to its default value.

    output : bool, optional, default=False
        If True, returns a dictionary with lists of default variables, set variables, and undefined variables.

    Returns:
    --------
    out : dict, optional
        If `output=True`, returns a dictionary with the following structure:
        - "defaultvalues": List of variables set to their default value (${varname}).
        - "setvalues": List of variables defined with values other than their default.
        - "undefined": List of variables that are undefined.
    """
    out = {"defaultvalues": [], "setvalues": [], "undefined": []}

    for key in self.TEMPLATE:
        template = self.TEMPLATE[key]
        # Call the check_variables method of ScriptTemplate for each TEMPLATE key
        result = template.check_variables(verbose=verbose, seteval=seteval)

        # Update the output dictionary if needed
        out["defaultvalues"].extend(result["defaultvalues"])
        out["setvalues"].extend(result["setvalues"])
        out["undefined"].extend(result["undefined"])

    if output:
        return out
def createEmptyVariables(self, vars)

Creates empty variables in DEFINITIONS if they don't already exist.

Parameters:

vars : str or list of str The variable name or list of variable names to be created in DEFINITIONS.

Expand source code
def createEmptyVariables(self, vars):
    """
    Creates empty variables in DEFINITIONS if they don't already exist.

    Parameters:
    -----------
    vars : str or list of str
        The variable name or list of variable names to be created in DEFINITIONS.
    """
    if isinstance(vars, str):
        vars = [vars]  # Convert single variable name to list for uniform processing
    for varname in vars:
        if varname not in self.DEFINITIONS:
            self.DEFINITIONS.setattr(varname,"${" + varname + "}")
def detect_all_variables(self)

Detects all variables across all templates in the dscript object.

This method iterates through all ScriptTemplate objects in the dscript and collects variables from each template using the detect_variables method.

Returns:

list A sorted list of unique variables detected in all templates.

Expand source code
def detect_all_variables(self):
    """
    Detects all variables across all templates in the dscript object.

    This method iterates through all ScriptTemplate objects in the dscript and
    collects variables from each template using the detect_variables method.

    Returns:
    --------
    list
        A sorted list of unique variables detected in all templates.
    """
    all_variables = set()  # Use a set to avoid duplicates
    # Iterate through all templates in the dscript object
    for template_key, template in self.TEMPLATE.items():
        # Ensure the template is a ScriptTemplate and has the detect_variables method
        if isinstance(template, ScriptTemplate):
            detected_vars = template.detect_variables()
            all_variables.update(detected_vars)  # Add the detected variables to the set
    return sorted(all_variables)  # Return a sorted list of unique variables
def do(self, printflag=None, verbose=None, softrun=False, return_definitions=False, comment_chars='#%', **USER)

Executes or previews all ScriptTemplate instances in TEMPLATE, concatenating their processed content. Allows for optional headers and footers based on verbosity settings, and offers a preliminary preview mode with softrun. Accumulates definitions across all templates if return_definitions=True.

Parameters

printflag : bool, optional
If True, enables print output during execution. Defaults to the instance's print flag if None.
verbose : bool, optional
If True, includes headers and footers in the output, providing additional detail. Defaults to the instance's verbosity setting if None.
softrun : bool, optional
If True, executes the script in a preliminary mode: - Bypasses full variable substitution for a preview of the content, useful for validating structure. - If False (default), performs full processing, including variable substitutions and evaluations.
return_definitions : bool, optional
If True, returns a tuple where the second element contains accumulated definitions from all templates. If False (default), returns only the concatenated 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.
**USER : keyword arguments
Allows for the provision of additional user-defined definitions, where each keyword represents a definition key and the associated value represents the definition's content. These definitions can override or supplement template-level definitions during execution.

Returns

str or tuple
  • If return_definitions=False, returns the concatenated output of all ScriptTemplate instances, with optional headers, footers, and execution summary based on verbosity.
  • If return_definitions=True, returns a tuple of (output, accumulated_definitions), where accumulated_definitions contains all definitions used across templates.

Notes

  • Each ScriptTemplate in TEMPLATE is processed individually using its own do() method.
  • The softrun mode provides a preliminary content preview without full variable substitution, helpful for inspecting the script structure or gathering local definitions.
  • When verbose is enabled, the method includes detailed headers, footers, and a summary of processed and ignored items, providing insight into the script's construction and variable usage.
  • Accumulated definitions from each ScriptTemplate are combined if return_definitions=True, which can be useful for tracking all variables and definitions applied across the templates.

Example

>>> dscript_instance = dscript(name="ExampleScript")
>>> dscript_instance.TEMPLATE[0] = ScriptTemplate(
...     content=["units ${units}", "boundary ${boundary}"],
...     definitions=lambdaScriptdata(units="lj", boundary="p p p"),
...     attributes={'eval': True}
... )
>>> dscript_instance.do(verbose=True, units="real")
# Output:
# --------------
# TEMPLATE "ExampleScript"
# --------------
units real
boundary p p p
# ---> Total items: 2 - Ignored items: 0
Expand source code
def do(self, printflag=None, verbose=None, softrun=False, return_definitions=False,comment_chars="#%", **USER):
    """
    Executes or previews all `ScriptTemplate` instances in `TEMPLATE`, concatenating their processed content.
    Allows for optional headers and footers based on verbosity settings, and offers a preliminary preview mode with `softrun`.
    Accumulates definitions across all templates if `return_definitions=True`.

    Parameters
    ----------
    printflag : bool, optional
        If `True`, enables print output during execution. Defaults to the instance's print flag if `None`.
    verbose : bool, optional
        If `True`, includes headers and footers in the output, providing additional detail.
        Defaults to the instance's verbosity setting if `None`.
    softrun : bool, optional
        If `True`, executes the script in a preliminary mode:
        - Bypasses full variable substitution for a preview of the content, useful for validating structure.
        - If `False` (default), performs full processing, including variable substitutions and evaluations.
    return_definitions : bool, optional
        If `True`, returns a tuple where the second element contains accumulated definitions from all templates.
        If `False` (default), returns only the concatenated 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.
    **USER : keyword arguments
        Allows for the provision of additional user-defined definitions, where each keyword represents a
        definition key and the associated value represents the definition's content. These definitions
        can override or supplement template-level definitions during execution.

    Returns
    -------
    str or tuple
        - If `return_definitions=False`, returns the concatenated output of all `ScriptTemplate` instances,
          with optional headers, footers, and execution summary based on verbosity.
        - If `return_definitions=True`, returns a tuple of (`output`, `accumulated_definitions`), where
          `accumulated_definitions` contains all definitions used across templates.

    Notes
    -----
    - Each `ScriptTemplate` in `TEMPLATE` is processed individually using its own `do()` method.
    - The `softrun` mode provides a preliminary content preview without full variable substitution,
      helpful for inspecting the script structure or gathering local definitions.
    - When `verbose` is enabled, the method includes detailed headers, footers, and a summary of processed and
      ignored items, providing insight into the script's construction and variable usage.
    - Accumulated definitions from each `ScriptTemplate` are combined if `return_definitions=True`, which can be
      useful for tracking all variables and definitions applied across the templates.

    Example
    -------
    >>> dscript_instance = dscript(name="ExampleScript")
    >>> dscript_instance.TEMPLATE[0] = ScriptTemplate(
    ...     content=["units ${units}", "boundary ${boundary}"],
    ...     definitions=lambdaScriptdata(units="lj", boundary="p p p"),
    ...     attributes={'eval': True}
    ... )
    >>> dscript_instance.do(verbose=True, units="real")
    # Output:
    # --------------
    # TEMPLATE "ExampleScript"
    # --------------
    units real
    boundary p p p
    # ---> Total items: 2 - Ignored items: 0
    """

    printflag = self.printflag if printflag is None else printflag
    verbose = self.verbose if verbose is None else verbose
    header = f"# --------------[ TEMPLATE \"{self.name}\" ]--------------" if verbose else ""
    footer = "# --------------------------------------------" if verbose else ""

    # Initialize output, counters, and optional definitions accumulator
    output = [header]
    non_empty_lines = 0
    ignored_lines = 0
    accumulated_definitions = lambdaScriptdata() if return_definitions else None

    for key, template in self.TEMPLATE.items():
        # Process each template with softrun if enabled, otherwise use full processing
        result = template.do(softrun=softrun,USER=lambdaScriptdata(**USER))
        if result:
            # Apply comment removal based on verbosity
            final_result = result if verbose else remove_comments(result,comment_chars=comment_chars)
            if final_result or verbose:
                output.append(final_result)
                non_empty_lines += 1
            else:
                ignored_lines += 1
            # Accumulate definitions if return_definitions is enabled
            if return_definitions:
                accumulated_definitions += template.definitions
        else:
            ignored_lines += 1

    # Add footer summary if verbose
    nel_word = 'items' if non_empty_lines > 1 else 'item'
    il_word = 'items' if ignored_lines > 1 else 'item'
    footer += f"\n# ---> Total {nel_word}: {non_empty_lines} - Ignored {il_word}: {ignored_lines}" if verbose else ""
    output.append(footer)

    # Concatenate output and determine return type based on return_definitions
    output_content = "\n".join(output)
    return (output_content, accumulated_definitions) if return_definitions else output_content
def generator(self)

Returns

STR
generated code corresponding to dscript (using dscript syntax/language).
Expand source code
def generator(self):
    """
    Returns
    -------
    STR
        generated code corresponding to dscript (using dscript syntax/language).

    """
    return self.save(generatoronly=True)
def get_attributes_by_index(self, index)

Returns the attributes of the ScriptTemplate at the specified index.

Expand source code
def get_attributes_by_index(self, index):
    """ Returns the attributes of the ScriptTemplate at the specified index."""
    key = list(self.TEMPLATE.keys())[index]
    return self.TEMPLATE[key].attributes
def get_content_by_index(self, index, do=True, protected=True)

Returns the content of the ScriptTemplate at the specified index.

Parameters:

index : int The index of the template in the TEMPLATE dictionary. do : bool, optional (default=True) If True, the content will be processed based on conditions and evaluation flags. protected : bool, optional (default=True) Controls whether variable evaluation is protected (e.g., prevents overwriting certain definitions).

Returns:

str or list of str The content of the template after processing, or an empty string if conditions or evaluation flags block it.

Expand source code
def get_content_by_index(self, index, do=True, protected=True):
    """
    Returns the content of the ScriptTemplate at the specified index.

    Parameters:
    -----------
    index : int
        The index of the template in the TEMPLATE dictionary.
    do : bool, optional (default=True)
        If True, the content will be processed based on conditions and evaluation flags.
    protected : bool, optional (default=True)
        Controls whether variable evaluation is protected (e.g., prevents overwriting certain definitions).

    Returns:
    --------
    str or list of str
        The content of the template after processing, or an empty string if conditions or evaluation flags block it.
    """
    key = list(self.TEMPLATE.keys())[index]
    s = self.TEMPLATE[key].content
    att = self.TEMPLATE[key].attributes
    # Return an empty string if the facultative attribute is True and do is True
    if att["facultative"] and do:
        return ""
    # Evaluate the condition (if any)
    if att["condition"] is not None:
        cond = eval(self.DEFINITIONS.formateval(att["condition"], protected))
    else:
        cond = True
    # If the condition is met, process the content
    if cond:
        # Apply formateval only if the eval attribute is True and do is True
        if att["eval"] and do:
            if isinstance(s, list):
                # Apply formateval to each item in the list if s is a list
                return [self.DEFINITIONS.formateval(line, protected) for line in s]
            else:
                # Apply formateval to the single string content
                return self.DEFINITIONS.formateval(s, protected)
        else:
            return s  # Return the raw content if no evaluation is needed
    elif do:
        return ""  # Return an empty string if the condition is not met and do is True
    else:
        return s  # Return the raw content if do is False
def items(self)
Expand source code
def items(self):
    return ((key, s.content) for key, s in self.TEMPLATE.items())
def keys(self)

Return the keys of the TEMPLATE.

Expand source code
def keys(self):
    """Return the keys of the TEMPLATE."""
    return self.TEMPLATE.keys()
def pipescript(self, *keys, printflag=None, verbose=None, verbosity=None, **USER)

Returns a pipescript object by combining script objects corresponding to the given keys.

Parameters:

keys : one or more keys that correspond to the TEMPLATE entries. printflag : bool, optional Whether to enable printing of additional information. verbose : bool, optional Whether to run in verbose mode for debugging or detailed output. *USER : dict, optional Additional user-defined variables to pass into the script.

Returns:

A pipescript object that combines the script objects generated from the selected dscript subobjects.

Expand source code
def pipescript(self, *keys, printflag=None, verbose=None, verbosity=None, **USER):
    """
    Returns a pipescript object by combining script objects corresponding to the given keys.

    Parameters:
    -----------
    *keys : one or more keys that correspond to the `TEMPLATE` entries.
    printflag : bool, optional
        Whether to enable printing of additional information.
    verbose : bool, optional
        Whether to run in verbose mode for debugging or detailed output.
    **USER : dict, optional
        Additional user-defined variables to pass into the script.

    Returns:
    --------
    A `pipescript` object that combines the script objects generated from the selected
    dscript subobjects.
    """
    # Start with an empty pipescript
    # combined_pipescript = None
    # # Iterate over the provided keys to extract corresponding subobjects
    # for key in keys:
    #     # Extract the dscript subobject for the given key
    #     sub_dscript = self(key)
    #     # Convert the dscript subobject to a script object, passing USER, printflag, and verbose
    #     script_obj = sub_dscript.script(printflag=printflag, verbose=verbose, **USER)
    #     # Combine script objects into a pipescript object
    #     if combined_pipescript is None:
    #         combined_pipescript = pipescript(script_obj)  # Initialize pipescript
    #     else:
    #         combined_pipescript = combined_pipescript | script_obj  # Use pipe operator
    # if combined_pipescript is None:
    #     ValueError('The conversion to pipescript from {type{self}} falled')
    # return combined_pipescript
    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

    # Loop over all keys in TEMPLATE and combine them
    combined_pipescript = None
    for key in self.keys():
        # Create a new dscript object with only the current key in TEMPLATE
        focused_dscript = dscript(name=f"{self.name}:{key}")
        focused_dscript.TEMPLATE[key] = self.TEMPLATE[key]
        focused_dscript.TEMPLATE[key].definitions = scriptdata(**self.TEMPLATE[key].definitions)
        focused_dscript.DEFINITIONS = scriptdata(**self.DEFINITIONS)
        focused_dscript.SECTIONS = self.SECTIONS[:]
        focused_dscript.section = self.section
        focused_dscript.position = self.position
        focused_dscript.role = self.role
        focused_dscript.description = self.description
        focused_dscript.userid = self.userid
        focused_dscript.version = self.version
        focused_dscript.verbose = verbose
        focused_dscript.printflag = printflag

        # Convert the focused dscript object to a script object
        script_obj = focused_dscript.script(printflag=printflag, verbose=verbose, **USER)

        # Combine the script objects into a pipescript object using the pipe operator
        if combined_pipescript is None:
            combined_pipescript = pipescript(script_obj)  # Initialize pipescript
        else:
            combined_pipescript = combined_pipescript | pipescript(script_obj)  # Use pipe operator

    if combined_pipescript is None:
        ValueError('The conversion to pipescript from {type{self}} falled')
    return combined_pipescript
def reorder(self, order)

Reorder the TEMPLATE lines according to a list of indices.

Expand source code
def reorder(self, order):
    """Reorder the TEMPLATE lines according to a list of indices."""
    # Get the original items as a list of (key, value) pairs
    original_items = list(self.TEMPLATE.items())
    # Create a new dictionary with reordered scripts, preserving original keys
    new_scripts = {original_items[i][0]: original_items[i][1] for i in order}
    # Create a new dscript object with reordered scripts
    reordered_script = dscript()
    reordered_script.TEMPLATE = new_scripts
    return reordered_script
def save(self, filename=None, foldername=None, overwrite=False, generatoronly=False, onlyusedvariables=True)

Save the current script instance to a text file.

Parameters

filename : str, optional
The name of the file to save the script to. If not provided, self.name is used. The extension ".txt" is automatically appended if not included.
foldername : str, optional
The directory where the file will be saved. If not provided, it defaults to the system's temporary directory. If the filename does not include a full path, this folder will be used.
overwrite : bool, default=True
Whether to overwrite the file if it already exists. If set to False, an exception is raised if the file exists.
generatoronly : bool, default=False
If True, the method returns the generated content string without saving to a file.
onlyusedvariables : bool, default=True
If True, local definitions are only saved if they are used within the template content. If False, all local definitions are saved, regardless of whether they are referenced in the template.

Raises

FileExistsError
If the file already exists and overwrite is set to False.

Notes

  • The script is saved in a plain text format, and each section (global parameters, definitions, template, and attributes) is written in a structured format with appropriate comments.
  • If self.name is used as the filename, it must be a valid string that can serve as a file name.
  • The file structure follows the format: # DSCRIPT SAVE FILE # generated on YYYY-MM-DD on user@hostname

    GLOBAL PARAMETERS

    { … }

    DEFINITIONS (number of definitions=…)

    key=value

    TEMPLATES (number of items=…)

    key: template_content

    ATTRIBUTES (number of items with explicit attributes=…)

    key:{attr1=value1, attr2=value2, …}

Expand source code
def save(self, filename=None, foldername=None, overwrite=False, generatoronly=False, onlyusedvariables=True):
    """
    Save the current script instance to a text file.

    Parameters
    ----------
    filename : str, optional
        The name of the file to save the script to. If not provided, `self.name` is used.
        The extension ".txt" is automatically appended if not included.

    foldername : str, optional
        The directory where the file will be saved. If not provided, it defaults to the system's
        temporary directory. If the filename does not include a full path, this folder will be used.

    overwrite : bool, default=True
        Whether to overwrite the file if it already exists. If set to False, an exception is raised
        if the file exists.

    generatoronly : bool, default=False
        If True, the method returns the generated content string without saving to a file.

    onlyusedvariables : bool, default=True
        If True, local definitions are only saved if they are used within the template content.
        If False, all local definitions are saved, regardless of whether they are referenced in
        the template.

    Raises
    ------
    FileExistsError
        If the file already exists and `overwrite` is set to False.

    Notes
    -----
    - The script is saved in a plain text format, and each section (global parameters, definitions,
      template, and attributes) is written in a structured format with appropriate comments.
    - If `self.name` is used as the filename, it must be a valid string that can serve as a file name.
    - The file structure follows the format:
        # DSCRIPT SAVE FILE
        # generated on YYYY-MM-DD on user@hostname

        # GLOBAL PARAMETERS
        { ... }

        # DEFINITIONS (number of definitions=...)
        key=value

        # TEMPLATES (number of items=...)
        key: template_content

        # ATTRIBUTES (number of items with explicit attributes=...)
        key:{attr1=value1, attr2=value2, ...}
    """
    # At the beginning of the save method
    start_time = time.time()  # Start the timer

    if not generatoronly:
        # Use self.name if filename is not provided
        if filename is None:
            filename = span(self.name, sep="\n")

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

        # Construct the full path
        if foldername in [None, ""]:  # Handle cases where foldername is None or an empty string
            filepath = os.path.abspath(filename)
        else:
            filepath = os.path.join(foldername, filename)

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

    # Header with current date, username, and host
    header = "# DSCRIPT SAVE FILE\n"
    header += "\n"*2
    if generatoronly:
        header += dscript.header(verbose=True,filepath='dynamic code generation (no file)',
                                 name = self.name, version=self.version, license=self.license, email=self.email)
    else:
        header += dscript.header(verbose=True,filepath=filepath,
                                 name = self.name, version=self.version, license=self.license, email=self.email)
    header += "\n"*2

    # Global parameters in strict Python syntax
    global_params = "# GLOBAL PARAMETERS (8 parameters)\n"
    global_params += "{\n"
    global_params += f"    SECTIONS = {self.SECTIONS},\n"
    global_params += f"    section = {self.section},\n"
    global_params += f"    position = {self.position},\n"
    global_params += f"    role = {self.role!r},\n"
    global_params += f"    description = {self.description!r},\n"
    global_params += f"    userid = {self.userid!r},\n"
    global_params += f"    version = {self.version},\n"
    global_params += f"    verbose = {self.verbose}\n"
    global_params += "}\n"

    # Initialize definitions with self.DEFINITIONS
    allvars = self.DEFINITIONS

    # Temporary dictionary to track global variable information
    global_var_info = {}

    # Loop over each template item to detect and record variable usage and overrides
    for template_index, (key, script_template) in enumerate(self.TEMPLATE.items()):
        # Detect variables used in this template
        used_variables = script_template.detect_variables()

        # Loop over each template item to detect and record variable usage and overrides
        for template_index, (key, script_template) in enumerate(self.TEMPLATE.items()):
            # Detect variables used in this template
            used_variables = script_template.detect_variables()

            # Check each variable used in this template
            for var in used_variables:
                # Get global and local values for the variable
                global_value = getattr(allvars, var, None)
                local_value = getattr(script_template.definitions, var, None)
                is_global = var in allvars  # Check if the variable originates in global space
                is_default = is_global and (
                    global_value is None or global_value == "" or global_value == f"${{{var}}}"
                )

                # If the variable is not yet tracked, initialize its info
                if var not in global_var_info:
                    global_var_info[var] = {
                        "value": global_value,       # Initial value from allvars if exists
                        "updatedvalue": global_value,# Initial value from allvars if exists
                        "is_default": is_default,    # Check if it’s set to a default value
                        "first_def": None,           # First definition (to be updated later)
                        "first_use": template_index, # First time the variable is used
                        "first_val": global_value,
                        "override_index": template_index if local_value is not None else None,  # Set override if defined locally
                        "is_global": is_global       # Track if the variable originates as global
                    }
                else:
                    # Update `override_index` if the variable is defined locally and its value changes
                    if local_value is not None:
                        # Check if the local value differs from the tracked value in global_var_info
                        current_value = global_var_info[var]["value"]
                        if current_value != local_value:
                            global_var_info[var]["override_index"] = template_index
                            global_var_info[var]["updatedvalue"] = local_value  # Update the tracked value


        # Second loop: Update `first_def` for all variables
        for template_index, (key, script_template) in enumerate(self.TEMPLATE.items()):
            local_definitions = script_template.definitions.keys()
            for var in local_definitions:
                if var in global_var_info and global_var_info[var]["first_def"] is None:
                    global_var_info[var]["first_def"] = template_index
                    global_var_info[var]["first_val"] = getattr(script_template.definitions, var)


    # Filter global definitions based on usage, overrides, and first_def
    filtered_globals = {
        var: info for var, info in global_var_info.items()
        if (info["is_global"] or info["is_default"]) and (info["override_index"] is None)
        }


    # Generate the definitions output based on filtered globals
    definitions = f"\n# GLOBAL DEFINITIONS (number of definitions={len(filtered_globals)})\n"
    for var, info in filtered_globals.items():
        if info["is_default"] and (info["first_def"]>info["first_use"] if info["first_def"] else True):
            definitions += f"{var} = ${{{var}}}  # value assumed to be defined outside this DSCRIPT file\n"
        else:
            value = info["first_val"] #info["value"]
            if value in ["", None]:
                definitions += f'{var} = ""\n'
            elif isinstance(value, str):
                safe_value = value.replace('\\', '\\\\').replace('\n', '\\n')
                definitions += f"{var} = {safe_value}\n"
            else:
                definitions += f"{var} = {value}\n"

    # Template (number of lines/items)
    printsinglecontent = False
    template = f"\n# TEMPLATES (number of items={len(self.TEMPLATE)})\n"
    for key, script_template in self.TEMPLATE.items():
        # Get local template definitions and detected variables
        template_vars = script_template.definitions
        used_variables = script_template.detect_variables()
        islocal = False
        # Temporary dictionary to accumulate variables to add to allvars
        valid_local_vars = lambdaScriptdata()
        # Write template-specific definitions only if they meet the updated conditions
        for var in template_vars.keys():
            # Conditions for adding a variable to the local template and to `allvars`
            if (var in used_variables or not onlyusedvariables) and (
                script_template.is_variable_set_value_only(var) and
                (var not in allvars or getattr(template_vars, var) != getattr(allvars, var))
            ):
                # Start local definitions section if this is the first local variable for the template
                if not islocal:
                    template += f"\n# LOCAL DEFINITIONS for key '{key}'\n"
                    islocal = True
                # Retrieve and process the variable value
                value = getattr(template_vars, var)
                if value in ["", None]:
                    template += f'{var} = ""\n'  # Set empty or None values as ""
                elif isinstance(value, str):
                    safe_value = value.replace('\\', '\\\\').replace('\n', '\\n')
                    template += f"{var} = {safe_value}\n"
                else:
                    template += f"{var} = {value}\n"
                # Add the variable to valid_local_vars for selective update of allvars
                valid_local_vars.setattr(var, value)
        # Update allvars only with filtered, valid local variables
        allvars += valid_local_vars

        # Write the template content
        if isinstance(script_template.content, list):
            if len(script_template.content) == 1:
                # Single-line template saved as a single line
                content_str = script_template.content[0].strip()
                template += "" if printsinglecontent else "\n"
                template += f"{key}: {content_str}\n"
                printsinglecontent = True
            else:
                content_str = '\n    '.join(script_template.content)
                template += f"\n{key}: [\n    {content_str}\n ]\n"
                printsinglecontent = False
        else:
            template += "" if printsinglecontent else "\n"
            template += f"{key}: {script_template.content}\n"
            printsinglecontent = True

    # Attributes (number of lines/items with explicit attributes)
    attributes = f"# ATTRIBUTES (number of items with explicit attributes={len(self.TEMPLATE)})\n"
    for key, script_template in self.TEMPLATE.items():
        attr_str = ", ".join(f"{attr_name}={repr(attr_value)}"
                             for attr_name, attr_value in script_template.attributes.items())
        attributes += f"{key}:{{{attr_str}}}\n"

    # Combine all sections into one content
    content = header + "\n" + global_params + "\n" + definitions + "\n" + template + "\n" + attributes + "\n"


    # Append footer information to the content
    non_empty_lines = sum(1 for line in content.splitlines() if line.strip())  # Count non-empty lines
    execution_time = time.time() - start_time  # Calculate the execution time in seconds
    # Prepare the footer content
    footer_lines = [
        ["Non-empty lines", str(non_empty_lines)],
        ["Execution time (seconds)", f"{execution_time:.4f}"],
    ]
    # Format footer into tabular style
    footer_content = [
        f"{row[0]:<25} {row[1]:<15}" for row in footer_lines
    ]
    # Use frame_header to format footer
    footer = frame_header(
        lines=["DSCRIPT SAVE FILE generator"] + footer_content,
        style=1
    )
    # Append footer to the content
    content += f"\n{footer}"

    if generatoronly:
        return content
    else:
        # Write the content to the file
        with open(filepath, 'w') as f:
            f.write(content)
        print(f"\nScript saved to {filepath}")
        return filepath
def script(self, printflag=None, verbose=None, verbosity=None, **USER)

returns the corresponding script

Expand source code
def script(self,printflag=None, verbose=None, verbosity=None, **USER):
    """
    returns the corresponding script
    """
    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
    return lamdaScript(self,persistentfile=True, persistentfolder=None,
                       printflag=printflag, verbose=verbose,
                       **USER)
def set_all_variables(self)

Ensures that all variables in the templates are added to the global definitions with default values if they are not already defined.

Expand source code
def set_all_variables(self):
    """
    Ensures that all variables in the templates are added to the global definitions
    with default values if they are not already defined.
    """
    for key, script_template in self.TEMPLATE.items():
        # Check and update the global definitions with template-specific variables
        for var in script_template.detect_variables():
            if var not in self.DEFINITIONS:
                # Add undefined variables with their default value
                self.DEFINITIONS.setattr(var, f"${{{var}}}")  # Set default as ${varname}
def values(self)

Return the ScriptTemplate objects in TEMPLATE.

Expand source code
def values(self):
    """Return the ScriptTemplate objects in TEMPLATE."""
    return self.TEMPLATE.values()
class lambdaScriptdata (sortdefinitions=False, **kwargs)

Class to manage lambda script parameters.

This class holds definitions and variables used within the script templates. These parameters are typically set up as global variables that can be accessed by script sections for evaluation and substitution.

Example Usage:

definitions = lambdaScriptdata(var1=10, var2="value")

Attributes:

_type : str The type of the data ("LSD" by default). _fulltype : str Full type description of the lambda script data. _ftype : str Short description of the type (parameter definition).

Methods:

This class inherits methods from the paramauto class, which allows automatic handling of parameters and script data.

Constructor for lambdaScriptdata. 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 lambdaScriptdata(paramauto):
    """
    Class to manage lambda script parameters.

    This class holds definitions and variables used within the script templates.
    These parameters are typically set up as global variables that can be accessed
    by script sections for evaluation and substitution.

    Example Usage:
    --------------
    definitions = lambdaScriptdata(var1=10, var2="value")

    Attributes:
    -----------
    _type : str
        The type of the data ("LSD" by default).
    _fulltype : str
        Full type description of the lambda script data.
    _ftype : str
        Short description of the type (parameter definition).

    Methods:
    --------
    This class inherits methods from the `paramauto` class, which allows automatic
    handling of parameters and script data.
    """

    _type = "LSD"
    _fulltype = "Lambda Script Parameters"
    _ftype = "parameter definition"

    def __init__(self, _protection=False, _evaluation=True, sortdefinitions=False, **kwargs):
        """
        Constructor for lambdaScriptdata. It forces the parent's _returnerror parameter to False.

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

Ancestors

  • pizza.private.mstruct.paramauto
  • pizza.private.mstruct.param
  • pizza.private.mstruct.struct
class lamdaScript (dscriptobj, persistentfile=True, persistentfolder=None, printflag=False, verbose=True, softrun=True, **userdefinitions)

lamdaScript

The lamdaScript class is a specialized subclass of script that acts as a wrapper for generating script objects from dscript instances. It facilitates the creation of scripts with persistent storage options and user-defined configurations.

Attributes

name : str
The name of the script.
SECTIONS : list
Inherited list of script sections from the script class.
position : int
The position index of the script section.
role : str
The role of the script section, derived from its position.
description : str
A brief description of the script.
userid : str
The user ID associated with the script.
version : float
The version of the script.
verbose : bool
Flag to enable verbose output.
DEFINITIONS : scriptdata
Definitions of variables used in the script.
USER : lambdaScriptdata
User-defined variables specific to lamdaScript.
TEMPLATE : dict
A dictionary of script templates.

Methods

do() Generates the complete LAMMPS script by processing all script sections.

Special Methods

contains(key) Allows checking if a specific section exists using the in keyword. str() Returns the string representation of the script.

Usage Example

from dscript import lamdaScript, dscript

# Create an existing dscript instance
existing_dscript = dscript(name="ExistingScript")
existing_dscript.role = "Custom Role"

# Create a lamdaScript instance based on the existing dscript
ls = lamdaScript(existing_dscript)
print(ls.role)  # Outputs: "Custom Role"

Initialize a new lambdaScript instance.

This constructor creates a lambdaScript object based on an existing dscriptobj, providing options for persistent storage, verbose output, and user-defined configurations. A lambdaScript represents an anonymous or temporary script that can either preserve the original script structure or partially evaluate variable definitions based on the softrun flag.

Parameters

dscriptobj : dscript
An existing dscript object to base the new instance on.
persistentfile : bool, optional
If True, the script will be saved to a persistent file. Defaults to True.
persistentfolder : str or None, optional
The folder where the persistent file will be saved. If None, a temporary location is used. Defaults to None.
printflag : bool, optional
If True, enables printing of the script details during execution. Defaults to False.
verbose : bool, optional
If True, provides detailed output during script initialization and execution. Defaults to True.
softrun : bool, optional
Determines whether a pre-execution run is carried out to partially evaluate and substitute variables. - If True (default), the variable definitions and original script content are preserved without full evaluation, meaning no substitution is carried out on the dscript's templates or definitions. - If False, performs an initial evaluation phase that substitutes available variables and captures the local definitions before creating the lambdaScript object.
**userdefinitions
Additional user-defined variables and configurations to be included in the lambdaScript.

Raises

TypeError
If dscriptobj is not an instance of the dscript class.

Example

existing_dscript = dscript(name="ExistingScript")
ls = lambdaScript(existing_dscript, var3="3", softrun=False)
Expand source code
class lamdaScript(script):

    """
    lamdaScript
    ===========

    The `lamdaScript` class is a specialized subclass of `script` that acts as a wrapper
    for generating script objects from `dscript` instances. It facilitates the creation
    of scripts with persistent storage options and user-defined configurations.

    Attributes
    ----------
    name : str
        The name of the script.
    SECTIONS : list
        Inherited list of script sections from the `script` class.
    position : int
        The position index of the script section.
    role : str
        The role of the script section, derived from its position.
    description : str
        A brief description of the script.
    userid : str
        The user ID associated with the script.
    version : float
        The version of the script.
    verbose : bool
        Flag to enable verbose output.
    DEFINITIONS : scriptdata
        Definitions of variables used in the script.
    USER : lambdaScriptdata
        User-defined variables specific to `lamdaScript`.
    TEMPLATE : dict
        A dictionary of script templates.

    Methods
    -------
    do()
        Generates the complete LAMMPS script by processing all script sections.

    Special Methods
    ----------------
    __contains__(key)
        Allows checking if a specific section exists using the `in` keyword.
    __str__()
        Returns the string representation of the script.

    Usage Example
    -------------
    ```python
    from dscript import lamdaScript, dscript

    # Create an existing dscript instance
    existing_dscript = dscript(name="ExistingScript")
    existing_dscript.role = "Custom Role"

    # Create a lamdaScript instance based on the existing dscript
    ls = lamdaScript(existing_dscript)
    print(ls.role)  # Outputs: "Custom Role"
    ```
    """
    name = ""

    def __init__(self, dscriptobj, persistentfile=True, persistentfolder=None,
                 printflag = False, verbose = True, softrun = True,
                 **userdefinitions):
        """
            Initialize a new `lambdaScript` instance.

            This constructor creates a `lambdaScript` object based on an existing `dscriptobj`,
            providing options for persistent storage, verbose output, and user-defined configurations.
            A `lambdaScript` represents an anonymous or temporary script that can either preserve the
            original script structure or partially evaluate variable definitions based on the `softrun` flag.

            Parameters
            ----------
            dscriptobj : dscript
                An existing `dscript` object to base the new instance on.
            persistentfile : bool, optional
                If `True`, the script will be saved to a persistent file. Defaults to `True`.
            persistentfolder : str or None, optional
                The folder where the persistent file will be saved. If `None`, a temporary location is used.
                Defaults to `None`.
            printflag : bool, optional
                If `True`, enables printing of the script details during execution. Defaults to `False`.
            verbose : bool, optional
                If `True`, provides detailed output during script initialization and execution. Defaults to `True`.
            softrun : bool, optional
                Determines whether a pre-execution run is carried out to partially evaluate and substitute variables.
                - If `True` (default), the variable definitions and original script content are preserved without full
                  evaluation, meaning no substitution is carried out on the `dscript`'s templates or definitions.
                - If `False`, performs an initial evaluation phase that substitutes available variables and captures
                  the local definitions before creating the `lambdaScript` object.
            **userdefinitions
                Additional user-defined variables and configurations to be included in the `lambdaScript`.

            Raises
            ------
            TypeError
                If `dscriptobj` is not an instance of the `dscript` class.

            Example
            -------
            ```python
            existing_dscript = dscript(name="ExistingScript")
            ls = lambdaScript(existing_dscript, var3="3", softrun=False)
            ```
        """
        if not isinstance(dscriptobj, dscript):
            raise TypeError(f"The 'dscriptobj' object must be of class dscript not {type(dscriptobj).__name__}.")
        verbose = dscriptobj.verbose if verbose is None else dscriptobj.verbose
        super().__init__(persistentfile=persistentfile, persistentfolder=persistentfolder, printflag=printflag, verbose=verbose, **userdefinitions)
        self.name = dscriptobj.name
        self.SECTIONS = dscriptobj.SECTIONS
        self.section = dscriptobj.section
        self.position = dscriptobj.position
        self._role = dscriptobj.role  # Initialize an internal storage for the role
        self.description = dscriptobj.description
        self.userid = dscriptobj.userid
        self.version= dscriptobj.version
        self.verbose = verbose
        self.printflag = printflag
        self.DEFINITIONS = dscriptobj.DEFINITIONS
        if softrun:
            self.TEMPLATE,localdefinitions = dscriptobj.do(softrun=softrun,return_definitions=True)
            self.USER = localdefinitions + lambdaScriptdata(**self.USER)
        else:
            self.TEMPLATE = dscriptobj.do(softrun=softrun)
            self.USER = lambdaScriptdata(**self.USER)

    @property
    def role(self):
        """Override the role property to include a setter."""
        # If _role is set, return it; otherwise, use the inherited logic
        if self._role is not None:
            return self._role
        elif self.section in range(len(self.SECTIONS)):
            return self.SECTIONS[self.section]
        else:
            return ""

    @role.setter
    def role(self, value):
        """Allow setting the role."""
        self._role = value

Ancestors

  • pizza.script.script

Class variables

var name

Instance variables

var role

Override the role property to include a setter.

Expand source code
@property
def role(self):
    """Override the role property to include a setter."""
    # If _role is set, return it; otherwise, use the inherited logic
    if self._role is not None:
        return self._role
    elif self.section in range(len(self.SECTIONS)):
        return self.SECTIONS[self.section]
    else:
        return ""
class paramauto (sortdefinitions=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(param,self).__add__(p,sortdefinitions=True,raiseerror=False)

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

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

Ancestors

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

Subclasses

  • lambdaScriptdata
  • pizza.dscript.lambdaScriptdata
  • pizza.forcefield.parameterforcefield
  • pizza.region.regiondata
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

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

    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 """
        if isinstance(s,pipescript):
            dup = deepduplicate(self)
            return dup | s      # + or | are synonyms
        else:
            raise TypeError("The operand should be a pipescript")

    def __iadd__(self,s):
        """ overload += as pipe without copy """
        if isinstance(s,pipescript):
            return self | s      # + or | are synonyms
        else:
            raise TypeError("The operand should be a pipescript")

    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("The operand should be a pipescript")


    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)):
            rightarg = pipescript(s,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, **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.

        **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]
            )

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

        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)

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, **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.

**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, **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.

    **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]
        )

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

    return outd
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 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 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.
        """
        printflag = self.printflag if printflag is None else printflag
        verbose = self.verbose if verbose is None else verbose
        inputs = self.DEFINITIONS + self.USER
        for k in inputs.keys():
            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("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):
        """
        Detects variables in the content of the template using the pattern r'\$\{(\w+)\}'.

        Returns:
        --------
        list
            A list of unique variable names detected in the content.
        """
        # Regular expression to match variables in the format ${varname}
        variable_pattern = re.compile(r'\$\{(\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 = {variable for line in lines for variable in variable_pattern.findall(line)}
        # Return the list of unique variables
        return list(detected_vars)

Subclasses

  • lamdaScript
  • pizza.dscript.lamdaScript
  • pizza.region.LammpsGeneric
  • pizza.script.boundarysection
  • pizza.script.discretizationsection
  • pizza.script.dumpsection
  • pizza.script.geometrysection
  • pizza.script.globalsection
  • pizza.script.initializesection
  • pizza.script.integrationsection
  • pizza.script.interactionsection
  • pizza.script.runsection
  • pizza.script.statussection

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)

Detects variables in the content of the template using the pattern r'\${(\w+)}'.

Returns:

list A list of unique variable names detected in the content.

Expand source code
def detect_variables(self):
    """
    Detects variables in the content of the template using the pattern r'\$\{(\w+)\}'.

    Returns:
    --------
    list
        A list of unique variable names detected in the content.
    """
    # Regular expression to match variables in the format ${varname}
    variable_pattern = re.compile(r'\$\{(\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 = {variable for line in lines for variable in variable_pattern.findall(line)}
    # 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="dscript.script" href="#dscript.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.
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.
    """
    printflag = self.printflag if printflag is None else printflag
    verbose = self.verbose if verbose is None else verbose
    inputs = self.DEFINITIONS + self.USER
    for k in inputs.keys():
        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, **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