Module region

================================================================================ REGION Module Documentation ================================================================================

Project: Pizza3 Authors: Olivier Vitrac, Han Chen Copyright: 2024 Credits: Olivier Vitrac, Han Chen License: GPLv3 Maintainer: Olivier Vitrac Email: olivier.vitrac@agroparistech.fr Version: 0.9999

Overview

The REGION module provides a suite of tools to define and manipulate native geometries in Python for LAMMPS (Large-scale Atomic/Molecular Massively Parallel Simulator). It is designed to facilitate the creation, concatenation, and manipulation of geometric regions used in molecular dynamics simulations.

Public Features

  • Concatenation of Regions:
  • R1 + R2 concatenates two regions (objects of R2 are inherited in R1, higher precedence for R2).
  • Generation of Objects:
  • R.do() generates the objects (similar functionality to do() in pizza.script).
  • Script Generation:
  • R.script returns the script (similar functionality to script in pizza.script).
  • Object Deletion:
  • R.o1 = [] deletes object o1.
  • Union of Objects:
  • R.union(o1, o2, name=...) creates a union of o1 and o2 (in the LAMMPS sense, see region manual).

Classes And Methods

Class: region

Description:

The region class is used to define and manipulate geometries for LAMMPS simulations. It includes methods for creating different geometric shapes, combining regions, and generating corresponding LAMMPS scripts.

Methods:

  • __init__(self, name='', width=0, height=0, depth=0, regionunits='lattice', separationdistance=0.0, lattice_scale=1.0)
  • Description: Initializes a new region with specified dimensions and parameters.
  • Parameters:

    • name (str): The name of the region.
    • width (float): Width of the region.
    • height (float): Height of the region.
    • depth (float): Depth of the region.
    • regionunits (str): Units of the region dimensions ('lattice' or 'si').
    • separationdistance (float): Separation distance between regions.
    • lattice_scale (float): Scale of the lattice.
  • do(self)

  • Description: Generates the objects within the region.
  • Returns: None

  • script(self)

  • Description: Returns the LAMMPS script for the region.
  • Returns: str

  • union(self, o1, o2, name='')

  • Description: Creates a union of two objects within the region.
  • Parameters:
    • o1 (object): The first object.
    • o2 (object): The second object.
    • name (str): The name of the union.
  • Returns: None

  • cylinder(self, name, dim, c1, c2, radius, lo, hi, beadtype)

  • Description: Adds a cylinder to the region.
  • Parameters:
    • name (str): The name of the cylinder.
    • dim (str): Dimension along which the cylinder is oriented ('x', 'y', or 'z').
    • c1 (float): First coordinate of the center of the cylinder's base.
    • c2 (float): Second coordinate of the center of the cylinder's base.
    • radius (float): Radius of the cylinder.
    • lo (float): Lower bound of the cylinder along the specified dimension.
    • hi (float): Upper bound of the cylinder along the specified dimension.
    • beadtype (int): Type of beads to use for the cylinder.
  • Returns: None

  • delete(self, name)

  • Description: Deletes an object from the region.
  • Parameters:
    • name (str): The name of the object to delete.
  • Returns: None

Examples

Below are some examples demonstrating how to use the REGION module:

  1. Concatenating Two Regions: python R1 = pizza.regions() R2 = pizza.regions() R = R1 + R2

  2. Generating Objects: python R.do()

  3. Retrieving the Script: python script_content = R.script()

  4. Deleting an Object: python R.o1 = []

  5. Creating a Union of Objects: python R.union(o1, o2, name='union_name')

  6. Adding a Cylinder: ```python R.cylinder(name='cyl1', dim='z', c1=0, c2=0, radius=1.0, lo=0.0, hi=5.0, beadtype=1)

Advanced Features

Public features (i.e. to be used by the end-user) Let R1, R2 being pizza.regions() R = R1 + R2 concatenates two regions (objects of R2 are inherited in R1, higher precedence for R2) R.do() generate the objects (do() should work as in pizza.script) R.script returns the script (script() should work as in pizza.script)

    Let o1, o2 are objects of R
    R.o1 = [] delete o1
    R.union(o1,o2,name=...) creates an union of o1 and o2 (in the LAMMPS sense, see region manual)
    R.intersect(o1,o2,name=...) creates an intersection of o1 and o2 (in the LAMMPS sense, see region manual)
    R.eval(expr,name=)
            expr any algebraic expression including +
            o1+o2+...

Private features (i.e. to be used inside the code) Overloading operators +, +=, | for any coregeometry object Note that coregeometry have four main SECTIONS (scripts) SECTIONS["variables"] SECTIONS["region"] SECTIONS["create"] SECTIONS["group"] SECTIONS["move"] USER, VARIABLES are overdefined as attributes

    +, += merge regions (no piping)
    | pipe them

Add other geometries: block, sphere, cylinder....

```

Dependencies

  • Python 3.x
  • LAMMPS
  • pizza3.pizza

Installation

To use the REGION 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 -*-

"""

================================================================================
REGION Module Documentation
================================================================================

Project: Pizza3
Authors: Olivier Vitrac, Han Chen
Copyright: 2024
Credits: Olivier Vitrac, Han Chen
License: GPLv3
Maintainer: Olivier Vitrac
Email: olivier.vitrac@agroparistech.fr
Version: 0.9999

Overview
--------
The REGION module provides a suite of tools to define and manipulate native geometries in Python
 for LAMMPS (Large-scale Atomic/Molecular Massively Parallel Simulator). It is designed to facilitate
 the creation, concatenation, and manipulation of geometric regions used in molecular dynamics simulations.

Public Features
---------------
- Concatenation of Regions:
  - `R1 + R2` concatenates two regions (objects of R2 are inherited in R1, higher precedence for R2).
- Generation of Objects:
  - `R.do()` generates the objects (similar functionality to `do()` in pizza.script).
- Script Generation:
  - `R.script` returns the script (similar functionality to `script()` in pizza.script).
- Object Deletion:
  - `R.o1 = []` deletes object `o1`.
- Union of Objects:
  - `R.union(o1, o2, name=...)` creates a union of `o1` and `o2` (in the LAMMPS sense, see region manual).

Classes and Methods
-------------------
### Class: `region`
#### Description:
The `region` class is used to define and manipulate geometries for LAMMPS simulations. It includes
 methods for creating different geometric shapes, combining regions, and generating corresponding LAMMPS scripts.

#### Methods:
- **`__init__(self, name='', width=0, height=0, depth=0, regionunits='lattice', separationdistance=0.0, lattice_scale=1.0)`**
  - **Description**: Initializes a new region with specified dimensions and parameters.
  - **Parameters**:
    - `name` (str): The name of the region.
    - `width` (float): Width of the region.
    - `height` (float): Height of the region.
    - `depth` (float): Depth of the region.
    - `regionunits` (str): Units of the region dimensions ('lattice' or 'si').
    - `separationdistance` (float): Separation distance between regions.
    - `lattice_scale` (float): Scale of the lattice.

- **`do(self)`**
  - **Description**: Generates the objects within the region.
  - **Returns**: None

- **`script(self)`**
  - **Description**: Returns the LAMMPS script for the region.
  - **Returns**: str

- **`union(self, o1, o2, name='')`**
  - **Description**: Creates a union of two objects within the region.
  - **Parameters**:
    - `o1` (object): The first object.
    - `o2` (object): The second object.
    - `name` (str): The name of the union.
  - **Returns**: None

- **`cylinder(self, name, dim, c1, c2, radius, lo, hi, beadtype)`**
  - **Description**: Adds a cylinder to the region.
  - **Parameters**:
    - `name` (str): The name of the cylinder.
    - `dim` (str): Dimension along which the cylinder is oriented ('x', 'y', or 'z').
    - `c1` (float): First coordinate of the center of the cylinder's base.
    - `c2` (float): Second coordinate of the center of the cylinder's base.
    - `radius` (float): Radius of the cylinder.
    - `lo` (float): Lower bound of the cylinder along the specified dimension.
    - `hi` (float): Upper bound of the cylinder along the specified dimension.
    - `beadtype` (int): Type of beads to use for the cylinder.
  - **Returns**: None

- **`delete(self, name)`**
  - **Description**: Deletes an object from the region.
  - **Parameters**:
    - `name` (str): The name of the object to delete.
  - **Returns**: None

Examples
--------
Below are some examples demonstrating how to use the REGION module:

1. **Concatenating Two Regions**:
    ```python
    R1 = pizza.regions()
    R2 = pizza.regions()
    R = R1 + R2
    ```

2. **Generating Objects**:
    ```python
    R.do()
    ```

3. **Retrieving the Script**:
    ```python
    script_content = R.script()
    ```

4. **Deleting an Object**:
    ```python
    R.o1 = []
    ```

5. **Creating a Union of Objects**:
    ```python
    R.union(o1, o2, name='union_name')
    ```

6. **Adding a Cylinder**:
    ```python
    R.cylinder(name='cyl1', dim='z', c1=0, c2=0, radius=1.0, lo=0.0, hi=5.0, beadtype=1)


Advanced features
-----------------
Public features (i.e. to be used by the end-user)
        Let R1, R2 being pizza.regions()
        R = R1 + R2 concatenates two regions (objects of R2 are inherited in R1, higher precedence for R2)
        R.do()   generate the objects (do() should work as in pizza.script)
        R.script returns the script (script() should work as in pizza.script)

        Let o1, o2 are objects of R
        R.o1 = [] delete o1
        R.union(o1,o2,name=...) creates an union of o1 and o2 (in the LAMMPS sense, see region manual)
        R.intersect(o1,o2,name=...) creates an intersection of o1 and o2 (in the LAMMPS sense, see region manual)
        R.eval(expr,name=)
                expr any algebraic expression including +
                o1+o2+...

Private features (i.e. to be used inside the code)
        Overloading operators +, +=, | for any coregeometry object
        Note that coregeometry have four main SECTIONS (scripts)
       SECTIONS["variables"]
       SECTIONS["region"]
       SECTIONS["create"]
       SECTIONS["group"]
       SECTIONS["move"]
                USER, VARIABLES are overdefined as attributes

        +, += merge regions (no piping)
        | pipe them

   Add other geometries: block, sphere, cylinder....

    ```

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

Installation
------------
To use the REGION 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"
__copyright__ = "Copyright 2024"
__credits__ = ["Olivier Vitrac", "Han Chen"]
__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
# 2023-01-04 code initialization
# 2023-01-10 early alpha version
# 2023-01-11 alpha version (many fixes), wrap and align all displays with textwrap.fill, textwrap.shorten
# 2023-01-12 implement methods of keyword(units/rotate/open)
# 2023-01-18 split a script into subscripts to be executed in pipescripts
# 2023-01-19 use of LammpsGeneric.do() and hasvariables flag to manage VARIABLES
# 2023-01-20 app PythonPath management for VScode, add comments region.ellipsoid()
# 2023-01-22 todo list added
# 2023-01-24 implementation (o1 = R.E1, o2 = R.E2): o1+o2+... and o1 | o2 | ...
# 2023-01-25 full implementation of eval(), set(), get(), __setattr__(), __getattr__(), iterators
# 2023-01-26 add an example with for and join
# 2023-01-27 add coregometry.__iadd__, region.pipescript, region.script, region.do()
# 2023-01-27 true alpha version, workable with https://andeplane.github.io/atomify/
# 2023-01-31 fix browser and temporary files on Linux
# 2023-02-06 major update, internal documentation for all objects, livelammps attribute
# 2023-02-16 fix object counters, add width, height, depth to region(), data are stored in live
# 2023-02-16 add a specific >> (__rshift__) method for LammpsVariables, to be used by pipescript.do()
# 2023-02-21 add gel compression exemple, modification of the footer section to account for several beadtypes
# 2023-03-16 add emulsion, collection
# 2023-07-07 fix region.union()
# 2023-07-15 add the preffix "$" to units, fix prism and other minor issues
# 2023-07-15 (code works with the current state of Workshop4)
# 2023-07-17 avoid duplicates if union or intersect is used, early implemeantion of "move"
# 2023-07-19 add region.hasfixmove, region.livelammps.options["static" | "dynamic"]
# 2023-07-19 early design for LammpsGroup class, Group class, region.group()
# 2023-07-20 reimplement, validate and extend the original emulsion example
# 2023-07-25 add group section (not active by default)
# 2023-07-29 symmetric design for coregeometry and collection objects with flag control, implementation in pipescript
# 2023-07-29 fix for the class LammpsCollectionGroup() - previous bug resolved
# 2023-08-11 full implementation of the space-filled model such as in pizza.raster
# 2024-04-18 workshop compatible (i.e., implementation of region.scriptobject(), to be used along with region.do())
# 2024-06-14 add mass, density attributes to all region objects and region (overdefinitions are possible), natoms return the number of atoms
# 2024-06-20 debug the calculation of volume of cylinder
# 2024-07-03 full implementation of scaling in pizza.region()
# 2024-07-04 implementation of scaling with formula (when variables are used), add live attributes to region along with an updated LammpsHeader
# 2024-07-05 full implementation of natoms, geometry
# 2024-07-29 consolidation of the method scriptobject (note that a do() is required before calling scriptobject)
# 2024-08-02 community implementation
# 2024-08-31 add method R.beadtypes(), class headerbox(), method R.headerbox()
# 2024-08-01 more robust implementation via method: scriptHeaders() and headersData object
# 2024-10-08 add lattice_scale
# 2024-12-01 standarize scripting features, automatically call script/pscript methods
# 2024-12-09 fix getattr for region objects to be compatible with inspect, pdoc


# %% Imports and private library
import os, sys, math
from datetime import datetime
from copy import copy as duplicate
from copy import deepcopy as deepduplicate
from textwrap import fill, shorten
from webbrowser import open as livelammps

# update python path if needed (for development only)
# try: pwd = os.path.abspath(os.path.join(os.path.dirname(__file__),".."))
# except NameError: pwd = os.getcwd()
# try: sys.path.index(pwd) # sys.path.insert(0, os.getcwd())
# except ValueError: sys.path.append(pwd)
# print('\n>>>',pwd,'\n')
# os.chdir(pwd)

# import struct, param, paramauto, span
from pizza.private.mstruct import *
from pizza.script import pipescript, script, scriptdata, scriptobject, span
from pizza.forcefield import *


__all__ = ['Block', 'Collection', 'Cone', 'Cylinder', 'Ellipsoid', 'Evalgeometry', 'Intersect', 'LammpsCollectionGroup', 'LammpsCreate', 'LammpsFooter', 'LammpsFooterPreview', 'LammpsGeneric', 'LammpsGroup', 'LammpsHeader', 'LammpsHeaderBox', 'LammpsHeaderInit', 'LammpsHeaderLattice', 'LammpsHeaderMass', 'LammpsMove', 'LammpsRegion', 'LammpsSetGroup', 'LammpsSpacefilling', 'LammpsVariables', 'Plane', 'Prism', 'Sphere', 'Union', 'cleanname', 'coregeometry', 'emulsion', 'forcefield', 'headersRegiondata', 'none', 'param', 'paramauto', 'parameterforcefield', 'pipescript', 'pstr', 'region', 'regioncollection', 'regiondata', 'rigidwall', 'saltTLSPH', 'scatter', 'script', 'scriptdata', 'scriptobject', 'smd', 'solidfood', 'span', 'struct', 'tlsph', 'ulsph', 'water', 'wrap']


# protected properties in region
protectedregionkeys = ('name', 'live', 'nbeads' 'volume', 'mass', 'radius', 'contactradius', 'velocities',
                        'forces', 'filename', 'index', 'objects', 'nobjects', 'counter','_iter_',
                        'livelammps','copy', 'hasfixmove', 'spacefilling', 'isspacefilled', 'spacefillingbeadtype','mass','density',
                        'units','center','separationdistance','regionunits',
                        'lattice_scale','lattice_style','lattice_scale_siunits', 'lattice_spacing',
                        'geometry', 'natoms', 'headersData'
                            )

# livelammps
#livelammpsURL = 'https://editor.lammps.org/'
livelammpsURL = "https://andeplane.github.io/atomify/"
livetemplate = {
    'mass':'mass                    %d 1.0',
    'pair_coeff':'pair_coeff        %d %d 1.0 1.0 2.5',
    }
groupprefix = "GRP"  # prefix for all group IDs created from a named region
fixmoveprefix = "FM" # prefix for all fix move IDs created from a named region
# %% Low level functions
# wrap and indent text for variables
wrap = lambda k,op,v,indent,width,maxwidth: fill(
        shorten(v,width=maxwidth+indent,
        fix_sentence_endings=True),
        width=width+indent,
        initial_indent=" "*(indent-len(k)-len(op)-2)+f'{k} {op} ',
        subsequent_indent=' '*(indent+(1 if v[0]=='"' else 0) )
        )

# remove $ from variable names
cleanname = lambda name: "".join([x for x in name if x!="$"])

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

class regiondata(paramauto):
    """
        class of script parameters
            Typical constructor:
                DEFINITIONS = regiondata(
                    var1 = value1,
                    var2 = value2
                    )
        See script, struct, param to get review all methods attached to it
    """
    _type = "RD"
    _fulltype = "region data"
    _ftype = "definition"

    def generatorforlammps(self,verbose=False,hasvariables=False):
        """
            generate LAMMPS code from regiondata (struct)
            generatorforlammps(verbose,hasvariables)
            hasvariables = False is used to prevent a call of generatorforLammps()
            for scripts others than LammpsGeneric ones
        """
        nk = len(self)
        if nk>0:
            self.sortdefinitions(raiseerror=False)
            s = self.tostruct()
            ik = 0
            fmt = "variable %s equal %s"
            cmd = "\n#"+"_"*40+"\n"+f"#[{str(datetime.now())}]\n" if verbose else ""
            cmd += f"\n# Definition of {nk} variables (URL: https://docs.lammps.org/variable.html)\n"
            if hasvariables:
                for k in s.keys():
                    ik += 1
                    end = "\n" if ik<nk else "\n"*2
                    v = getattr(s,k)
                    if v is None: v = "NULL"
                    if isinstance(v,(int,float)) or v == None:
                        cmd += fmt % (k,v)+end
                    elif isinstance(v,str):
                        cmd += fmt % (k,f'{v}')+end
                    elif isinstance(v,(list,tuple)):
                        cmd += fmt % (k,span(v))+end
                    else:
                        raise TypeError(f"unsupported type for the variable {k} set to {v}")
                if verbose: cmd += "#"+"_"*40+"\n"
        return cmd

class regioncollection(struct):
    """ regioncollection class container (not to be called directly) """
    _type = "collect"               # object type
    _fulltype = "Collections"    # full name
    _ftype = "collection"        # field name
    def __init__(self,*obj,**kwobj):
        # store the objects with their alias
        super().__init__(**kwobj)
        # store objects with their real names
        for o in obj:
            if isinstance(o,region):
                s = struct.dict2struct(o.objects)
                list_s = s.keys()
                for i in range(len(list_s)): self.setattr(list_s[i], s[i].copy())
            elif o!=None:
                self.setattr(o.name, o.copy())

# Class for headersData (added on 2024-09-01)
class headersRegiondata(regiondata):
    """
        class of script parameters
            Typical constructor:
                DEFINITIONS = headersRegiondata(
                    var1 = value1,
                    var2 = value2
                    )
        See script, struct, param to get review all methods attached to it
    """
    _type = "HRD"
    _fulltype = "Header parameters - helper for scripts"
    _ftype = "header definition"


# %% PRIVATE SUB-CLASSES
# Use the equivalent methods of raster() to call these constructors

class LammpsGeneric(script):
    """
        common class to override standard do() method from script
        LammpsVariables, LammpsRegion, LammpsCreate are LammpsGeneric
        note:: the only difference with the common script class is that
        LammpsGeneric accepts VARIABLES AND To SHOW THEM
    """
    def do(self,printflag=True,verbose=False):
        """ generate the LAMMPS code with VARIABLE definitions """
        if self.DEFINITIONS.hasvariables and hasattr(self,'VARIABLES'): # attribute VARIABLES checked 2023-08-11
            cmd = f"#[{str(datetime.now())}] {self.name} > {self.SECTIONS[0]}" \
                if verbose else ""
            if len(self.VARIABLES)>0: cmd += \
            self.VARIABLES.generatorforlammps(verbose=verbose,hasvariables=True)
        else:
            cmd = ""
        cmd += super().do(printflag=False,verbose=verbose)
        if printflag: print(cmd)
        return cmd

class LammpsVariables(LammpsGeneric):
    """
        script for LAMMPS variables section
        myvars = LammpsVariables(regiondata(var1=...),ID='....',style='....')
    """
    name = "LammpsVariables"
    SECTIONS = ["VARIABLES"]
    position = 2
    role = "variable command definition"
    description = "variable name style args"
    userid = "variable"
    version = 0.1
    verbose = True

    # Definitions used in TEMPLATE
    DEFINITIONS = scriptdata(
                    ID = "${ID}",
                 style = "${style}",
          hasvariables = True
                    )

    # Template  (using % instead of # enables replacements)
    TEMPLATE = "% variables to be used for ${ID} ${style}"

    def __init__(self,VARIABLES=regiondata(),**userdefinitions):
        """ constructor of LammpsVariables """
        super().__init__(**userdefinitions)
        self.VARIABLES = VARIABLES

    # override >>
    def __rshift__(self,s):
        """ overload right  shift operator (keep only the last template) """
        if isinstance(s,script):
            dup = deepduplicate(self) # instead of duplicate (added 2023-08-11)
            dup.DEFINITIONS = dup.DEFINITIONS + s.DEFINITIONS
            dup.USER = dup.USER + s.USER
            if self.DEFINITIONS.hasvariables and s.DEFINITIONS.hasvariables:
                dup.VARIABLES = s.VARIABLES
            dup.TEMPLATE = s.TEMPLATE
            return dup
        else:
            raise TypeError("the second operand must a script object")


class LammpsCreate(LammpsGeneric):
    """ script for LAMMPS variables section """
    name = "LammpsCreate"
    SECTIONS = ["create_atoms"]
    position = 4
    role = "create_atoms command"
    description = "create_atoms type style args keyword values ..."
    userid = "create"
    version = 0.1
    verbose = True

    # Definitions used in TEMPLATE
    DEFINITIONS = scriptdata(
                    ID = "${ID}",
                 style = "${style}",
                 hasvariables = False
                    )

    # Template (using % instead of # enables replacements)
    TEMPLATE = """
% Create atoms of type ${beadtype} for ${ID} ${style} (https://docs.lammps.org/create_atoms.html)
create_atoms ${beadtype} region ${ID}
"""

class LammpsSetGroup(LammpsGeneric):
    """ script for LAMMPS set group section """
    name = "LammpsSetGroup"
    SECTIONS = ["set group"]
    position = 4
    role = "create_atoms command"
    description = "set group groupID type beadtype"
    userid = "setgroup"
    version = 0.1
    verbose = True

    # Definitions used in TEMPLATE
    DEFINITIONS = scriptdata(
                    ID = "${ID}",
               groupID = "$"+groupprefix+"${ID}", # freeze the interpretation,
          hasvariables = False
                    )

    # Template (using % instead of # enables replacements)
    TEMPLATE = """
% Reassign atom type to ${beadtype} for the group ${groupID} associated with region ${ID} (https://docs.lammps.org/set.html)
set group ${groupID} type ${beadtype}
"""

class LammpsMove(LammpsGeneric):
    """ script for LAMMPS variables section """
    name = "LammpsMove"
    SECTIONS = ["move_fix"]
    position = 6
    role = "move along a trajectory"
    description = "fix ID group-ID move style args keyword values ..."
    userid = "move"
    version = 0.2
    verbose = True

    # Definitions used in TEMPLATE
    DEFINITIONS = scriptdata(
                    ID = "${ID}",
                moveID = "$"+fixmoveprefix+"${ID}", # freeze the interpretation
               groupID = "$"+groupprefix+"${ID}", # freeze the interpretation
                 style = "${style}",
                  args = "${args}",
          hasvariables = False
                    )

    # Template (using % instead of # enables replacements)
    TEMPLATE = """
# Move atoms fix ID group-ID move style args keyword values (https://docs.lammps.org/fix_move.html)
% move_fix for group ${groupID} using ${style}
% prefix "g" added to ${ID} to indicate a group of atoms
% prefix "fm" added to ${ID} to indicate the ID of the fix move
fix ${moveID} ${groupID} move ${style} ${args}
"""


class LammpsRegion(LammpsGeneric):
    """ generic region based on script """
    name = "LammpsRegion"
    SECTIONS = ["REGION"]
    position = 3
    role = "region command definition"
    description = "region ID style args keyword arg"
    userid = "region"              # user name
    version = 0.1                  # version
    verbose = True

    # DEFINITIONS USED IN TEMPLATE
    DEFINITIONS = scriptdata(
                    ID = "${ID}",
                 style = "${style}",
                  args = "${args}",
                  side = "${side}",
                 units = "${units}",
                  move = "${move}",
                rotate = "${rotate}",
                  open = "${open}",
          hasvariables = False
                    )

    # Template  (using % instead of # enables replacements)
    TEMPLATE = """
% 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}
"""


class LammpsGroup(LammpsGeneric):
    """ generic group class based on script """
    name = "LammpsGroup"
    SECTIONS = ["GROUP"]
    position = 5
    role = "group command definition"
    description = "group ID region regionID"
    userid = "region"              # user name
    version = 0.2                  # version
    verbose = True

    # DEFINITIONS USED IN TEMPLATE
    DEFINITIONS = scriptdata(
                    ID = "${ID}",
               groupID = "$"+groupprefix+"${ID}", # freeze the interpretation
          countgroupID = "$count"+"${groupID}", # either using $
           grouptoshow = ["${groupID}"], # or []
                 hasvariables = False
                    )

    # Template  (using % instead of # enables replacements)
    TEMPLATE = """
% Create group ${groupID} region ${ID} (URL: https://docs.lammps.org/group.html)
group ${groupID} region ${ID}
variable ${countgroupID} equal count(${grouptoshow})
print "Number of atoms in ${groupID}: \${{countgroupID}}"
"""


class LammpsCollectionGroup(LammpsGeneric):
    """ Collection group class based on script """
    name = "LammpsCollection Group"
    SECTIONS = ["COLLECTIONGROUP"]
    position = 6
    role = "group command definition for a collection"
    description = "group ID union regionID1 regionID2..."
    userid = "collectionregion"              # user name
    version = 0.3                            # version
    verbose = True

    # DEFINITIONS USED IN TEMPLATE
    DEFINITIONS = scriptdata(
                    ID = "${ID}",
               groupID = "$"+groupprefix+"${ID}", # freeze the interpretation
          hasvariables = False
                    )

    # Template  (ID is spanned over all regionIDs)
    TEMPLATE = """
% Create group ${groupID} region ${ID} (URL: https://docs.lammps.org/group.html)
group ${groupID} union ${ID}
"""

class LammpsHeader(LammpsGeneric):
    """ generic header for pizza.region """
    name = "LammpsHeader"
    SECTIONS = ["HEADER"]
    position = 0
    role = "header for live view"
    description = "To be used with https://editor.lammps.org/"
    userid = "header"              # user name
    version = 0.1                  # version
    verbose = False

    # DEFINITIONS USED IN TEMPLATE
    DEFINITIONS = scriptdata(
                    width = 10,
                   height = 10,
                    depth = 10,
                    nbeads = 1,
            hasvariables = False
                    )

    # Template
    TEMPLATE = """
# --------------[    INIT   ]--------------
# assuming generic LJ units and style
units           ${live_units}
atom_style          ${live_atom_style}
lattice             ${live_lattice_style} ${live_lattice_scale}
# ------------------------------------------

# --------------[    B O X   ]--------------
variable        halfwidth equal ${width}/2
variable        halfheight equal ${height}/2
variable        halfdepth equal ${depth}/2
region box block -${halfwidth} ${halfwidth} -${halfheight} ${halfheight} -${halfdepth} ${halfdepth}
create_box      ${nbeads} box
# ------------------------------------------
"""

class LammpsHeaderInit(LammpsGeneric): # --- helper script ---
    """
    Generates an initialization header script for a pizza.region object in LAMMPS.

    This class constructs a LAMMPS header based on user-defined properties stored
    in `R.headersData` of the pizza.region object. Properties set to `None` or an
    empty string will be omitted from the script.

    Attributes:
        DEFINITIONS: Defines the parameters like dimension, units, boundary, etc.,
        that can be set in `R.headersData`.

    Methods:
        __init__(persistentfile=True, persistentfolder=None, **userdefinitions):
            Initializes the header script and sets up the `USER` attribute.

        generate_template():
            Creates the header template based on the provided `USER` definitions.

    Note: This class is primarily intended for internal use within the simulation setup.
    """
    name = "LammpsHeaderBox"
    SECTIONS = ["HEADER"]
    position = -2
    role = "initialization header for pizza.region"
    description = "helper method"
    userid = "headerinit"          # user name
    version = 0.1                  # version
    verbose = False

    # DEFINITIONS USED IN TEMPLATE
    # circular references (the variable is defined by its field in USER of class regiondata)
    # are not needed but this explicits the requirements.
    # All fields are stored in R.headersData with R a region object.
    # Use R.headersData.property = value to assign a value
    # Use R.headersData.property = None or "" to prevent the initialization of property
    DEFINITIONS = scriptdata(
                regionname = "${name}",
                 dimension = "${dimension}",
                     units = "${units}",
                  boundary = "${boundary}",
                atom_style = "${atom_style}",
               atom_modify = "${atom_modify}",
               comm_modify = "${comm_modify}",
              neigh_modify = "${neigh_modify}",
                    newton = "${newton}",
            hasvariables = False
                    )

    def __init__(self, persistentfile=True, persistentfolder=None, **userdefinitions):
        """Constructor adding instance definitions stored in USER."""
        super().__init__(persistentfile, persistentfolder, **userdefinitions)
        self.generate_template()

    def generate_template(self):
        """Generate the TEMPLATE based on USER definitions."""
        self.TEMPLATE = """
% --------------[ Initialization for <${name}:${boxid}>   ]--------------
    """
        self.TEMPLATE += '# set a parameter to None or "" to remove the definition\n'
        if self.USER.dimension:   self.TEMPLATE += "dimension    ${dimension}\n"
        if self.USER.units:       self.TEMPLATE += "units        ${units}\n"
        if self.USER.boundary:    self.TEMPLATE += "boundary     ${boundary}\n"
        if self.USER.atom_style:  self.TEMPLATE += "atom_style   ${atom_style}\n"
        if self.USER.atom_modify: self.TEMPLATE += "atom_modify  ${atom_modify}\n"
        if self.USER.comm_modify: self.TEMPLATE += "comm_modify  ${comm_modify}\n"
        if self.USER.neigh_modify:self.TEMPLATE += "neigh_modify ${neigh_modify}\n"
        if self.USER.newton:      self.TEMPLATE += "newton       ${newton}\n"
        self.TEMPLATE += "# ------------------------------------------\n"


class LammpsHeaderLattice(LammpsGeneric): # --- helper script ---
    """
        Lattice header for pizza.region

        Use R.headersData.property = value to assign a value
        with R a pizza.region object
    """
    name = "LammpsHeaderLattice"
    SECTIONS = ["HEADER"]
    position = 0
    role = "lattice header for pizza.region"
    description = "helper method"
    userid = "headerlattice"       # user name
    version = 0.1                  # version
    verbose = False

    # DEFINITIONS USED IN TEMPLATE
    # circular references (the variable is defined by its field in USER of class regiondata)
    # are not needed but this explicits the requirements.
    # All fields are stored in R.headersData with R a region object.
    # Use R.headersData.property = value to assign a value
    DEFINITIONS = scriptdata(
             lattice_style = "${lattice_style}",
             lattice_scale = "${lattice_scale}",
            hasvariables = False
                    )
    def __init__(self, persistentfile=True, persistentfolder=None, **userdefinitions):
        """Constructor adding instance definitions stored in USER."""
        super().__init__(persistentfile, persistentfolder, **userdefinitions)
        self.generate_template()

    def generate_template(self):
        """Generate the TEMPLATE based on USER definitions."""
        self.TEMPLATE = "\n% --------------[ Lattice for <${name}:${boxid}>, style=${lattice_style}, scale=${lattice_scale} ]--------------\n"
        if self.USER.lattice_spacing is None:
            self.TEMPLATE += "lattice ${lattice_style} ${lattice_scale}\n"
        else:
            self.TEMPLATE += "lattice ${lattice_style} ${lattice_scale} spacing ${lattice_spacing}\n"
        self.TEMPLATE += "# ------------------------------------------\n"


class LammpsHeaderBox(LammpsGeneric): # --- helper script ---
    """
        Box header for pizza.region

        Use R.headersData.property = value to assign a value
        with R a pizza.region object
    """
    name = "LammpsHeaderBox"
    SECTIONS = ["HEADER"]
    position = 0
    role = "box header for pizza.region"
    description = "helper method"
    userid = "headerbox"           # user name
    version = 0.1                  # version
    verbose = False

    # DEFINITIONS USED IN TEMPLATE
    # circular references (the variable is defined by its field in USER of class regiondata)
    # are not needed but this explicits the requirements.
    # All fields are stored in R.headersData with R a region object.
    # Use R.headersData.property = value to assign a value
    # Extra arguments
    #   ${boxid_arg} is by default "box"
    #   ${boxunits_arg} can be "", "units lattice", "units box"
    DEFINITIONS = scriptdata(
                      name = "${name}",
                      xmin = "${xmin}",
                      xmax = "${xmax}",
                      ymin = "${ymin}",
                      ymax = "${ymax}",
                      zmin = "${zmin}",
                      zmax = "${zmax}",
                    nbeads = "${nbeads}",
                     boxid = "${boxid}",
              boxunits_arg = "",     # default units
            hasvariables = False
                    )

    # Template
    TEMPLATE = """
% --------------[ Box for <${name}:${boxid}> incl. ${nbeads} bead types ]--------------
region ${boxid} block ${xmin} ${xmax} ${ymin} ${ymax} ${zmin} ${zmax} ${boxunits_arg}
create_box      ${nbeads} ${boxid}
# ------------------------------------------
"""

class LammpsHeaderMass(LammpsGeneric):
    """
    Mass assignment header for pizza.region.

    Use R.headersData.property = value to assign a value
    with R a pizza.region object.
    """
    name = "LammpsHeaderMass"
    SECTIONS = ["HEADER"]
    position = 2  # Positioned after other headers like Box and Lattice
    role = "mass assignment header for pizza.region"
    description = "Assigns masses to bead types based on nbeads and default mass."
    userid = "headermass"  # User identifier
    version = 0.1
    verbose = False

    # DEFINITIONS USED IN TEMPLATE
    # All fields are stored in R.headersData with R a region object.
    # Use R.headersData.property = value to assign a value.
    # Mass overrides are provided via the 'mass' keyword argument as a list or tuple.
    DEFINITIONS = scriptdata(
        nbeads="${nbeads}",  # these default values are not used
        mass="${mass}",      # but reported for records
        hasvariables=False
    )

    def __init__(self, persistentfile=True, persistentfolder=None, **userdefinitions):
        """
            Constructor adding instance definitions stored in USER.

            Parameters:
                persistentfile (bool, optional): Whether to use a persistent file. Defaults to True.
                persistentfolder (str, optional): Folder path for persistent files. Defaults to None.
                **userdefinitions: Arbitrary keyword arguments for user definitions.
                    - mass (list or tuple, optional): List or tuple to override masses for specific bead types.
                      Example: mass=[1.2, 1.0, 0.8] assigns mass 1.2 to bead type 1, 1.0 to bead type 2,
                      and 0.8 to bead type 3.
        """
        super().__init__(persistentfile, persistentfolder, **userdefinitions)
        self.generate_template()

    def generate_template(self):
        """
            Generate the TEMPLATE for mass assignments based on USER definitions.

            The method constructs mass assignments for each bead type. If `mass` overrides
            are provided as a list or tuple, it assigns the specified mass to the corresponding
            bead types. Otherwise, it uses the default `mass` value from `USER.headersData.mass`.
        """
        # Retrieve user-defined parameters
        nbeads = self.USER.nbeads
        mass = self.USER.mass
        # Validate mass
        if not isinstance(mass, (list, tuple)): mass = [mass]  # Convert single value to a list
        if len(mass) > nbeads:
            mass = mass[:nbeads]  # Truncate excess entries
        elif len(mass) < nbeads:
            last_mass = mass[-1]  # Repeat the last value for missing entries
            mass += [last_mass] * (nbeads - len(mass))
        # Initialize TEMPLATE with header comment
        self.TEMPLATE = "\n% --------------[ Mass Assignments for <${name}:${boxid}>" + f" (nbeads={nbeads}) " +" ]--------------\n"
        # Iterate over bead types and assign masses
        for bead_type in range(1, nbeads + 1):
            bead_mass = mass[bead_type - 1]
            if isinstance(bead_mass, str):
                # If mass is a string (e.g., formula), ensure proper formatting
                mass_str = f"({bead_mass})"
            else:
                # If mass is a numeric value, convert to string
                mass_str = f"{bead_mass}"
            self.TEMPLATE += f"mass {bead_type} {mass_str}\n"
        # Close the TEMPLATE with a comment
        self.TEMPLATE += "# ------------------------------------------\n"


class LammpsFooterPreview(LammpsGeneric): # --- helper script ---
    """
        Box header for pizza.region

        Use R.headersData.property = value to assign a value
        with R a pizza.region object
    """
    name = "LammpsFooterPreview"
    SECTIONS = ["Footer"]
    position = 0
    role = "box footer for pizza.region"
    description = "helper method"
    userid = "footerpreview"       # user name
    version = 0.1                  # version
    verbose = False

    # DEFINITIONS USED IN TEMPLATE
    # circular references (the variable is defined by its field in USER of class regiondata)
    # are not needed but this explicits the requirements.
    # All fields are stored in R.headersData with R a region object.
    # Use R.headersData.property = value to assign a value
    # Extra arguments
    #   ${boxid_arg} is by default "box"
    #   ${boxunits_arg} can be "", "units lattice", "units box"
    DEFINITIONS = scriptdata(
                filename = "${previewfilename}",
            hasvariables = False
                    )

    # Template
    TEMPLATE = """
% --------------[ Preview for <${name}:${boxid}> incl. ${nbeads} bead types ]--------------
% Output the initial geometry to a dump file "${previewfilename}" for visualization
dump initial_dump all custom 1 ${previewfilename} id type x y z
run 0
# ------------------------------------------
"""

class LammpsSpacefilling(LammpsGeneric):
    """ Spacefilling script: fill space with a block """
    name = "LammpsSpacefilling"
    SECTIONS = ["SPACEFILLING"]
    position = 1
    role = "fill space with fillingbeadtype atoms"
    description = 'fill the whole space (region "filledspace") with default atoms (beadtype)'
    userid = "spacefilling"              # user name
    version = 0.1                        # version
    verbose = False

    # DEFINITIONS USED IN TEMPLATE
    DEFINITIONS = scriptdata(
             fillingunits = "${fillingunits}",
             fillingwidth = "${fillingwidth}",
            fillingheight = "${fillingheight}",
             fillingdepth = "${fillingdepth}",
               fillingxlo = "-${fillingwidth}/2",
               fillingxhi = "${fillingwidth}/2",
               fillingylo = "-${fillingheight}/2",
               fillingyhi = "${fillingheight}/2",
               fillingzlo = "-${fillingdepth}/2",
               fillingzhi = "${fillingdepth}/2",
          fillingbeadtype = "${fillingbeadtype}",
             fillingstyle = "${block}",
             hasvariables = False
                    )

    # Template
    TEMPLATE = """
region filledspace ${fillingstyle} ${fillingxlo} ${fillingxhi} ${fillingylo} ${fillingyhi} ${fillingzlo} ${fillingzhi}
create_atoms ${fillingbeadtype} region filledspace
# ------------------------------------------
"""

class LammpsFooter(LammpsGeneric):
    """ generic header for pizza.region """
    name = "LammpsFooter"
    SECTIONS = ["FOOTER"]
    position = 1000
    role = "footer for live view"
    description = "To be used with https://editor.lammps.org/"
    userid = "footer"              # user name
    version = 0.1                  # version
    verbose = False

    # DEFINITIONS USED IN TEMPLATE
    DEFINITIONS = scriptdata(
                      run = 1,
             hasvariables = False
                    )

    # Template
    TEMPLATE = """
# --------------[  DYNAMICS  ]--------------
${mass}
velocity            all create 1.44 87287 loop geom
pair_style          lj/cut 2.5
${pair_coeff}
neighbor            0.3 bin
neigh_modify    delay 0 every 20 check no
fix                     1 all nve
run                     ${run}
# ------------------------------------------
"""

class coregeometry:
    """
        core geometry object
        (helper class for attributes, side,units, move, rotate, open)

        SECTIONS store scripts (variables, region and create for the geometry)
        USER = common USER definitions for the three scripts
        VARIABLES = variables definitions (used by variables only)
        update() propagate USER to the three scripts
        script returns SECTIONS as a pipescript
        do() generate the script

        Parameters to be used along scriptobject()
                 style
            forcefield
                 group
        They are stored SCRIPTOBJECT_USER

    """

    _version = "0.35"
    __custom_documentations__ = "pizza.region.coregeometry class"


    def __init__(self,USER=regiondata(),VARIABLES=regiondata(),
                 hasgroup=False, hasmove=False, spacefilling=False,
                 style="smd",
                 forcefield=rigidwall(),
                 group=[],
                 mass=1, density=1,
                 lattice_style="sc", lattice_scale=1, lattice_scale_siunits=1 # added on 2024-07-05
                 ):
        """
            constructor of the generic core geometry
                USER: any definitions requires by the geometry
           VARIABLES: variables used to define the geometry (to be used in LAMMPS)
           hasgroup, hasmove: flag to force the sections group and move
           SECTIONS: they must be PIZZA.script

           The flag spacefilling is true of the container of objects (class region) is filled with beads
        """
        self.USER = USER
        self.SECTIONS = {
            'variables': LammpsVariables(VARIABLES,**USER),
               'region': LammpsRegion(**USER),
               'create': LammpsCreate(**USER),
                'group': LammpsGroup(**USER),
             'setgroup': LammpsSetGroup(**USER),
                 'move': LammpsMove(**USER)
            }
        self.FLAGSECTIONS = {
            'variables': True,
               'region': True,
               'create': not spacefilling,
                'group': hasgroup,
             'setgroup': spacefilling,
                 'move': hasmove
            }
        self.spacefilling = spacefilling

        # add comptaibility with scriptobjects
        self.SCRIPTOBJECT_USER = {
                 'style': style,
            'forcefield': forcefield,
                 'group': group
            }
        # collect information from parent region
        self.mass = mass
        self.density = density
        self.lattice_style = lattice_style
        self.lattice_scale = lattice_scale
        self.lattice_scale_siunits = lattice_scale_siunits

    def update(self):
        """ update the USER content for all three scripts """
        if isinstance(self.SECTIONS["variables"],script):
            self.SECTIONS["variables"].USER += self.USER
        if isinstance(self.SECTIONS["region"],script):
            self.SECTIONS["region"].USER += self.USER
        if isinstance(self.SECTIONS["create"],script):
            self.SECTIONS["create"].USER += self.USER
        if isinstance(self.SECTIONS["group"],script):
            self.SECTIONS["group"].USER += self.USER
        if isinstance(self.SECTIONS["setgroup"],script):
            self.SECTIONS["setgroup"].USER += self.USER
        if isinstance(self.SECTIONS["move"],script):
            self.SECTIONS["move"].USER += self.USER


    def copy(self,beadtype=None,name=""):
        """ returns a copy of the graphical object """
        if self.alike != "mixed":
            dup = deepduplicate(self)
            if beadtype != None: # update beadtype
                dup.beadtype = beadtype
            if name != "": # update name
                dup.name = name
            return dup
        else:
            raise ValueError("collections cannot be copied, regenerate the collection instead")


    def creategroup(self):
        """  force the group creation in script """
        self.FLAGSECTIONS["group"] = True

    def setgroup(self):
        """  force the group creation in script """
        self.FLAGSECTIONS["setgroup"] = True

    def createmove(self):
        """  force the fix move creation in script """
        self.FLAGSECTIONS["move"] = True

    def removegroup(self):
        """  force the group creation in script """
        self.FLAGSECTIONS["group"] = False

    def removemove(self):
        """  force the fix move creation in script """
        self.FLAGSECTIONS["move"] = False

    def scriptobject(self, beadtype=None, name=None, fullname=None, group=None, style=None, forcefield=None, USER = scriptdata()):
        """
        Method to return a scriptobject based on region instead of an input file
        Syntax similar to script.scriptobject
        OBJ = scriptobject(...)
        Implemented properties:
            beadtype=1,2,...
            name="short name"
            fullname = "comprehensive name"
            style = "smd"
            forcefield = any valid forcefield instance (default = rigidwall())
        """
        # Set defaults using instance attributes if parameters are None
        if beadtype is None:
            beadtype = self.beadtype
        if name is None:
            name = f"{self.name} bead"
        if fullname is None:
            fullname = f"beads of type {self.beadtype} | object {self.name} of kind region.{self.kind}"
        if group is None:
            group = self.SCRIPTOBJECT_USER["group"]
        if style is None:
            style = self.SCRIPTOBJECT_USER["style"]
        if forcefield is None:
            style = self.SCRIPTOBJECT_USER["forcefield"]
        return scriptobject(
            beadtype=beadtype,
            name=name,
            fullname=fullname,
            style=style,
            group=group,
            filename=None,  # No need for a file
            USER = USER
        )

    @property
    def hasvariables(self):
        """ return the flag VARIABLES """
        return isinstance(self.SECTIONS["variables"],script) \
               and self.FLAGSECTIONS["variables"]

    @property
    def hasregion(self):
        """ return the flag REGION """
        return isinstance(self.SECTIONS["region"],script) \
               and self.FLAGSECTIONS["region"]

    @property
    def hascreate(self):
        """ return the flag CREATE """
        return isinstance(self.SECTIONS["create"],script) \
               and self.FLAGSECTIONS["create"] \
               and (not self.spacefilling)

    @property
    def hasgroup(self):
        """ return the flag GROUP """
        return isinstance(self.SECTIONS["group"],script) \
               and self.FLAGSECTIONS["group"]

    @property
    def hassetgroup(self):
        """ return the flag GROUP """
        return isinstance(self.SECTIONS["setgroup"],script) \
               and self.FLAGSECTIONS["setgroup"] \
               and self.hasgroup \
               and (not self.hascreate)

    @property
    def hasmove(self):
        """ return the flag MOVE """
        return isinstance(self.SECTIONS["move"],script) \
               and self.FLAGSECTIONS["move"]

    @property
    def isspacefilled(self):
        """ return the flag spacefilling """
        return isinstance(self.SECTIONS["spacefilling"],script) \
               and self.FLAGSECTIONS["spacefilling"]

    @property
    def flags(self):
        """ return a list of all flags that are currently set """
        flag_names = list(self.SECTIONS.keys())
        return [flag for flag in flag_names if getattr(self, f"has{flag}")]

    @property
    def shortflags(self):
        """ return a string made from the first letter of each set flag """
        return "".join([flag[0] for flag in self.flags])


    @property
    def VARIABLES(self):
        """ return variables """
        if isinstance(self.SECTIONS["variables"],script):
            return self.SECTIONS["variables"].VARIABLES
        else:
            v = regiondata()
            for i in range(len(self.SECTIONS["variables"].scripts)):
                v = v + self.SECTIONS["variables"].scripts[i].VARIABLES
            return v

    @property
    def script(self):
        """ generates a pipe script from sections """
        self.update()
        ptmp = self.SECTIONS["variables"] if self.hasvariables else None
        if self.hasregion:
            ptmp = self.SECTIONS["region"] if ptmp is None else ptmp | self.SECTIONS["region"]
        if self.hascreate:
            ptmp = self.SECTIONS["create"] if ptmp is None else ptmp | self.SECTIONS["create"]
        if self.hasgroup:
            ptmp = self.SECTIONS["group"] if ptmp is None else ptmp | self.SECTIONS["group"]
        if self.hassetgroup:
            ptmp = self.SECTIONS["setgroup"] if ptmp is None else ptmp | self.SECTIONS["setgroup"]
        if self.hasmove:
            ptmp = self.SECTIONS["move"] if ptmp is None else ptmp | self.SECTIONS["move"]
        return ptmp
        # before 2023-07-17
        #return self.SECTIONS["variables"] | self.SECTIONS["region"] | self.SECTIONS["create"]

    def do(self,printflag=False,verbosity=1):
        """ generates a script """
        p = self.script # intentional, force script before do(), comment added on 2023-07-17
        cmd = p.do(printflag=printflag,verbosity=verbosity)
        # if printflag: print(cmd)
        return cmd

    def __repr__(self):
        """ display method"""
        nVAR = len(self.VARIABLES)
        print("%s - %s object - beadtype=%d " % (self.name, self.kind,self.beadtype))
        if hasattr(self,"filename"): print(f'\tfilename: "{self.filename}"')
        if nVAR>0:
            print(f"\t<-- {nVAR} variables are defined -->")
            print(f"\tUse {self.name}.VARIABLES to see details and their evaluation")
            for k,v in self.VARIABLES.items():
                v0 = '"'+v+'"' if isinstance(v,str) else repr(v)
                print(wrap(k,"=",v0,20,40,80))
        print("\t<-- keyword arg -->")
        haskeys = False
        for k in ("side","move","units","rotate","open"):
            if k in self.USER:
                v = self.USER.getattr(k)
                if v != "":
                    print(wrap(k,":",v[1:],20,60,80))
                    haskeys = True
        if not haskeys: print(wrap("no keywords","<","from side|move|units|rotate|open",20,60,80))
        flags = self.flags
        if flags: print(f'defined scripts: {span(flags,sep=",")}',"\n")
        print("\n"+self.geometry) # added 2024-07-05
        return "%s object: %s (beadtype=%d)" % (self.kind,self.name,self.beadtype)


    # ~~~~ validator for region arguments (the implementation is specific and not generic as fix move ones)
    def sidearg(self,side):
        """
            Validation of side arguments for region command (https://docs.lammps.org/region.html)
            side value = in or out
              in = the region is inside the specified geometry
              out = the region is outside the specified geometry
        """
        prefix = "$"
        if side is None:
            return ""
        elif isinstance(side, str):
            side = side.lower()
            if side in ("in","out"):
                return f"{prefix} side {side}"
            elif side in ("","none"):
                return ""
            else:
                raise ValueError(f'the value of side: "{side}" is not recognized')
        else:
            raise ValueError('the parameter side can be "in|out|None"')

    def movearg(self,move):
        """
            Validation of move arguments for region command (https://docs.lammps.org/region.html)
            move args = v_x v_y v_z
              v_x,v_y,v_z = equal-style variables for x,y,z displacement of region over time (distance units)
        """
        prefix = "$"
        if move is None:
            return ""
        elif isinstance(move, str):
            move = move.lower()
            if move in("","none"):
                return ""
            else:
                return f"{prefix} move {move}"
        elif isinstance(move,(list,tuple)):
            if len(move)<3:
                print("NULL will be added to move")
            elif len(move)>3:
                print("move will be truncated to 3 elements")
            movevalid = ["NULL","NULL","NULL"]
            for i in range(min(3,len(move))):
                if isinstance(move[i],str):
                    if move[i].upper()!="NULL":
                        if prefix in move[i]:
                            # we assume a numeric result after evaluation
                            # Pizza variables will be evaluated
                            # formateval for the evaluation of ${}
                            # eval for residual expressions
                            movevalid[i] = round(eval(self.VARIABLES.formateval(move[i])),6)
                        else:
                            # we assume a variable (LAMMPS variable, not Pizza ones)
                            movevalid[i] = "v_" + move[i]
                elif not isinstance(move[i],(int,float)):
                    if (move[i] is not None):
                        raise TypeError("move values should be str, int or float")
            return f"{prefix} move {span(movevalid)}"
        else:
            raise TypeError("the parameter move should be a list or tuple")

    def unitsarg(self,units):
        """
            Validation for units arguments for region command (https://docs.lammps.org/region.html)
            units value = lattice or box
              lattice = the geometry is defined in lattice units
              box = the geometry is defined in simulation box units
        """
        prefix = "$"
        if units is None:
            return ""
        elif isinstance(units,str):
            units = units.lower()
            if units in ("lattice","box"):
                return f"{prefix} units {units}"
            elif (units=="") or (units=="none"):
                return ""
            else:
                raise ValueError(f'the value of side: "{units}" is not recognized')
        else:
            raise TypeError('the parameter units can be "lattice|box|None"')

    def rotatearg(self,rotate):
        """
            Validation of rotate arguments for region command (https://docs.lammps.org/region.html)
            rotate args = v_theta Px Py Pz Rx Ry Rz
              v_theta = equal-style variable for rotaton of region over time (in radians)
              Px,Py,Pz = origin for axis of rotation (distance units)
              Rx,Ry,Rz = axis of rotation vector
        """
        prefix = "$"
        if rotate is None:
            return ""
        elif isinstance(rotate, str):
            rotate = rotate.lower()
            if rotate in ("","none",None):
                return ""
            else:
                return f"{prefix} rotate {rotate}"
        elif isinstance(rotate,(list,tuple)):
            if len(rotate)<7:
                print("NULL will be added to rotate")
            elif len(rotate)>7:
                print("rotate will be truncated to 7 elements")
            rotatevalid = ["NULL"]*7
            for i in range(min(7,len(rotate))):
                if isinstance(rotate[i],str):
                    if rotate[i].upper()!="NULL":
                        if prefix in rotate[i]:
                            rotatevalid[i] = round(eval(self.VARIABLES.formateval(rotate[i])),6)
                        else:
                            rotatevalid[i] = rotate[i]
                elif not isinstance(rotate[i],(int,float)):
                    if (rotate[i] is not None):
                        raise TypeError("rotate values should be str, int or float")
            return f"{prefix} move {span(rotatevalid)}"
        else:
            raise TypeError("the parameter rotate should be a list or tuple")

    def openarg(self,open):
        """
            Validation of open arguments for region command (https://docs.lammps.org/region.html)
            open value = integer from 1-6 corresponding to face index (see below)
            The indices specified as part of the open keyword have the following meanings:

            For style block, indices 1-6 correspond to the xlo, xhi, ylo, yhi, zlo, zhi surfaces of the block.
            I.e. 1 is the yz plane at x = xlo, 2 is the yz-plane at x = xhi, 3 is the xz plane at y = ylo,
            4 is the xz plane at y = yhi, 5 is the xy plane at z = zlo, 6 is the xy plane at z = zhi).
            In the second-to-last example above, the region is a box open at both xy planes.

            For style prism, values 1-6 have the same mapping as for style block.
            I.e. in an untilted prism, open indices correspond to the xlo, xhi, ylo, yhi, zlo, zhi surfaces.

            For style cylinder, index 1 corresponds to the flat end cap at the low coordinate along the cylinder axis,
            index 2 corresponds to the high-coordinate flat end cap along the cylinder axis, and index 3 is the curved
            cylinder surface. For example, a cylinder region with open 1 open 2 keywords will be open at both ends
            (e.g. a section of pipe), regardless of the cylinder orientation.
        """
        prefix = "$"
        if open in ("","none",None):
            return ""
        elif isinstance(open, str):
            raise TypeError(" the parameter open should be an integer or a list/tuple of integers from 1-6")
        elif isinstance(open, int):
            if open in range(1,7):
                return f"{prefix} open {open}"
            else:
                raise TypeError(" open value should be integer from 1-6")
        elif isinstance(open, (list,tuple)):
            openvalid = [f"{prefix} open {i}" for i in range(1,7) if i in open]
            return f"$ {span(openvalid)}"
    # ~~~~ end validator for region arguments

    # ~~~~ validator for fix move arguments (implemented generically on 2023-07-17)
    def fixmoveargvalidator(self, argtype, arg, arglen):
        """
            Validation of arguments for fix move command in LAMMPS (https://docs.lammps.org/fix_move.html)

            LAMMPS syntax:
                fix ID group-ID move style args
                - linear args = Vx Vy Vz
                - wiggle args = Ax Ay Az period
                - rotate args = Px Py Pz Rx Ry Rz period
                - transrot args = Vx Vy Vz Px Py Pz Rx Ry Rz period
                - variable args = v_dx v_dy v_dz v_vx v_vy v_vz

            Args:
                argtype: Type of the argument (linear, wiggle, rotate, transrot, variable)
                arg: The argument to validate
                arglen: Expected length of the argument
        """
        prefix = "$"
        if arg in ("","none",None):
            return ""
        elif isinstance(arg,(list,tuple)):
            if len(arg) < arglen:
                print(f"NULL will be added to {argtype}")
            elif len(arg) > arglen:
                print(f"{argtype} will be truncated to {arglen} elements")
            argvalid = ["NULL"]*arglen
            for i in range(min(arglen,len(arg))):
                if isinstance(arg[i],str):
                    if arg[i].upper()!="NULL":
                        if prefix in arg[i]:
                            argvalid[i] = round(eval(self.VARIABLES.formateval(arg[i])),6)
                        else:
                            argvalid[i] = arg[i]
                elif not isinstance(arg[i],(int,float)):
                    if (arg[i] is not None):
                        raise TypeError(f"{argtype} values should be str, int or float")
            return f"{prefix} move {span(argvalid)}"
        else:
            raise TypeError(f"the parameter {argtype} should be a list or tuple")


    def fixmoveargs(self, linear=None, wiggle=None, rotate=None, transrot=None, variable=None):
        """
            Validates all arguments for fix move command in LAMMPS (https://docs.lammps.org/fix_move.html)
            the result is adictionary, all fixmove can be combined
        """
        argsdict = {
            "linear": [linear, 3],
            "wiggle": [wiggle, 4],
            "rotate": [rotate, 7],
            "transrot": [transrot, 10],
            "variable": [variable, 6]
        }

        for argtype, arginfo in argsdict.items():
            arg, arglen = arginfo
            if arg is not None:
                argsdict[argtype] = self.fixmoveargvalidator(argtype, arg, arglen)
        return argsdict


    def get_fixmovesyntax(self, argtype=None):
        """
        Returns the syntax for LAMMPS command, or detailed explanation for a specific argument type

        Args:
        argtype: Optional; Type of the argument (linear, wiggle, rotate, transrot, variable)
        """
        syntax = {
            "linear": "linear args = Vx Vy Vz\n"
                      "Vx,Vy,Vz = components of velocity vector (velocity units), any component can be specified as NULL",
            "wiggle": "wiggle args = Ax Ay Az period\n"
                       "Ax,Ay,Az = components of amplitude vector (distance units), any component can be specified as NULL\n"
                       "period = period of oscillation (time units)",
            "rotate": "rotate args = Px Py Pz Rx Ry Rz period\n"
                       "Px,Py,Pz = origin point of axis of rotation (distance units)\n"
                       "Rx,Ry,Rz = axis of rotation vector\n"
                       "period = period of rotation (time units)",
            "transrot": "transrot args = Vx Vy Vz Px Py Pz Rx Ry Rz period\n"
                        "Vx,Vy,Vz = components of velocity vector (velocity units)\n"
                        "Px,Py,Pz = origin point of axis of rotation (distance units)\n"
                        "Rx,Ry,Rz = axis of rotation vector\n"
                        "period = period of rotation (time units)",
            "variable": "variable args = v_dx v_dy v_dz v_vx v_vy v_vz\n"
                        "v_dx,v_dy,v_dz = 3 variable names that calculate x,y,z displacement as function of time, any component can be specified as NULL\n"
                        "v_vx,v_vy,v_vz = 3 variable names that calculate x,y,z velocity as function of time, any component can be specified as NULL",
        }

        base_syntax = (
            "fix ID group-ID move style args\n"
            " - linear args = Vx Vy Vz\n"
            " - wiggle args = Ax Ay Az period\n"
            " - rotate args = Px Py Pz Rx Ry Rz period\n"
            " - transrot args = Vx Vy Vz Px Py Pz Rx Ry Rz period\n"
            " - variable args = v_dx v_dy v_dz v_vx v_vy v_vz\n\n"
            'use get_movesyntax("movemethod") for details'
            "manual: https://docs.lammps.org/fix_move.html"
        )

        return syntax.get(argtype, base_syntax)

    # ~~~~ end validator for fix move arguments

    def __add__(self,C):
        """ overload addition ("+") operator """
        if isinstance(C,coregeometry):
            dup = deepduplicate(self)
            dup.name = cleanname(self.name) +"+"+ cleanname(C.name)
            dup.USER = dup.USER + C.USER
            dup.USER.ID = "$" + cleanname(self.USER.ID) +"+"+ cleanname(C.USER.ID)
            dup.SECTIONS["variables"] = dup.SECTIONS["variables"] + C.SECTIONS["variables"]
            dup.SECTIONS["region"] = dup.SECTIONS["region"] + C.SECTIONS["region"]
            dup.SECTIONS["create"] = dup.SECTIONS["create"] + C.SECTIONS["create"]
            dup.SECTIONS["group"] = dup.SECTIONS["group"] + C.SECTIONS["group"]
            dup.SECTIONS["move"] = dup.SECTIONS["move"] + C.SECTIONS["move"]
            dup.FLAGSECTIONS["variables"] = dup.FLAGSECTIONS["variables"] or C.FLAGSECTIONS["variables"]
            dup.FLAGSECTIONS["region"] = dup.FLAGSECTIONS["region"] or C.FLAGSECTIONS["region"]
            dup.FLAGSECTIONS["create"] = dup.FLAGSECTIONS["create"] or C.FLAGSECTIONS["create"]
            dup.FLAGSECTIONS["group"] = dup.FLAGSECTIONS["group"] or C.FLAGSECTIONS["group"]
            dup.FLAGSECTIONS["move"] = dup.FLAGSECTIONS["move"] or C.FLAGSECTIONS["move"]
            return dup
        raise TypeError("the second operand must a region.coregeometry object")

    def __iadd__(self,C):
        """ overload iaddition ("+=") operator """
        if isinstance(C,coregeometry):
            self.USER += C.USER
            self.SECTIONS["variables"] += C.SECTIONS["variables"]
            self.SECTIONS["region"] += C.SECTIONS["region"]
            self.SECTIONS["create"] += C.SECTIONS["create"]
            self.SECTIONS["group"] += C.SECTIONS["group"]
            self.SECTIONS["move"] += C.SECTIONS["move"]
            self.FLAGSECTIONS["variables"] = self.FLAGSECTIONS["variables"] or C.FLAGSECTIONS["variables"]
            self.FLAGSECTIONS["region"] = self.FLAGSECTIONS["region"] or C.FLAGSECTIONS["region"]
            self.FLAGSECTIONS["create"] = self.FLAGSECTIONS["create"] or C.FLAGSECTIONS["create"]
            self.FLAGSECTIONS["group"] = self.FLAGSECTIONS["group"] or C.FLAGSECTIONS["group"]
            self.FLAGSECTIONS["move"] = self.FLAGSECTIONS["move"] or C.FLAGSECTIONS["move"]
            return self
        raise TypeError("the operand must a region.coregeometry object")

    def __or__(self,C):
        """ overload | pipe """
        if isinstance(C,coregeometry):
            dup = deepduplicate(self)
            dup.name = cleanname(self.name) +"|"+ cleanname(C.name)
            dup.USER = dup.USER + C.USER
            dup.USER.ID = "$" + cleanname(self.USER.ID) +"|"+ cleanname(C.USER.ID)
            dup.SECTIONS["variables"] = dup.SECTIONS["variables"] | C.SECTIONS["variables"]
            dup.SECTIONS["region"] = dup.SECTIONS["region"] | C.SECTIONS["region"]
            dup.SECTIONS["create"] = dup.SECTIONS["create"] | C.SECTIONS["create"]
            dup.SECTIONS["group"] = dup.SECTIONS["group"] | C.SECTIONS["group"]
            dup.SECTIONS["move"] = dup.SECTIONS["move"] | C.SECTIONS["move"]
            self.FLAGSECTIONS["variables"] = self.FLAGSECTIONS["variables"] or C.FLAGSECTIONS["variables"]
            self.FLAGSECTIONS["region"] = self.FLAGSECTIONS["region"] or C.FLAGSECTIONS["region"]
            self.FLAGSECTIONS["create"] = self.FLAGSECTIONS["create"] or C.FLAGSECTIONS["create"]
            self.FLAGSECTIONS["group"] = self.FLAGSECTIONS["group"] or C.FLAGSECTIONS["group"]
            self.FLAGSECTIONS["move"] = self.FLAGSECTIONS["move"] or C.FLAGSECTIONS["move"]
            return dup
        raise TypeError("the second operand must a region.coregeometry object")

    # copy and deep copy methods for the class (required)
    def __getstate__(self):
        """ getstate for cooperative inheritance / duplication """
        return self.__dict__.copy()

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

    def __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)) # replace duplicatedeep by deepduplicate (OV: 2023-07-28)
        return copie

    # Return the number of atoms
    @property
    def natoms(self):
        """Calculate the number of beads based on density, mass, and volume"""
        if hasattr(self, 'volume'):
            try:
                volume_siunits = self.volume("si")
                voxel_volume_siunits = self.lattice_scale**3
                number_of_beads = volume_siunits / voxel_volume_siunits
                packing_factors = {
                    'sc': 1.0,
                    'fcc': 4.0,
                    'bcc': 2.0,
                    'hcp': 6.0,  # Approximate value, requires specific volume calculation for accuracy
                    'dia': 8.0,
                    'bco': 2.0,  # Assuming orthorhombic lattice similar to bcc
                    'fco': 4.0,  # Assuming orthorhombic lattice similar to fcc
                }
                packing_factor = packing_factors.get(self.lattice_style, 1.0)  # Default to simple cubic if unknown
                number_of_beads *= packing_factor
                return round(number_of_beads)
            except Exception as e:
                print(f"Error calculating number of beads: {e}")
                return None
        else:
            print("Volume attribute is missing.")
            return None

    # return parent region details
    @property
    def regiondetails(self):
        return "\n".join((
        f"\n--- | Region Details | ---",
        f"Name: {self.name}",
        f"Lattice Style: {self.lattice_style}",
        f"Lattice Scale: {self.lattice_scale}",
        f"Lattice Scale (SI units): {self.lattice_scale_siunits}",
        f"Volume: {self.volume()}",
        f"Volume (SI units): {self.volume('si')}",
        f"Number of Atoms: {self.natoms}","\n"
        ))


    # return geometry details (2024-07-04)
    @property
    def geometry(self):
        """Return the geometry details of the object."""
        details = self.regiondetails
        details += "\n--- | Geometry Details | ---\n"
        if hasattr(self.USER, 'geometry'):
            details += self.USER.geometry
        else:
            details = "No geometry available.\n"
        return details


class Block(coregeometry):
    """ Block class """

    def __init__(self,counter,index=None,subindex=None, mass=1, density=1,
                 lattice_style="sc",lattice_scale=1,lattice_scale_siunits=1,
                 hasgroup=False,hasmove=False,spacefilling=False,
                 style=None, group=None, forcefield=None, **variables):
        self.name = "block%03d" % counter[1]
        self.kind = "block"     # kind of object
        self.alike = "block"    # similar object for plotting
        self.beadtype = 1       # bead type
        self.index = counter[0] if index is None else index
        self.subindex = subindex
        self.mass = mass
        self.density = density

        # call the generic constructor
        super().__init__(
                USER = regiondata(style="$block"),
                VARIABLES = regiondata(**variables),
                hasgroup=hasgroup,hasmove=hasmove,spacefilling=spacefilling,
                mass=mass, density=density,
                lattice_style=lattice_style,
                lattice_scale=lattice_scale,
                lattice_scale_siunits=lattice_scale_siunits,
                style=style, group=group, forcefield=forcefield # script object properties
                )

    def volume(self,units=None):
        """Calculate the volume of the block based on USER.args"""
        #args = [xlo, xhi, ylo, yhi, zlo, zhi]
        try:
            # Extract the arguments from USER.args
            args = self.USER.args_siunits if units=="si" else self.USER.args
            xlo = float(args[0])
            xhi = float(args[1])
            ylo = float(args[2])
            yhi = float(args[3])
            zlo = float(args[4])
            zhi = float(args[5])

            # Calculate the dimensions of the block
            length = xhi - xlo
            width = yhi - ylo
            height = zhi - zlo

            # Calculate the volume of the block
            volume = length * width * height
            return volume
        except Exception as e:
            print(f"Error calculating volume: {e}")
            return None


class Cone(coregeometry):
    """ Cone class """

    def __init__(self,counter,index=None,subindex=None, mass=1, density=1,
                 lattice_style="sc",lattice_scale=1,lattice_scale_siunits=1,
                 hasgroup=False,hasmove=False,spacefilling=False,
                 style=None, group=None, forcefield=None, **variables):
        self.name = "cone%03d" % counter[1]
        self.kind = "cone"     # kind of object
        self.alike = "cone"    # similar object for plotting
        self.beadtype = 1      # bead type
        self.index = counter[0] if index is None else index
        self.subindex = subindex
        self.mass = mass
        self.density = density
        # call the generic constructor
        super().__init__(
                USER = regiondata(style="$cone"),
                VARIABLES = regiondata(**variables),
                hasgroup=hasgroup,hasmove=hasmove,spacefilling=spacefilling,
                mass=mass, density=density,
                lattice_style=lattice_style,
                lattice_scale=lattice_scale,
                lattice_scale_siunits=lattice_scale_siunits,
                style=style, group=group, forcefield=forcefield # script object properties
                )

    def volume(self,units=None):
        """Calculate the volume of the cone based on USER.args"""
        #args = [dim, c1, c2, radlo, radhi, lo, hi]
        try:
            # Extract the arguments from USER.args
            args = self.USER.args_siunits if units=="si" else self.USER.args
            radius_low = float(args[3])
            radius_high = float(args[4])
            lo = float(args[5])
            hi = float(args[6])
            # Calculate the height of the cone
            height = hi - lo
            # Calculate the volume of the cone (assuming a conical frustum if radii are different)
            if radius_low == radius_high:
                volume = (1/3) * 3.141592653589793 * (radius_low ** 2) * height
            else:
                volume = (1/3) * 3.141592653589793 * height * (radius_low ** 2 + radius_low * radius_high + radius_high ** 2)
            return volume
        except Exception as e:
            print(f"Error calculating volume: {e}")
            return None


class Cylinder(coregeometry):
    """ Cylinder class """
    def __init__(self,counter,index=None,subindex=None, mass=1, density=1,
                 lattice_style="sc",lattice_scale=1,lattice_scale_siunits=1,
                 hasgroup=False,hasmove=False,spacefilling=False,
                 style=None, group=None, forcefield=None, **variables):
        self.name = "cylinder%03d" % counter[1]
        self.kind = "cylinder"     # kind of object
        self.alike = "cylinder"    # similar object for plotting
        self.beadtype = 1          # bead type
        self.index = counter[0] if index is None else index
        self.subindex = subindex
        self.mass = mass
        self.density = density
        # call the generic constructor
        super().__init__(
                USER = regiondata(style="$cylinder"),
                VARIABLES = regiondata(**variables),
                hasgroup=hasgroup,hasmove=hasmove,spacefilling=spacefilling,
                mass=mass, density=density,
                lattice_style=lattice_style,
                lattice_scale=lattice_scale,
                lattice_scale_siunits=lattice_scale_siunits,
                style=style, group=group, forcefield=forcefield # script object properties
                )

    def volume(self,units=None):
        """Calculate the volume of the cylinder based on USER.args"""
        # args = [dim,c1,c2,radius,lo,hi]
        try:
            # Extract the arguments from USER.args
            args = self.USER.args_siunits if units=="si" else self.USER.args
            radius = float(args[3])
            lo = float(args[4])
            hi = float(args[5])
            # Calculate the height of the cylinder
            height = hi - lo
            # Calculate the volume of the cylinder
            volume = 3.141592653589793 * (radius ** 2) * height
            return volume
        except Exception as e:
            print(f"Error calculating volume: {e}")
            return None

class Ellipsoid(coregeometry):
    """ Ellipsoid class """

    def __init__(self,counter,index=None,subindex=None, mass=1, density=1,
                 lattice_style="sc",lattice_scale=1,lattice_scale_siunits=1,
                 hasgroup=False,hasmove=False,spacefilling=False,
                 style=None, group=None, forcefield=None, **variables):
        self.name = "ellipsoid%03d" % counter[1]
        self.kind = "ellipsoid"     # kind of object
        self.alike = "ellipsoid"    # similar object for plotting
        self.beadtype = 1           # bead type
        self.index = counter[0] if index is None else index
        self.subindex = subindex
        self.mass = mass
        self.density = density
        # call the generic constructor
        super().__init__(
                USER = regiondata(style="$ellipsoid"),
                VARIABLES = regiondata(**variables),
                hasgroup=hasgroup,hasmove=hasmove,spacefilling=spacefilling,
                mass=mass, density=density,
                lattice_style=lattice_style,
                lattice_scale=lattice_scale,
                lattice_scale_siunits=lattice_scale_siunits,
                style=style, group=group, forcefield=forcefield # script object properties
                )

    def volume(self,units=None):
        #args = [x, y, z, a, b, c]
        """Calculate the volume of the ellipsoid based on USER.args"""
        try:
            # Extract the arguments from USER.args
            args = self.USER.args_siunits if units=="si" else self.USER.args
            a = float(args[3])
            b = float(args[4])
            c = float(args[5])
            # Calculate the volume of the ellipsoid
            volume = (4/3) * 3.141592653589793 * a * b * c
            return volume
        except Exception as e:
            print(f"Error calculating volume: {e}")
            return None

class Plane(coregeometry):
    """ Plane class """

    def __init__(self,counter,index=None,subindex=None, mass=1, density=1,
                 lattice_style="sc",lattice_scale=1,lattice_scale_siunits=1,
                 hasgroup=False,hasmove=False,spacefilling=False,
                 style=None, group=None, forcefield=None, **variables):
        self.name = "plane%03d" % counter[1]
        self.kind = "plane"      # kind of object
        self.alike = "plane"     # similar object for plotting
        self.beadtype = 1       # bead type
        self.index = counter[0] if index is None else index
        self.subindex = subindex
        self.mass = mass
        self.density = density
        # call the generic constructor
        super().__init__(
                USER = regiondata(style="$plane"),
                VARIABLES = regiondata(**variables),
                hasgroup=hasgroup,hasmove=hasmove,spacefilling=spacefilling,
                style=style, group=group, forcefield=forcefield # script object properties
                )

    @property
    def volume(self,units=None):
        """Dummy method returning None for volume"""
        #args = [px, py, pz, nx, ny, nz]
        return None

class Prism(coregeometry):
    """ Prism class """

    def __init__(self,counter,index=None,subindex=None, mass=1, density=1,
                 lattice_style="sc",lattice_scale=1,lattice_scale_siunits=1,
                 hasgroup=False,hasmove=False,spacefilling=False,
                 style=None, group=None, forcefield=None, **variables):
        self.name = "prism%03d" % counter[1]
        self.kind = "prism"      # kind of object
        self.alike = "prism"     # similar object for plotting
        self.beadtype = 1       # bead type
        self.index = counter[0] if index is None else index
        self.subindex = subindex
        self.mass = mass
        self.density = density
        # call the generic constructor
        super().__init__(
                USER = regiondata(style="$prism"),
                VARIABLES = regiondata(**variables),
                hasgroup=hasgroup,hasmove=hasmove,spacefilling=spacefilling,
                mass=mass, density=density,
                lattice_style=lattice_style,
                lattice_scale=lattice_scale,
                lattice_scale_siunits=lattice_scale_siunits,
                style=style, group=group, forcefield=forcefield # script object properties
                )

    def volume(self,units=None):
        """Calculate the volume of the prism based on USER.args"""
        #args = [xlo, xhi, ylo, yhi, zlo, zhi, xy, xz, yz]
        try:
            # Extract the arguments from USER.args
            args = self.USER.args_siunits if units=="si" else self.USER.args
            xlo = float(args[0])
            xhi = float(args[1])
            ylo = float(args[2])
            yhi = float(args[3])
            zlo = float(args[4])
            zhi = float(args[5])
            # Calculate the dimensions of the prism
            length = xhi - xlo
            width = yhi - ylo
            height = zhi - zlo
            # Calculate the volume of the prism
            volume = length * width * height
            return volume
        except Exception as e:
            print(f"Error calculating volume: {e}")
            return None

class Sphere(coregeometry):
    """ Sphere class """

    def __init__(self,counter,index=None,subindex=None, mass=1, density=1,
                 lattice_style="sc",lattice_scale=1,lattice_scale_siunits=1,
                 hasgroup=False,hasmove=False,spacefilling=False,
                 style=None, group=None, forcefield=None, **variables):
        self.name = "sphere%03d" % counter[1]
        self.kind = "sphere"      # kind of object
        self.alike = "ellipsoid"     # similar object for plotting
        self.beadtype = 1       # bead type
        self.index = counter[0] if index is None else index
        self.subindex = subindex
        self.mass = mass
        self.density = density
        # call the generic constructor
        super().__init__(
                USER = regiondata(style="$sphere"),
                VARIABLES = regiondata(**variables),
                hasgroup=hasgroup,hasmove=hasmove,spacefilling=spacefilling,
                mass=mass, density=density,
                lattice_style=lattice_style,
                lattice_scale=lattice_scale,
                lattice_scale_siunits=lattice_scale_siunits,
                style=style, group=group, forcefield=forcefield # script object properties
                )

    def volume(self,units=None):
        """Calculate the volume of the sphere based on USER.args"""
        #args = [x, y, z, radius]
        try:
            # Extract the arguments from USER.args
            args = self.USER.args_siunits if units=="si" else self.USER.args
            radius = float(args[3])
            # Calculate the volume of the sphere
            volume = (4/3) * 3.141592653589793 * (radius ** 3)
            return volume
        except Exception as e:
            print(f"Error calculating volume: {e}")
            return None

class Union(coregeometry):
    """ Union class """

    def __init__(self,counter,index=None,subindex=None,
                 hasgroup=False,hasmove=False,spacefilling=False,**variables):
        self.name = "union%03d" % counter[1]
        self.kind = "union"      # kind of object
        self.alike = "operator"     # similar object for plotting
        self.beadtype = 1       # bead type
        self.index = counter[0] if index is None else index
        self.subindex = subindex
        # call the generic constructor
        super().__init__(
                USER = regiondata(style="$union"),
                VARIABLES = regiondata(**variables),
                hasgroup=hasgroup,hasmove=hasmove,spacefilling=spacefilling
                )

class Intersect(coregeometry):
    """ Intersect class """

    def __init__(self,counter,index=None,subindex=None,
                 hasgroup=False,hasmove=False,spacefilling=False,**variables):
        self.name = "intersect%03d" % counter[1]
        self.kind = "intersect"      # kind of object
        self.alike = "operator"     # similar object for plotting
        self.beadtype = 1       # bead type
        self.index = counter[0] if index is None else index
        self.subindex = subindex
        # call the generic constructor
        super().__init__(
                USER = regiondata(style="$intersect"),
                VARIABLES = regiondata(**variables),
                hasgroup=hasgroup,hasmove=hasmove,spacefilling=spacefilling
                )

class Evalgeometry(coregeometry):
    """ generic class to store evaluated objects with region.eval() """

    def __init__(self,counter,index=None,subindex=None,
                 hasgroup=False,hasmove=False,spacefilling=False):
        self.name = "eval%03d" % counter[1]
        self.kind = "eval"      # kind of object
        self.alike = "eval"     # similar object for plotting
        self.beadtype = 1       # bead type
        self.index = counter[0] if index is None else index
        self.subindex = subindex
        super().__init__(hasgroup=hasgroup,hasmove=hasmove,spacefilling=spacefilling)


class Collection:
    """
        Collection class (including many objects)
    """
    _version = "0.31"
    __custom_documentations__ = "pizza.region.Collection class"

    # CONSTRUCTOR
    def __init__(self,counter,
                 name=None,
                 index = None,
                 subindex = None,
                 hasgroup = False,
                 USER = regiondata()):
        if (name is None) or (name==""):
            self.name = "collect%03d" % counter[1]
        elif name in self:
            raise KeyError(f'the name "{name}" already exist')
        else:
            self.name = name
        if not isinstance(USER,regiondata):
            raise TypeError("USER should be a regiondata object")
        USER.groupID = "$"+self.name # the content is frozen
        USER.ID = ""
        self.USER = USER
        self.kind = "collection"    # kind of object
        self.alike = "mixed"        # similar object for plotting
        self.index = counter[0] if index is None else index
        self.subindex = counter[1]
        self.collection = regioncollection()
        self.SECTIONS = {
                'group': LammpsCollectionGroup(**USER)
        }
        self.FLAGSECTIONS = {"group": hasgroup}

    def update(self):
        """ update the USER content for the script """
        if isinstance(self.SECTIONS["group"],script):
            self.USER.ID = "$"\
                +span([groupprefix+x for x in self.list()]) # the content is frozen
            self.SECTIONS["group"].USER += self.USER

    def creategroup(self):
        """  force the group creation in script """
        for o in self.collection: o.creategroup()
        self.update()
        self.FLAGSECTIONS["group"] = True

    def removegroup(self,recursive=True):
        """  force the group creation in script """
        if recursive:
            for o in self.collection: o.removegroup()
        self.FLAGSECTIONS["group"] = False

    @property
    def hasgroup(self):
        """ return the flag hasgroup """
        return self.FLAGSECTIONS["group"]

    @property
    def flags(self):
        """ return a list of all flags that are currently set """
        flag_names = list(self.SECTIONS.keys())
        return [flag for flag in flag_names if getattr(self, f"has{flag}")]

    @property
    def shortflags(self):
        """ return a string made from the first letter of each set flag """
        return "".join([flag[0] for flag in self.flags])

    @property
    def script(self):
        """ generates a pipe script from SECTIONS """
        self.update()
        return self.SECTIONS["group"]

    def __repr__(self):
        keylengths = [len(key) for key in self.collection.keys()]
        width = max(10,max(keylengths)+2)
        fmt = "%%%ss:" % width
        line = ( fmt % ('-'*(width-2)) ) + ( '-'*(min(40,width*5)) )
        print(line,"  %s - %s object" % (self.name, self.kind), line,sep="\n")
        for key,value in self.collection.items():
            flags = "("+self.collection[key].shortflags+")" if self.collection[key].flags else "(no script)"
            print(fmt % key,value.kind,
                  '"%s"' % value.name," > ",flags)
        flags = self.flags
        if flags: print(line,f'defined scripts: {span(flags,sep=",")}',sep="\n")
        print(line)
        return "%s object: %s (beadtype=[%s])" % (self.kind,self.name,", ".join(map(str,self.beadtype)))

    # GET -----------------------------
    def get(self,name):
        """ returns the object """
        if name in self.collection:
            return self.collection.getattr(name)
        elif name in ["collection","hasgroup","flags","shortflags","script"]:
            return getattr(self,name)
        else:
            raise ValueError('the object "%s" does not exist, use list()' % name)

    # GETATTR --------------------------
    def __getattr__(self,key):
        """ get attribute override """
        return self.get(key)

    @property
    def beadtype(self):
        """ returns the beadtypes used in the collection """
        b = []
        for o in self.collection:
            if o.beadtype not in b:
                b.append(o.beadtype)
        if len(b)==0:
            return 1
        else:
            return b

    # GROUP -------------------------------
    def group(self):
        """ return the grouped coregeometry object """
        if len(self) == 0:return pipescript()
        # execute all objects
        for i in range(len(self)): self.collection[i].do()
        # concatenate all objects into a pipe script
        liste = [x.SECTIONS["variables"] for x in self.collection if x.hasvariables] + \
                [x.SECTIONS["region"]    for x in self.collection if x.hasregion] + \
                [x.SECTIONS["create"]    for x in self.collection if x.hascreate] + \
                [x.SECTIONS["group"]     for x in self.collection if x.hasgroup] + \
                [x.SECTIONS["setgroup"]  for x in self.collection if x.hassetgroup] + \
                [x.SECTIONS["move"]      for x in self.collection if x.hasmove]
        return pipescript.join(liste)

    # LEN ---------------------------------
    def __len__(self):
        """ return length of collection """
        return len(self.collection)

    # LIST ---------------------------------
    def list(self):
        """ return the list of objects """
        return self.collection.keys()



# %% region class (main class)
class region:
    """
    The `region` class represents a simulation region, centered at the origin (0, 0, 0) by default,
    and is characterized by its physical dimensions, properties, and boundary conditions. It supports
    setting up lattice structures, particle properties, and options for live previews.

    Attributes:
    ----------
    name : str, optional
        Name of the region (default is 'region container').

    dimension : int, optional
        Number of spatial dimensions for the simulation (either 2 or 3, default is 3).

    boundary : list of str or None, optional
        Boundary conditions for each dimension. If None, defaults to ["sm"] * dimension.
        Must be a list of length `dimension`, where "s" indicates shrink-wrapped, and "m" indicates a non-periodic boundary.

    nbeads : int, optional
        Number of beads in the region (default is 1).

    units : str, optional
        Units for the simulation box (default is "").

    Particle Properties:
    -------------------
    mass : float, optional
        Mass of particles in the region (default is 1).

    volume : float, optional
        Volume of the region (default is 1).

    density : float, optional
        Density of the region (default is 1).

    radius : float, optional
        Radius of the particles (default is 1.5).

    contactradius : float, optional
        Contact radius of the particles (default is 0.5).

    velocities : list of floats, optional
        Initial velocities of particles (default is [0, 0, 0]).

    forces : list of floats, optional
        External forces acting on the particles (default is [0, 0, 0]).

    Other Properties:
    ----------------
    filename : str, optional
        Name of the output file (default is an empty string, which will auto-generate a name based on the region name).

    index : int, optional
        Index or identifier for the region.

    run : int, optional
        Run configuration parameter (default is 1).

    Box Properties:
    ---------------
    center : list of floats, optional
        Center of the simulation box for coordinate scaling (default is [0, 0, 0]).

    width : float, optional
        Width of the region (default is 10).

    height : float, optional
        Height of the region (default is 10).

    depth : float, optional
        Depth of the region (default is 10).

    hasfixmove : bool, optional
        Indicates whether the region has a fixed movement (default is False).

    Spacefilling Design:
    -------------------
    spacefilling : bool, optional
        Indicates whether the design is space-filling (default is False).

    fillingbeadtype : int, optional
        Type of bead used for space filling (default is 1).

    Lattice Properties:
    ------------------
    regionunits : str, optional
        Defines the units of the region. Can be either "lattice" (default) or "si".

    separationdistance : float, optional
        Separation distance between atoms in SI units (default is 5e-6).

    lattice_scale : float, optional
        Scaling factor for the lattice, used mainly in visualization (default is 0.8442).

    lattice_spacing : list or None, optional
        Specifies the spacing between lattice points. If None, the default spacing is used. Can be a list of [dx, dy, dz].

    lattice_style : str, optional
        Specifies the lattice structure style (default is "fcc"). Accepts any LAMMPS valid style, e.g., "sc" for simple cubic.

    Atom Properties:
    ----------------
    atom_style : str, optional
        Defines the atom style for the region (default is "smd").

    atom_modify : list of str, optional
        LAMMPS command for atom modification (default is ["map", "array"]).

    comm_modify : list of str, optional
        LAMMPS command for communication modification (default is ["vel", "yes"]).

    neigh_modify : list, optional
        LAMMPS command for neighbor list modification (default is ["every", 10, "delay", 0, "check", "yes"]).

    newton : str, optional
        Specifies the Newton flag (default is "off").

    Live Preview:
    ------------
    live_units : str, optional
        Units for live preview (default is "lj", for Lennard-Jones units).

    live_atom_style : str, optional
        Atom style used specifically for live LAMMPS sessions (default is "atomic").

    livepreview_options : dict, optional
        Contains options for live preview. The dictionary includes 'static' (default: run = 1) and 'dynamic' (default: run = 100) options.

    Methods:
    -------
    __init__ :
        Constructor method to initialize all the attributes of the `region` class.
    """

    _version = "0.9997"
    __custom_documentations__ = "pizza.region.region class"

    # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
    #
    # CONSTRUCTOR METHOD
    #
    #
    # The constructor include
    #   the main container: objects (a dictionnary)
    #   several attributes covering current and future use of PIZZA.REGION()
    #
    # The original constructor is derived from PIZZA.RASTER() with
    # an intent to allow at some point some forward and backward port between
    # objects of the class PIZZA.RASTER() and PIZZA.REGION().
    #
    # The code will evolve according to the needs, please come back regularly.
    #
    # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-

    # CONSTRUCTOR ----------------------------
    def __init__(self,
                 # container properties
                 name="region container",
                 dimension = 3,
                 boundary = None,
                 nbeads=1,
                 units = "",

                 # particle properties
                 mass=1.0,
                 volume=1.0,
                 density=1.0,
                 radius=1.5,
                 contactradius=0.5,
                 velocities=[0.0,0.0,0.0],
                 forces=[0.0,0.0,0.0],

                 # other properties
                 filename="",
                 previewfilename="",
                 index = None,
                 run=1,

                 # Box lengths
                 center = [0.0,0.0,0.0],    # center of the box for coordinates scaling
                 width = 10.0,  # along x
                 height = 10.0, # along y
                 depth = 10.0,  # along z
                 hasfixmove = False, # by default no fix move

                 # Spacefilling design (added on 2023-08-10)
                 spacefilling = False,
                 fillingbeadtype = 1,

                 # Lattice properties
                 boxid = "box",             # default value for ${boxid_arg}
                 regionunits = "lattice",   # units ("lattice" or "si")
                 separationdistance = 5e-6, # SI units
                 lattice_scale = 0.8442,    # LJ units (for visualization)
                 lattice_spacing = None,    # lattice spacing is not used by default (set [dx dy dz] if needed)
                 lattice_style = "fcc" ,    # any valid lattice style accepted by LAMMPS (sc=simple cubic)

                 # Atom properties
                 atom_style = "smd",
                 atom_modify = ["map","array"],
                 comm_modify = ["vel","yes"],
                 neigh_modify = ["every",10,"delay",0,"check","yes"],
                 newton ="off",

                 # Live preview
                 live_units = "lj",         # units to be used ONLY with livelammps (https://andeplane.github.io/atomify/)
                 live_atom_style = "atomic",# atom style to be used ONLY with livelammps (https://andeplane.github.io/atomify/)

                 # livepreview options
                 livepreview_options = {
                     'static':{'run':1},
                     'dynamic':{'run':100}
                     },

                 # common flags (for scripting)
                 printflag = False,
                 verbose = True,
                 verbosity = None

                 ):
        """ constructor """
        self.name = name

        # Ensure dimension is an integer (must be 2 or 3 for LAMMPS)
        if not isinstance(dimension, int) or dimension not in (2, 3):
            raise ValueError("dimension must be either 2 or 3.")

        # Handle boundary input
        if boundary is None:
            boundary = ["sm"] * dimension
        elif isinstance(boundary, list):
            if len(boundary) != dimension:
                raise ValueError(f"The length of boundary ({len(boundary)}) must match the dimension ({dimension}).")
        else:
            raise ValueError("boundary must be a list of strings or None.")

        # Validate regionunits
        if regionunits not in ("lattice", "si"):
            raise ValueError("regionunits can only be 'lattice' or 'si'.")

        # Lattice scaling logic
        lattice_scale_siunits = lattice_scale if regionunits == "si" else separationdistance
        if lattice_scale_siunits is None or lattice_scale_siunits=="":
            lattice_scale_siunits = separationdistance
        if lattice_spacing == "":
            lattice_spacing = None
        elif isinstance(lattice_spacing, (int, float)):
            lattice_spacing = [lattice_spacing] * dimension
        elif isinstance(lattice_spacing, list):
            lattice_spacing = lattice_spacing + [lattice_spacing[-1]] * (dimension - len(lattice_spacing)) if len(lattice_spacing) < dimension else lattice_spacing[:dimension]

        # live data (updated 2024-07-04)
        live_lattice_scale = lattice_scale/separationdistance if regionunits == "si" else lattice_scale
        live_box_scale = 1/lattice_scale_siunits if regionunits == "si" else 1
        self.live = regiondata(nbeads=nbeads,
                               run=run,
                               width=math.ceil(width*live_box_scale),    # live_box_scale force lattice units for live visualization
                               height=math.ceil(height*live_box_scale),  # live_box_scale force lattice units for live visualization
                               depth=math.ceil(depth*live_box_scale),    # live_box_scale force lattice units for live visualization
                               live_units = "$"+live_units,
                               live_atom_style = "$"+live_atom_style,
                               live_lattice_style="$"+lattice_style,
                               live_lattice_scale=live_lattice_scale)
        # generic SMD properties (to be rescaled)
        self.volume = volume
        self.mass = mass
        self.density = density
        self.radius = radius
        self.contactradius = contactradius
        self.velocities = velocities
        self.forces = forces
        if filename == "":
            self.filename = f"region_{self.name}"
        else:
            self.filename = filename
        self.index = index
        self.objects = {}    # object container
        self.nobjects = 0    # total number of objects (alive)
        # count objects per type
        self.counter = {
                  "ellipsoid":0,
                  "block":0,
                  "sphere":0,
                  "cone":0,
                  "cylinder":0,
                  "prism":0,
                  "plane":0,
                  "union":0,
                  "intersect":0,
                  "eval":0,
                  "collection":0,
                  "all":0
            }
        # fix move flag
        self.hasfixmove = hasfixmove
        # livelammps (for live sessions) - added 2023-02-06
        self.livelammps = {
            "URL": livelammpsURL,
         "active": False,
           "file": None,
        "options": livepreview_options
            }
        # space filling  (added 2023-08-10)
        self.spacefilling = {
                   "flag": spacefilling,
           "fillingstyle": "$block",
        "fillingbeadtype": fillingbeadtype,
           "fillingwidth": width,
          "fillingheight": height,
           "fillingdepth": depth,
           "fillingunits": units
               }
        # region object units
        self.regionunits = regionunits
        # lattice
        self.units = units
        self.center = center
        self.separationdistance = separationdistance
        self.lattice_scale = lattice_scale
        self.lattice_spacing = lattice_spacing
        self.lattice_scale_siunits = lattice_scale_siunits
        self.lattice_style = lattice_style
        # headers for header scripts (added 2024-09-01)
        # geometry is assumed to be units set by ${boxunits_arg} (new standard 2024-11-26)
        self.headersData = headersRegiondata(
            # use $ and [] to prevent execution
            name = "$"+name,
            previewfilename = "$dump.initial."+self.filename if previewfilename=="" else "$"+previewfilename,
            # Initialize Lammps
            dimension = dimension,
            units = "$"+units,
            boundary = boundary,
            atom_style = "$" + atom_style,
            atom_modify = atom_modify,
            comm_modify = comm_modify,
            neigh_modify = neigh_modify,
            newton ="$" + newton,
            # Box (added 2024-11-26)
            boxid = "$"+boxid,
            boxunits_arg = "$units box" if regionunits=="si" else "", # standard on 2025-11-26
            # Lattice
            lattice_style = "$"+lattice_style,
            lattice_scale = lattice_scale,
            lattice_spacing = lattice_spacing,
            # Box
            xmin = -(width/2)  +center[0],
            xmax = +(width/2)   +center[0],
            ymin = -(height/2) +center[1],
            ymax = +(height/2) +center[1],
            zmin = -(depth/2)  +center[2],
            zmax = +(depth/2)  +center[2],
            nbeads = nbeads,
            mass = mass
            )
        self.printflag = printflag
        self.verbose = verbose if verbosity is None else verbosity>0
        self.verbosity = 0 if not verbose else verbosity

    # Method for coordinate/length scaling and translation including with formula embedded strings (updated 2024-07-03, fixed 2024-07-04)
    # Note that the translation is not fully required since the scaling applies also to full coordinates.
    # However, an implementation is provided for arbitrary offset.
    def scale_and_translate(self, value, offset=0):
        """
        Scale and translate a value or encapsulate the formula within a string.

        If self.regionunits is "si", only the offset is applied without scaling.
        Otherwise, scaling and translation are performed based on self.units ("si" or "lattice").

        Parameters:
            value (str or float): The value or formula to be scaled and translated.
            offset (float, optional): The offset to apply. Defaults to 0.

        Returns:
            str or float: The scaled and translated value or formula.
        """
        if self.regionunits == "si":
            # Only apply offset without scaling
            if isinstance(value, str):
                if offset:
                    translated = f"({value}) - {offset}"
                else:
                    translated = f"{value}"
                return translated
            else:
                if offset:
                    return value - offset
                else:
                    return value
        else:
            # Existing behavior based on self.units
            if isinstance(value, str):
                if offset:
                    translated = f"({value}) - {offset}"
                else:
                    translated = f"{value}"
                if self.units == "si":
                    return f"({translated}) / {self.lattice_scale} + {offset / self.lattice_scale}"
                else:  # "lattice"
                    return f"({translated}) * {self.lattice_scale} + {offset * self.lattice_scale}"
            else:
                if offset:
                    translated = value - offset
                else:
                    translated = value
                if self.units == "si":
                    return translated / self.lattice_scale + (offset / self.lattice_scale)
                else:  # "lattice"
                    return translated * self.lattice_scale + (offset * self.lattice_scale)



    # space filling attributes (cannot be changed)
    @property
    def isspacefilled(self):
        return self.spacefilling["flag"]

    @property
    def spacefillingbeadtype(self):
        return self.spacefilling["fillingbeadtype"]

    # total number of atoms in the region
    @property
    def natoms(self):
        """Count the total number of atoms in all objects within the region."""
        total_atoms = 0
        for eachobj in self:
            total_atoms += eachobj.natoms
        return total_atoms

    # details if the geometry of the region
    @property
    def geometry(self):
        """Display the dimensions and characteristics of the region and its objects."""
        details = f"Region: {self.name}\n"
        details += f"Total atoms: {self.natoms}\n"
        details += f"Span: width={self.spacefilling['fillingwidth']}, height={self.spacefilling['fillingheight']}, depth={self.spacefilling['fillingdepth']}\n"
        details += f"Box center: {self.center}\n"
        details += "Objects in the region:\n\n"
        for obj in self:
            details += "\n\n"+"-"*32+"\n"
            details += f"\nObject: {obj.name}\n"
            details += f"Type: {type(obj).__name__}\n"
            if hasattr(obj, 'geometry'):
                details += "\n"+"-"*32+"\n"
                details += obj.geometry
            else:
                details += "No geometry information available.\n"
        print(details)

    # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
    #
    # REGION.GEOMETRY constructors
    #
    #
    #   These methods create the 3D geometry objects (at least their code)
    #   A geometry is a collection of PIZZA.SCRIPT() objects (LAMMPS codelet)
    #   not a real geometry. The distinction between the creation (definition)
    #   and the execution (generation) of the gometry object existed already
    #   in PIZZA.RASTER(), but here they remain codelets as ONLY LAMMPS can
    #   generate the real object.
    #
    #   This level of abstraction makes it possible to mix PIZZA variables
    #   (USER, PIZZA.SCRIPT.USER, PIZZA.PIPESCRIPT.USER) with LAMMPS variables.
    #   The same object template can be used in different LAMMPS scripts with
    #   different values and without writting additional Python code.
    #   In shorts: USER fields store PIZZA.SCRIPT() like variables
    #              (they are compiled [statically] before LAMMPS execution)
    #              VARIABLES are defined in the generated LAMMPS script but
    #              created [dynamically] in LAMMPS. Note that these variables
    #              are defined explicitly with the LAMMPS variable command:
    #                   variable name style args ...
    #   Note: static variables can have only one single value for LAMMPS, which
    #         is known before LAMMPS is launched. The others can be assigned
    #         at runtime when LAMMPS is running.
    #   Example with complex definitions
    #       R.ellipsoid(0,0,0,1,1,1,name="E2",side="out",
    #                   move=["left","${up}*3",None],
    #                   up=0.1)
    #       R.E2.VARIABLES.left = '"swiggle(%s,%s,%s)"%(${a},${b},${c})'
    #       R.E2.VARIABLES.a="${b}-5"
    #       R.E2.VARIABLES.b=5
    #       R.E2.VARIABLES.c=100
    #
    #   The methods PIZZA.REGION.DO(), PIZZA.REGION.DOLIVE() compiles
    #   (statically) and generate the corresponding LAMMPS code. The static
    #   compiler accepts hybrid constructions where USER and VARIABLES are
    #   mixed. Any undefined variables will be assumed to be defined elsewhere
    #   in the LAMMPS code.
    #
    #  Current attributes of PIZZA.REGION.OBJECT cover current and future use
    #  of these objects and will allow some point some forward and backward
    #  compatibility with the same PIZZA.RASTER.OBJECT.
    #
    #
    #   References:
    #       https://docs.lammps.org/region.html
    #       https://docs.lammps.org/variable.html
    #       https://docs.lammps.org/create_atoms.html
    #       https://docs.lammps.org/create_box.html
    #
    #
    #   List of implemented geometries (shown here with the LAMMPS syntax)
    #       block args = xlo xhi ylo yhi zlo zhi
    #       cone args = dim c1 c2 radlo radhi lo hi
    #       cylinder args = dim c1 c2 radius lo hi
    #       ellipsoid args = x y z a b c <-- first method to be implemented
    #       plane args = px py pz nx ny n
    #       prism args = xlo xhi ylo yhi zlo zhi xy xz yz
    #       sphere args = x y z radius
    #       union args = N reg-ID1 reg-ID2 ..
    #       intersect args = N reg-ID1 reg-ID2 ...
    #
    # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-

    # BLOCK method ---------------------------
    # block args = xlo xhi ylo yhi zlo zhi
    # xlo,xhi,ylo,yhi,zlo,zhi = bounds of block in all dimensions (distance units)
    def block(self,xlo=-5,xhi=5,ylo=-5,yhi=5,zlo=-5,zhi=5,
                  name=None,beadtype=None,fake=False,
                  mass=None, density=None,
                  side=None,units=None,move=None,rotate=None,open=None,
                  index = None,subindex = None,
                  **variables
                  ):
        """
        creates a block region
            xlo,xhi,ylo,yhi,zlo,zhi = bounds of block in all dimensions (distance units)

            URL: https://docs.lammps.org/region.html

            Main properties = default value
                name = "block001"
            beadtype = 1
                fake = False (use True to test the execution)
     index, subindex = object index and subindex

            Extra properties
                side = "in|out"
               units = "lattice|box" ("box" is forced if regionunits=="si")
                move = "[${v1} ${v2} ${v3}]" or [v1,v2,v3] as a list
                       with v1,v2,v3 equal-style variables for x,y,z displacement
                       of region over time (distance units)
              rotate = string or 1x7 list (see move) coding for vtheta Px Py Pz Rx Ry Rz
                       vtheta = equal-style variable for rotation of region over time (in radians)
                       Px,Py,Pz = origin for axis of rotation (distance units)
                       Rx,Ry,Rz = axis of rotation vector
                open = integer from 1-6 corresponding to face index

            See examples for elliposid()
        """
        # prepare object creation
        kind = "block"
        if index is None: index = self.counter["all"]+1
        if subindex is None: subindex = self.counter[kind]+1
        units = "box" if self.regionunits=="si" and units is None else units  # force box units of regionunits=="si"
        # create the object B with B for block
        obj_mass = mass if mass is not None else self.mass
        obj_density = density if density is not None else self.density
        B = Block((self.counter["all"]+1,self.counter[kind]+1),
                      spacefilling=self.isspacefilled, # added on 2023-08-11
                      mass=obj_mass, density=obj_density, # added on 2024-06-14
                      index=index,subindex=subindex,
                      lattice_style=self.lattice_style,
                      lattice_scale=self.lattice_scale,
                      lattice_scale_siunits=self.lattice_scale_siunits,
                      **variables)
        # feed USER fields
        if name not in (None,""): B.name = name # object name (if not defined, default name will be used)
        if name in self.name: raise NameError('the name "%s" is already used' % name)
        if beadtype is not None: B.beadtype = beadtype # bead type (if not defined, default index will apply)
        B.USER.ID = "$"+B.name        # add $ to prevent its execution
        # geometry args (2024-07-04)  -------------------------------------
        args = [xlo, xhi, ylo, yhi, zlo, zhi]  # args = [....] as defined in the class Block
        args_scaled = [
            self.scale_and_translate(xlo, self.center[0]),
            self.scale_and_translate(xhi, self.center[0]),
            self.scale_and_translate(ylo, self.center[1]),
            self.scale_and_translate(yhi, self.center[1]),
            self.scale_and_translate(zlo, self.center[2]),
            self.scale_and_translate(zhi, self.center[2])
        ]
        if self.units == "si":
            B.USER.args = args_scaled
            B.USER.args_siunits = args
        else:  # "lattice"
            B.USER.args = args
            B.USER.args_siunits = args_scaled
        # geometry
        B.USER.geometry = (
            f"Block Region: {B.name}\n"
            "Coordinates: [xlo,xhi,ylo,yhi,zlo,zhi] = bounds of block in all dimensions"
            f"Coordinates (scaled): {B.USER.args}\n"
            f"Coordinates (SI units): {B.USER.args_siunits}\n"
            f"\talong x: [{B.USER.args[0]}, {B.USER.args[1]}]\n"
            f"\talong y: [{B.USER.args[2]}, {B.USER.args[3]}]\n"
            f"\talong z: [{B.USER.args[4]}, {B.USER.args[5]}]"
        )
        # other attributes  -------------------------------------
        B.USER.beadtype = B.beadtype  # beadtype to be used for create_atoms
        B.USER.side = B.sidearg(side) # extra parameter side
        B.USER.move = B.movearg(move) # move arg
        B.USER.units = B.unitsarg(units) # units
        B.USER.rotate = B.rotatearg(rotate) # rotate
        B.USER.open = B.openarg(open) # open
        # Create the object if not fake
        if fake:
            return B
        else:
            self.counter["all"] += 1
            self.counter[kind] +=1
            self.objects[name] = B
            self.nobjects += 1
            return None

    # CONE method ---------------------------
    # cone args = dim c1 c2 radlo radhi lo hi
    # dim = x or y or z = axis of cone
    # c1,c2 = coords of cone axis in other 2 dimensions (distance units)
    # radlo,radhi = cone radii at lo and hi end (distance units)
    # lo,hi = bounds of cone in dim (distance units)
    def cone(self,dim="z",c1=0,c2=0,radlo=2,radhi=5,lo=-10,hi=10,
                  name=None,beadtype=None,fake=False,
                  mass=None, density=None,
                  side=None,units=None,move=None,rotate=None,open=None,
                  index = None,subindex = None,
                  **variables
                  ):
        """
        creates a cone region
            dim = "x" or "y" or "z" = axis of the cone
                 note: USER, LAMMPS variables are not authorized here
            c1,c2 = coords of cone axis in other 2 dimensions (distance units)
            radlo,radhi = cone radii at lo and hi end (distance units)
            lo,hi = bounds of cone in dim (distance units)

            URL: https://docs.lammps.org/region.html

            Main properties = default value
                name = "cone001"
            beadtype = 1
                fake = False (use True to test the execution)
     index, subindex = object index and subindex

            Extra properties
                side = "in|out"
               units = "lattice|box" ("box" is forced if regionunits=="si")
                move = "[${v1} ${v2} ${v3}]" or [v1,v2,v3] as a list
                       with v1,v2,v3 equal-style variables for x,y,z displacement
                       of region over time (distance units)
              rotate = string or 1x7 list (see move) coding for vtheta Px Py Pz Rx Ry Rz
                       vtheta = equal-style variable for rotation of region over time (in radians)
                       Px,Py,Pz = origin for axis of rotation (distance units)
                       Rx,Ry,Rz = axis of rotation vector
                open = integer from 1-6 corresponding to face index

            See examples for elliposid()
        """
        # prepare object creation
        kind = "cone"
        if index is None: index = self.counter["all"]+1
        if subindex is None: subindex = self.counter[kind]+1
        units = "box" if self.regionunits=="si" and units is None else units  # force box units of regionunits=="si"
        # create the object C with C for cone
        obj_mass = mass if mass is not None else self.mass
        obj_density = density if density is not None else self.density
        C = Cone((self.counter["all"]+1,self.counter[kind]+1),
                      spacefilling=self.isspacefilled, # added on 2023-08-11
                      mass=obj_mass, density=obj_density, # added on 2024-06-14
                      index=index,subindex=subindex,
                      lattice_style=self.lattice_style,
                      lattice_scale=self.lattice_scale,
                      lattice_scale_siunits=self.lattice_scale_siunits,
                      **variables)
        # feed USER fields
        if name not in (None,""): C.name = name # object name (if not defined, default name will be used)
        if name in self.name: raise NameError('the name "%s" is already used' % name)
        if beadtype is not None: C.beadtype = beadtype # bead type (if not defined, default index will apply)
        C.USER.ID = "$"+C.name        # add $ to prevent its execution
        # geometry args (2024-07-04)  -------------------------------------
        args = [dim, c1, c2, radlo, radhi, lo, hi]  # args = [....] as defined in the class Cone
        if dim == "x":  # x-axis
            args_scaled = [
                dim,
                self.scale_and_translate(c1, self.center[1]),
                self.scale_and_translate(c2, self.center[2]),
                self.scale_and_translate(radlo, 0),
                self.scale_and_translate(radhi, 0),
                self.scale_and_translate(lo, self.center[0]),
                self.scale_and_translate(hi, self.center[0])
            ]
        elif dim == "y":  # y-axis
            args_scaled = [
                dim,
                self.scale_and_translate(c1, self.center[0]),
                self.scale_and_translate(c2, self.center[2]),
                self.scale_and_translate(radlo, 0),
                self.scale_and_translate(radhi, 0),
                self.scale_and_translate(lo, self.center[1]),
                self.scale_and_translate(hi, self.center[1])
            ]
        else:  # z-axis
            args_scaled = [
                dim,
                self.scale_and_translate(c1, self.center[0]),
                self.scale_and_translate(c2, self.center[1]),
                self.scale_and_translate(radlo, 0),
                self.scale_and_translate(radhi, 0),
                self.scale_and_translate(lo, self.center[2]),
                self.scale_and_translate(hi, self.center[2])
            ]

        if self.units == "si":
            C.USER.args = args_scaled
            C.USER.args_siunits = args
        else:  # "lattice"
            C.USER.args = args
            C.USER.args_siunits = args_scaled
        # geometry
        C.USER.geometry = (
            f"Cone Region: {C.name}\n"
            "Coordinates: [dim,c1,c2,radlo,radhi,lo,hi] = dimensions of cone\n"
            f"Coordinates (scaled): {C.USER.args}\n"
            f"Coordinates (SI units): {C.USER.args_siunits}\n"
            f"\tdim: {C.USER.args[0]}\n"
            f"\tc1: {C.USER.args[1]}\n"
            f"\tc2: {C.USER.args[2]}\n"
            f"\tradlo: {C.USER.args[3]}\n"
            f"\tradhi: {C.USER.args[4]}\n"
            f"\tlo: {C.USER.args[5]}\n"
            f"\thi: {C.USER.args[6]}"
        )
        # other attributes  -------------------------------------
        C.USER.beadtype = C.beadtype  # beadtype to be used for create_atoms
        C.USER.side = C.sidearg(side) # extra parameter side
        C.USER.move = C.movearg(move) # move arg
        C.USER.units = C.unitsarg(units) # units
        C.USER.rotate = C.rotatearg(rotate) # rotate
        C.USER.open = C.openarg(open) # open
        # Create the object if not fake
        if fake:
            return C
        else:
            self.counter["all"] += 1
            self.counter[kind] +=1
            self.objects[name] = C
            self.nobjects += 1
            return None

    # CYLINDER method ---------------------------
    # cylinder args = dim c1 c2 radius lo hi
    # dim = x or y or z = axis of cylinder
    # c1,c2 = coords of cylinder axis in other 2 dimensions (distance units)
    # radius = cylinder radius (distance units)
    # c1,c2, and radius can be a variable (see below)
    # lo,hi = bounds of cylinder in dim (distance units)
    def cylinder(self,dim="z",c1=0,c2=0,radius=4,lo=-10,hi=10,
                  name=None,beadtype=None,fake=False,
                  mass=None, density=None,
                  side=None,units=None,move=None,rotate=None,open=None,
                  index = None,subindex = None,
                  **variables
                  ):
        """
        creates a cylinder region
              dim = x or y or z = axis of cylinder
              c1,c2 = coords of cylinder axis in other 2 dimensions (distance units)
              radius = cylinder radius (distance units)
              c1,c2, and radius can be a LAMMPS variable
              lo,hi = bounds of cylinder in dim (distance units)

            URL: https://docs.lammps.org/region.html

            Main properties = default value
                name = "cylinder001"
            beadtype = 1
                fake = False (use True to test the execution)
     index, subindex = object index and subindex

            Extra properties
                side = "in|out"
               units = "lattice|box" ("box" is forced if regionunits=="si")
                move = "[${v1} ${v2} ${v3}]" or [v1,v2,v3] as a list
                       with v1,v2,v3 equal-style variables for x,y,z displacement
                       of region over time (distance units)
              rotate = string or 1x7 list (see move) coding for vtheta Px Py Pz Rx Ry Rz
                       vtheta = equal-style variable for rotation of region over time (in radians)
                       Px,Py,Pz = origin for axis of rotation (distance units)
                       Rx,Ry,Rz = axis of rotation vector
                open = integer from 1-6 corresponding to face index

            See examples for elliposid()
        """
        # prepare object creation
        kind = "cylinder"
        if index is None: index = self.counter["all"]+1
        if subindex is None: subindex = self.counter[kind]+1
        units = "box" if self.regionunits=="si" and units is None else units  # force box units of regionunits=="si"
        # create the object C with C for cylinder
        obj_mass = mass if mass is not None else self.mass
        obj_density = density if density is not None else self.density
        C = Cylinder((self.counter["all"]+1,self.counter[kind]+1),
                      spacefilling=self.isspacefilled, # added on 2023-08-11
                      mass=obj_mass, density=obj_density,
                      index=index,subindex=subindex,
                      lattice_style=self.lattice_style,
                      lattice_scale=self.lattice_scale,
                      lattice_scale_siunits=self.lattice_scale_siunits,
                      **variables)
        # feed USER fields
        if name not in (None,""): C.name = name # object name (if not defined, default name will be used)
        if name in self.name: raise NameError('the name "%s" is already used' % name)
        if beadtype is not None: C.beadtype = beadtype # bead type (if not defined, default index will apply)
        C.USER.ID = "$"+C.name        # add $ to prevent its execution
        # geometry args (2024-07-04)  -------------------------------------
        args = [dim, c1, c2, radius, lo, hi]  # args = [....] as defined in the class Cylinder
        if dim == "x":  # x-axis
            args_scaled = [
                dim,
                self.scale_and_translate(c1, self.center[1]),
                self.scale_and_translate(c2, self.center[2]),
                self.scale_and_translate(radius, 0),
                self.scale_and_translate(lo, self.center[0]),
                self.scale_and_translate(hi, self.center[0])
            ]
        elif dim == "y":  # y-axis
            args_scaled = [
                dim,
                self.scale_and_translate(c1, self.center[0]),
                self.scale_and_translate(c2, self.center[2]),
                self.scale_and_translate(radius, 0),
                self.scale_and_translate(lo, self.center[1]),
                self.scale_and_translate(hi, self.center[1])
            ]
        else:  # z-axis
            args_scaled = [
                dim,
                self.scale_and_translate(c1, self.center[0]),
                self.scale_and_translate(c2, self.center[1]),
                self.scale_and_translate(radius, 0),
                self.scale_and_translate(lo, self.center[2]),
                self.scale_and_translate(hi, self.center[2])
            ]
        if self.units == "si":
            C.USER.args = args_scaled
            C.USER.args_siunits = args
        else:  # "lattice"
            C.USER.args = args
            C.USER.args_siunits = args_scaled
        # geometry
        C.USER.geometry = (
            f"Cylinder Region: {C.name}\n"
            "Coordinates: [dim,c1,c2,radius,lo,hi] = dimensions of cylinder\n"
            f"Coordinates (scaled): {C.USER.args}\n"
            f"Coordinates (SI units): {C.USER.args_siunits}\n"
            f"\tdim: {C.USER.args[0]}\n"
            f"\tc1: {C.USER.args[1]}\n"
            f"\tc2: {C.USER.args[2]}\n"
            f"\tradius: {C.USER.args[3]}\n"
            f"\tlo: {C.USER.args[4]}\n"
            f"\thi: {C.USER.args[5]}"
        )
        # other attributes  -------------------------------------
        C.USER.beadtype = C.beadtype  # beadtype to be used for create_atoms
        C.USER.side = C.sidearg(side) # extra parameter side
        C.USER.move = C.movearg(move) # move arg
        C.USER.units = C.unitsarg(units) # units
        C.USER.rotate = C.rotatearg(rotate) # rotate
        C.USER.open = C.openarg(open) # open
        # Create the object if not fake
        if fake:
            return C
        else:
            self.counter["all"] += 1
            self.counter[kind] +=1
            self.objects[name] = C
            self.nobjects += 1
            return None

    # ELLIPSOID method ---------------------------
    # ellipsoid args = x y z a b c
    # x,y,z = center of ellipsoid (distance units)
    # a,b,c = half the length of the principal axes of the ellipsoid (distance units)
    # x,y,z,a,b,c can be variables
    def ellipsoid(self,x=0,y=0,z=0,a=5,b=3,c=2,
                  name=None,beadtype=None,fake=False,
                  mass=None, density=None,
                  side=None,units=None,move=None,rotate=None,open=None,
                  index = None,subindex = None,
                  **variables
                  ):
        """
        creates an ellipsoid region
            ellipsoid(x,y,z,a,b,c [,name=None,beadtype=None,property=value,...])
            x,y,z = center of ellipsoid (distance units)
            a,b,c = half the length of the principal axes of the ellipsoid (distance units)

            URL: https://docs.lammps.org/region.html

            Main properties = default value
                name = "ellipsoid001"
            beadtype = 1
                fake = False (use True to test the execution)
                index, subindex = object index and subindex

            Extra properties
                side = "in|out"
               units = "lattice|box" ("box" is forced if regionunits=="si")
                move = "[${v1} ${v2} ${v3}]" or [v1,v2,v3] as a list
                       with v1,v2,v3 equal-style variables for x,y,z displacement
                       of region over time (distance units)
              rotate = string or 1x7 list (see move) coding for vtheta Px Py Pz Rx Ry Rz
                       vtheta = equal-style variable for rotation of region over time (in radians)
                       Px,Py,Pz = origin for axis of rotation (distance units)
                       Rx,Ry,Rz = axis of rotation vector
                open = integer from 1-6 corresponding to face index


            Examples:
                # example with variables created either at creation or later
                    R = region(name="my region")
                    R.ellipsoid(0, 0, 0, 1, 1, 1,name="E1",toto=3)
                    repr(R.E1)
                    R.E1.VARIABLES.a=1
                    R.E1.VARIABLES.b=2
                    R.E1.VARIABLES.c="(${a},${b},100)"
                    R.E1.VARIABLES.d = '"%s%s" %("test",${c}) # note that test could be replaced by any function'
                # example with extra parameters
                    R.ellipsoid(0,0,0,1,1,1,name="E2",side="out",move=["left","${up}*3",None],up=0.1)
                    R.E2.VARIABLES.left = '"swiggle(%s,%s,%s)"%(${a},${b},${c})'
                    R.E2.VARIABLES.a="${b}-5"
                    R.E2.VARIABLES.b=5
                    R.E2.VARIABLES.c=100
        """
        # prepare object creation
        kind = "ellipsoid"
        if index is None: index = self.counter["all"]+1
        if subindex is None: subindex = self.counter[kind]+1
        units = "box" if self.regionunits=="si" and units is None else units  # force box units of regionunits=="si"
        # create the object E with E for Ellipsoid
        obj_mass = mass if mass is not None else self.mass
        obj_density = density if density is not None else self.density
        E = Ellipsoid((self.counter["all"]+1,self.counter[kind]+1),
                      spacefilling=self.isspacefilled, # added on 2023-08-11
                      mass=obj_mass, density=obj_density,
                      index=index,subindex=subindex,
                      lattice_style=self.lattice_style,
                      lattice_scale=self.lattice_scale,
                      lattice_scale_siunits=self.lattice_scale_siunits,
                      **variables)
        # feed USER fields
        if name not in (None,""): E.name = name # object name (if not defined, default name will be used)
        if name in self.name: raise NameError('the name "%s" is already used' % name)
        if beadtype is not None: E.beadtype = beadtype # bead type (if not defined, default index will apply)
        E.USER.ID = "$"+E.name        # add $ to prevent its execution
        # geometry args (2024-07-04)  -------------------------------------
        args = [x, y, z, a, b, c]  # args = [....] as defined in the class Ellipsoid
        args_scaled = [
            self.scale_and_translate(x, self.center[0]),
            self.scale_and_translate(y, self.center[1]),
            self.scale_and_translate(z, self.center[2]),
            self.scale_and_translate(a, 0),
            self.scale_and_translate(b, 0),
            self.scale_and_translate(c, 0)
        ]
        if self.units == "si":
            E.USER.args = args_scaled
            E.USER.args_siunits = args
        else:  # "lattice"
            E.USER.args = args
            E.USER.args_siunits = args_scaled
        # geometry
        E.USER.geometry = (
            f"Ellipsoid Region: {E.name}\n"
            "Coordinates: [x,y,z,a,b,c] = center and radii of ellipsoid\n"
            f"Coordinates (scaled): {E.USER.args}\n"
            f"Coordinates (SI units): {E.USER.args_siunits}\n"
            f"\tcenter: [{E.USER.args[0]}, {E.USER.args[1]}, {E.USER.args[2]}]\n"
            f"\ta: {E.USER.args[3]}\n"
            f"\tb: {E.USER.args[4]}\n"
            f"\tc: {E.USER.args[5]}"
        )
        # other attributes  -------------------------------------
        E.USER.beadtype = E.beadtype  # beadtype to be used for create_atoms
        E.USER.side = E.sidearg(side) # extra parameter side
        E.USER.move = E.movearg(move) # move arg
        E.USER.units = E.unitsarg(units) # units
        E.USER.rotate = E.rotatearg(rotate) # rotate
        E.USER.open = E.openarg(open) # open
        # Create the object if not fake
        if fake:
            return E
        else:
            self.counter["all"] += 1
            self.counter[kind] +=1
            self.objects[name] = E
            self.nobjects += 1
            return None

    # PLANE method ---------------------------
    # plane args = px py pz nx ny nz
    # px,py,pz = point on the plane (distance units)
    # nx,ny,nz = direction normal to plane (distance units)
    def plane(self,px=0,py=0,pz=0,nx=0,ny=0,nz=1,
                  name=None,beadtype=None,fake=False,
                  side=None,units=None,move=None,rotate=None,open=None,
                  index = None,subindex = None,
                  **variables
                  ):
        """
        creates a plane region
              px,py,pz = point on the plane (distance units)
              nx,ny,nz = direction normal to plane (distance units)

            URL: https://docs.lammps.org/region.html

            Main properties = default value
                name = "plane001"
            beadtype = 1
                fake = False (use True to test the execution)
     index, subindex = object index and subindex

            Extra properties
                side = "in|out"
               units = "lattice|box" ("box" is forced if regionunits=="si")
                move = "[${v1} ${v2} ${v3}]" or [v1,v2,v3] as a list
                       with v1,v2,v3 equal-style variables for x,y,z displacement
                       of region over time (distance units)
              rotate = string or 1x7 list (see move) coding for vtheta Px Py Pz Rx Ry Rz
                       vtheta = equal-style variable for rotation of region over time (in radians)
                       Px,Py,Pz = origin for axis of rotation (distance units)
                       Rx,Ry,Rz = axis of rotation vector
                open = integer from 1-6 corresponding to face index

            See examples for elliposid()
        """
        # prepare object creation
        kind = "plane"
        if index is None: index = self.counter["all"]+1
        if subindex is None: subindex = self.counter[kind]+1
        units = "box" if self.regionunits=="si" and units is None else units  # force box units of regionunits=="si"
        # create the object P with P for plane
        P = Plane((self.counter["all"]+1,self.counter[kind]+1),
                      spacefilling=self.isspacefilled, # added on 2023-08-11
                      mass=self.mass, density=self.density, # added on 2024-06-14
                      index=index,subindex=subindex,
                      lattice_style=self.lattice_style,
                      lattice_scale=self.lattice_scale,
                      lattice_scale_siunits=self.lattice_scale_siunits,
                      **variables)
        # feed USER fields
        if name not in (None,""): P.name = name # object name (if not defined, default name will be used)
        if name in self.name: raise NameError('the name "%s" is already used' % name)
        if beadtype is not None: P.beadtype = beadtype # bead type (if not defined, default index will apply)
        P.USER.ID = "$"+P.name        # add $ to prevent its execution
        # geometry args (2024-07-04) ---------------------------
        args = [px, py, pz, nx, ny, nz]  # args = [....] as defined in the class Plane
        args_scaled = [
            self.scale_and_translate(px, self.center[0]),
            self.scale_and_translate(py, self.center[1]),
            self.scale_and_translate(pz, self.center[2]),
            self.scale_and_translate(nx, 0),
            self.scale_and_translate(ny, 0),
            self.scale_and_translate(nz, 0)
        ]
        if self.units == "si":
            P.USER.args = args_scaled
            P.USER.args_siunits = args
        else:  # "lattice"
            P.USER.args = args
            P.USER.args_siunits = args_scaled
        # geometry
        P.USER.geometry = (
            f"Plane Region: {P.name}\n"
            "Coordinates: [px,py,pz,nx,ny,nz] = point and normal vector of plane\n"
            f"Coordinates (scaled): {P.USER.args}\n"
            f"Coordinates (SI units): {P.USER.args_siunits}\n"
            f"\tpoint: [{P.USER.args[0]}, {P.USER.args[1]}, {P.USER.args[2]}]\n"
            f"\tnormal: [{P.USER.args[3]}, {P.USER.args[4]}, {P.USER.args[5]}]"
            )
        # other attributes ---------------------------
        P.USER.beadtype = P.beadtype  # beadtype to be used for create_atoms
        P.USER.side = P.sidearg(side) # extra parameter side
        P.USER.move = P.movearg(move) # move arg
        P.USER.units = P.unitsarg(units) # units
        P.USER.rotate = P.rotatearg(rotate) # rotate
        P.USER.open = P.openarg(open) # open
        # Create the object if not fake
        if fake:
            return P
        else:
            self.counter["all"] += 1
            self.counter[kind] +=1
            self.objects[name] = P
            self.nobjects += 1
            return None

    # PRISM method ---------------------------
    # prism args = xlo xhi ylo yhi zlo zhi xy xz yz
    # xlo,xhi,ylo,yhi,zlo,zhi = bounds of untilted prism (distance units)
    # xy = distance to tilt y in x direction (distance units)
    # xz = distance to tilt z in x direction (distance units)
    # yz = distance to tilt z in y direction (distance units)
    def prism(self,xlo=-5,xhi=5,ylo=-5,yhi=5,zlo=-5,zhi=5,xy=1,xz=1,yz=1,
                  name=None,beadtype=None,fake=False,
                  mass=None, density=None,
                  side=None,units=None,move=None,rotate=None,open=None,
                  index = None,subindex = None,
                  **variables
                  ):
        """
        creates a prism region
            xlo,xhi,ylo,yhi,zlo,zhi = bounds of untilted prism (distance units)
            xy = distance to tilt y in x direction (distance units)
            xz = distance to tilt z in x direction (distance units)
            yz = distance to tilt z in y direction (distance units)

            URL: https://docs.lammps.org/region.html

            Main properties = default value
                name = "prism001"
            beadtype = 1
                fake = False (use True to test the execution)
     index, subindex = object index and subindex

            Extra properties
                side = "in|out"
               units = "lattice|box" ("box" is forced if regionunits=="si")
                move = "[${v1} ${v2} ${v3}]" or [v1,v2,v3] as a list
                       with v1,v2,v3 equal-style variables for x,y,z displacement
                       of region over time (distance units)
              rotate = string or 1x7 list (see move) coding for vtheta Px Py Pz Rx Ry Rz
                       vtheta = equal-style variable for rotation of region over time (in radians)
                       Px,Py,Pz = origin for axis of rotation (distance units)
                       Rx,Ry,Rz = axis of rotation vector
                open = integer from 1-6 corresponding to face index

            See examples for elliposid()
        """
        # prepare object creation
        kind = "prism"
        if index is None: index = self.counter["all"]+1
        if subindex is None: subindex = self.counter[kind]+1
        units = "box" if self.regionunits=="si" and units is None else units  # force box units of regionunits=="si"
        # create the object P with P for prism
        obj_mass = mass if mass is not None else self.mass
        obj_density = density if density is not None else self.density
        P = Prism((self.counter["all"]+1,self.counter[kind]+1),
                      spacefilling=self.isspacefilled, # added on 2023-08-11
                      mass=obj_mass, density=obj_density, # added on 2024-06-14
                      index=index,subindex=subindex,
                      lattice_style=self.lattice_style,
                      lattice_scale=self.lattice_scale,
                      lattice_scale_siunits=self.lattice_scale_siunits,
                      **variables)
        # feed USER fields
        if name not in (None,""): P.name = name # object name (if not defined, default name will be used)
        if name in self.name: raise NameError('the name "%s" is already used' % name)
        if beadtype is not None: P.beadtype = beadtype # bead type (if not defined, default index will apply)
        P.USER.ID = "$"+P.name        # add $ to prevent its execution
        # geometry args (2024-07-04) ---------------------------
        args = [xlo, xhi, ylo, yhi, zlo, zhi, xy, xz, yz]  # args = [....] as defined in the class Prism
        args_scaled = [
            self.scale_and_translate(xlo, self.center[0]),
            self.scale_and_translate(xhi, self.center[0]),
            self.scale_and_translate(ylo, self.center[1]),
            self.scale_and_translate(yhi, self.center[1]),
            self.scale_and_translate(zlo, self.center[2]),
            self.scale_and_translate(zhi, self.center[2]),
            self.scale_and_translate(xy, 0),
            self.scale_and_translate(xz, 0),
            self.scale_and_translate(yz, 0)
        ]
        if self.units == "si":
            P.USER.args = args_scaled
            P.USER.args_siunits = args
        else:  # "lattice"
            P.USER.args = args
            P.USER.args_siunits = args_scaled
        # geometry
        P.USER.geometry = (
            f"Prism Region: {P.name}\n"
            "Coordinates: [xlo,xhi,ylo,yhi,zlo,zhi,xy,xz,yz] = bounds and tilts of prism\n"
            f"Coordinates (scaled): {P.USER.args}\n"
            f"Coordinates (SI units): {P.USER.args_siunits}\n"
            f"\tbounds: [{P.USER.args[0]}, {P.USER.args[1]}, {P.USER.args[2]}, {P.USER.args[3]}, {P.USER.args[4]}, {P.USER.args[5]}]\n"
            f"\ttilts: [{P.USER.args[6]}, {P.USER.args[7]}, {P.USER.args[8]}]"
        )
        # other attributes ---------------------------
        P.USER.beadtype = P.beadtype  # beadtype to be used for create_atoms
        P.USER.side = P.sidearg(side) # extra parameter side
        P.USER.move = P.movearg(move) # move arg
        P.USER.units = P.unitsarg(units) # units
        P.USER.rotate = P.rotatearg(rotate) # rotate
        P.USER.open = P.openarg(open) # open
        # Create the object if not fake
        if fake:
            return P
        else:
            self.counter["all"] += 1
            self.counter[kind] +=1
            self.objects[name] = P
            self.nobjects += 1
            return None

    # SPHERE method ---------------------------
    # sphere args = x y z radius
    # x,y,z = center of sphere (distance units)
    # radius = radius of sphere (distance units)
    # x,y,z, and radius can be a variable (see below)
    def sphere(self,x=0,y=0,z=0,radius=3,
                  name=None,beadtype=None,fake=False,
                  mass=None, density=None,
                  side=None,units=None,move=None,rotate=None,open=None,
                  index = None,subindex = None,
                  **variables
                  ):
        """
        creates a sphere region
              x,y,z = center of sphere (distance units)
              radius = radius of sphere (distance units)
              x,y,z, and radius can be a variable

            URL: https://docs.lammps.org/region.html

            Main properties = default value
                name = "sphere001"
            beadtype = 1
                fake = False (use True to test the execution)
     index, subindex = object index and subindex

            Extra properties
                side = "in|out"
               units = "lattice|box" ("box" is forced if regionunits=="si")
                move = "[${v1} ${v2} ${v3}]" or [v1,v2,v3] as a list
                       with v1,v2,v3 equal-style variables for x,y,z displacement
                       of region over time (distance units)
              rotate = string or 1x7 list (see move) coding for vtheta Px Py Pz Rx Ry Rz
                       vtheta = equal-style variable for rotation of region over time (in radians)
                       Px,Py,Pz = origin for axis of rotation (distance units)
                       Rx,Ry,Rz = axis of rotation vector
                open = integer from 1-6 corresponding to face index

            See examples for elliposid()
        """
        # prepare object creation
        kind = "sphere"
        if index is None: index = self.counter["all"]+1
        if subindex is None: subindex = self.counter[kind]+1
        units = "box" if self.regionunits=="si" and units is None else units  # force box units of regionunits=="si"
        # create the object S with S for sphere
        obj_mass = mass if mass is not None else self.mass
        obj_density = density if density is not None else self.density
        S = Sphere((self.counter["all"]+1,self.counter[kind]+1),
                      spacefilling=self.isspacefilled, # added on 2023-08-11
                      mass=obj_mass, density=obj_density, # added on 2024-06-14
                      index=index,subindex=subindex,
                      lattice_style=self.lattice_style,
                      lattice_scale=self.lattice_scale,
                      lattice_scale_siunits=self.lattice_scale_siunits,
                      **variables)
        # feed USER fields
        if name not in (None,""): S.name = name # object name (if not defined, default name will be used)
        if name in self.name: raise NameError('the name "%s" is already used' % name)
        if beadtype is not None: S.beadtype = beadtype # bead type (if not defined, default index will apply)
        S.USER.ID = "$"+S.name        # add $ to prevent its execution
        # geometry args (2024-07-04) ---------------------------
        args = [x, y, z, radius]  # args = [....] as defined in the class Sphere
        args_scaled = [
            self.scale_and_translate(x, self.center[0]),
            self.scale_and_translate(y, self.center[1]),
            self.scale_and_translate(z, self.center[2]),
            self.scale_and_translate(radius, 0)
        ]
        if self.units == "si":
            S.USER.args = args_scaled
            S.USER.args_siunits = args
        else:  # "lattice"
            S.USER.args = args
            S.USER.args_siunits = args_scaled
        # geometry
        S.USER.geometry = (
            f"Sphere Region: {S.name}\n"
            "Coordinates: [x,y,z,radius] = center and radius of sphere\n"
            f"Coordinates (scaled): {S.USER.args}\n"
            f"Coordinates (SI units): {S.USER.args_siunits}\n"
            f"\tcenter: [{S.USER.args[0]}, {S.USER.args[1]}, {S.USER.args[2]}]\n"
            f"\tradius: {S.USER.args[3]}"
        )
        # other attributes ---------------------------
        S.USER.beadtype = S.beadtype  # beadtype to be used for create_atoms
        S.USER.side = S.sidearg(side) # extra parameter side
        S.USER.move = S.movearg(move) # move arg
        S.USER.units = S.unitsarg(units) # units
        S.USER.rotate = S.rotatearg(rotate) # rotate
        S.USER.open = S.openarg(open) # open
        # Create the object if not fake
        if fake:
            return S
        else:
            self.counter["all"] += 1
            self.counter[kind] +=1
            self.objects[name] = S
            self.nobjects += 1
            return None

    # UNION method ---------------------------
    # union args = N reg-ID1 reg-ID2
    def union(self,*regID,
              name=None,beadtype=1,fake=False,
              index = None,subindex = None,
              **variables):
        """
        creates a union region
              union("reg-ID1","reg-ID2",name="myname",beadtype=1,...)
              reg-ID1,reg-ID2, ... = IDs of regions to join together

            URL: https://docs.lammps.org/region.html

            Main properties = default value
                name = "union001"
            beadtype = 1
                fake = False (use True to test the execution)
     index, subindex = object index and subindex
        """
        kind = "union"
        if index is None: index = self.counter["all"]+1
        if subindex is None: subindex = self.counter[kind]+1
        # create the object U with U for union
        U = Union((self.counter["all"]+1,self.counter[kind]+1),
                      index=index,subindex=subindex,**variables)
        # feed USER fields
        if name not in (None,""): U.name = name # object name (if not defined, default name will be used)
        if name in self.name: raise NameError('the name "%s" is already used' % name)
        if beadtype is not None: U.beadtype = beadtype # bead type (if not defined, default index will apply)
        U.USER.ID = "$"+U.name        # add $ to prevent its execution
        U.USER.side, U.USER.move, U.USER.units, U.USER.rotate, U.USER.open = "","","","",""
        # build arguments based on regID
        nregID = len(regID)
        if nregID<2: raise ValueError('two objects must be given at least for an union')
        args = [None] # the number of arguments is not known yet
        validID = range(nregID)
        for ireg in validID:
            if isinstance(regID[ireg],int):
                if regID[ireg] in validID:
                    args.append(self.names[regID[ireg]])
                else:
                    raise IndexError(f"the index {regID[ireg]} exceeds the number of objects {len(self)}")
            elif isinstance(regID[ireg],str):
                if regID[ireg] in self:
                    args.append(regID[ireg])
                else:
                    raise KeyError(f'the object "{regID[ireg]}" does not exist')
            else:
                raise KeyError(f"the {ireg+1}th object should be given as a string or an index")
            # prevent the creation of atoms merged (avoid duplicates)
            self.objects[regID[ireg]].FLAGSECTIONS["create"] = False
        args[0] = len(regID)
        U.USER.args = args   # args = [....] as defined in the class Union
        # Create the object if not fake
        if fake:
            return U
        else:
            self.counter["all"] += 1
            self.counter[kind] +=1
            self.objects[name] = U
            self.nobjects += 1
            return None

    # UNION method ---------------------------
    # union args = N reg-ID1 reg-ID2
    def intersect(self,*regID,
              name=None,beadtype=1,fake=False,
              index = None,subindex = None,
              **variables):
        """
        creates an intersection region
              intersect("reg-ID1","reg-ID2",name="myname",beadtype=1,...)
              reg-ID1,reg-ID2, ... = IDs of regions to join together

            URL: https://docs.lammps.org/region.html

            Main properties = default value
                name = "intersect001"
            beadtype = 1
                fake = False (use True to test the execution)
     index, subindex = object index and subindex
        """
        kind = "intersect"
        if index is None: index = self.counter["all"]+1
        if subindex is None: subindex = self.counter[kind]+1
        # create the object I with I for intersect
        I = Intersect((self.counter["all"]+1,self.counter[kind]+1),
                      index=index,subindex=subindex,**variables)
        # feed USER fields
        if name not in (None,""): I.name = name # object name (if not defined, default name will be used)
        if name in self.name: raise NameError('the name "%s" is already used' % name)
        if beadtype is not None: I.beadtype = beadtype # bead type (if not defined, default index will apply)
        I.USER.ID = "$"+I.name        # add $ to prevent its execution
        I.USER.side, I.USER.move, I.USER.units, I.USER.rotate, I.USER.open = "","","","",""
        # build arguments based on regID
        nregID = len(regID)
        if nregID<2: raise ValueError('two objects must be given at least for an intersection')
        args = [None] # the number of arguments is not known yet
        validID = range(nregID)
        for ireg in validID:
            if isinstance(regID[ireg],int):
                if regID[ireg] in validID:
                    args.append(self.names[regID[ireg]])
                else:
                    raise IndexError(f"the index {regID[ireg]} exceeds the number of objects {len(self)}")
            elif isinstance(regID[ireg],str):
                if regID[ireg] in self:
                    args.append(regID[ireg])
                else:
                    raise KeyError(f'the object "{regID[ireg]}" does not exist')
            else:
                raise KeyError(f"the {ireg+1}th object should be given as a string or an index")
            # prevent the creation of atoms (avoid duplicates)
            self.objects[regID[ireg]].FLAGSECTIONS["create"] = False
        args[0] = len(regID)
        I.USER.args = args   # args = [....] as defined in the class Union
        # Create the object if not fake
        if fake:
            return I
        else:
            self.counter["all"] += 1
            self.counter[kind] +=1
            self.objects[name] = I
            self.nobjects += 1
            return None


    # Group method ---------------------------
    def group(self,obj,name=None,fake=False):
        pass


    # COLLECTION method ---------------------------
    def collection(self,*obj,name=None,beadtype=None,fake=False,
              index = None,subindex = None,
              **kwobj):
        kind = "collection"
        if index is None: index = self.counter["all"]+1
        if subindex is None: subindex = self.counter[kind]+1
        # create the object C with C for collection
        C = Collection((index,subindex))
        if name not in (None,""): C.name = name # object name (if not defined, default name will be used)
        if name in self.name: raise NameError('the name "%s" is already used' % name)
        if beadtype is not None: C.beadtype = beadtype # bead type (if not defined, default index will apply)
        # add objects
        C.collection = regioncollection(*obj,**kwobj)
        # apply modifications (beadtype, ismask)
        for o in C.collection.keys():
            tmp = C.collection.getattr(o)
            if beadtype != None: tmp.beadtype = beadtype
            C.collection.setattr(o,tmp)
        # Create the object if not fake
        if fake:
            return C
        else:
            self.counter["all"] += 1
            self.counter[kind] +=1
            self.objects[name] = C
            self.nobjects += 1
            return None

    def scatter(self,
                 E,
                 name="emulsion",
                 beadtype=None,
                 ):
        """


        Parameters
        ----------
        E : scatter or emulsion object
            codes for x,y,z and r.
        name : string, optional
            name of the collection. The default is "emulsion".
        beadtype : integer, optional
            for all objects. The default is 1.

        Raises
        ------
        TypeError
            Return an error of the object is not a scatter type.

        Returns
        -------
        None.

        """
        if isinstance(E,scatter):
            collect = {}
            for i in range(E.n):
                b = E.beadtype[i] if beadtype==None else beadtype
                nameobj = "glob%02d" % i
                collect[nameobj] = self.sphere(E.x[i],E.y[i],E.z[i],E.r[i],
                            name=nameobj,beadtype=b,fake=True)
            self.collection(**collect,name=name)
        else:
            raise TypeError("the first argument must be an emulsion object")



    # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
    #
    # LOW-LEVEL METHODS
    #
    #
    # Low-level methods to manipulate and operate region objects (e.g., R).
    # They implement essentially some Python standards with the following
    # shortcut: R[i] or R[objecti] and R.objecti and R.objects[objecti] are
    # the same ith object where R.objects is the original container
    # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-

    # repr() method ----------------------------
    def __repr__(self):
        """ display method """
        spacefillingstr = f"\n(space filled with beads of type {self.spacefillingbeadtype})" \
            if self.isspacefilled else ""
        print("-"*40)
        print('REGION container "%s" with %d objects %s\n(units="%s", lattice="%s", scale=%0.4g [m])' \
              % (self.name,self.nobjects,spacefillingstr,self.units,self.lattice_style,self.lattice_scale_siunits))
        if self.nobjects>0:
            names = self.names
            l = [len(n) for n in names]
            width = max(10,max(l)+2)
            fmt = "%%%ss:" % width
            for i in range(self.nobjects):
                flags = "("+self.objects[names[i]].shortflags+")" if self.objects[names[i]].flags else "(no script)"
                if isinstance(self.objects[names[i]],Collection):
                        print(fmt % names[i]," %s region (%d beadtypes)" % \
                              (self.objects[names[i]].kind,len(self.objects[names[i]].beadtype))," > ",flags)
                else:
                    print(fmt % names[i]," %s region (beadtype=%d)" % \
                          (self.objects[names[i]].kind,self.objects[names[i]].beadtype)," > ",flags)
            print(wrap("they are",":",", ".join(self.names),10,60,80))
        print("-"*40)
        return "REGION container %s with %d objects (%s)" % \
            (self.name,self.nobjects,",".join(self.names))

    # str() method ----------------------------
    def __str__(self):
        """ string representation of a region """
        return "REGION container %s with %d objects (%s)" % \
            (self.name,self.nobjects,",".join(self.names))

    # generic GET method ----------------------------
    def get(self,name):
        """ returns the object """
        if name in self.objects:
            return self.objects[name]
        else:
            raise NameError('the object "%s" does not exist, use list()' % name)

    # getattr() method ----------------------------
    def __getattr__(self,name):
        """ getattr attribute override """
        if (name in self.__dict__) or (name in protectedregionkeys):
            return self.__dict__[name] # higher precedence for root attributes
        if name in protectedregionkeys:
            return getattr(type(self), name).__get__(self) # for methods decorated as properties (@property)
        # Handle special cases like __wrapped__ explicitly
        if name == "__wrapped__":
            return None  # Default value or appropriate behavior
        # Leave legitimate __dunder__ attributes to the default mechanism
        if name.startswith("__") and name.endswith("__"):
            raise AttributeError(f"{type(self).__name__!r} object has no attribute {name!r}")
        # Default
        return self.get(name)

    # generic SET method ----------------------------
    def set(self,name,value):
        """ set field and value """
        if isinstance(value,list) and len(value)==0:
            if name not in self.objects:
                raise NameError('the object "%s" does not exist, use list()' % name)
            self.delete(name)
        elif isinstance(value,coregeometry):
            if name in self.objects: self.delete(name)
            if isinstance(value.SECTIONS,pipescript) or isinstance(value,Evalgeometry):
                self.eval(deepduplicate(value),name) # not a scalar
            else: # scalar
                self.objects[name] = deepduplicate(value)
                self.objects[name].name = name
                self.nobjects += 1
                self.counter["all"] += 1
                self.objects[name].index = self.counter["all"]
                self.counter[value.kind] += 1

    # setattr() method ----------------------------
    def __setattr__(self,name,value):
        """ setattr override """
        if name in protectedregionkeys: # do not forget to increment protectedregionkeys
            self.__dict__[name] = value # if not, you may enter in infinite loops
        else:
            self.set(name,value)

    # generic HASATTR method ----------------------------
    def hasattr(self,name):
        """ return true if the object exist """
        if not isinstance(name,str): raise TypeError("please provide a string")
        return name in self.objects

    # IN operator ----------------------------
    def __contains__(self,obj):
        """ in override """
        return self.hasattr(obj)

    # len() method ----------------------------
    def __len__(self):
        """ len method """
        return len(self.objects)

    # indexing [int] and ["str"] method ----------------------------
    def __getitem__(self,idx):
        """
            R[i] returns the ith element of the structure
            R[:4] returns a structure with the four first fields
            R[[1,3]] returns the second and fourth elements
        """
        if isinstance(idx,int):
            if idx<len(self):
                return self.get(self.names[idx])
            raise IndexError(f"the index should be comprised between 0 and {len(self)-1}")
        elif isinstance(idx,str):
            if idx in self:
                return self.get(idx)
            raise NameError(f'{idx} does not exist, use list() to list objects')
        elif isinstance(idx,list):
            pass
        elif isinstance(idx,slice):
            return self.__getitem__(self,list(range(*idx.indices(len(self)))))
        else:
            raise IndexError("not implemented yet")

    # duplication GET method based on DICT ----------------------------
    def __getstate__(self):
        """ getstate for cooperative inheritance / duplication """
        return self.__dict__.copy()

    # duplication SET method based on DICT ----------------------------
    def __setstate__(self,state):
        """ setstate for cooperative inheritance / duplication """
        self.__dict__.update(state)

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

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


    # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
    #
    # MIDDLE-LEVEL METHODS
    #
    #
    # These methods are specific to PIZZA.REGION() objects.
    # They bring useful methods for the user and developer.
    # Similar methods exist in PIZZA.RASTER()
    # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-

    # LIST method ----------------------------
    def list(self):
        """ list objects """
        fmt = "%%%ss:" % max(10,max([len(n) for n in self.names])+2)
        print('REGION container "%s" with %d objects' % (self.name,self.nobjects))
        for o in self.objects.keys():
            print(fmt % self.objects[o].name,"%-10s" % self.objects[o].kind,
                  "(beadtype=%d,object index=[%d,%d])" % \
                      (self.objects[o].beadtype,
                       self.objects[o].index,
                       self.objects[o].subindex))

    # NAMES method set as an attribute ----------------------------
    @property
    def names(self):
        """ return the names of objects sorted as index """
        namesunsorted=namessorted=list(self.objects.keys())
        nobj = len(namesunsorted)
        if nobj<1:
            return []
        elif nobj<2:
            return namessorted
        else:
            for iobj in range(nobj):
                namessorted[self.objects[namesunsorted[iobj]].index-1] = namesunsorted[iobj]
            return namessorted

    # NBEADS method set as an attribute
    @property
    def nbeads(self):
        "return the number of beadtypes used"
        if len(self)>0:
            guess = max(len(self.count()),self.live.nbeads)
            return guess+1 if self.isspacefilled else guess
        else:
            return self.live.nbeads

    # COUNT method
    def count(self):
        """ count objects by type """
        typlist = []
        for  o in self.names:
            if isinstance(self.objects[o].beadtype,list):
                typlist += self.objects[o].beadtype
            else:
                typlist.append(self.objects[o].beadtype)
        utypes = list(set(typlist))
        c = []
        for t in utypes:
            c.append((t,typlist.count(t)))
        return c

    # BEADTYPES property
    @property
    def beadtypes(self):
        """ list the beadtypes """
        return [ x[0] for x in self.count() ]

    # DELETE method
    def delete(self,name):
        """ delete object """
        if name in self.objects:
            kind = self.objects[name].kind
            del self.objects[name]
            self.nobjects -= 1
            self.counter[kind] -= 1
            self.counter["all"] -= 1
        else:
            raise NameError("%s does not exist (use list()) to list valid objects" % name)

    # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
    #
    # HIGH-LEVEL METHODS
    #
    #
    # These methods are connect PIZZA.REGION() objects with their equivalent
    # as PIZZA.SCRIPT() and PIZZA.PIPESCRIPT() objects and methods.
    #
    # They are essential to PIZZA.REGION(). They do not have equivalent in
    # PIZZA.RASTER(). They use extensively the methods attached to :
    #        PIZZA.REGION.LAMMPSGENERIC()
    #        PIZZA.REGION.COREGEOMETRY()
    #
    # Current real-time rendering relies on
    #   https://andeplane.github.io/atomify/
    # which gives better results than
    #   https://editor.lammps.org/
    # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-

    # EVALUATE algebraic operation on PIZZA.REGION() objects (operation on codes)
    def eval(self,expression,name=None,beadtype = None,
             fake=False,index = None,subindex = None):
        """
            evaluates (i.e, combine scripts) an expression combining objects
                R= region(name="my region")
                R.eval(o1+o2+...,name='obj')
                R.eval(o1|o2|...,name='obj')
            R.name will be the resulting object of class region.eval (region.coregeometry)
        """
        if not isinstance(expression, coregeometry): raise TypeError("the argument should be a region.coregeometry")
        # prepare object creation
        kind = "eval"
        self.counter["all"] += 1
        self.counter[kind] +=1
        if index is None: index = self.counter["all"]
        if subindex is None: subindex = self.counter[kind]
        # create the object E with E for Ellipsoid
        E = Evalgeometry((self.counter["all"],self.counter[kind]),
                      index=index,subindex=subindex)
        # link expression to E
        if beadtype is not None: E.beadtype = beadtype # bead type (if not defined, default index will apply)
        if name is None: name = expression.name
        if name in self.name: raise NameError('the name "%s" is already used' % name)
        E.name = name
        E.SECTIONS = expression.SECTIONS
        E.USER = expression.USER
        if isinstance(E.SECTIONS,pipescript):
            # set beadtypes for all sections and scripts in the pipeline
            for i in E.SECTIONS.keys():
                for j in range(len(E.SECTIONS[i])):
                    E.SECTIONS[i].USER[j].beadtype = E.beadtype
        E.USER.beadtype = beadtype
        # Create the object if not fake
        if fake:
            self.counter["all"] -= 1
            self.counter[kind] -= 1
            return E
        else:
            self.objects[name] = E
            self.nobjects += 1
            return None

    # PIPESCRIPT method generates a pipe for all objects and sections
    def pipescript(self,printflag=False,verbose=False,verbosity=0):
        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
        """ pipescript all objects in the region """
        if len(self)<1: return pipescript()
        # execute all objects
        for myobj in self:
            if not isinstance(myobj,Collection): myobj.do(printflag=printflag,verbosity=verbosity)
        # concatenate all objects into a pipe script
        # for collections, only group is accepted
        liste = [x.SECTIONS["variables"] for x in self if not isinstance(x,Collection) and x.hasvariables] + \
                [x.SECTIONS["region"]    for x in self if not isinstance(x,Collection) and x.hasregion] + \
                [x.SECTIONS["create"]    for x in self if not isinstance(x,Collection) and x.hascreate] + \
                [x.SECTIONS["group"]     for x in self if not isinstance(x,Collection) and x.hasgroup] + \
                [x.SECTIONS["setgroup"]  for x in self if not isinstance(x,Collection) and x.hassetgroup] + \
                [x.SECTIONS["move"]      for x in self if not isinstance(x,Collection) and x.hasmove]
        # add the objects within the collection
        for x in self:
            if isinstance(x,Collection): liste += x.group()
        # add the eventual group for the collection
        liste += [x.SECTIONS["group"] for x in self if isinstance(x,Collection) and x.hasgroup]
        # chain all scripts
        return pipescript.join(liste)

    # SCRIPT add header and footer to PIPECRIPT
    def script(self,live=False, printflag=None, verbose=None, verbosity=None):
        """ script all objects in the region """
        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
        s = self.pipescript(printflag=printflag,verbose=verbose,verbosity=verbosity).script(printflag=printflag,verbose=verbose,verbosity=verbosity)
        if self.isspacefilled:
            USERspacefilling =regiondata(**self.spacefilling)
            s = LammpsSpacefilling(**USERspacefilling)+s
        if live:
            beadtypes = self.beadtypes
            USER = regiondata(**self.live)
            USER.nbeads = self.nbeads
            USER.mass = "$"
            USER.pair_coeff = "$"
            # list beadtype and prepare  mass, pair_coeff
            beadtypes = [ x[0] for x in self.count() ]
            if self.isspacefilled and self.spacefillingbeadtype not in beadtypes:
                beadtypes = [self.spacefillingbeadtype]+beadtypes
            for b in beadtypes:
                USER.mass += livetemplate["mass"] % b +"\n"
                USER.pair_coeff += livetemplate["pair_coeff"] %(b,b) +"\n"
            for b1 in beadtypes:
                for b2 in beadtypes:
                    if b2>b1:
                        USER.pair_coeff += livetemplate["pair_coeff"] %(b1,b2) +"\n"
            livemode = "dynamic" if self.hasfixmove else "static"
            USER.run =self.livelammps["options"][livemode]["run"]
            s = LammpsHeader(**USER)+s+LammpsFooter(**USER)
        return s

    # SCRIPTHEADERS add header scripts for initializing script, lattice, box for region
    def scriptHeaders(self, what=["init", "lattice", "box"], pipescript=False, **userdefinitions):
        """
            Generate and return LAMMPS header scripts for initializing the simulation, defining the lattice,
            and specifying the simulation box for all region objects.

            Parameters:
            - what (list of str): Specifies which scripts to generate. Options are "init", "lattice", "box", "mass" and "preview".
                                  Multiple scripts can be generated by passing a list of these options.
                                  Default is ["init", "lattice", "box"].
            - pipescript (bool): If True, the generated scripts are combined with `|` instead of `+`. Default is False.

            Property/pair value
            - nbeads (int): Specifies the number of beads, overriding the default if larger than `self.nbeads`.
                            Default is 1.
            - mass (real value or list): Sets the mass for each bead, overrriding `self.mass`
                            Default is 1.0.


            Returns:
            - object: The combined header scripts as a single object.
                      Header values can be overridden by updating `self.headersData`.

            Raises:
            - Exception: If no valid script options are provided in `what`.

            Example usage:
                sRheader = R.scriptHeaders("box").do()  # Generate the box header script.
                sRallheaders = R.scriptHeaders(["init", "lattice", "box"])  # Generate all headers.

                Example usage without naming parameters:
                sRheader = R.scriptHeaders("box")  # "what" specified as "box", nbeads defaults to 1.

                Example of overriding values
                sRheader = R.scriptHeaders("lattice",lattice_style = "$sq")  # Generate the lattice header script with the overridden value.
        """
        # handle overrides
        USERregion = self.headersData + regiondata(**userdefinitions)
        # Fix singletons
        if not isinstance(what, list):
            what = [what]
        # Generate the initialization script
        scripts = []  # Store all generated script objects here
        if "init" in what:
            scripts.append(LammpsHeaderInit(**USERregion))
        # Generate the lattice script
        if "lattice" in what:
            scripts.append(LammpsHeaderLattice(**USERregion))
        # Generate the box script
        if "box" in what:
            scripts.append(LammpsHeaderBox(**USERregion))
            if self.isspacefilled:
                scripts.append(LammpsSpacefilling(**self.spacefilling))
        # Generate the mass script
        if "mass" in what:
            scripts.append(LammpsHeaderMass(**USERregion))
        # Generate the preview script
        if "preview" in what:
            scripts.append(LammpsFooterPreview(**USERregion))
        if not scripts:
            raise Exception('nothing to do (use: "init", "lattice", "box", "mass" or "preview" within [ ])')

        # Combine the scripts based on the pipescript flag
        combined_script = scripts[0]  # Initialize the combined script with the first element
        for script in scripts[1:]:
            if pipescript:
                # Combine scripts using the | operator, maintaining pipescript format
                combined_script = combined_script | script  # p_ab = s_a | s_b or p_ab = s_a | p_b
            else:
                # Combine scripts using the + operator, maintaining regular script format
                combined_script = combined_script + script  # s_ab = s_a + s_b
        return combined_script


    def pscriptHeaders(self, what=["init", "lattice", "box"], **userdefinitions):
        """
        Surrogate method for generating LAMMPS pipescript headers.
        Calls the `scriptHeaders` method with `pipescript=True`.

        Parameters:
        - what (list of str): Specifies which scripts to generate. Options are "init", "lattice", and "box".
                              Multiple scripts can be generated by passing a list of these options.
                              Default is ["init", "lattice", "box"].
        Property/pair value
        - nbeads (int): Specifies the number of beads, overriding the default if larger than `self.nbeads`.
                        Default is 1.
        - mass (real value or list): Sets the mass for each bead, overrriding `self.mass`
                        Default is 1.0.
        Returns:
        - object: The combined pipescript header scripts as a single object.
        """
        # Call scriptHeaders with pipescript=True
        return self.scriptHeaders(what=what, pipescript=True, **userdefinitions)


    # DO METHOD = main static compiler
    def do(self, printflag=False, verbosity=1):
        """ execute the entire script """
        return self.pipescript().do(printflag=printflag, verbosity=verbosity)

    # DOLIVE = fast code generation for online rendering
    def dolive(self):
        """
            execute the entire script for online testing
            see: https://editor.lammps.org/
        """
        self.livelammps["file"] = self.script(live=True).tmpwrite()
        if not self.livelammps["active"]:
            livelammps(self.livelammps["URL"],new=0)
            self.livelammps["active"] = True
        return self.livelammps["file"]

# %% scatter class and emulsion class
#    Simplified scatter and emulsion generator
#    generalized from its 2D version in raster.scatter and raster.emulsion
#    added on 2023-03-10

class scatter():
    """ generic top scatter class """
    def __init__(self):
        """
        The scatter class provides an easy constructor
        to distribute in space objects according to their
        positions x,y,z size r (radius) and beadtype.

        The class is used to derive emulsions.

        Returns
        -------
        None.
        """
        self.x = np.array([],dtype=int)
        self.y = np.array([],dtype=int)
        self.z = np.array([],dtype=int)
        self.r = np.array([],dtype=int)
        self.beadtype = []

    @property
    def n(self):
        return len(self.x)

    def pairdist(self,x,y,z):
        """ pair distance to the surface of all disks/spheres """
        if self.n==0:
            return np.Inf
        else:
            return np.sqrt((x-self.x)**2+(y-self.y)**2+(z-self.z)**2)-self.r


class emulsion(scatter):
    """ emulsion generator """

    def __init__(self, xmin=10, ymin=10, zmin=10, xmax=90, ymax=90, zmax=90,
                 maxtrials=1000, beadtype=1, forcedinsertion=True):
        """


        Parameters
        ----------
        The insertions are performed between xmin,ymin and xmax,ymax
        xmin : int64 or real, optional
            x left corner. The default is 10.
        ymin : int64 or real, optional
            y bottom corner. The default is 10.
        zmin : int64 or real, optional
            z bottom corner. The default is 10.
        xmax : int64 or real, optional
            x right corner. The default is 90.
        ymax : int64 or real, optional
            y top corner. The default is 90.
        zmax : int64 or real, optional
            z top corner. The default is 90.
        beadtype : default beadtype to apply if not precised at insertion
        maxtrials : integer, optional
            Maximum of attempts for an object. The default is 1000.
        forcedinsertion : logical, optional
            Set it to true to force the next insertion. The default is True.

        Returns
        -------
        None.

        """
        super().__init__()
        self.xmin, self.xmax, self.ymin, self.ymax, self.zmin, self.zmax = xmin, xmax, ymin, ymax, zmin, zmax
        self.lastinsertion = (None,None,None,None,None) # x,y,z,r, beadtype
        self.length = xmax-xmin
        self.width = ymax-ymin
        self.height = zmax-zmin
        self.defautbeadtype = beadtype
        self.maxtrials = maxtrials
        self.forcedinsertion = forcedinsertion

    def __repr__(self):
        print(f" Emulsion object\n\t{self.length}x{self.width}x{self.height} starting at x={self.xmin}, y={self.ymin}, z={self.zmin}")
        print(f"\tcontains {self.n} insertions")
        print("\tmaximum insertion trials:", self.maxtrials)
        print("\tforce next insertion if previous fails:", self.forcedinsertion)
        return f"emulsion with {self.n} insertions"


    def walldist(self,x,y,z):
        """ shortest distance to the wall """
        return min(abs(x-self.xmin),abs(y-self.ymin),abs(z-self.zmin),abs(x-self.xmax),abs(y-self.ymax),abs(z-self.zmax))

    def dist(self,x,y,z):
        """ shortest distance of the center (x,y) to the wall or any object"""
        return np.minimum(np.min(self.pairdist(x,y,z)),self.walldist(x,y,z))

    def accepted(self,x,y,z,r):
        """ acceptation criterion """
        return self.dist(x,y,z)>r

    def rand(self):
        """ random position x,y  """
        return  np.random.uniform(low=self.xmin,high=self.xmax), \
                np.random.uniform(low=self.ymin,high=self.ymax),\
                np.random.uniform(low=self.zmin,high=self.zmax)

    def setbeadtype(self,beadtype):
        """ set the default or the supplied beadtype  """
        if beadtype == None:
            self.beadtype.append(self.defautbeadtype)
            return self.defautbeadtype
        else:
            self.beadtype.append(beadtype)
            return beadtype

    def insertone(self,x=None,y=None,z=None,r=None,beadtype=None,overlap=False):
        """
            insert one object of radius r
            properties:
                x,y,z coordinates (if missing, picked randomly from uniform distribution)
                r radius (default = 2% of diagonal)
                beadtype (default = defautbeadtype)
                overlap = False (accept only if no overlap)
        """
        attempt, success = 0, False
        random = (x==None) or (y==None) or (z==None)
        if r==None:
            r = 0.02*np.sqrt(self.length**2+self.width**2+self.height**2)
        while not success and attempt<self.maxtrials:
            attempt += 1
            if random: x,y,z = self.rand()
            if overlap:
                success = True
            else:
                success = self.accepted(x,y,z,r)
        if success:
            self.x = np.append(self.x,x)
            self.y = np.append(self.y,y)
            self.z = np.append(self.z,z)
            self.r = np.append(self.r,r)
            b=self.setbeadtype(beadtype)
            self.lastinsertion = (x,y,z,r,b)
        return success

    def insertion(self,rlist,beadtype=None):
        """
            insert a list of objects
                nsuccess=insertion(rlist,beadtype=None)
                beadtype=b forces the value b
                if None, defaultbeadtype is used instead
        """
        rlist.sort(reverse=True)
        ntodo = len(rlist)
        n = nsuccess = 0
        stop = False
        while not stop:
            n += 1
            success = self.insertone(r=rlist[n-1],beadtype=beadtype)
            if success: nsuccess += 1
            stop = (n==ntodo) or (not success and not self.forcedinsertion)
        if nsuccess==ntodo:
            print(f"{nsuccess} objects inserted successfully")
        else:
            print(f"partial success: {nsuccess} of {ntodo} objects inserted")
        return nsuccess



# %% 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__':

    R = region(name="my region", mass=2, density=5)
    # Create a Block object using the block method of the region container with specific dimensions
    R.block(xlo=0, xhi=10, ylo=0, yhi=10, zlo=0, zhi=10, name="B1",mass=3)
    # Access the natoms property of the Block object
    print("Number of atoms in the block:", R.B1.natoms)

    # early example
    a=region(name="region A")
    b=region(name="region B")
    c = [a,b]
    # step 1
    R = region(name="my region")
    R.ellipsoid(0, 0, 0, 1, 1, 1,name="E1",toto=3)
    R
    repr(R.E1)
    R.E1.VARIABLES.a=1
    R.E1.VARIABLES.b=2
    R.E1.VARIABLES.c="(${a},${b},100)"
    R.E1.VARIABLES.d = '"%s%s" %("test",${c}) # note that test could be replaced by any function'
    R.E1
    code1 = R.E1.do()
    print(code1)
    # step 2
    R.ellipsoid(0,0,0,1,1,1,name="E2",side="out",move=["left","${up}*3",None],up=0.1)
    R.E2.VARIABLES.left = '"swiggle(%s,%s,%s)"%(${a},${b},${c})'
    R.E2.VARIABLES.a="${b}-5"
    R.E2.VARIABLES.b=5
    R.E2.VARIABLES.c=100
    code2 = R.E2.do()
    print(R)
    repr(R.E2)
    print(code2)
    print(R.names)
    R.list()

    # eval objects
    R.set('E3',R.E2)
    R.E3.beadtype = 2
    R.set('add',R.E1 + R.E2)
    R.addd2 = R.E1 + R.E2
    R.eval(R.E1 | R.E2,'E12')


    # How to manage pipelines
    print("\n","-"*20,"pipeline","-"*20)
    p = R.E2.script
    s = p.script() # first execution
    s = p.script() # do nothing
    s # check

    # reorganize scripts
    print("\n","-"*20,"change order","-"*20)
    p.clear() # undo executions first
    q = p[[0,2,1]]
    sq = q.script()
    print(q.do())

    # join sections
    liste = [x.SECTIONS["variables"] for x in R]
    pliste = pipescript.join(liste)


    # Example closer to production
    P = region(name="live test",width = 20)
    P.ellipsoid(0, 0, 0, "${Ra}", "${Rb}", "${Rc}",
              name="E1", Ra=5,Rb=2,Rc=3)
    P.sphere(7,0,0,radius="${R}",name = "S1", R=2)
    cmd = P.do()
    print(cmd)
    #outputfile = P.dolive()

    # EXAMPLE: gel compression
    scale = 1
    name = ['top','food','tongue','bottom']
    radius = [10,5,8,10]
    height = [1,4,3,1]
    spacer = 2 * scale
    radius = [r*scale for r in radius]
    height = [h*scale for h in height]
    position_original = [spacer+height[1]+height[2]+height[3],
                          height[2]+height[3],
                          height[3],
                          0]
    beadtype = [1,2,3,1]
    total_height = sum(height) +spacer
    position = [x-total_height/2 for x in position_original]
    B = region(name = 'region container',
                width=2*max(radius),
                height=total_height,
                depth=2*max(radius))
    for i in range(len(name)):
        B.cylinder(name = name[i],
                    c1=0,
                    c2=0,
                    radius=radius[i],
                    lo=position[i],
                    hi=position[i]+height[i],
                    beadtype=beadtype[i])
    B.dolive()

    # Draft for workshop
    sB = B.do()
    b1 = B[0].scriptobject()
    b2 = B[1].scriptobject()
    b3 = B[2].scriptobject()
    b4 = B[3].scriptobject()
    collection = b1 + b2 + b3 + b4;

    # # emulsion example
    scale = 1 # tested up to scale = 10 to reach million of beads
    mag = 3
    e = emulsion(xmin=-5*mag, ymin=-5*mag, zmin=-5*mag,xmax=5*mag, ymax=5*mag, zmax=5*mag)
    e.insertion([2,2,2,1,1.6,1.2,1.4,1.3],beadtype=3)
    e.insertion([0.6,0.3,2,1.5,1.5,1,2,1.2,1.1,1.3],beadtype=1)
    e.insertion([3,1,2,2,4,1,1.2,2,2.5,1.2,1.4,1.6,1.7],beadtype=2)
    e.insertion([3,1,2,2,4,1,5.2,2,4.5,1.2,1.4,1.6,1.7],beadtype=4)

    # b = region()
    # a = region()
    # a.sphere(1,1,1,1,name='sphere1')
    # a.sphere(1,2,2,1,name='sphere2')
    # b.collection(a, name='acollection')

    C = region(name='cregion',width=11*mag,height=11*mag,depth=11*mag)
    C.scatter(e)
    C.script()
    g = C.emulsion.group()
    C.dolive()


    # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    #             F O R   P R O D U C T I O N
    # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    # History: 2024-07-04 (first version), 2024-07-29 (update), 2024-09-01 (community, per request)

    """
=== [  S Y N O P S I S  ] ===
This script provides a detailed example of simulating gel compression using cylindrical objects
within a defined region, employing SI units. The example is designed for production and includes
steps to create, script, and visualize the simulation setup using LAMMPS-compatible scripts.

Key Features:
1. **Geometry Setup**:
    - Four cylindrical objects ('top', 'food', 'tongue', 'bottom') are defined with specific radii
      and heights.
    - The cylinders are positioned within a central container, with spacing determined by a spacer
      element.
    - The total height of the system is calculated, and the objects are centered within the region.

2. **Forcefield Assignment**:
    - Each object is assigned a bead type and grouped with attributes such as rigidity or softness.
    - Custom forcefields are applied to each object, simulating different physical properties like
      rigid walls or soft materials.

3. **Region Definition**:
    - A simulation region is created with specific dimensions, accounting for the maximum radius of
      the cylinders and the total height of the system.
    - The region is defined in SI units, with additional parameters like separation distance and
      lattice scale.

4. **Script Generation**:
    - The script converts the defined region and objects into LAMMPS-compatible code.
    - Header scripts for initialization, lattice, and the bounding box are generated.
    - The example emphasizes the flexibility in scripting, allowing dynamic reordering and
      combination of scripts.

5. **Execution and Visualization**:
    - The region setup is executed for visualization purposes, enabling control and inspection of
      the geometry.
    - The geometry details, including an estimation of the number of atoms, are provided for
      further analysis.

This example showcases how to effectively set up a gel compression simulation, highlighting key
aspects of geometry definition, forcefield application, and scripting for simulation execution.
"""

    # EXAMPLE: gel compression with SI units
    name = ['top', 'food', 'tongue', 'bottom']
    radius = [10e-3, 5e-3, 8e-3, 10e-3]  # in m
    height = [1e-3, 4e-3, 3e-3, 1e-3]  # in m
    spacer = 2e-3  # in m

    # Calculate positions in SI units (meters)
    position_original = [
        spacer + height[1] + height[2] + height[3],
        height[2] + height[3],
        height[3],
        0
    ]
    total_height = sum(height) + spacer * 1e-3  # converting spacer to meters

    # Center positions around the middle of the container
    position = [x - total_height / 2 for x in position_original]

    # information for beads
    # add attributes to forcefields to match your needs or derive new forcefields
    beadtypes = [1, 2, 3, 1]
    groups = [["rigid","wall1"],["food1","soft"],["food2","soft"],["rigid","wall2"]]
    forcefields = [rigidwall(),solidfood(),solidfood(),rigidwall()]

    # Create the region container with SI units
    R = region(
        name='region container',
        width=2 * max(radius),
        height=total_height,
        depth=2 * max(radius),
        regionunits="si",
        separationdistance=100e-6,  # 50 µm
        lattice_scale=100e-6  # 50 µm
    )

    # Add cylinders to the region R
    # the objects are added "statically"
    # since they contain variables a do() is required to make them a script
    nobjects = len(name)
    for i in range(nobjects):
        R.cylinder(
            name=name[i],
            dim="z",  # Assuming z-axis as the dimension
            c1=0,
            c2=0,
            radius=radius[i],
            lo=position[i],
            hi=position[i] + height[i],
            beadtype=beadtypes[i],
            style="smd",      # the script oject properties
            group=groups[i],  # can be defined in the geometry or
            forcefield=forcefields[i] # when scriptoject() is called
        )

    # Compile statically all objects
    # sR contains the LAMMPS code to generate all region objects and their atoms
    # sR is a string, all variables have been executed
    sR = R.do() # this line force the execution of R

    # Header Scripts facilitate the deployment and initialization of region objects.
    # ------------- Summary ---------------
    # Available scripts include "init", "lattice", and "box".
    # Multiple scripts can be generated simultaneously by specifying them in a list.
    #   For example: ["init", "lattice", "box"] will generate all three scripts.
    # Script parameters and variables can be customized via R.headersData.
    #   For instance: R.headersData.lattice_style = "$sq"
    #   This overrides the lattice style, which was originally set in the region object.
    #   The "$" prefix indicates that lattice_style is a static value.
    #   Alternatively, R.headersData.lattice_style = ["sq"] can also be used.
    # --------------------------------------
    # use help(R.scriptHeaders) to get a full help
    # Note: sRheader is a string since a do()
    sRheader = R.scriptHeaders("box").do() # generate the box that contains R
    print(sRheader)
    # To generate all header scripts in the specified order, use R.scriptHeaders.
    # Note: sRallheaders is a script object. Use sRallheaders.do() to convert it into a string.
    # Scripts can be dynamically combined using the + operator or statically with the & operator.
    # Scripts can also be combined with pipescripts using the + or | (piped) operator.
    # Region and collection objects are considered pipescripts.
    #
    # Comment on the differences between scripts and pipescripts:
    #   - Scripts operate within a single variable space and cannot be reordered once combined.
    #   - Pipescripts, however, include both global and local variable spaces and can be reordered,
    #     and indexed, offering greater flexibility in complex simulations.
    #
    # A property can be removed from the initialization process by setting it to None or ""
    # In this example, atom_style is removed as it also set with forcefields
    R.headersData.atom_style = None
    sRallheaders = R.scriptHeaders(["init", "lattice", "box"] )

    # Generate information on beads from the scripted objects
    # note that scriptobject is a method of script extended to region
    # the region must have been preallably scripted, which has been done with "sR = R.do()"
    # Note that the current implementation include also style definitions in init
    b = []
    for i in range(nobjects):
        # style, group and forcefield can be overdefined if needed
        b.append(R[i].scriptobject(style="smd"))
    collection = b[0] + b[1] + b[2] + b[3]

    # The script corresponding to the collection is given by:
    # scollection is an object of the class script
    # its final execution can be still affected by variables
    scollection = collection.script.do()

    # Execute the region setup only for visualization (control only)
    R.dolive()

    # The detail of the geometry with an estimation of the number of atoms (control only)
    R.geometry

    # to be continued as in the previous workshops
    # sRheader, sR and scollection can be concatenated (they are strings)
    # Note that scripts can be concatenated before do()

Functions

def cleanname(name)
Expand source code
cleanname = lambda name: "".join([x for x in name if x!="$"])
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 ""
def wrap(k, op, v, indent, width, maxwidth)
Expand source code
wrap = lambda k,op,v,indent,width,maxwidth: fill(
        shorten(v,width=maxwidth+indent,
        fix_sentence_endings=True),
        width=width+indent,
        initial_indent=" "*(indent-len(k)-len(op)-2)+f'{k} {op} ',
        subsequent_indent=' '*(indent+(1 if v[0]=='"' else 0) )
        )

Classes

class Block (counter, index=None, subindex=None, mass=1, density=1, lattice_style='sc', lattice_scale=1, lattice_scale_siunits=1, hasgroup=False, hasmove=False, spacefilling=False, style=None, group=None, forcefield=None, **variables)

Block class

constructor of the generic core geometry USER: any definitions requires by the geometry VARIABLES: variables used to define the geometry (to be used in LAMMPS) hasgroup, hasmove: flag to force the sections group and move SECTIONS: they must be PIZZA.script

The flag spacefilling is true of the container of objects (class region) is filled with beads

Expand source code
class Block(coregeometry):
    """ Block class """

    def __init__(self,counter,index=None,subindex=None, mass=1, density=1,
                 lattice_style="sc",lattice_scale=1,lattice_scale_siunits=1,
                 hasgroup=False,hasmove=False,spacefilling=False,
                 style=None, group=None, forcefield=None, **variables):
        self.name = "block%03d" % counter[1]
        self.kind = "block"     # kind of object
        self.alike = "block"    # similar object for plotting
        self.beadtype = 1       # bead type
        self.index = counter[0] if index is None else index
        self.subindex = subindex
        self.mass = mass
        self.density = density

        # call the generic constructor
        super().__init__(
                USER = regiondata(style="$block"),
                VARIABLES = regiondata(**variables),
                hasgroup=hasgroup,hasmove=hasmove,spacefilling=spacefilling,
                mass=mass, density=density,
                lattice_style=lattice_style,
                lattice_scale=lattice_scale,
                lattice_scale_siunits=lattice_scale_siunits,
                style=style, group=group, forcefield=forcefield # script object properties
                )

    def volume(self,units=None):
        """Calculate the volume of the block based on USER.args"""
        #args = [xlo, xhi, ylo, yhi, zlo, zhi]
        try:
            # Extract the arguments from USER.args
            args = self.USER.args_siunits if units=="si" else self.USER.args
            xlo = float(args[0])
            xhi = float(args[1])
            ylo = float(args[2])
            yhi = float(args[3])
            zlo = float(args[4])
            zhi = float(args[5])

            # Calculate the dimensions of the block
            length = xhi - xlo
            width = yhi - ylo
            height = zhi - zlo

            # Calculate the volume of the block
            volume = length * width * height
            return volume
        except Exception as e:
            print(f"Error calculating volume: {e}")
            return None

Ancestors

Methods

def volume(self, units=None)

Calculate the volume of the block based on USER.args

Expand source code
def volume(self,units=None):
    """Calculate the volume of the block based on USER.args"""
    #args = [xlo, xhi, ylo, yhi, zlo, zhi]
    try:
        # Extract the arguments from USER.args
        args = self.USER.args_siunits if units=="si" else self.USER.args
        xlo = float(args[0])
        xhi = float(args[1])
        ylo = float(args[2])
        yhi = float(args[3])
        zlo = float(args[4])
        zhi = float(args[5])

        # Calculate the dimensions of the block
        length = xhi - xlo
        width = yhi - ylo
        height = zhi - zlo

        # Calculate the volume of the block
        volume = length * width * height
        return volume
    except Exception as e:
        print(f"Error calculating volume: {e}")
        return None

Inherited members

class Collection (counter, name=None, index=None, subindex=None, hasgroup=False, USER=region data (RD object) with 0 definitions)

Collection class (including many objects)

Expand source code
class Collection:
    """
        Collection class (including many objects)
    """
    _version = "0.31"
    __custom_documentations__ = "pizza.region.Collection class"

    # CONSTRUCTOR
    def __init__(self,counter,
                 name=None,
                 index = None,
                 subindex = None,
                 hasgroup = False,
                 USER = regiondata()):
        if (name is None) or (name==""):
            self.name = "collect%03d" % counter[1]
        elif name in self:
            raise KeyError(f'the name "{name}" already exist')
        else:
            self.name = name
        if not isinstance(USER,regiondata):
            raise TypeError("USER should be a regiondata object")
        USER.groupID = "$"+self.name # the content is frozen
        USER.ID = ""
        self.USER = USER
        self.kind = "collection"    # kind of object
        self.alike = "mixed"        # similar object for plotting
        self.index = counter[0] if index is None else index
        self.subindex = counter[1]
        self.collection = regioncollection()
        self.SECTIONS = {
                'group': LammpsCollectionGroup(**USER)
        }
        self.FLAGSECTIONS = {"group": hasgroup}

    def update(self):
        """ update the USER content for the script """
        if isinstance(self.SECTIONS["group"],script):
            self.USER.ID = "$"\
                +span([groupprefix+x for x in self.list()]) # the content is frozen
            self.SECTIONS["group"].USER += self.USER

    def creategroup(self):
        """  force the group creation in script """
        for o in self.collection: o.creategroup()
        self.update()
        self.FLAGSECTIONS["group"] = True

    def removegroup(self,recursive=True):
        """  force the group creation in script """
        if recursive:
            for o in self.collection: o.removegroup()
        self.FLAGSECTIONS["group"] = False

    @property
    def hasgroup(self):
        """ return the flag hasgroup """
        return self.FLAGSECTIONS["group"]

    @property
    def flags(self):
        """ return a list of all flags that are currently set """
        flag_names = list(self.SECTIONS.keys())
        return [flag for flag in flag_names if getattr(self, f"has{flag}")]

    @property
    def shortflags(self):
        """ return a string made from the first letter of each set flag """
        return "".join([flag[0] for flag in self.flags])

    @property
    def script(self):
        """ generates a pipe script from SECTIONS """
        self.update()
        return self.SECTIONS["group"]

    def __repr__(self):
        keylengths = [len(key) for key in self.collection.keys()]
        width = max(10,max(keylengths)+2)
        fmt = "%%%ss:" % width
        line = ( fmt % ('-'*(width-2)) ) + ( '-'*(min(40,width*5)) )
        print(line,"  %s - %s object" % (self.name, self.kind), line,sep="\n")
        for key,value in self.collection.items():
            flags = "("+self.collection[key].shortflags+")" if self.collection[key].flags else "(no script)"
            print(fmt % key,value.kind,
                  '"%s"' % value.name," > ",flags)
        flags = self.flags
        if flags: print(line,f'defined scripts: {span(flags,sep=",")}',sep="\n")
        print(line)
        return "%s object: %s (beadtype=[%s])" % (self.kind,self.name,", ".join(map(str,self.beadtype)))

    # GET -----------------------------
    def get(self,name):
        """ returns the object """
        if name in self.collection:
            return self.collection.getattr(name)
        elif name in ["collection","hasgroup","flags","shortflags","script"]:
            return getattr(self,name)
        else:
            raise ValueError('the object "%s" does not exist, use list()' % name)

    # GETATTR --------------------------
    def __getattr__(self,key):
        """ get attribute override """
        return self.get(key)

    @property
    def beadtype(self):
        """ returns the beadtypes used in the collection """
        b = []
        for o in self.collection:
            if o.beadtype not in b:
                b.append(o.beadtype)
        if len(b)==0:
            return 1
        else:
            return b

    # GROUP -------------------------------
    def group(self):
        """ return the grouped coregeometry object """
        if len(self) == 0:return pipescript()
        # execute all objects
        for i in range(len(self)): self.collection[i].do()
        # concatenate all objects into a pipe script
        liste = [x.SECTIONS["variables"] for x in self.collection if x.hasvariables] + \
                [x.SECTIONS["region"]    for x in self.collection if x.hasregion] + \
                [x.SECTIONS["create"]    for x in self.collection if x.hascreate] + \
                [x.SECTIONS["group"]     for x in self.collection if x.hasgroup] + \
                [x.SECTIONS["setgroup"]  for x in self.collection if x.hassetgroup] + \
                [x.SECTIONS["move"]      for x in self.collection if x.hasmove]
        return pipescript.join(liste)

    # LEN ---------------------------------
    def __len__(self):
        """ return length of collection """
        return len(self.collection)

    # LIST ---------------------------------
    def list(self):
        """ return the list of objects """
        return self.collection.keys()

Instance variables

var beadtype

returns the beadtypes used in the collection

Expand source code
@property
def beadtype(self):
    """ returns the beadtypes used in the collection """
    b = []
    for o in self.collection:
        if o.beadtype not in b:
            b.append(o.beadtype)
    if len(b)==0:
        return 1
    else:
        return b
var flags

return a list of all flags that are currently set

Expand source code
@property
def flags(self):
    """ return a list of all flags that are currently set """
    flag_names = list(self.SECTIONS.keys())
    return [flag for flag in flag_names if getattr(self, f"has{flag}")]
var hasgroup

return the flag hasgroup

Expand source code
@property
def hasgroup(self):
    """ return the flag hasgroup """
    return self.FLAGSECTIONS["group"]
var script

generates a pipe script from SECTIONS

Expand source code
@property
def script(self):
    """ generates a pipe script from SECTIONS """
    self.update()
    return self.SECTIONS["group"]
var shortflags

return a string made from the first letter of each set flag

Expand source code
@property
def shortflags(self):
    """ return a string made from the first letter of each set flag """
    return "".join([flag[0] for flag in self.flags])

Methods

def creategroup(self)

force the group creation in script

Expand source code
def creategroup(self):
    """  force the group creation in script """
    for o in self.collection: o.creategroup()
    self.update()
    self.FLAGSECTIONS["group"] = True
def get(self, name)

returns the object

Expand source code
def get(self,name):
    """ returns the object """
    if name in self.collection:
        return self.collection.getattr(name)
    elif name in ["collection","hasgroup","flags","shortflags","script"]:
        return getattr(self,name)
    else:
        raise ValueError('the object "%s" does not exist, use list()' % name)
def group(self)

return the grouped coregeometry object

Expand source code
def group(self):
    """ return the grouped coregeometry object """
    if len(self) == 0:return pipescript()
    # execute all objects
    for i in range(len(self)): self.collection[i].do()
    # concatenate all objects into a pipe script
    liste = [x.SECTIONS["variables"] for x in self.collection if x.hasvariables] + \
            [x.SECTIONS["region"]    for x in self.collection if x.hasregion] + \
            [x.SECTIONS["create"]    for x in self.collection if x.hascreate] + \
            [x.SECTIONS["group"]     for x in self.collection if x.hasgroup] + \
            [x.SECTIONS["setgroup"]  for x in self.collection if x.hassetgroup] + \
            [x.SECTIONS["move"]      for x in self.collection if x.hasmove]
    return pipescript.join(liste)
def list(self)

return the list of objects

Expand source code
def list(self):
    """ return the list of objects """
    return self.collection.keys()
def removegroup(self, recursive=True)

force the group creation in script

Expand source code
def removegroup(self,recursive=True):
    """  force the group creation in script """
    if recursive:
        for o in self.collection: o.removegroup()
    self.FLAGSECTIONS["group"] = False
def update(self)

update the USER content for the script

Expand source code
def update(self):
    """ update the USER content for the script """
    if isinstance(self.SECTIONS["group"],script):
        self.USER.ID = "$"\
            +span([groupprefix+x for x in self.list()]) # the content is frozen
        self.SECTIONS["group"].USER += self.USER
class Cone (counter, index=None, subindex=None, mass=1, density=1, lattice_style='sc', lattice_scale=1, lattice_scale_siunits=1, hasgroup=False, hasmove=False, spacefilling=False, style=None, group=None, forcefield=None, **variables)

Cone class

constructor of the generic core geometry USER: any definitions requires by the geometry VARIABLES: variables used to define the geometry (to be used in LAMMPS) hasgroup, hasmove: flag to force the sections group and move SECTIONS: they must be PIZZA.script

The flag spacefilling is true of the container of objects (class region) is filled with beads

Expand source code
class Cone(coregeometry):
    """ Cone class """

    def __init__(self,counter,index=None,subindex=None, mass=1, density=1,
                 lattice_style="sc",lattice_scale=1,lattice_scale_siunits=1,
                 hasgroup=False,hasmove=False,spacefilling=False,
                 style=None, group=None, forcefield=None, **variables):
        self.name = "cone%03d" % counter[1]
        self.kind = "cone"     # kind of object
        self.alike = "cone"    # similar object for plotting
        self.beadtype = 1      # bead type
        self.index = counter[0] if index is None else index
        self.subindex = subindex
        self.mass = mass
        self.density = density
        # call the generic constructor
        super().__init__(
                USER = regiondata(style="$cone"),
                VARIABLES = regiondata(**variables),
                hasgroup=hasgroup,hasmove=hasmove,spacefilling=spacefilling,
                mass=mass, density=density,
                lattice_style=lattice_style,
                lattice_scale=lattice_scale,
                lattice_scale_siunits=lattice_scale_siunits,
                style=style, group=group, forcefield=forcefield # script object properties
                )

    def volume(self,units=None):
        """Calculate the volume of the cone based on USER.args"""
        #args = [dim, c1, c2, radlo, radhi, lo, hi]
        try:
            # Extract the arguments from USER.args
            args = self.USER.args_siunits if units=="si" else self.USER.args
            radius_low = float(args[3])
            radius_high = float(args[4])
            lo = float(args[5])
            hi = float(args[6])
            # Calculate the height of the cone
            height = hi - lo
            # Calculate the volume of the cone (assuming a conical frustum if radii are different)
            if radius_low == radius_high:
                volume = (1/3) * 3.141592653589793 * (radius_low ** 2) * height
            else:
                volume = (1/3) * 3.141592653589793 * height * (radius_low ** 2 + radius_low * radius_high + radius_high ** 2)
            return volume
        except Exception as e:
            print(f"Error calculating volume: {e}")
            return None

Ancestors

Methods

def volume(self, units=None)

Calculate the volume of the cone based on USER.args

Expand source code
def volume(self,units=None):
    """Calculate the volume of the cone based on USER.args"""
    #args = [dim, c1, c2, radlo, radhi, lo, hi]
    try:
        # Extract the arguments from USER.args
        args = self.USER.args_siunits if units=="si" else self.USER.args
        radius_low = float(args[3])
        radius_high = float(args[4])
        lo = float(args[5])
        hi = float(args[6])
        # Calculate the height of the cone
        height = hi - lo
        # Calculate the volume of the cone (assuming a conical frustum if radii are different)
        if radius_low == radius_high:
            volume = (1/3) * 3.141592653589793 * (radius_low ** 2) * height
        else:
            volume = (1/3) * 3.141592653589793 * height * (radius_low ** 2 + radius_low * radius_high + radius_high ** 2)
        return volume
    except Exception as e:
        print(f"Error calculating volume: {e}")
        return None

Inherited members

class Cylinder (counter, index=None, subindex=None, mass=1, density=1, lattice_style='sc', lattice_scale=1, lattice_scale_siunits=1, hasgroup=False, hasmove=False, spacefilling=False, style=None, group=None, forcefield=None, **variables)

Cylinder class

constructor of the generic core geometry USER: any definitions requires by the geometry VARIABLES: variables used to define the geometry (to be used in LAMMPS) hasgroup, hasmove: flag to force the sections group and move SECTIONS: they must be PIZZA.script

The flag spacefilling is true of the container of objects (class region) is filled with beads

Expand source code
class Cylinder(coregeometry):
    """ Cylinder class """
    def __init__(self,counter,index=None,subindex=None, mass=1, density=1,
                 lattice_style="sc",lattice_scale=1,lattice_scale_siunits=1,
                 hasgroup=False,hasmove=False,spacefilling=False,
                 style=None, group=None, forcefield=None, **variables):
        self.name = "cylinder%03d" % counter[1]
        self.kind = "cylinder"     # kind of object
        self.alike = "cylinder"    # similar object for plotting
        self.beadtype = 1          # bead type
        self.index = counter[0] if index is None else index
        self.subindex = subindex
        self.mass = mass
        self.density = density
        # call the generic constructor
        super().__init__(
                USER = regiondata(style="$cylinder"),
                VARIABLES = regiondata(**variables),
                hasgroup=hasgroup,hasmove=hasmove,spacefilling=spacefilling,
                mass=mass, density=density,
                lattice_style=lattice_style,
                lattice_scale=lattice_scale,
                lattice_scale_siunits=lattice_scale_siunits,
                style=style, group=group, forcefield=forcefield # script object properties
                )

    def volume(self,units=None):
        """Calculate the volume of the cylinder based on USER.args"""
        # args = [dim,c1,c2,radius,lo,hi]
        try:
            # Extract the arguments from USER.args
            args = self.USER.args_siunits if units=="si" else self.USER.args
            radius = float(args[3])
            lo = float(args[4])
            hi = float(args[5])
            # Calculate the height of the cylinder
            height = hi - lo
            # Calculate the volume of the cylinder
            volume = 3.141592653589793 * (radius ** 2) * height
            return volume
        except Exception as e:
            print(f"Error calculating volume: {e}")
            return None

Ancestors

Methods

def volume(self, units=None)

Calculate the volume of the cylinder based on USER.args

Expand source code
def volume(self,units=None):
    """Calculate the volume of the cylinder based on USER.args"""
    # args = [dim,c1,c2,radius,lo,hi]
    try:
        # Extract the arguments from USER.args
        args = self.USER.args_siunits if units=="si" else self.USER.args
        radius = float(args[3])
        lo = float(args[4])
        hi = float(args[5])
        # Calculate the height of the cylinder
        height = hi - lo
        # Calculate the volume of the cylinder
        volume = 3.141592653589793 * (radius ** 2) * height
        return volume
    except Exception as e:
        print(f"Error calculating volume: {e}")
        return None

Inherited members

class Ellipsoid (counter, index=None, subindex=None, mass=1, density=1, lattice_style='sc', lattice_scale=1, lattice_scale_siunits=1, hasgroup=False, hasmove=False, spacefilling=False, style=None, group=None, forcefield=None, **variables)

Ellipsoid class

constructor of the generic core geometry USER: any definitions requires by the geometry VARIABLES: variables used to define the geometry (to be used in LAMMPS) hasgroup, hasmove: flag to force the sections group and move SECTIONS: they must be PIZZA.script

The flag spacefilling is true of the container of objects (class region) is filled with beads

Expand source code
class Ellipsoid(coregeometry):
    """ Ellipsoid class """

    def __init__(self,counter,index=None,subindex=None, mass=1, density=1,
                 lattice_style="sc",lattice_scale=1,lattice_scale_siunits=1,
                 hasgroup=False,hasmove=False,spacefilling=False,
                 style=None, group=None, forcefield=None, **variables):
        self.name = "ellipsoid%03d" % counter[1]
        self.kind = "ellipsoid"     # kind of object
        self.alike = "ellipsoid"    # similar object for plotting
        self.beadtype = 1           # bead type
        self.index = counter[0] if index is None else index
        self.subindex = subindex
        self.mass = mass
        self.density = density
        # call the generic constructor
        super().__init__(
                USER = regiondata(style="$ellipsoid"),
                VARIABLES = regiondata(**variables),
                hasgroup=hasgroup,hasmove=hasmove,spacefilling=spacefilling,
                mass=mass, density=density,
                lattice_style=lattice_style,
                lattice_scale=lattice_scale,
                lattice_scale_siunits=lattice_scale_siunits,
                style=style, group=group, forcefield=forcefield # script object properties
                )

    def volume(self,units=None):
        #args = [x, y, z, a, b, c]
        """Calculate the volume of the ellipsoid based on USER.args"""
        try:
            # Extract the arguments from USER.args
            args = self.USER.args_siunits if units=="si" else self.USER.args
            a = float(args[3])
            b = float(args[4])
            c = float(args[5])
            # Calculate the volume of the ellipsoid
            volume = (4/3) * 3.141592653589793 * a * b * c
            return volume
        except Exception as e:
            print(f"Error calculating volume: {e}")
            return None

Ancestors

Methods

def volume(self, units=None)

Calculate the volume of the ellipsoid based on USER.args

Expand source code
def volume(self,units=None):
    #args = [x, y, z, a, b, c]
    """Calculate the volume of the ellipsoid based on USER.args"""
    try:
        # Extract the arguments from USER.args
        args = self.USER.args_siunits if units=="si" else self.USER.args
        a = float(args[3])
        b = float(args[4])
        c = float(args[5])
        # Calculate the volume of the ellipsoid
        volume = (4/3) * 3.141592653589793 * a * b * c
        return volume
    except Exception as e:
        print(f"Error calculating volume: {e}")
        return None

Inherited members

class Evalgeometry (counter, index=None, subindex=None, hasgroup=False, hasmove=False, spacefilling=False)

generic class to store evaluated objects with region.eval()

constructor of the generic core geometry USER: any definitions requires by the geometry VARIABLES: variables used to define the geometry (to be used in LAMMPS) hasgroup, hasmove: flag to force the sections group and move SECTIONS: they must be PIZZA.script

The flag spacefilling is true of the container of objects (class region) is filled with beads

Expand source code
class Evalgeometry(coregeometry):
    """ generic class to store evaluated objects with region.eval() """

    def __init__(self,counter,index=None,subindex=None,
                 hasgroup=False,hasmove=False,spacefilling=False):
        self.name = "eval%03d" % counter[1]
        self.kind = "eval"      # kind of object
        self.alike = "eval"     # similar object for plotting
        self.beadtype = 1       # bead type
        self.index = counter[0] if index is None else index
        self.subindex = subindex
        super().__init__(hasgroup=hasgroup,hasmove=hasmove,spacefilling=spacefilling)

Ancestors

Inherited members

class Intersect (counter, index=None, subindex=None, hasgroup=False, hasmove=False, spacefilling=False, **variables)

Intersect class

constructor of the generic core geometry USER: any definitions requires by the geometry VARIABLES: variables used to define the geometry (to be used in LAMMPS) hasgroup, hasmove: flag to force the sections group and move SECTIONS: they must be PIZZA.script

The flag spacefilling is true of the container of objects (class region) is filled with beads

Expand source code
class Intersect(coregeometry):
    """ Intersect class """

    def __init__(self,counter,index=None,subindex=None,
                 hasgroup=False,hasmove=False,spacefilling=False,**variables):
        self.name = "intersect%03d" % counter[1]
        self.kind = "intersect"      # kind of object
        self.alike = "operator"     # similar object for plotting
        self.beadtype = 1       # bead type
        self.index = counter[0] if index is None else index
        self.subindex = subindex
        # call the generic constructor
        super().__init__(
                USER = regiondata(style="$intersect"),
                VARIABLES = regiondata(**variables),
                hasgroup=hasgroup,hasmove=hasmove,spacefilling=spacefilling
                )

Ancestors

Inherited members

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

Collection group class based on script

constructor adding instance definitions stored in USER

Expand source code
class LammpsCollectionGroup(LammpsGeneric):
    """ Collection group class based on script """
    name = "LammpsCollection Group"
    SECTIONS = ["COLLECTIONGROUP"]
    position = 6
    role = "group command definition for a collection"
    description = "group ID union regionID1 regionID2..."
    userid = "collectionregion"              # user name
    version = 0.3                            # version
    verbose = True

    # DEFINITIONS USED IN TEMPLATE
    DEFINITIONS = scriptdata(
                    ID = "${ID}",
               groupID = "$"+groupprefix+"${ID}", # freeze the interpretation
          hasvariables = False
                    )

    # Template  (ID is spanned over all regionIDs)
    TEMPLATE = """
% Create group ${groupID} region ${ID} (URL: https://docs.lammps.org/group.html)
group ${groupID} union ${ID}
"""

Ancestors

Class variables

var DEFINITIONS
var SECTIONS
var TEMPLATE
var description
var name
var position
var role
var userid
var verbose
var version

Inherited members

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

script for LAMMPS variables section

constructor adding instance definitions stored in USER

Expand source code
class LammpsCreate(LammpsGeneric):
    """ script for LAMMPS variables section """
    name = "LammpsCreate"
    SECTIONS = ["create_atoms"]
    position = 4
    role = "create_atoms command"
    description = "create_atoms type style args keyword values ..."
    userid = "create"
    version = 0.1
    verbose = True

    # Definitions used in TEMPLATE
    DEFINITIONS = scriptdata(
                    ID = "${ID}",
                 style = "${style}",
                 hasvariables = False
                    )

    # Template (using % instead of # enables replacements)
    TEMPLATE = """
% Create atoms of type ${beadtype} for ${ID} ${style} (https://docs.lammps.org/create_atoms.html)
create_atoms ${beadtype} region ${ID}
"""

Ancestors

Class variables

var DEFINITIONS
var SECTIONS
var TEMPLATE
var description
var name
var position
var role
var userid
var verbose
var version

Inherited members

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

generic header for pizza.region

constructor adding instance definitions stored in USER

Expand source code
class LammpsFooter(LammpsGeneric):
    """ generic header for pizza.region """
    name = "LammpsFooter"
    SECTIONS = ["FOOTER"]
    position = 1000
    role = "footer for live view"
    description = "To be used with https://editor.lammps.org/"
    userid = "footer"              # user name
    version = 0.1                  # version
    verbose = False

    # DEFINITIONS USED IN TEMPLATE
    DEFINITIONS = scriptdata(
                      run = 1,
             hasvariables = False
                    )

    # Template
    TEMPLATE = """
# --------------[  DYNAMICS  ]--------------
${mass}
velocity            all create 1.44 87287 loop geom
pair_style          lj/cut 2.5
${pair_coeff}
neighbor            0.3 bin
neigh_modify    delay 0 every 20 check no
fix                     1 all nve
run                     ${run}
# ------------------------------------------
"""

Ancestors

Class variables

var DEFINITIONS
var SECTIONS
var TEMPLATE
var description
var name
var position
var role
var userid
var verbose
var version

Inherited members

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

Box header for pizza.region

Use R.headersData.property = value to assign a value with R a pizza.region object

constructor adding instance definitions stored in USER

Expand source code
class LammpsFooterPreview(LammpsGeneric): # --- helper script ---
    """
        Box header for pizza.region

        Use R.headersData.property = value to assign a value
        with R a pizza.region object
    """
    name = "LammpsFooterPreview"
    SECTIONS = ["Footer"]
    position = 0
    role = "box footer for pizza.region"
    description = "helper method"
    userid = "footerpreview"       # user name
    version = 0.1                  # version
    verbose = False

    # DEFINITIONS USED IN TEMPLATE
    # circular references (the variable is defined by its field in USER of class regiondata)
    # are not needed but this explicits the requirements.
    # All fields are stored in R.headersData with R a region object.
    # Use R.headersData.property = value to assign a value
    # Extra arguments
    #   ${boxid_arg} is by default "box"
    #   ${boxunits_arg} can be "", "units lattice", "units box"
    DEFINITIONS = scriptdata(
                filename = "${previewfilename}",
            hasvariables = False
                    )

    # Template
    TEMPLATE = """
% --------------[ Preview for <${name}:${boxid}> incl. ${nbeads} bead types ]--------------
% Output the initial geometry to a dump file "${previewfilename}" for visualization
dump initial_dump all custom 1 ${previewfilename} id type x y z
run 0
# ------------------------------------------
"""

Ancestors

Class variables

var DEFINITIONS
var SECTIONS
var TEMPLATE
var description
var name
var position
var role
var userid
var verbose
var version

Inherited members

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

common class to override standard do() method from script LammpsVariables, LammpsRegion, LammpsCreate are LammpsGeneric note:: the only difference with the common script class is that LammpsGeneric accepts VARIABLES AND To SHOW THEM

constructor adding instance definitions stored in USER

Expand source code
class LammpsGeneric(script):
    """
        common class to override standard do() method from script
        LammpsVariables, LammpsRegion, LammpsCreate are LammpsGeneric
        note:: the only difference with the common script class is that
        LammpsGeneric accepts VARIABLES AND To SHOW THEM
    """
    def do(self,printflag=True,verbose=False):
        """ generate the LAMMPS code with VARIABLE definitions """
        if self.DEFINITIONS.hasvariables and hasattr(self,'VARIABLES'): # attribute VARIABLES checked 2023-08-11
            cmd = f"#[{str(datetime.now())}] {self.name} > {self.SECTIONS[0]}" \
                if verbose else ""
            if len(self.VARIABLES)>0: cmd += \
            self.VARIABLES.generatorforlammps(verbose=verbose,hasvariables=True)
        else:
            cmd = ""
        cmd += super().do(printflag=False,verbose=verbose)
        if printflag: print(cmd)
        return cmd

Ancestors

  • pizza.script.script

Subclasses

Methods

def do(self, printflag=True, verbose=False)

generate the LAMMPS code with VARIABLE definitions

Expand source code
def do(self,printflag=True,verbose=False):
    """ generate the LAMMPS code with VARIABLE definitions """
    if self.DEFINITIONS.hasvariables and hasattr(self,'VARIABLES'): # attribute VARIABLES checked 2023-08-11
        cmd = f"#[{str(datetime.now())}] {self.name} > {self.SECTIONS[0]}" \
            if verbose else ""
        if len(self.VARIABLES)>0: cmd += \
        self.VARIABLES.generatorforlammps(verbose=verbose,hasvariables=True)
    else:
        cmd = ""
    cmd += super().do(printflag=False,verbose=verbose)
    if printflag: print(cmd)
    return cmd
class LammpsGroup (persistentfile=True, persistentfolder=None, printflag=False, verbose=False, verbosity=None, **userdefinitions)

generic group class based on script

constructor adding instance definitions stored in USER

Expand source code
class LammpsGroup(LammpsGeneric):
    """ generic group class based on script """
    name = "LammpsGroup"
    SECTIONS = ["GROUP"]
    position = 5
    role = "group command definition"
    description = "group ID region regionID"
    userid = "region"              # user name
    version = 0.2                  # version
    verbose = True

    # DEFINITIONS USED IN TEMPLATE
    DEFINITIONS = scriptdata(
                    ID = "${ID}",
               groupID = "$"+groupprefix+"${ID}", # freeze the interpretation
          countgroupID = "$count"+"${groupID}", # either using $
           grouptoshow = ["${groupID}"], # or []
                 hasvariables = False
                    )

    # Template  (using % instead of # enables replacements)
    TEMPLATE = """
% Create group ${groupID} region ${ID} (URL: https://docs.lammps.org/group.html)
group ${groupID} region ${ID}
variable ${countgroupID} equal count(${grouptoshow})
print "Number of atoms in ${groupID}: \${{countgroupID}}"
"""

Ancestors

Class variables

var DEFINITIONS
var SECTIONS
var TEMPLATE
var description
var name
var position
var role
var userid
var verbose
var version

Inherited members

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

generic header for pizza.region

constructor adding instance definitions stored in USER

Expand source code
class LammpsHeader(LammpsGeneric):
    """ generic header for pizza.region """
    name = "LammpsHeader"
    SECTIONS = ["HEADER"]
    position = 0
    role = "header for live view"
    description = "To be used with https://editor.lammps.org/"
    userid = "header"              # user name
    version = 0.1                  # version
    verbose = False

    # DEFINITIONS USED IN TEMPLATE
    DEFINITIONS = scriptdata(
                    width = 10,
                   height = 10,
                    depth = 10,
                    nbeads = 1,
            hasvariables = False
                    )

    # Template
    TEMPLATE = """
# --------------[    INIT   ]--------------
# assuming generic LJ units and style
units           ${live_units}
atom_style          ${live_atom_style}
lattice             ${live_lattice_style} ${live_lattice_scale}
# ------------------------------------------

# --------------[    B O X   ]--------------
variable        halfwidth equal ${width}/2
variable        halfheight equal ${height}/2
variable        halfdepth equal ${depth}/2
region box block -${halfwidth} ${halfwidth} -${halfheight} ${halfheight} -${halfdepth} ${halfdepth}
create_box      ${nbeads} box
# ------------------------------------------
"""

Ancestors

Class variables

var DEFINITIONS
var SECTIONS
var TEMPLATE
var description
var name
var position
var role
var userid
var verbose
var version

Inherited members

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

Box header for pizza.region

Use R.headersData.property = value to assign a value with R a pizza.region object

constructor adding instance definitions stored in USER

Expand source code
class LammpsHeaderBox(LammpsGeneric): # --- helper script ---
    """
        Box header for pizza.region

        Use R.headersData.property = value to assign a value
        with R a pizza.region object
    """
    name = "LammpsHeaderBox"
    SECTIONS = ["HEADER"]
    position = 0
    role = "box header for pizza.region"
    description = "helper method"
    userid = "headerbox"           # user name
    version = 0.1                  # version
    verbose = False

    # DEFINITIONS USED IN TEMPLATE
    # circular references (the variable is defined by its field in USER of class regiondata)
    # are not needed but this explicits the requirements.
    # All fields are stored in R.headersData with R a region object.
    # Use R.headersData.property = value to assign a value
    # Extra arguments
    #   ${boxid_arg} is by default "box"
    #   ${boxunits_arg} can be "", "units lattice", "units box"
    DEFINITIONS = scriptdata(
                      name = "${name}",
                      xmin = "${xmin}",
                      xmax = "${xmax}",
                      ymin = "${ymin}",
                      ymax = "${ymax}",
                      zmin = "${zmin}",
                      zmax = "${zmax}",
                    nbeads = "${nbeads}",
                     boxid = "${boxid}",
              boxunits_arg = "",     # default units
            hasvariables = False
                    )

    # Template
    TEMPLATE = """
% --------------[ Box for <${name}:${boxid}> incl. ${nbeads} bead types ]--------------
region ${boxid} block ${xmin} ${xmax} ${ymin} ${ymax} ${zmin} ${zmax} ${boxunits_arg}
create_box      ${nbeads} ${boxid}
# ------------------------------------------
"""

Ancestors

Class variables

var DEFINITIONS
var SECTIONS
var TEMPLATE
var description
var name
var position
var role
var userid
var verbose
var version

Inherited members

class LammpsHeaderInit (persistentfile=True, persistentfolder=None, **userdefinitions)

Generates an initialization header script for a pizza.region object in LAMMPS.

This class constructs a LAMMPS header based on user-defined properties stored in R.headersData of the pizza.region object. Properties set to None or an empty string will be omitted from the script.

Attributes

DEFINITIONS
Defines the parameters like dimension, units, boundary, etc.,

that can be set in R.headersData.

Methods

init(persistentfile=True, persistentfolder=None, **userdefinitions): Initializes the header script and sets up the USER attribute.

generate_template(): Creates the header template based on the provided USER definitions.

Note: This class is primarily intended for internal use within the simulation setup.

Constructor adding instance definitions stored in USER.

Expand source code
class LammpsHeaderInit(LammpsGeneric): # --- helper script ---
    """
    Generates an initialization header script for a pizza.region object in LAMMPS.

    This class constructs a LAMMPS header based on user-defined properties stored
    in `R.headersData` of the pizza.region object. Properties set to `None` or an
    empty string will be omitted from the script.

    Attributes:
        DEFINITIONS: Defines the parameters like dimension, units, boundary, etc.,
        that can be set in `R.headersData`.

    Methods:
        __init__(persistentfile=True, persistentfolder=None, **userdefinitions):
            Initializes the header script and sets up the `USER` attribute.

        generate_template():
            Creates the header template based on the provided `USER` definitions.

    Note: This class is primarily intended for internal use within the simulation setup.
    """
    name = "LammpsHeaderBox"
    SECTIONS = ["HEADER"]
    position = -2
    role = "initialization header for pizza.region"
    description = "helper method"
    userid = "headerinit"          # user name
    version = 0.1                  # version
    verbose = False

    # DEFINITIONS USED IN TEMPLATE
    # circular references (the variable is defined by its field in USER of class regiondata)
    # are not needed but this explicits the requirements.
    # All fields are stored in R.headersData with R a region object.
    # Use R.headersData.property = value to assign a value
    # Use R.headersData.property = None or "" to prevent the initialization of property
    DEFINITIONS = scriptdata(
                regionname = "${name}",
                 dimension = "${dimension}",
                     units = "${units}",
                  boundary = "${boundary}",
                atom_style = "${atom_style}",
               atom_modify = "${atom_modify}",
               comm_modify = "${comm_modify}",
              neigh_modify = "${neigh_modify}",
                    newton = "${newton}",
            hasvariables = False
                    )

    def __init__(self, persistentfile=True, persistentfolder=None, **userdefinitions):
        """Constructor adding instance definitions stored in USER."""
        super().__init__(persistentfile, persistentfolder, **userdefinitions)
        self.generate_template()

    def generate_template(self):
        """Generate the TEMPLATE based on USER definitions."""
        self.TEMPLATE = """
% --------------[ Initialization for <${name}:${boxid}>   ]--------------
    """
        self.TEMPLATE += '# set a parameter to None or "" to remove the definition\n'
        if self.USER.dimension:   self.TEMPLATE += "dimension    ${dimension}\n"
        if self.USER.units:       self.TEMPLATE += "units        ${units}\n"
        if self.USER.boundary:    self.TEMPLATE += "boundary     ${boundary}\n"
        if self.USER.atom_style:  self.TEMPLATE += "atom_style   ${atom_style}\n"
        if self.USER.atom_modify: self.TEMPLATE += "atom_modify  ${atom_modify}\n"
        if self.USER.comm_modify: self.TEMPLATE += "comm_modify  ${comm_modify}\n"
        if self.USER.neigh_modify:self.TEMPLATE += "neigh_modify ${neigh_modify}\n"
        if self.USER.newton:      self.TEMPLATE += "newton       ${newton}\n"
        self.TEMPLATE += "# ------------------------------------------\n"

Ancestors

Class variables

var DEFINITIONS
var SECTIONS
var description
var name
var position
var role
var userid
var verbose
var version

Methods

def generate_template(self)

Generate the TEMPLATE based on USER definitions.

Expand source code
    def generate_template(self):
        """Generate the TEMPLATE based on USER definitions."""
        self.TEMPLATE = """
% --------------[ Initialization for <${name}:${boxid}>   ]--------------
    """
        self.TEMPLATE += '# set a parameter to None or "" to remove the definition\n'
        if self.USER.dimension:   self.TEMPLATE += "dimension    ${dimension}\n"
        if self.USER.units:       self.TEMPLATE += "units        ${units}\n"
        if self.USER.boundary:    self.TEMPLATE += "boundary     ${boundary}\n"
        if self.USER.atom_style:  self.TEMPLATE += "atom_style   ${atom_style}\n"
        if self.USER.atom_modify: self.TEMPLATE += "atom_modify  ${atom_modify}\n"
        if self.USER.comm_modify: self.TEMPLATE += "comm_modify  ${comm_modify}\n"
        if self.USER.neigh_modify:self.TEMPLATE += "neigh_modify ${neigh_modify}\n"
        if self.USER.newton:      self.TEMPLATE += "newton       ${newton}\n"
        self.TEMPLATE += "# ------------------------------------------\n"

Inherited members

class LammpsHeaderLattice (persistentfile=True, persistentfolder=None, **userdefinitions)

Lattice header for pizza.region

Use R.headersData.property = value to assign a value with R a pizza.region object

Constructor adding instance definitions stored in USER.

Expand source code
class LammpsHeaderLattice(LammpsGeneric): # --- helper script ---
    """
        Lattice header for pizza.region

        Use R.headersData.property = value to assign a value
        with R a pizza.region object
    """
    name = "LammpsHeaderLattice"
    SECTIONS = ["HEADER"]
    position = 0
    role = "lattice header for pizza.region"
    description = "helper method"
    userid = "headerlattice"       # user name
    version = 0.1                  # version
    verbose = False

    # DEFINITIONS USED IN TEMPLATE
    # circular references (the variable is defined by its field in USER of class regiondata)
    # are not needed but this explicits the requirements.
    # All fields are stored in R.headersData with R a region object.
    # Use R.headersData.property = value to assign a value
    DEFINITIONS = scriptdata(
             lattice_style = "${lattice_style}",
             lattice_scale = "${lattice_scale}",
            hasvariables = False
                    )
    def __init__(self, persistentfile=True, persistentfolder=None, **userdefinitions):
        """Constructor adding instance definitions stored in USER."""
        super().__init__(persistentfile, persistentfolder, **userdefinitions)
        self.generate_template()

    def generate_template(self):
        """Generate the TEMPLATE based on USER definitions."""
        self.TEMPLATE = "\n% --------------[ Lattice for <${name}:${boxid}>, style=${lattice_style}, scale=${lattice_scale} ]--------------\n"
        if self.USER.lattice_spacing is None:
            self.TEMPLATE += "lattice ${lattice_style} ${lattice_scale}\n"
        else:
            self.TEMPLATE += "lattice ${lattice_style} ${lattice_scale} spacing ${lattice_spacing}\n"
        self.TEMPLATE += "# ------------------------------------------\n"

Ancestors

Class variables

var DEFINITIONS
var SECTIONS
var description
var name
var position
var role
var userid
var verbose
var version

Methods

def generate_template(self)

Generate the TEMPLATE based on USER definitions.

Expand source code
def generate_template(self):
    """Generate the TEMPLATE based on USER definitions."""
    self.TEMPLATE = "\n% --------------[ Lattice for <${name}:${boxid}>, style=${lattice_style}, scale=${lattice_scale} ]--------------\n"
    if self.USER.lattice_spacing is None:
        self.TEMPLATE += "lattice ${lattice_style} ${lattice_scale}\n"
    else:
        self.TEMPLATE += "lattice ${lattice_style} ${lattice_scale} spacing ${lattice_spacing}\n"
    self.TEMPLATE += "# ------------------------------------------\n"

Inherited members

class LammpsHeaderMass (persistentfile=True, persistentfolder=None, **userdefinitions)

Mass assignment header for pizza.region.

Use R.headersData.property = value to assign a value with R a pizza.region object.

Constructor adding instance definitions stored in USER.

Parameters

persistentfile (bool, optional): Whether to use a persistent file. Defaults to True. persistentfolder (str, optional): Folder path for persistent files. Defaults to None. **userdefinitions: Arbitrary keyword arguments for user definitions. - mass (list or tuple, optional): List or tuple to override masses for specific bead types. Example: mass=[1.2, 1.0, 0.8] assigns mass 1.2 to bead type 1, 1.0 to bead type 2, and 0.8 to bead type 3.

Expand source code
class LammpsHeaderMass(LammpsGeneric):
    """
    Mass assignment header for pizza.region.

    Use R.headersData.property = value to assign a value
    with R a pizza.region object.
    """
    name = "LammpsHeaderMass"
    SECTIONS = ["HEADER"]
    position = 2  # Positioned after other headers like Box and Lattice
    role = "mass assignment header for pizza.region"
    description = "Assigns masses to bead types based on nbeads and default mass."
    userid = "headermass"  # User identifier
    version = 0.1
    verbose = False

    # DEFINITIONS USED IN TEMPLATE
    # All fields are stored in R.headersData with R a region object.
    # Use R.headersData.property = value to assign a value.
    # Mass overrides are provided via the 'mass' keyword argument as a list or tuple.
    DEFINITIONS = scriptdata(
        nbeads="${nbeads}",  # these default values are not used
        mass="${mass}",      # but reported for records
        hasvariables=False
    )

    def __init__(self, persistentfile=True, persistentfolder=None, **userdefinitions):
        """
            Constructor adding instance definitions stored in USER.

            Parameters:
                persistentfile (bool, optional): Whether to use a persistent file. Defaults to True.
                persistentfolder (str, optional): Folder path for persistent files. Defaults to None.
                **userdefinitions: Arbitrary keyword arguments for user definitions.
                    - mass (list or tuple, optional): List or tuple to override masses for specific bead types.
                      Example: mass=[1.2, 1.0, 0.8] assigns mass 1.2 to bead type 1, 1.0 to bead type 2,
                      and 0.8 to bead type 3.
        """
        super().__init__(persistentfile, persistentfolder, **userdefinitions)
        self.generate_template()

    def generate_template(self):
        """
            Generate the TEMPLATE for mass assignments based on USER definitions.

            The method constructs mass assignments for each bead type. If `mass` overrides
            are provided as a list or tuple, it assigns the specified mass to the corresponding
            bead types. Otherwise, it uses the default `mass` value from `USER.headersData.mass`.
        """
        # Retrieve user-defined parameters
        nbeads = self.USER.nbeads
        mass = self.USER.mass
        # Validate mass
        if not isinstance(mass, (list, tuple)): mass = [mass]  # Convert single value to a list
        if len(mass) > nbeads:
            mass = mass[:nbeads]  # Truncate excess entries
        elif len(mass) < nbeads:
            last_mass = mass[-1]  # Repeat the last value for missing entries
            mass += [last_mass] * (nbeads - len(mass))
        # Initialize TEMPLATE with header comment
        self.TEMPLATE = "\n% --------------[ Mass Assignments for <${name}:${boxid}>" + f" (nbeads={nbeads}) " +" ]--------------\n"
        # Iterate over bead types and assign masses
        for bead_type in range(1, nbeads + 1):
            bead_mass = mass[bead_type - 1]
            if isinstance(bead_mass, str):
                # If mass is a string (e.g., formula), ensure proper formatting
                mass_str = f"({bead_mass})"
            else:
                # If mass is a numeric value, convert to string
                mass_str = f"{bead_mass}"
            self.TEMPLATE += f"mass {bead_type} {mass_str}\n"
        # Close the TEMPLATE with a comment
        self.TEMPLATE += "# ------------------------------------------\n"

Ancestors

Class variables

var DEFINITIONS
var SECTIONS
var description
var name
var position
var role
var userid
var verbose
var version

Methods

def generate_template(self)

Generate the TEMPLATE for mass assignments based on USER definitions.

The method constructs mass assignments for each bead type. If mass overrides are provided as a list or tuple, it assigns the specified mass to the corresponding bead types. Otherwise, it uses the default mass value from USER.headersData.mass.

Expand source code
def generate_template(self):
    """
        Generate the TEMPLATE for mass assignments based on USER definitions.

        The method constructs mass assignments for each bead type. If `mass` overrides
        are provided as a list or tuple, it assigns the specified mass to the corresponding
        bead types. Otherwise, it uses the default `mass` value from `USER.headersData.mass`.
    """
    # Retrieve user-defined parameters
    nbeads = self.USER.nbeads
    mass = self.USER.mass
    # Validate mass
    if not isinstance(mass, (list, tuple)): mass = [mass]  # Convert single value to a list
    if len(mass) > nbeads:
        mass = mass[:nbeads]  # Truncate excess entries
    elif len(mass) < nbeads:
        last_mass = mass[-1]  # Repeat the last value for missing entries
        mass += [last_mass] * (nbeads - len(mass))
    # Initialize TEMPLATE with header comment
    self.TEMPLATE = "\n% --------------[ Mass Assignments for <${name}:${boxid}>" + f" (nbeads={nbeads}) " +" ]--------------\n"
    # Iterate over bead types and assign masses
    for bead_type in range(1, nbeads + 1):
        bead_mass = mass[bead_type - 1]
        if isinstance(bead_mass, str):
            # If mass is a string (e.g., formula), ensure proper formatting
            mass_str = f"({bead_mass})"
        else:
            # If mass is a numeric value, convert to string
            mass_str = f"{bead_mass}"
        self.TEMPLATE += f"mass {bead_type} {mass_str}\n"
    # Close the TEMPLATE with a comment
    self.TEMPLATE += "# ------------------------------------------\n"

Inherited members

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

script for LAMMPS variables section

constructor adding instance definitions stored in USER

Expand source code
class LammpsMove(LammpsGeneric):
    """ script for LAMMPS variables section """
    name = "LammpsMove"
    SECTIONS = ["move_fix"]
    position = 6
    role = "move along a trajectory"
    description = "fix ID group-ID move style args keyword values ..."
    userid = "move"
    version = 0.2
    verbose = True

    # Definitions used in TEMPLATE
    DEFINITIONS = scriptdata(
                    ID = "${ID}",
                moveID = "$"+fixmoveprefix+"${ID}", # freeze the interpretation
               groupID = "$"+groupprefix+"${ID}", # freeze the interpretation
                 style = "${style}",
                  args = "${args}",
          hasvariables = False
                    )

    # Template (using % instead of # enables replacements)
    TEMPLATE = """
# Move atoms fix ID group-ID move style args keyword values (https://docs.lammps.org/fix_move.html)
% move_fix for group ${groupID} using ${style}
% prefix "g" added to ${ID} to indicate a group of atoms
% prefix "fm" added to ${ID} to indicate the ID of the fix move
fix ${moveID} ${groupID} move ${style} ${args}
"""

Ancestors

Class variables

var DEFINITIONS
var SECTIONS
var TEMPLATE
var description
var name
var position
var role
var userid
var verbose
var version

Inherited members

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

generic region based on script

constructor adding instance definitions stored in USER

Expand source code
class LammpsRegion(LammpsGeneric):
    """ generic region based on script """
    name = "LammpsRegion"
    SECTIONS = ["REGION"]
    position = 3
    role = "region command definition"
    description = "region ID style args keyword arg"
    userid = "region"              # user name
    version = 0.1                  # version
    verbose = True

    # DEFINITIONS USED IN TEMPLATE
    DEFINITIONS = scriptdata(
                    ID = "${ID}",
                 style = "${style}",
                  args = "${args}",
                  side = "${side}",
                 units = "${units}",
                  move = "${move}",
                rotate = "${rotate}",
                  open = "${open}",
          hasvariables = False
                    )

    # Template  (using % instead of # enables replacements)
    TEMPLATE = """
% 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}
"""

Ancestors

Class variables

var DEFINITIONS
var SECTIONS
var TEMPLATE
var description
var name
var position
var role
var userid
var verbose
var version

Inherited members

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

script for LAMMPS set group section

constructor adding instance definitions stored in USER

Expand source code
class LammpsSetGroup(LammpsGeneric):
    """ script for LAMMPS set group section """
    name = "LammpsSetGroup"
    SECTIONS = ["set group"]
    position = 4
    role = "create_atoms command"
    description = "set group groupID type beadtype"
    userid = "setgroup"
    version = 0.1
    verbose = True

    # Definitions used in TEMPLATE
    DEFINITIONS = scriptdata(
                    ID = "${ID}",
               groupID = "$"+groupprefix+"${ID}", # freeze the interpretation,
          hasvariables = False
                    )

    # Template (using % instead of # enables replacements)
    TEMPLATE = """
% Reassign atom type to ${beadtype} for the group ${groupID} associated with region ${ID} (https://docs.lammps.org/set.html)
set group ${groupID} type ${beadtype}
"""

Ancestors

Class variables

var DEFINITIONS
var SECTIONS
var TEMPLATE
var description
var name
var position
var role
var userid
var verbose
var version

Inherited members

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

Spacefilling script: fill space with a block

constructor adding instance definitions stored in USER

Expand source code
class LammpsSpacefilling(LammpsGeneric):
    """ Spacefilling script: fill space with a block """
    name = "LammpsSpacefilling"
    SECTIONS = ["SPACEFILLING"]
    position = 1
    role = "fill space with fillingbeadtype atoms"
    description = 'fill the whole space (region "filledspace") with default atoms (beadtype)'
    userid = "spacefilling"              # user name
    version = 0.1                        # version
    verbose = False

    # DEFINITIONS USED IN TEMPLATE
    DEFINITIONS = scriptdata(
             fillingunits = "${fillingunits}",
             fillingwidth = "${fillingwidth}",
            fillingheight = "${fillingheight}",
             fillingdepth = "${fillingdepth}",
               fillingxlo = "-${fillingwidth}/2",
               fillingxhi = "${fillingwidth}/2",
               fillingylo = "-${fillingheight}/2",
               fillingyhi = "${fillingheight}/2",
               fillingzlo = "-${fillingdepth}/2",
               fillingzhi = "${fillingdepth}/2",
          fillingbeadtype = "${fillingbeadtype}",
             fillingstyle = "${block}",
             hasvariables = False
                    )

    # Template
    TEMPLATE = """
region filledspace ${fillingstyle} ${fillingxlo} ${fillingxhi} ${fillingylo} ${fillingyhi} ${fillingzlo} ${fillingzhi}
create_atoms ${fillingbeadtype} region filledspace
# ------------------------------------------
"""

Ancestors

Class variables

var DEFINITIONS
var SECTIONS
var TEMPLATE
var description
var name
var position
var role
var userid
var verbose
var version

Inherited members

class LammpsVariables (VARIABLES=region data (RD object) with 0 definitions, **userdefinitions)

script for LAMMPS variables section myvars = LammpsVariables(regiondata(var1=…),ID='....',style='....')

constructor of LammpsVariables

Expand source code
class LammpsVariables(LammpsGeneric):
    """
        script for LAMMPS variables section
        myvars = LammpsVariables(regiondata(var1=...),ID='....',style='....')
    """
    name = "LammpsVariables"
    SECTIONS = ["VARIABLES"]
    position = 2
    role = "variable command definition"
    description = "variable name style args"
    userid = "variable"
    version = 0.1
    verbose = True

    # Definitions used in TEMPLATE
    DEFINITIONS = scriptdata(
                    ID = "${ID}",
                 style = "${style}",
          hasvariables = True
                    )

    # Template  (using % instead of # enables replacements)
    TEMPLATE = "% variables to be used for ${ID} ${style}"

    def __init__(self,VARIABLES=regiondata(),**userdefinitions):
        """ constructor of LammpsVariables """
        super().__init__(**userdefinitions)
        self.VARIABLES = VARIABLES

    # override >>
    def __rshift__(self,s):
        """ overload right  shift operator (keep only the last template) """
        if isinstance(s,script):
            dup = deepduplicate(self) # instead of duplicate (added 2023-08-11)
            dup.DEFINITIONS = dup.DEFINITIONS + s.DEFINITIONS
            dup.USER = dup.USER + s.USER
            if self.DEFINITIONS.hasvariables and s.DEFINITIONS.hasvariables:
                dup.VARIABLES = s.VARIABLES
            dup.TEMPLATE = s.TEMPLATE
            return dup
        else:
            raise TypeError("the second operand must a script object")

Ancestors

Class variables

var DEFINITIONS
var SECTIONS
var TEMPLATE
var description
var name
var position
var role
var userid
var verbose
var version

Inherited members

class Plane (counter, index=None, subindex=None, mass=1, density=1, lattice_style='sc', lattice_scale=1, lattice_scale_siunits=1, hasgroup=False, hasmove=False, spacefilling=False, style=None, group=None, forcefield=None, **variables)

Plane class

constructor of the generic core geometry USER: any definitions requires by the geometry VARIABLES: variables used to define the geometry (to be used in LAMMPS) hasgroup, hasmove: flag to force the sections group and move SECTIONS: they must be PIZZA.script

The flag spacefilling is true of the container of objects (class region) is filled with beads

Expand source code
class Plane(coregeometry):
    """ Plane class """

    def __init__(self,counter,index=None,subindex=None, mass=1, density=1,
                 lattice_style="sc",lattice_scale=1,lattice_scale_siunits=1,
                 hasgroup=False,hasmove=False,spacefilling=False,
                 style=None, group=None, forcefield=None, **variables):
        self.name = "plane%03d" % counter[1]
        self.kind = "plane"      # kind of object
        self.alike = "plane"     # similar object for plotting
        self.beadtype = 1       # bead type
        self.index = counter[0] if index is None else index
        self.subindex = subindex
        self.mass = mass
        self.density = density
        # call the generic constructor
        super().__init__(
                USER = regiondata(style="$plane"),
                VARIABLES = regiondata(**variables),
                hasgroup=hasgroup,hasmove=hasmove,spacefilling=spacefilling,
                style=style, group=group, forcefield=forcefield # script object properties
                )

    @property
    def volume(self,units=None):
        """Dummy method returning None for volume"""
        #args = [px, py, pz, nx, ny, nz]
        return None

Ancestors

Instance variables

var volume

Dummy method returning None for volume

Expand source code
@property
def volume(self,units=None):
    """Dummy method returning None for volume"""
    #args = [px, py, pz, nx, ny, nz]
    return None

Inherited members

class Prism (counter, index=None, subindex=None, mass=1, density=1, lattice_style='sc', lattice_scale=1, lattice_scale_siunits=1, hasgroup=False, hasmove=False, spacefilling=False, style=None, group=None, forcefield=None, **variables)

Prism class

constructor of the generic core geometry USER: any definitions requires by the geometry VARIABLES: variables used to define the geometry (to be used in LAMMPS) hasgroup, hasmove: flag to force the sections group and move SECTIONS: they must be PIZZA.script

The flag spacefilling is true of the container of objects (class region) is filled with beads

Expand source code
class Prism(coregeometry):
    """ Prism class """

    def __init__(self,counter,index=None,subindex=None, mass=1, density=1,
                 lattice_style="sc",lattice_scale=1,lattice_scale_siunits=1,
                 hasgroup=False,hasmove=False,spacefilling=False,
                 style=None, group=None, forcefield=None, **variables):
        self.name = "prism%03d" % counter[1]
        self.kind = "prism"      # kind of object
        self.alike = "prism"     # similar object for plotting
        self.beadtype = 1       # bead type
        self.index = counter[0] if index is None else index
        self.subindex = subindex
        self.mass = mass
        self.density = density
        # call the generic constructor
        super().__init__(
                USER = regiondata(style="$prism"),
                VARIABLES = regiondata(**variables),
                hasgroup=hasgroup,hasmove=hasmove,spacefilling=spacefilling,
                mass=mass, density=density,
                lattice_style=lattice_style,
                lattice_scale=lattice_scale,
                lattice_scale_siunits=lattice_scale_siunits,
                style=style, group=group, forcefield=forcefield # script object properties
                )

    def volume(self,units=None):
        """Calculate the volume of the prism based on USER.args"""
        #args = [xlo, xhi, ylo, yhi, zlo, zhi, xy, xz, yz]
        try:
            # Extract the arguments from USER.args
            args = self.USER.args_siunits if units=="si" else self.USER.args
            xlo = float(args[0])
            xhi = float(args[1])
            ylo = float(args[2])
            yhi = float(args[3])
            zlo = float(args[4])
            zhi = float(args[5])
            # Calculate the dimensions of the prism
            length = xhi - xlo
            width = yhi - ylo
            height = zhi - zlo
            # Calculate the volume of the prism
            volume = length * width * height
            return volume
        except Exception as e:
            print(f"Error calculating volume: {e}")
            return None

Ancestors

Methods

def volume(self, units=None)

Calculate the volume of the prism based on USER.args

Expand source code
def volume(self,units=None):
    """Calculate the volume of the prism based on USER.args"""
    #args = [xlo, xhi, ylo, yhi, zlo, zhi, xy, xz, yz]
    try:
        # Extract the arguments from USER.args
        args = self.USER.args_siunits if units=="si" else self.USER.args
        xlo = float(args[0])
        xhi = float(args[1])
        ylo = float(args[2])
        yhi = float(args[3])
        zlo = float(args[4])
        zhi = float(args[5])
        # Calculate the dimensions of the prism
        length = xhi - xlo
        width = yhi - ylo
        height = zhi - zlo
        # Calculate the volume of the prism
        volume = length * width * height
        return volume
    except Exception as e:
        print(f"Error calculating volume: {e}")
        return None

Inherited members

class Sphere (counter, index=None, subindex=None, mass=1, density=1, lattice_style='sc', lattice_scale=1, lattice_scale_siunits=1, hasgroup=False, hasmove=False, spacefilling=False, style=None, group=None, forcefield=None, **variables)

Sphere class

constructor of the generic core geometry USER: any definitions requires by the geometry VARIABLES: variables used to define the geometry (to be used in LAMMPS) hasgroup, hasmove: flag to force the sections group and move SECTIONS: they must be PIZZA.script

The flag spacefilling is true of the container of objects (class region) is filled with beads

Expand source code
class Sphere(coregeometry):
    """ Sphere class """

    def __init__(self,counter,index=None,subindex=None, mass=1, density=1,
                 lattice_style="sc",lattice_scale=1,lattice_scale_siunits=1,
                 hasgroup=False,hasmove=False,spacefilling=False,
                 style=None, group=None, forcefield=None, **variables):
        self.name = "sphere%03d" % counter[1]
        self.kind = "sphere"      # kind of object
        self.alike = "ellipsoid"     # similar object for plotting
        self.beadtype = 1       # bead type
        self.index = counter[0] if index is None else index
        self.subindex = subindex
        self.mass = mass
        self.density = density
        # call the generic constructor
        super().__init__(
                USER = regiondata(style="$sphere"),
                VARIABLES = regiondata(**variables),
                hasgroup=hasgroup,hasmove=hasmove,spacefilling=spacefilling,
                mass=mass, density=density,
                lattice_style=lattice_style,
                lattice_scale=lattice_scale,
                lattice_scale_siunits=lattice_scale_siunits,
                style=style, group=group, forcefield=forcefield # script object properties
                )

    def volume(self,units=None):
        """Calculate the volume of the sphere based on USER.args"""
        #args = [x, y, z, radius]
        try:
            # Extract the arguments from USER.args
            args = self.USER.args_siunits if units=="si" else self.USER.args
            radius = float(args[3])
            # Calculate the volume of the sphere
            volume = (4/3) * 3.141592653589793 * (radius ** 3)
            return volume
        except Exception as e:
            print(f"Error calculating volume: {e}")
            return None

Ancestors

Methods

def volume(self, units=None)

Calculate the volume of the sphere based on USER.args

Expand source code
def volume(self,units=None):
    """Calculate the volume of the sphere based on USER.args"""
    #args = [x, y, z, radius]
    try:
        # Extract the arguments from USER.args
        args = self.USER.args_siunits if units=="si" else self.USER.args
        radius = float(args[3])
        # Calculate the volume of the sphere
        volume = (4/3) * 3.141592653589793 * (radius ** 3)
        return volume
    except Exception as e:
        print(f"Error calculating volume: {e}")
        return None

Inherited members

class Union (counter, index=None, subindex=None, hasgroup=False, hasmove=False, spacefilling=False, **variables)

Union class

constructor of the generic core geometry USER: any definitions requires by the geometry VARIABLES: variables used to define the geometry (to be used in LAMMPS) hasgroup, hasmove: flag to force the sections group and move SECTIONS: they must be PIZZA.script

The flag spacefilling is true of the container of objects (class region) is filled with beads

Expand source code
class Union(coregeometry):
    """ Union class """

    def __init__(self,counter,index=None,subindex=None,
                 hasgroup=False,hasmove=False,spacefilling=False,**variables):
        self.name = "union%03d" % counter[1]
        self.kind = "union"      # kind of object
        self.alike = "operator"     # similar object for plotting
        self.beadtype = 1       # bead type
        self.index = counter[0] if index is None else index
        self.subindex = subindex
        # call the generic constructor
        super().__init__(
                USER = regiondata(style="$union"),
                VARIABLES = regiondata(**variables),
                hasgroup=hasgroup,hasmove=hasmove,spacefilling=spacefilling
                )

Ancestors

Inherited members

class coregeometry (USER=region data (RD object) with 0 definitions, VARIABLES=region data (RD object) with 0 definitions, hasgroup=False, hasmove=False, spacefilling=False, style='smd', forcefield=LAMMPS:SMD:none:walls, group=[], mass=1, density=1, lattice_style='sc', lattice_scale=1, lattice_scale_siunits=1)

core geometry object (helper class for attributes, side,units, move, rotate, open)

SECTIONS store scripts (variables, region and create for the geometry) USER = common USER definitions for the three scripts VARIABLES = variables definitions (used by variables only) update() propagate USER to the three scripts script returns SECTIONS as a pipescript do() generate the script

Parameters to be used along scriptobject() style forcefield group They are stored SCRIPTOBJECT_USER

constructor of the generic core geometry USER: any definitions requires by the geometry VARIABLES: variables used to define the geometry (to be used in LAMMPS) hasgroup, hasmove: flag to force the sections group and move SECTIONS: they must be PIZZA.script

The flag spacefilling is true of the container of objects (class region) is filled with beads

Expand source code
class coregeometry:
    """
        core geometry object
        (helper class for attributes, side,units, move, rotate, open)

        SECTIONS store scripts (variables, region and create for the geometry)
        USER = common USER definitions for the three scripts
        VARIABLES = variables definitions (used by variables only)
        update() propagate USER to the three scripts
        script returns SECTIONS as a pipescript
        do() generate the script

        Parameters to be used along scriptobject()
                 style
            forcefield
                 group
        They are stored SCRIPTOBJECT_USER

    """

    _version = "0.35"
    __custom_documentations__ = "pizza.region.coregeometry class"


    def __init__(self,USER=regiondata(),VARIABLES=regiondata(),
                 hasgroup=False, hasmove=False, spacefilling=False,
                 style="smd",
                 forcefield=rigidwall(),
                 group=[],
                 mass=1, density=1,
                 lattice_style="sc", lattice_scale=1, lattice_scale_siunits=1 # added on 2024-07-05
                 ):
        """
            constructor of the generic core geometry
                USER: any definitions requires by the geometry
           VARIABLES: variables used to define the geometry (to be used in LAMMPS)
           hasgroup, hasmove: flag to force the sections group and move
           SECTIONS: they must be PIZZA.script

           The flag spacefilling is true of the container of objects (class region) is filled with beads
        """
        self.USER = USER
        self.SECTIONS = {
            'variables': LammpsVariables(VARIABLES,**USER),
               'region': LammpsRegion(**USER),
               'create': LammpsCreate(**USER),
                'group': LammpsGroup(**USER),
             'setgroup': LammpsSetGroup(**USER),
                 'move': LammpsMove(**USER)
            }
        self.FLAGSECTIONS = {
            'variables': True,
               'region': True,
               'create': not spacefilling,
                'group': hasgroup,
             'setgroup': spacefilling,
                 'move': hasmove
            }
        self.spacefilling = spacefilling

        # add comptaibility with scriptobjects
        self.SCRIPTOBJECT_USER = {
                 'style': style,
            'forcefield': forcefield,
                 'group': group
            }
        # collect information from parent region
        self.mass = mass
        self.density = density
        self.lattice_style = lattice_style
        self.lattice_scale = lattice_scale
        self.lattice_scale_siunits = lattice_scale_siunits

    def update(self):
        """ update the USER content for all three scripts """
        if isinstance(self.SECTIONS["variables"],script):
            self.SECTIONS["variables"].USER += self.USER
        if isinstance(self.SECTIONS["region"],script):
            self.SECTIONS["region"].USER += self.USER
        if isinstance(self.SECTIONS["create"],script):
            self.SECTIONS["create"].USER += self.USER
        if isinstance(self.SECTIONS["group"],script):
            self.SECTIONS["group"].USER += self.USER
        if isinstance(self.SECTIONS["setgroup"],script):
            self.SECTIONS["setgroup"].USER += self.USER
        if isinstance(self.SECTIONS["move"],script):
            self.SECTIONS["move"].USER += self.USER


    def copy(self,beadtype=None,name=""):
        """ returns a copy of the graphical object """
        if self.alike != "mixed":
            dup = deepduplicate(self)
            if beadtype != None: # update beadtype
                dup.beadtype = beadtype
            if name != "": # update name
                dup.name = name
            return dup
        else:
            raise ValueError("collections cannot be copied, regenerate the collection instead")


    def creategroup(self):
        """  force the group creation in script """
        self.FLAGSECTIONS["group"] = True

    def setgroup(self):
        """  force the group creation in script """
        self.FLAGSECTIONS["setgroup"] = True

    def createmove(self):
        """  force the fix move creation in script """
        self.FLAGSECTIONS["move"] = True

    def removegroup(self):
        """  force the group creation in script """
        self.FLAGSECTIONS["group"] = False

    def removemove(self):
        """  force the fix move creation in script """
        self.FLAGSECTIONS["move"] = False

    def scriptobject(self, beadtype=None, name=None, fullname=None, group=None, style=None, forcefield=None, USER = scriptdata()):
        """
        Method to return a scriptobject based on region instead of an input file
        Syntax similar to script.scriptobject
        OBJ = scriptobject(...)
        Implemented properties:
            beadtype=1,2,...
            name="short name"
            fullname = "comprehensive name"
            style = "smd"
            forcefield = any valid forcefield instance (default = rigidwall())
        """
        # Set defaults using instance attributes if parameters are None
        if beadtype is None:
            beadtype = self.beadtype
        if name is None:
            name = f"{self.name} bead"
        if fullname is None:
            fullname = f"beads of type {self.beadtype} | object {self.name} of kind region.{self.kind}"
        if group is None:
            group = self.SCRIPTOBJECT_USER["group"]
        if style is None:
            style = self.SCRIPTOBJECT_USER["style"]
        if forcefield is None:
            style = self.SCRIPTOBJECT_USER["forcefield"]
        return scriptobject(
            beadtype=beadtype,
            name=name,
            fullname=fullname,
            style=style,
            group=group,
            filename=None,  # No need for a file
            USER = USER
        )

    @property
    def hasvariables(self):
        """ return the flag VARIABLES """
        return isinstance(self.SECTIONS["variables"],script) \
               and self.FLAGSECTIONS["variables"]

    @property
    def hasregion(self):
        """ return the flag REGION """
        return isinstance(self.SECTIONS["region"],script) \
               and self.FLAGSECTIONS["region"]

    @property
    def hascreate(self):
        """ return the flag CREATE """
        return isinstance(self.SECTIONS["create"],script) \
               and self.FLAGSECTIONS["create"] \
               and (not self.spacefilling)

    @property
    def hasgroup(self):
        """ return the flag GROUP """
        return isinstance(self.SECTIONS["group"],script) \
               and self.FLAGSECTIONS["group"]

    @property
    def hassetgroup(self):
        """ return the flag GROUP """
        return isinstance(self.SECTIONS["setgroup"],script) \
               and self.FLAGSECTIONS["setgroup"] \
               and self.hasgroup \
               and (not self.hascreate)

    @property
    def hasmove(self):
        """ return the flag MOVE """
        return isinstance(self.SECTIONS["move"],script) \
               and self.FLAGSECTIONS["move"]

    @property
    def isspacefilled(self):
        """ return the flag spacefilling """
        return isinstance(self.SECTIONS["spacefilling"],script) \
               and self.FLAGSECTIONS["spacefilling"]

    @property
    def flags(self):
        """ return a list of all flags that are currently set """
        flag_names = list(self.SECTIONS.keys())
        return [flag for flag in flag_names if getattr(self, f"has{flag}")]

    @property
    def shortflags(self):
        """ return a string made from the first letter of each set flag """
        return "".join([flag[0] for flag in self.flags])


    @property
    def VARIABLES(self):
        """ return variables """
        if isinstance(self.SECTIONS["variables"],script):
            return self.SECTIONS["variables"].VARIABLES
        else:
            v = regiondata()
            for i in range(len(self.SECTIONS["variables"].scripts)):
                v = v + self.SECTIONS["variables"].scripts[i].VARIABLES
            return v

    @property
    def script(self):
        """ generates a pipe script from sections """
        self.update()
        ptmp = self.SECTIONS["variables"] if self.hasvariables else None
        if self.hasregion:
            ptmp = self.SECTIONS["region"] if ptmp is None else ptmp | self.SECTIONS["region"]
        if self.hascreate:
            ptmp = self.SECTIONS["create"] if ptmp is None else ptmp | self.SECTIONS["create"]
        if self.hasgroup:
            ptmp = self.SECTIONS["group"] if ptmp is None else ptmp | self.SECTIONS["group"]
        if self.hassetgroup:
            ptmp = self.SECTIONS["setgroup"] if ptmp is None else ptmp | self.SECTIONS["setgroup"]
        if self.hasmove:
            ptmp = self.SECTIONS["move"] if ptmp is None else ptmp | self.SECTIONS["move"]
        return ptmp
        # before 2023-07-17
        #return self.SECTIONS["variables"] | self.SECTIONS["region"] | self.SECTIONS["create"]

    def do(self,printflag=False,verbosity=1):
        """ generates a script """
        p = self.script # intentional, force script before do(), comment added on 2023-07-17
        cmd = p.do(printflag=printflag,verbosity=verbosity)
        # if printflag: print(cmd)
        return cmd

    def __repr__(self):
        """ display method"""
        nVAR = len(self.VARIABLES)
        print("%s - %s object - beadtype=%d " % (self.name, self.kind,self.beadtype))
        if hasattr(self,"filename"): print(f'\tfilename: "{self.filename}"')
        if nVAR>0:
            print(f"\t<-- {nVAR} variables are defined -->")
            print(f"\tUse {self.name}.VARIABLES to see details and their evaluation")
            for k,v in self.VARIABLES.items():
                v0 = '"'+v+'"' if isinstance(v,str) else repr(v)
                print(wrap(k,"=",v0,20,40,80))
        print("\t<-- keyword arg -->")
        haskeys = False
        for k in ("side","move","units","rotate","open"):
            if k in self.USER:
                v = self.USER.getattr(k)
                if v != "":
                    print(wrap(k,":",v[1:],20,60,80))
                    haskeys = True
        if not haskeys: print(wrap("no keywords","<","from side|move|units|rotate|open",20,60,80))
        flags = self.flags
        if flags: print(f'defined scripts: {span(flags,sep=",")}',"\n")
        print("\n"+self.geometry) # added 2024-07-05
        return "%s object: %s (beadtype=%d)" % (self.kind,self.name,self.beadtype)


    # ~~~~ validator for region arguments (the implementation is specific and not generic as fix move ones)
    def sidearg(self,side):
        """
            Validation of side arguments for region command (https://docs.lammps.org/region.html)
            side value = in or out
              in = the region is inside the specified geometry
              out = the region is outside the specified geometry
        """
        prefix = "$"
        if side is None:
            return ""
        elif isinstance(side, str):
            side = side.lower()
            if side in ("in","out"):
                return f"{prefix} side {side}"
            elif side in ("","none"):
                return ""
            else:
                raise ValueError(f'the value of side: "{side}" is not recognized')
        else:
            raise ValueError('the parameter side can be "in|out|None"')

    def movearg(self,move):
        """
            Validation of move arguments for region command (https://docs.lammps.org/region.html)
            move args = v_x v_y v_z
              v_x,v_y,v_z = equal-style variables for x,y,z displacement of region over time (distance units)
        """
        prefix = "$"
        if move is None:
            return ""
        elif isinstance(move, str):
            move = move.lower()
            if move in("","none"):
                return ""
            else:
                return f"{prefix} move {move}"
        elif isinstance(move,(list,tuple)):
            if len(move)<3:
                print("NULL will be added to move")
            elif len(move)>3:
                print("move will be truncated to 3 elements")
            movevalid = ["NULL","NULL","NULL"]
            for i in range(min(3,len(move))):
                if isinstance(move[i],str):
                    if move[i].upper()!="NULL":
                        if prefix in move[i]:
                            # we assume a numeric result after evaluation
                            # Pizza variables will be evaluated
                            # formateval for the evaluation of ${}
                            # eval for residual expressions
                            movevalid[i] = round(eval(self.VARIABLES.formateval(move[i])),6)
                        else:
                            # we assume a variable (LAMMPS variable, not Pizza ones)
                            movevalid[i] = "v_" + move[i]
                elif not isinstance(move[i],(int,float)):
                    if (move[i] is not None):
                        raise TypeError("move values should be str, int or float")
            return f"{prefix} move {span(movevalid)}"
        else:
            raise TypeError("the parameter move should be a list or tuple")

    def unitsarg(self,units):
        """
            Validation for units arguments for region command (https://docs.lammps.org/region.html)
            units value = lattice or box
              lattice = the geometry is defined in lattice units
              box = the geometry is defined in simulation box units
        """
        prefix = "$"
        if units is None:
            return ""
        elif isinstance(units,str):
            units = units.lower()
            if units in ("lattice","box"):
                return f"{prefix} units {units}"
            elif (units=="") or (units=="none"):
                return ""
            else:
                raise ValueError(f'the value of side: "{units}" is not recognized')
        else:
            raise TypeError('the parameter units can be "lattice|box|None"')

    def rotatearg(self,rotate):
        """
            Validation of rotate arguments for region command (https://docs.lammps.org/region.html)
            rotate args = v_theta Px Py Pz Rx Ry Rz
              v_theta = equal-style variable for rotaton of region over time (in radians)
              Px,Py,Pz = origin for axis of rotation (distance units)
              Rx,Ry,Rz = axis of rotation vector
        """
        prefix = "$"
        if rotate is None:
            return ""
        elif isinstance(rotate, str):
            rotate = rotate.lower()
            if rotate in ("","none",None):
                return ""
            else:
                return f"{prefix} rotate {rotate}"
        elif isinstance(rotate,(list,tuple)):
            if len(rotate)<7:
                print("NULL will be added to rotate")
            elif len(rotate)>7:
                print("rotate will be truncated to 7 elements")
            rotatevalid = ["NULL"]*7
            for i in range(min(7,len(rotate))):
                if isinstance(rotate[i],str):
                    if rotate[i].upper()!="NULL":
                        if prefix in rotate[i]:
                            rotatevalid[i] = round(eval(self.VARIABLES.formateval(rotate[i])),6)
                        else:
                            rotatevalid[i] = rotate[i]
                elif not isinstance(rotate[i],(int,float)):
                    if (rotate[i] is not None):
                        raise TypeError("rotate values should be str, int or float")
            return f"{prefix} move {span(rotatevalid)}"
        else:
            raise TypeError("the parameter rotate should be a list or tuple")

    def openarg(self,open):
        """
            Validation of open arguments for region command (https://docs.lammps.org/region.html)
            open value = integer from 1-6 corresponding to face index (see below)
            The indices specified as part of the open keyword have the following meanings:

            For style block, indices 1-6 correspond to the xlo, xhi, ylo, yhi, zlo, zhi surfaces of the block.
            I.e. 1 is the yz plane at x = xlo, 2 is the yz-plane at x = xhi, 3 is the xz plane at y = ylo,
            4 is the xz plane at y = yhi, 5 is the xy plane at z = zlo, 6 is the xy plane at z = zhi).
            In the second-to-last example above, the region is a box open at both xy planes.

            For style prism, values 1-6 have the same mapping as for style block.
            I.e. in an untilted prism, open indices correspond to the xlo, xhi, ylo, yhi, zlo, zhi surfaces.

            For style cylinder, index 1 corresponds to the flat end cap at the low coordinate along the cylinder axis,
            index 2 corresponds to the high-coordinate flat end cap along the cylinder axis, and index 3 is the curved
            cylinder surface. For example, a cylinder region with open 1 open 2 keywords will be open at both ends
            (e.g. a section of pipe), regardless of the cylinder orientation.
        """
        prefix = "$"
        if open in ("","none",None):
            return ""
        elif isinstance(open, str):
            raise TypeError(" the parameter open should be an integer or a list/tuple of integers from 1-6")
        elif isinstance(open, int):
            if open in range(1,7):
                return f"{prefix} open {open}"
            else:
                raise TypeError(" open value should be integer from 1-6")
        elif isinstance(open, (list,tuple)):
            openvalid = [f"{prefix} open {i}" for i in range(1,7) if i in open]
            return f"$ {span(openvalid)}"
    # ~~~~ end validator for region arguments

    # ~~~~ validator for fix move arguments (implemented generically on 2023-07-17)
    def fixmoveargvalidator(self, argtype, arg, arglen):
        """
            Validation of arguments for fix move command in LAMMPS (https://docs.lammps.org/fix_move.html)

            LAMMPS syntax:
                fix ID group-ID move style args
                - linear args = Vx Vy Vz
                - wiggle args = Ax Ay Az period
                - rotate args = Px Py Pz Rx Ry Rz period
                - transrot args = Vx Vy Vz Px Py Pz Rx Ry Rz period
                - variable args = v_dx v_dy v_dz v_vx v_vy v_vz

            Args:
                argtype: Type of the argument (linear, wiggle, rotate, transrot, variable)
                arg: The argument to validate
                arglen: Expected length of the argument
        """
        prefix = "$"
        if arg in ("","none",None):
            return ""
        elif isinstance(arg,(list,tuple)):
            if len(arg) < arglen:
                print(f"NULL will be added to {argtype}")
            elif len(arg) > arglen:
                print(f"{argtype} will be truncated to {arglen} elements")
            argvalid = ["NULL"]*arglen
            for i in range(min(arglen,len(arg))):
                if isinstance(arg[i],str):
                    if arg[i].upper()!="NULL":
                        if prefix in arg[i]:
                            argvalid[i] = round(eval(self.VARIABLES.formateval(arg[i])),6)
                        else:
                            argvalid[i] = arg[i]
                elif not isinstance(arg[i],(int,float)):
                    if (arg[i] is not None):
                        raise TypeError(f"{argtype} values should be str, int or float")
            return f"{prefix} move {span(argvalid)}"
        else:
            raise TypeError(f"the parameter {argtype} should be a list or tuple")


    def fixmoveargs(self, linear=None, wiggle=None, rotate=None, transrot=None, variable=None):
        """
            Validates all arguments for fix move command in LAMMPS (https://docs.lammps.org/fix_move.html)
            the result is adictionary, all fixmove can be combined
        """
        argsdict = {
            "linear": [linear, 3],
            "wiggle": [wiggle, 4],
            "rotate": [rotate, 7],
            "transrot": [transrot, 10],
            "variable": [variable, 6]
        }

        for argtype, arginfo in argsdict.items():
            arg, arglen = arginfo
            if arg is not None:
                argsdict[argtype] = self.fixmoveargvalidator(argtype, arg, arglen)
        return argsdict


    def get_fixmovesyntax(self, argtype=None):
        """
        Returns the syntax for LAMMPS command, or detailed explanation for a specific argument type

        Args:
        argtype: Optional; Type of the argument (linear, wiggle, rotate, transrot, variable)
        """
        syntax = {
            "linear": "linear args = Vx Vy Vz\n"
                      "Vx,Vy,Vz = components of velocity vector (velocity units), any component can be specified as NULL",
            "wiggle": "wiggle args = Ax Ay Az period\n"
                       "Ax,Ay,Az = components of amplitude vector (distance units), any component can be specified as NULL\n"
                       "period = period of oscillation (time units)",
            "rotate": "rotate args = Px Py Pz Rx Ry Rz period\n"
                       "Px,Py,Pz = origin point of axis of rotation (distance units)\n"
                       "Rx,Ry,Rz = axis of rotation vector\n"
                       "period = period of rotation (time units)",
            "transrot": "transrot args = Vx Vy Vz Px Py Pz Rx Ry Rz period\n"
                        "Vx,Vy,Vz = components of velocity vector (velocity units)\n"
                        "Px,Py,Pz = origin point of axis of rotation (distance units)\n"
                        "Rx,Ry,Rz = axis of rotation vector\n"
                        "period = period of rotation (time units)",
            "variable": "variable args = v_dx v_dy v_dz v_vx v_vy v_vz\n"
                        "v_dx,v_dy,v_dz = 3 variable names that calculate x,y,z displacement as function of time, any component can be specified as NULL\n"
                        "v_vx,v_vy,v_vz = 3 variable names that calculate x,y,z velocity as function of time, any component can be specified as NULL",
        }

        base_syntax = (
            "fix ID group-ID move style args\n"
            " - linear args = Vx Vy Vz\n"
            " - wiggle args = Ax Ay Az period\n"
            " - rotate args = Px Py Pz Rx Ry Rz period\n"
            " - transrot args = Vx Vy Vz Px Py Pz Rx Ry Rz period\n"
            " - variable args = v_dx v_dy v_dz v_vx v_vy v_vz\n\n"
            'use get_movesyntax("movemethod") for details'
            "manual: https://docs.lammps.org/fix_move.html"
        )

        return syntax.get(argtype, base_syntax)

    # ~~~~ end validator for fix move arguments

    def __add__(self,C):
        """ overload addition ("+") operator """
        if isinstance(C,coregeometry):
            dup = deepduplicate(self)
            dup.name = cleanname(self.name) +"+"+ cleanname(C.name)
            dup.USER = dup.USER + C.USER
            dup.USER.ID = "$" + cleanname(self.USER.ID) +"+"+ cleanname(C.USER.ID)
            dup.SECTIONS["variables"] = dup.SECTIONS["variables"] + C.SECTIONS["variables"]
            dup.SECTIONS["region"] = dup.SECTIONS["region"] + C.SECTIONS["region"]
            dup.SECTIONS["create"] = dup.SECTIONS["create"] + C.SECTIONS["create"]
            dup.SECTIONS["group"] = dup.SECTIONS["group"] + C.SECTIONS["group"]
            dup.SECTIONS["move"] = dup.SECTIONS["move"] + C.SECTIONS["move"]
            dup.FLAGSECTIONS["variables"] = dup.FLAGSECTIONS["variables"] or C.FLAGSECTIONS["variables"]
            dup.FLAGSECTIONS["region"] = dup.FLAGSECTIONS["region"] or C.FLAGSECTIONS["region"]
            dup.FLAGSECTIONS["create"] = dup.FLAGSECTIONS["create"] or C.FLAGSECTIONS["create"]
            dup.FLAGSECTIONS["group"] = dup.FLAGSECTIONS["group"] or C.FLAGSECTIONS["group"]
            dup.FLAGSECTIONS["move"] = dup.FLAGSECTIONS["move"] or C.FLAGSECTIONS["move"]
            return dup
        raise TypeError("the second operand must a region.coregeometry object")

    def __iadd__(self,C):
        """ overload iaddition ("+=") operator """
        if isinstance(C,coregeometry):
            self.USER += C.USER
            self.SECTIONS["variables"] += C.SECTIONS["variables"]
            self.SECTIONS["region"] += C.SECTIONS["region"]
            self.SECTIONS["create"] += C.SECTIONS["create"]
            self.SECTIONS["group"] += C.SECTIONS["group"]
            self.SECTIONS["move"] += C.SECTIONS["move"]
            self.FLAGSECTIONS["variables"] = self.FLAGSECTIONS["variables"] or C.FLAGSECTIONS["variables"]
            self.FLAGSECTIONS["region"] = self.FLAGSECTIONS["region"] or C.FLAGSECTIONS["region"]
            self.FLAGSECTIONS["create"] = self.FLAGSECTIONS["create"] or C.FLAGSECTIONS["create"]
            self.FLAGSECTIONS["group"] = self.FLAGSECTIONS["group"] or C.FLAGSECTIONS["group"]
            self.FLAGSECTIONS["move"] = self.FLAGSECTIONS["move"] or C.FLAGSECTIONS["move"]
            return self
        raise TypeError("the operand must a region.coregeometry object")

    def __or__(self,C):
        """ overload | pipe """
        if isinstance(C,coregeometry):
            dup = deepduplicate(self)
            dup.name = cleanname(self.name) +"|"+ cleanname(C.name)
            dup.USER = dup.USER + C.USER
            dup.USER.ID = "$" + cleanname(self.USER.ID) +"|"+ cleanname(C.USER.ID)
            dup.SECTIONS["variables"] = dup.SECTIONS["variables"] | C.SECTIONS["variables"]
            dup.SECTIONS["region"] = dup.SECTIONS["region"] | C.SECTIONS["region"]
            dup.SECTIONS["create"] = dup.SECTIONS["create"] | C.SECTIONS["create"]
            dup.SECTIONS["group"] = dup.SECTIONS["group"] | C.SECTIONS["group"]
            dup.SECTIONS["move"] = dup.SECTIONS["move"] | C.SECTIONS["move"]
            self.FLAGSECTIONS["variables"] = self.FLAGSECTIONS["variables"] or C.FLAGSECTIONS["variables"]
            self.FLAGSECTIONS["region"] = self.FLAGSECTIONS["region"] or C.FLAGSECTIONS["region"]
            self.FLAGSECTIONS["create"] = self.FLAGSECTIONS["create"] or C.FLAGSECTIONS["create"]
            self.FLAGSECTIONS["group"] = self.FLAGSECTIONS["group"] or C.FLAGSECTIONS["group"]
            self.FLAGSECTIONS["move"] = self.FLAGSECTIONS["move"] or C.FLAGSECTIONS["move"]
            return dup
        raise TypeError("the second operand must a region.coregeometry object")

    # copy and deep copy methods for the class (required)
    def __getstate__(self):
        """ getstate for cooperative inheritance / duplication """
        return self.__dict__.copy()

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

    def __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)) # replace duplicatedeep by deepduplicate (OV: 2023-07-28)
        return copie

    # Return the number of atoms
    @property
    def natoms(self):
        """Calculate the number of beads based on density, mass, and volume"""
        if hasattr(self, 'volume'):
            try:
                volume_siunits = self.volume("si")
                voxel_volume_siunits = self.lattice_scale**3
                number_of_beads = volume_siunits / voxel_volume_siunits
                packing_factors = {
                    'sc': 1.0,
                    'fcc': 4.0,
                    'bcc': 2.0,
                    'hcp': 6.0,  # Approximate value, requires specific volume calculation for accuracy
                    'dia': 8.0,
                    'bco': 2.0,  # Assuming orthorhombic lattice similar to bcc
                    'fco': 4.0,  # Assuming orthorhombic lattice similar to fcc
                }
                packing_factor = packing_factors.get(self.lattice_style, 1.0)  # Default to simple cubic if unknown
                number_of_beads *= packing_factor
                return round(number_of_beads)
            except Exception as e:
                print(f"Error calculating number of beads: {e}")
                return None
        else:
            print("Volume attribute is missing.")
            return None

    # return parent region details
    @property
    def regiondetails(self):
        return "\n".join((
        f"\n--- | Region Details | ---",
        f"Name: {self.name}",
        f"Lattice Style: {self.lattice_style}",
        f"Lattice Scale: {self.lattice_scale}",
        f"Lattice Scale (SI units): {self.lattice_scale_siunits}",
        f"Volume: {self.volume()}",
        f"Volume (SI units): {self.volume('si')}",
        f"Number of Atoms: {self.natoms}","\n"
        ))


    # return geometry details (2024-07-04)
    @property
    def geometry(self):
        """Return the geometry details of the object."""
        details = self.regiondetails
        details += "\n--- | Geometry Details | ---\n"
        if hasattr(self.USER, 'geometry'):
            details += self.USER.geometry
        else:
            details = "No geometry available.\n"
        return details

Subclasses

Instance variables

var VARIABLES

return variables

Expand source code
@property
def VARIABLES(self):
    """ return variables """
    if isinstance(self.SECTIONS["variables"],script):
        return self.SECTIONS["variables"].VARIABLES
    else:
        v = regiondata()
        for i in range(len(self.SECTIONS["variables"].scripts)):
            v = v + self.SECTIONS["variables"].scripts[i].VARIABLES
        return v
var flags

return a list of all flags that are currently set

Expand source code
@property
def flags(self):
    """ return a list of all flags that are currently set """
    flag_names = list(self.SECTIONS.keys())
    return [flag for flag in flag_names if getattr(self, f"has{flag}")]
var geometry

Return the geometry details of the object.

Expand source code
@property
def geometry(self):
    """Return the geometry details of the object."""
    details = self.regiondetails
    details += "\n--- | Geometry Details | ---\n"
    if hasattr(self.USER, 'geometry'):
        details += self.USER.geometry
    else:
        details = "No geometry available.\n"
    return details
var hascreate

return the flag CREATE

Expand source code
@property
def hascreate(self):
    """ return the flag CREATE """
    return isinstance(self.SECTIONS["create"],script) \
           and self.FLAGSECTIONS["create"] \
           and (not self.spacefilling)
var hasgroup

return the flag GROUP

Expand source code
@property
def hasgroup(self):
    """ return the flag GROUP """
    return isinstance(self.SECTIONS["group"],script) \
           and self.FLAGSECTIONS["group"]
var hasmove

return the flag MOVE

Expand source code
@property
def hasmove(self):
    """ return the flag MOVE """
    return isinstance(self.SECTIONS["move"],script) \
           and self.FLAGSECTIONS["move"]
var hasregion

return the flag REGION

Expand source code
@property
def hasregion(self):
    """ return the flag REGION """
    return isinstance(self.SECTIONS["region"],script) \
           and self.FLAGSECTIONS["region"]
var hassetgroup

return the flag GROUP

Expand source code
@property
def hassetgroup(self):
    """ return the flag GROUP """
    return isinstance(self.SECTIONS["setgroup"],script) \
           and self.FLAGSECTIONS["setgroup"] \
           and self.hasgroup \
           and (not self.hascreate)
var hasvariables

return the flag VARIABLES

Expand source code
@property
def hasvariables(self):
    """ return the flag VARIABLES """
    return isinstance(self.SECTIONS["variables"],script) \
           and self.FLAGSECTIONS["variables"]
var isspacefilled

return the flag spacefilling

Expand source code
@property
def isspacefilled(self):
    """ return the flag spacefilling """
    return isinstance(self.SECTIONS["spacefilling"],script) \
           and self.FLAGSECTIONS["spacefilling"]
var natoms

Calculate the number of beads based on density, mass, and volume

Expand source code
@property
def natoms(self):
    """Calculate the number of beads based on density, mass, and volume"""
    if hasattr(self, 'volume'):
        try:
            volume_siunits = self.volume("si")
            voxel_volume_siunits = self.lattice_scale**3
            number_of_beads = volume_siunits / voxel_volume_siunits
            packing_factors = {
                'sc': 1.0,
                'fcc': 4.0,
                'bcc': 2.0,
                'hcp': 6.0,  # Approximate value, requires specific volume calculation for accuracy
                'dia': 8.0,
                'bco': 2.0,  # Assuming orthorhombic lattice similar to bcc
                'fco': 4.0,  # Assuming orthorhombic lattice similar to fcc
            }
            packing_factor = packing_factors.get(self.lattice_style, 1.0)  # Default to simple cubic if unknown
            number_of_beads *= packing_factor
            return round(number_of_beads)
        except Exception as e:
            print(f"Error calculating number of beads: {e}")
            return None
    else:
        print("Volume attribute is missing.")
        return None
var regiondetails
Expand source code
@property
def regiondetails(self):
    return "\n".join((
    f"\n--- | Region Details | ---",
    f"Name: {self.name}",
    f"Lattice Style: {self.lattice_style}",
    f"Lattice Scale: {self.lattice_scale}",
    f"Lattice Scale (SI units): {self.lattice_scale_siunits}",
    f"Volume: {self.volume()}",
    f"Volume (SI units): {self.volume('si')}",
    f"Number of Atoms: {self.natoms}","\n"
    ))
var script

generates a pipe script from sections

Expand source code
@property
def script(self):
    """ generates a pipe script from sections """
    self.update()
    ptmp = self.SECTIONS["variables"] if self.hasvariables else None
    if self.hasregion:
        ptmp = self.SECTIONS["region"] if ptmp is None else ptmp | self.SECTIONS["region"]
    if self.hascreate:
        ptmp = self.SECTIONS["create"] if ptmp is None else ptmp | self.SECTIONS["create"]
    if self.hasgroup:
        ptmp = self.SECTIONS["group"] if ptmp is None else ptmp | self.SECTIONS["group"]
    if self.hassetgroup:
        ptmp = self.SECTIONS["setgroup"] if ptmp is None else ptmp | self.SECTIONS["setgroup"]
    if self.hasmove:
        ptmp = self.SECTIONS["move"] if ptmp is None else ptmp | self.SECTIONS["move"]
    return ptmp
    # before 2023-07-17
    #return self.SECTIONS["variables"] | self.SECTIONS["region"] | self.SECTIONS["create"]
var shortflags

return a string made from the first letter of each set flag

Expand source code
@property
def shortflags(self):
    """ return a string made from the first letter of each set flag """
    return "".join([flag[0] for flag in self.flags])

Methods

def copy(self, beadtype=None, name='')

returns a copy of the graphical object

Expand source code
def copy(self,beadtype=None,name=""):
    """ returns a copy of the graphical object """
    if self.alike != "mixed":
        dup = deepduplicate(self)
        if beadtype != None: # update beadtype
            dup.beadtype = beadtype
        if name != "": # update name
            dup.name = name
        return dup
    else:
        raise ValueError("collections cannot be copied, regenerate the collection instead")
def creategroup(self)

force the group creation in script

Expand source code
def creategroup(self):
    """  force the group creation in script """
    self.FLAGSECTIONS["group"] = True
def createmove(self)

force the fix move creation in script

Expand source code
def createmove(self):
    """  force the fix move creation in script """
    self.FLAGSECTIONS["move"] = True
def do(self, printflag=False, verbosity=1)

generates a script

Expand source code
def do(self,printflag=False,verbosity=1):
    """ generates a script """
    p = self.script # intentional, force script before do(), comment added on 2023-07-17
    cmd = p.do(printflag=printflag,verbosity=verbosity)
    # if printflag: print(cmd)
    return cmd
def fixmoveargs(self, linear=None, wiggle=None, rotate=None, transrot=None, variable=None)

Validates all arguments for fix move command in LAMMPS (https://docs.lammps.org/fix_move.html) the result is adictionary, all fixmove can be combined

Expand source code
def fixmoveargs(self, linear=None, wiggle=None, rotate=None, transrot=None, variable=None):
    """
        Validates all arguments for fix move command in LAMMPS (https://docs.lammps.org/fix_move.html)
        the result is adictionary, all fixmove can be combined
    """
    argsdict = {
        "linear": [linear, 3],
        "wiggle": [wiggle, 4],
        "rotate": [rotate, 7],
        "transrot": [transrot, 10],
        "variable": [variable, 6]
    }

    for argtype, arginfo in argsdict.items():
        arg, arglen = arginfo
        if arg is not None:
            argsdict[argtype] = self.fixmoveargvalidator(argtype, arg, arglen)
    return argsdict
def fixmoveargvalidator(self, argtype, arg, arglen)

Validation of arguments for fix move command in LAMMPS (https://docs.lammps.org/fix_move.html)

LAMMPS syntax: fix ID group-ID move style args - linear args = Vx Vy Vz - wiggle args = Ax Ay Az period - rotate args = Px Py Pz Rx Ry Rz period - transrot args = Vx Vy Vz Px Py Pz Rx Ry Rz period - variable args = v_dx v_dy v_dz v_vx v_vy v_vz

Args

argtype
Type of the argument (linear, wiggle, rotate, transrot, variable)
arg
The argument to validate
arglen
Expected length of the argument
Expand source code
def fixmoveargvalidator(self, argtype, arg, arglen):
    """
        Validation of arguments for fix move command in LAMMPS (https://docs.lammps.org/fix_move.html)

        LAMMPS syntax:
            fix ID group-ID move style args
            - linear args = Vx Vy Vz
            - wiggle args = Ax Ay Az period
            - rotate args = Px Py Pz Rx Ry Rz period
            - transrot args = Vx Vy Vz Px Py Pz Rx Ry Rz period
            - variable args = v_dx v_dy v_dz v_vx v_vy v_vz

        Args:
            argtype: Type of the argument (linear, wiggle, rotate, transrot, variable)
            arg: The argument to validate
            arglen: Expected length of the argument
    """
    prefix = "$"
    if arg in ("","none",None):
        return ""
    elif isinstance(arg,(list,tuple)):
        if len(arg) < arglen:
            print(f"NULL will be added to {argtype}")
        elif len(arg) > arglen:
            print(f"{argtype} will be truncated to {arglen} elements")
        argvalid = ["NULL"]*arglen
        for i in range(min(arglen,len(arg))):
            if isinstance(arg[i],str):
                if arg[i].upper()!="NULL":
                    if prefix in arg[i]:
                        argvalid[i] = round(eval(self.VARIABLES.formateval(arg[i])),6)
                    else:
                        argvalid[i] = arg[i]
            elif not isinstance(arg[i],(int,float)):
                if (arg[i] is not None):
                    raise TypeError(f"{argtype} values should be str, int or float")
        return f"{prefix} move {span(argvalid)}"
    else:
        raise TypeError(f"the parameter {argtype} should be a list or tuple")
def get_fixmovesyntax(self, argtype=None)

Returns the syntax for LAMMPS command, or detailed explanation for a specific argument type

Args: argtype: Optional; Type of the argument (linear, wiggle, rotate, transrot, variable)

Expand source code
def get_fixmovesyntax(self, argtype=None):
    """
    Returns the syntax for LAMMPS command, or detailed explanation for a specific argument type

    Args:
    argtype: Optional; Type of the argument (linear, wiggle, rotate, transrot, variable)
    """
    syntax = {
        "linear": "linear args = Vx Vy Vz\n"
                  "Vx,Vy,Vz = components of velocity vector (velocity units), any component can be specified as NULL",
        "wiggle": "wiggle args = Ax Ay Az period\n"
                   "Ax,Ay,Az = components of amplitude vector (distance units), any component can be specified as NULL\n"
                   "period = period of oscillation (time units)",
        "rotate": "rotate args = Px Py Pz Rx Ry Rz period\n"
                   "Px,Py,Pz = origin point of axis of rotation (distance units)\n"
                   "Rx,Ry,Rz = axis of rotation vector\n"
                   "period = period of rotation (time units)",
        "transrot": "transrot args = Vx Vy Vz Px Py Pz Rx Ry Rz period\n"
                    "Vx,Vy,Vz = components of velocity vector (velocity units)\n"
                    "Px,Py,Pz = origin point of axis of rotation (distance units)\n"
                    "Rx,Ry,Rz = axis of rotation vector\n"
                    "period = period of rotation (time units)",
        "variable": "variable args = v_dx v_dy v_dz v_vx v_vy v_vz\n"
                    "v_dx,v_dy,v_dz = 3 variable names that calculate x,y,z displacement as function of time, any component can be specified as NULL\n"
                    "v_vx,v_vy,v_vz = 3 variable names that calculate x,y,z velocity as function of time, any component can be specified as NULL",
    }

    base_syntax = (
        "fix ID group-ID move style args\n"
        " - linear args = Vx Vy Vz\n"
        " - wiggle args = Ax Ay Az period\n"
        " - rotate args = Px Py Pz Rx Ry Rz period\n"
        " - transrot args = Vx Vy Vz Px Py Pz Rx Ry Rz period\n"
        " - variable args = v_dx v_dy v_dz v_vx v_vy v_vz\n\n"
        'use get_movesyntax("movemethod") for details'
        "manual: https://docs.lammps.org/fix_move.html"
    )

    return syntax.get(argtype, base_syntax)
def movearg(self, move)

Validation of move arguments for region command (https://docs.lammps.org/region.html) move args = v_x v_y v_z v_x,v_y,v_z = equal-style variables for x,y,z displacement of region over time (distance units)

Expand source code
def movearg(self,move):
    """
        Validation of move arguments for region command (https://docs.lammps.org/region.html)
        move args = v_x v_y v_z
          v_x,v_y,v_z = equal-style variables for x,y,z displacement of region over time (distance units)
    """
    prefix = "$"
    if move is None:
        return ""
    elif isinstance(move, str):
        move = move.lower()
        if move in("","none"):
            return ""
        else:
            return f"{prefix} move {move}"
    elif isinstance(move,(list,tuple)):
        if len(move)<3:
            print("NULL will be added to move")
        elif len(move)>3:
            print("move will be truncated to 3 elements")
        movevalid = ["NULL","NULL","NULL"]
        for i in range(min(3,len(move))):
            if isinstance(move[i],str):
                if move[i].upper()!="NULL":
                    if prefix in move[i]:
                        # we assume a numeric result after evaluation
                        # Pizza variables will be evaluated
                        # formateval for the evaluation of ${}
                        # eval for residual expressions
                        movevalid[i] = round(eval(self.VARIABLES.formateval(move[i])),6)
                    else:
                        # we assume a variable (LAMMPS variable, not Pizza ones)
                        movevalid[i] = "v_" + move[i]
            elif not isinstance(move[i],(int,float)):
                if (move[i] is not None):
                    raise TypeError("move values should be str, int or float")
        return f"{prefix} move {span(movevalid)}"
    else:
        raise TypeError("the parameter move should be a list or tuple")
def openarg(self, open)

Validation of open arguments for region command (https://docs.lammps.org/region.html) open value = integer from 1-6 corresponding to face index (see below) The indices specified as part of the open keyword have the following meanings:

For style block, indices 1-6 correspond to the xlo, xhi, ylo, yhi, zlo, zhi surfaces of the block. I.e. 1 is the yz plane at x = xlo, 2 is the yz-plane at x = xhi, 3 is the xz plane at y = ylo, 4 is the xz plane at y = yhi, 5 is the xy plane at z = zlo, 6 is the xy plane at z = zhi). In the second-to-last example above, the region is a box open at both xy planes.

For style prism, values 1-6 have the same mapping as for style block. I.e. in an untilted prism, open indices correspond to the xlo, xhi, ylo, yhi, zlo, zhi surfaces.

For style cylinder, index 1 corresponds to the flat end cap at the low coordinate along the cylinder axis, index 2 corresponds to the high-coordinate flat end cap along the cylinder axis, and index 3 is the curved cylinder surface. For example, a cylinder region with open 1 open 2 keywords will be open at both ends (e.g. a section of pipe), regardless of the cylinder orientation.

Expand source code
def openarg(self,open):
    """
        Validation of open arguments for region command (https://docs.lammps.org/region.html)
        open value = integer from 1-6 corresponding to face index (see below)
        The indices specified as part of the open keyword have the following meanings:

        For style block, indices 1-6 correspond to the xlo, xhi, ylo, yhi, zlo, zhi surfaces of the block.
        I.e. 1 is the yz plane at x = xlo, 2 is the yz-plane at x = xhi, 3 is the xz plane at y = ylo,
        4 is the xz plane at y = yhi, 5 is the xy plane at z = zlo, 6 is the xy plane at z = zhi).
        In the second-to-last example above, the region is a box open at both xy planes.

        For style prism, values 1-6 have the same mapping as for style block.
        I.e. in an untilted prism, open indices correspond to the xlo, xhi, ylo, yhi, zlo, zhi surfaces.

        For style cylinder, index 1 corresponds to the flat end cap at the low coordinate along the cylinder axis,
        index 2 corresponds to the high-coordinate flat end cap along the cylinder axis, and index 3 is the curved
        cylinder surface. For example, a cylinder region with open 1 open 2 keywords will be open at both ends
        (e.g. a section of pipe), regardless of the cylinder orientation.
    """
    prefix = "$"
    if open in ("","none",None):
        return ""
    elif isinstance(open, str):
        raise TypeError(" the parameter open should be an integer or a list/tuple of integers from 1-6")
    elif isinstance(open, int):
        if open in range(1,7):
            return f"{prefix} open {open}"
        else:
            raise TypeError(" open value should be integer from 1-6")
    elif isinstance(open, (list,tuple)):
        openvalid = [f"{prefix} open {i}" for i in range(1,7) if i in open]
        return f"$ {span(openvalid)}"
def removegroup(self)

force the group creation in script

Expand source code
def removegroup(self):
    """  force the group creation in script """
    self.FLAGSECTIONS["group"] = False
def removemove(self)

force the fix move creation in script

Expand source code
def removemove(self):
    """  force the fix move creation in script """
    self.FLAGSECTIONS["move"] = False
def rotatearg(self, rotate)

Validation of rotate arguments for region command (https://docs.lammps.org/region.html) rotate args = v_theta Px Py Pz Rx Ry Rz v_theta = equal-style variable for rotaton of region over time (in radians) Px,Py,Pz = origin for axis of rotation (distance units) Rx,Ry,Rz = axis of rotation vector

Expand source code
def rotatearg(self,rotate):
    """
        Validation of rotate arguments for region command (https://docs.lammps.org/region.html)
        rotate args = v_theta Px Py Pz Rx Ry Rz
          v_theta = equal-style variable for rotaton of region over time (in radians)
          Px,Py,Pz = origin for axis of rotation (distance units)
          Rx,Ry,Rz = axis of rotation vector
    """
    prefix = "$"
    if rotate is None:
        return ""
    elif isinstance(rotate, str):
        rotate = rotate.lower()
        if rotate in ("","none",None):
            return ""
        else:
            return f"{prefix} rotate {rotate}"
    elif isinstance(rotate,(list,tuple)):
        if len(rotate)<7:
            print("NULL will be added to rotate")
        elif len(rotate)>7:
            print("rotate will be truncated to 7 elements")
        rotatevalid = ["NULL"]*7
        for i in range(min(7,len(rotate))):
            if isinstance(rotate[i],str):
                if rotate[i].upper()!="NULL":
                    if prefix in rotate[i]:
                        rotatevalid[i] = round(eval(self.VARIABLES.formateval(rotate[i])),6)
                    else:
                        rotatevalid[i] = rotate[i]
            elif not isinstance(rotate[i],(int,float)):
                if (rotate[i] is not None):
                    raise TypeError("rotate values should be str, int or float")
        return f"{prefix} move {span(rotatevalid)}"
    else:
        raise TypeError("the parameter rotate should be a list or tuple")
def scriptobject(self, beadtype=None, name=None, fullname=None, group=None, style=None, forcefield=None, USER=script data (SD object) with 0 definitions)

Method to return a scriptobject based on region instead of an input file Syntax similar to script.scriptobject OBJ = scriptobject(…) Implemented properties: beadtype=1,2,… name="short name" fullname = "comprehensive name" style = "smd" forcefield = any valid forcefield instance (default = rigidwall())

Expand source code
def scriptobject(self, beadtype=None, name=None, fullname=None, group=None, style=None, forcefield=None, USER = scriptdata()):
    """
    Method to return a scriptobject based on region instead of an input file
    Syntax similar to script.scriptobject
    OBJ = scriptobject(...)
    Implemented properties:
        beadtype=1,2,...
        name="short name"
        fullname = "comprehensive name"
        style = "smd"
        forcefield = any valid forcefield instance (default = rigidwall())
    """
    # Set defaults using instance attributes if parameters are None
    if beadtype is None:
        beadtype = self.beadtype
    if name is None:
        name = f"{self.name} bead"
    if fullname is None:
        fullname = f"beads of type {self.beadtype} | object {self.name} of kind region.{self.kind}"
    if group is None:
        group = self.SCRIPTOBJECT_USER["group"]
    if style is None:
        style = self.SCRIPTOBJECT_USER["style"]
    if forcefield is None:
        style = self.SCRIPTOBJECT_USER["forcefield"]
    return scriptobject(
        beadtype=beadtype,
        name=name,
        fullname=fullname,
        style=style,
        group=group,
        filename=None,  # No need for a file
        USER = USER
    )
def setgroup(self)

force the group creation in script

Expand source code
def setgroup(self):
    """  force the group creation in script """
    self.FLAGSECTIONS["setgroup"] = True
def sidearg(self, side)

Validation of side arguments for region command (https://docs.lammps.org/region.html) side value = in or out in = the region is inside the specified geometry out = the region is outside the specified geometry

Expand source code
def sidearg(self,side):
    """
        Validation of side arguments for region command (https://docs.lammps.org/region.html)
        side value = in or out
          in = the region is inside the specified geometry
          out = the region is outside the specified geometry
    """
    prefix = "$"
    if side is None:
        return ""
    elif isinstance(side, str):
        side = side.lower()
        if side in ("in","out"):
            return f"{prefix} side {side}"
        elif side in ("","none"):
            return ""
        else:
            raise ValueError(f'the value of side: "{side}" is not recognized')
    else:
        raise ValueError('the parameter side can be "in|out|None"')
def unitsarg(self, units)

Validation for units arguments for region command (https://docs.lammps.org/region.html) units value = lattice or box lattice = the geometry is defined in lattice units box = the geometry is defined in simulation box units

Expand source code
def unitsarg(self,units):
    """
        Validation for units arguments for region command (https://docs.lammps.org/region.html)
        units value = lattice or box
          lattice = the geometry is defined in lattice units
          box = the geometry is defined in simulation box units
    """
    prefix = "$"
    if units is None:
        return ""
    elif isinstance(units,str):
        units = units.lower()
        if units in ("lattice","box"):
            return f"{prefix} units {units}"
        elif (units=="") or (units=="none"):
            return ""
        else:
            raise ValueError(f'the value of side: "{units}" is not recognized')
    else:
        raise TypeError('the parameter units can be "lattice|box|None"')
def update(self)

update the USER content for all three scripts

Expand source code
def update(self):
    """ update the USER content for all three scripts """
    if isinstance(self.SECTIONS["variables"],script):
        self.SECTIONS["variables"].USER += self.USER
    if isinstance(self.SECTIONS["region"],script):
        self.SECTIONS["region"].USER += self.USER
    if isinstance(self.SECTIONS["create"],script):
        self.SECTIONS["create"].USER += self.USER
    if isinstance(self.SECTIONS["group"],script):
        self.SECTIONS["group"].USER += self.USER
    if isinstance(self.SECTIONS["setgroup"],script):
        self.SECTIONS["setgroup"].USER += self.USER
    if isinstance(self.SECTIONS["move"],script):
        self.SECTIONS["move"].USER += self.USER
class emulsion (xmin=10, ymin=10, zmin=10, xmax=90, ymax=90, zmax=90, maxtrials=1000, beadtype=1, forcedinsertion=True)

emulsion generator

Parameters

The insertions are performed between xmin,ymin and xmax,ymax
xmin : int64 or real, optional
x left corner. The default is 10.
ymin : int64 or real, optional
y bottom corner. The default is 10.
zmin : int64 or real, optional
z bottom corner. The default is 10.
xmax : int64 or real, optional
x right corner. The default is 90.
ymax : int64 or real, optional
y top corner. The default is 90.
zmax : int64 or real, optional
z top corner. The default is 90.
beadtype : default beadtype to apply if not precised at insertion
 
maxtrials : integer, optional
Maximum of attempts for an object. The default is 1000.
forcedinsertion : logical, optional
Set it to true to force the next insertion. The default is True.

Returns

None.

Expand source code
class emulsion(scatter):
    """ emulsion generator """

    def __init__(self, xmin=10, ymin=10, zmin=10, xmax=90, ymax=90, zmax=90,
                 maxtrials=1000, beadtype=1, forcedinsertion=True):
        """


        Parameters
        ----------
        The insertions are performed between xmin,ymin and xmax,ymax
        xmin : int64 or real, optional
            x left corner. The default is 10.
        ymin : int64 or real, optional
            y bottom corner. The default is 10.
        zmin : int64 or real, optional
            z bottom corner. The default is 10.
        xmax : int64 or real, optional
            x right corner. The default is 90.
        ymax : int64 or real, optional
            y top corner. The default is 90.
        zmax : int64 or real, optional
            z top corner. The default is 90.
        beadtype : default beadtype to apply if not precised at insertion
        maxtrials : integer, optional
            Maximum of attempts for an object. The default is 1000.
        forcedinsertion : logical, optional
            Set it to true to force the next insertion. The default is True.

        Returns
        -------
        None.

        """
        super().__init__()
        self.xmin, self.xmax, self.ymin, self.ymax, self.zmin, self.zmax = xmin, xmax, ymin, ymax, zmin, zmax
        self.lastinsertion = (None,None,None,None,None) # x,y,z,r, beadtype
        self.length = xmax-xmin
        self.width = ymax-ymin
        self.height = zmax-zmin
        self.defautbeadtype = beadtype
        self.maxtrials = maxtrials
        self.forcedinsertion = forcedinsertion

    def __repr__(self):
        print(f" Emulsion object\n\t{self.length}x{self.width}x{self.height} starting at x={self.xmin}, y={self.ymin}, z={self.zmin}")
        print(f"\tcontains {self.n} insertions")
        print("\tmaximum insertion trials:", self.maxtrials)
        print("\tforce next insertion if previous fails:", self.forcedinsertion)
        return f"emulsion with {self.n} insertions"


    def walldist(self,x,y,z):
        """ shortest distance to the wall """
        return min(abs(x-self.xmin),abs(y-self.ymin),abs(z-self.zmin),abs(x-self.xmax),abs(y-self.ymax),abs(z-self.zmax))

    def dist(self,x,y,z):
        """ shortest distance of the center (x,y) to the wall or any object"""
        return np.minimum(np.min(self.pairdist(x,y,z)),self.walldist(x,y,z))

    def accepted(self,x,y,z,r):
        """ acceptation criterion """
        return self.dist(x,y,z)>r

    def rand(self):
        """ random position x,y  """
        return  np.random.uniform(low=self.xmin,high=self.xmax), \
                np.random.uniform(low=self.ymin,high=self.ymax),\
                np.random.uniform(low=self.zmin,high=self.zmax)

    def setbeadtype(self,beadtype):
        """ set the default or the supplied beadtype  """
        if beadtype == None:
            self.beadtype.append(self.defautbeadtype)
            return self.defautbeadtype
        else:
            self.beadtype.append(beadtype)
            return beadtype

    def insertone(self,x=None,y=None,z=None,r=None,beadtype=None,overlap=False):
        """
            insert one object of radius r
            properties:
                x,y,z coordinates (if missing, picked randomly from uniform distribution)
                r radius (default = 2% of diagonal)
                beadtype (default = defautbeadtype)
                overlap = False (accept only if no overlap)
        """
        attempt, success = 0, False
        random = (x==None) or (y==None) or (z==None)
        if r==None:
            r = 0.02*np.sqrt(self.length**2+self.width**2+self.height**2)
        while not success and attempt<self.maxtrials:
            attempt += 1
            if random: x,y,z = self.rand()
            if overlap:
                success = True
            else:
                success = self.accepted(x,y,z,r)
        if success:
            self.x = np.append(self.x,x)
            self.y = np.append(self.y,y)
            self.z = np.append(self.z,z)
            self.r = np.append(self.r,r)
            b=self.setbeadtype(beadtype)
            self.lastinsertion = (x,y,z,r,b)
        return success

    def insertion(self,rlist,beadtype=None):
        """
            insert a list of objects
                nsuccess=insertion(rlist,beadtype=None)
                beadtype=b forces the value b
                if None, defaultbeadtype is used instead
        """
        rlist.sort(reverse=True)
        ntodo = len(rlist)
        n = nsuccess = 0
        stop = False
        while not stop:
            n += 1
            success = self.insertone(r=rlist[n-1],beadtype=beadtype)
            if success: nsuccess += 1
            stop = (n==ntodo) or (not success and not self.forcedinsertion)
        if nsuccess==ntodo:
            print(f"{nsuccess} objects inserted successfully")
        else:
            print(f"partial success: {nsuccess} of {ntodo} objects inserted")
        return nsuccess

Ancestors

Methods

def accepted(self, x, y, z, r)

acceptation criterion

Expand source code
def accepted(self,x,y,z,r):
    """ acceptation criterion """
    return self.dist(x,y,z)>r
def dist(self, x, y, z)

shortest distance of the center (x,y) to the wall or any object

Expand source code
def dist(self,x,y,z):
    """ shortest distance of the center (x,y) to the wall or any object"""
    return np.minimum(np.min(self.pairdist(x,y,z)),self.walldist(x,y,z))
def insertion(self, rlist, beadtype=None)

insert a list of objects nsuccess=insertion(rlist,beadtype=None) beadtype=b forces the value b if None, defaultbeadtype is used instead

Expand source code
def insertion(self,rlist,beadtype=None):
    """
        insert a list of objects
            nsuccess=insertion(rlist,beadtype=None)
            beadtype=b forces the value b
            if None, defaultbeadtype is used instead
    """
    rlist.sort(reverse=True)
    ntodo = len(rlist)
    n = nsuccess = 0
    stop = False
    while not stop:
        n += 1
        success = self.insertone(r=rlist[n-1],beadtype=beadtype)
        if success: nsuccess += 1
        stop = (n==ntodo) or (not success and not self.forcedinsertion)
    if nsuccess==ntodo:
        print(f"{nsuccess} objects inserted successfully")
    else:
        print(f"partial success: {nsuccess} of {ntodo} objects inserted")
    return nsuccess
def insertone(self, x=None, y=None, z=None, r=None, beadtype=None, overlap=False)

insert one object of radius r properties: x,y,z coordinates (if missing, picked randomly from uniform distribution) r radius (default = 2% of diagonal) beadtype (default = defautbeadtype) overlap = False (accept only if no overlap)

Expand source code
def insertone(self,x=None,y=None,z=None,r=None,beadtype=None,overlap=False):
    """
        insert one object of radius r
        properties:
            x,y,z coordinates (if missing, picked randomly from uniform distribution)
            r radius (default = 2% of diagonal)
            beadtype (default = defautbeadtype)
            overlap = False (accept only if no overlap)
    """
    attempt, success = 0, False
    random = (x==None) or (y==None) or (z==None)
    if r==None:
        r = 0.02*np.sqrt(self.length**2+self.width**2+self.height**2)
    while not success and attempt<self.maxtrials:
        attempt += 1
        if random: x,y,z = self.rand()
        if overlap:
            success = True
        else:
            success = self.accepted(x,y,z,r)
    if success:
        self.x = np.append(self.x,x)
        self.y = np.append(self.y,y)
        self.z = np.append(self.z,z)
        self.r = np.append(self.r,r)
        b=self.setbeadtype(beadtype)
        self.lastinsertion = (x,y,z,r,b)
    return success
def rand(self)

random position x,y

Expand source code
def rand(self):
    """ random position x,y  """
    return  np.random.uniform(low=self.xmin,high=self.xmax), \
            np.random.uniform(low=self.ymin,high=self.ymax),\
            np.random.uniform(low=self.zmin,high=self.zmax)
def setbeadtype(self, beadtype)

set the default or the supplied beadtype

Expand source code
def setbeadtype(self,beadtype):
    """ set the default or the supplied beadtype  """
    if beadtype == None:
        self.beadtype.append(self.defautbeadtype)
        return self.defautbeadtype
    else:
        self.beadtype.append(beadtype)
        return beadtype
def walldist(self, x, y, z)

shortest distance to the wall

Expand source code
def walldist(self,x,y,z):
    """ shortest distance to the wall """
    return min(abs(x-self.xmin),abs(y-self.ymin),abs(z-self.zmin),abs(x-self.xmax),abs(y-self.ymax),abs(z-self.zmax))

Inherited members

class forcefield

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

Attributes:

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

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

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

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

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

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

Methods:

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

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

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

Notes:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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


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

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

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

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

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


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

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

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

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

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

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

Subclasses

  • pizza.forcefield.smd

Class variables

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

Static methods

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

print header

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

Methods

def getallattributes(self)

advanced method to get all attributes including class ones

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

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

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

Parameters:

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

Returns:

str The formatted diagonal pair coefficient command string.

Raises:

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

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

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

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

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

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

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

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

Parameters:

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

Returns:

str The formatted off-diagonal pair coefficient command string.

Raises:

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

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

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

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

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

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

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

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

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

Parameters:

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

Returns:

str The formatted pair style command string.

Raises:

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

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

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

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

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

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

class of script parameters Typical constructor: DEFINITIONS = headersRegiondata( var1 = value1, var2 = value2 ) See script, struct, param to get review all methods attached to it

constructor

Expand source code
class headersRegiondata(regiondata):
    """
        class of script parameters
            Typical constructor:
                DEFINITIONS = headersRegiondata(
                    var1 = value1,
                    var2 = value2
                    )
        See script, struct, param to get review all methods attached to it
    """
    _type = "HRD"
    _fulltype = "Header parameters - helper for scripts"
    _ftype = "header definition"

Ancestors

  • regiondata
  • pizza.private.mstruct.paramauto
  • pizza.private.mstruct.param
  • pizza.private.mstruct.struct

Inherited members

class none

SMD:TLSPH forcefield (updated Lagrangian)

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

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

Ancestors

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

Subclasses

  • pizza.forcefield.rigidwall

Class variables

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

Class: param

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


Features

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

Examples

Basic Usage with Evaluation

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

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

Handling Text Parameters

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

Text Evaluation and Formatting

Evaluate Strings

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

Prevent Evaluation

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

Advanced Usage

Rearranging and Sorting Definitions

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

Error Handling

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

Sorting unresolved definitions raises errors unless explicitly suppressed:

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

Utility Methods

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

Overloaded Methods and Operators

Supported Operators

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

Notes

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

constructor

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

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

    ---

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

    ---

    ### Examples

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

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

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

    ---

    ### Text Evaluation and Formatting

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

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

    ---

    ### Advanced Usage

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

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

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

    ---

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

    ---

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

    ---

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

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

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

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

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

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

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

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

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

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


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

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

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

        """
        # Evaluate all DEFINITIONS
        # the argument s is only used by formateval() for error management
        tmp = struct()
        for key,value in self.items():
            # strings are assumed to be expressions on one single line
            if isinstance(value,str):
                # replace ${variable} (Bash, Lammps syntax) by {variable} (Python syntax)
                # use \${variable} to prevent replacement (espace with \)
                # Protect variables if required
                ispstr = isinstance(value,pstr)
                valuesafe = pstr.eval(value,ispstr=ispstr) # value.strip()
                if protection or self._protection:
                    valuesafe, escape0 = self.protect(valuesafe)
                else:
                    escape0 = False
                valuesafe, escape = param.escape(valuesafe)
                escape = escape or escape0
                # replace "^" (Matlab, Lammps exponent) by "**" (Python syntax)
                valuesafe = pstr.eval(valuesafe.replace("^","**"),ispstr=ispstr)
                # Remove all content after #
                # if the first character is '#', it is not comment (e.g. MarkDown titles)
                poscomment = valuesafe.find("#")
                if poscomment>0: valuesafe = valuesafe[0:poscomment].strip()
                # Literal string starts with $
                if not self._evaluation:
                    tmp.setattr(key, pstr.eval(tmp.format(valuesafe,escape),ispstr=ispstr))
                elif valuesafe.startswith("$") and not escape:
                    tmp.setattr(key,tmp.format(valuesafe[1:].lstrip())) # discard $
                elif valuesafe.startswith("%"):
                    tmp.setattr(key,tmp.format(valuesafe[1:].lstrip())) # discard %
                else: # string empty or which can be evaluated
                    if valuesafe=="":
                        tmp.setattr(key,valuesafe) # empty content
                    else:
                        if isinstance(value,pstr): # keep path
                            tmp.setattr(key, pstr.topath(tmp.format(valuesafe,escape=escape)))
                        elif escape:  # partial evaluation
                            tmp.setattr(key, tmp.format(valuesafe,escape=True))
                        else: # full evaluation
                            try:
                                resstr = tmp.format(valuesafe,raiseerror=False)
                            except (KeyError,NameError) as nameerr:
                                if self._returnerror: # added on 2024-09-06
                                    strnameerr = str(nameerr).replace("'","")
                                    tmp.setattr(key,'< undef %s "${%s}" >' % \
                                            (self._ftype,strnameerr))
                                else:
                                    tmp.setattr(key,value) #we keep the original value
                            except (SyntaxError,TypeError,ValueError) as commonerr:
                                tmp.setattr(key,"ERROR < %s >" % commonerr)
                            except Exception as othererr:
                                tmp.setattr(key,"Unknown Error < %s >" % othererr)
                            else:
                                try:
                                    reseval = eval(resstr)
                                except Exception as othererr:
                                    tmp.setattr(key,"Eval Error < %s >" % othererr)
                                else:
                                    tmp.setattr(key,reseval)
            elif isinstance(value,(int,float,list,tuple)): # already a number
                tmp.setattr(key, value) # store the value with the key
            else: # unsupported types
                if s.find("{"+key+"}")>=0:
                    print(f'*** WARNING ***\n\tIn the {self._ftype}:"\n{s}\n"')
                else:
                    print(f'unable to interpret the "{key}" of type {type(value)}')
        return tmp

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

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

                where:
                    p is a param object

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

        """
        tmp = self.eval(s,protection=protection)
        # Do all replacements in s (keep comments)
        if len(tmp)==0:
            return s
        else:
            ispstr = isinstance(s,pstr)
            ssafe, escape = param.escape(s)
            slines = ssafe.split("\n")
            for i in range(len(slines)):
                poscomment = slines[i].find("#")
                if poscomment>=0:
                    while (poscomment>0) and (slines[i][poscomment-1]==" "):
                        poscomment -= 1
                    comment = slines[i][poscomment:len(slines[i])]
                    slines[i]  = slines[i][0:poscomment]
                else:
                    comment = ""
                # Protect variables if required
                if protection or self._protection:
                    slines[i], escape2 = self.protect(slines[i])
                # conversion
                if ispstr:
                    slines[i] = pstr.eval(tmp.format(slines[i],escape=escape),ispstr=ispstr)
                else:
                    slines[i] = tmp.format(slines[i],escape=escape)+comment
                # convert starting % into # to authorize replacement in comments
                if len(slines[i])>0:
                    if slines[i][0] == "%": slines[i]="#"+slines[i][1:]
            return "\n".join(slines)

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

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

Ancestors

  • pizza.private.mstruct.struct

Subclasses

  • pizza.private.mstruct.paramauto
  • pizza.script.scriptdata

Static methods

def escape(s)

escape \${} as ${{}} –> keep variable names convert ${} as {} –> prepare Python replacement

Examples

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

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

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

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

Expand source code
@staticmethod
def escape(s):
    """
        escape \${} as ${{}} --> keep variable names
        convert ${} as {} --> prepare Python replacement

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

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

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

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

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

Methods

def eval(self, s='', protection=False)

Eval method for structure such as MS.alias

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

where :
    p is a param object
    s is a structure with evaluated fields
    string is only used to determine whether definitions have been forgotten
Expand source code
def eval(self,s="",protection=False):
    """
        Eval method for structure such as MS.alias

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

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

    """
    # Evaluate all DEFINITIONS
    # the argument s is only used by formateval() for error management
    tmp = struct()
    for key,value in self.items():
        # strings are assumed to be expressions on one single line
        if isinstance(value,str):
            # replace ${variable} (Bash, Lammps syntax) by {variable} (Python syntax)
            # use \${variable} to prevent replacement (espace with \)
            # Protect variables if required
            ispstr = isinstance(value,pstr)
            valuesafe = pstr.eval(value,ispstr=ispstr) # value.strip()
            if protection or self._protection:
                valuesafe, escape0 = self.protect(valuesafe)
            else:
                escape0 = False
            valuesafe, escape = param.escape(valuesafe)
            escape = escape or escape0
            # replace "^" (Matlab, Lammps exponent) by "**" (Python syntax)
            valuesafe = pstr.eval(valuesafe.replace("^","**"),ispstr=ispstr)
            # Remove all content after #
            # if the first character is '#', it is not comment (e.g. MarkDown titles)
            poscomment = valuesafe.find("#")
            if poscomment>0: valuesafe = valuesafe[0:poscomment].strip()
            # Literal string starts with $
            if not self._evaluation:
                tmp.setattr(key, pstr.eval(tmp.format(valuesafe,escape),ispstr=ispstr))
            elif valuesafe.startswith("$") and not escape:
                tmp.setattr(key,tmp.format(valuesafe[1:].lstrip())) # discard $
            elif valuesafe.startswith("%"):
                tmp.setattr(key,tmp.format(valuesafe[1:].lstrip())) # discard %
            else: # string empty or which can be evaluated
                if valuesafe=="":
                    tmp.setattr(key,valuesafe) # empty content
                else:
                    if isinstance(value,pstr): # keep path
                        tmp.setattr(key, pstr.topath(tmp.format(valuesafe,escape=escape)))
                    elif escape:  # partial evaluation
                        tmp.setattr(key, tmp.format(valuesafe,escape=True))
                    else: # full evaluation
                        try:
                            resstr = tmp.format(valuesafe,raiseerror=False)
                        except (KeyError,NameError) as nameerr:
                            if self._returnerror: # added on 2024-09-06
                                strnameerr = str(nameerr).replace("'","")
                                tmp.setattr(key,'< undef %s "${%s}" >' % \
                                        (self._ftype,strnameerr))
                            else:
                                tmp.setattr(key,value) #we keep the original value
                        except (SyntaxError,TypeError,ValueError) as commonerr:
                            tmp.setattr(key,"ERROR < %s >" % commonerr)
                        except Exception as othererr:
                            tmp.setattr(key,"Unknown Error < %s >" % othererr)
                        else:
                            try:
                                reseval = eval(resstr)
                            except Exception as othererr:
                                tmp.setattr(key,"Eval Error < %s >" % othererr)
                            else:
                                tmp.setattr(key,reseval)
        elif isinstance(value,(int,float,list,tuple)): # already a number
            tmp.setattr(key, value) # store the value with the key
        else: # unsupported types
            if s.find("{"+key+"}")>=0:
                print(f'*** WARNING ***\n\tIn the {self._ftype}:"\n{s}\n"')
            else:
                print(f'unable to interpret the "{key}" of type {type(value)}')
    return tmp
def formateval(self, s, protection=False)

format method with evaluation feature

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

where:
    p is a param object

Example:
    definitions = param(a=1,b="${a}",c="\${a}")
    text = definitions.formateval("this my text ${a}, ${b}, ${c}")
    print(text)
Expand source code
def formateval(self,s,protection=False):
    """
        format method with evaluation feature

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

            where:
                p is a param object

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

    """
    tmp = self.eval(s,protection=protection)
    # Do all replacements in s (keep comments)
    if len(tmp)==0:
        return s
    else:
        ispstr = isinstance(s,pstr)
        ssafe, escape = param.escape(s)
        slines = ssafe.split("\n")
        for i in range(len(slines)):
            poscomment = slines[i].find("#")
            if poscomment>=0:
                while (poscomment>0) and (slines[i][poscomment-1]==" "):
                    poscomment -= 1
                comment = slines[i][poscomment:len(slines[i])]
                slines[i]  = slines[i][0:poscomment]
            else:
                comment = ""
            # Protect variables if required
            if protection or self._protection:
                slines[i], escape2 = self.protect(slines[i])
            # conversion
            if ispstr:
                slines[i] = pstr.eval(tmp.format(slines[i],escape=escape),ispstr=ispstr)
            else:
                slines[i] = tmp.format(slines[i],escape=escape)+comment
            # convert starting % into # to authorize replacement in comments
            if len(slines[i])>0:
                if slines[i][0] == "%": slines[i]="#"+slines[i][1:]
        return "\n".join(slines)
def protect(self, s='')

protect $variable as ${variable}

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

convert dynamic a param() object to a static struct() object. note: no interpretation note: use tostruct() to interpret them and convert it to struct note: tostatic().struct2param() makes it reversible

Expand source code
def tostatic(self):
    """ convert dynamic a param() object to a static struct() object.
        note: no interpretation
        note: use tostruct() to interpret them and convert it to struct
        note: tostatic().struct2param() makes it reversible
    """
    return struct.fromkeysvalues(self.keys(),self.values(),makeparam=False)
def tostruct(self, protection=False)

generate the evaluated structure tostruct(protection=False)

Expand source code
def tostruct(self,protection=False):
    """
        generate the evaluated structure
            tostruct(protection=False)
    """
    return self.eval(protection=protection)
class paramauto (sortdefinitions=False, **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

  • pizza.dscript.lambdaScriptdata
  • pizza.forcefield.parameterforcefield
  • pizza.region.regiondata
  • regiondata
class parameterforcefield (sortdefinitions=False, **kwargs)

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

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

Parameters:

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

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

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

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

Ancestors

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

Subclasses

  • pizza.generic.genericdata
class pipescript (s=None, name=None, printflag=False, verbose=True, verbosity=None)

pipescript: A Class for Managing Script Pipelines

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

Key Features:

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

Practical Use Cases:

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

Methods:

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

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

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

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

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

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

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

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

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

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

Class: pstr

A specialized string class for handling paths and filenames, derived from struct. The pstr class ensures compatibility with POSIX-style paths and provides enhanced operations for path manipulation.


Features

  • Maintains POSIX-style paths.
  • Automatically handles trailing slashes.
  • Supports path concatenation using /.
  • Converts seamlessly back to str for compatibility with string methods.
  • Includes additional utility methods for path evaluation and formatting.

Examples

Basic Usage

a = pstr("this/is/mypath//")
b = pstr("mylocalfolder/myfile.ext")
c = a / b
print(c)  # this/is/mypath/mylocalfolder/myfile.ext

Keeping Trailing Slashes

a = pstr("this/is/mypath//")
print(a)  # this/is/mypath/

Path Operations

Path Concatenation

Use the / operator to concatenate paths:

a = pstr("folder/subfolder")
b = pstr("file.txt")
c = a / b
print(c)  # folder/subfolder/file.txt

Path Evaluation

Evaluate or convert paths while preserving the pstr type:

result = pstr.eval("some/path/afterreplacement", ispstr=True)
print(result)  # some/path/afterreplacement

Advanced Usage

Using String Methods

Methods like replace() convert pstr back to str. To retain the pstr type:

new_path = pstr.eval(a.replace("mypath", "newpath"), ispstr=True)
print(new_path)  # this/is/newpath/

Handling POSIX Paths

The pstr.topath() method ensures the path remains POSIX-compliant:

path = pstr("C:\Windows\Path")
posix_path = path.topath()
print(posix_path)  # C:/Windows/Path

Overloaded Operators

Supported Operators

  • /: Concatenates two paths (__truediv__).
  • +: Concatenates strings as paths, resulting in a pstr object (__add__).
  • +=: Adds to an existing pstr object (__iadd__).

Utility Methods

Method Description
eval(value) Evaluates the path or string for compatibility with pstr.
topath() Returns the POSIX-compliant path.

Notes

  • Use pstr for consistent and safe handling of file paths across different platforms.
  • Converts back to str when using non-pstr specific methods to ensure compatibility.
Expand source code
class pstr(str):
    """
    Class: `pstr`
    =============

    A specialized string class for handling paths and filenames, derived from `struct`.
    The `pstr` class ensures compatibility with POSIX-style paths and provides enhanced
    operations for path manipulation.

    ---

    ### Features
    - Maintains POSIX-style paths.
    - Automatically handles trailing slashes.
    - Supports path concatenation using `/`.
    - Converts seamlessly back to `str` for compatibility with string methods.
    - Includes additional utility methods for path evaluation and formatting.

    ---

    ### Examples

    #### Basic Usage
    ```python
    a = pstr("this/is/mypath//")
    b = pstr("mylocalfolder/myfile.ext")
    c = a / b
    print(c)  # this/is/mypath/mylocalfolder/myfile.ext
    ```

    #### Keeping Trailing Slashes
    ```python
    a = pstr("this/is/mypath//")
    print(a)  # this/is/mypath/
    ```

    ---

    ### Path Operations

    #### Path Concatenation
    Use the `/` operator to concatenate paths:
    ```python
    a = pstr("folder/subfolder")
    b = pstr("file.txt")
    c = a / b
    print(c)  # folder/subfolder/file.txt
    ```

    #### Path Evaluation
    Evaluate or convert paths while preserving the `pstr` type:
    ```python
    result = pstr.eval("some/path/afterreplacement", ispstr=True)
    print(result)  # some/path/afterreplacement
    ```

    ---

    ### Advanced Usage

    #### Using String Methods
    Methods like `replace()` convert `pstr` back to `str`. To retain the `pstr` type:
    ```python
    new_path = pstr.eval(a.replace("mypath", "newpath"), ispstr=True)
    print(new_path)  # this/is/newpath/
    ```

    #### Handling POSIX Paths
    The `pstr.topath()` method ensures the path remains POSIX-compliant:
    ```python
    path = pstr("C:\\Windows\\Path")
    posix_path = path.topath()
    print(posix_path)  # C:/Windows/Path
    ```

    ---

    ### Overloaded Operators

    #### Supported Operators
    - `/`: Concatenates two paths (`__truediv__`).
    - `+`: Concatenates strings as paths, resulting in a `pstr` object (`__add__`).
    - `+=`: Adds to an existing `pstr` object (`__iadd__`).

    ---

    ### Utility Methods

    | Method          | Description                                  |
    |------------------|----------------------------------------------|
    | `eval(value)`    | Evaluates the path or string for compatibility with `pstr`. |
    | `topath()`       | Returns the POSIX-compliant path.           |

    ---

    ### Notes
    - Use `pstr` for consistent and safe handling of file paths across different platforms.
    - Converts back to `str` when using non-`pstr` specific methods to ensure compatibility.
    """

    def __repr__(self):
        result = self.topath()
        if result[-1] != "/" and self[-1] == "/":
            result += "/"
        return result

    def topath(self):
        """ return a validated path """
        value = pstr(PurePath(self))
        if value[-1] != "/" and self [-1]=="/":
            value += "/"
        return value


    @staticmethod
    def eval(value,ispstr=False):
        """ evaluate the path of it os a path """
        if isinstance(value,pstr):
            return value.topath()
        elif isinstance(value,PurePath) or ispstr:
            return pstr(value).topath()
        else:
            return value

    def __truediv__(self,value):
        """ overload / """
        operand = pstr.eval(value)
        result = pstr(PurePath(self) / operand)
        if result[-1] != "/" and operand[-1] == "/":
            result += "/"
        return result

    def __add__(self,value):
        return pstr(str(self)+value)

    def __iadd__(self,value):
        return pstr(str(self)+value)

Ancestors

  • builtins.str

Static methods

def eval(value, ispstr=False)

evaluate the path of it os a path

Expand source code
@staticmethod
def eval(value,ispstr=False):
    """ evaluate the path of it os a path """
    if isinstance(value,pstr):
        return value.topath()
    elif isinstance(value,PurePath) or ispstr:
        return pstr(value).topath()
    else:
        return value

Methods

def topath(self)

return a validated path

Expand source code
def topath(self):
    """ return a validated path """
    value = pstr(PurePath(self))
    if value[-1] != "/" and self [-1]=="/":
        value += "/"
    return value
class region (name='region container', dimension=3, boundary=None, nbeads=1, units='', mass=1.0, volume=1.0, density=1.0, radius=1.5, contactradius=0.5, velocities=[0.0, 0.0, 0.0], forces=[0.0, 0.0, 0.0], filename='', previewfilename='', index=None, run=1, center=[0.0, 0.0, 0.0], width=10.0, height=10.0, depth=10.0, hasfixmove=False, spacefilling=False, fillingbeadtype=1, boxid='box', regionunits='lattice', separationdistance=5e-06, lattice_scale=0.8442, lattice_spacing=None, lattice_style='fcc', atom_style='smd', atom_modify=['map', 'array'], comm_modify=['vel', 'yes'], neigh_modify=['every', 10, 'delay', 0, 'check', 'yes'], newton='off', live_units='lj', live_atom_style='atomic', livepreview_options={'static': {'run': 1}, 'dynamic': {'run': 100}}, printflag=False, verbose=True, verbosity=None)

The region class represents a simulation region, centered at the origin (0, 0, 0) by default, and is characterized by its physical dimensions, properties, and boundary conditions. It supports setting up lattice structures, particle properties, and options for live previews.

Attributes:

name : str, optional Name of the region (default is 'region container').

dimension : int, optional Number of spatial dimensions for the simulation (either 2 or 3, default is 3).

boundary : list of str or None, optional Boundary conditions for each dimension. If None, defaults to ["sm"] * dimension. Must be a list of length dimension, where "s" indicates shrink-wrapped, and "m" indicates a non-periodic boundary.

nbeads : int, optional Number of beads in the region (default is 1).

units : str, optional Units for the simulation box (default is "").

Particle Properties:

mass : float, optional Mass of particles in the region (default is 1).

volume : float, optional Volume of the region (default is 1).

density : float, optional Density of the region (default is 1).

radius : float, optional Radius of the particles (default is 1.5).

contactradius : float, optional Contact radius of the particles (default is 0.5).

velocities : list of floats, optional Initial velocities of particles (default is [0, 0, 0]).

forces : list of floats, optional External forces acting on the particles (default is [0, 0, 0]).

Other Properties:

filename : str, optional Name of the output file (default is an empty string, which will auto-generate a name based on the region name).

index : int, optional Index or identifier for the region.

run : int, optional Run configuration parameter (default is 1).

Box Properties:

center : list of floats, optional Center of the simulation box for coordinate scaling (default is [0, 0, 0]).

width : float, optional Width of the region (default is 10).

height : float, optional Height of the region (default is 10).

depth : float, optional Depth of the region (default is 10).

hasfixmove : bool, optional Indicates whether the region has a fixed movement (default is False).

Spacefilling Design:

spacefilling : bool, optional Indicates whether the design is space-filling (default is False).

fillingbeadtype : int, optional Type of bead used for space filling (default is 1).

Lattice Properties:

regionunits : str, optional Defines the units of the region. Can be either "lattice" (default) or "si".

separationdistance : float, optional Separation distance between atoms in SI units (default is 5e-6).

lattice_scale : float, optional Scaling factor for the lattice, used mainly in visualization (default is 0.8442).

lattice_spacing : list or None, optional Specifies the spacing between lattice points. If None, the default spacing is used. Can be a list of [dx, dy, dz].

lattice_style : str, optional Specifies the lattice structure style (default is "fcc"). Accepts any LAMMPS valid style, e.g., "sc" for simple cubic.

Atom Properties:

atom_style : str, optional Defines the atom style for the region (default is "smd").

atom_modify : list of str, optional LAMMPS command for atom modification (default is ["map", "array"]).

comm_modify : list of str, optional LAMMPS command for communication modification (default is ["vel", "yes"]).

neigh_modify : list, optional LAMMPS command for neighbor list modification (default is ["every", 10, "delay", 0, "check", "yes"]).

newton : str, optional Specifies the Newton flag (default is "off").

Live Preview:

live_units : str, optional Units for live preview (default is "lj", for Lennard-Jones units).

live_atom_style : str, optional Atom style used specifically for live LAMMPS sessions (default is "atomic").

livepreview_options : dict, optional Contains options for live preview. The dictionary includes 'static' (default: run = 1) and 'dynamic' (default: run = 100) options.

Methods:

init : Constructor method to initialize all the attributes of the region class.

constructor

Expand source code
class region:
    """
    The `region` class represents a simulation region, centered at the origin (0, 0, 0) by default,
    and is characterized by its physical dimensions, properties, and boundary conditions. It supports
    setting up lattice structures, particle properties, and options for live previews.

    Attributes:
    ----------
    name : str, optional
        Name of the region (default is 'region container').

    dimension : int, optional
        Number of spatial dimensions for the simulation (either 2 or 3, default is 3).

    boundary : list of str or None, optional
        Boundary conditions for each dimension. If None, defaults to ["sm"] * dimension.
        Must be a list of length `dimension`, where "s" indicates shrink-wrapped, and "m" indicates a non-periodic boundary.

    nbeads : int, optional
        Number of beads in the region (default is 1).

    units : str, optional
        Units for the simulation box (default is "").

    Particle Properties:
    -------------------
    mass : float, optional
        Mass of particles in the region (default is 1).

    volume : float, optional
        Volume of the region (default is 1).

    density : float, optional
        Density of the region (default is 1).

    radius : float, optional
        Radius of the particles (default is 1.5).

    contactradius : float, optional
        Contact radius of the particles (default is 0.5).

    velocities : list of floats, optional
        Initial velocities of particles (default is [0, 0, 0]).

    forces : list of floats, optional
        External forces acting on the particles (default is [0, 0, 0]).

    Other Properties:
    ----------------
    filename : str, optional
        Name of the output file (default is an empty string, which will auto-generate a name based on the region name).

    index : int, optional
        Index or identifier for the region.

    run : int, optional
        Run configuration parameter (default is 1).

    Box Properties:
    ---------------
    center : list of floats, optional
        Center of the simulation box for coordinate scaling (default is [0, 0, 0]).

    width : float, optional
        Width of the region (default is 10).

    height : float, optional
        Height of the region (default is 10).

    depth : float, optional
        Depth of the region (default is 10).

    hasfixmove : bool, optional
        Indicates whether the region has a fixed movement (default is False).

    Spacefilling Design:
    -------------------
    spacefilling : bool, optional
        Indicates whether the design is space-filling (default is False).

    fillingbeadtype : int, optional
        Type of bead used for space filling (default is 1).

    Lattice Properties:
    ------------------
    regionunits : str, optional
        Defines the units of the region. Can be either "lattice" (default) or "si".

    separationdistance : float, optional
        Separation distance between atoms in SI units (default is 5e-6).

    lattice_scale : float, optional
        Scaling factor for the lattice, used mainly in visualization (default is 0.8442).

    lattice_spacing : list or None, optional
        Specifies the spacing between lattice points. If None, the default spacing is used. Can be a list of [dx, dy, dz].

    lattice_style : str, optional
        Specifies the lattice structure style (default is "fcc"). Accepts any LAMMPS valid style, e.g., "sc" for simple cubic.

    Atom Properties:
    ----------------
    atom_style : str, optional
        Defines the atom style for the region (default is "smd").

    atom_modify : list of str, optional
        LAMMPS command for atom modification (default is ["map", "array"]).

    comm_modify : list of str, optional
        LAMMPS command for communication modification (default is ["vel", "yes"]).

    neigh_modify : list, optional
        LAMMPS command for neighbor list modification (default is ["every", 10, "delay", 0, "check", "yes"]).

    newton : str, optional
        Specifies the Newton flag (default is "off").

    Live Preview:
    ------------
    live_units : str, optional
        Units for live preview (default is "lj", for Lennard-Jones units).

    live_atom_style : str, optional
        Atom style used specifically for live LAMMPS sessions (default is "atomic").

    livepreview_options : dict, optional
        Contains options for live preview. The dictionary includes 'static' (default: run = 1) and 'dynamic' (default: run = 100) options.

    Methods:
    -------
    __init__ :
        Constructor method to initialize all the attributes of the `region` class.
    """

    _version = "0.9997"
    __custom_documentations__ = "pizza.region.region class"

    # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
    #
    # CONSTRUCTOR METHOD
    #
    #
    # The constructor include
    #   the main container: objects (a dictionnary)
    #   several attributes covering current and future use of PIZZA.REGION()
    #
    # The original constructor is derived from PIZZA.RASTER() with
    # an intent to allow at some point some forward and backward port between
    # objects of the class PIZZA.RASTER() and PIZZA.REGION().
    #
    # The code will evolve according to the needs, please come back regularly.
    #
    # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-

    # CONSTRUCTOR ----------------------------
    def __init__(self,
                 # container properties
                 name="region container",
                 dimension = 3,
                 boundary = None,
                 nbeads=1,
                 units = "",

                 # particle properties
                 mass=1.0,
                 volume=1.0,
                 density=1.0,
                 radius=1.5,
                 contactradius=0.5,
                 velocities=[0.0,0.0,0.0],
                 forces=[0.0,0.0,0.0],

                 # other properties
                 filename="",
                 previewfilename="",
                 index = None,
                 run=1,

                 # Box lengths
                 center = [0.0,0.0,0.0],    # center of the box for coordinates scaling
                 width = 10.0,  # along x
                 height = 10.0, # along y
                 depth = 10.0,  # along z
                 hasfixmove = False, # by default no fix move

                 # Spacefilling design (added on 2023-08-10)
                 spacefilling = False,
                 fillingbeadtype = 1,

                 # Lattice properties
                 boxid = "box",             # default value for ${boxid_arg}
                 regionunits = "lattice",   # units ("lattice" or "si")
                 separationdistance = 5e-6, # SI units
                 lattice_scale = 0.8442,    # LJ units (for visualization)
                 lattice_spacing = None,    # lattice spacing is not used by default (set [dx dy dz] if needed)
                 lattice_style = "fcc" ,    # any valid lattice style accepted by LAMMPS (sc=simple cubic)

                 # Atom properties
                 atom_style = "smd",
                 atom_modify = ["map","array"],
                 comm_modify = ["vel","yes"],
                 neigh_modify = ["every",10,"delay",0,"check","yes"],
                 newton ="off",

                 # Live preview
                 live_units = "lj",         # units to be used ONLY with livelammps (https://andeplane.github.io/atomify/)
                 live_atom_style = "atomic",# atom style to be used ONLY with livelammps (https://andeplane.github.io/atomify/)

                 # livepreview options
                 livepreview_options = {
                     'static':{'run':1},
                     'dynamic':{'run':100}
                     },

                 # common flags (for scripting)
                 printflag = False,
                 verbose = True,
                 verbosity = None

                 ):
        """ constructor """
        self.name = name

        # Ensure dimension is an integer (must be 2 or 3 for LAMMPS)
        if not isinstance(dimension, int) or dimension not in (2, 3):
            raise ValueError("dimension must be either 2 or 3.")

        # Handle boundary input
        if boundary is None:
            boundary = ["sm"] * dimension
        elif isinstance(boundary, list):
            if len(boundary) != dimension:
                raise ValueError(f"The length of boundary ({len(boundary)}) must match the dimension ({dimension}).")
        else:
            raise ValueError("boundary must be a list of strings or None.")

        # Validate regionunits
        if regionunits not in ("lattice", "si"):
            raise ValueError("regionunits can only be 'lattice' or 'si'.")

        # Lattice scaling logic
        lattice_scale_siunits = lattice_scale if regionunits == "si" else separationdistance
        if lattice_scale_siunits is None or lattice_scale_siunits=="":
            lattice_scale_siunits = separationdistance
        if lattice_spacing == "":
            lattice_spacing = None
        elif isinstance(lattice_spacing, (int, float)):
            lattice_spacing = [lattice_spacing] * dimension
        elif isinstance(lattice_spacing, list):
            lattice_spacing = lattice_spacing + [lattice_spacing[-1]] * (dimension - len(lattice_spacing)) if len(lattice_spacing) < dimension else lattice_spacing[:dimension]

        # live data (updated 2024-07-04)
        live_lattice_scale = lattice_scale/separationdistance if regionunits == "si" else lattice_scale
        live_box_scale = 1/lattice_scale_siunits if regionunits == "si" else 1
        self.live = regiondata(nbeads=nbeads,
                               run=run,
                               width=math.ceil(width*live_box_scale),    # live_box_scale force lattice units for live visualization
                               height=math.ceil(height*live_box_scale),  # live_box_scale force lattice units for live visualization
                               depth=math.ceil(depth*live_box_scale),    # live_box_scale force lattice units for live visualization
                               live_units = "$"+live_units,
                               live_atom_style = "$"+live_atom_style,
                               live_lattice_style="$"+lattice_style,
                               live_lattice_scale=live_lattice_scale)
        # generic SMD properties (to be rescaled)
        self.volume = volume
        self.mass = mass
        self.density = density
        self.radius = radius
        self.contactradius = contactradius
        self.velocities = velocities
        self.forces = forces
        if filename == "":
            self.filename = f"region_{self.name}"
        else:
            self.filename = filename
        self.index = index
        self.objects = {}    # object container
        self.nobjects = 0    # total number of objects (alive)
        # count objects per type
        self.counter = {
                  "ellipsoid":0,
                  "block":0,
                  "sphere":0,
                  "cone":0,
                  "cylinder":0,
                  "prism":0,
                  "plane":0,
                  "union":0,
                  "intersect":0,
                  "eval":0,
                  "collection":0,
                  "all":0
            }
        # fix move flag
        self.hasfixmove = hasfixmove
        # livelammps (for live sessions) - added 2023-02-06
        self.livelammps = {
            "URL": livelammpsURL,
         "active": False,
           "file": None,
        "options": livepreview_options
            }
        # space filling  (added 2023-08-10)
        self.spacefilling = {
                   "flag": spacefilling,
           "fillingstyle": "$block",
        "fillingbeadtype": fillingbeadtype,
           "fillingwidth": width,
          "fillingheight": height,
           "fillingdepth": depth,
           "fillingunits": units
               }
        # region object units
        self.regionunits = regionunits
        # lattice
        self.units = units
        self.center = center
        self.separationdistance = separationdistance
        self.lattice_scale = lattice_scale
        self.lattice_spacing = lattice_spacing
        self.lattice_scale_siunits = lattice_scale_siunits
        self.lattice_style = lattice_style
        # headers for header scripts (added 2024-09-01)
        # geometry is assumed to be units set by ${boxunits_arg} (new standard 2024-11-26)
        self.headersData = headersRegiondata(
            # use $ and [] to prevent execution
            name = "$"+name,
            previewfilename = "$dump.initial."+self.filename if previewfilename=="" else "$"+previewfilename,
            # Initialize Lammps
            dimension = dimension,
            units = "$"+units,
            boundary = boundary,
            atom_style = "$" + atom_style,
            atom_modify = atom_modify,
            comm_modify = comm_modify,
            neigh_modify = neigh_modify,
            newton ="$" + newton,
            # Box (added 2024-11-26)
            boxid = "$"+boxid,
            boxunits_arg = "$units box" if regionunits=="si" else "", # standard on 2025-11-26
            # Lattice
            lattice_style = "$"+lattice_style,
            lattice_scale = lattice_scale,
            lattice_spacing = lattice_spacing,
            # Box
            xmin = -(width/2)  +center[0],
            xmax = +(width/2)   +center[0],
            ymin = -(height/2) +center[1],
            ymax = +(height/2) +center[1],
            zmin = -(depth/2)  +center[2],
            zmax = +(depth/2)  +center[2],
            nbeads = nbeads,
            mass = mass
            )
        self.printflag = printflag
        self.verbose = verbose if verbosity is None else verbosity>0
        self.verbosity = 0 if not verbose else verbosity

    # Method for coordinate/length scaling and translation including with formula embedded strings (updated 2024-07-03, fixed 2024-07-04)
    # Note that the translation is not fully required since the scaling applies also to full coordinates.
    # However, an implementation is provided for arbitrary offset.
    def scale_and_translate(self, value, offset=0):
        """
        Scale and translate a value or encapsulate the formula within a string.

        If self.regionunits is "si", only the offset is applied without scaling.
        Otherwise, scaling and translation are performed based on self.units ("si" or "lattice").

        Parameters:
            value (str or float): The value or formula to be scaled and translated.
            offset (float, optional): The offset to apply. Defaults to 0.

        Returns:
            str or float: The scaled and translated value or formula.
        """
        if self.regionunits == "si":
            # Only apply offset without scaling
            if isinstance(value, str):
                if offset:
                    translated = f"({value}) - {offset}"
                else:
                    translated = f"{value}"
                return translated
            else:
                if offset:
                    return value - offset
                else:
                    return value
        else:
            # Existing behavior based on self.units
            if isinstance(value, str):
                if offset:
                    translated = f"({value}) - {offset}"
                else:
                    translated = f"{value}"
                if self.units == "si":
                    return f"({translated}) / {self.lattice_scale} + {offset / self.lattice_scale}"
                else:  # "lattice"
                    return f"({translated}) * {self.lattice_scale} + {offset * self.lattice_scale}"
            else:
                if offset:
                    translated = value - offset
                else:
                    translated = value
                if self.units == "si":
                    return translated / self.lattice_scale + (offset / self.lattice_scale)
                else:  # "lattice"
                    return translated * self.lattice_scale + (offset * self.lattice_scale)



    # space filling attributes (cannot be changed)
    @property
    def isspacefilled(self):
        return self.spacefilling["flag"]

    @property
    def spacefillingbeadtype(self):
        return self.spacefilling["fillingbeadtype"]

    # total number of atoms in the region
    @property
    def natoms(self):
        """Count the total number of atoms in all objects within the region."""
        total_atoms = 0
        for eachobj in self:
            total_atoms += eachobj.natoms
        return total_atoms

    # details if the geometry of the region
    @property
    def geometry(self):
        """Display the dimensions and characteristics of the region and its objects."""
        details = f"Region: {self.name}\n"
        details += f"Total atoms: {self.natoms}\n"
        details += f"Span: width={self.spacefilling['fillingwidth']}, height={self.spacefilling['fillingheight']}, depth={self.spacefilling['fillingdepth']}\n"
        details += f"Box center: {self.center}\n"
        details += "Objects in the region:\n\n"
        for obj in self:
            details += "\n\n"+"-"*32+"\n"
            details += f"\nObject: {obj.name}\n"
            details += f"Type: {type(obj).__name__}\n"
            if hasattr(obj, 'geometry'):
                details += "\n"+"-"*32+"\n"
                details += obj.geometry
            else:
                details += "No geometry information available.\n"
        print(details)

    # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
    #
    # REGION.GEOMETRY constructors
    #
    #
    #   These methods create the 3D geometry objects (at least their code)
    #   A geometry is a collection of PIZZA.SCRIPT() objects (LAMMPS codelet)
    #   not a real geometry. The distinction between the creation (definition)
    #   and the execution (generation) of the gometry object existed already
    #   in PIZZA.RASTER(), but here they remain codelets as ONLY LAMMPS can
    #   generate the real object.
    #
    #   This level of abstraction makes it possible to mix PIZZA variables
    #   (USER, PIZZA.SCRIPT.USER, PIZZA.PIPESCRIPT.USER) with LAMMPS variables.
    #   The same object template can be used in different LAMMPS scripts with
    #   different values and without writting additional Python code.
    #   In shorts: USER fields store PIZZA.SCRIPT() like variables
    #              (they are compiled [statically] before LAMMPS execution)
    #              VARIABLES are defined in the generated LAMMPS script but
    #              created [dynamically] in LAMMPS. Note that these variables
    #              are defined explicitly with the LAMMPS variable command:
    #                   variable name style args ...
    #   Note: static variables can have only one single value for LAMMPS, which
    #         is known before LAMMPS is launched. The others can be assigned
    #         at runtime when LAMMPS is running.
    #   Example with complex definitions
    #       R.ellipsoid(0,0,0,1,1,1,name="E2",side="out",
    #                   move=["left","${up}*3",None],
    #                   up=0.1)
    #       R.E2.VARIABLES.left = '"swiggle(%s,%s,%s)"%(${a},${b},${c})'
    #       R.E2.VARIABLES.a="${b}-5"
    #       R.E2.VARIABLES.b=5
    #       R.E2.VARIABLES.c=100
    #
    #   The methods PIZZA.REGION.DO(), PIZZA.REGION.DOLIVE() compiles
    #   (statically) and generate the corresponding LAMMPS code. The static
    #   compiler accepts hybrid constructions where USER and VARIABLES are
    #   mixed. Any undefined variables will be assumed to be defined elsewhere
    #   in the LAMMPS code.
    #
    #  Current attributes of PIZZA.REGION.OBJECT cover current and future use
    #  of these objects and will allow some point some forward and backward
    #  compatibility with the same PIZZA.RASTER.OBJECT.
    #
    #
    #   References:
    #       https://docs.lammps.org/region.html
    #       https://docs.lammps.org/variable.html
    #       https://docs.lammps.org/create_atoms.html
    #       https://docs.lammps.org/create_box.html
    #
    #
    #   List of implemented geometries (shown here with the LAMMPS syntax)
    #       block args = xlo xhi ylo yhi zlo zhi
    #       cone args = dim c1 c2 radlo radhi lo hi
    #       cylinder args = dim c1 c2 radius lo hi
    #       ellipsoid args = x y z a b c <-- first method to be implemented
    #       plane args = px py pz nx ny n
    #       prism args = xlo xhi ylo yhi zlo zhi xy xz yz
    #       sphere args = x y z radius
    #       union args = N reg-ID1 reg-ID2 ..
    #       intersect args = N reg-ID1 reg-ID2 ...
    #
    # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-

    # BLOCK method ---------------------------
    # block args = xlo xhi ylo yhi zlo zhi
    # xlo,xhi,ylo,yhi,zlo,zhi = bounds of block in all dimensions (distance units)
    def block(self,xlo=-5,xhi=5,ylo=-5,yhi=5,zlo=-5,zhi=5,
                  name=None,beadtype=None,fake=False,
                  mass=None, density=None,
                  side=None,units=None,move=None,rotate=None,open=None,
                  index = None,subindex = None,
                  **variables
                  ):
        """
        creates a block region
            xlo,xhi,ylo,yhi,zlo,zhi = bounds of block in all dimensions (distance units)

            URL: https://docs.lammps.org/region.html

            Main properties = default value
                name = "block001"
            beadtype = 1
                fake = False (use True to test the execution)
     index, subindex = object index and subindex

            Extra properties
                side = "in|out"
               units = "lattice|box" ("box" is forced if regionunits=="si")
                move = "[${v1} ${v2} ${v3}]" or [v1,v2,v3] as a list
                       with v1,v2,v3 equal-style variables for x,y,z displacement
                       of region over time (distance units)
              rotate = string or 1x7 list (see move) coding for vtheta Px Py Pz Rx Ry Rz
                       vtheta = equal-style variable for rotation of region over time (in radians)
                       Px,Py,Pz = origin for axis of rotation (distance units)
                       Rx,Ry,Rz = axis of rotation vector
                open = integer from 1-6 corresponding to face index

            See examples for elliposid()
        """
        # prepare object creation
        kind = "block"
        if index is None: index = self.counter["all"]+1
        if subindex is None: subindex = self.counter[kind]+1
        units = "box" if self.regionunits=="si" and units is None else units  # force box units of regionunits=="si"
        # create the object B with B for block
        obj_mass = mass if mass is not None else self.mass
        obj_density = density if density is not None else self.density
        B = Block((self.counter["all"]+1,self.counter[kind]+1),
                      spacefilling=self.isspacefilled, # added on 2023-08-11
                      mass=obj_mass, density=obj_density, # added on 2024-06-14
                      index=index,subindex=subindex,
                      lattice_style=self.lattice_style,
                      lattice_scale=self.lattice_scale,
                      lattice_scale_siunits=self.lattice_scale_siunits,
                      **variables)
        # feed USER fields
        if name not in (None,""): B.name = name # object name (if not defined, default name will be used)
        if name in self.name: raise NameError('the name "%s" is already used' % name)
        if beadtype is not None: B.beadtype = beadtype # bead type (if not defined, default index will apply)
        B.USER.ID = "$"+B.name        # add $ to prevent its execution
        # geometry args (2024-07-04)  -------------------------------------
        args = [xlo, xhi, ylo, yhi, zlo, zhi]  # args = [....] as defined in the class Block
        args_scaled = [
            self.scale_and_translate(xlo, self.center[0]),
            self.scale_and_translate(xhi, self.center[0]),
            self.scale_and_translate(ylo, self.center[1]),
            self.scale_and_translate(yhi, self.center[1]),
            self.scale_and_translate(zlo, self.center[2]),
            self.scale_and_translate(zhi, self.center[2])
        ]
        if self.units == "si":
            B.USER.args = args_scaled
            B.USER.args_siunits = args
        else:  # "lattice"
            B.USER.args = args
            B.USER.args_siunits = args_scaled
        # geometry
        B.USER.geometry = (
            f"Block Region: {B.name}\n"
            "Coordinates: [xlo,xhi,ylo,yhi,zlo,zhi] = bounds of block in all dimensions"
            f"Coordinates (scaled): {B.USER.args}\n"
            f"Coordinates (SI units): {B.USER.args_siunits}\n"
            f"\talong x: [{B.USER.args[0]}, {B.USER.args[1]}]\n"
            f"\talong y: [{B.USER.args[2]}, {B.USER.args[3]}]\n"
            f"\talong z: [{B.USER.args[4]}, {B.USER.args[5]}]"
        )
        # other attributes  -------------------------------------
        B.USER.beadtype = B.beadtype  # beadtype to be used for create_atoms
        B.USER.side = B.sidearg(side) # extra parameter side
        B.USER.move = B.movearg(move) # move arg
        B.USER.units = B.unitsarg(units) # units
        B.USER.rotate = B.rotatearg(rotate) # rotate
        B.USER.open = B.openarg(open) # open
        # Create the object if not fake
        if fake:
            return B
        else:
            self.counter["all"] += 1
            self.counter[kind] +=1
            self.objects[name] = B
            self.nobjects += 1
            return None

    # CONE method ---------------------------
    # cone args = dim c1 c2 radlo radhi lo hi
    # dim = x or y or z = axis of cone
    # c1,c2 = coords of cone axis in other 2 dimensions (distance units)
    # radlo,radhi = cone radii at lo and hi end (distance units)
    # lo,hi = bounds of cone in dim (distance units)
    def cone(self,dim="z",c1=0,c2=0,radlo=2,radhi=5,lo=-10,hi=10,
                  name=None,beadtype=None,fake=False,
                  mass=None, density=None,
                  side=None,units=None,move=None,rotate=None,open=None,
                  index = None,subindex = None,
                  **variables
                  ):
        """
        creates a cone region
            dim = "x" or "y" or "z" = axis of the cone
                 note: USER, LAMMPS variables are not authorized here
            c1,c2 = coords of cone axis in other 2 dimensions (distance units)
            radlo,radhi = cone radii at lo and hi end (distance units)
            lo,hi = bounds of cone in dim (distance units)

            URL: https://docs.lammps.org/region.html

            Main properties = default value
                name = "cone001"
            beadtype = 1
                fake = False (use True to test the execution)
     index, subindex = object index and subindex

            Extra properties
                side = "in|out"
               units = "lattice|box" ("box" is forced if regionunits=="si")
                move = "[${v1} ${v2} ${v3}]" or [v1,v2,v3] as a list
                       with v1,v2,v3 equal-style variables for x,y,z displacement
                       of region over time (distance units)
              rotate = string or 1x7 list (see move) coding for vtheta Px Py Pz Rx Ry Rz
                       vtheta = equal-style variable for rotation of region over time (in radians)
                       Px,Py,Pz = origin for axis of rotation (distance units)
                       Rx,Ry,Rz = axis of rotation vector
                open = integer from 1-6 corresponding to face index

            See examples for elliposid()
        """
        # prepare object creation
        kind = "cone"
        if index is None: index = self.counter["all"]+1
        if subindex is None: subindex = self.counter[kind]+1
        units = "box" if self.regionunits=="si" and units is None else units  # force box units of regionunits=="si"
        # create the object C with C for cone
        obj_mass = mass if mass is not None else self.mass
        obj_density = density if density is not None else self.density
        C = Cone((self.counter["all"]+1,self.counter[kind]+1),
                      spacefilling=self.isspacefilled, # added on 2023-08-11
                      mass=obj_mass, density=obj_density, # added on 2024-06-14
                      index=index,subindex=subindex,
                      lattice_style=self.lattice_style,
                      lattice_scale=self.lattice_scale,
                      lattice_scale_siunits=self.lattice_scale_siunits,
                      **variables)
        # feed USER fields
        if name not in (None,""): C.name = name # object name (if not defined, default name will be used)
        if name in self.name: raise NameError('the name "%s" is already used' % name)
        if beadtype is not None: C.beadtype = beadtype # bead type (if not defined, default index will apply)
        C.USER.ID = "$"+C.name        # add $ to prevent its execution
        # geometry args (2024-07-04)  -------------------------------------
        args = [dim, c1, c2, radlo, radhi, lo, hi]  # args = [....] as defined in the class Cone
        if dim == "x":  # x-axis
            args_scaled = [
                dim,
                self.scale_and_translate(c1, self.center[1]),
                self.scale_and_translate(c2, self.center[2]),
                self.scale_and_translate(radlo, 0),
                self.scale_and_translate(radhi, 0),
                self.scale_and_translate(lo, self.center[0]),
                self.scale_and_translate(hi, self.center[0])
            ]
        elif dim == "y":  # y-axis
            args_scaled = [
                dim,
                self.scale_and_translate(c1, self.center[0]),
                self.scale_and_translate(c2, self.center[2]),
                self.scale_and_translate(radlo, 0),
                self.scale_and_translate(radhi, 0),
                self.scale_and_translate(lo, self.center[1]),
                self.scale_and_translate(hi, self.center[1])
            ]
        else:  # z-axis
            args_scaled = [
                dim,
                self.scale_and_translate(c1, self.center[0]),
                self.scale_and_translate(c2, self.center[1]),
                self.scale_and_translate(radlo, 0),
                self.scale_and_translate(radhi, 0),
                self.scale_and_translate(lo, self.center[2]),
                self.scale_and_translate(hi, self.center[2])
            ]

        if self.units == "si":
            C.USER.args = args_scaled
            C.USER.args_siunits = args
        else:  # "lattice"
            C.USER.args = args
            C.USER.args_siunits = args_scaled
        # geometry
        C.USER.geometry = (
            f"Cone Region: {C.name}\n"
            "Coordinates: [dim,c1,c2,radlo,radhi,lo,hi] = dimensions of cone\n"
            f"Coordinates (scaled): {C.USER.args}\n"
            f"Coordinates (SI units): {C.USER.args_siunits}\n"
            f"\tdim: {C.USER.args[0]}\n"
            f"\tc1: {C.USER.args[1]}\n"
            f"\tc2: {C.USER.args[2]}\n"
            f"\tradlo: {C.USER.args[3]}\n"
            f"\tradhi: {C.USER.args[4]}\n"
            f"\tlo: {C.USER.args[5]}\n"
            f"\thi: {C.USER.args[6]}"
        )
        # other attributes  -------------------------------------
        C.USER.beadtype = C.beadtype  # beadtype to be used for create_atoms
        C.USER.side = C.sidearg(side) # extra parameter side
        C.USER.move = C.movearg(move) # move arg
        C.USER.units = C.unitsarg(units) # units
        C.USER.rotate = C.rotatearg(rotate) # rotate
        C.USER.open = C.openarg(open) # open
        # Create the object if not fake
        if fake:
            return C
        else:
            self.counter["all"] += 1
            self.counter[kind] +=1
            self.objects[name] = C
            self.nobjects += 1
            return None

    # CYLINDER method ---------------------------
    # cylinder args = dim c1 c2 radius lo hi
    # dim = x or y or z = axis of cylinder
    # c1,c2 = coords of cylinder axis in other 2 dimensions (distance units)
    # radius = cylinder radius (distance units)
    # c1,c2, and radius can be a variable (see below)
    # lo,hi = bounds of cylinder in dim (distance units)
    def cylinder(self,dim="z",c1=0,c2=0,radius=4,lo=-10,hi=10,
                  name=None,beadtype=None,fake=False,
                  mass=None, density=None,
                  side=None,units=None,move=None,rotate=None,open=None,
                  index = None,subindex = None,
                  **variables
                  ):
        """
        creates a cylinder region
              dim = x or y or z = axis of cylinder
              c1,c2 = coords of cylinder axis in other 2 dimensions (distance units)
              radius = cylinder radius (distance units)
              c1,c2, and radius can be a LAMMPS variable
              lo,hi = bounds of cylinder in dim (distance units)

            URL: https://docs.lammps.org/region.html

            Main properties = default value
                name = "cylinder001"
            beadtype = 1
                fake = False (use True to test the execution)
     index, subindex = object index and subindex

            Extra properties
                side = "in|out"
               units = "lattice|box" ("box" is forced if regionunits=="si")
                move = "[${v1} ${v2} ${v3}]" or [v1,v2,v3] as a list
                       with v1,v2,v3 equal-style variables for x,y,z displacement
                       of region over time (distance units)
              rotate = string or 1x7 list (see move) coding for vtheta Px Py Pz Rx Ry Rz
                       vtheta = equal-style variable for rotation of region over time (in radians)
                       Px,Py,Pz = origin for axis of rotation (distance units)
                       Rx,Ry,Rz = axis of rotation vector
                open = integer from 1-6 corresponding to face index

            See examples for elliposid()
        """
        # prepare object creation
        kind = "cylinder"
        if index is None: index = self.counter["all"]+1
        if subindex is None: subindex = self.counter[kind]+1
        units = "box" if self.regionunits=="si" and units is None else units  # force box units of regionunits=="si"
        # create the object C with C for cylinder
        obj_mass = mass if mass is not None else self.mass
        obj_density = density if density is not None else self.density
        C = Cylinder((self.counter["all"]+1,self.counter[kind]+1),
                      spacefilling=self.isspacefilled, # added on 2023-08-11
                      mass=obj_mass, density=obj_density,
                      index=index,subindex=subindex,
                      lattice_style=self.lattice_style,
                      lattice_scale=self.lattice_scale,
                      lattice_scale_siunits=self.lattice_scale_siunits,
                      **variables)
        # feed USER fields
        if name not in (None,""): C.name = name # object name (if not defined, default name will be used)
        if name in self.name: raise NameError('the name "%s" is already used' % name)
        if beadtype is not None: C.beadtype = beadtype # bead type (if not defined, default index will apply)
        C.USER.ID = "$"+C.name        # add $ to prevent its execution
        # geometry args (2024-07-04)  -------------------------------------
        args = [dim, c1, c2, radius, lo, hi]  # args = [....] as defined in the class Cylinder
        if dim == "x":  # x-axis
            args_scaled = [
                dim,
                self.scale_and_translate(c1, self.center[1]),
                self.scale_and_translate(c2, self.center[2]),
                self.scale_and_translate(radius, 0),
                self.scale_and_translate(lo, self.center[0]),
                self.scale_and_translate(hi, self.center[0])
            ]
        elif dim == "y":  # y-axis
            args_scaled = [
                dim,
                self.scale_and_translate(c1, self.center[0]),
                self.scale_and_translate(c2, self.center[2]),
                self.scale_and_translate(radius, 0),
                self.scale_and_translate(lo, self.center[1]),
                self.scale_and_translate(hi, self.center[1])
            ]
        else:  # z-axis
            args_scaled = [
                dim,
                self.scale_and_translate(c1, self.center[0]),
                self.scale_and_translate(c2, self.center[1]),
                self.scale_and_translate(radius, 0),
                self.scale_and_translate(lo, self.center[2]),
                self.scale_and_translate(hi, self.center[2])
            ]
        if self.units == "si":
            C.USER.args = args_scaled
            C.USER.args_siunits = args
        else:  # "lattice"
            C.USER.args = args
            C.USER.args_siunits = args_scaled
        # geometry
        C.USER.geometry = (
            f"Cylinder Region: {C.name}\n"
            "Coordinates: [dim,c1,c2,radius,lo,hi] = dimensions of cylinder\n"
            f"Coordinates (scaled): {C.USER.args}\n"
            f"Coordinates (SI units): {C.USER.args_siunits}\n"
            f"\tdim: {C.USER.args[0]}\n"
            f"\tc1: {C.USER.args[1]}\n"
            f"\tc2: {C.USER.args[2]}\n"
            f"\tradius: {C.USER.args[3]}\n"
            f"\tlo: {C.USER.args[4]}\n"
            f"\thi: {C.USER.args[5]}"
        )
        # other attributes  -------------------------------------
        C.USER.beadtype = C.beadtype  # beadtype to be used for create_atoms
        C.USER.side = C.sidearg(side) # extra parameter side
        C.USER.move = C.movearg(move) # move arg
        C.USER.units = C.unitsarg(units) # units
        C.USER.rotate = C.rotatearg(rotate) # rotate
        C.USER.open = C.openarg(open) # open
        # Create the object if not fake
        if fake:
            return C
        else:
            self.counter["all"] += 1
            self.counter[kind] +=1
            self.objects[name] = C
            self.nobjects += 1
            return None

    # ELLIPSOID method ---------------------------
    # ellipsoid args = x y z a b c
    # x,y,z = center of ellipsoid (distance units)
    # a,b,c = half the length of the principal axes of the ellipsoid (distance units)
    # x,y,z,a,b,c can be variables
    def ellipsoid(self,x=0,y=0,z=0,a=5,b=3,c=2,
                  name=None,beadtype=None,fake=False,
                  mass=None, density=None,
                  side=None,units=None,move=None,rotate=None,open=None,
                  index = None,subindex = None,
                  **variables
                  ):
        """
        creates an ellipsoid region
            ellipsoid(x,y,z,a,b,c [,name=None,beadtype=None,property=value,...])
            x,y,z = center of ellipsoid (distance units)
            a,b,c = half the length of the principal axes of the ellipsoid (distance units)

            URL: https://docs.lammps.org/region.html

            Main properties = default value
                name = "ellipsoid001"
            beadtype = 1
                fake = False (use True to test the execution)
                index, subindex = object index and subindex

            Extra properties
                side = "in|out"
               units = "lattice|box" ("box" is forced if regionunits=="si")
                move = "[${v1} ${v2} ${v3}]" or [v1,v2,v3] as a list
                       with v1,v2,v3 equal-style variables for x,y,z displacement
                       of region over time (distance units)
              rotate = string or 1x7 list (see move) coding for vtheta Px Py Pz Rx Ry Rz
                       vtheta = equal-style variable for rotation of region over time (in radians)
                       Px,Py,Pz = origin for axis of rotation (distance units)
                       Rx,Ry,Rz = axis of rotation vector
                open = integer from 1-6 corresponding to face index


            Examples:
                # example with variables created either at creation or later
                    R = region(name="my region")
                    R.ellipsoid(0, 0, 0, 1, 1, 1,name="E1",toto=3)
                    repr(R.E1)
                    R.E1.VARIABLES.a=1
                    R.E1.VARIABLES.b=2
                    R.E1.VARIABLES.c="(${a},${b},100)"
                    R.E1.VARIABLES.d = '"%s%s" %("test",${c}) # note that test could be replaced by any function'
                # example with extra parameters
                    R.ellipsoid(0,0,0,1,1,1,name="E2",side="out",move=["left","${up}*3",None],up=0.1)
                    R.E2.VARIABLES.left = '"swiggle(%s,%s,%s)"%(${a},${b},${c})'
                    R.E2.VARIABLES.a="${b}-5"
                    R.E2.VARIABLES.b=5
                    R.E2.VARIABLES.c=100
        """
        # prepare object creation
        kind = "ellipsoid"
        if index is None: index = self.counter["all"]+1
        if subindex is None: subindex = self.counter[kind]+1
        units = "box" if self.regionunits=="si" and units is None else units  # force box units of regionunits=="si"
        # create the object E with E for Ellipsoid
        obj_mass = mass if mass is not None else self.mass
        obj_density = density if density is not None else self.density
        E = Ellipsoid((self.counter["all"]+1,self.counter[kind]+1),
                      spacefilling=self.isspacefilled, # added on 2023-08-11
                      mass=obj_mass, density=obj_density,
                      index=index,subindex=subindex,
                      lattice_style=self.lattice_style,
                      lattice_scale=self.lattice_scale,
                      lattice_scale_siunits=self.lattice_scale_siunits,
                      **variables)
        # feed USER fields
        if name not in (None,""): E.name = name # object name (if not defined, default name will be used)
        if name in self.name: raise NameError('the name "%s" is already used' % name)
        if beadtype is not None: E.beadtype = beadtype # bead type (if not defined, default index will apply)
        E.USER.ID = "$"+E.name        # add $ to prevent its execution
        # geometry args (2024-07-04)  -------------------------------------
        args = [x, y, z, a, b, c]  # args = [....] as defined in the class Ellipsoid
        args_scaled = [
            self.scale_and_translate(x, self.center[0]),
            self.scale_and_translate(y, self.center[1]),
            self.scale_and_translate(z, self.center[2]),
            self.scale_and_translate(a, 0),
            self.scale_and_translate(b, 0),
            self.scale_and_translate(c, 0)
        ]
        if self.units == "si":
            E.USER.args = args_scaled
            E.USER.args_siunits = args
        else:  # "lattice"
            E.USER.args = args
            E.USER.args_siunits = args_scaled
        # geometry
        E.USER.geometry = (
            f"Ellipsoid Region: {E.name}\n"
            "Coordinates: [x,y,z,a,b,c] = center and radii of ellipsoid\n"
            f"Coordinates (scaled): {E.USER.args}\n"
            f"Coordinates (SI units): {E.USER.args_siunits}\n"
            f"\tcenter: [{E.USER.args[0]}, {E.USER.args[1]}, {E.USER.args[2]}]\n"
            f"\ta: {E.USER.args[3]}\n"
            f"\tb: {E.USER.args[4]}\n"
            f"\tc: {E.USER.args[5]}"
        )
        # other attributes  -------------------------------------
        E.USER.beadtype = E.beadtype  # beadtype to be used for create_atoms
        E.USER.side = E.sidearg(side) # extra parameter side
        E.USER.move = E.movearg(move) # move arg
        E.USER.units = E.unitsarg(units) # units
        E.USER.rotate = E.rotatearg(rotate) # rotate
        E.USER.open = E.openarg(open) # open
        # Create the object if not fake
        if fake:
            return E
        else:
            self.counter["all"] += 1
            self.counter[kind] +=1
            self.objects[name] = E
            self.nobjects += 1
            return None

    # PLANE method ---------------------------
    # plane args = px py pz nx ny nz
    # px,py,pz = point on the plane (distance units)
    # nx,ny,nz = direction normal to plane (distance units)
    def plane(self,px=0,py=0,pz=0,nx=0,ny=0,nz=1,
                  name=None,beadtype=None,fake=False,
                  side=None,units=None,move=None,rotate=None,open=None,
                  index = None,subindex = None,
                  **variables
                  ):
        """
        creates a plane region
              px,py,pz = point on the plane (distance units)
              nx,ny,nz = direction normal to plane (distance units)

            URL: https://docs.lammps.org/region.html

            Main properties = default value
                name = "plane001"
            beadtype = 1
                fake = False (use True to test the execution)
     index, subindex = object index and subindex

            Extra properties
                side = "in|out"
               units = "lattice|box" ("box" is forced if regionunits=="si")
                move = "[${v1} ${v2} ${v3}]" or [v1,v2,v3] as a list
                       with v1,v2,v3 equal-style variables for x,y,z displacement
                       of region over time (distance units)
              rotate = string or 1x7 list (see move) coding for vtheta Px Py Pz Rx Ry Rz
                       vtheta = equal-style variable for rotation of region over time (in radians)
                       Px,Py,Pz = origin for axis of rotation (distance units)
                       Rx,Ry,Rz = axis of rotation vector
                open = integer from 1-6 corresponding to face index

            See examples for elliposid()
        """
        # prepare object creation
        kind = "plane"
        if index is None: index = self.counter["all"]+1
        if subindex is None: subindex = self.counter[kind]+1
        units = "box" if self.regionunits=="si" and units is None else units  # force box units of regionunits=="si"
        # create the object P with P for plane
        P = Plane((self.counter["all"]+1,self.counter[kind]+1),
                      spacefilling=self.isspacefilled, # added on 2023-08-11
                      mass=self.mass, density=self.density, # added on 2024-06-14
                      index=index,subindex=subindex,
                      lattice_style=self.lattice_style,
                      lattice_scale=self.lattice_scale,
                      lattice_scale_siunits=self.lattice_scale_siunits,
                      **variables)
        # feed USER fields
        if name not in (None,""): P.name = name # object name (if not defined, default name will be used)
        if name in self.name: raise NameError('the name "%s" is already used' % name)
        if beadtype is not None: P.beadtype = beadtype # bead type (if not defined, default index will apply)
        P.USER.ID = "$"+P.name        # add $ to prevent its execution
        # geometry args (2024-07-04) ---------------------------
        args = [px, py, pz, nx, ny, nz]  # args = [....] as defined in the class Plane
        args_scaled = [
            self.scale_and_translate(px, self.center[0]),
            self.scale_and_translate(py, self.center[1]),
            self.scale_and_translate(pz, self.center[2]),
            self.scale_and_translate(nx, 0),
            self.scale_and_translate(ny, 0),
            self.scale_and_translate(nz, 0)
        ]
        if self.units == "si":
            P.USER.args = args_scaled
            P.USER.args_siunits = args
        else:  # "lattice"
            P.USER.args = args
            P.USER.args_siunits = args_scaled
        # geometry
        P.USER.geometry = (
            f"Plane Region: {P.name}\n"
            "Coordinates: [px,py,pz,nx,ny,nz] = point and normal vector of plane\n"
            f"Coordinates (scaled): {P.USER.args}\n"
            f"Coordinates (SI units): {P.USER.args_siunits}\n"
            f"\tpoint: [{P.USER.args[0]}, {P.USER.args[1]}, {P.USER.args[2]}]\n"
            f"\tnormal: [{P.USER.args[3]}, {P.USER.args[4]}, {P.USER.args[5]}]"
            )
        # other attributes ---------------------------
        P.USER.beadtype = P.beadtype  # beadtype to be used for create_atoms
        P.USER.side = P.sidearg(side) # extra parameter side
        P.USER.move = P.movearg(move) # move arg
        P.USER.units = P.unitsarg(units) # units
        P.USER.rotate = P.rotatearg(rotate) # rotate
        P.USER.open = P.openarg(open) # open
        # Create the object if not fake
        if fake:
            return P
        else:
            self.counter["all"] += 1
            self.counter[kind] +=1
            self.objects[name] = P
            self.nobjects += 1
            return None

    # PRISM method ---------------------------
    # prism args = xlo xhi ylo yhi zlo zhi xy xz yz
    # xlo,xhi,ylo,yhi,zlo,zhi = bounds of untilted prism (distance units)
    # xy = distance to tilt y in x direction (distance units)
    # xz = distance to tilt z in x direction (distance units)
    # yz = distance to tilt z in y direction (distance units)
    def prism(self,xlo=-5,xhi=5,ylo=-5,yhi=5,zlo=-5,zhi=5,xy=1,xz=1,yz=1,
                  name=None,beadtype=None,fake=False,
                  mass=None, density=None,
                  side=None,units=None,move=None,rotate=None,open=None,
                  index = None,subindex = None,
                  **variables
                  ):
        """
        creates a prism region
            xlo,xhi,ylo,yhi,zlo,zhi = bounds of untilted prism (distance units)
            xy = distance to tilt y in x direction (distance units)
            xz = distance to tilt z in x direction (distance units)
            yz = distance to tilt z in y direction (distance units)

            URL: https://docs.lammps.org/region.html

            Main properties = default value
                name = "prism001"
            beadtype = 1
                fake = False (use True to test the execution)
     index, subindex = object index and subindex

            Extra properties
                side = "in|out"
               units = "lattice|box" ("box" is forced if regionunits=="si")
                move = "[${v1} ${v2} ${v3}]" or [v1,v2,v3] as a list
                       with v1,v2,v3 equal-style variables for x,y,z displacement
                       of region over time (distance units)
              rotate = string or 1x7 list (see move) coding for vtheta Px Py Pz Rx Ry Rz
                       vtheta = equal-style variable for rotation of region over time (in radians)
                       Px,Py,Pz = origin for axis of rotation (distance units)
                       Rx,Ry,Rz = axis of rotation vector
                open = integer from 1-6 corresponding to face index

            See examples for elliposid()
        """
        # prepare object creation
        kind = "prism"
        if index is None: index = self.counter["all"]+1
        if subindex is None: subindex = self.counter[kind]+1
        units = "box" if self.regionunits=="si" and units is None else units  # force box units of regionunits=="si"
        # create the object P with P for prism
        obj_mass = mass if mass is not None else self.mass
        obj_density = density if density is not None else self.density
        P = Prism((self.counter["all"]+1,self.counter[kind]+1),
                      spacefilling=self.isspacefilled, # added on 2023-08-11
                      mass=obj_mass, density=obj_density, # added on 2024-06-14
                      index=index,subindex=subindex,
                      lattice_style=self.lattice_style,
                      lattice_scale=self.lattice_scale,
                      lattice_scale_siunits=self.lattice_scale_siunits,
                      **variables)
        # feed USER fields
        if name not in (None,""): P.name = name # object name (if not defined, default name will be used)
        if name in self.name: raise NameError('the name "%s" is already used' % name)
        if beadtype is not None: P.beadtype = beadtype # bead type (if not defined, default index will apply)
        P.USER.ID = "$"+P.name        # add $ to prevent its execution
        # geometry args (2024-07-04) ---------------------------
        args = [xlo, xhi, ylo, yhi, zlo, zhi, xy, xz, yz]  # args = [....] as defined in the class Prism
        args_scaled = [
            self.scale_and_translate(xlo, self.center[0]),
            self.scale_and_translate(xhi, self.center[0]),
            self.scale_and_translate(ylo, self.center[1]),
            self.scale_and_translate(yhi, self.center[1]),
            self.scale_and_translate(zlo, self.center[2]),
            self.scale_and_translate(zhi, self.center[2]),
            self.scale_and_translate(xy, 0),
            self.scale_and_translate(xz, 0),
            self.scale_and_translate(yz, 0)
        ]
        if self.units == "si":
            P.USER.args = args_scaled
            P.USER.args_siunits = args
        else:  # "lattice"
            P.USER.args = args
            P.USER.args_siunits = args_scaled
        # geometry
        P.USER.geometry = (
            f"Prism Region: {P.name}\n"
            "Coordinates: [xlo,xhi,ylo,yhi,zlo,zhi,xy,xz,yz] = bounds and tilts of prism\n"
            f"Coordinates (scaled): {P.USER.args}\n"
            f"Coordinates (SI units): {P.USER.args_siunits}\n"
            f"\tbounds: [{P.USER.args[0]}, {P.USER.args[1]}, {P.USER.args[2]}, {P.USER.args[3]}, {P.USER.args[4]}, {P.USER.args[5]}]\n"
            f"\ttilts: [{P.USER.args[6]}, {P.USER.args[7]}, {P.USER.args[8]}]"
        )
        # other attributes ---------------------------
        P.USER.beadtype = P.beadtype  # beadtype to be used for create_atoms
        P.USER.side = P.sidearg(side) # extra parameter side
        P.USER.move = P.movearg(move) # move arg
        P.USER.units = P.unitsarg(units) # units
        P.USER.rotate = P.rotatearg(rotate) # rotate
        P.USER.open = P.openarg(open) # open
        # Create the object if not fake
        if fake:
            return P
        else:
            self.counter["all"] += 1
            self.counter[kind] +=1
            self.objects[name] = P
            self.nobjects += 1
            return None

    # SPHERE method ---------------------------
    # sphere args = x y z radius
    # x,y,z = center of sphere (distance units)
    # radius = radius of sphere (distance units)
    # x,y,z, and radius can be a variable (see below)
    def sphere(self,x=0,y=0,z=0,radius=3,
                  name=None,beadtype=None,fake=False,
                  mass=None, density=None,
                  side=None,units=None,move=None,rotate=None,open=None,
                  index = None,subindex = None,
                  **variables
                  ):
        """
        creates a sphere region
              x,y,z = center of sphere (distance units)
              radius = radius of sphere (distance units)
              x,y,z, and radius can be a variable

            URL: https://docs.lammps.org/region.html

            Main properties = default value
                name = "sphere001"
            beadtype = 1
                fake = False (use True to test the execution)
     index, subindex = object index and subindex

            Extra properties
                side = "in|out"
               units = "lattice|box" ("box" is forced if regionunits=="si")
                move = "[${v1} ${v2} ${v3}]" or [v1,v2,v3] as a list
                       with v1,v2,v3 equal-style variables for x,y,z displacement
                       of region over time (distance units)
              rotate = string or 1x7 list (see move) coding for vtheta Px Py Pz Rx Ry Rz
                       vtheta = equal-style variable for rotation of region over time (in radians)
                       Px,Py,Pz = origin for axis of rotation (distance units)
                       Rx,Ry,Rz = axis of rotation vector
                open = integer from 1-6 corresponding to face index

            See examples for elliposid()
        """
        # prepare object creation
        kind = "sphere"
        if index is None: index = self.counter["all"]+1
        if subindex is None: subindex = self.counter[kind]+1
        units = "box" if self.regionunits=="si" and units is None else units  # force box units of regionunits=="si"
        # create the object S with S for sphere
        obj_mass = mass if mass is not None else self.mass
        obj_density = density if density is not None else self.density
        S = Sphere((self.counter["all"]+1,self.counter[kind]+1),
                      spacefilling=self.isspacefilled, # added on 2023-08-11
                      mass=obj_mass, density=obj_density, # added on 2024-06-14
                      index=index,subindex=subindex,
                      lattice_style=self.lattice_style,
                      lattice_scale=self.lattice_scale,
                      lattice_scale_siunits=self.lattice_scale_siunits,
                      **variables)
        # feed USER fields
        if name not in (None,""): S.name = name # object name (if not defined, default name will be used)
        if name in self.name: raise NameError('the name "%s" is already used' % name)
        if beadtype is not None: S.beadtype = beadtype # bead type (if not defined, default index will apply)
        S.USER.ID = "$"+S.name        # add $ to prevent its execution
        # geometry args (2024-07-04) ---------------------------
        args = [x, y, z, radius]  # args = [....] as defined in the class Sphere
        args_scaled = [
            self.scale_and_translate(x, self.center[0]),
            self.scale_and_translate(y, self.center[1]),
            self.scale_and_translate(z, self.center[2]),
            self.scale_and_translate(radius, 0)
        ]
        if self.units == "si":
            S.USER.args = args_scaled
            S.USER.args_siunits = args
        else:  # "lattice"
            S.USER.args = args
            S.USER.args_siunits = args_scaled
        # geometry
        S.USER.geometry = (
            f"Sphere Region: {S.name}\n"
            "Coordinates: [x,y,z,radius] = center and radius of sphere\n"
            f"Coordinates (scaled): {S.USER.args}\n"
            f"Coordinates (SI units): {S.USER.args_siunits}\n"
            f"\tcenter: [{S.USER.args[0]}, {S.USER.args[1]}, {S.USER.args[2]}]\n"
            f"\tradius: {S.USER.args[3]}"
        )
        # other attributes ---------------------------
        S.USER.beadtype = S.beadtype  # beadtype to be used for create_atoms
        S.USER.side = S.sidearg(side) # extra parameter side
        S.USER.move = S.movearg(move) # move arg
        S.USER.units = S.unitsarg(units) # units
        S.USER.rotate = S.rotatearg(rotate) # rotate
        S.USER.open = S.openarg(open) # open
        # Create the object if not fake
        if fake:
            return S
        else:
            self.counter["all"] += 1
            self.counter[kind] +=1
            self.objects[name] = S
            self.nobjects += 1
            return None

    # UNION method ---------------------------
    # union args = N reg-ID1 reg-ID2
    def union(self,*regID,
              name=None,beadtype=1,fake=False,
              index = None,subindex = None,
              **variables):
        """
        creates a union region
              union("reg-ID1","reg-ID2",name="myname",beadtype=1,...)
              reg-ID1,reg-ID2, ... = IDs of regions to join together

            URL: https://docs.lammps.org/region.html

            Main properties = default value
                name = "union001"
            beadtype = 1
                fake = False (use True to test the execution)
     index, subindex = object index and subindex
        """
        kind = "union"
        if index is None: index = self.counter["all"]+1
        if subindex is None: subindex = self.counter[kind]+1
        # create the object U with U for union
        U = Union((self.counter["all"]+1,self.counter[kind]+1),
                      index=index,subindex=subindex,**variables)
        # feed USER fields
        if name not in (None,""): U.name = name # object name (if not defined, default name will be used)
        if name in self.name: raise NameError('the name "%s" is already used' % name)
        if beadtype is not None: U.beadtype = beadtype # bead type (if not defined, default index will apply)
        U.USER.ID = "$"+U.name        # add $ to prevent its execution
        U.USER.side, U.USER.move, U.USER.units, U.USER.rotate, U.USER.open = "","","","",""
        # build arguments based on regID
        nregID = len(regID)
        if nregID<2: raise ValueError('two objects must be given at least for an union')
        args = [None] # the number of arguments is not known yet
        validID = range(nregID)
        for ireg in validID:
            if isinstance(regID[ireg],int):
                if regID[ireg] in validID:
                    args.append(self.names[regID[ireg]])
                else:
                    raise IndexError(f"the index {regID[ireg]} exceeds the number of objects {len(self)}")
            elif isinstance(regID[ireg],str):
                if regID[ireg] in self:
                    args.append(regID[ireg])
                else:
                    raise KeyError(f'the object "{regID[ireg]}" does not exist')
            else:
                raise KeyError(f"the {ireg+1}th object should be given as a string or an index")
            # prevent the creation of atoms merged (avoid duplicates)
            self.objects[regID[ireg]].FLAGSECTIONS["create"] = False
        args[0] = len(regID)
        U.USER.args = args   # args = [....] as defined in the class Union
        # Create the object if not fake
        if fake:
            return U
        else:
            self.counter["all"] += 1
            self.counter[kind] +=1
            self.objects[name] = U
            self.nobjects += 1
            return None

    # UNION method ---------------------------
    # union args = N reg-ID1 reg-ID2
    def intersect(self,*regID,
              name=None,beadtype=1,fake=False,
              index = None,subindex = None,
              **variables):
        """
        creates an intersection region
              intersect("reg-ID1","reg-ID2",name="myname",beadtype=1,...)
              reg-ID1,reg-ID2, ... = IDs of regions to join together

            URL: https://docs.lammps.org/region.html

            Main properties = default value
                name = "intersect001"
            beadtype = 1
                fake = False (use True to test the execution)
     index, subindex = object index and subindex
        """
        kind = "intersect"
        if index is None: index = self.counter["all"]+1
        if subindex is None: subindex = self.counter[kind]+1
        # create the object I with I for intersect
        I = Intersect((self.counter["all"]+1,self.counter[kind]+1),
                      index=index,subindex=subindex,**variables)
        # feed USER fields
        if name not in (None,""): I.name = name # object name (if not defined, default name will be used)
        if name in self.name: raise NameError('the name "%s" is already used' % name)
        if beadtype is not None: I.beadtype = beadtype # bead type (if not defined, default index will apply)
        I.USER.ID = "$"+I.name        # add $ to prevent its execution
        I.USER.side, I.USER.move, I.USER.units, I.USER.rotate, I.USER.open = "","","","",""
        # build arguments based on regID
        nregID = len(regID)
        if nregID<2: raise ValueError('two objects must be given at least for an intersection')
        args = [None] # the number of arguments is not known yet
        validID = range(nregID)
        for ireg in validID:
            if isinstance(regID[ireg],int):
                if regID[ireg] in validID:
                    args.append(self.names[regID[ireg]])
                else:
                    raise IndexError(f"the index {regID[ireg]} exceeds the number of objects {len(self)}")
            elif isinstance(regID[ireg],str):
                if regID[ireg] in self:
                    args.append(regID[ireg])
                else:
                    raise KeyError(f'the object "{regID[ireg]}" does not exist')
            else:
                raise KeyError(f"the {ireg+1}th object should be given as a string or an index")
            # prevent the creation of atoms (avoid duplicates)
            self.objects[regID[ireg]].FLAGSECTIONS["create"] = False
        args[0] = len(regID)
        I.USER.args = args   # args = [....] as defined in the class Union
        # Create the object if not fake
        if fake:
            return I
        else:
            self.counter["all"] += 1
            self.counter[kind] +=1
            self.objects[name] = I
            self.nobjects += 1
            return None


    # Group method ---------------------------
    def group(self,obj,name=None,fake=False):
        pass


    # COLLECTION method ---------------------------
    def collection(self,*obj,name=None,beadtype=None,fake=False,
              index = None,subindex = None,
              **kwobj):
        kind = "collection"
        if index is None: index = self.counter["all"]+1
        if subindex is None: subindex = self.counter[kind]+1
        # create the object C with C for collection
        C = Collection((index,subindex))
        if name not in (None,""): C.name = name # object name (if not defined, default name will be used)
        if name in self.name: raise NameError('the name "%s" is already used' % name)
        if beadtype is not None: C.beadtype = beadtype # bead type (if not defined, default index will apply)
        # add objects
        C.collection = regioncollection(*obj,**kwobj)
        # apply modifications (beadtype, ismask)
        for o in C.collection.keys():
            tmp = C.collection.getattr(o)
            if beadtype != None: tmp.beadtype = beadtype
            C.collection.setattr(o,tmp)
        # Create the object if not fake
        if fake:
            return C
        else:
            self.counter["all"] += 1
            self.counter[kind] +=1
            self.objects[name] = C
            self.nobjects += 1
            return None

    def scatter(self,
                 E,
                 name="emulsion",
                 beadtype=None,
                 ):
        """


        Parameters
        ----------
        E : scatter or emulsion object
            codes for x,y,z and r.
        name : string, optional
            name of the collection. The default is "emulsion".
        beadtype : integer, optional
            for all objects. The default is 1.

        Raises
        ------
        TypeError
            Return an error of the object is not a scatter type.

        Returns
        -------
        None.

        """
        if isinstance(E,scatter):
            collect = {}
            for i in range(E.n):
                b = E.beadtype[i] if beadtype==None else beadtype
                nameobj = "glob%02d" % i
                collect[nameobj] = self.sphere(E.x[i],E.y[i],E.z[i],E.r[i],
                            name=nameobj,beadtype=b,fake=True)
            self.collection(**collect,name=name)
        else:
            raise TypeError("the first argument must be an emulsion object")



    # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
    #
    # LOW-LEVEL METHODS
    #
    #
    # Low-level methods to manipulate and operate region objects (e.g., R).
    # They implement essentially some Python standards with the following
    # shortcut: R[i] or R[objecti] and R.objecti and R.objects[objecti] are
    # the same ith object where R.objects is the original container
    # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-

    # repr() method ----------------------------
    def __repr__(self):
        """ display method """
        spacefillingstr = f"\n(space filled with beads of type {self.spacefillingbeadtype})" \
            if self.isspacefilled else ""
        print("-"*40)
        print('REGION container "%s" with %d objects %s\n(units="%s", lattice="%s", scale=%0.4g [m])' \
              % (self.name,self.nobjects,spacefillingstr,self.units,self.lattice_style,self.lattice_scale_siunits))
        if self.nobjects>0:
            names = self.names
            l = [len(n) for n in names]
            width = max(10,max(l)+2)
            fmt = "%%%ss:" % width
            for i in range(self.nobjects):
                flags = "("+self.objects[names[i]].shortflags+")" if self.objects[names[i]].flags else "(no script)"
                if isinstance(self.objects[names[i]],Collection):
                        print(fmt % names[i]," %s region (%d beadtypes)" % \
                              (self.objects[names[i]].kind,len(self.objects[names[i]].beadtype))," > ",flags)
                else:
                    print(fmt % names[i]," %s region (beadtype=%d)" % \
                          (self.objects[names[i]].kind,self.objects[names[i]].beadtype)," > ",flags)
            print(wrap("they are",":",", ".join(self.names),10,60,80))
        print("-"*40)
        return "REGION container %s with %d objects (%s)" % \
            (self.name,self.nobjects,",".join(self.names))

    # str() method ----------------------------
    def __str__(self):
        """ string representation of a region """
        return "REGION container %s with %d objects (%s)" % \
            (self.name,self.nobjects,",".join(self.names))

    # generic GET method ----------------------------
    def get(self,name):
        """ returns the object """
        if name in self.objects:
            return self.objects[name]
        else:
            raise NameError('the object "%s" does not exist, use list()' % name)

    # getattr() method ----------------------------
    def __getattr__(self,name):
        """ getattr attribute override """
        if (name in self.__dict__) or (name in protectedregionkeys):
            return self.__dict__[name] # higher precedence for root attributes
        if name in protectedregionkeys:
            return getattr(type(self), name).__get__(self) # for methods decorated as properties (@property)
        # Handle special cases like __wrapped__ explicitly
        if name == "__wrapped__":
            return None  # Default value or appropriate behavior
        # Leave legitimate __dunder__ attributes to the default mechanism
        if name.startswith("__") and name.endswith("__"):
            raise AttributeError(f"{type(self).__name__!r} object has no attribute {name!r}")
        # Default
        return self.get(name)

    # generic SET method ----------------------------
    def set(self,name,value):
        """ set field and value """
        if isinstance(value,list) and len(value)==0:
            if name not in self.objects:
                raise NameError('the object "%s" does not exist, use list()' % name)
            self.delete(name)
        elif isinstance(value,coregeometry):
            if name in self.objects: self.delete(name)
            if isinstance(value.SECTIONS,pipescript) or isinstance(value,Evalgeometry):
                self.eval(deepduplicate(value),name) # not a scalar
            else: # scalar
                self.objects[name] = deepduplicate(value)
                self.objects[name].name = name
                self.nobjects += 1
                self.counter["all"] += 1
                self.objects[name].index = self.counter["all"]
                self.counter[value.kind] += 1

    # setattr() method ----------------------------
    def __setattr__(self,name,value):
        """ setattr override """
        if name in protectedregionkeys: # do not forget to increment protectedregionkeys
            self.__dict__[name] = value # if not, you may enter in infinite loops
        else:
            self.set(name,value)

    # generic HASATTR method ----------------------------
    def hasattr(self,name):
        """ return true if the object exist """
        if not isinstance(name,str): raise TypeError("please provide a string")
        return name in self.objects

    # IN operator ----------------------------
    def __contains__(self,obj):
        """ in override """
        return self.hasattr(obj)

    # len() method ----------------------------
    def __len__(self):
        """ len method """
        return len(self.objects)

    # indexing [int] and ["str"] method ----------------------------
    def __getitem__(self,idx):
        """
            R[i] returns the ith element of the structure
            R[:4] returns a structure with the four first fields
            R[[1,3]] returns the second and fourth elements
        """
        if isinstance(idx,int):
            if idx<len(self):
                return self.get(self.names[idx])
            raise IndexError(f"the index should be comprised between 0 and {len(self)-1}")
        elif isinstance(idx,str):
            if idx in self:
                return self.get(idx)
            raise NameError(f'{idx} does not exist, use list() to list objects')
        elif isinstance(idx,list):
            pass
        elif isinstance(idx,slice):
            return self.__getitem__(self,list(range(*idx.indices(len(self)))))
        else:
            raise IndexError("not implemented yet")

    # duplication GET method based on DICT ----------------------------
    def __getstate__(self):
        """ getstate for cooperative inheritance / duplication """
        return self.__dict__.copy()

    # duplication SET method based on DICT ----------------------------
    def __setstate__(self,state):
        """ setstate for cooperative inheritance / duplication """
        self.__dict__.update(state)

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

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


    # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
    #
    # MIDDLE-LEVEL METHODS
    #
    #
    # These methods are specific to PIZZA.REGION() objects.
    # They bring useful methods for the user and developer.
    # Similar methods exist in PIZZA.RASTER()
    # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-

    # LIST method ----------------------------
    def list(self):
        """ list objects """
        fmt = "%%%ss:" % max(10,max([len(n) for n in self.names])+2)
        print('REGION container "%s" with %d objects' % (self.name,self.nobjects))
        for o in self.objects.keys():
            print(fmt % self.objects[o].name,"%-10s" % self.objects[o].kind,
                  "(beadtype=%d,object index=[%d,%d])" % \
                      (self.objects[o].beadtype,
                       self.objects[o].index,
                       self.objects[o].subindex))

    # NAMES method set as an attribute ----------------------------
    @property
    def names(self):
        """ return the names of objects sorted as index """
        namesunsorted=namessorted=list(self.objects.keys())
        nobj = len(namesunsorted)
        if nobj<1:
            return []
        elif nobj<2:
            return namessorted
        else:
            for iobj in range(nobj):
                namessorted[self.objects[namesunsorted[iobj]].index-1] = namesunsorted[iobj]
            return namessorted

    # NBEADS method set as an attribute
    @property
    def nbeads(self):
        "return the number of beadtypes used"
        if len(self)>0:
            guess = max(len(self.count()),self.live.nbeads)
            return guess+1 if self.isspacefilled else guess
        else:
            return self.live.nbeads

    # COUNT method
    def count(self):
        """ count objects by type """
        typlist = []
        for  o in self.names:
            if isinstance(self.objects[o].beadtype,list):
                typlist += self.objects[o].beadtype
            else:
                typlist.append(self.objects[o].beadtype)
        utypes = list(set(typlist))
        c = []
        for t in utypes:
            c.append((t,typlist.count(t)))
        return c

    # BEADTYPES property
    @property
    def beadtypes(self):
        """ list the beadtypes """
        return [ x[0] for x in self.count() ]

    # DELETE method
    def delete(self,name):
        """ delete object """
        if name in self.objects:
            kind = self.objects[name].kind
            del self.objects[name]
            self.nobjects -= 1
            self.counter[kind] -= 1
            self.counter["all"] -= 1
        else:
            raise NameError("%s does not exist (use list()) to list valid objects" % name)

    # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
    #
    # HIGH-LEVEL METHODS
    #
    #
    # These methods are connect PIZZA.REGION() objects with their equivalent
    # as PIZZA.SCRIPT() and PIZZA.PIPESCRIPT() objects and methods.
    #
    # They are essential to PIZZA.REGION(). They do not have equivalent in
    # PIZZA.RASTER(). They use extensively the methods attached to :
    #        PIZZA.REGION.LAMMPSGENERIC()
    #        PIZZA.REGION.COREGEOMETRY()
    #
    # Current real-time rendering relies on
    #   https://andeplane.github.io/atomify/
    # which gives better results than
    #   https://editor.lammps.org/
    # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-

    # EVALUATE algebraic operation on PIZZA.REGION() objects (operation on codes)
    def eval(self,expression,name=None,beadtype = None,
             fake=False,index = None,subindex = None):
        """
            evaluates (i.e, combine scripts) an expression combining objects
                R= region(name="my region")
                R.eval(o1+o2+...,name='obj')
                R.eval(o1|o2|...,name='obj')
            R.name will be the resulting object of class region.eval (region.coregeometry)
        """
        if not isinstance(expression, coregeometry): raise TypeError("the argument should be a region.coregeometry")
        # prepare object creation
        kind = "eval"
        self.counter["all"] += 1
        self.counter[kind] +=1
        if index is None: index = self.counter["all"]
        if subindex is None: subindex = self.counter[kind]
        # create the object E with E for Ellipsoid
        E = Evalgeometry((self.counter["all"],self.counter[kind]),
                      index=index,subindex=subindex)
        # link expression to E
        if beadtype is not None: E.beadtype = beadtype # bead type (if not defined, default index will apply)
        if name is None: name = expression.name
        if name in self.name: raise NameError('the name "%s" is already used' % name)
        E.name = name
        E.SECTIONS = expression.SECTIONS
        E.USER = expression.USER
        if isinstance(E.SECTIONS,pipescript):
            # set beadtypes for all sections and scripts in the pipeline
            for i in E.SECTIONS.keys():
                for j in range(len(E.SECTIONS[i])):
                    E.SECTIONS[i].USER[j].beadtype = E.beadtype
        E.USER.beadtype = beadtype
        # Create the object if not fake
        if fake:
            self.counter["all"] -= 1
            self.counter[kind] -= 1
            return E
        else:
            self.objects[name] = E
            self.nobjects += 1
            return None

    # PIPESCRIPT method generates a pipe for all objects and sections
    def pipescript(self,printflag=False,verbose=False,verbosity=0):
        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
        """ pipescript all objects in the region """
        if len(self)<1: return pipescript()
        # execute all objects
        for myobj in self:
            if not isinstance(myobj,Collection): myobj.do(printflag=printflag,verbosity=verbosity)
        # concatenate all objects into a pipe script
        # for collections, only group is accepted
        liste = [x.SECTIONS["variables"] for x in self if not isinstance(x,Collection) and x.hasvariables] + \
                [x.SECTIONS["region"]    for x in self if not isinstance(x,Collection) and x.hasregion] + \
                [x.SECTIONS["create"]    for x in self if not isinstance(x,Collection) and x.hascreate] + \
                [x.SECTIONS["group"]     for x in self if not isinstance(x,Collection) and x.hasgroup] + \
                [x.SECTIONS["setgroup"]  for x in self if not isinstance(x,Collection) and x.hassetgroup] + \
                [x.SECTIONS["move"]      for x in self if not isinstance(x,Collection) and x.hasmove]
        # add the objects within the collection
        for x in self:
            if isinstance(x,Collection): liste += x.group()
        # add the eventual group for the collection
        liste += [x.SECTIONS["group"] for x in self if isinstance(x,Collection) and x.hasgroup]
        # chain all scripts
        return pipescript.join(liste)

    # SCRIPT add header and footer to PIPECRIPT
    def script(self,live=False, printflag=None, verbose=None, verbosity=None):
        """ script all objects in the region """
        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
        s = self.pipescript(printflag=printflag,verbose=verbose,verbosity=verbosity).script(printflag=printflag,verbose=verbose,verbosity=verbosity)
        if self.isspacefilled:
            USERspacefilling =regiondata(**self.spacefilling)
            s = LammpsSpacefilling(**USERspacefilling)+s
        if live:
            beadtypes = self.beadtypes
            USER = regiondata(**self.live)
            USER.nbeads = self.nbeads
            USER.mass = "$"
            USER.pair_coeff = "$"
            # list beadtype and prepare  mass, pair_coeff
            beadtypes = [ x[0] for x in self.count() ]
            if self.isspacefilled and self.spacefillingbeadtype not in beadtypes:
                beadtypes = [self.spacefillingbeadtype]+beadtypes
            for b in beadtypes:
                USER.mass += livetemplate["mass"] % b +"\n"
                USER.pair_coeff += livetemplate["pair_coeff"] %(b,b) +"\n"
            for b1 in beadtypes:
                for b2 in beadtypes:
                    if b2>b1:
                        USER.pair_coeff += livetemplate["pair_coeff"] %(b1,b2) +"\n"
            livemode = "dynamic" if self.hasfixmove else "static"
            USER.run =self.livelammps["options"][livemode]["run"]
            s = LammpsHeader(**USER)+s+LammpsFooter(**USER)
        return s

    # SCRIPTHEADERS add header scripts for initializing script, lattice, box for region
    def scriptHeaders(self, what=["init", "lattice", "box"], pipescript=False, **userdefinitions):
        """
            Generate and return LAMMPS header scripts for initializing the simulation, defining the lattice,
            and specifying the simulation box for all region objects.

            Parameters:
            - what (list of str): Specifies which scripts to generate. Options are "init", "lattice", "box", "mass" and "preview".
                                  Multiple scripts can be generated by passing a list of these options.
                                  Default is ["init", "lattice", "box"].
            - pipescript (bool): If True, the generated scripts are combined with `|` instead of `+`. Default is False.

            Property/pair value
            - nbeads (int): Specifies the number of beads, overriding the default if larger than `self.nbeads`.
                            Default is 1.
            - mass (real value or list): Sets the mass for each bead, overrriding `self.mass`
                            Default is 1.0.


            Returns:
            - object: The combined header scripts as a single object.
                      Header values can be overridden by updating `self.headersData`.

            Raises:
            - Exception: If no valid script options are provided in `what`.

            Example usage:
                sRheader = R.scriptHeaders("box").do()  # Generate the box header script.
                sRallheaders = R.scriptHeaders(["init", "lattice", "box"])  # Generate all headers.

                Example usage without naming parameters:
                sRheader = R.scriptHeaders("box")  # "what" specified as "box", nbeads defaults to 1.

                Example of overriding values
                sRheader = R.scriptHeaders("lattice",lattice_style = "$sq")  # Generate the lattice header script with the overridden value.
        """
        # handle overrides
        USERregion = self.headersData + regiondata(**userdefinitions)
        # Fix singletons
        if not isinstance(what, list):
            what = [what]
        # Generate the initialization script
        scripts = []  # Store all generated script objects here
        if "init" in what:
            scripts.append(LammpsHeaderInit(**USERregion))
        # Generate the lattice script
        if "lattice" in what:
            scripts.append(LammpsHeaderLattice(**USERregion))
        # Generate the box script
        if "box" in what:
            scripts.append(LammpsHeaderBox(**USERregion))
            if self.isspacefilled:
                scripts.append(LammpsSpacefilling(**self.spacefilling))
        # Generate the mass script
        if "mass" in what:
            scripts.append(LammpsHeaderMass(**USERregion))
        # Generate the preview script
        if "preview" in what:
            scripts.append(LammpsFooterPreview(**USERregion))
        if not scripts:
            raise Exception('nothing to do (use: "init", "lattice", "box", "mass" or "preview" within [ ])')

        # Combine the scripts based on the pipescript flag
        combined_script = scripts[0]  # Initialize the combined script with the first element
        for script in scripts[1:]:
            if pipescript:
                # Combine scripts using the | operator, maintaining pipescript format
                combined_script = combined_script | script  # p_ab = s_a | s_b or p_ab = s_a | p_b
            else:
                # Combine scripts using the + operator, maintaining regular script format
                combined_script = combined_script + script  # s_ab = s_a + s_b
        return combined_script


    def pscriptHeaders(self, what=["init", "lattice", "box"], **userdefinitions):
        """
        Surrogate method for generating LAMMPS pipescript headers.
        Calls the `scriptHeaders` method with `pipescript=True`.

        Parameters:
        - what (list of str): Specifies which scripts to generate. Options are "init", "lattice", and "box".
                              Multiple scripts can be generated by passing a list of these options.
                              Default is ["init", "lattice", "box"].
        Property/pair value
        - nbeads (int): Specifies the number of beads, overriding the default if larger than `self.nbeads`.
                        Default is 1.
        - mass (real value or list): Sets the mass for each bead, overrriding `self.mass`
                        Default is 1.0.
        Returns:
        - object: The combined pipescript header scripts as a single object.
        """
        # Call scriptHeaders with pipescript=True
        return self.scriptHeaders(what=what, pipescript=True, **userdefinitions)


    # DO METHOD = main static compiler
    def do(self, printflag=False, verbosity=1):
        """ execute the entire script """
        return self.pipescript().do(printflag=printflag, verbosity=verbosity)

    # DOLIVE = fast code generation for online rendering
    def dolive(self):
        """
            execute the entire script for online testing
            see: https://editor.lammps.org/
        """
        self.livelammps["file"] = self.script(live=True).tmpwrite()
        if not self.livelammps["active"]:
            livelammps(self.livelammps["URL"],new=0)
            self.livelammps["active"] = True
        return self.livelammps["file"]

Instance variables

var beadtypes

list the beadtypes

Expand source code
@property
def beadtypes(self):
    """ list the beadtypes """
    return [ x[0] for x in self.count() ]
var geometry

Display the dimensions and characteristics of the region and its objects.

Expand source code
@property
def geometry(self):
    """Display the dimensions and characteristics of the region and its objects."""
    details = f"Region: {self.name}\n"
    details += f"Total atoms: {self.natoms}\n"
    details += f"Span: width={self.spacefilling['fillingwidth']}, height={self.spacefilling['fillingheight']}, depth={self.spacefilling['fillingdepth']}\n"
    details += f"Box center: {self.center}\n"
    details += "Objects in the region:\n\n"
    for obj in self:
        details += "\n\n"+"-"*32+"\n"
        details += f"\nObject: {obj.name}\n"
        details += f"Type: {type(obj).__name__}\n"
        if hasattr(obj, 'geometry'):
            details += "\n"+"-"*32+"\n"
            details += obj.geometry
        else:
            details += "No geometry information available.\n"
    print(details)
var isspacefilled
Expand source code
@property
def isspacefilled(self):
    return self.spacefilling["flag"]
var names

return the names of objects sorted as index

Expand source code
@property
def names(self):
    """ return the names of objects sorted as index """
    namesunsorted=namessorted=list(self.objects.keys())
    nobj = len(namesunsorted)
    if nobj<1:
        return []
    elif nobj<2:
        return namessorted
    else:
        for iobj in range(nobj):
            namessorted[self.objects[namesunsorted[iobj]].index-1] = namesunsorted[iobj]
        return namessorted
var natoms

Count the total number of atoms in all objects within the region.

Expand source code
@property
def natoms(self):
    """Count the total number of atoms in all objects within the region."""
    total_atoms = 0
    for eachobj in self:
        total_atoms += eachobj.natoms
    return total_atoms
var nbeads

return the number of beadtypes used

Expand source code
@property
def nbeads(self):
    "return the number of beadtypes used"
    if len(self)>0:
        guess = max(len(self.count()),self.live.nbeads)
        return guess+1 if self.isspacefilled else guess
    else:
        return self.live.nbeads
var spacefillingbeadtype
Expand source code
@property
def spacefillingbeadtype(self):
    return self.spacefilling["fillingbeadtype"]

Methods

def block(self, xlo=-5, xhi=5, ylo=-5, yhi=5, zlo=-5, zhi=5, name=None, beadtype=None, fake=False, mass=None, density=None, side=None, units=None, move=None, rotate=None, open=None, index=None, subindex=None, **variables)

creates a block region xlo,xhi,ylo,yhi,zlo,zhi = bounds of block in all dimensions (distance units)

   URL: <https://docs.lammps.org/region.html>

   Main properties = default value
       name = "block001"
   beadtype = 1
       fake = False (use True to test the execution)

index, subindex = object index and subindex

   Extra properties
       side = "in|out"
      units = "lattice|box" ("box" is forced if regionunits=="si")
       move = "[${v1} ${v2} ${v3}]" or [v1,v2,v3] as a list
              with v1,v2,v3 equal-style variables for x,y,z displacement
              of region over time (distance units)
     rotate = string or 1x7 list (see move) coding for vtheta Px Py Pz Rx Ry Rz
              vtheta = equal-style variable for rotation of region over time (in radians)
              Px,Py,Pz = origin for axis of rotation (distance units)
              Rx,Ry,Rz = axis of rotation vector
       open = integer from 1-6 corresponding to face index

   See examples for elliposid()
Expand source code
def block(self,xlo=-5,xhi=5,ylo=-5,yhi=5,zlo=-5,zhi=5,
              name=None,beadtype=None,fake=False,
              mass=None, density=None,
              side=None,units=None,move=None,rotate=None,open=None,
              index = None,subindex = None,
              **variables
              ):
    """
    creates a block region
        xlo,xhi,ylo,yhi,zlo,zhi = bounds of block in all dimensions (distance units)

        URL: https://docs.lammps.org/region.html

        Main properties = default value
            name = "block001"
        beadtype = 1
            fake = False (use True to test the execution)
 index, subindex = object index and subindex

        Extra properties
            side = "in|out"
           units = "lattice|box" ("box" is forced if regionunits=="si")
            move = "[${v1} ${v2} ${v3}]" or [v1,v2,v3] as a list
                   with v1,v2,v3 equal-style variables for x,y,z displacement
                   of region over time (distance units)
          rotate = string or 1x7 list (see move) coding for vtheta Px Py Pz Rx Ry Rz
                   vtheta = equal-style variable for rotation of region over time (in radians)
                   Px,Py,Pz = origin for axis of rotation (distance units)
                   Rx,Ry,Rz = axis of rotation vector
            open = integer from 1-6 corresponding to face index

        See examples for elliposid()
    """
    # prepare object creation
    kind = "block"
    if index is None: index = self.counter["all"]+1
    if subindex is None: subindex = self.counter[kind]+1
    units = "box" if self.regionunits=="si" and units is None else units  # force box units of regionunits=="si"
    # create the object B with B for block
    obj_mass = mass if mass is not None else self.mass
    obj_density = density if density is not None else self.density
    B = Block((self.counter["all"]+1,self.counter[kind]+1),
                  spacefilling=self.isspacefilled, # added on 2023-08-11
                  mass=obj_mass, density=obj_density, # added on 2024-06-14
                  index=index,subindex=subindex,
                  lattice_style=self.lattice_style,
                  lattice_scale=self.lattice_scale,
                  lattice_scale_siunits=self.lattice_scale_siunits,
                  **variables)
    # feed USER fields
    if name not in (None,""): B.name = name # object name (if not defined, default name will be used)
    if name in self.name: raise NameError('the name "%s" is already used' % name)
    if beadtype is not None: B.beadtype = beadtype # bead type (if not defined, default index will apply)
    B.USER.ID = "$"+B.name        # add $ to prevent its execution
    # geometry args (2024-07-04)  -------------------------------------
    args = [xlo, xhi, ylo, yhi, zlo, zhi]  # args = [....] as defined in the class Block
    args_scaled = [
        self.scale_and_translate(xlo, self.center[0]),
        self.scale_and_translate(xhi, self.center[0]),
        self.scale_and_translate(ylo, self.center[1]),
        self.scale_and_translate(yhi, self.center[1]),
        self.scale_and_translate(zlo, self.center[2]),
        self.scale_and_translate(zhi, self.center[2])
    ]
    if self.units == "si":
        B.USER.args = args_scaled
        B.USER.args_siunits = args
    else:  # "lattice"
        B.USER.args = args
        B.USER.args_siunits = args_scaled
    # geometry
    B.USER.geometry = (
        f"Block Region: {B.name}\n"
        "Coordinates: [xlo,xhi,ylo,yhi,zlo,zhi] = bounds of block in all dimensions"
        f"Coordinates (scaled): {B.USER.args}\n"
        f"Coordinates (SI units): {B.USER.args_siunits}\n"
        f"\talong x: [{B.USER.args[0]}, {B.USER.args[1]}]\n"
        f"\talong y: [{B.USER.args[2]}, {B.USER.args[3]}]\n"
        f"\talong z: [{B.USER.args[4]}, {B.USER.args[5]}]"
    )
    # other attributes  -------------------------------------
    B.USER.beadtype = B.beadtype  # beadtype to be used for create_atoms
    B.USER.side = B.sidearg(side) # extra parameter side
    B.USER.move = B.movearg(move) # move arg
    B.USER.units = B.unitsarg(units) # units
    B.USER.rotate = B.rotatearg(rotate) # rotate
    B.USER.open = B.openarg(open) # open
    # Create the object if not fake
    if fake:
        return B
    else:
        self.counter["all"] += 1
        self.counter[kind] +=1
        self.objects[name] = B
        self.nobjects += 1
        return None
def collection(self, *obj, name=None, beadtype=None, fake=False, index=None, subindex=None, **kwobj)
Expand source code
def collection(self,*obj,name=None,beadtype=None,fake=False,
          index = None,subindex = None,
          **kwobj):
    kind = "collection"
    if index is None: index = self.counter["all"]+1
    if subindex is None: subindex = self.counter[kind]+1
    # create the object C with C for collection
    C = Collection((index,subindex))
    if name not in (None,""): C.name = name # object name (if not defined, default name will be used)
    if name in self.name: raise NameError('the name "%s" is already used' % name)
    if beadtype is not None: C.beadtype = beadtype # bead type (if not defined, default index will apply)
    # add objects
    C.collection = regioncollection(*obj,**kwobj)
    # apply modifications (beadtype, ismask)
    for o in C.collection.keys():
        tmp = C.collection.getattr(o)
        if beadtype != None: tmp.beadtype = beadtype
        C.collection.setattr(o,tmp)
    # Create the object if not fake
    if fake:
        return C
    else:
        self.counter["all"] += 1
        self.counter[kind] +=1
        self.objects[name] = C
        self.nobjects += 1
        return None
def cone(self, dim='z', c1=0, c2=0, radlo=2, radhi=5, lo=-10, hi=10, name=None, beadtype=None, fake=False, mass=None, density=None, side=None, units=None, move=None, rotate=None, open=None, index=None, subindex=None, **variables)

creates a cone region dim = "x" or "y" or "z" = axis of the cone note: USER, LAMMPS variables are not authorized here c1,c2 = coords of cone axis in other 2 dimensions (distance units) radlo,radhi = cone radii at lo and hi end (distance units) lo,hi = bounds of cone in dim (distance units)

   URL: <https://docs.lammps.org/region.html>

   Main properties = default value
       name = "cone001"
   beadtype = 1
       fake = False (use True to test the execution)

index, subindex = object index and subindex

   Extra properties
       side = "in|out"
      units = "lattice|box" ("box" is forced if regionunits=="si")
       move = "[${v1} ${v2} ${v3}]" or [v1,v2,v3] as a list
              with v1,v2,v3 equal-style variables for x,y,z displacement
              of region over time (distance units)
     rotate = string or 1x7 list (see move) coding for vtheta Px Py Pz Rx Ry Rz
              vtheta = equal-style variable for rotation of region over time (in radians)
              Px,Py,Pz = origin for axis of rotation (distance units)
              Rx,Ry,Rz = axis of rotation vector
       open = integer from 1-6 corresponding to face index

   See examples for elliposid()
Expand source code
def cone(self,dim="z",c1=0,c2=0,radlo=2,radhi=5,lo=-10,hi=10,
              name=None,beadtype=None,fake=False,
              mass=None, density=None,
              side=None,units=None,move=None,rotate=None,open=None,
              index = None,subindex = None,
              **variables
              ):
    """
    creates a cone region
        dim = "x" or "y" or "z" = axis of the cone
             note: USER, LAMMPS variables are not authorized here
        c1,c2 = coords of cone axis in other 2 dimensions (distance units)
        radlo,radhi = cone radii at lo and hi end (distance units)
        lo,hi = bounds of cone in dim (distance units)

        URL: https://docs.lammps.org/region.html

        Main properties = default value
            name = "cone001"
        beadtype = 1
            fake = False (use True to test the execution)
 index, subindex = object index and subindex

        Extra properties
            side = "in|out"
           units = "lattice|box" ("box" is forced if regionunits=="si")
            move = "[${v1} ${v2} ${v3}]" or [v1,v2,v3] as a list
                   with v1,v2,v3 equal-style variables for x,y,z displacement
                   of region over time (distance units)
          rotate = string or 1x7 list (see move) coding for vtheta Px Py Pz Rx Ry Rz
                   vtheta = equal-style variable for rotation of region over time (in radians)
                   Px,Py,Pz = origin for axis of rotation (distance units)
                   Rx,Ry,Rz = axis of rotation vector
            open = integer from 1-6 corresponding to face index

        See examples for elliposid()
    """
    # prepare object creation
    kind = "cone"
    if index is None: index = self.counter["all"]+1
    if subindex is None: subindex = self.counter[kind]+1
    units = "box" if self.regionunits=="si" and units is None else units  # force box units of regionunits=="si"
    # create the object C with C for cone
    obj_mass = mass if mass is not None else self.mass
    obj_density = density if density is not None else self.density
    C = Cone((self.counter["all"]+1,self.counter[kind]+1),
                  spacefilling=self.isspacefilled, # added on 2023-08-11
                  mass=obj_mass, density=obj_density, # added on 2024-06-14
                  index=index,subindex=subindex,
                  lattice_style=self.lattice_style,
                  lattice_scale=self.lattice_scale,
                  lattice_scale_siunits=self.lattice_scale_siunits,
                  **variables)
    # feed USER fields
    if name not in (None,""): C.name = name # object name (if not defined, default name will be used)
    if name in self.name: raise NameError('the name "%s" is already used' % name)
    if beadtype is not None: C.beadtype = beadtype # bead type (if not defined, default index will apply)
    C.USER.ID = "$"+C.name        # add $ to prevent its execution
    # geometry args (2024-07-04)  -------------------------------------
    args = [dim, c1, c2, radlo, radhi, lo, hi]  # args = [....] as defined in the class Cone
    if dim == "x":  # x-axis
        args_scaled = [
            dim,
            self.scale_and_translate(c1, self.center[1]),
            self.scale_and_translate(c2, self.center[2]),
            self.scale_and_translate(radlo, 0),
            self.scale_and_translate(radhi, 0),
            self.scale_and_translate(lo, self.center[0]),
            self.scale_and_translate(hi, self.center[0])
        ]
    elif dim == "y":  # y-axis
        args_scaled = [
            dim,
            self.scale_and_translate(c1, self.center[0]),
            self.scale_and_translate(c2, self.center[2]),
            self.scale_and_translate(radlo, 0),
            self.scale_and_translate(radhi, 0),
            self.scale_and_translate(lo, self.center[1]),
            self.scale_and_translate(hi, self.center[1])
        ]
    else:  # z-axis
        args_scaled = [
            dim,
            self.scale_and_translate(c1, self.center[0]),
            self.scale_and_translate(c2, self.center[1]),
            self.scale_and_translate(radlo, 0),
            self.scale_and_translate(radhi, 0),
            self.scale_and_translate(lo, self.center[2]),
            self.scale_and_translate(hi, self.center[2])
        ]

    if self.units == "si":
        C.USER.args = args_scaled
        C.USER.args_siunits = args
    else:  # "lattice"
        C.USER.args = args
        C.USER.args_siunits = args_scaled
    # geometry
    C.USER.geometry = (
        f"Cone Region: {C.name}\n"
        "Coordinates: [dim,c1,c2,radlo,radhi,lo,hi] = dimensions of cone\n"
        f"Coordinates (scaled): {C.USER.args}\n"
        f"Coordinates (SI units): {C.USER.args_siunits}\n"
        f"\tdim: {C.USER.args[0]}\n"
        f"\tc1: {C.USER.args[1]}\n"
        f"\tc2: {C.USER.args[2]}\n"
        f"\tradlo: {C.USER.args[3]}\n"
        f"\tradhi: {C.USER.args[4]}\n"
        f"\tlo: {C.USER.args[5]}\n"
        f"\thi: {C.USER.args[6]}"
    )
    # other attributes  -------------------------------------
    C.USER.beadtype = C.beadtype  # beadtype to be used for create_atoms
    C.USER.side = C.sidearg(side) # extra parameter side
    C.USER.move = C.movearg(move) # move arg
    C.USER.units = C.unitsarg(units) # units
    C.USER.rotate = C.rotatearg(rotate) # rotate
    C.USER.open = C.openarg(open) # open
    # Create the object if not fake
    if fake:
        return C
    else:
        self.counter["all"] += 1
        self.counter[kind] +=1
        self.objects[name] = C
        self.nobjects += 1
        return None
def count(self)

count objects by type

Expand source code
def count(self):
    """ count objects by type """
    typlist = []
    for  o in self.names:
        if isinstance(self.objects[o].beadtype,list):
            typlist += self.objects[o].beadtype
        else:
            typlist.append(self.objects[o].beadtype)
    utypes = list(set(typlist))
    c = []
    for t in utypes:
        c.append((t,typlist.count(t)))
    return c
def cylinder(self, dim='z', c1=0, c2=0, radius=4, lo=-10, hi=10, name=None, beadtype=None, fake=False, mass=None, density=None, side=None, units=None, move=None, rotate=None, open=None, index=None, subindex=None, **variables)

creates a cylinder region dim = x or y or z = axis of cylinder c1,c2 = coords of cylinder axis in other 2 dimensions (distance units) radius = cylinder radius (distance units) c1,c2, and radius can be a LAMMPS variable lo,hi = bounds of cylinder in dim (distance units)

   URL: <https://docs.lammps.org/region.html>

   Main properties = default value
       name = "cylinder001"
   beadtype = 1
       fake = False (use True to test the execution)

index, subindex = object index and subindex

   Extra properties
       side = "in|out"
      units = "lattice|box" ("box" is forced if regionunits=="si")
       move = "[${v1} ${v2} ${v3}]" or [v1,v2,v3] as a list
              with v1,v2,v3 equal-style variables for x,y,z displacement
              of region over time (distance units)
     rotate = string or 1x7 list (see move) coding for vtheta Px Py Pz Rx Ry Rz
              vtheta = equal-style variable for rotation of region over time (in radians)
              Px,Py,Pz = origin for axis of rotation (distance units)
              Rx,Ry,Rz = axis of rotation vector
       open = integer from 1-6 corresponding to face index

   See examples for elliposid()
Expand source code
def cylinder(self,dim="z",c1=0,c2=0,radius=4,lo=-10,hi=10,
              name=None,beadtype=None,fake=False,
              mass=None, density=None,
              side=None,units=None,move=None,rotate=None,open=None,
              index = None,subindex = None,
              **variables
              ):
    """
    creates a cylinder region
          dim = x or y or z = axis of cylinder
          c1,c2 = coords of cylinder axis in other 2 dimensions (distance units)
          radius = cylinder radius (distance units)
          c1,c2, and radius can be a LAMMPS variable
          lo,hi = bounds of cylinder in dim (distance units)

        URL: https://docs.lammps.org/region.html

        Main properties = default value
            name = "cylinder001"
        beadtype = 1
            fake = False (use True to test the execution)
 index, subindex = object index and subindex

        Extra properties
            side = "in|out"
           units = "lattice|box" ("box" is forced if regionunits=="si")
            move = "[${v1} ${v2} ${v3}]" or [v1,v2,v3] as a list
                   with v1,v2,v3 equal-style variables for x,y,z displacement
                   of region over time (distance units)
          rotate = string or 1x7 list (see move) coding for vtheta Px Py Pz Rx Ry Rz
                   vtheta = equal-style variable for rotation of region over time (in radians)
                   Px,Py,Pz = origin for axis of rotation (distance units)
                   Rx,Ry,Rz = axis of rotation vector
            open = integer from 1-6 corresponding to face index

        See examples for elliposid()
    """
    # prepare object creation
    kind = "cylinder"
    if index is None: index = self.counter["all"]+1
    if subindex is None: subindex = self.counter[kind]+1
    units = "box" if self.regionunits=="si" and units is None else units  # force box units of regionunits=="si"
    # create the object C with C for cylinder
    obj_mass = mass if mass is not None else self.mass
    obj_density = density if density is not None else self.density
    C = Cylinder((self.counter["all"]+1,self.counter[kind]+1),
                  spacefilling=self.isspacefilled, # added on 2023-08-11
                  mass=obj_mass, density=obj_density,
                  index=index,subindex=subindex,
                  lattice_style=self.lattice_style,
                  lattice_scale=self.lattice_scale,
                  lattice_scale_siunits=self.lattice_scale_siunits,
                  **variables)
    # feed USER fields
    if name not in (None,""): C.name = name # object name (if not defined, default name will be used)
    if name in self.name: raise NameError('the name "%s" is already used' % name)
    if beadtype is not None: C.beadtype = beadtype # bead type (if not defined, default index will apply)
    C.USER.ID = "$"+C.name        # add $ to prevent its execution
    # geometry args (2024-07-04)  -------------------------------------
    args = [dim, c1, c2, radius, lo, hi]  # args = [....] as defined in the class Cylinder
    if dim == "x":  # x-axis
        args_scaled = [
            dim,
            self.scale_and_translate(c1, self.center[1]),
            self.scale_and_translate(c2, self.center[2]),
            self.scale_and_translate(radius, 0),
            self.scale_and_translate(lo, self.center[0]),
            self.scale_and_translate(hi, self.center[0])
        ]
    elif dim == "y":  # y-axis
        args_scaled = [
            dim,
            self.scale_and_translate(c1, self.center[0]),
            self.scale_and_translate(c2, self.center[2]),
            self.scale_and_translate(radius, 0),
            self.scale_and_translate(lo, self.center[1]),
            self.scale_and_translate(hi, self.center[1])
        ]
    else:  # z-axis
        args_scaled = [
            dim,
            self.scale_and_translate(c1, self.center[0]),
            self.scale_and_translate(c2, self.center[1]),
            self.scale_and_translate(radius, 0),
            self.scale_and_translate(lo, self.center[2]),
            self.scale_and_translate(hi, self.center[2])
        ]
    if self.units == "si":
        C.USER.args = args_scaled
        C.USER.args_siunits = args
    else:  # "lattice"
        C.USER.args = args
        C.USER.args_siunits = args_scaled
    # geometry
    C.USER.geometry = (
        f"Cylinder Region: {C.name}\n"
        "Coordinates: [dim,c1,c2,radius,lo,hi] = dimensions of cylinder\n"
        f"Coordinates (scaled): {C.USER.args}\n"
        f"Coordinates (SI units): {C.USER.args_siunits}\n"
        f"\tdim: {C.USER.args[0]}\n"
        f"\tc1: {C.USER.args[1]}\n"
        f"\tc2: {C.USER.args[2]}\n"
        f"\tradius: {C.USER.args[3]}\n"
        f"\tlo: {C.USER.args[4]}\n"
        f"\thi: {C.USER.args[5]}"
    )
    # other attributes  -------------------------------------
    C.USER.beadtype = C.beadtype  # beadtype to be used for create_atoms
    C.USER.side = C.sidearg(side) # extra parameter side
    C.USER.move = C.movearg(move) # move arg
    C.USER.units = C.unitsarg(units) # units
    C.USER.rotate = C.rotatearg(rotate) # rotate
    C.USER.open = C.openarg(open) # open
    # Create the object if not fake
    if fake:
        return C
    else:
        self.counter["all"] += 1
        self.counter[kind] +=1
        self.objects[name] = C
        self.nobjects += 1
        return None
def delete(self, name)

delete object

Expand source code
def delete(self,name):
    """ delete object """
    if name in self.objects:
        kind = self.objects[name].kind
        del self.objects[name]
        self.nobjects -= 1
        self.counter[kind] -= 1
        self.counter["all"] -= 1
    else:
        raise NameError("%s does not exist (use list()) to list valid objects" % name)
def do(self, printflag=False, verbosity=1)

execute the entire script

Expand source code
def do(self, printflag=False, verbosity=1):
    """ execute the entire script """
    return self.pipescript().do(printflag=printflag, verbosity=verbosity)
def dolive(self)

execute the entire script for online testing see: https://editor.lammps.org/

Expand source code
def dolive(self):
    """
        execute the entire script for online testing
        see: https://editor.lammps.org/
    """
    self.livelammps["file"] = self.script(live=True).tmpwrite()
    if not self.livelammps["active"]:
        livelammps(self.livelammps["URL"],new=0)
        self.livelammps["active"] = True
    return self.livelammps["file"]
def ellipsoid(self, x=0, y=0, z=0, a=5, b=3, c=2, name=None, beadtype=None, fake=False, mass=None, density=None, side=None, units=None, move=None, rotate=None, open=None, index=None, subindex=None, **variables)

creates an ellipsoid region ellipsoid(x,y,z,a,b,c [,name=None,beadtype=None,property=value,…]) x,y,z = center of ellipsoid (distance units) a,b,c = half the length of the principal axes of the ellipsoid (distance units)

URL: <https://docs.lammps.org/region.html>

Main properties = default value
    name = "ellipsoid001"
beadtype = 1
    fake = False (use True to test the execution)
    index, subindex = object index and subindex

Extra properties
    side = "in|out"
   units = "lattice|box" ("box" is forced if regionunits=="si")
    move = "[${v1} ${v2} ${v3}]" or [v1,v2,v3] as a list
           with v1,v2,v3 equal-style variables for x,y,z displacement
           of region over time (distance units)
  rotate = string or 1x7 list (see move) coding for vtheta Px Py Pz Rx Ry Rz
           vtheta = equal-style variable for rotation of region over time (in radians)
           Px,Py,Pz = origin for axis of rotation (distance units)
           Rx,Ry,Rz = axis of rotation vector
    open = integer from 1-6 corresponding to face index


Examples:
    # example with variables created either at creation or later
        R = region(name="my region")
        R.ellipsoid(0, 0, 0, 1, 1, 1,name="E1",toto=3)
        repr(R.E1)
        R.E1.VARIABLES.a=1
        R.E1.VARIABLES.b=2
        R.E1.VARIABLES.c="(${a},${b},100)"
        R.E1.VARIABLES.d = '"%s%s" %("test",${c}) # note that test could be replaced by any function'
    # example with extra parameters
        R.ellipsoid(0,0,0,1,1,1,name="E2",side="out",move=["left","${up}*3",None],up=0.1)
        R.E2.VARIABLES.left = '"swiggle(%s,%s,%s)"%(${a},${b},${c})'
        R.E2.VARIABLES.a="${b}-5"
        R.E2.VARIABLES.b=5
        R.E2.VARIABLES.c=100
Expand source code
def ellipsoid(self,x=0,y=0,z=0,a=5,b=3,c=2,
              name=None,beadtype=None,fake=False,
              mass=None, density=None,
              side=None,units=None,move=None,rotate=None,open=None,
              index = None,subindex = None,
              **variables
              ):
    """
    creates an ellipsoid region
        ellipsoid(x,y,z,a,b,c [,name=None,beadtype=None,property=value,...])
        x,y,z = center of ellipsoid (distance units)
        a,b,c = half the length of the principal axes of the ellipsoid (distance units)

        URL: https://docs.lammps.org/region.html

        Main properties = default value
            name = "ellipsoid001"
        beadtype = 1
            fake = False (use True to test the execution)
            index, subindex = object index and subindex

        Extra properties
            side = "in|out"
           units = "lattice|box" ("box" is forced if regionunits=="si")
            move = "[${v1} ${v2} ${v3}]" or [v1,v2,v3] as a list
                   with v1,v2,v3 equal-style variables for x,y,z displacement
                   of region over time (distance units)
          rotate = string or 1x7 list (see move) coding for vtheta Px Py Pz Rx Ry Rz
                   vtheta = equal-style variable for rotation of region over time (in radians)
                   Px,Py,Pz = origin for axis of rotation (distance units)
                   Rx,Ry,Rz = axis of rotation vector
            open = integer from 1-6 corresponding to face index


        Examples:
            # example with variables created either at creation or later
                R = region(name="my region")
                R.ellipsoid(0, 0, 0, 1, 1, 1,name="E1",toto=3)
                repr(R.E1)
                R.E1.VARIABLES.a=1
                R.E1.VARIABLES.b=2
                R.E1.VARIABLES.c="(${a},${b},100)"
                R.E1.VARIABLES.d = '"%s%s" %("test",${c}) # note that test could be replaced by any function'
            # example with extra parameters
                R.ellipsoid(0,0,0,1,1,1,name="E2",side="out",move=["left","${up}*3",None],up=0.1)
                R.E2.VARIABLES.left = '"swiggle(%s,%s,%s)"%(${a},${b},${c})'
                R.E2.VARIABLES.a="${b}-5"
                R.E2.VARIABLES.b=5
                R.E2.VARIABLES.c=100
    """
    # prepare object creation
    kind = "ellipsoid"
    if index is None: index = self.counter["all"]+1
    if subindex is None: subindex = self.counter[kind]+1
    units = "box" if self.regionunits=="si" and units is None else units  # force box units of regionunits=="si"
    # create the object E with E for Ellipsoid
    obj_mass = mass if mass is not None else self.mass
    obj_density = density if density is not None else self.density
    E = Ellipsoid((self.counter["all"]+1,self.counter[kind]+1),
                  spacefilling=self.isspacefilled, # added on 2023-08-11
                  mass=obj_mass, density=obj_density,
                  index=index,subindex=subindex,
                  lattice_style=self.lattice_style,
                  lattice_scale=self.lattice_scale,
                  lattice_scale_siunits=self.lattice_scale_siunits,
                  **variables)
    # feed USER fields
    if name not in (None,""): E.name = name # object name (if not defined, default name will be used)
    if name in self.name: raise NameError('the name "%s" is already used' % name)
    if beadtype is not None: E.beadtype = beadtype # bead type (if not defined, default index will apply)
    E.USER.ID = "$"+E.name        # add $ to prevent its execution
    # geometry args (2024-07-04)  -------------------------------------
    args = [x, y, z, a, b, c]  # args = [....] as defined in the class Ellipsoid
    args_scaled = [
        self.scale_and_translate(x, self.center[0]),
        self.scale_and_translate(y, self.center[1]),
        self.scale_and_translate(z, self.center[2]),
        self.scale_and_translate(a, 0),
        self.scale_and_translate(b, 0),
        self.scale_and_translate(c, 0)
    ]
    if self.units == "si":
        E.USER.args = args_scaled
        E.USER.args_siunits = args
    else:  # "lattice"
        E.USER.args = args
        E.USER.args_siunits = args_scaled
    # geometry
    E.USER.geometry = (
        f"Ellipsoid Region: {E.name}\n"
        "Coordinates: [x,y,z,a,b,c] = center and radii of ellipsoid\n"
        f"Coordinates (scaled): {E.USER.args}\n"
        f"Coordinates (SI units): {E.USER.args_siunits}\n"
        f"\tcenter: [{E.USER.args[0]}, {E.USER.args[1]}, {E.USER.args[2]}]\n"
        f"\ta: {E.USER.args[3]}\n"
        f"\tb: {E.USER.args[4]}\n"
        f"\tc: {E.USER.args[5]}"
    )
    # other attributes  -------------------------------------
    E.USER.beadtype = E.beadtype  # beadtype to be used for create_atoms
    E.USER.side = E.sidearg(side) # extra parameter side
    E.USER.move = E.movearg(move) # move arg
    E.USER.units = E.unitsarg(units) # units
    E.USER.rotate = E.rotatearg(rotate) # rotate
    E.USER.open = E.openarg(open) # open
    # Create the object if not fake
    if fake:
        return E
    else:
        self.counter["all"] += 1
        self.counter[kind] +=1
        self.objects[name] = E
        self.nobjects += 1
        return None
def eval(self, expression, name=None, beadtype=None, fake=False, index=None, subindex=None)

evaluates (i.e, combine scripts) an expression combining objects R= region(name="my region") R.eval(o1+o2+…,name='obj') R.eval(o1|o2|…,name='obj') R.name will be the resulting object of class region.eval (region.coregeometry)

Expand source code
def eval(self,expression,name=None,beadtype = None,
         fake=False,index = None,subindex = None):
    """
        evaluates (i.e, combine scripts) an expression combining objects
            R= region(name="my region")
            R.eval(o1+o2+...,name='obj')
            R.eval(o1|o2|...,name='obj')
        R.name will be the resulting object of class region.eval (region.coregeometry)
    """
    if not isinstance(expression, coregeometry): raise TypeError("the argument should be a region.coregeometry")
    # prepare object creation
    kind = "eval"
    self.counter["all"] += 1
    self.counter[kind] +=1
    if index is None: index = self.counter["all"]
    if subindex is None: subindex = self.counter[kind]
    # create the object E with E for Ellipsoid
    E = Evalgeometry((self.counter["all"],self.counter[kind]),
                  index=index,subindex=subindex)
    # link expression to E
    if beadtype is not None: E.beadtype = beadtype # bead type (if not defined, default index will apply)
    if name is None: name = expression.name
    if name in self.name: raise NameError('the name "%s" is already used' % name)
    E.name = name
    E.SECTIONS = expression.SECTIONS
    E.USER = expression.USER
    if isinstance(E.SECTIONS,pipescript):
        # set beadtypes for all sections and scripts in the pipeline
        for i in E.SECTIONS.keys():
            for j in range(len(E.SECTIONS[i])):
                E.SECTIONS[i].USER[j].beadtype = E.beadtype
    E.USER.beadtype = beadtype
    # Create the object if not fake
    if fake:
        self.counter["all"] -= 1
        self.counter[kind] -= 1
        return E
    else:
        self.objects[name] = E
        self.nobjects += 1
        return None
def get(self, name)

returns the object

Expand source code
def get(self,name):
    """ returns the object """
    if name in self.objects:
        return self.objects[name]
    else:
        raise NameError('the object "%s" does not exist, use list()' % name)
def group(self, obj, name=None, fake=False)
Expand source code
def group(self,obj,name=None,fake=False):
    pass
def hasattr(self, name)

return true if the object exist

Expand source code
def hasattr(self,name):
    """ return true if the object exist """
    if not isinstance(name,str): raise TypeError("please provide a string")
    return name in self.objects
def intersect(self, *regID, name=None, beadtype=1, fake=False, index=None, subindex=None, **variables)

creates an intersection region intersect("reg-ID1","reg-ID2",name="myname",beadtype=1,…) reg-ID1,reg-ID2, … = IDs of regions to join together

   URL: <https://docs.lammps.org/region.html>

   Main properties = default value
       name = "intersect001"
   beadtype = 1
       fake = False (use True to test the execution)

index, subindex = object index and subindex

Expand source code
def intersect(self,*regID,
          name=None,beadtype=1,fake=False,
          index = None,subindex = None,
          **variables):
    """
    creates an intersection region
          intersect("reg-ID1","reg-ID2",name="myname",beadtype=1,...)
          reg-ID1,reg-ID2, ... = IDs of regions to join together

        URL: https://docs.lammps.org/region.html

        Main properties = default value
            name = "intersect001"
        beadtype = 1
            fake = False (use True to test the execution)
 index, subindex = object index and subindex
    """
    kind = "intersect"
    if index is None: index = self.counter["all"]+1
    if subindex is None: subindex = self.counter[kind]+1
    # create the object I with I for intersect
    I = Intersect((self.counter["all"]+1,self.counter[kind]+1),
                  index=index,subindex=subindex,**variables)
    # feed USER fields
    if name not in (None,""): I.name = name # object name (if not defined, default name will be used)
    if name in self.name: raise NameError('the name "%s" is already used' % name)
    if beadtype is not None: I.beadtype = beadtype # bead type (if not defined, default index will apply)
    I.USER.ID = "$"+I.name        # add $ to prevent its execution
    I.USER.side, I.USER.move, I.USER.units, I.USER.rotate, I.USER.open = "","","","",""
    # build arguments based on regID
    nregID = len(regID)
    if nregID<2: raise ValueError('two objects must be given at least for an intersection')
    args = [None] # the number of arguments is not known yet
    validID = range(nregID)
    for ireg in validID:
        if isinstance(regID[ireg],int):
            if regID[ireg] in validID:
                args.append(self.names[regID[ireg]])
            else:
                raise IndexError(f"the index {regID[ireg]} exceeds the number of objects {len(self)}")
        elif isinstance(regID[ireg],str):
            if regID[ireg] in self:
                args.append(regID[ireg])
            else:
                raise KeyError(f'the object "{regID[ireg]}" does not exist')
        else:
            raise KeyError(f"the {ireg+1}th object should be given as a string or an index")
        # prevent the creation of atoms (avoid duplicates)
        self.objects[regID[ireg]].FLAGSECTIONS["create"] = False
    args[0] = len(regID)
    I.USER.args = args   # args = [....] as defined in the class Union
    # Create the object if not fake
    if fake:
        return I
    else:
        self.counter["all"] += 1
        self.counter[kind] +=1
        self.objects[name] = I
        self.nobjects += 1
        return None
def list(self)

list objects

Expand source code
def list(self):
    """ list objects """
    fmt = "%%%ss:" % max(10,max([len(n) for n in self.names])+2)
    print('REGION container "%s" with %d objects' % (self.name,self.nobjects))
    for o in self.objects.keys():
        print(fmt % self.objects[o].name,"%-10s" % self.objects[o].kind,
              "(beadtype=%d,object index=[%d,%d])" % \
                  (self.objects[o].beadtype,
                   self.objects[o].index,
                   self.objects[o].subindex))
def pipescript(self, printflag=False, verbose=False, verbosity=0)
Expand source code
def pipescript(self,printflag=False,verbose=False,verbosity=0):
    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
    """ pipescript all objects in the region """
    if len(self)<1: return pipescript()
    # execute all objects
    for myobj in self:
        if not isinstance(myobj,Collection): myobj.do(printflag=printflag,verbosity=verbosity)
    # concatenate all objects into a pipe script
    # for collections, only group is accepted
    liste = [x.SECTIONS["variables"] for x in self if not isinstance(x,Collection) and x.hasvariables] + \
            [x.SECTIONS["region"]    for x in self if not isinstance(x,Collection) and x.hasregion] + \
            [x.SECTIONS["create"]    for x in self if not isinstance(x,Collection) and x.hascreate] + \
            [x.SECTIONS["group"]     for x in self if not isinstance(x,Collection) and x.hasgroup] + \
            [x.SECTIONS["setgroup"]  for x in self if not isinstance(x,Collection) and x.hassetgroup] + \
            [x.SECTIONS["move"]      for x in self if not isinstance(x,Collection) and x.hasmove]
    # add the objects within the collection
    for x in self:
        if isinstance(x,Collection): liste += x.group()
    # add the eventual group for the collection
    liste += [x.SECTIONS["group"] for x in self if isinstance(x,Collection) and x.hasgroup]
    # chain all scripts
    return pipescript.join(liste)
def plane(self, px=0, py=0, pz=0, nx=0, ny=0, nz=1, name=None, beadtype=None, fake=False, side=None, units=None, move=None, rotate=None, open=None, index=None, subindex=None, **variables)

creates a plane region px,py,pz = point on the plane (distance units) nx,ny,nz = direction normal to plane (distance units)

   URL: <https://docs.lammps.org/region.html>

   Main properties = default value
       name = "plane001"
   beadtype = 1
       fake = False (use True to test the execution)

index, subindex = object index and subindex

   Extra properties
       side = "in|out"
      units = "lattice|box" ("box" is forced if regionunits=="si")
       move = "[${v1} ${v2} ${v3}]" or [v1,v2,v3] as a list
              with v1,v2,v3 equal-style variables for x,y,z displacement
              of region over time (distance units)
     rotate = string or 1x7 list (see move) coding for vtheta Px Py Pz Rx Ry Rz
              vtheta = equal-style variable for rotation of region over time (in radians)
              Px,Py,Pz = origin for axis of rotation (distance units)
              Rx,Ry,Rz = axis of rotation vector
       open = integer from 1-6 corresponding to face index

   See examples for elliposid()
Expand source code
def plane(self,px=0,py=0,pz=0,nx=0,ny=0,nz=1,
              name=None,beadtype=None,fake=False,
              side=None,units=None,move=None,rotate=None,open=None,
              index = None,subindex = None,
              **variables
              ):
    """
    creates a plane region
          px,py,pz = point on the plane (distance units)
          nx,ny,nz = direction normal to plane (distance units)

        URL: https://docs.lammps.org/region.html

        Main properties = default value
            name = "plane001"
        beadtype = 1
            fake = False (use True to test the execution)
 index, subindex = object index and subindex

        Extra properties
            side = "in|out"
           units = "lattice|box" ("box" is forced if regionunits=="si")
            move = "[${v1} ${v2} ${v3}]" or [v1,v2,v3] as a list
                   with v1,v2,v3 equal-style variables for x,y,z displacement
                   of region over time (distance units)
          rotate = string or 1x7 list (see move) coding for vtheta Px Py Pz Rx Ry Rz
                   vtheta = equal-style variable for rotation of region over time (in radians)
                   Px,Py,Pz = origin for axis of rotation (distance units)
                   Rx,Ry,Rz = axis of rotation vector
            open = integer from 1-6 corresponding to face index

        See examples for elliposid()
    """
    # prepare object creation
    kind = "plane"
    if index is None: index = self.counter["all"]+1
    if subindex is None: subindex = self.counter[kind]+1
    units = "box" if self.regionunits=="si" and units is None else units  # force box units of regionunits=="si"
    # create the object P with P for plane
    P = Plane((self.counter["all"]+1,self.counter[kind]+1),
                  spacefilling=self.isspacefilled, # added on 2023-08-11
                  mass=self.mass, density=self.density, # added on 2024-06-14
                  index=index,subindex=subindex,
                  lattice_style=self.lattice_style,
                  lattice_scale=self.lattice_scale,
                  lattice_scale_siunits=self.lattice_scale_siunits,
                  **variables)
    # feed USER fields
    if name not in (None,""): P.name = name # object name (if not defined, default name will be used)
    if name in self.name: raise NameError('the name "%s" is already used' % name)
    if beadtype is not None: P.beadtype = beadtype # bead type (if not defined, default index will apply)
    P.USER.ID = "$"+P.name        # add $ to prevent its execution
    # geometry args (2024-07-04) ---------------------------
    args = [px, py, pz, nx, ny, nz]  # args = [....] as defined in the class Plane
    args_scaled = [
        self.scale_and_translate(px, self.center[0]),
        self.scale_and_translate(py, self.center[1]),
        self.scale_and_translate(pz, self.center[2]),
        self.scale_and_translate(nx, 0),
        self.scale_and_translate(ny, 0),
        self.scale_and_translate(nz, 0)
    ]
    if self.units == "si":
        P.USER.args = args_scaled
        P.USER.args_siunits = args
    else:  # "lattice"
        P.USER.args = args
        P.USER.args_siunits = args_scaled
    # geometry
    P.USER.geometry = (
        f"Plane Region: {P.name}\n"
        "Coordinates: [px,py,pz,nx,ny,nz] = point and normal vector of plane\n"
        f"Coordinates (scaled): {P.USER.args}\n"
        f"Coordinates (SI units): {P.USER.args_siunits}\n"
        f"\tpoint: [{P.USER.args[0]}, {P.USER.args[1]}, {P.USER.args[2]}]\n"
        f"\tnormal: [{P.USER.args[3]}, {P.USER.args[4]}, {P.USER.args[5]}]"
        )
    # other attributes ---------------------------
    P.USER.beadtype = P.beadtype  # beadtype to be used for create_atoms
    P.USER.side = P.sidearg(side) # extra parameter side
    P.USER.move = P.movearg(move) # move arg
    P.USER.units = P.unitsarg(units) # units
    P.USER.rotate = P.rotatearg(rotate) # rotate
    P.USER.open = P.openarg(open) # open
    # Create the object if not fake
    if fake:
        return P
    else:
        self.counter["all"] += 1
        self.counter[kind] +=1
        self.objects[name] = P
        self.nobjects += 1
        return None
def prism(self, xlo=-5, xhi=5, ylo=-5, yhi=5, zlo=-5, zhi=5, xy=1, xz=1, yz=1, name=None, beadtype=None, fake=False, mass=None, density=None, side=None, units=None, move=None, rotate=None, open=None, index=None, subindex=None, **variables)

creates a prism region xlo,xhi,ylo,yhi,zlo,zhi = bounds of untilted prism (distance units) xy = distance to tilt y in x direction (distance units) xz = distance to tilt z in x direction (distance units) yz = distance to tilt z in y direction (distance units)

   URL: <https://docs.lammps.org/region.html>

   Main properties = default value
       name = "prism001"
   beadtype = 1
       fake = False (use True to test the execution)

index, subindex = object index and subindex

   Extra properties
       side = "in|out"
      units = "lattice|box" ("box" is forced if regionunits=="si")
       move = "[${v1} ${v2} ${v3}]" or [v1,v2,v3] as a list
              with v1,v2,v3 equal-style variables for x,y,z displacement
              of region over time (distance units)
     rotate = string or 1x7 list (see move) coding for vtheta Px Py Pz Rx Ry Rz
              vtheta = equal-style variable for rotation of region over time (in radians)
              Px,Py,Pz = origin for axis of rotation (distance units)
              Rx,Ry,Rz = axis of rotation vector
       open = integer from 1-6 corresponding to face index

   See examples for elliposid()
Expand source code
def prism(self,xlo=-5,xhi=5,ylo=-5,yhi=5,zlo=-5,zhi=5,xy=1,xz=1,yz=1,
              name=None,beadtype=None,fake=False,
              mass=None, density=None,
              side=None,units=None,move=None,rotate=None,open=None,
              index = None,subindex = None,
              **variables
              ):
    """
    creates a prism region
        xlo,xhi,ylo,yhi,zlo,zhi = bounds of untilted prism (distance units)
        xy = distance to tilt y in x direction (distance units)
        xz = distance to tilt z in x direction (distance units)
        yz = distance to tilt z in y direction (distance units)

        URL: https://docs.lammps.org/region.html

        Main properties = default value
            name = "prism001"
        beadtype = 1
            fake = False (use True to test the execution)
 index, subindex = object index and subindex

        Extra properties
            side = "in|out"
           units = "lattice|box" ("box" is forced if regionunits=="si")
            move = "[${v1} ${v2} ${v3}]" or [v1,v2,v3] as a list
                   with v1,v2,v3 equal-style variables for x,y,z displacement
                   of region over time (distance units)
          rotate = string or 1x7 list (see move) coding for vtheta Px Py Pz Rx Ry Rz
                   vtheta = equal-style variable for rotation of region over time (in radians)
                   Px,Py,Pz = origin for axis of rotation (distance units)
                   Rx,Ry,Rz = axis of rotation vector
            open = integer from 1-6 corresponding to face index

        See examples for elliposid()
    """
    # prepare object creation
    kind = "prism"
    if index is None: index = self.counter["all"]+1
    if subindex is None: subindex = self.counter[kind]+1
    units = "box" if self.regionunits=="si" and units is None else units  # force box units of regionunits=="si"
    # create the object P with P for prism
    obj_mass = mass if mass is not None else self.mass
    obj_density = density if density is not None else self.density
    P = Prism((self.counter["all"]+1,self.counter[kind]+1),
                  spacefilling=self.isspacefilled, # added on 2023-08-11
                  mass=obj_mass, density=obj_density, # added on 2024-06-14
                  index=index,subindex=subindex,
                  lattice_style=self.lattice_style,
                  lattice_scale=self.lattice_scale,
                  lattice_scale_siunits=self.lattice_scale_siunits,
                  **variables)
    # feed USER fields
    if name not in (None,""): P.name = name # object name (if not defined, default name will be used)
    if name in self.name: raise NameError('the name "%s" is already used' % name)
    if beadtype is not None: P.beadtype = beadtype # bead type (if not defined, default index will apply)
    P.USER.ID = "$"+P.name        # add $ to prevent its execution
    # geometry args (2024-07-04) ---------------------------
    args = [xlo, xhi, ylo, yhi, zlo, zhi, xy, xz, yz]  # args = [....] as defined in the class Prism
    args_scaled = [
        self.scale_and_translate(xlo, self.center[0]),
        self.scale_and_translate(xhi, self.center[0]),
        self.scale_and_translate(ylo, self.center[1]),
        self.scale_and_translate(yhi, self.center[1]),
        self.scale_and_translate(zlo, self.center[2]),
        self.scale_and_translate(zhi, self.center[2]),
        self.scale_and_translate(xy, 0),
        self.scale_and_translate(xz, 0),
        self.scale_and_translate(yz, 0)
    ]
    if self.units == "si":
        P.USER.args = args_scaled
        P.USER.args_siunits = args
    else:  # "lattice"
        P.USER.args = args
        P.USER.args_siunits = args_scaled
    # geometry
    P.USER.geometry = (
        f"Prism Region: {P.name}\n"
        "Coordinates: [xlo,xhi,ylo,yhi,zlo,zhi,xy,xz,yz] = bounds and tilts of prism\n"
        f"Coordinates (scaled): {P.USER.args}\n"
        f"Coordinates (SI units): {P.USER.args_siunits}\n"
        f"\tbounds: [{P.USER.args[0]}, {P.USER.args[1]}, {P.USER.args[2]}, {P.USER.args[3]}, {P.USER.args[4]}, {P.USER.args[5]}]\n"
        f"\ttilts: [{P.USER.args[6]}, {P.USER.args[7]}, {P.USER.args[8]}]"
    )
    # other attributes ---------------------------
    P.USER.beadtype = P.beadtype  # beadtype to be used for create_atoms
    P.USER.side = P.sidearg(side) # extra parameter side
    P.USER.move = P.movearg(move) # move arg
    P.USER.units = P.unitsarg(units) # units
    P.USER.rotate = P.rotatearg(rotate) # rotate
    P.USER.open = P.openarg(open) # open
    # Create the object if not fake
    if fake:
        return P
    else:
        self.counter["all"] += 1
        self.counter[kind] +=1
        self.objects[name] = P
        self.nobjects += 1
        return None
def pscriptHeaders(self, what=['init', 'lattice', 'box'], **userdefinitions)

Surrogate method for generating LAMMPS pipescript headers. Calls the scriptHeaders method with pipescript=True.

Parameters: - what (list of str): Specifies which scripts to generate. Options are "init", "lattice", and "box". Multiple scripts can be generated by passing a list of these options. Default is ["init", "lattice", "box"]. Property/pair value - nbeads (int): Specifies the number of beads, overriding the default if larger than self.nbeads. Default is 1. - mass (real value or list): Sets the mass for each bead, overrriding self.mass Default is 1.0. Returns: - object: The combined pipescript header scripts as a single object.

Expand source code
def pscriptHeaders(self, what=["init", "lattice", "box"], **userdefinitions):
    """
    Surrogate method for generating LAMMPS pipescript headers.
    Calls the `scriptHeaders` method with `pipescript=True`.

    Parameters:
    - what (list of str): Specifies which scripts to generate. Options are "init", "lattice", and "box".
                          Multiple scripts can be generated by passing a list of these options.
                          Default is ["init", "lattice", "box"].
    Property/pair value
    - nbeads (int): Specifies the number of beads, overriding the default if larger than `self.nbeads`.
                    Default is 1.
    - mass (real value or list): Sets the mass for each bead, overrriding `self.mass`
                    Default is 1.0.
    Returns:
    - object: The combined pipescript header scripts as a single object.
    """
    # Call scriptHeaders with pipescript=True
    return self.scriptHeaders(what=what, pipescript=True, **userdefinitions)
def scale_and_translate(self, value, offset=0)

Scale and translate a value or encapsulate the formula within a string.

If self.regionunits is "si", only the offset is applied without scaling. Otherwise, scaling and translation are performed based on self.units ("si" or "lattice").

Parameters

value (str or float): The value or formula to be scaled and translated. offset (float, optional): The offset to apply. Defaults to 0.

Returns

str or float
The scaled and translated value or formula.
Expand source code
def scale_and_translate(self, value, offset=0):
    """
    Scale and translate a value or encapsulate the formula within a string.

    If self.regionunits is "si", only the offset is applied without scaling.
    Otherwise, scaling and translation are performed based on self.units ("si" or "lattice").

    Parameters:
        value (str or float): The value or formula to be scaled and translated.
        offset (float, optional): The offset to apply. Defaults to 0.

    Returns:
        str or float: The scaled and translated value or formula.
    """
    if self.regionunits == "si":
        # Only apply offset without scaling
        if isinstance(value, str):
            if offset:
                translated = f"({value}) - {offset}"
            else:
                translated = f"{value}"
            return translated
        else:
            if offset:
                return value - offset
            else:
                return value
    else:
        # Existing behavior based on self.units
        if isinstance(value, str):
            if offset:
                translated = f"({value}) - {offset}"
            else:
                translated = f"{value}"
            if self.units == "si":
                return f"({translated}) / {self.lattice_scale} + {offset / self.lattice_scale}"
            else:  # "lattice"
                return f"({translated}) * {self.lattice_scale} + {offset * self.lattice_scale}"
        else:
            if offset:
                translated = value - offset
            else:
                translated = value
            if self.units == "si":
                return translated / self.lattice_scale + (offset / self.lattice_scale)
            else:  # "lattice"
                return translated * self.lattice_scale + (offset * self.lattice_scale)
def scatter(self, E, name='emulsion', beadtype=None)

Parameters

E : scatter or emulsion object
codes for x,y,z and r.
name : string, optional
name of the collection. The default is "emulsion".
beadtype : integer, optional
for all objects. The default is 1.

Raises

TypeError
Return an error of the object is not a scatter type.

Returns

None.

Expand source code
def scatter(self,
             E,
             name="emulsion",
             beadtype=None,
             ):
    """


    Parameters
    ----------
    E : scatter or emulsion object
        codes for x,y,z and r.
    name : string, optional
        name of the collection. The default is "emulsion".
    beadtype : integer, optional
        for all objects. The default is 1.

    Raises
    ------
    TypeError
        Return an error of the object is not a scatter type.

    Returns
    -------
    None.

    """
    if isinstance(E,scatter):
        collect = {}
        for i in range(E.n):
            b = E.beadtype[i] if beadtype==None else beadtype
            nameobj = "glob%02d" % i
            collect[nameobj] = self.sphere(E.x[i],E.y[i],E.z[i],E.r[i],
                        name=nameobj,beadtype=b,fake=True)
        self.collection(**collect,name=name)
    else:
        raise TypeError("the first argument must be an emulsion object")
def script(self, live=False, printflag=None, verbose=None, verbosity=None)

script all objects in the region

Expand source code
def script(self,live=False, printflag=None, verbose=None, verbosity=None):
    """ script all objects in the region """
    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
    s = self.pipescript(printflag=printflag,verbose=verbose,verbosity=verbosity).script(printflag=printflag,verbose=verbose,verbosity=verbosity)
    if self.isspacefilled:
        USERspacefilling =regiondata(**self.spacefilling)
        s = LammpsSpacefilling(**USERspacefilling)+s
    if live:
        beadtypes = self.beadtypes
        USER = regiondata(**self.live)
        USER.nbeads = self.nbeads
        USER.mass = "$"
        USER.pair_coeff = "$"
        # list beadtype and prepare  mass, pair_coeff
        beadtypes = [ x[0] for x in self.count() ]
        if self.isspacefilled and self.spacefillingbeadtype not in beadtypes:
            beadtypes = [self.spacefillingbeadtype]+beadtypes
        for b in beadtypes:
            USER.mass += livetemplate["mass"] % b +"\n"
            USER.pair_coeff += livetemplate["pair_coeff"] %(b,b) +"\n"
        for b1 in beadtypes:
            for b2 in beadtypes:
                if b2>b1:
                    USER.pair_coeff += livetemplate["pair_coeff"] %(b1,b2) +"\n"
        livemode = "dynamic" if self.hasfixmove else "static"
        USER.run =self.livelammps["options"][livemode]["run"]
        s = LammpsHeader(**USER)+s+LammpsFooter(**USER)
    return s
def scriptHeaders(self, what=['init', 'lattice', 'box'], pipescript=False, **userdefinitions)

Generate and return LAMMPS header scripts for initializing the simulation, defining the lattice, and specifying the simulation box for all region objects.

Parameters: - what (list of str): Specifies which scripts to generate. Options are "init", "lattice", "box", "mass" and "preview". Multiple scripts can be generated by passing a list of these options. Default is ["init", "lattice", "box"]. - pipescript (bool): If True, the generated scripts are combined with | instead of +. Default is False.

Property/pair value - nbeads (int): Specifies the number of beads, overriding the default if larger than self.nbeads. Default is 1. - mass (real value or list): Sets the mass for each bead, overrriding self.mass Default is 1.0.

Returns: - object: The combined header scripts as a single object. Header values can be overridden by updating self.headersData.

Raises: - Exception: If no valid script options are provided in what.

Example usage: sRheader = R.scriptHeaders("box").do() # Generate the box header script. sRallheaders = R.scriptHeaders(["init", "lattice", "box"]) # Generate all headers.

Example usage without naming parameters:
sRheader = R.scriptHeaders("box")  # "what" specified as "box", nbeads defaults to 1.

Example of overriding values
sRheader = R.scriptHeaders("lattice",lattice_style = "$sq")  # Generate the lattice header script with the overridden value.
Expand source code
def scriptHeaders(self, what=["init", "lattice", "box"], pipescript=False, **userdefinitions):
    """
        Generate and return LAMMPS header scripts for initializing the simulation, defining the lattice,
        and specifying the simulation box for all region objects.

        Parameters:
        - what (list of str): Specifies which scripts to generate. Options are "init", "lattice", "box", "mass" and "preview".
                              Multiple scripts can be generated by passing a list of these options.
                              Default is ["init", "lattice", "box"].
        - pipescript (bool): If True, the generated scripts are combined with `|` instead of `+`. Default is False.

        Property/pair value
        - nbeads (int): Specifies the number of beads, overriding the default if larger than `self.nbeads`.
                        Default is 1.
        - mass (real value or list): Sets the mass for each bead, overrriding `self.mass`
                        Default is 1.0.


        Returns:
        - object: The combined header scripts as a single object.
                  Header values can be overridden by updating `self.headersData`.

        Raises:
        - Exception: If no valid script options are provided in `what`.

        Example usage:
            sRheader = R.scriptHeaders("box").do()  # Generate the box header script.
            sRallheaders = R.scriptHeaders(["init", "lattice", "box"])  # Generate all headers.

            Example usage without naming parameters:
            sRheader = R.scriptHeaders("box")  # "what" specified as "box", nbeads defaults to 1.

            Example of overriding values
            sRheader = R.scriptHeaders("lattice",lattice_style = "$sq")  # Generate the lattice header script with the overridden value.
    """
    # handle overrides
    USERregion = self.headersData + regiondata(**userdefinitions)
    # Fix singletons
    if not isinstance(what, list):
        what = [what]
    # Generate the initialization script
    scripts = []  # Store all generated script objects here
    if "init" in what:
        scripts.append(LammpsHeaderInit(**USERregion))
    # Generate the lattice script
    if "lattice" in what:
        scripts.append(LammpsHeaderLattice(**USERregion))
    # Generate the box script
    if "box" in what:
        scripts.append(LammpsHeaderBox(**USERregion))
        if self.isspacefilled:
            scripts.append(LammpsSpacefilling(**self.spacefilling))
    # Generate the mass script
    if "mass" in what:
        scripts.append(LammpsHeaderMass(**USERregion))
    # Generate the preview script
    if "preview" in what:
        scripts.append(LammpsFooterPreview(**USERregion))
    if not scripts:
        raise Exception('nothing to do (use: "init", "lattice", "box", "mass" or "preview" within [ ])')

    # Combine the scripts based on the pipescript flag
    combined_script = scripts[0]  # Initialize the combined script with the first element
    for script in scripts[1:]:
        if pipescript:
            # Combine scripts using the | operator, maintaining pipescript format
            combined_script = combined_script | script  # p_ab = s_a | s_b or p_ab = s_a | p_b
        else:
            # Combine scripts using the + operator, maintaining regular script format
            combined_script = combined_script + script  # s_ab = s_a + s_b
    return combined_script
def set(self, name, value)

set field and value

Expand source code
def set(self,name,value):
    """ set field and value """
    if isinstance(value,list) and len(value)==0:
        if name not in self.objects:
            raise NameError('the object "%s" does not exist, use list()' % name)
        self.delete(name)
    elif isinstance(value,coregeometry):
        if name in self.objects: self.delete(name)
        if isinstance(value.SECTIONS,pipescript) or isinstance(value,Evalgeometry):
            self.eval(deepduplicate(value),name) # not a scalar
        else: # scalar
            self.objects[name] = deepduplicate(value)
            self.objects[name].name = name
            self.nobjects += 1
            self.counter["all"] += 1
            self.objects[name].index = self.counter["all"]
            self.counter[value.kind] += 1
def sphere(self, x=0, y=0, z=0, radius=3, name=None, beadtype=None, fake=False, mass=None, density=None, side=None, units=None, move=None, rotate=None, open=None, index=None, subindex=None, **variables)

creates a sphere region x,y,z = center of sphere (distance units) radius = radius of sphere (distance units) x,y,z, and radius can be a variable

   URL: <https://docs.lammps.org/region.html>

   Main properties = default value
       name = "sphere001"
   beadtype = 1
       fake = False (use True to test the execution)

index, subindex = object index and subindex

   Extra properties
       side = "in|out"
      units = "lattice|box" ("box" is forced if regionunits=="si")
       move = "[${v1} ${v2} ${v3}]" or [v1,v2,v3] as a list
              with v1,v2,v3 equal-style variables for x,y,z displacement
              of region over time (distance units)
     rotate = string or 1x7 list (see move) coding for vtheta Px Py Pz Rx Ry Rz
              vtheta = equal-style variable for rotation of region over time (in radians)
              Px,Py,Pz = origin for axis of rotation (distance units)
              Rx,Ry,Rz = axis of rotation vector
       open = integer from 1-6 corresponding to face index

   See examples for elliposid()
Expand source code
def sphere(self,x=0,y=0,z=0,radius=3,
              name=None,beadtype=None,fake=False,
              mass=None, density=None,
              side=None,units=None,move=None,rotate=None,open=None,
              index = None,subindex = None,
              **variables
              ):
    """
    creates a sphere region
          x,y,z = center of sphere (distance units)
          radius = radius of sphere (distance units)
          x,y,z, and radius can be a variable

        URL: https://docs.lammps.org/region.html

        Main properties = default value
            name = "sphere001"
        beadtype = 1
            fake = False (use True to test the execution)
 index, subindex = object index and subindex

        Extra properties
            side = "in|out"
           units = "lattice|box" ("box" is forced if regionunits=="si")
            move = "[${v1} ${v2} ${v3}]" or [v1,v2,v3] as a list
                   with v1,v2,v3 equal-style variables for x,y,z displacement
                   of region over time (distance units)
          rotate = string or 1x7 list (see move) coding for vtheta Px Py Pz Rx Ry Rz
                   vtheta = equal-style variable for rotation of region over time (in radians)
                   Px,Py,Pz = origin for axis of rotation (distance units)
                   Rx,Ry,Rz = axis of rotation vector
            open = integer from 1-6 corresponding to face index

        See examples for elliposid()
    """
    # prepare object creation
    kind = "sphere"
    if index is None: index = self.counter["all"]+1
    if subindex is None: subindex = self.counter[kind]+1
    units = "box" if self.regionunits=="si" and units is None else units  # force box units of regionunits=="si"
    # create the object S with S for sphere
    obj_mass = mass if mass is not None else self.mass
    obj_density = density if density is not None else self.density
    S = Sphere((self.counter["all"]+1,self.counter[kind]+1),
                  spacefilling=self.isspacefilled, # added on 2023-08-11
                  mass=obj_mass, density=obj_density, # added on 2024-06-14
                  index=index,subindex=subindex,
                  lattice_style=self.lattice_style,
                  lattice_scale=self.lattice_scale,
                  lattice_scale_siunits=self.lattice_scale_siunits,
                  **variables)
    # feed USER fields
    if name not in (None,""): S.name = name # object name (if not defined, default name will be used)
    if name in self.name: raise NameError('the name "%s" is already used' % name)
    if beadtype is not None: S.beadtype = beadtype # bead type (if not defined, default index will apply)
    S.USER.ID = "$"+S.name        # add $ to prevent its execution
    # geometry args (2024-07-04) ---------------------------
    args = [x, y, z, radius]  # args = [....] as defined in the class Sphere
    args_scaled = [
        self.scale_and_translate(x, self.center[0]),
        self.scale_and_translate(y, self.center[1]),
        self.scale_and_translate(z, self.center[2]),
        self.scale_and_translate(radius, 0)
    ]
    if self.units == "si":
        S.USER.args = args_scaled
        S.USER.args_siunits = args
    else:  # "lattice"
        S.USER.args = args
        S.USER.args_siunits = args_scaled
    # geometry
    S.USER.geometry = (
        f"Sphere Region: {S.name}\n"
        "Coordinates: [x,y,z,radius] = center and radius of sphere\n"
        f"Coordinates (scaled): {S.USER.args}\n"
        f"Coordinates (SI units): {S.USER.args_siunits}\n"
        f"\tcenter: [{S.USER.args[0]}, {S.USER.args[1]}, {S.USER.args[2]}]\n"
        f"\tradius: {S.USER.args[3]}"
    )
    # other attributes ---------------------------
    S.USER.beadtype = S.beadtype  # beadtype to be used for create_atoms
    S.USER.side = S.sidearg(side) # extra parameter side
    S.USER.move = S.movearg(move) # move arg
    S.USER.units = S.unitsarg(units) # units
    S.USER.rotate = S.rotatearg(rotate) # rotate
    S.USER.open = S.openarg(open) # open
    # Create the object if not fake
    if fake:
        return S
    else:
        self.counter["all"] += 1
        self.counter[kind] +=1
        self.objects[name] = S
        self.nobjects += 1
        return None
def union(self, *regID, name=None, beadtype=1, fake=False, index=None, subindex=None, **variables)

creates a union region union("reg-ID1","reg-ID2",name="myname",beadtype=1,…) reg-ID1,reg-ID2, … = IDs of regions to join together

   URL: <https://docs.lammps.org/region.html>

   Main properties = default value
       name = "union001"
   beadtype = 1
       fake = False (use True to test the execution)

index, subindex = object index and subindex

Expand source code
def union(self,*regID,
          name=None,beadtype=1,fake=False,
          index = None,subindex = None,
          **variables):
    """
    creates a union region
          union("reg-ID1","reg-ID2",name="myname",beadtype=1,...)
          reg-ID1,reg-ID2, ... = IDs of regions to join together

        URL: https://docs.lammps.org/region.html

        Main properties = default value
            name = "union001"
        beadtype = 1
            fake = False (use True to test the execution)
 index, subindex = object index and subindex
    """
    kind = "union"
    if index is None: index = self.counter["all"]+1
    if subindex is None: subindex = self.counter[kind]+1
    # create the object U with U for union
    U = Union((self.counter["all"]+1,self.counter[kind]+1),
                  index=index,subindex=subindex,**variables)
    # feed USER fields
    if name not in (None,""): U.name = name # object name (if not defined, default name will be used)
    if name in self.name: raise NameError('the name "%s" is already used' % name)
    if beadtype is not None: U.beadtype = beadtype # bead type (if not defined, default index will apply)
    U.USER.ID = "$"+U.name        # add $ to prevent its execution
    U.USER.side, U.USER.move, U.USER.units, U.USER.rotate, U.USER.open = "","","","",""
    # build arguments based on regID
    nregID = len(regID)
    if nregID<2: raise ValueError('two objects must be given at least for an union')
    args = [None] # the number of arguments is not known yet
    validID = range(nregID)
    for ireg in validID:
        if isinstance(regID[ireg],int):
            if regID[ireg] in validID:
                args.append(self.names[regID[ireg]])
            else:
                raise IndexError(f"the index {regID[ireg]} exceeds the number of objects {len(self)}")
        elif isinstance(regID[ireg],str):
            if regID[ireg] in self:
                args.append(regID[ireg])
            else:
                raise KeyError(f'the object "{regID[ireg]}" does not exist')
        else:
            raise KeyError(f"the {ireg+1}th object should be given as a string or an index")
        # prevent the creation of atoms merged (avoid duplicates)
        self.objects[regID[ireg]].FLAGSECTIONS["create"] = False
    args[0] = len(regID)
    U.USER.args = args   # args = [....] as defined in the class Union
    # Create the object if not fake
    if fake:
        return U
    else:
        self.counter["all"] += 1
        self.counter[kind] +=1
        self.objects[name] = U
        self.nobjects += 1
        return None
class regioncollection (*obj, **kwobj)

regioncollection class container (not to be called directly)

constructor

Expand source code
class regioncollection(struct):
    """ regioncollection class container (not to be called directly) """
    _type = "collect"               # object type
    _fulltype = "Collections"    # full name
    _ftype = "collection"        # field name
    def __init__(self,*obj,**kwobj):
        # store the objects with their alias
        super().__init__(**kwobj)
        # store objects with their real names
        for o in obj:
            if isinstance(o,region):
                s = struct.dict2struct(o.objects)
                list_s = s.keys()
                for i in range(len(list_s)): self.setattr(list_s[i], s[i].copy())
            elif o!=None:
                self.setattr(o.name, o.copy())

Ancestors

  • pizza.private.mstruct.struct
class regiondata (sortdefinitions=False, **kwargs)

class of script parameters Typical constructor: DEFINITIONS = regiondata( var1 = value1, var2 = value2 ) See script, struct, param to get review all methods attached to it

constructor

Expand source code
class regiondata(paramauto):
    """
        class of script parameters
            Typical constructor:
                DEFINITIONS = regiondata(
                    var1 = value1,
                    var2 = value2
                    )
        See script, struct, param to get review all methods attached to it
    """
    _type = "RD"
    _fulltype = "region data"
    _ftype = "definition"

    def generatorforlammps(self,verbose=False,hasvariables=False):
        """
            generate LAMMPS code from regiondata (struct)
            generatorforlammps(verbose,hasvariables)
            hasvariables = False is used to prevent a call of generatorforLammps()
            for scripts others than LammpsGeneric ones
        """
        nk = len(self)
        if nk>0:
            self.sortdefinitions(raiseerror=False)
            s = self.tostruct()
            ik = 0
            fmt = "variable %s equal %s"
            cmd = "\n#"+"_"*40+"\n"+f"#[{str(datetime.now())}]\n" if verbose else ""
            cmd += f"\n# Definition of {nk} variables (URL: https://docs.lammps.org/variable.html)\n"
            if hasvariables:
                for k in s.keys():
                    ik += 1
                    end = "\n" if ik<nk else "\n"*2
                    v = getattr(s,k)
                    if v is None: v = "NULL"
                    if isinstance(v,(int,float)) or v == None:
                        cmd += fmt % (k,v)+end
                    elif isinstance(v,str):
                        cmd += fmt % (k,f'{v}')+end
                    elif isinstance(v,(list,tuple)):
                        cmd += fmt % (k,span(v))+end
                    else:
                        raise TypeError(f"unsupported type for the variable {k} set to {v}")
                if verbose: cmd += "#"+"_"*40+"\n"
        return cmd

Ancestors

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

Subclasses

Methods

def generatorforlammps(self, verbose=False, hasvariables=False)

generate LAMMPS code from regiondata (struct) generatorforlammps(verbose,hasvariables) hasvariables = False is used to prevent a call of generatorforLammps() for scripts others than LammpsGeneric ones

Expand source code
def generatorforlammps(self,verbose=False,hasvariables=False):
    """
        generate LAMMPS code from regiondata (struct)
        generatorforlammps(verbose,hasvariables)
        hasvariables = False is used to prevent a call of generatorforLammps()
        for scripts others than LammpsGeneric ones
    """
    nk = len(self)
    if nk>0:
        self.sortdefinitions(raiseerror=False)
        s = self.tostruct()
        ik = 0
        fmt = "variable %s equal %s"
        cmd = "\n#"+"_"*40+"\n"+f"#[{str(datetime.now())}]\n" if verbose else ""
        cmd += f"\n# Definition of {nk} variables (URL: https://docs.lammps.org/variable.html)\n"
        if hasvariables:
            for k in s.keys():
                ik += 1
                end = "\n" if ik<nk else "\n"*2
                v = getattr(s,k)
                if v is None: v = "NULL"
                if isinstance(v,(int,float)) or v == None:
                    cmd += fmt % (k,v)+end
                elif isinstance(v,str):
                    cmd += fmt % (k,f'{v}')+end
                elif isinstance(v,(list,tuple)):
                    cmd += fmt % (k,span(v))+end
                else:
                    raise TypeError(f"unsupported type for the variable {k} set to {v}")
            if verbose: cmd += "#"+"_"*40+"\n"
    return cmd
class rigidwall (beadtype=1, userid=None, USER=forcefield (FF object) with 0 parameters)

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

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

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

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

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

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

Ancestors

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

Class variables

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

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

override any property with USER.property = value

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

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

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

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

Ancestors

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

Class variables

var description
var name
var userid
var version
class scatter

generic top scatter class

The scatter class provides an easy constructor to distribute in space objects according to their positions x,y,z size r (radius) and beadtype.

The class is used to derive emulsions.

Returns

None.

Expand source code
class scatter():
    """ generic top scatter class """
    def __init__(self):
        """
        The scatter class provides an easy constructor
        to distribute in space objects according to their
        positions x,y,z size r (radius) and beadtype.

        The class is used to derive emulsions.

        Returns
        -------
        None.
        """
        self.x = np.array([],dtype=int)
        self.y = np.array([],dtype=int)
        self.z = np.array([],dtype=int)
        self.r = np.array([],dtype=int)
        self.beadtype = []

    @property
    def n(self):
        return len(self.x)

    def pairdist(self,x,y,z):
        """ pair distance to the surface of all disks/spheres """
        if self.n==0:
            return np.Inf
        else:
            return np.sqrt((x-self.x)**2+(y-self.y)**2+(z-self.z)**2)-self.r

Subclasses

Instance variables

var n
Expand source code
@property
def n(self):
    return len(self.x)

Methods

def pairdist(self, x, y, z)

pair distance to the surface of all disks/spheres

Expand source code
def pairdist(self,x,y,z):
    """ pair distance to the surface of all disks/spheres """
    if self.n==0:
        return np.Inf
    else:
        return np.sqrt((x-self.x)**2+(y-self.y)**2+(z-self.z)**2)-self.r
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

  • 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
  • LammpsGeneric

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="region.script" href="#region.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
class scriptobject (beadtype=1, name=None, fullname='', filename='', style='smd', mass=1.0, forcefield=LAMMPS:SMD:none:walls, group=[], USER=script data (SD object) with 0 definitions)

scriptobject: A Class for Managing Script Objects in LAMMPS

The scriptobject class is designed to represent individual objects in LAMMPS scripts, such as beads, atoms, or other components. Each object is associated with a forcefield instance that defines the physical interactions of the object, and the class supports a variety of properties for detailed object definition. Additionally, scriptobject instances can be grouped together and compared based on their properties, such as beadtype and name.

Key Features:

  • Forcefield Integration: Each scriptobject is associated with a forcefield instance, allowing for customized physical interactions. Forcefields can be passed via the USER keyword for dynamic parameterization.
  • Grouping: Multiple scriptobject instances can be combined into a scriptobjectgroup using the + operator, allowing for complex collections of objects.
  • Object Comparison: scriptobject instances can be compared and sorted based on their beadtype and name, enabling efficient organization and manipulation of objects.
  • Piping and Execution: Supports the pipe (|) operator, allowing scriptobject instances to be used in script pipelines alongside other script elements.

Practical Use Cases:

  • Object Definition in LAMMPS: Use scriptobject to represent individual objects in a simulation, including their properties and associated forcefields.
  • Forcefield Parameterization: Pass customized parameters to the forcefield via the USER keyword to dynamically adjust the physical interactions.
  • Grouping and Sorting: Combine multiple objects into groups, or sort them based on their properties (e.g., beadtype) for easier management in complex simulations.

Methods:

init(self, beadtype=1, name="undefined", fullname="", filename="", style="smd", forcefield=rigidwall(), group=[], USER=scriptdata()): Initializes a new scriptobject with the specified properties, including beadtype, name, forcefield, and optional group.

str(self): Returns a string representation of the scriptobject, showing its beadtype and name.

add(self, SO): Combines two scriptobject instances or a scriptobject with a scriptobjectgroup. Raises an error if the two objects have the same name or if the second operand is not a valid scriptobject or scriptobjectgroup.

or(self, pipe): Overloads the pipe (|) operator to integrate the scriptobject into a pipeline.

eq(self, SO): Compares two scriptobject instances, returning True if they have the same beadtype and name.

ne(self, SO): Returns True if the two scriptobject instances differ in either beadtype or name.

lt(self, SO): Compares the beadtype of two scriptobject instances, returning True if the left object's beadtype is less than the right object's.

gt(self, SO): Compares the beadtype of two scriptobject instances, returning True if the left object's beadtype is greater than the right object's.

le(self, SO): Returns True if the beadtype of the left scriptobject is less than or equal to the right scriptobject.

ge(self, SO): Returns True if the beadtype of the left scriptobject is greater than or equal to the right scriptobject.

Attributes:

beadtype : int The type of bead or object, used for distinguishing between different types in the simulation. name : str A short name for the object, useful for quick identification. fullname : str A comprehensive name for the object. If not provided, defaults to the name with "object definition". filename : str The path to the file containing the input data for the object. style : str The style of the object (e.g., "smd" for smoothed dynamics). forcefield : forcefield The forcefield instance associated with the object, defining its physical interactions. group : list A list of other scriptobject instances that are grouped with this object. USER : scriptdata A collection of user-defined variables for customizing the forcefield or other properties.

Original Content:

The scriptobject class enables the definition of objects within LAMMPS scripts, providing: - Beadtype and Naming: Objects are distinguished by their beadtype and name, allowing for comparison and sorting based on these properties. - Forcefield Support: Objects are linked to a forcefield instance, and user-defined forcefield parameters can be passed through the USER keyword. - Group Management: Multiple objects can be grouped together using the + operator, forming a scriptobjectgroup. - Comparison Operators: Objects can be compared based on their beadtype and name, using standard comparison operators (==, <, >, etc.). - Pipelines: scriptobject instances can be integrated into pipelines, supporting the | operator for use in sequential script execution.

Example Usage:

from pizza.scriptobject import scriptobject, rigidwall, scriptdata

# Define a script object with custom properties
obj1 = scriptobject(beadtype=1, name="bead1", forcefield=rigidwall(USER=scriptdata(param1=10)))

# Combine two objects into a group
obj2 = scriptobject(beadtype=2, name="bead2")
group = obj1 + obj2

# Print object information
print(obj1)
print(group)

The output will be:

script object | type=1 | name=bead1
scriptobjectgroup containing 2 objects

Overview

class of script object
    OBJ = scriptobject(...)
    Implemented properties:
        beadtype=1,2,...
        name="short name"
        fullname = "comprehensive name"
        filename = "/path/to/your/inputfile"
        style = "smd"
        forcefield = any valid forcefield instance (default = rigidwall())
        mass = 1.0

note: use a forcefield instance with the keywork USER to pass user FF parameters
examples:   rigidwall(USER=scriptdata(...))
            solidfood(USER==scriptdata(...))
            water(USER==scriptdata(...))

group objects with OBJ1+OBJ2... into scriptobjectgroups

objects can be compared and sorted based on beadtype and name

constructor

Expand source code
class scriptobject(struct):
    """
    scriptobject: A Class for Managing Script Objects in LAMMPS

    The `scriptobject` class is designed to represent individual objects in LAMMPS scripts,
    such as beads, atoms, or other components. Each object is associated with a `forcefield`
    instance that defines the physical interactions of the object, and the class supports
    a variety of properties for detailed object definition. Additionally, `scriptobject`
    instances can be grouped together and compared based on their properties, such as
    `beadtype` and `name`.

    Key Features:
    -------------
    - **Forcefield Integration**: Each `scriptobject` is associated with a `forcefield`
      instance, allowing for customized physical interactions. Forcefields can be passed
      via the `USER` keyword for dynamic parameterization.
    - **Grouping**: Multiple `scriptobject` instances can be combined into a
      `scriptobjectgroup` using the `+` operator, allowing for complex collections of objects.
    - **Object Comparison**: `scriptobject` instances can be compared and sorted based on
      their `beadtype` and `name`, enabling efficient organization and manipulation of objects.
    - **Piping and Execution**: Supports the pipe (`|`) operator, allowing `scriptobject`
      instances to be used in script pipelines alongside other script elements.

    Practical Use Cases:
    --------------------
    - **Object Definition in LAMMPS**: Use `scriptobject` to represent individual objects in
      a simulation, including their properties and associated forcefields.
    - **Forcefield Parameterization**: Pass customized parameters to the forcefield via the
      `USER` keyword to dynamically adjust the physical interactions.
    - **Grouping and Sorting**: Combine multiple objects into groups, or sort them based
      on their properties (e.g., `beadtype`) for easier management in complex simulations.

    Methods:
    --------
    __init__(self, beadtype=1, name="undefined", fullname="", filename="", style="smd",
             forcefield=rigidwall(), group=[], USER=scriptdata()):
        Initializes a new `scriptobject` with the specified properties, including `beadtype`,
        `name`, `forcefield`, and optional `group`.

    __str__(self):
        Returns a string representation of the `scriptobject`, showing its `beadtype` and `name`.

    __add__(self, SO):
        Combines two `scriptobject` instances or a `scriptobject` with a `scriptobjectgroup`.
        Raises an error if the two objects have the same `name` or if the second operand is not
        a valid `scriptobject` or `scriptobjectgroup`.

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

    __eq__(self, SO):
        Compares two `scriptobject` instances, returning `True` if they have the same
        `beadtype` and `name`.

    __ne__(self, SO):
        Returns `True` if the two `scriptobject` instances differ in either `beadtype` or `name`.

    __lt__(self, SO):
        Compares the `beadtype` of two `scriptobject` instances, returning `True` if the
        left object's `beadtype` is less than the right object's.

    __gt__(self, SO):
        Compares the `beadtype` of two `scriptobject` instances, returning `True` if the
        left object's `beadtype` is greater than the right object's.

    __le__(self, SO):
        Returns `True` if the `beadtype` of the left `scriptobject` is less than or equal to
        the right `scriptobject`.

    __ge__(self, SO):
        Returns `True` if the `beadtype` of the left `scriptobject` is greater than or equal
        to the right `scriptobject`.

    Attributes:
    -----------
    beadtype : int
        The type of bead or object, used for distinguishing between different types in the simulation.
    name : str
        A short name for the object, useful for quick identification.
    fullname : str
        A comprehensive name for the object. If not provided, defaults to the `name` with "object definition".
    filename : str
        The path to the file containing the input data for the object.
    style : str
        The style of the object (e.g., "smd" for smoothed dynamics).
    forcefield : forcefield
        The forcefield instance associated with the object, defining its physical interactions.
    group : list
        A list of other `scriptobject` instances that are grouped with this object.
    USER : scriptdata
        A collection of user-defined variables for customizing the forcefield or other properties.

    Original Content:
    -----------------
    The `scriptobject` class enables the definition of objects within LAMMPS scripts, providing:
    - **Beadtype and Naming**: Objects are distinguished by their `beadtype` and `name`, allowing
      for comparison and sorting based on these properties.
    - **Forcefield Support**: Objects are linked to a forcefield instance, and user-defined forcefield
      parameters can be passed through the `USER` keyword.
    - **Group Management**: Multiple objects can be grouped together using the `+` operator, forming
      a `scriptobjectgroup`.
    - **Comparison Operators**: Objects can be compared based on their `beadtype` and `name`, using
      standard comparison operators (`==`, `<`, `>`, etc.).
    - **Pipelines**: `scriptobject` instances can be integrated into pipelines, supporting the `|`
      operator for use in sequential script execution.

    Example Usage:
    --------------
    ```
    from pizza.scriptobject import scriptobject, rigidwall, scriptdata

    # Define a script object with custom properties
    obj1 = scriptobject(beadtype=1, name="bead1", forcefield=rigidwall(USER=scriptdata(param1=10)))

    # Combine two objects into a group
    obj2 = scriptobject(beadtype=2, name="bead2")
    group = obj1 + obj2

    # Print object information
    print(obj1)
    print(group)
    ```

    The output will be:
    ```
    script object | type=1 | name=bead1
    scriptobjectgroup containing 2 objects
    ```

    OVERVIEW
    --------------

        class of script object
            OBJ = scriptobject(...)
            Implemented properties:
                beadtype=1,2,...
                name="short name"
                fullname = "comprehensive name"
                filename = "/path/to/your/inputfile"
                style = "smd"
                forcefield = any valid forcefield instance (default = rigidwall())
                mass = 1.0

        note: use a forcefield instance with the keywork USER to pass user FF parameters
        examples:   rigidwall(USER=scriptdata(...))
                    solidfood(USER==scriptdata(...))
                    water(USER==scriptdata(...))

        group objects with OBJ1+OBJ2... into scriptobjectgroups

        objects can be compared and sorted based on beadtype and name

    """
    _type = "SO"
    _fulltype = "script object"
    _ftype = "propertie"

    def __init__(self,
                 beadtype = 1,
                 name = None,
                 fullname="",
                 filename="",
                 style="smd",
                 mass=1.0, # added on 2024-11-29
                 forcefield=rigidwall(),
                 group=[],
                 USER = scriptdata()
                 ):
        name = f"beadtype={beadtype}" if name is None else name
        if not isinstance(name,str):
            TypeError(f"name must a string or None got {type(name)}")
        if fullname=="": fullname = name + " object definition"
        if not isinstance(group,list): group = [group]
        forcefield.beadtype = beadtype
        forcefield.userid = name
        forcefield.USER = USER
        super(scriptobject,self).__init__(
              beadtype = beadtype,
                  name = name,
              fullname = fullname,
              filename = filename,
                 style = style,
            forcefield = forcefield,
                  mass = mass,
                 group = group,
                  USER = USER
                 )

    def __str__(self):
        """ string representation """
        return f"{self._fulltype} | type={self.beadtype} | name={self.name}"

    def __add__(self, SO):
        if isinstance(SO,scriptobject):
            if SO.name != self.name:
                if SO.beadtype == self.beadtype:
                   SO.beadtype =  self.beadtype+1
                return scriptobjectgroup(self,SO)
            else:
                raise ValueError('the object "%s" already exists' % SO.name)
        elif isinstance(SO,scriptobjectgroup):
            return scriptobjectgroup(self)+SO
        else:
            return ValueError("The object should a script object or its container")

    def __or__(self, pipe):
        """ overload | or for pipe """
        if isinstance(pipe,(pipescript,script,scriptobject,scriptobjectgroup)):
            return pipescript(self) | pipe
        else:
            raise ValueError("the argument must a pipescript, a scriptobject or a scriptobjectgroup")

    def __eq__(self, SO):
        return isinstance(SO,scriptobject) and (self.beadtype == SO.beadtype) and (self.mass == SO.mass) \
            and (self.name == SO.name)

    def __ne__(self, SO):
        return not isinstance(SO,scriptobject) or (self.beadtype != SO.beadtype) or (self.mass != SO.mass) or (self.name != SO.name)

    def __lt__(self, SO):
        return self.beadtype < SO.beadtype

    def __gt__(self, SO):
        return self.beadtype > SO.beadtype

    def __le__(self, SO):
        return self.beadtype <= SO.beadtype

    def __ge__(self, SO):
        return self.beadtype >= SO.beadtype

Ancestors

  • pizza.private.mstruct.struct
class smd

SMD forcefield

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

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

Ancestors

  • pizza.forcefield.forcefield

Subclasses

  • pizza.forcefield.none
  • pizza.forcefield.tlsph
  • pizza.forcefield.ulsph

Class variables

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

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

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

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

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

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

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

Ancestors

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

Class variables

var description
var name
var userid
var version
class struct (**kwargs)

Class: struct

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


Features

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

Examples

Basic Usage

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

Using param for Evaluation

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

Concatenation and Subtraction

Fields from the right-most structure overwrite existing values.

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

Practical Shorthands

Constructing a Structure from Keys

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

Building a Structure from Variables in a String

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

Indexing and Iteration

Structures can be indexed or sliced like lists.

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

Dynamic Dependency Management

struct provides control over dependencies, sorting, and evaluation.

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

For dynamic evaluation, use param:

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

Overloaded Methods and Operators

Supported Operators

  • +: Concatenation of two structures (__add__).
  • -: Subtraction of fields (__sub__).
  • len(): Number of fields (__len__).
  • in: Check for field existence (__contains__).

Method Overview

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

Dynamic Properties

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

constructor

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

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

    ---

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

    ---

    ### Examples

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

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

    ---

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

    ---

    ### Practical Shorthands

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

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

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

    ---

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

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

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

    ---

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

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

    ---

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

    ---
    """

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

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

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


    # Methods
    def __init__(self,**kwargs):
        """ constructor """
        # Optionally extend _excludedattr here
        self._excludedattr = self._excludedattr | {'_excludedattr', '_type', '_fulltype','_ftype'} # addition 2024-10-11
        self.set(**kwargs)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    def __getitem__(self,idx):
        """
            s[i] returns the ith element of the structure
            s[:4] returns a structure with the four first fields
            s[[1,3]] returns the second and fourth elements
        """
        if isinstance(idx,int):
            if idx<len(self):
                return self.getattr(self.keys()[idx])
            raise IndexError(f"the {self._ftype} index should be comprised between 0 and {len(self)-1}")
        elif isinstance(idx,slice):
            return struct.fromkeysvalues(self.keys()[idx], self.values()[idx])
        elif isinstance(idx,(list,tuple)):
            k,v= self.keys(), self.values()
            nk = len(k)
            s = param() if isinstance(self,param) else struct()
            for i in idx:
                if isinstance(i,int) and i>=0 and i<nk:
                    s.setattr(k[i],v[i])
                else:
                    raise IndexError("idx must contains only integers ranged between 0 and %d" % (nk-1))
            return s
        elif isinstance(idx,str):
            return self.getattr(idx)
        else:
            raise TypeError("The index must be an integer or a slice and not a %s" % type(idx).__name__)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

    def generator(self):
        """ generate Python code of the equivalent structure """
        nk = len(self)
        if nk==0:
            print("X = struct()")
        else:
            ik = 0
            fmt = "%%%ss=" % max(10,max([len(k) for k in self.keys()])+2)
            print("\nX = struct(")
            for k in self.keys():
                ik += 1
                end = ",\n" if ik<nk else "\n"+(fmt[:-1] % ")")+"\n"
                v = getattr(self,k)
                if isinstance(v,(int,float)) or v == None:
                    print(fmt % k,v,end=end)
                elif isinstance(v,str):
                    print(fmt % k,f'"{v}"',end=end)
                elif isinstance(v,(list,tuple)):
                    print(fmt % k,v,end=end)
                else:
                    print(fmt % k,"/* unsupported type */",end=end)

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

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


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

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

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


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

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

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

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


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

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

        Protected attributes defined in _excludedattr are not updated.

        Usage:
        ------
        s.update(a=10, b=[1, 2, 3], new_field="new_value")
        """
        protected_attributes = getattr(self, '_excludedattr', ())

        for key, value in kwargs.items():
            if key in protected_attributes:
                print(f"Warning: Cannot update protected attribute '{key}'")
            else:
                self.setattr(key, value)


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

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

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

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

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

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

        return sub_struct


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

Subclasses

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

Static methods

def dict2struct(dico, makeparam=False)

create a structure from a dictionary

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

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

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

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

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

isstrexpression(string) returns true if s contains an expression

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

read the equivalent structure read(filename)

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

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

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

scan(string) scan a string for variables

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

Instance variables

var isempty

isempty is set to True for an empty structure

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

same structure with True if it is an expression

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

Methods

def check(self, default)

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

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

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

clear() delete all fields while preserving the original class

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

display method

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

optimize display

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

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

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

returns a structure from keys

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

generate Python code of the equivalent structure

Expand source code
def generator(self):
    """ generate Python code of the equivalent structure """
    nk = len(self)
    if nk==0:
        print("X = struct()")
    else:
        ik = 0
        fmt = "%%%ss=" % max(10,max([len(k) for k in self.keys()])+2)
        print("\nX = struct(")
        for k in self.keys():
            ik += 1
            end = ",\n" if ik<nk else "\n"+(fmt[:-1] % ")")+"\n"
            v = getattr(self,k)
            if isinstance(v,(int,float)) or v == None:
                print(fmt % k,v,end=end)
            elif isinstance(v,str):
                print(fmt % k,f'"{v}"',end=end)
            elif isinstance(v,(list,tuple)):
                print(fmt % k,v,end=end)
            else:
                print(fmt % k,"/* unsupported type */",end=end)
def getattr(self, key)

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

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

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

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

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

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

return all elements as iterable key, value

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

return the fields

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

sort keys by length()

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

initialization

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

set field and value

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

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

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

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

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

create a dictionary from the current structure

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

convert an object struct() to param()

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

Update multiple fields at once, while protecting certain attributes.

Parameters:

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

Protected attributes defined in _excludedattr are not updated.

Usage:

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

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

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

    Protected attributes defined in _excludedattr are not updated.

    Usage:
    ------
    s.update(a=10, b=[1, 2, 3], new_field="new_value")
    """
    protected_attributes = getattr(self, '_excludedattr', ())

    for key, value in kwargs.items():
        if key in protected_attributes:
            print(f"Warning: Cannot update protected attribute '{key}'")
        else:
            self.setattr(key, value)
def values(self)

return the values

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

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

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

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

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

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

zip keys and values

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

SMD:TLSPH forcefield (total Lagrangian)

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

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

Ancestors

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

Subclasses

  • pizza.forcefield.saltTLSPH
  • pizza.forcefield.solidfood

Class variables

var PAIR_DIAGCOEFF
var PAIR_OFFDIAGCOEFF
var description
var name
class ulsph

SMD:ULSPH forcefield (updated Lagrangian)

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

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

Ancestors

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

Subclasses

  • pizza.forcefield.water

Class variables

var PAIR_DIAGCOEFF
var PAIR_OFFDIAGCOEFF
var description
var name
class water (beadtype=1, userid=None, USER=forcefield (FF object) with 0 parameters)

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

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

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

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

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

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

Ancestors

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

Class variables

var description
var name
var userid
var version