Module group
Expand source code
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
__project__ = "Pizza3"
__author__ = "Olivier Vitrac, Han Chen"
__copyright__ = "Copyright 2023"
__credits__ = ["Olivier Vitrac","Han Chen"]
__license__ = "GPLv3"
__maintainer__ = "Olivier Vitrac"
__email__ = "olivier.vitrac@agroparistech.fr"
__version__ = "0.9999"
"""
===============================================
pizza.group Class Manual
===============================================
## Introduction
The `group` class provides a Pythonic interface to manage groups of atoms in LAMMPS simulations through the Pizza.py toolkit. In LAMMPS, groups are fundamental for applying operations to subsets of atoms, such as assigning force fields, computing properties, or setting up constraints. The `group` class allows users to create, manipulate, and combine groups of atoms programmatically, mirroring the functionality of LAMMPS group commands within a Python environment.
Unlike the `pizza.region` class, which allows for dynamic geometrical definitions, the `group` class operates statically. This choice is due to the nature of atom groups in LAMMPS, which are snapshots of atom collections at a specific time, rather than dynamic entities that change over time.
## Overview
Groups are stored and managed via a collection of operations. Each operation represents a LAMMPS group command or a combination of groups using algebraic operations. The `group` class facilitates both the creation of basic groups and the combination of existing groups using union, intersection, and subtraction.
The class supports both high-level methods, which correspond directly to LAMMPS commands, and low-level methods for internal management of group operations.
---
## High-Level Methods
These methods correspond directly to LAMMPS group commands and allow for the creation and manipulation of groups based on various criteria.
### `region(group_name, regionID)`
- **Description**: Defines a group of atoms based on a specified region ID.
- **Usage**: `G.region('group_name', 'regionID')`
- **Example**:
- In LAMMPS:
```lammps
region myRegion block 0 10 0 10 0 10 units box
group myGroup region myRegion
```
- In Python:
```python
G = group()
G.region('myGroup', 'myRegion')
```
### `type(group_name, type_values)`
- **Description**: Selects atoms by their type and assigns them to a group.
- **Usage**: `G.type('group_name', type_values)`
- **Example**:
- In LAMMPS:
```lammps
group myGroup type 1 2
```
- In Python:
```python
G = group()
G.type('myGroup', [1, 2])
```
### `id(group_name, id_values)`
- **Description**: Selects atoms by their IDs and assigns them to a group.
- **Usage**: `G.id('group_name', id_values)`
- **Example**:
- In LAMMPS:
```lammps
group myGroup id 1 2 3
```
- In Python:
```python
G = group()
G.id('myGroup', [1, 2, 3])
```
### `variable(group_name, variable_name, expression, style="atom")`
- **Description**: Defines a group based on an atom-style variable expression.
- **Usage**: `G.variable('group_name', 'variable_name', 'expression', style='atom')`
- **Example**:
- In LAMMPS:
```lammps
variable myVar atom "x > 5"
group myGroup variable myVar
```
- In Python:
```python
G = group()
G.variable('myGroup', 'myVar', 'x > 5')
```
### `create(group_name)`
- **Description**: Creates a new empty group or clears an existing group.
- **Usage**: `G.create('group_name')`
- **Example**:
- In LAMMPS:
```lammps
group myGroup clear
```
- In Python:
```python
G = group()
G.create('myGroup')
```
### Algebraic Operations
The `group` class supports algebraic operations between groups using the overloaded operators `+`, `-`, and `*` for union, subtraction, and intersection, respectively.
#### `union(group_name, *groups)`
- **Description**: Creates a new group by performing the union of specified groups.
- **Usage**: `G.union('new_group', *groups)`
- **Example**:
- In LAMMPS:
```lammps
group newGroup union group1 group2
```
- In Python:
```python
G.union('newGroup', 'group1', 'group2')
```
#### `intersect(group_name, *groups)`
- **Description**: Creates a new group by performing the intersection of specified groups.
- **Usage**: `G.intersect('new_group', *groups)`
- **Example**:
- In LAMMPS:
```lammps
group newGroup intersect group1 group2
```
- In Python:
```python
G.intersect('newGroup', 'group1', 'group2')
```
#### `subtract(group_name, *groups)`
- **Description**: Creates a new group by subtracting specified groups from the first group.
- **Usage**: `G.subtract('new_group', *groups)`
- **Example**:
- In LAMMPS:
```lammps
group newGroup subtract group1 group2
```
- In Python:
```python
G.subtract('newGroup', 'group1', 'group2')
```
### `evaluate(group_name, group_op)`
- **Description**: Evaluates complex group expressions involving algebraic operations and stores the result in a new group.
- **Usage**: `G.evaluate('new_group', group_op)`
- **Example**:
```python
G = group()
G.create('o1')
G.create('o2')
G.create('o3')
complex_op = G['o1'] + G['o2'] + G['o3']
G.evaluate('combinedGroup', complex_op)
Created on Wed Aug 16 16:25:19 2023
@author: olivi
"""
# Revision history
# 2023-08-17 RC with documentation, the script method is not implemented yet (to be done via G.code())
# 2023-08-18 consolidation of the GroupOperation mechanism (please still alpha version)
# 2023-09-01 the class GroupOperation has been removed and the collapse is carried within Operation to avoid unecessary complications [major release]
# 2024-10-04 code optimization
# 2024-10-07 implementation of dscript, script, pipescript
# 2024-10-08 code finalization (flexible initialization), updated documentation
# 2024-10-11 advanced indexing and scripting
# 2024-10-12 add copy and deepcopy features
# 2024-11-29 add groupobjects, groupcollection
# 2024-12-01 standarize scripting features, automatically call script/pscript methods
# %% Dependencies
# pizza.group is independent of region and can performs only static operations.
# It is designed to be compatible with region via G.script()
from typing import List, Union, Optional, Tuple
import hashlib, random, string, copy
from pizza.script import span, pipescript
from pizza.dscript import dscript
__all__ = ['Operation', 'dscript', 'format_table', 'generate_random_name', 'group', 'groupcollection', 'groupobject', 'pipescript', 'span', 'truncate_text']
# %% Private functions
# Helper function that generates a random string of a specified length using uppercase and lowercase letters.
def generate_random_name(length=8):
letters = string.ascii_letters # 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
return ''.join(random.choice(letters) for _ in range(length))
# Helper function that truncates text
def truncate_text(txt, maxwidth):
"""
Truncates the input text to fit within the specified maximum width.
If the text is longer than `maxwidth`, it is shortened by keeping
the beginning and trailing parts, separated by " [...] ".
### Parameters:
txt (str): The text to truncate.
maxwidth (int): The maximum allowed width for the text.
### Returns:
str: The truncated text with " [...] " in the middle if truncation occurs.
### Example:
>>> truncate_text("This is a long string that needs truncation.", 20)
'This is [...] truncation.'
"""
# If the text fits within maxwidth, return it as is
if len(txt) <= maxwidth:
return txt
# Calculate the part lengths for beginning and end
ellipsis = " [...] "
split_length = (maxwidth - len(ellipsis)) // 2
beginning = txt[:split_length]
trailing = txt[-split_length:]
return f"{beginning}{ellipsis}{trailing}"
# Helper function that generate a pretty table
def format_table(
headers: List[str],
rows: List[List[str]],
col_max_widths: List[int],
align: Union[str, List[str]] = "C",
) -> str:
"""
Formats a table with given headers and rows, truncating text based on maximum column widths
and aligning content based on the specified alignment.
### Parameters:
headers (List[str]): List of column headers.
rows (List[List[str]]): List of rows, where each row is a list of column values.
col_max_widths (List[int]): List of maximum widths for each column.
align (str or List[str], optional): Alignment for each column. Can be a single string
(applies to all columns) or a list of strings (one for each column).
Options are:
- "C" or "center" for centered alignment (default).
- "L" or "left" for left alignment.
- "R" or "right" for right alignment.
Case insensitive.
### Returns:
str: A formatted table as a string.
### Raises:
ValueError: If `align` is a list but its length does not match the number of columns.
### Example:
>>> headers = ["Idx", "Name", "Value"]
>>> rows = [[1, "Example", "12345"], [2, "LongerName", "67890"]]
>>> col_max_widths = [5, 10, 8]
>>> align = ["R", "C", "L"]
>>> print(format_table(headers, rows, col_max_widths, align))
Idx | Name | Value
----- | ---------- | --------
1 | Example | 12345
2 | LongerName | 67890
"""
# Normalize alignment input
if isinstance(align, str):
align = [align.upper()] * len(headers)
elif isinstance(align, list):
align = [a.upper() for a in align]
if len(align) != len(headers):
raise ValueError("The length of `align` must match the number of columns.")
else:
raise TypeError("`align` must be a string or a list of strings.")
# Determine actual column widths based on headers and max widths
col_widths = [
min(max(len(header), *(len(str(row[i])) for row in rows)), max_w)
for i, header in enumerate(headers)
for max_w in [col_max_widths[i]]
]
# Prepare alignment formatting
alignments = []
for a in align:
if a in ("C", "CENTER"):
alignments.append("^")
elif a in ("L", "LEFT"):
alignments.append("<")
elif a in ("R", "RIGHT"):
alignments.append(">")
else:
raise ValueError(f"Invalid alignment value: {a}")
# Prepare header line
header_line = " | ".join(
f"{headers[i]:{alignments[i]}{col_widths[i]}}" for i in range(len(headers))
)
# Prepare separator
separator = " | ".join("-" * col_widths[i] for i in range(len(col_widths)))
# Prepare rows
formatted_rows = []
for row in rows:
formatted_row = [
f"{str(row[i]):{alignments[i]}{col_widths[i]}}" for i in range(len(row))
]
formatted_rows.append(" | ".join(formatted_row))
# Combine all parts
table = [header_line, separator] + formatted_rows + [separator]
return "\n".join(table)
# %% Low-level classes groupobjects, groupcollection
# groupobject class for groups defined based on a collection of groupobjects
class groupobject:
"""
Represents an object with a bead type, associated groups, and an optional mass.
### Attributes:
beadtype (int or float): The bead type identifier. Must be an integer or a real scalar.
group (List[str]): List of group names the object belongs to.
mass (Optional[float]): The mass of the bead. Must be a real scalar or None.
name (Optional[str]): Name of the object
### Examples:
>>> o1 = groupobject(beadtype=1, group=["all", "A"], mass=1.0)
>>> o2 = groupobject(beadtype=2, group=["all", "B", "C"])
>>> o3 = groupobject(beadtype=3, group="C", mass=2.5)
"""
def __init__(self, beadtype: Union[int, float], group: Union[str, List[str], Tuple[str, ...]], \
mass: Optional[Union[int, float]] = None, name: Optional[str]=None, ):
"""
Initializes a new instance of the groupobject class.
### Parameters:
beadtype (int or float): The bead type identifier.
group (str, list of str, or tuple of str): Group names the object belongs to.
mass (float or int, optional): The mass of the bead.
### Raises:
TypeError: If `beadtype` is not an int or float.
TypeError: If `group` is not a str, list of str, or tuple of str.
TypeError: If `mass` is not a float, int, or None.
"""
# Validate beadtype
if not isinstance(beadtype, (int, float)):
raise TypeError(f"'beadtype' must be an integer or float, got {type(beadtype).__name__} instead.")
self.beadtype = beadtype
# Validate group
if isinstance(group, str):
self.group = [group]
elif isinstance(group, (list, tuple)):
if not all(isinstance(g, str) for g in group):
raise TypeError("All elements in 'group' must be strings.")
self.group = list(group)
else:
raise TypeError(f"'group' must be a string, list of strings, or tuple of strings, got {type(group).__name__} instead.")
# Validate mass
if mass is not None and not isinstance(mass, (int, float)):
raise TypeError(f"'mass' must be a real scalar (int or float) or None, got {type(mass).__name__} instead.")
self.mass = mass
# Validate name
self.name = f"beadtype={beadtype}" if name is None else name
def __str__(self) -> str:
"""
Returns a readable string representation of the groupobject.
"""
return f"groupobject | type={self.beadtype} | name={self.group} | mass={self.mass}"
def __repr__(self) -> str:
"""
Returns an unambiguous string representation of the groupobject.
### Returns:
str: String representation including beadtype, group, and mass (if not None).
"""
return f"groupobject(beadtype={self.beadtype}, group={span(self.group,',','[',']')}, name={self.name}, mass={self.mass})"
def __add__(self, other: Union['groupobject', 'groupcollection']) -> 'groupcollection':
"""
Adds this groupobject to another groupobject or a groupcollection.
### Parameters:
other (groupobject or groupcollection): The object to add.
### Returns:
groupcollection: A new groupcollection instance containing the combined objects.
### Raises:
TypeError: If `other` is neither a `groupobject` nor a `groupcollection`.
"""
if isinstance(other, groupobject):
return groupcollection([self, other])
elif isinstance(other, groupcollection):
return groupcollection([self] + other.collection)
else:
raise TypeError("Addition only supported between `groupobject` or `groupcollection` instances.")
def __radd__(self, other: Union['groupobject', 'groupcollection']) -> 'groupcollection':
"""
Adds this groupobject to another groupobject or a groupcollection from the right.
### Parameters:
other (groupobject or groupcollection): The object to add.
### Returns:
groupcollection: A new groupcollection instance containing the combined objects.
### Raises:
TypeError: If `other` is neither a `groupobject` nor a `groupcollection`.
"""
if isinstance(other, groupobject):
return groupcollection([other, self])
elif isinstance(other, groupcollection):
return groupcollection(other.collection + [self])
else:
raise TypeError("Addition only supported between `groupobject` or `groupcollection` instances.")
class groupcollection:
"""
Represents a collection of `groupobject` instances, typically formed by combining them.
### Attributes:
collection (List[groupobject]): The list of `groupobject` instances in the collection.
name (str): The name of the collection, generated automatically if not provided.
### Examples:
>>> o1 = groupobject(beadtype=1, group=["all", "A"], mass=1.0)
>>> o2 = groupobject(beadtype=2, group=["all", "B", "C"])
>>> G = groupcollection([o1, o2])
>>> print(G.name)
beadtype=1,2
"""
def __init__(self, collection: List['groupobject'], name: Optional[str] = None):
"""
Initializes a new instance of the groupcollection class.
### Parameters:
collection (List[groupobject]): A list of `groupobject` instances to include in the collection.
name (str, optional): The name of the collection. If not provided, a default name is generated.
### Raises:
ValueError: If the `collection` is empty or contains invalid elements.
"""
if not isinstance(collection, list) or not all(isinstance(obj, groupobject) for obj in collection):
raise ValueError("`collection` must be a list of `groupobject` instances.")
if not collection:
raise ValueError("`collection` cannot be empty.")
self.collection = collection
# Automatically assign a name if not provided
if name is None:
# Extract names or "beadtype=X" patterns
beadtype_names = [
obj.name if obj.name.startswith("beadtype=") else None for obj in collection
]
# If all objects have beadtype names, use "beadtype=1,2,3" pattern
if all(beadtype_names):
beadtypes = [obj.beadtype for obj in collection]
self.name = "beadtype={}".format(",".join(map(str, sorted(beadtypes))))
else:
# Fall back to concatenating names
self.name = ",".join(obj.name for obj in collection)
else:
self.name = name
def __add__(self, other: Union[groupobject, 'groupcollection']) -> 'groupcollection':
"""
Adds a `groupobject` or another `groupcollection` to this collection.
### Parameters:
other (groupobject or groupcollection): The object to add.
### Returns:
groupcollection: A new `groupcollection` instance containing the combined objects.
### Raises:
TypeError: If `other` is neither a `groupobject` nor a `groupcollection`.
"""
if isinstance(other, groupobject):
return groupcollection(self.collection + [other])
elif isinstance(other, groupcollection):
return groupcollection(self.collection + other.collection)
else:
raise TypeError("Addition only supported between `groupobject` or `groupcollection` instances.")
def __iadd__(self, other: Union[groupobject, List[groupobject], Tuple[groupobject, ...]]) -> 'groupcollection':
"""
In-place addition of a `groupobject` or a list/tuple of `groupobject` instances.
### Parameters:
other (groupobject or list/tuple of groupobject): The object(s) to add.
### Returns:
groupcollection: The updated `groupcollection` instance.
### Raises:
TypeError: If `other` is not a `groupobject` or a list/tuple of `groupobject` instances.
"""
if isinstance(other, groupobject):
self.collection.append(other)
elif isinstance(other, (list, tuple)):
for obj in other:
if not isinstance(obj, groupobject):
raise TypeError("All items to add must be `groupobject` instances.")
self.collection.extend(other)
else:
raise TypeError("In-place addition only supported with `groupobject` instances or lists/tuples of `groupobject` instances.")
return self
def __repr__(self) -> str:
"""
Returns a neatly formatted string representation of the groupcollection.
The representation includes a table of `beadtype`, `group` (max width 50 characters), and `mass`.
It also provides a summary of the total number of `groupobject` instances.
### Returns:
str: Formatted string representation of the collection.
"""
headers = ["Beadtype", 'Name', "Group", "Mass"]
col_max_widths = [10, 20, 30, 10]
# Prepare rows
rows = []
for obj in self.collection:
beadtype_str = str(obj.beadtype)
group_str = ", ".join(obj.group)
mass_str = f"{obj.mass}" if obj.mass is not None else "None"
rows.append([beadtype_str, obj.name, group_str, mass_str])
# Generate the table
table = format_table(headers, rows, col_max_widths)
# Add summary
summary = f"Total groupobjects: {len(self)}"
return f"{table}\n{summary}"
def __str__(self) -> str:
"""
Returns the same representation as `__repr__`.
### Returns:
str: Formatted string representation of the collection.
"""
return f"groupcollection including {len(self)} groupobjects"
def __len__(self) -> int:
"""
Returns the number of `groupobject` instances in the collection.
### Returns:
int: Number of objects in the collection.
"""
return len(self.collection)
def __getitem__(self, index: int) -> groupobject:
"""
Retrieves a `groupobject` by its index.
### Parameters:
index (int): The index of the `groupobject` to retrieve.
### Returns:
groupobject: The `groupobject` at the specified index.
### Raises:
IndexError: If the index is out of range.
"""
return self.collection[index]
def __iter__(self):
"""
Returns an iterator over the `groupobject` instances in the collection.
### Returns:
Iterator[groupobject]: An iterator over the collection.
"""
return iter(self.collection)
def append(self, obj: groupobject):
"""
Appends a `groupobject` to the collection.
### Parameters:
obj (groupobject): The object to append.
### Raises:
TypeError: If `obj` is not a `groupobject` instance.
"""
if not isinstance(obj, groupobject):
raise TypeError("Only `groupobject` instances can be appended.")
self.collection.append(obj)
def extend(self, objs: Union[List[groupobject], Tuple[groupobject, ...]]):
"""
Extends the collection with a list or tuple of `groupobject` instances.
### Parameters:
objs (list or tuple of groupobject): The objects to extend the collection with.
### Raises:
TypeError: If `objs` is not a list or tuple.
TypeError: If any item in `objs` is not a `groupobject` instance.
"""
if not isinstance(objs, (list, tuple)):
raise TypeError("`objs` must be a list or tuple of `groupobject` instances.")
for obj in objs:
if not isinstance(obj, groupobject):
raise TypeError("All items to extend must be `groupobject` instances.")
self.collection.extend(objs)
def remove(self, obj: groupobject):
"""
Removes a `groupobject` from the collection.
### Parameters:
obj (groupobject): The object to remove.
### Raises:
ValueError: If `obj` is not found in the collection.
"""
self.collection.remove(obj)
def clear(self):
"""
Clears all `groupobject` instances from the collection.
"""
self.collection.clear()
def mass(self, name: Optional[str] = None, default_mass: Optional[Union[str, int, float]] = "${mass}", verbose: Optional[bool] = True) -> 'dscript':
"""
Generates LAMMPS mass commands for each unique beadtype in the collection.
The method iterates through all `groupobject` instances in the collection,
collects unique beadtypes, and ensures that each beadtype has a consistent mass.
If a beadtype has `mass=None`, it assigns a default mass as specified by `default_mass`.
### Parameters:
name (str, optional): The name to assign to the resulting `dscript` object. Defaults to a generated name.
default_mass (str, optional): The default mass value to assign when a beadtype's mass is `None`.
Defaults to `"${mass}"`.
verbose (bool, optional): If `True`, includes a comment header in the output. Defaults to `True`.
### Returns:
dscript: A `dscript` object containing the mass commands for each beadtype, formatted as follows:
```
mass 1 1.0
mass 2 1.0
mass 3 2.5
```
The `collection` attribute of the `dscript` object holds the formatted mass commands as a single string.
### Raises:
ValueError: If a beadtype has inconsistent mass values across different `groupobject` instances.
### Example:
```python
# Create groupobject instances
o1 = groupobject(beadtype=1, group=["all", "A"], mass=1.0)
o2 = groupobject(beadtype=2, group=["all", "B", "C"])
o3 = groupobject(beadtype=3, group="C", mass=2.5)
# Initialize a groupcollection with the groupobjects
G = groupcollection([o1, o2, o3])
# Generate mass commands
M = G.mass()
print(M.do())
```
**Output:**
```
mass 1 1.0
mass 2 ${mass}
mass 3 2.5
```
"""
beadtype_mass = {}
for obj in self.collection:
bt = obj.beadtype
mass = obj.mass if obj.mass is not None else "${mass}"
if bt in beadtype_mass:
if beadtype_mass[bt] != mass:
raise ValueError(
f"Inconsistent masses for beadtype {bt}: {beadtype_mass[bt]} vs {mass}"
)
else:
beadtype_mass[bt] = mass
# Sort beadtypes for consistent ordering
sorted_beadtypes = sorted(beadtype_mass.keys())
# Generate mass commands
lines = [f"mass {bt} {beadtype_mass[bt]}" for bt in sorted_beadtypes]
# return a dscript object
idD = f"<dscript:group:{self.name}:mass>"
description = f"{idD} definitions for {len(self)} beads"
if verbose:
lines.insert(0, "# "+description)
D = dscript(name=idD if name is None else name, description=f"{idD} with {len(self)} beads")
D.collection = "\n".join(lines)
D.DEFINITIONS.mass = default_mass
return D
# %% Main classes Operation, group
# Class for binary and unary operators
class Operation:
"""
Represents a LAMMPS group operation, allowing algebraic manipulation and code generation.
### Overview
The `Operation` class models a group operation in LAMMPS (Large-scale Atomic/Molecular Massively
Parallel Simulator), encapsulating an operator, its operands, a name, and the corresponding LAMMPS code.
It supports algebraic operations between groups (union, intersection, subtraction) using the
overloaded operators '+', '-', and '*', enabling users to build complex group expressions
programmatically.
### Attributes
- **operator (str)**: The operator applied to the operands. It can be:
- A finalized LAMMPS operator: `'variable'`, `'byvariable'`, `'byregion'`, `'bytype'`, `'byid'`, `'create'`, `'clear'`,
`'union'`, `'intersect'`, `'subtract'`.
- An algebraic operator: `'+'`, `'-'`, `'*'`.
- An empty string or `None` for operations without an explicit operator.
- **operands (list)**: A list of operands for the operation. Operands can be:
- Instances of `Operation`, representing nested operations.
- Strings representing group names or other identifiers.
- Integers or lists (e.g., atom types or IDs).
- **name (str)**: The name of the operation, which can be manually set or auto-generated.
- If not provided, a unique hash-based name is generated using the `generate_hashname` method.
- **code (str)**: The LAMMPS code that this operation represents.
- This is generated when the operation is finalized and corresponds to the actual command(s)
that would be included in a LAMMPS input script.
- **criteria (dict, optional)**: Criteria used to define the operation.
- Useful for operations like `bytype`, `byregion`, `byid`, etc., where the criteria specify
the selection parameters.
### Methods
- `__init__(self, operator, operands, name="", code="", criteria=None)`:
Initializes a new `Operation` instance with the given parameters.
- **operator** (str): The operator applied to the operands.
- **operands** (list or single operand): The operands for the operation.
- **name** (str, optional): The name of the operation.
- **code** (str, optional): The LAMMPS code representing the operation.
- **criteria** (dict, optional): Criteria used to define the operation.
- `append(self, operand)`:
Appends a single operand to the operands list.
- `extend(self, operands)`:
Extends the operands list with multiple operands.
- `is_unary(self)`:
Checks if the `Operation` instance has exactly one operand.
- `is_empty(self)`:
Checks if the `Operation` instance has no operands.
- `generateID(self)`:
Generates an ID for the instance based on its operands or name.
Used internally for generating unique names.
- `generate_hashname(self, prefix="grp", ID=None)`:
Generates a hash-based name for the instance.
- **prefix** (str): A string prefix for the generated name.
- **ID** (str, optional): An optional ID to include in the hash. If `None`, it is generated from operands.
- `get_proper_operand(self)`:
Returns the appropriate operand representation depending on whether the operation is finalized.
- `_operate(self, other, operator)`:
Combines the operation with another `Operation` instance using the specified operator.
- **other** (`Operation`): The other operation to combine with.
- **operator** (str): The operator to apply ('+', '-', '*').
- `isfinalized(self)`:
Checks if the operation has been finalized.
- An operation is considered finalized if its operator is not one of the algebraic operators '+', '-', '*'.
- `__add__(self, other)`:
Overloads the '+' operator to support union of two `Operation` instances.
- `__sub__(self, other)`:
Overloads the '-' operator to support subtraction between two `Operation` instances.
- `__mul__(self, other)`:
Overloads the '*' operator to support intersection between two `Operation` instances.
- `__repr__(self)`:
Returns a detailed string representation of the operation, useful for debugging.
- `__str__(self)`:
Returns a concise string representation of the operation.
- `script(self)`:
Generates the LAMMPS code for this operation using `dscript` and `script` classes.
- Returns a `script` object containing the LAMMPS code.
### Operator Overloading
The `Operation` class overloads the following operators to enable algebraic manipulation:
- **Addition (`+`)**:
- Combines two operations using union.
- Example: `op3 = op1 + op2`
- **Subtraction (`-`)**:
- Subtracts one operation from another.
- Example: `op3 = op1 - op2`
- **Multiplication (`*`)**:
- Intersects two operations.
- Example: `op3 = op1 * op2`
### Usage Example
```python
# Create basic operations
op1 = Operation('bytype', [1], name='group1')
op2 = Operation('bytype', [2], name='group2')
# Combine operations using algebraic operators
combined_op = op1 + op2 # Union of group1 and group2
# Initialize group manager
G = group()
G.add_operation(op1)
G.add_operation(op2)
# Evaluate the combined operation and store the result
G.evaluate('combined_group', combined_op)
# Access the generated LAMMPS code
print(G.code())
# Output:
# group group1 type 1
# group group2 type 2
# group combined_group union group1 group2
```
### Notes
- **Lazy Evaluation**:
- Operations are combined and stored symbolically until they are evaluated.
- The actual LAMMPS code is generated when the operation is finalized (e.g., via the `evaluate` method in the `group` class).
- **Name Generation**:
- If an operation's name is not provided, it is auto-generated using a hash of its operator and operands.
- This ensures uniqueness and avoids naming conflicts.
- **Criteria Attribute**:
- The `criteria` attribute allows storing the parameters used to define operations.
- Useful for debugging, documentation, or reconstructing operations.
- **Finalization**:
- An operation is finalized when it represents an actual LAMMPS command and has generated code.
- Finalized operations are not further combined; instead, their names are used in subsequent operations.
- **Integration with `dscript` and `script`**:
- The `script` method generates the LAMMPS code for the operation using `dscript` and `script` classes.
- Facilitates integration with script management tools and advanced scripting workflows.
- **Error Handling**:
- Methods include error checks to ensure operands are valid and operations are correctly formed.
- Raises informative exceptions for invalid operations or unsupported types.
### Integration with Group Class
The `Operation` class is designed to work closely with the `group` class, which manages a collection of operations and handles evaluation and code generation.
- **Adding Operations**:
- Use `group.add_operation(operation)` to add an `Operation` instance to a `group`.
- **Evaluating Operations**:
- Use `group.evaluate(group_name, operation)` to finalize an operation and generate LAMMPS code.
- **Accessing Operations**:
- Operations can be accessed via the `group` instance using indexing, attribute access, or methods.
### Additional Information
- **String Representations**:
- `__str__`: Provides a concise representation, useful for quick inspection.
- `__repr__`: Provides a detailed representation, useful for debugging.
- **Example of `__str__` Output**:
- For an operation representing `group1 + group2`:
```python
print(str(combined_op))
# Output: <combined_group=[group1+group2]>
```
- **Example of `__repr__` Output**:
```python
print(repr(combined_op))
# Output:
# Operation Details:
# - Name: combined_group
# - Operator: +
# - Operands: group1, group2
# - LAMMPS Code:
```
### Examples
**Defining Operations Based on Atom Types**
```python
op1 = Operation('bytype', [1], name='type1')
op2 = Operation('bytype', [2], name='type2')
G.add_operation(op1)
G.add_operation(op2)
```
**Combining and Evaluating Operations**
```python
combined_op = op1 + op2 # Union of 'type1' and 'type2'
G.evaluate('all_types', combined_op)
```
**Accessing Generated Code**
```python
print(G.code())
# Output:
# group type1 type 1
# group type2 type 2
# group all_types union type1 type2
```
**Using Criteria Attribute**
```python
# Access criteria used to define an operation
print(op1.criteria)
# Output:
# {'type': [1]}
```
### Important Notes
- **Algebraic Operators vs. Finalized Operators**:
- Algebraic operators ('+', '-', '*') are used for symbolic manipulation.
- Finalized operators represent actual LAMMPS commands.
- **Operator Precedence**:
- When combining operations, consider the order of operations.
- Use parentheses to ensure the desired evaluation order.
- **Integration with Scripting Tools**:
- The `script` method allows operations to be integrated into larger scripting workflows.
- Supports advanced features like variable substitution and conditional execution.
### Conclusion
The `Operation` class provides a powerful and flexible way to model and manipulate group operations in LAMMPS simulations. By supporting algebraic operations and integrating with script management tools, it enables users to build complex group definitions programmatically, enhancing productivity and reducing errors in simulation setup.
"""
def __init__(self, operator, operands, name="", code="", criteria=None):
"""Initializes a new Operation instance with given parameters."""
self.operator = operator
self.operands = operands if isinstance(operands, list) else [operands]
if not name:
self.name = self.generate_hashname()
else:
self.name = name
self.code = code
self.criteria = criteria # Store criteria for this operation
def append(self, operand):
"""Appends a single operand to the operands list of the Operation instance."""
self.operands.append(operand)
def extend(self, operands):
"""Extends the operands list of the Operation instance with multiple operands."""
self.operands.extend(operands)
def is_unary(self):
"""Checks if the Operation instance has exactly one operand."""
return len(self.operands) == 1
def is_empty(self):
"""Checks if the Operation instance has no operands."""
return len(self.operands) == 0
def generateID(self):
"""Generates an ID for the Operation instance based on its operands or name."""
# if self.operands:
# return "".join([str(op) for op in self.operands])
# else:
# return self.name
operand_ids = ''.join(op.name if isinstance(op, Operation) else str(op) for op in self.operands)
return operand_ids
def generate_hashname(self,prefix="grp",ID=None):
"""Generates an ID for the Operation instance based on its operands or name."""
# if ID is None: ID = self.generateID()
# s = self.operator+ID
# return prefix+hashlib.sha256(s.encode()).hexdigest()[:6]
if ID is None:
ID = self.generateID()
s = self.operator + ID
return prefix + hashlib.sha256(s.encode()).hexdigest()[:6]
def __repr__(self):
""" detailed representation """
operand_str = ', '.join(str(op) for op in self.operands)
return (
f"Operation Details:\n"
f" - Name: {self.name}\n"
f" - Operator: {self.operator}\n"
f" - Operands: {operand_str}\n"
f" - LAMMPS Code: {self.code}"
)
def __str__(self):
""" string representation """
operands = [str(op) for op in self.operands]
operand_str = ', '.join(operands)
if self.is_empty():
return f"<{self.operator} {self.name}>"
elif self.is_unary():
return f"<{self.name}={self.operator}({operand_str})>"
else: # Polynary
operator_symbol_mapping = {
'union': '+',
'intersect': '*',
'subtract': '-'
}
operator_symbol = operator_symbol_mapping.get(self.operator, self.operator)
formatted_operands = operator_symbol.join(operands)
return f"<{self.name}=[{formatted_operands}]>"
def isfinalized(self):
"""
Checks whether the Operation instance is finalized.
Returns:
- bool: True if the Operation is finalized, otherwise False.
Functionality:
- An Operation is considered finalized if its operator is not
one of the algebraic operators '+', '-', '*'.
"""
return self.operator not in ('+', '-', '*')
def __add__(self, other):
""" overload + as union """
return self._operate(other,'+')
def __sub__(self, other):
""" overload - as subtract """
return self._operate(other,'-')
def __mul__(self, other):
""" overload * as intersect """
return self._operate(other,'*')
def get_proper_operand(self):
# """
# Returns the proper operand depending on whether the operation is finalized.
# """
# return span(self.operands) if not self.isfinalized() else self.name
"""
Returns the proper operand depending on whether the operation is finalized.
"""
if self.isfinalized():
return self.name
else:
# Collect operand names
operand_names = [
op.name if isinstance(op, Operation) else str(op)
for op in self.operands
]
return span(operand_names)
def _operate(self, other, operator):
"""
Implements algebraic operations between self and other Operation instances.
Parameters:
- other (Operation): The other Operation instance to be operated with.
- operator (str): The operation to be performed. Supported values are '+', '-', '*'.
Returns:
- Operation: A new Operation instance reflecting the result of the operation.
"""
# Ensure other is also an instance of Operation
if not isinstance(other, Operation):
raise TypeError(f"Unsupported type: {type(other)}")
# Map operator to prefix
prefix_map = {'+': 'add', '-': 'sub', '*': 'mul'}
prefix = prefix_map.get(operator, 'op')
# Prepare operands list
operands = []
# Handle self
if self.operator == operator and not self.isfinalized():
operands.extend(self.operands)
else:
operands.append(self)
# Handle other
if other.operator == operator and not other.isfinalized():
operands.extend(other.operands)
else:
operands.append(other)
# Generate a new name
name = self.generate_hashname(prefix=prefix, ID=self.generateID() + other.generateID())
# Create a new Operation
new_op = Operation(operator, operands, name=name)
return new_op
def script(self):
"""
Generate the LAMMPS code using the dscript and script classes.
Returns:
- script onject: The LAMMPS code generated by this operation.
"""
# Create a dscript object to store the operation code
dscript_obj = dscript()
# Use a dummy key for the dscript template
dscript_obj["dummy"] = self.code
# Convert the dscript object to a script object
script_obj = dscript_obj.script()
return script_obj
class group:
"""
A class for managing LAMMPS group operations and generating LAMMPS scripts.
### Overview
The `group` class provides an object-oriented interface to define and manage
groups of atoms in LAMMPS simulations. Groups in LAMMPS are collections of
atoms that can be manipulated together, allowing users to apply fixes,
compute properties, or perform operations on specific subsets of atoms.
This class allows you to create, combine, and manipulate groups using
algebraic operations (union, intersection, subtraction), and to generate
the corresponding LAMMPS commands. It also provides methods to output the
group commands as scripts, which can be integrated into LAMMPS input files.
### Key Features
- **Create and Manage Groups**: Define new groups based on atom types,
regions, IDs, or variables.
- **Flexible Group Creation**: Create multiple groups at once and define groups
using concise criteria through the `add_group_criteria` method.
- **Algebraic Operations**: Combine groups using union (`+`), intersection (`*`),
and subtraction (`-`) operations.
- **Subindexing with Callable Syntax**: Retrieve multiple group operations
by calling the `group` instance with names or indices.
- **Script Generation**: Generate LAMMPS script lines for group definitions,
which can be output as scripts or pipelines.
- **Dynamic Evaluation**: Evaluate complex group expressions and store the
resulting operations.
- **Integration with Scripting Tools**: Convert group operations into
`dscript` or `pipescript` objects for advanced script management.
### LAMMPS Context
In LAMMPS (Large-scale Atomic/Molecular Massively Parallel Simulator),
groups are fundamental for specifying subsets of atoms for applying
operations like forces, fixes, and computes. The `group` command in LAMMPS
allows users to define groups based on various criteria such as atom IDs,
types, regions, and variables.
This `group` class abstracts the complexity of managing group definitions
and operations, providing a high-level interface to define and manipulate
groups programmatically.
### Usage Examples
**Creating Multiple Groups at Once**
```python
# Create groups 'o1', 'o2', 'o3', 'o4' upon instantiation
G = group(group_names=['o1', 'o2', 'o3', 'o4'])
```
**Defining Groups Based on Criteria**
```python
G = group()
G.add_group_criteria('lower', type=[1])
G.add_group_criteria('central', region='central_cyl')
G.add_group_criteria('new_group', create=True)
G.add_group_criteria('upper', clear=True)
G.add_group_criteria('subtract_group', subtract=['group1', 'group2'])
```
**Defining a Group Based on a Variable**
```python
G = group()
G.variable('myVar', 'x > 5') # Assign a variable
G.byvariable('myGroup', 'myVar') # Define group based on the variable
```
**Using `add_group_criteria` with Variable**
```python
G.add_group_criteria('myGroup', variable={'name': 'myVar', 'expression': 'x > 5'})
```
**Defining Groups Using a Dictionary**
```python
group_definitions = {
'group1': {'type': [1, 2]},
'group2': {'region': 'my_region'},
'group3': {'variable': {'name': 'var1', 'expression': 'x > 5'}},
'group4': {'union': ['group1', 'group2']},
}
G.add_group_criteria(group_definitions)
```
**Creating a Group from a Collection of `groupobject` Instances**
```python
# Import the classes
# from groupobject import groupobject
# from group import group
# Create groupobject instances
o1 = groupobject(beadtype=1, group=["all", "A"], mass=1.0)
o2 = groupobject(beadtype=2, group=["all", "B", "C"])
o3 = groupobject(beadtype=3, group="C", mass=2.5)
# Create a group instance with the collection
G = group(name="mycollection", collection=[o1, o2, o3])
# or
G = group(name=None, collection=[o1, o2, o3]) # Name will be generated as "1+2+3"
# Generate the LAMMPS script
script_content = G.code()
print(script_content)
```
**Expected Output:**
```
group all type 1 2 3
group A type 1
group B type 2
group C type 2 3
```
**Accessing Groups and Performing Operations**
```python
# Access groups via attribute-style or item-style access
op1 = G.group1
op2 = G['group2']
# Combine groups using algebraic operations
complex_op = op1 + op2 - G.group3
# Evaluate the operation and store the result
G.evaluate('combined_group', complex_op)
```
**Subindexing with Callable Syntax**
```python
# Retrieve multiple operations by names or indices
subG = G('group1', 2, 'group3') # Retrieves 'group1', third operation, and 'group3'
# Display the names of operations in subG
operation_names = [op.name for op in subG._operations]
print(operation_names) # Output: ['group1', 'group3', 'group3']
# Generate the LAMMPS script for the subgroup
script_content = subG.code()
print(script_content)
```
**Generating LAMMPS Script**
```python
script_content = G.code()
print(script_content)
```
### Class Methods
#### Initialization and Setup
- `__init__(self, name=None, groups=None, group_names=None, printflag=False, verbose=True)`: Initializes a new `group` instance.
- `name` (str): Optional name for the group instance.
- `groups` (dict): Dictionary of group definitions to create upon initialization.
- `group_names` (list): List of group names to create empty groups upon initialization.
- `collection` (list or tuple of groupobject, optional): Collection of groupobject instances.
- `printflag` (bool): If True, enables printing of script generation.
- `verbose` (bool): If True, enables verbose output.
- `create_groups(self, *group_names)`: Creates multiple new groups with the given names.
- `add_group_criteria(self, *args, **kwargs)`: Adds group(s) based on criteria.
- Supports two usages:
- `add_group_criteria(group_name, **criteria)`: Adds a single group.
- `add_group_criteria(group_definitions)`: Adds multiple groups from a dictionary.
#### Group Creation Methods
- `create(self, group_name)`: Creates a new empty group with the given name.
- `bytype(self, group_name, type_values)`: Defines a group based on atom types.
- `byid(self, group_name, id_values)`: Defines a group based on atom IDs.
- `byregion(self, group_name, region_name)`: Defines a group based on a region.
- `variable(self, variable_name, expression, style="atom")`: Assigns an expression to a LAMMPS variable.
- `byvariable(self, group_name, variable_name)`: Defines a group based on a variable.
- `clear(self, group_name)`: Clears an existing group.
#### Algebraic Operations
- `union(self, group_name, *groups)`: Performs a union of the specified groups.
- `intersect(self, group_name, *groups)`: Performs an intersection of the specified groups.
- `subtract(self, group_name, *groups)`: Subtracts groups from the first group.
- `evaluate(self, group_name, group_op)`: Evaluates a group operation and stores the result.
#### Access and Manipulation
- `__getitem__(self, key)`: Allows accessing operations by name or index.
- `__getattr__(self, operation_name)`: Enables attribute-style access to operations.
- `__call__(self, *keys)`: Returns a new `group` instance containing specified operations.
- **Parameters**:
- `*keys` (str or int): One or more names or indices of operations to retrieve.
- **Returns**:
- `group`: A new `group` instance containing the specified operations.
- **Example**:
```python
subG = G('group1', 1, 'group3') # Retrieves 'group1', second operation, and 'group3'
```
- `list(self)`: Returns a list of all operation names.
- `find(self, name)`: Finds the index of an operation based on its name.
- `disp(self, name)`: Displays the content of an operation.
- `delete(self, name)`: Deletes an operation by name.
- `copy(self, source_name, new_name)`: Copies an existing operation to a new name.
- `rename(self, old_name, new_name)`: Renames an existing operation.
- `reindex(self, name, new_idx)`: Changes the index of an operation.
#### Script Generation
- `code(self)`: Returns the generated LAMMPS commands as a string.
- `dscript(self, name=None)`: Generates a `dscript` object containing the group's commands.
- `script(self, name=None)`: Generates a script object containing the group's commands.
- `pipescript(self)`: Generates a `pipescript` object containing each group's command as a separate script in a pipeline.
### Operator Overloading
- **Addition (`+`)**: Union of groups.
- **Subtraction (`-`)**: Subtraction of groups.
- **Multiplication (`*`)**: Intersection of groups.
These operators are overloaded in the `Operation` class and can be used to combine group operations.
### Internal Functionality
The class maintains a list `_operations`, which stores all the group operations
defined. Each operation is an instance of the `Operation` class, which represents
a LAMMPS group command along with its operands and operator.
### Subindexing with Callable Syntax
The `group` class allows you to retrieve multiple operations by calling the
instance with names or indices:
- `__call__(self, *keys)`: Returns a new `group` instance containing the specified operations.
- **Parameters**:
- `*keys` (str or int): One or more names or indices of operations to retrieve.
- **Returns**:
- `group`: A new `group` instance containing the specified operations.
- **Example**:
```python
subG = G('group1', 1, 'group3') # Retrieves 'group1', second operation, and 'group3'
```
- **Notes**:
- Indices are 0-based integers.
- Duplicate operations are avoided in the new `group` instance.
### Integration with Scripts
The `group` class integrates with `dscript` and `pipescript` classes to allow
advanced script management:
- **`dscript`**: A dynamic script management class that handles script lines
with variable substitution and conditional execution.
- **`pipescript`**: Manages a pipeline of scripts, allowing sequential execution
and advanced variable space management.
### Important Notes
- **Operator Overloading**: The class overloads the `+`, `-`, and `*` operators
for the `Operation` class to perform union, subtraction, and intersection
respectively.
- **Flexible Group Creation**: Groups can be created upon instantiation or added later using concise methods.
- **Variable Assignment and Group Definition**: When defining groups based on variables, variable assignment and group creation are handled separately.
- **Subindexing with `__call__`**: The `__call__` method allows you to retrieve multiple operations and create subgroups.
- **Error Handling**: The class includes robust error checking and provides informative error messages.
- **Lazy Evaluation**: Group operations are stored and only evaluated when
the `evaluate` method is called.
- **Name Management**: The class ensures that group names are unique within
the instance to prevent conflicts.
### Conclusion
The `group` class simplifies the management of groups in LAMMPS simulations,
allowing for clear and maintainable code when dealing with complex group
operations. By providing high-level abstractions, operator overloading,
subindexing capabilities, and integration with script management tools,
it enhances productivity and reduces the potential for errors in simulation setup.
"""
def __init__(self, name=None, groups=None, group_names=None, collection=None, printflag=False, verbose=True, verbosity=None):
"""
Initializes a new instance of the group class.
### Parameters:
name (str, optional): Name for the group instance. If `None` or empty and `collection` is provided,
generates a name based on beadtypes (e.g., "1+2+3").
groups (dict, optional): Dictionary of group definitions to create upon initialization.
group_names (list or tuple, optional): List of group names to create empty groups upon initialization.
collection (list, tuple, or groupcollection, optional):
- If a list or tuple, it should contain `groupobject` instances.
- If a `groupcollection` object, it will extract the `groupobject` instances from it.
printflag (bool, optional): If `True`, enables printing of script generation.
verbose (bool, optional): If `True`, enables verbose output.
### Raises:
TypeError:
- If `groups` is not a dictionary.
- If `group_names` is not a list or tuple.
- If `collection` is not a list, tuple, or `groupcollection` object.
- If any item in `collection` (when it's a list or tuple) is not a `groupobject` instance.
"""
self._in_construction = True # Indicate that the object is under construction
# Handle 'name' parameter
if not name:
name = generate_random_name()
self._name = name
# Initialize other attributes
self._operations = []
self.printflag = printflag
self.verbose = verbose if verbosity is None else verbosity>0
self.verbosity = verbosity
self._in_construction = False # Set the flag to indicate construction is finished
# Handle 'groups' parameter
if groups:
if not isinstance(groups, dict):
raise TypeError("Parameter 'groups' must be a dictionary.")
self.add_group_criteria(groups)
# Handle 'group_names' parameter
if group_names:
if not isinstance(group_names, (list, tuple)):
raise TypeError("Parameter 'group_names' must be a list or tuple of group names.")
self.create_groups(*group_names)
# Handle 'collection' parameter
if collection:
if isinstance(collection, groupcollection):
# Extract the list of groupobject instances from the groupcollection
collection = collection.collection
elif not isinstance(collection, (list, tuple)):
raise TypeError("Parameter 'collection' must be a list, tuple, or `groupcollection` object.")
# If collection is a list or tuple, validate its items
if isinstance(collection, (list, tuple)):
for obj in collection:
if not isinstance(obj, groupobject):
raise TypeError("All items in 'collection' must be `groupobject` instances.")
self.generate_group_definitions_from_collection(collection)
def create_groups(self, *group_names):
for group_name in group_names:
if not isinstance(group_name, str):
raise TypeError(f"Group name must be a string, got {type(group_name)}")
self.create(group_name)
def __str__(self):
return f'Group "{self._name}" with {len(self._operations)} operations\n'
def format_cell_content(self, content, max_width):
content = str(content) if content is not None else ''
if len(content) > max_width:
start = content[: (max_width - 5) // 2]
end = content[-((max_width - 5) // 2):]
content = f"{start} ... {end}"
return content
def __repr__(self):
"""
Returns a neatly formatted table representation of the group's operations.
Each row represents an operation in the group, displaying its index, name,
operator, and operands. The table adjusts column widths dynamically and
truncates content based on maximum column widths.
### Returns:
str: A formatted string representation of the group operations.
"""
# Define headers for the table
headers = ["Idx", "Name", "Operator", "Operands"]
col_max_widths = [5, 20, 20, 40]
align = ["R", "C", "C", "L"]
# Prepare rows by iterating over the operations
rows = []
for idx, op in enumerate(self._operations):
rows.append([
str(idx), # Index
str(op.name), # Name
str(op.operator), # Operator
str(span(op.operands)), # Operands
])
# Use the helper to format the table
table = format_table(headers, rows, col_max_widths, align)
# Append the string representation of the group itself
return f"{table}\n\n{str(self)}"
def __len__(self):
""" return the number of stored operations """
return len(self._operations)
def list(self):
""" return the list of all operations """
return [op.name for op in self._operations]
def code(self):
"""
Joins the `code` attributes of all stored `operation` objects with '\n'.
"""
return '\n'.join([op.code for op in self._operations])
def find(self, name):
"""Returns the index of an operation based on its name."""
if '_operations' in self.__dict__:
for i, op in enumerate(self._operations):
if op.name == name:
return i
return None
def disp(self, name):
""" display the content of an operation """
idx = self.find(name)
if idx is not None:
return self._operations[idx].__repr__()
else:
return "Operation not found"
def clearall(self):
""" clear all operations """
self._operations = []
def delete(self, name):
"""
Deletes one or more stored operations based on their names.
Parameters:
-----------
name : str, list, or tuple
The name(s) of the operation(s) to delete. If a list or tuple is provided,
all specified operations will be deleted.
Usage:
------
G.delete('operation_name')
G.delete(['operation1', 'operation2'])
G.delete(('operation1', 'operation2'))
Raises:
-------
ValueError
If any of the specified operations are not found.
"""
# Handle a single string, list, or tuple
if isinstance(name, (list, tuple)):
not_found = []
for n in name:
idx = self.find(n)
if idx is not None:
del self._operations[idx]
else:
not_found.append(n)
# If any names were not found, raise an exception
if not_found:
raise ValueError(f"Operation(s) {', '.join(not_found)} not found.")
elif isinstance(name, str):
idx = self.find(name)
if idx is not None:
del self._operations[idx]
else:
raise ValueError(f"Operation {name} not found.")
else:
raise TypeError("The 'name' parameter must be a string, list, or tuple.")
def copy(self, source_name, new_name):
"""
Copies a stored operation to a new operation with a different name.
Parameters:
source_name: str
Name of the source operation to copy
new_name: str
Name of the new operation
Usage:
G.copy('source_operation', 'new_operation')
"""
idx = self.find(source_name)
if idx is not None:
copied_operation = self._operations[idx].clone()
copied_operation.name = new_name
self.add_operation(copied_operation)
else:
raise ValueError(f"Operation {source_name} not found.")
def rename(self, old_name, new_name):
"""
Rename a stored operation.
Parameters:
old_name: str
Current name of the operation
new_name: str
New name to assign to the operation
Usage:
G.rename('old_operation', 'new_operation')
"""
idx = self.find(old_name)
if idx is not None:
if new_name == old_name:
raise ValueError("The new name should be different from the previous one.")
elif new_name in self.list():
raise ValueError("Operation name must be unique.")
self._operations[idx].name = new_name
else:
raise ValueError(f"Operation '{old_name}' not found.")
def reindex(self, name, new_idx):
"""
Change the index of a stored operation.
Parameters:
name: str
Name of the operation to reindex
new_idx: int
New index for the operation
Usage:
G.reindex('operation_name', 2)
"""
idx = self.find(name)
if idx is not None and 0 <= new_idx < len(self._operations):
op = self._operations.pop(idx)
self._operations.insert(new_idx, op)
else:
raise ValueError(f"Operation '{name}' not found or new index {new_idx} out of range.")
# ------------- indexing and attribute overloading
def __getitem__(self, key):
"""
Enable shorthand for G.operations[G.find(operation_name)] using G[operation_name],
or accessing operation by index using G[index].
"""
if isinstance(key, str):
idx = self.find(key)
if idx is not None:
return self._operations[idx]
else:
raise KeyError(f"Operation '{key}' not found.")
elif isinstance(key, int):
if -len(self._operations) <= key < len(self._operations):
return self._operations[key]
else:
raise IndexError("Operation index out of range.")
else:
raise TypeError("Key must be an operation name (string) or index (integer).")
def __getattr__(self, name):
"""
Allows accessing operations via attribute-style notation.
If the attribute is one of the core attributes, returns it directly.
For other attributes, searches for an operation with a matching name
in the _operations list.
Parameters:
-----------
name : str
The name of the attribute or operation to access.
Returns:
--------
The value of the attribute if it's a core attribute, or the operation
associated with the specified name if found in _operations.
Raises:
-------
AttributeError
If the attribute or operation is not found.
"""
# Handle core attributes directly
if name in {'_name', '_operations', 'printflag', 'verbose'}:
# Use object.__getattribute__ to avoid recursion
return object.__getattribute__(self, name)
# Search for the operation in _operations
elif '_operations' in self.__dict__:
for op in self._operations:
if op.name == name:
return op
# If not found, raise an AttributeError
raise AttributeError(f"Attribute or operation '{name}' not found.")
def __setattr__(self, name, value):
"""
Allows deletion of an operation via 'G.operation_name = []' after construction.
During construction, attributes are set normally.
"""
if getattr(self, '_in_construction', True):
# During construction, set attributes normally
super().__setattr__(name, value)
else:
# After construction
if isinstance(value, list) and len(value) == 0:
# Handle deletion syntax
idx = self.find(name)
if idx is not None:
del self._operations[idx]
else:
raise AttributeError(f"Operation '{name}' not found for deletion.")
else:
# Set attribute normally
super().__setattr__(name, value)
def _get_subobject(self, key):
"""
Retrieves a subobject based on the provided key.
Parameters:
-----------
key : str or int
The key used to retrieve the subobject.
Returns:
--------
Operation
The operation corresponding to the key.
Raises:
-------
KeyError
If the key is not found.
IndexError
If the index is out of range.
TypeError
If the key is not a string or integer.
"""
if isinstance(key, str):
# If key is a string, treat it as a group name
for operation in self._operations:
if operation.name == key:
return operation
raise KeyError(f"No operation found with name '{key}'.")
elif isinstance(key, int):
# If key is an integer, treat it as an index (0-based)
if 0 <= key < len(self._operations):
return self._operations[key]
else:
raise IndexError(f"Index {key} is out of range.")
else:
raise TypeError("Key must be a string or integer.")
def __call__(self, *keys):
"""
Allows subindexing of the group object using callable syntax with multiple keys.
Parameters:
-----------
*keys : str or int
One or more keys used to retrieve subobjects or perform subindexing.
Returns:
--------
group
A new group instance containing the specified operations.
Example:
--------
subG = G('a', 1, 'c') # Retrieves operations 'a', second operation, and 'c'
"""
selected_operations = []
for key in keys:
operation = self._get_subobject(key)
selected_operations.append(operation)
# Create a new group instance with the selected operations
new_group = group(name=f"{self._name}_subgroup", printflag=self.printflag, verbose=self.verbose)
new_group._operations = selected_operations
return new_group
def operation_exists(self,operation_name):
"""
Returns true if "operation_name" exists
To be used by Operation, not by end-user, which should prefer find()
"""
return any(op.name == operation_name for op in self._operations)
def get_by_name(self,operation_name):
"""
Returns the operation matching "operation_name"
Usage: group.get_by_name("operation_name")
To be used by Operation, not by end-user, which should prefer getattr()
"""
for op in self._operations:
if op.name == operation_name:
return op
raise AttributeError(f"Operation with name '{operation_name}' not found.")
def add_operation(self,operation):
""" add an operation """
if operation.name in self.list():
raise ValueError(f"The operation '{operation.name}' already exists.")
else:
self._operations.append(operation)
# --------- LAMMPS methods
def variable(self, variable_name, expression, style="atom"):
"""
Assigns an expression to a LAMMPS variable.
Parameters:
- variable_name (str): The name of the variable to be assigned.
- expression (str): The expression to assign to the variable.
- style (str): The type of variable (default is "atom").
"""
if not isinstance(variable_name, str):
raise TypeError(f"Variable name must be a string, got {type(variable_name)}")
if not isinstance(expression, str):
raise TypeError(f"Expression must be a string, got {type(expression)}")
if not isinstance(style, str):
raise TypeError(f"Style must be a string, got {type(style)}")
lammps_code = f"variable {variable_name} {style} \"{expression}\""
op = Operation("variable", [variable_name, expression], code=lammps_code)
self.add_operation(op)
def byvariable(self, group_name, variable_name):
"""
Sets a group of atoms based on a variable.
Parameters:
- group_name: str, the name of the group.
- variable_name: str, the name of the variable to define the group.
"""
if not isinstance(group_name, str):
raise TypeError(f"Group name must be a string, got {type(group_name)}")
if not isinstance(variable_name, str):
raise TypeError(f"Variable name must be a string, got {type(variable_name)}")
lammps_code = f"group {group_name} variable {variable_name}"
op = Operation("byvariable", [variable_name], name=group_name, code=lammps_code)
self.add_operation(op)
def byregion(self, group_name, region_name):
"""
set a group of atoms based on a regionID
G.region(group_name,regionID)
"""
lammps_code = f"group {group_name} region {region_name}"
criteria = {"region": region_name}
op = Operation("byregion", [region_name], name=group_name, code=lammps_code, criteria=criteria)
self.add_operation(op)
def bytype(self, group_name, type_values):
"""
select atoms by type and store them in group
G.type(group_name,type_values)
"""
if not isinstance(type_values, (list, tuple)):
type_values = [type_values]
lammps_code = f"group {group_name} type {span(type_values)}"
criteria = {"type": type_values}
op = Operation("bytype", type_values, name=group_name, code=lammps_code, criteria=criteria)
self.add_operation(op)
def byid(self, group_name, id_values):
"""
select atoms by id and store them in group
G.id(group_name,id_values)
"""
if not isinstance(id_values, (list, tuple)):
id_values = [id_values]
lammps_code = f"group {group_name} id {span(id_values)}"
criteria = {"id": id_values}
op = Operation("byid", id_values, name=group_name, code=lammps_code, criteria=criteria)
self.add_operation(op)
def create(self, group_name):
"""
create group
G.create(group_name)
"""
lammps_code = f"group {group_name} clear"
criteria = {"clear": True}
op = Operation("create", [], name=group_name, code=lammps_code, criteria=criteria)
self.add_operation(op)
def clear(self, group_name):
"""
clear group
G.clear(group_name)
"""
lammps_code = f"group {group} clear"
criteria = {"clear": True}
op = Operation("clear", [], name=group_name, code=lammps_code, criteria=criteria)
self.add_operation(op)
def union(self,group_name, *groups):
"""
Union group1, group2, group3 and store the result in group_name.
Example usage:
group.union(group_name, group1, group2, group3,...)
"""
lammps_code = f"group {group_name} union {span(groups)}"
criteria = {"union": groups}
op = Operation("union", groups, name=group_name, code=lammps_code, criteria=criteria)
self.add_operation(op)
def intersect(self,group_name, *groups):
"""
Intersect group1, group2, group3 and store the result in group_name.
Example usage:
group.intersect(group_name, group1, group2, group3,...)
"""
lammps_code = f"group {group_name} intersect {span(groups)}"
criteria = {"intersect": groups}
op = Operation("intersect", groups, name=group_name, code=lammps_code, criteria=criteria)
self.add_operation(op)
def subtract(self,group_name, *groups):
"""
Subtract group2, group3 from group1 and store the result in group_name.
Example usage:
group.subtract(group_name, group1, group2, group3,...)
"""
lammps_code = f"group {group_name} subtract {span(groups)}"
criteria = {"subtract": groups}
op = Operation("subtract", groups, name=group_name, code=lammps_code, criteria=criteria)
self.add_operation(op)
def evaluate(self, group_name, group_op):
"""
Evaluates the operation and stores the result in a new group.
Expressions could combine +, - and * like o1+o2+o3-o4+o5+o6
Parameters:
-----------
groupname : str
The name of the group that will store the result.
group_op : Operation
The operation to evaluate.
"""
if not isinstance(group_op, Operation):
raise TypeError("Expected an instance of Operation.")
if group_name in self.list():
raise ValueError(f"The operation '{group_name}' already exists.")
# If the operation is already finalized, no need to evaluate
if group_op.isfinalized():
if group_op.name != group_name:
# If names differ, create a copy with the new name
self.copy(group_op.name, group_name)
return
# Recursively evaluate operands
operand_names = []
for op in group_op.operands:
if isinstance(op, Operation):
if op.isfinalized():
# Use existing finalized operation name
operand_names.append(op.name)
else:
# Generate a unique name if the operation doesn't have one
if not op.name:
op.name = op.generate_hashname()
# Recursively evaluate the operand operation
self.evaluate(op.name, op)
operand_names.append(op.name)
else:
operand_names.append(str(op))
# Call the appropriate method based on the operator
if group_op.operator == '+':
self.union(group_name, *operand_names)
group_op.operator = 'union'
elif group_op.operator == '-':
self.subtract(group_name, *operand_names)
group_op.operator = 'subtract'
elif group_op.operator == '*':
self.intersect(group_name, *operand_names)
group_op.operator = 'intersect'
else:
raise ValueError(f"Unknown operator: {group_op.operator}")
# Update the operation
group_op.name = group_name
# Get the last added operation
finalized_op = self._operations[-1]
group_op.code = finalized_op.code
group_op.operands = operand_names
# Add the operation to the group's _operations if not already added
if group_op.name not in self.list():
self._operations.append(group_op)
def add_group_criteria(self, *args, **kwargs):
"""
Adds group(s) using existing methods based on key-value pairs.
Supports two usages:
1. add_group_criteria(group_name, **criteria)
2. add_group_criteria(group_definitions)
Parameters:
- group_name (str): The name of the group.
- **criteria: Criteria for group creation.
OR
- group_definitions (dict): A dictionary where keys are group names and values are criteria dictionaries.
Raises:
- TypeError: If arguments are invalid.
Usage:
- G.add_group_criteria('group_name', type=[1,2])
- G.add_group_criteria({'group1': {'type': [1]}, 'group2': {'region': 'regionID'}})
"""
if len(args) == 1 and isinstance(args[0], dict):
# Called with group_definitions dict
group_definitions = args[0]
for group_name, criteria in group_definitions.items():
self.add_group_criteria_single(group_name, **criteria)
elif len(args) == 1 and isinstance(args[0], str):
# Called with group_name and criteria
group_name = args[0]
if not kwargs:
raise ValueError(f"No criteria provided for group '{group_name}'.")
self.add_group_criteria_single(group_name, **kwargs)
else:
raise TypeError("Invalid arguments. Use add_group_criteria(group_name, **criteria) or add_group_criteria(group_definitions).")
def add_group_criteria_single(self, group_name, **criteria):
"""
Adds a single group based on criteria.
Parameters:
- group_name (str): The name of the group.
- **criteria: Criteria for group creation.
Raises:
- TypeError: If group_name is not a string.
- ValueError: If no valid criteria are provided or if criteria are invalid.
Example (advanced):
G = group()
group_definitions = {
'myGroup': {
'variable': {
'name': 'myVar',
'expression': 'x > 5',
'style': 'atom'
}
}
}
G.add_group_criteria(group_definitions)
print(G.code())
Expected output
variable myVar atom "x > 5"
group myGroup variable myVar
"""
if not isinstance(group_name, str):
raise TypeError(f"Group name must be a string, got {type(group_name)}")
if not criteria:
raise ValueError(f"No criteria provided for group '{group_name}'.")
if "type" in criteria:
type_values = criteria["type"]
if not isinstance(type_values, (list, tuple, int)):
raise TypeError("Type values must be an integer or a list/tuple of integers.")
self.bytype(group_name, type_values)
elif "region" in criteria:
region_name = criteria["region"]
if not isinstance(region_name, str):
raise TypeError("Region name must be a string.")
self.byregion(group_name, region_name)
elif "id" in criteria:
id_values = criteria["id"]
if not isinstance(id_values, (list, tuple, int)):
raise TypeError("ID values must be an integer or a list/tuple of integers.")
self.byid(group_name, id_values)
elif "variable" in criteria:
var_info = criteria["variable"]
if not isinstance(var_info, dict):
raise TypeError("Variable criteria must be a dictionary.")
required_keys = {'name', 'expression'}
if not required_keys.issubset(var_info.keys()):
missing = required_keys - var_info.keys()
raise ValueError(f"Variable criteria missing keys: {missing}")
var_name = var_info['name']
expression = var_info['expression']
style = var_info.get('style', 'atom')
# First, assign the variable
self.variable(var_name, expression, style)
# Then, create the group based on the variable
self.byvariable(group_name, var_name)
elif "union" in criteria:
groups = criteria["union"]
if not isinstance(groups, (list, tuple)):
raise TypeError("Union groups must be a list or tuple of group names.")
self.union(group_name, *groups)
elif "intersect" in criteria:
groups = criteria["intersect"]
if not isinstance(groups, (list, tuple)):
raise TypeError("Intersect groups must be a list or tuple of group names.")
self.intersect(group_name, *groups)
elif "subtract" in criteria:
groups = criteria["subtract"]
if not isinstance(groups, (list, tuple)):
raise TypeError("Subtract groups must be a list or tuple of group names.")
self.subtract(group_name, *groups)
elif "create" in criteria and criteria["create"]:
self.create(group_name)
elif "clear" in criteria and criteria["clear"]:
self.clear(group_name)
else:
raise ValueError(f"No valid criterion provided for group '{group_name}'.")
def get_group_criteria(self, group_name):
"""
Retrieve the criteria that define a group. Handles group_name as a string or number.
Parameters:
- group_name: str or int, the name or number of the group.
Returns:
- dict or str: The criteria used to define the group, or a message if defined by multiple criteria.
Raises:
- ValueError: If the group does not exist.
"""
# Check if group_name exists in _operations
if group_name not in self._operations:
raise ValueError(f"Group '{group_name}' does not exist.")
# Retrieve all operations related to the group directly from _operations
operations = self._operations[group_name]
if not operations:
raise ValueError(f"No operations found for group '{group_name}'.")
criteria = {}
for op in operations:
if op.criteria:
criteria.update(op.criteria)
# If multiple criteria, return a message
if len(criteria) > 1:
return f"Group '{group_name}' is defined by multiple criteria: {criteria}"
return criteria
def generate_group_definitions_from_collection(self,collection):
"""
Generates group definitions based on the collection of groupobject instances.
This method populates the groups based on beadtypes and associated group names.
"""
group_defs = {}
for obj in collection:
for group_name in obj.group:
if group_name not in group_defs:
group_defs[group_name] = {'type': []}
if obj.beadtype not in group_defs[group_name]['type']:
group_defs[group_name]['type'].append(obj.beadtype)
# Now, add these group definitions
self.add_group_criteria(group_defs)
def dscript(self, name=None, printflag=None, verbose=None, verbosity=None):
"""
Generates a dscript object containing the group's LAMMPS commands.
Parameters:
- name (str): Optional name for the script object.
- printflag (bool, default=False): print on the current console if True
- verbose (bool, default=True): keep comments if True
Returns:
- dscript: A dscript object containing the group's code.
"""
if name is None:
name = self._name
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
# Create a new dscript object
dscript_obj = dscript(name=name,printflag=printflag, verbose=verbose, verbosity=verbosity)
# Add each line of the group's code to the script object
for idx, op in enumerate(self._operations):
# Use the index as the key for the script line
dscript_obj[idx] = op.code
return dscript_obj
def script(self, name=None, printflag=None, verbose=None, verbosity=None):
"""
Generates a script object containing the group's LAMMPS commands.
Parameters:
- name (str): Optional name for the script object.
- printflag (bool, default=False): print on the current console if True
- verbose (bool, default=True): keep comments if True
Returns:
- script: A script object containing the group's code.
"""
if name is None:
name = self._name
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
script_obj = self.dscript(name=name,printflag=printflag, verbose=verbose, verbosity=verbosity).script(printflag=printflag, verbose=verbose, verbosity=verbosity)
return script_obj
def pipescript(self, printflag=None, verbose=None, verbosity=None):
"""
Generates a pipescript object containing the group's LAMMPS commands.
Parameters:
- printflag (bool, default=False): print on the current console if True
- verbose (bool, default=True): keep comments if True
Returns:
- pipescript: A pipescript object containing the group's code lines as individual scripts in the pipeline.
"""
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
# Create a list to hold script objects
script_list = []
# For each operation, create a script object and add it to the list
for op in self._operations:
# Create a dscript object with the code line
dscript_obj = dscript(printflag=printflag, verbose=verbose, verbosity=verbosity)
dscript_obj["dummy"] = op.code
# Convert the dscript to a script object
script_obj = dscript_obj.script(printflag=printflag, verbose=verbose, verbosity=verbosity)
# Add the script object to the list
script_list.append(script_obj)
# Use the static method 'join' to create a pipescript from the list
if script_list:
pipe_obj = pipescript.join(script_list)
else:
pipe_obj = pipescript()
return pipe_obj
# 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, copy.deepcopy(v, memo))
return copie
def count(self,name=None, selection: Optional[List[str]] = None) -> 'dscript':
"""
Generates DSCRIPT counters for specified groups with LAMMPS variable definitions and print commands.
The method retrieves the list of group names using `self.list()`. If `selection` is provided, it
filters the groups to include only those specified. It then creates a variable for each selected
group that counts the number of atoms in that group and generates corresponding print commands.
The commands are encapsulated within a `dscript` object for execution.
### Parameters:
selection (list of str, optional):
- List of group names to be numbered.
- If `None`, all groups in the collection are numbered.
### Returns:
dscript: A `dscript` object containing the variable definitions and print commands, formatted as follows:
```
variable n_lower equal "count(lower)"
variable n_middle equal "count(middle)"
variable n_upper equal "count(upper)"
print "Number of atoms in lower: ${n_lower}"
print "Number of atoms in middle: ${n_middle}"
print "Number of atoms in upper: ${n_upper}"
```
The `variables` attribute holds the variable definitions, and the `printvariables` attribute
holds the print commands, each separated by a newline.
### Raises:
ValueError:
- If any group specified in `selection` does not exist in the collection.
- If `selection` contains duplicate group names.
TypeError:
- If `selection` is not a list of strings.
### Example:
```python
# Create groupobject instances
g1 = groupobject(beadtype=1,group = 'lower')
g2 = groupobject(beadtype=2,group = 'middle')
g3 = groupobject(beadtype=3,group = 'upper')
# Initialize a group with the groupobjects
G = group(name="1+2+3",collection=(g1,g2,g3)) or collection=g1+g2+g3
# add other groups
G.evaluate("all", G.lower + G.middle + G.upper)
G.evaluate("external", G.all - G.middle)
# Generate number commands for all groups
N = G.count(selection=["all","lower","middle","lower"])
print(N.do())
```
**Output:**
```
variable n_all equal "count(all)"
variable n_lower equal "count(lower)"
variable n_middle equal "count(middle)"
variable n_upper equal "count(upper)"
print "Number of atoms in all: ${n_all}"
print "Number of atoms in lower: ${n_lower}"
print "Number of atoms in middle: ${n_middle}"
print "Number of atoms in upper: ${n_upper}"
```
"""
# Retrieve the list of all group names
all_group_names = self.list()
# If selection is provided, validate it
if selection is not None:
if not isinstance(selection, (list, tuple)):
raise TypeError("Parameter 'selection' must be a list of group names.")
if not all(isinstance(gitem, str) for gitem in selection):
raise TypeError("All items in 'selection' must be strings representing group names.")
if len(selection) != len(set(selection)):
raise ValueError("Duplicate group names found in 'selection'. Each group should be unique.")
# Check that all selected groups exist
missing_groups = set(selection) - set(all_group_names)
if missing_groups:
raise ValueError(f"The following groups are not present in the collection: {', '.join(missing_groups)}")
# Use the selected groups
target_groups = selection
else:
# If no selection, target all groups
target_groups = all_group_names
# Initialize lists to hold variable definitions and print commands
variable_definitions = []
print_commands = []
for gitem in target_groups:
# Validate group name
if not gitem:
raise ValueError("Group names must be non-empty strings.")
# Create a valid variable name by replacing any non-alphanumeric characters with underscores
variable_name = f"n_{gitem}".replace(" ", "_").replace("-", "_")
# Define the variable to count the number of atoms in the group
variable_definitions.append(f'variable {variable_name} equal "count({gitem})"')
# Define the corresponding print command
print_commands.append(f'print "Number of atoms in {gitem}: ${{{variable_name}}}"')
# Create a dscript object with the generated commands
idD = f"<dscript:group:{self._name}:count>"
D = dscript(
name=idD if name is None else name,
description=f"{idD} for group counts"
)
D.variables = "\n".join(variable_definitions)
D.printvariables = "\n".join(print_commands)
D.set_all_variables()
return D
# %% debug section - generic code to test methods (press F5)
if __name__ == '__main__':
# Example Usage
G = group()
G.variable("groupname","variablename","myexpression myexpression myexpression myexpression and again 1234")
print(G.disp("groupname"))
G.byregion("regiongroup","myregionID")
print(G.disp("regiongroup"))
G.union("uniongroup","group1","group2","group3")
print(G.disp("uniongroup"))
# LAMMPS example
# group myGroup region myRegion
# group typeGroup type 1 2
# variable myVar atom "x + y"
# group varGroup variable myVar
# group unionGroup union myGroup typeGroup
# Assuming Operation class is properly defined and includes necessary methods
G0 = group()
G0.byregion('myGroup', 'myRegion')
G0.bytype('typeGroup', [1, 2])
G0.variable('myVar', 'x + y')
G0.byvariable('varGroup', 'myVar')
# Perform group operations
union_op = G0['myGroup'] + G0['typeGroup']
G0.evaluate('unionGroup', union_op)
# Generate LAMMPS script
print(G0.code())
# Advanced Usage
G = group()
G.create_groups('o1', 'o2', 'o3', 'o4')
G.create('o5')
G.create('o6')
G.create('o7')
G.evaluate("debug0",G.o1+G.o2+G.o3 + G.o4 + (G.o5 +G.o6) + G.o7)
G.evaluate("debug1",G.o1+G.o2)
G.evaluate("debug2",G.o1+G.o2+G.o3-(G.o4+G.o5)+(G.o6*G.o7))
print(repr(G))
# Example to prepare workshop
G = group()
G.add_group_criteria("lower", type=[1])
G.add_group_criteria("central", region="central_cyl")
G.add_group_criteria("new_group", create=True)
G.add_group_criteria("upper", clear=True)
G.add_group_criteria("subtract_group", subtract=["group1", "group2"])
Functions
def format_table(headers: List[str], rows: List[List[str]], col_max_widths: List[int], align: Union[str, List[str]] = 'C') ‑> str
-
Formats a table with given headers and rows, truncating text based on maximum column widths and aligning content based on the specified alignment.
Parameters:
headers (List[str]): List of column headers. rows (List[List[str]]): List of rows, where each row is a list of column values. col_max_widths (List[int]): List of maximum widths for each column. align (str or List[str], optional): Alignment for each column. Can be a single string (applies to all columns) or a list of strings (one for each column). Options are: - "C" or "center" for centered alignment (default). - "L" or "left" for left alignment. - "R" or "right" for right alignment. Case insensitive.
Returns:
str: A formatted table as a string.
Raises:
ValueError: If <code>align</code> is a list but its length does not match the number of columns.
Example:
>>> headers = ["Idx", "Name", "Value"] >>> rows = [[1, "Example", "12345"], [2, "LongerName", "67890"]] >>> col_max_widths = [5, 10, 8] >>> align = ["R", "C", "L"] >>> print(format_table(headers, rows, col_max_widths, align)) Idx | Name | Value ----- | ---------- | -------- 1 | Example | 12345 2 | LongerName | 67890
Expand source code
def format_table( headers: List[str], rows: List[List[str]], col_max_widths: List[int], align: Union[str, List[str]] = "C", ) -> str: """ Formats a table with given headers and rows, truncating text based on maximum column widths and aligning content based on the specified alignment. ### Parameters: headers (List[str]): List of column headers. rows (List[List[str]]): List of rows, where each row is a list of column values. col_max_widths (List[int]): List of maximum widths for each column. align (str or List[str], optional): Alignment for each column. Can be a single string (applies to all columns) or a list of strings (one for each column). Options are: - "C" or "center" for centered alignment (default). - "L" or "left" for left alignment. - "R" or "right" for right alignment. Case insensitive. ### Returns: str: A formatted table as a string. ### Raises: ValueError: If `align` is a list but its length does not match the number of columns. ### Example: >>> headers = ["Idx", "Name", "Value"] >>> rows = [[1, "Example", "12345"], [2, "LongerName", "67890"]] >>> col_max_widths = [5, 10, 8] >>> align = ["R", "C", "L"] >>> print(format_table(headers, rows, col_max_widths, align)) Idx | Name | Value ----- | ---------- | -------- 1 | Example | 12345 2 | LongerName | 67890 """ # Normalize alignment input if isinstance(align, str): align = [align.upper()] * len(headers) elif isinstance(align, list): align = [a.upper() for a in align] if len(align) != len(headers): raise ValueError("The length of `align` must match the number of columns.") else: raise TypeError("`align` must be a string or a list of strings.") # Determine actual column widths based on headers and max widths col_widths = [ min(max(len(header), *(len(str(row[i])) for row in rows)), max_w) for i, header in enumerate(headers) for max_w in [col_max_widths[i]] ] # Prepare alignment formatting alignments = [] for a in align: if a in ("C", "CENTER"): alignments.append("^") elif a in ("L", "LEFT"): alignments.append("<") elif a in ("R", "RIGHT"): alignments.append(">") else: raise ValueError(f"Invalid alignment value: {a}") # Prepare header line header_line = " | ".join( f"{headers[i]:{alignments[i]}{col_widths[i]}}" for i in range(len(headers)) ) # Prepare separator separator = " | ".join("-" * col_widths[i] for i in range(len(col_widths))) # Prepare rows formatted_rows = [] for row in rows: formatted_row = [ f"{str(row[i]):{alignments[i]}{col_widths[i]}}" for i in range(len(row)) ] formatted_rows.append(" | ".join(formatted_row)) # Combine all parts table = [header_line, separator] + formatted_rows + [separator] return "\n".join(table)
def generate_random_name(length=8)
-
Expand source code
def generate_random_name(length=8): letters = string.ascii_letters # 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' return ''.join(random.choice(letters) for _ in range(length))
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 truncate_text(txt, maxwidth)
-
Truncates the input text to fit within the specified maximum width. If the text is longer than
maxwidth
, it is shortened by keeping the beginning and trailing parts, separated by " […] ".Parameters:
txt (str): The text to truncate. maxwidth (int): The maximum allowed width for the text.
Returns:
str: The truncated text with " [...] " in the middle if truncation occurs.
Example:
>>> truncate_text("This is a long string that needs truncation.", 20) 'This is [...] truncation.'
Expand source code
def truncate_text(txt, maxwidth): """ Truncates the input text to fit within the specified maximum width. If the text is longer than `maxwidth`, it is shortened by keeping the beginning and trailing parts, separated by " [...] ". ### Parameters: txt (str): The text to truncate. maxwidth (int): The maximum allowed width for the text. ### Returns: str: The truncated text with " [...] " in the middle if truncation occurs. ### Example: >>> truncate_text("This is a long string that needs truncation.", 20) 'This is [...] truncation.' """ # If the text fits within maxwidth, return it as is if len(txt) <= maxwidth: return txt # Calculate the part lengths for beginning and end ellipsis = " [...] " split_length = (maxwidth - len(ellipsis)) // 2 beginning = txt[:split_length] trailing = txt[-split_length:] return f"{beginning}{ellipsis}{trailing}"
Classes
class Operation (operator, operands, name='', code='', criteria=None)
-
Represents a LAMMPS group operation, allowing algebraic manipulation and code generation.
Overview
The
Operation
class models a group operation in LAMMPS (Large-scale Atomic/Molecular Massively Parallel Simulator), encapsulating an operator, its operands, a name, and the corresponding LAMMPS code. It supports algebraic operations between groups (union, intersection, subtraction) using the overloaded operators '+', '-', and '*', enabling users to build complex group expressions programmatically.Attributes
-
operator (str): The operator applied to the operands. It can be:
- A finalized LAMMPS operator:
'variable'
,'byvariable'
,'byregion'
,'bytype'
,'byid'
,'create'
,'clear'
,'union'
,'intersect'
,'subtract'
. - An algebraic operator:
'+'
,'-'
,'*'
. - An empty string or
None
for operations without an explicit operator.
- A finalized LAMMPS operator:
-
operands (list): A list of operands for the operation. Operands can be:
- Instances of
Operation
, representing nested operations. - Strings representing group names or other identifiers.
- Integers or lists (e.g., atom types or IDs).
- Instances of
-
name (str): The name of the operation, which can be manually set or auto-generated.
- If not provided, a unique hash-based name is generated using the
generate_hashname
method.
- If not provided, a unique hash-based name is generated using the
-
code (str): The LAMMPS code that this operation represents.
- This is generated when the operation is finalized and corresponds to the actual command(s) that would be included in a LAMMPS input script.
-
criteria (dict, optional): Criteria used to define the operation.
- Useful for operations like
bytype
,byregion
,byid
, etc., where the criteria specify the selection parameters.
- Useful for operations like
Methods
-
__init__(self, operator, operands, name="", code="", criteria=None)
: Initializes a newOperation
instance with the given parameters.- operator (str): The operator applied to the operands.
- operands (list or single operand): The operands for the operation.
- name (str, optional): The name of the operation.
- code (str, optional): The LAMMPS code representing the operation.
- criteria (dict, optional): Criteria used to define the operation.
-
append(self, operand)
: Appends a single operand to the operands list. -
extend(self, operands)
: Extends the operands list with multiple operands. -
is_unary(self)
: Checks if theOperation
instance has exactly one operand. -
is_empty(self)
: Checks if theOperation
instance has no operands. -
generateID(self)
: Generates an ID for the instance based on its operands or name. Used internally for generating unique names. -
generate_hashname(self, prefix="grp", ID=None)
: Generates a hash-based name for the instance.- prefix (str): A string prefix for the generated name.
- ID (str, optional): An optional ID to include in the hash. If
None
, it is generated from operands.
-
get_proper_operand(self)
: Returns the appropriate operand representation depending on whether the operation is finalized. -
_operate(self, other, operator)
: Combines the operation with anotherOperation
instance using the specified operator.- other (
Operation
): The other operation to combine with. - operator (str): The operator to apply ('+', '-', '*').
- other (
-
isfinalized(self)
: Checks if the operation has been finalized.- An operation is considered finalized if its operator is not one of the algebraic operators '+', '-', '*'.
-
__add__(self, other)
: Overloads the '+' operator to support union of twoOperation
instances. -
__sub__(self, other)
: Overloads the '-' operator to support subtraction between twoOperation
instances. -
__mul__(self, other)
: Overloads the '*' operator to support intersection between twoOperation
instances. -
__repr__(self)
: Returns a detailed string representation of the operation, useful for debugging. -
__str__(self)
: Returns a concise string representation of the operation. -
script(self)
: Generates the LAMMPS code for this operation usingdscript
andscript
classes.- Returns a
script
object containing the LAMMPS code.
- Returns a
Operator Overloading
The
Operation
class overloads the following operators to enable algebraic manipulation:-
Addition (
+
):- Combines two operations using union.
- Example:
op3 = op1 + op2
-
Subtraction (
-
):- Subtracts one operation from another.
- Example:
op3 = op1 - op2
-
Multiplication (
*
):- Intersects two operations.
- Example:
op3 = op1 * op2
Usage Example
# Create basic operations op1 = Operation('bytype', [1], name='group1') op2 = Operation('bytype', [2], name='group2') # Combine operations using algebraic operators combined_op = op1 + op2 # Union of group1 and group2 # Initialize group manager G = group() G.add_operation(op1) G.add_operation(op2) # Evaluate the combined operation and store the result G.evaluate('combined_group', combined_op) # Access the generated LAMMPS code print(G.code()) # Output: # group group1 type 1 # group group2 type 2 # group combined_group union group1 group2
Notes
-
Lazy Evaluation:
- Operations are combined and stored symbolically until they are evaluated.
- The actual LAMMPS code is generated when the operation is finalized (e.g., via the
evaluate
method in thegroup
class).
-
Name Generation:
- If an operation's name is not provided, it is auto-generated using a hash of its operator and operands.
- This ensures uniqueness and avoids naming conflicts.
-
Criteria Attribute:
- The
criteria
attribute allows storing the parameters used to define operations. - Useful for debugging, documentation, or reconstructing operations.
- The
-
Finalization:
- An operation is finalized when it represents an actual LAMMPS command and has generated code.
- Finalized operations are not further combined; instead, their names are used in subsequent operations.
-
Integration with
dscript
andscript
:- The
script
method generates the LAMMPS code for the operation usingdscript
andscript
classes. - Facilitates integration with script management tools and advanced scripting workflows.
- The
-
Error Handling:
- Methods include error checks to ensure operands are valid and operations are correctly formed.
- Raises informative exceptions for invalid operations or unsupported types.
Integration with Group Class
The
Operation
class is designed to work closely with thegroup
class, which manages a collection of operations and handles evaluation and code generation.- Adding Operations:
- Use
group.add_operation()(operation)
to add anOperation
instance to agroup
.
- Use
- Evaluating Operations:
- Use
group.evaluate()(group_name, operation)
to finalize an operation and generate LAMMPS code.
- Use
- Accessing Operations:
- Operations can be accessed via the
group
instance using indexing, attribute access, or methods.
- Operations can be accessed via the
Additional Information
-
String Representations:
__str__
: Provides a concise representation, useful for quick inspection.__repr__
: Provides a detailed representation, useful for debugging.
-
Example of
__str__
Output:- For an operation representing
group1 + group2
:python print(str(combined_op)) # Output: <combined_group=[group1+group2]>
- For an operation representing
-
Example of
__repr__
Output:python print(repr(combined_op)) # Output: # Operation Details: # - Name: combined_group # - Operator: + # - Operands: group1, group2 # - LAMMPS Code:
Examples
Defining Operations Based on Atom Types
op1 = Operation('bytype', [1], name='type1') op2 = Operation('bytype', [2], name='type2') G.add_operation(op1) G.add_operation(op2)
Combining and Evaluating Operations
combined_op = op1 + op2 # Union of 'type1' and 'type2' G.evaluate('all_types', combined_op)
Accessing Generated Code
print(G.code()) # Output: # group type1 type 1 # group type2 type 2 # group all_types union type1 type2
Using Criteria Attribute
# Access criteria used to define an operation print(op1.criteria) # Output: # {'type': [1]}
Important Notes
-
Algebraic Operators vs. Finalized Operators:
- Algebraic operators ('+', '-', '*') are used for symbolic manipulation.
- Finalized operators represent actual LAMMPS commands.
-
Operator Precedence:
- When combining operations, consider the order of operations.
- Use parentheses to ensure the desired evaluation order.
-
Integration with Scripting Tools:
- The
script
method allows operations to be integrated into larger scripting workflows. - Supports advanced features like variable substitution and conditional execution.
- The
Conclusion
The
Operation
class provides a powerful and flexible way to model and manipulate group operations in LAMMPS simulations. By supporting algebraic operations and integrating with script management tools, it enables users to build complex group definitions programmatically, enhancing productivity and reducing errors in simulation setup.Initializes a new Operation instance with given parameters.
Expand source code
class Operation: """ Represents a LAMMPS group operation, allowing algebraic manipulation and code generation. ### Overview The `Operation` class models a group operation in LAMMPS (Large-scale Atomic/Molecular Massively Parallel Simulator), encapsulating an operator, its operands, a name, and the corresponding LAMMPS code. It supports algebraic operations between groups (union, intersection, subtraction) using the overloaded operators '+', '-', and '*', enabling users to build complex group expressions programmatically. ### Attributes - **operator (str)**: The operator applied to the operands. It can be: - A finalized LAMMPS operator: `'variable'`, `'byvariable'`, `'byregion'`, `'bytype'`, `'byid'`, `'create'`, `'clear'`, `'union'`, `'intersect'`, `'subtract'`. - An algebraic operator: `'+'`, `'-'`, `'*'`. - An empty string or `None` for operations without an explicit operator. - **operands (list)**: A list of operands for the operation. Operands can be: - Instances of `Operation`, representing nested operations. - Strings representing group names or other identifiers. - Integers or lists (e.g., atom types or IDs). - **name (str)**: The name of the operation, which can be manually set or auto-generated. - If not provided, a unique hash-based name is generated using the `generate_hashname` method. - **code (str)**: The LAMMPS code that this operation represents. - This is generated when the operation is finalized and corresponds to the actual command(s) that would be included in a LAMMPS input script. - **criteria (dict, optional)**: Criteria used to define the operation. - Useful for operations like `bytype`, `byregion`, `byid`, etc., where the criteria specify the selection parameters. ### Methods - `__init__(self, operator, operands, name="", code="", criteria=None)`: Initializes a new `Operation` instance with the given parameters. - **operator** (str): The operator applied to the operands. - **operands** (list or single operand): The operands for the operation. - **name** (str, optional): The name of the operation. - **code** (str, optional): The LAMMPS code representing the operation. - **criteria** (dict, optional): Criteria used to define the operation. - `append(self, operand)`: Appends a single operand to the operands list. - `extend(self, operands)`: Extends the operands list with multiple operands. - `is_unary(self)`: Checks if the `Operation` instance has exactly one operand. - `is_empty(self)`: Checks if the `Operation` instance has no operands. - `generateID(self)`: Generates an ID for the instance based on its operands or name. Used internally for generating unique names. - `generate_hashname(self, prefix="grp", ID=None)`: Generates a hash-based name for the instance. - **prefix** (str): A string prefix for the generated name. - **ID** (str, optional): An optional ID to include in the hash. If `None`, it is generated from operands. - `get_proper_operand(self)`: Returns the appropriate operand representation depending on whether the operation is finalized. - `_operate(self, other, operator)`: Combines the operation with another `Operation` instance using the specified operator. - **other** (`Operation`): The other operation to combine with. - **operator** (str): The operator to apply ('+', '-', '*'). - `isfinalized(self)`: Checks if the operation has been finalized. - An operation is considered finalized if its operator is not one of the algebraic operators '+', '-', '*'. - `__add__(self, other)`: Overloads the '+' operator to support union of two `Operation` instances. - `__sub__(self, other)`: Overloads the '-' operator to support subtraction between two `Operation` instances. - `__mul__(self, other)`: Overloads the '*' operator to support intersection between two `Operation` instances. - `__repr__(self)`: Returns a detailed string representation of the operation, useful for debugging. - `__str__(self)`: Returns a concise string representation of the operation. - `script(self)`: Generates the LAMMPS code for this operation using `dscript` and `script` classes. - Returns a `script` object containing the LAMMPS code. ### Operator Overloading The `Operation` class overloads the following operators to enable algebraic manipulation: - **Addition (`+`)**: - Combines two operations using union. - Example: `op3 = op1 + op2` - **Subtraction (`-`)**: - Subtracts one operation from another. - Example: `op3 = op1 - op2` - **Multiplication (`*`)**: - Intersects two operations. - Example: `op3 = op1 * op2` ### Usage Example ```python # Create basic operations op1 = Operation('bytype', [1], name='group1') op2 = Operation('bytype', [2], name='group2') # Combine operations using algebraic operators combined_op = op1 + op2 # Union of group1 and group2 # Initialize group manager G = group() G.add_operation(op1) G.add_operation(op2) # Evaluate the combined operation and store the result G.evaluate('combined_group', combined_op) # Access the generated LAMMPS code print(G.code()) # Output: # group group1 type 1 # group group2 type 2 # group combined_group union group1 group2 ``` ### Notes - **Lazy Evaluation**: - Operations are combined and stored symbolically until they are evaluated. - The actual LAMMPS code is generated when the operation is finalized (e.g., via the `evaluate` method in the `group` class). - **Name Generation**: - If an operation's name is not provided, it is auto-generated using a hash of its operator and operands. - This ensures uniqueness and avoids naming conflicts. - **Criteria Attribute**: - The `criteria` attribute allows storing the parameters used to define operations. - Useful for debugging, documentation, or reconstructing operations. - **Finalization**: - An operation is finalized when it represents an actual LAMMPS command and has generated code. - Finalized operations are not further combined; instead, their names are used in subsequent operations. - **Integration with `dscript` and `script`**: - The `script` method generates the LAMMPS code for the operation using `dscript` and `script` classes. - Facilitates integration with script management tools and advanced scripting workflows. - **Error Handling**: - Methods include error checks to ensure operands are valid and operations are correctly formed. - Raises informative exceptions for invalid operations or unsupported types. ### Integration with Group Class The `Operation` class is designed to work closely with the `group` class, which manages a collection of operations and handles evaluation and code generation. - **Adding Operations**: - Use `group.add_operation(operation)` to add an `Operation` instance to a `group`. - **Evaluating Operations**: - Use `group.evaluate(group_name, operation)` to finalize an operation and generate LAMMPS code. - **Accessing Operations**: - Operations can be accessed via the `group` instance using indexing, attribute access, or methods. ### Additional Information - **String Representations**: - `__str__`: Provides a concise representation, useful for quick inspection. - `__repr__`: Provides a detailed representation, useful for debugging. - **Example of `__str__` Output**: - For an operation representing `group1 + group2`: ```python print(str(combined_op)) # Output: <combined_group=[group1+group2]> ``` - **Example of `__repr__` Output**: ```python print(repr(combined_op)) # Output: # Operation Details: # - Name: combined_group # - Operator: + # - Operands: group1, group2 # - LAMMPS Code: ``` ### Examples **Defining Operations Based on Atom Types** ```python op1 = Operation('bytype', [1], name='type1') op2 = Operation('bytype', [2], name='type2') G.add_operation(op1) G.add_operation(op2) ``` **Combining and Evaluating Operations** ```python combined_op = op1 + op2 # Union of 'type1' and 'type2' G.evaluate('all_types', combined_op) ``` **Accessing Generated Code** ```python print(G.code()) # Output: # group type1 type 1 # group type2 type 2 # group all_types union type1 type2 ``` **Using Criteria Attribute** ```python # Access criteria used to define an operation print(op1.criteria) # Output: # {'type': [1]} ``` ### Important Notes - **Algebraic Operators vs. Finalized Operators**: - Algebraic operators ('+', '-', '*') are used for symbolic manipulation. - Finalized operators represent actual LAMMPS commands. - **Operator Precedence**: - When combining operations, consider the order of operations. - Use parentheses to ensure the desired evaluation order. - **Integration with Scripting Tools**: - The `script` method allows operations to be integrated into larger scripting workflows. - Supports advanced features like variable substitution and conditional execution. ### Conclusion The `Operation` class provides a powerful and flexible way to model and manipulate group operations in LAMMPS simulations. By supporting algebraic operations and integrating with script management tools, it enables users to build complex group definitions programmatically, enhancing productivity and reducing errors in simulation setup. """ def __init__(self, operator, operands, name="", code="", criteria=None): """Initializes a new Operation instance with given parameters.""" self.operator = operator self.operands = operands if isinstance(operands, list) else [operands] if not name: self.name = self.generate_hashname() else: self.name = name self.code = code self.criteria = criteria # Store criteria for this operation def append(self, operand): """Appends a single operand to the operands list of the Operation instance.""" self.operands.append(operand) def extend(self, operands): """Extends the operands list of the Operation instance with multiple operands.""" self.operands.extend(operands) def is_unary(self): """Checks if the Operation instance has exactly one operand.""" return len(self.operands) == 1 def is_empty(self): """Checks if the Operation instance has no operands.""" return len(self.operands) == 0 def generateID(self): """Generates an ID for the Operation instance based on its operands or name.""" # if self.operands: # return "".join([str(op) for op in self.operands]) # else: # return self.name operand_ids = ''.join(op.name if isinstance(op, Operation) else str(op) for op in self.operands) return operand_ids def generate_hashname(self,prefix="grp",ID=None): """Generates an ID for the Operation instance based on its operands or name.""" # if ID is None: ID = self.generateID() # s = self.operator+ID # return prefix+hashlib.sha256(s.encode()).hexdigest()[:6] if ID is None: ID = self.generateID() s = self.operator + ID return prefix + hashlib.sha256(s.encode()).hexdigest()[:6] def __repr__(self): """ detailed representation """ operand_str = ', '.join(str(op) for op in self.operands) return ( f"Operation Details:\n" f" - Name: {self.name}\n" f" - Operator: {self.operator}\n" f" - Operands: {operand_str}\n" f" - LAMMPS Code: {self.code}" ) def __str__(self): """ string representation """ operands = [str(op) for op in self.operands] operand_str = ', '.join(operands) if self.is_empty(): return f"<{self.operator} {self.name}>" elif self.is_unary(): return f"<{self.name}={self.operator}({operand_str})>" else: # Polynary operator_symbol_mapping = { 'union': '+', 'intersect': '*', 'subtract': '-' } operator_symbol = operator_symbol_mapping.get(self.operator, self.operator) formatted_operands = operator_symbol.join(operands) return f"<{self.name}=[{formatted_operands}]>" def isfinalized(self): """ Checks whether the Operation instance is finalized. Returns: - bool: True if the Operation is finalized, otherwise False. Functionality: - An Operation is considered finalized if its operator is not one of the algebraic operators '+', '-', '*'. """ return self.operator not in ('+', '-', '*') def __add__(self, other): """ overload + as union """ return self._operate(other,'+') def __sub__(self, other): """ overload - as subtract """ return self._operate(other,'-') def __mul__(self, other): """ overload * as intersect """ return self._operate(other,'*') def get_proper_operand(self): # """ # Returns the proper operand depending on whether the operation is finalized. # """ # return span(self.operands) if not self.isfinalized() else self.name """ Returns the proper operand depending on whether the operation is finalized. """ if self.isfinalized(): return self.name else: # Collect operand names operand_names = [ op.name if isinstance(op, Operation) else str(op) for op in self.operands ] return span(operand_names) def _operate(self, other, operator): """ Implements algebraic operations between self and other Operation instances. Parameters: - other (Operation): The other Operation instance to be operated with. - operator (str): The operation to be performed. Supported values are '+', '-', '*'. Returns: - Operation: A new Operation instance reflecting the result of the operation. """ # Ensure other is also an instance of Operation if not isinstance(other, Operation): raise TypeError(f"Unsupported type: {type(other)}") # Map operator to prefix prefix_map = {'+': 'add', '-': 'sub', '*': 'mul'} prefix = prefix_map.get(operator, 'op') # Prepare operands list operands = [] # Handle self if self.operator == operator and not self.isfinalized(): operands.extend(self.operands) else: operands.append(self) # Handle other if other.operator == operator and not other.isfinalized(): operands.extend(other.operands) else: operands.append(other) # Generate a new name name = self.generate_hashname(prefix=prefix, ID=self.generateID() + other.generateID()) # Create a new Operation new_op = Operation(operator, operands, name=name) return new_op def script(self): """ Generate the LAMMPS code using the dscript and script classes. Returns: - script onject: The LAMMPS code generated by this operation. """ # Create a dscript object to store the operation code dscript_obj = dscript() # Use a dummy key for the dscript template dscript_obj["dummy"] = self.code # Convert the dscript object to a script object script_obj = dscript_obj.script() return script_obj
Methods
def append(self, operand)
-
Appends a single operand to the operands list of the Operation instance.
Expand source code
def append(self, operand): """Appends a single operand to the operands list of the Operation instance.""" self.operands.append(operand)
def extend(self, operands)
-
Extends the operands list of the Operation instance with multiple operands.
Expand source code
def extend(self, operands): """Extends the operands list of the Operation instance with multiple operands.""" self.operands.extend(operands)
def generateID(self)
-
Generates an ID for the Operation instance based on its operands or name.
Expand source code
def generateID(self): """Generates an ID for the Operation instance based on its operands or name.""" # if self.operands: # return "".join([str(op) for op in self.operands]) # else: # return self.name operand_ids = ''.join(op.name if isinstance(op, Operation) else str(op) for op in self.operands) return operand_ids
def generate_hashname(self, prefix='grp', ID=None)
-
Generates an ID for the Operation instance based on its operands or name.
Expand source code
def generate_hashname(self,prefix="grp",ID=None): """Generates an ID for the Operation instance based on its operands or name.""" # if ID is None: ID = self.generateID() # s = self.operator+ID # return prefix+hashlib.sha256(s.encode()).hexdigest()[:6] if ID is None: ID = self.generateID() s = self.operator + ID return prefix + hashlib.sha256(s.encode()).hexdigest()[:6]
def get_proper_operand(self)
-
Returns the proper operand depending on whether the operation is finalized.
Expand source code
def get_proper_operand(self): # """ # Returns the proper operand depending on whether the operation is finalized. # """ # return span(self.operands) if not self.isfinalized() else self.name """ Returns the proper operand depending on whether the operation is finalized. """ if self.isfinalized(): return self.name else: # Collect operand names operand_names = [ op.name if isinstance(op, Operation) else str(op) for op in self.operands ] return span(operand_names)
def is_empty(self)
-
Checks if the Operation instance has no operands.
Expand source code
def is_empty(self): """Checks if the Operation instance has no operands.""" return len(self.operands) == 0
def is_unary(self)
-
Checks if the Operation instance has exactly one operand.
Expand source code
def is_unary(self): """Checks if the Operation instance has exactly one operand.""" return len(self.operands) == 1
def isfinalized(self)
-
Checks whether the Operation instance is finalized. Returns: - bool: True if the Operation is finalized, otherwise False. Functionality: - An Operation is considered finalized if its operator is not one of the algebraic operators '+', '-', '*'.
Expand source code
def isfinalized(self): """ Checks whether the Operation instance is finalized. Returns: - bool: True if the Operation is finalized, otherwise False. Functionality: - An Operation is considered finalized if its operator is not one of the algebraic operators '+', '-', '*'. """ return self.operator not in ('+', '-', '*')
def script(self)
-
Generate the LAMMPS code using the dscript and script classes.
Returns: - script onject: The LAMMPS code generated by this operation.
Expand source code
def script(self): """ Generate the LAMMPS code using the dscript and script classes. Returns: - script onject: The LAMMPS code generated by this operation. """ # Create a dscript object to store the operation code dscript_obj = dscript() # Use a dummy key for the dscript template dscript_obj["dummy"] = self.code # Convert the dscript object to a script object script_obj = dscript_obj.script() return script_obj
-
class dscript (name=None, SECTIONS=['DYNAMIC'], section=0, position=None, role='dscript instance', description='dynamic script', userid='dscript', version=None, license=None, email=None, printflag=False, verbose=False, verbosity=None, **userdefinitions)
-
dscript: A Dynamic Script Management Class
The
dscript
class is designed to manage and dynamically generate multiple lines/items of a script, typically for use with LAMMPS or similar simulation tools. Each line in the script is represented as aScriptTemplate
object, and the class provides tools to easily manipulate, concatenate, and execute these script lines/items.Key Features:
- Dynamic Script Generation: Define and manage script lines/items dynamically, with variables that can be substituted at runtime.
- Conditional Execution: Add conditions to script lines/items so they are only included if certain criteria are met.
- Script Concatenation: Combine multiple script objects while maintaining control over variable precedence and script structure.
- User-Friendly Access: Easily access and manipulate script lines/items using familiar Python constructs like indexing and iteration.
Practical Use Cases:
- Custom LAMMPS Scripts: Generate complex simulation scripts with varying parameters based on dynamic conditions.
- Automation: Automate the creation of scripts for batch processing, simulations, or other repetitive tasks.
- Script Management: Manage and version-control different script sections and configurations easily.
Methods:
init(self, name=None): Initializes a new
dscript
object with an optional name.getitem(self, key): Retrieves a script line by its key. If a list of keys is provided, returns a new
dscript
object with lines/items reordered accordingly.setitem(self, key, value): Adds or updates a script line. If the value is an empty list, the corresponding script line is removed.
delitem(self, key): Deletes a script line by its key.
contains(self, key): Checks if a key exists in the script. Allows usage of
in
keyword.iter(self): Returns an iterator over the script lines/items, allowing for easy iteration through all lines/items in the
TEMPLATE
.len(self): Returns the number of script lines/items currently stored in the
TEMPLATE
.keys(self): Returns the keys of the
TEMPLATE
dictionary.values(self): Returns the
ScriptTemplate
objects stored as values in theTEMPLATE
.items(self): Returns the keys and
ScriptTemplate
objects from theTEMPLATE
as pairs.str(self): Returns a human-readable summary of the script, including the number of lines/items and total attributes. Shortcut:
str(S)
.repr(self): Provides a detailed string representation of the entire
dscript
object, including all script lines/items and their attributes. Useful for debugging.reorder(self, order): Reorders the script lines/items based on a given list of indices, creating a new
dscript
object with the reordered lines/items.get_content_by_index(self, index, do=True, protected=True): Returns the processed content of the script line at the specified index, with variables substituted based on the definitions and conditions applied.
get_attributes_by_index(self, index): Returns the attributes of the script line at the specified index.
add_dynamic_script(self, key, content="", definitions=None, verbose=None, **USER): Add a dynamic script step to the
dscript
object.createEmptyVariables(self, vars): Creates new variables in
DEFINITIONS
if they do not already exist. Accepts a single variable name or a list of variable names.do(self, printflag=None, verbose=None): Executes all script lines/items in the
TEMPLATE
, concatenating the results, and handling variable substitution. Returns the full script as a string.script(self, **userdefinitions): Generates a
lamdaScript
object from the currentdscript
object, applying any additional user definitions provided.pipescript(self, printflag=None, verbose=None, **USER): Returns a
pipescript
object by combining script objects for all keys in theTEMPLATE
. Each key inTEMPLATE
is handled separately, and the resulting scripts are combined using the|
operator.save(self, filename=None, foldername=None, overwrite=False): Saves the current script instance to a text file in a structured format. Includes metadata, global parameters, definitions, templates, and attributes.
write(scriptcontent, filename=None, foldername=None, overwrite=False): Writes the provided script content to a specified file in a given folder, with a header added if necessary, ensuring the correct file format.
load(cls, filename, foldername=None, numerickeys=True): Loads a script instance from a text file, restoring the content, definitions, templates, and attributes. Handles parsing and variable substitution based on the structure of the file.
parsesyntax(cls, content, numerickeys=True): Parses a script instance from a string input, restoring the content, definitions, templates, and attributes. Handles parsing and variable substitution based on the structure of the provided string, ensuring the correct format and key conversions when necessary.
Example:
Create a dscript object
R = dscript(name="MyScript")
Define global variables (DEFINITIONS)
R.DEFINITIONS.dimension = 3 R.DEFINITIONS.units = "$si"
Add script lines
R[0] = "dimension ${dimension}" R[1] = "units ${units}"
Generate and print the script
sR = R.script() print(sR.do())
Attributes:
name : str The name of the script, useful for identification. TEMPLATE : dict A dictionary storing script lines/items, with keys to identify each line. DEFINITIONS : lambdaScriptdata Stores the variables and parameters used within the script lines/items.
Initializes a new
dscript
object.The constructor sets up a new
dscript
object, which allows you to define and manage a script composed of multiple lines/items. Each line is stored in theTEMPLATE
dictionary, and variables used in the script are stored inDEFINITIONS
.Parameters:
name : str, optional The name of the script. If no name is provided, a random name will be generated automatically. The name is useful for identifying the script, especially when managing multiple scripts.
Example:
Create a dscript object with a specific name
R = dscript(name="ExampleScript")
Or create a dscript object with a random name
R = dscript()
After initialization, you can start adding script lines/items and defining variables.
Expand source code
class dscript: """ dscript: A Dynamic Script Management Class The `dscript` class is designed to manage and dynamically generate multiple lines/items of a script, typically for use with LAMMPS or similar simulation tools. Each line in the script is represented as a `ScriptTemplate` object, and the class provides tools to easily manipulate, concatenate, and execute these script lines/items. Key Features: ------------- - **Dynamic Script Generation**: Define and manage script lines/items dynamically, with variables that can be substituted at runtime. - **Conditional Execution**: Add conditions to script lines/items so they are only included if certain criteria are met. - **Script Concatenation**: Combine multiple script objects while maintaining control over variable precedence and script structure. - **User-Friendly Access**: Easily access and manipulate script lines/items using familiar Python constructs like indexing and iteration. Practical Use Cases: -------------------- - **Custom LAMMPS Scripts**: Generate complex simulation scripts with varying parameters based on dynamic conditions. - **Automation**: Automate the creation of scripts for batch processing, simulations, or other repetitive tasks. - **Script Management**: Manage and version-control different script sections and configurations easily. Methods: -------- __init__(self, name=None): Initializes a new `dscript` object with an optional name. __getitem__(self, key): Retrieves a script line by its key. If a list of keys is provided, returns a new `dscript` object with lines/items reordered accordingly. __setitem__(self, key, value): Adds or updates a script line. If the value is an empty list, the corresponding script line is removed. __delitem__(self, key): Deletes a script line by its key. __contains__(self, key): Checks if a key exists in the script. Allows usage of `in` keyword. __iter__(self): Returns an iterator over the script lines/items, allowing for easy iteration through all lines/items in the `TEMPLATE`. __len__(self): Returns the number of script lines/items currently stored in the `TEMPLATE`. keys(self): Returns the keys of the `TEMPLATE` dictionary. values(self): Returns the `ScriptTemplate` objects stored as values in the `TEMPLATE`. items(self): Returns the keys and `ScriptTemplate` objects from the `TEMPLATE` as pairs. __str__(self): Returns a human-readable summary of the script, including the number of lines/items and total attributes. Shortcut: `str(S)`. __repr__(self): Provides a detailed string representation of the entire `dscript` object, including all script lines/items and their attributes. Useful for debugging. reorder(self, order): Reorders the script lines/items based on a given list of indices, creating a new `dscript` object with the reordered lines/items. get_content_by_index(self, index, do=True, protected=True): Returns the processed content of the script line at the specified index, with variables substituted based on the definitions and conditions applied. get_attributes_by_index(self, index): Returns the attributes of the script line at the specified index. add_dynamic_script(self, key, content="", definitions=None, verbose=None, **USER): Add a dynamic script step to the `dscript` object. createEmptyVariables(self, vars): Creates new variables in `DEFINITIONS` if they do not already exist. Accepts a single variable name or a list of variable names. do(self, printflag=None, verbose=None): Executes all script lines/items in the `TEMPLATE`, concatenating the results, and handling variable substitution. Returns the full script as a string. script(self, **userdefinitions): Generates a `lamdaScript` object from the current `dscript` object, applying any additional user definitions provided. pipescript(self, printflag=None, verbose=None, **USER): Returns a `pipescript` object by combining script objects for all keys in the `TEMPLATE`. Each key in `TEMPLATE` is handled separately, and the resulting scripts are combined using the `|` operator. save(self, filename=None, foldername=None, overwrite=False): Saves the current script instance to a text file in a structured format. Includes metadata, global parameters, definitions, templates, and attributes. write(scriptcontent, filename=None, foldername=None, overwrite=False): Writes the provided script content to a specified file in a given folder, with a header added if necessary, ensuring the correct file format. load(cls, filename, foldername=None, numerickeys=True): Loads a script instance from a text file, restoring the content, definitions, templates, and attributes. Handles parsing and variable substitution based on the structure of the file. parsesyntax(cls, content, numerickeys=True): Parses a script instance from a string input, restoring the content, definitions, templates, and attributes. Handles parsing and variable substitution based on the structure of the provided string, ensuring the correct format and key conversions when necessary. Example: -------- # Create a dscript object R = dscript(name="MyScript") # Define global variables (DEFINITIONS) R.DEFINITIONS.dimension = 3 R.DEFINITIONS.units = "$si" # Add script lines R[0] = "dimension ${dimension}" R[1] = "units ${units}" # Generate and print the script sR = R.script() print(sR.do()) Attributes: ----------- name : str The name of the script, useful for identification. TEMPLATE : dict A dictionary storing script lines/items, with keys to identify each line. DEFINITIONS : lambdaScriptdata Stores the variables and parameters used within the script lines/items. """ # Class variable to list attributes that should not be treated as TEMPLATE entries construction_attributes = {'name', 'SECTIONS', 'section', 'position', 'role', 'description', 'userid', 'verbose', 'printflag', 'DEFINITIONS', 'TEMPLATE', 'version','license','email' } def __init__(self, name=None, SECTIONS = ["DYNAMIC"], section = 0, position = None, role = "dscript instance", description = "dynamic script", userid = "dscript", version = None, license = None, email = None, printflag = False, verbose = False, verbosity = None, **userdefinitions ): """ Initializes a new `dscript` object. The constructor sets up a new `dscript` object, which allows you to define and manage a script composed of multiple lines/items. Each line is stored in the `TEMPLATE` dictionary, and variables used in the script are stored in `DEFINITIONS`. Parameters: ----------- name : str, optional The name of the script. If no name is provided, a random name will be generated automatically. The name is useful for identifying the script, especially when managing multiple scripts. Example: -------- # Create a dscript object with a specific name R = dscript(name="ExampleScript") # Or create a dscript object with a random name R = dscript() After initialization, you can start adding script lines/items and defining variables. """ if name is None: self.name = autoname() else: self.name = name if (version is None) or (license is None) or (email is None): metadata = get_metadata() # retrieve all metadata version = metadata["version"] if version is None else version license = metadata["license"] if license is None else license email = metadata["email"] if email is None else email self.SECTIONS = SECTIONS if isinstance(SECTIONS,(list,tuple)) else [SECTIONS] self.section = section self.position = position if position is not None else 0 self.role = role self.description = description self.userid = userid self.version = version self.license = license self.email = email self.printflag = printflag self.verbose = verbose if verbosity is None else verbosity>0 self.verbosity = 0 if not verbose else verbosity self.DEFINITIONS = lambdaScriptdata(**userdefinitions) self.TEMPLATE = {} def __getattr__(self, attr): # During construction phase, we only access the predefined attributes if 'TEMPLATE' not in self.__dict__: if attr in self.__dict__: return self.__dict__[attr] raise AttributeError(f"'dscript' object has no attribute '{attr}'") # If TEMPLATE is initialized and attr is in TEMPLATE, return the corresponding ScriptTemplate entry if attr in self.TEMPLATE: return self.TEMPLATE[attr] # Fall back to internal __dict__ attributes if not in TEMPLATE if attr in self.__dict__: return self.__dict__[attr] raise AttributeError(f"'dscript' object has no attribute '{attr}'") def __setattr__(self, attr, value): # Handle internal attributes during the construction phase if 'TEMPLATE' not in self.__dict__: self.__dict__[attr] = value return # Handle construction attributes separately (name, TEMPLATE, USER) if attr in self.construction_attributes: self.__dict__[attr] = value # If TEMPLATE exists, and the attribute is intended for it, update TEMPLATE elif 'TEMPLATE' in self.__dict__: # Convert the value to a ScriptTemplate if needed, and update the template if attr in self.TEMPLATE: # Modify the existing ScriptTemplate object for this attribute if isinstance(value, str): self.TEMPLATE[attr].content = value elif isinstance(value, dict): self.TEMPLATE[attr].attributes.update(value) else: # Create a new entry if it does not exist self.TEMPLATE[attr] = ScriptTemplate(content=value,definitions=self.DEFINITIONS,verbose=self.verbose,userid=attr) else: # Default to internal attributes self.__dict__[attr] = value def __getitem__(self, key): """ Implements index-based retrieval, slicing, or reordering for dscript objects. Parameters: ----------- key : int, slice, list, or str - If `key` is an int, returns the corresponding template at that index. Supports negative indices to retrieve templates from the end. - If `key` is a slice, returns a new `dscript` object containing the templates in the specified range. - If `key` is a list, reorders the TEMPLATE based on the list of indices or keys. - If `key` is a string, treats it as a key and returns the corresponding template. Returns: -------- dscript or ScriptTemplate : Depending on the type of key, returns either a new dscript object (for slicing or reordering) or a ScriptTemplate object (for direct key access). """ # Handle list-based reordering if isinstance(key, list): return self.reorder(key) # Handle slicing elif isinstance(key, slice): new_dscript = dscript(name=f"{self.name}_slice_{key.start}_{key.stop}") keys = list(self.TEMPLATE.keys()) for k in keys[key]: new_dscript.TEMPLATE[k] = self.TEMPLATE[k] return new_dscript # Handle integer indexing with support for negative indices elif isinstance(key, int): keys = list(self.TEMPLATE.keys()) if key in self.TEMPLATE: # Check if the integer exists as a key return self.TEMPLATE[key] if key < 0: # Support negative indices key += len(keys) if key < 0 or key >= len(keys): # Check for index out of range raise IndexError(f"Index {key - len(keys)} is out of range") # Return the template corresponding to the integer index return self.TEMPLATE[keys[key]] # Handle key-based access (string keys) elif isinstance(key, str): if key in self.TEMPLATE: return self.TEMPLATE[key] raise KeyError(f"Key '{key}' does not exist in TEMPLATE.") def __setitem__(self, key, value): if (value == []) or (value is None): # If the value is an empty list, delete the corresponding key del self.TEMPLATE[key] else: # Otherwise, set the key to the new ScriptTemplate self.TEMPLATE[key] = ScriptTemplate(value, definitions=self.DEFINITIONS, verbose=self.verbose, userid=key) def __delitem__(self, key): del self.TEMPLATE[key] def __iter__(self): return iter(self.TEMPLATE.items()) # def keys(self): # return self.TEMPLATE.keys() # def values(self): # return (s.content for s in self.TEMPLATE.values()) def __contains__(self, key): return key in self.TEMPLATE def __len__(self): return len(self.TEMPLATE) def items(self): return ((key, s.content) for key, s in self.TEMPLATE.items()) def __str__(self): num_TEMPLATE = len(self.TEMPLATE) total_attributes = sum(len(s.attributes) for s in self.TEMPLATE.values()) return f"{num_TEMPLATE} TEMPLATE, {total_attributes} attributes" def __repr__(self): """Representation of dscript object with additional properties.""" repr_str = f"dscript object ({self.name})\n" # Add description, role, and version at the beginning repr_str += f"id: {self.userid}\n" if self.userid else "" repr_str += f"Descr: {self.description}\n" if self.description else "" repr_str += f"Role: {self.role} (v. {self.version})\n" repr_str += f'SECTIONS {span(self.SECTIONS,",","[","]")} | index: {self.section} | position: {self.position}\n' repr_str += f"\n\n\twith {len(self.TEMPLATE)} TEMPLATE" repr_str += "s" if len(self.TEMPLATE)>1 else "" repr_str += f" (with {len(self.DEFINITIONS)} DEFINITIONS)\n\n" # Add TEMPLATE information c = 0 for k, s in self.TEMPLATE.items(): head = f"| idx: {c} | key: {k} |" dashes = (50 - len(head)) // 2 repr_str += "-" * 50 + "\n" repr_str += f"{'<' * dashes}{head}{'>' * dashes}\n{repr(s)}\n" c += 1 return repr_str def keys(self): """Return the keys of the TEMPLATE.""" return self.TEMPLATE.keys() def values(self): """Return the ScriptTemplate objects in TEMPLATE.""" return self.TEMPLATE.values() def reorder(self, order): """Reorder the TEMPLATE lines according to a list of indices.""" # Get the original items as a list of (key, value) pairs original_items = list(self.TEMPLATE.items()) # Create a new dictionary with reordered scripts, preserving original keys new_scripts = {original_items[i][0]: original_items[i][1] for i in order} # Create a new dscript object with reordered scripts reordered_script = dscript() reordered_script.TEMPLATE = new_scripts return reordered_script def get_content_by_index(self, index, do=True, protected=True): """ Returns the content of the ScriptTemplate at the specified index. Parameters: ----------- index : int The index of the template in the TEMPLATE dictionary. do : bool, optional (default=True) If True, the content will be processed based on conditions and evaluation flags. protected : bool, optional (default=True) Controls whether variable evaluation is protected (e.g., prevents overwriting certain definitions). Returns: -------- str or list of str The content of the template after processing, or an empty string if conditions or evaluation flags block it. """ key = list(self.TEMPLATE.keys())[index] s = self.TEMPLATE[key].content att = self.TEMPLATE[key].attributes # Return an empty string if the facultative attribute is True and do is True if att["facultative"] and do: return "" # Evaluate the condition (if any) if att["condition"] is not None: cond = eval(self.DEFINITIONS.formateval(att["condition"], protected)) else: cond = True # If the condition is met, process the content if cond: # Apply formateval only if the eval attribute is True and do is True if att["eval"] and do: if isinstance(s, list): # Apply formateval to each item in the list if s is a list return [self.DEFINITIONS.formateval(line, protected) for line in s] else: # Apply formateval to the single string content return self.DEFINITIONS.formateval(s, protected) else: return s # Return the raw content if no evaluation is needed elif do: return "" # Return an empty string if the condition is not met and do is True else: return s # Return the raw content if do is False def get_attributes_by_index(self, index): """ Returns the attributes of the ScriptTemplate at the specified index.""" key = list(self.TEMPLATE.keys())[index] return self.TEMPLATE[key].attributes def __add__(self, other): """ Concatenates two dscript objects, creating a new dscript object that combines the TEMPLATE and DEFINITIONS of both. This operation avoids deep copying of definitions by creating a new lambdaScriptdata instance from the definitions. Parameters: ----------- other : dscript The other dscript object to concatenate with the current one. Returns: -------- result : dscript A new dscript object with the concatenated TEMPLATE and merged DEFINITIONS. Raises: ------- TypeError: If the other object is not an instance of dscript, script, pipescript """ if isinstance(other, dscript): # Step 1: Merge global DEFINITIONS from both self and other result = dscript(name=self.name+"+"+other.name,**(self.DEFINITIONS + other.DEFINITIONS)) # Step 2: Start by copying the TEMPLATE from self (no deepcopy for performance reasons) result.TEMPLATE = self.TEMPLATE.copy() # Step 3: Ensure that the local definitions for `self.TEMPLATE` are properly copied for key, value in self.TEMPLATE.items(): result.TEMPLATE[key].definitions = lambdaScriptdata(**self.TEMPLATE[key].definitions) # Step 4: Track the next available index if keys need to be created next_index = len(result.TEMPLATE) # Step 5: Add items from the other dscript object, updating or generating new keys as needed for key, value in other.TEMPLATE.items(): if key in result.TEMPLATE: # If key already exists in result, assign a new unique index while next_index in result.TEMPLATE: next_index += 1 new_key = next_index else: # Use the original key if it doesn't already exist new_key = key # Copy the TEMPLATE's content and definitions from `other` result.TEMPLATE[new_key] = value # Merge the local TEMPLATE definitions from `other` result.TEMPLATE[new_key].definitions = lambdaScriptdata(**other.TEMPLATE[key].definitions) return result elif isinstance(other,script): return self.script() + script elif isinstance(other,pipescript): return self.pipescript() | script else: raise TypeError(f"Cannot concatenate 'dscript' with '{type(other).__name__}'") def __call__(self, *keys): """ Extracts subobjects from the dscript based on the provided keys. Parameters: ----------- *keys : one or more keys that correspond to the `TEMPLATE` entries. Returns: -------- A new `dscript` object that contains only the selected script lines/items, along with the relevant definitions and attributes from the original object. """ # Create a new dscript object to store the extracted sub-objects result = dscript(name=f"{self.name}_subobject") # Copy the TEMPLATE entries corresponding to the provided keys for key in keys: if key in self.TEMPLATE: result.TEMPLATE[key] = self.TEMPLATE[key] else: raise KeyError(f"Key '{key}' not found in TEMPLATE.") # Copy the DEFINITIONS from the current object result.DEFINITIONS = copy.deepcopy(self.DEFINITIONS) # Copy other relevant attributes result.SECTIONS = self.SECTIONS[:] result.section = self.section result.position = self.position result.role = self.role result.description = self.description result.userid = self.userid result.version = self.version result.verbose = self.verbose result.printflag = self.printflag return result def createEmptyVariables(self, vars): """ Creates empty variables in DEFINITIONS if they don't already exist. Parameters: ----------- vars : str or list of str The variable name or list of variable names to be created in DEFINITIONS. """ if isinstance(vars, str): vars = [vars] # Convert single variable name to list for uniform processing for varname in vars: if varname not in self.DEFINITIONS: self.DEFINITIONS.setattr(varname,"${" + varname + "}") def do(self, printflag=None, verbose=None, softrun=False, return_definitions=False,comment_chars="#%", **USER): """ Executes or previews all `ScriptTemplate` instances in `TEMPLATE`, concatenating their processed content. Allows for optional headers and footers based on verbosity settings, and offers a preliminary preview mode with `softrun`. Accumulates definitions across all templates if `return_definitions=True`. Parameters ---------- printflag : bool, optional If `True`, enables print output during execution. Defaults to the instance's print flag if `None`. verbose : bool, optional If `True`, includes headers and footers in the output, providing additional detail. Defaults to the instance's verbosity setting if `None`. softrun : bool, optional If `True`, executes the script in a preliminary mode: - Bypasses full variable substitution for a preview of the content, useful for validating structure. - If `False` (default), performs full processing, including variable substitutions and evaluations. return_definitions : bool, optional If `True`, returns a tuple where the second element contains accumulated definitions from all templates. If `False` (default), returns only the concatenated output. comment_chars : str, optional (default: "#%") A string containing characters to identify the start of a comment. Any of these characters will mark the beginning of a comment unless within quotes. **USER : keyword arguments Allows for the provision of additional user-defined definitions, where each keyword represents a definition key and the associated value represents the definition's content. These definitions can override or supplement template-level definitions during execution. Returns ------- str or tuple - If `return_definitions=False`, returns the concatenated output of all `ScriptTemplate` instances, with optional headers, footers, and execution summary based on verbosity. - If `return_definitions=True`, returns a tuple of (`output`, `accumulated_definitions`), where `accumulated_definitions` contains all definitions used across templates. Notes ----- - Each `ScriptTemplate` in `TEMPLATE` is processed individually using its own `do()` method. - The `softrun` mode provides a preliminary content preview without full variable substitution, helpful for inspecting the script structure or gathering local definitions. - When `verbose` is enabled, the method includes detailed headers, footers, and a summary of processed and ignored items, providing insight into the script's construction and variable usage. - Accumulated definitions from each `ScriptTemplate` are combined if `return_definitions=True`, which can be useful for tracking all variables and definitions applied across the templates. Example ------- >>> dscript_instance = dscript(name="ExampleScript") >>> dscript_instance.TEMPLATE[0] = ScriptTemplate( ... content=["units ${units}", "boundary ${boundary}"], ... definitions=lambdaScriptdata(units="lj", boundary="p p p"), ... attributes={'eval': True} ... ) >>> dscript_instance.do(verbose=True, units="real") # Output: # -------------- # TEMPLATE "ExampleScript" # -------------- units real boundary p p p # ---> Total items: 2 - Ignored items: 0 """ printflag = self.printflag if printflag is None else printflag verbose = self.verbose if verbose is None else verbose header = f"# --------------[ TEMPLATE \"{self.name}\" ]--------------" if verbose else "" footer = "# --------------------------------------------" if verbose else "" # Initialize output, counters, and optional definitions accumulator output = [header] non_empty_lines = 0 ignored_lines = 0 accumulated_definitions = lambdaScriptdata() if return_definitions else None for key, template in self.TEMPLATE.items(): # Process each template with softrun if enabled, otherwise use full processing result = template.do(softrun=softrun,USER=lambdaScriptdata(**USER)) if result: # Apply comment removal based on verbosity final_result = result if verbose else remove_comments(result,comment_chars=comment_chars) if final_result or verbose: output.append(final_result) non_empty_lines += 1 else: ignored_lines += 1 # Accumulate definitions if return_definitions is enabled if return_definitions: accumulated_definitions += template.definitions else: ignored_lines += 1 # Add footer summary if verbose nel_word = 'items' if non_empty_lines > 1 else 'item' il_word = 'items' if ignored_lines > 1 else 'item' footer += f"\n# ---> Total {nel_word}: {non_empty_lines} - Ignored {il_word}: {ignored_lines}" if verbose else "" output.append(footer) # Concatenate output and determine return type based on return_definitions output_content = "\n".join(output) return (output_content, accumulated_definitions) if return_definitions else output_content def script(self,printflag=None, verbose=None, verbosity=None, **USER): """ returns the corresponding script """ printflag = self.printflag if printflag is None else printflag verbose = verbosity > 0 if verbosity is not None else (self.verbose if verbose is None else verbose) verbosity = 0 if not verbose else verbosity return lamdaScript(self,persistentfile=True, persistentfolder=None, printflag=printflag, verbose=verbose, **USER) def pipescript(self, *keys, printflag=None, verbose=None, verbosity=None, **USER): """ Returns a pipescript object by combining script objects corresponding to the given keys. Parameters: ----------- *keys : one or more keys that correspond to the `TEMPLATE` entries. printflag : bool, optional Whether to enable printing of additional information. verbose : bool, optional Whether to run in verbose mode for debugging or detailed output. **USER : dict, optional Additional user-defined variables to pass into the script. Returns: -------- A `pipescript` object that combines the script objects generated from the selected dscript subobjects. """ # Start with an empty pipescript # combined_pipescript = None # # Iterate over the provided keys to extract corresponding subobjects # for key in keys: # # Extract the dscript subobject for the given key # sub_dscript = self(key) # # Convert the dscript subobject to a script object, passing USER, printflag, and verbose # script_obj = sub_dscript.script(printflag=printflag, verbose=verbose, **USER) # # Combine script objects into a pipescript object # if combined_pipescript is None: # combined_pipescript = pipescript(script_obj) # Initialize pipescript # else: # combined_pipescript = combined_pipescript | script_obj # Use pipe operator # if combined_pipescript is None: # ValueError('The conversion to pipescript from {type{self}} falled') # return combined_pipescript printflag = self.printflag if printflag is None else printflag verbose = verbosity > 0 if verbosity is not None else (self.verbose if verbose is None else verbose) verbosity = 0 if not verbose else verbosity # Loop over all keys in TEMPLATE and combine them combined_pipescript = None for key in self.keys(): # Create a new dscript object with only the current key in TEMPLATE focused_dscript = dscript(name=f"{self.name}:{key}") focused_dscript.TEMPLATE[key] = self.TEMPLATE[key] focused_dscript.TEMPLATE[key].definitions = scriptdata(**self.TEMPLATE[key].definitions) focused_dscript.DEFINITIONS = scriptdata(**self.DEFINITIONS) focused_dscript.SECTIONS = self.SECTIONS[:] focused_dscript.section = self.section focused_dscript.position = self.position focused_dscript.role = self.role focused_dscript.description = self.description focused_dscript.userid = self.userid focused_dscript.version = self.version focused_dscript.verbose = verbose focused_dscript.printflag = printflag # Convert the focused dscript object to a script object script_obj = focused_dscript.script(printflag=printflag, verbose=verbose, **USER) # Combine the script objects into a pipescript object using the pipe operator if combined_pipescript is None: combined_pipescript = pipescript(script_obj) # Initialize pipescript else: combined_pipescript = combined_pipescript | pipescript(script_obj) # Use pipe operator if combined_pipescript is None: ValueError('The conversion to pipescript from {type{self}} falled') return combined_pipescript @staticmethod def header(name=None, verbose=True, verbosity=None, style=2, filepath=None, version=None, license=None, email=None): """ Generate a formatted header for the DSCRIPT file. ### Parameters: name (str, optional): The name of the script. If None, "Unnamed" is used. verbose (bool, optional): Whether to include the header. Default is True. verbosity (int, optional): Verbosity level. Overrides `verbose` if specified. style (int, optional): ASCII style for the header (default=2). filepath (str, optional): Full path to the file being saved. If None, the line mentioning the file path is excluded. version (str, optional): DSCRIPT version. If None, it is omitted from the header. license (str, optional): License type. If None, it is omitted from the header. email (str, optional): Contact email. If None, it is omitted from the header. ### Returns: str: A formatted string representing the script's metadata and initialization details. Returns an empty string if `verbose` is False. ### The header includes: - DSCRIPT version, license, and contact email, if provided. - The name of the script. - Filepath, if provided. - Information on where and when the script was generated. ### Notes: - If `verbosity` is specified, it overrides `verbose`. - Omits metadata lines if `version`, `license`, or `email` are not provided. """ # Resolve verbosity verbose = verbosity > 0 if verbosity is not None else verbose if not verbose: return "" # Validate inputs if name is None: name = "Unnamed" # Prepare metadata line metadata = [] if version: metadata.append(f"v{version}") if license: metadata.append(f"License: {license}") if email: metadata.append(f"Email: {email}") metadata_line = " | ".join(metadata) # Prepare the framed header content lines = [] if metadata_line: lines.append(f"PIZZA.DSCRIPT FILE {metadata_line}") lines += [ "", f"Name: {name}", ] # Add the filepath line if filepath is not None if filepath: lines.append(f"Path: {filepath}") lines += [ "", f"Generated on: {getpass.getuser()}@{socket.gethostname()}:{os.getcwd()}", f"{datetime.datetime.now().strftime('%A, %B %d, %Y at %H:%M:%S')}", ] # Use the shared frame_header function to format the framed content return frame_header(lines, style=style) # Generator -- added on 2024-10-17 # ----------- def generator(self): """ Returns ------- STR generated code corresponding to dscript (using dscript syntax/language). """ return self.save(generatoronly=True) # Save Method -- added on 2024-09-04 # ----------- def save(self, filename=None, foldername=None, overwrite=False, generatoronly=False, onlyusedvariables=True): """ Save the current script instance to a text file. Parameters ---------- filename : str, optional The name of the file to save the script to. If not provided, `self.name` is used. The extension ".txt" is automatically appended if not included. foldername : str, optional The directory where the file will be saved. If not provided, it defaults to the system's temporary directory. If the filename does not include a full path, this folder will be used. overwrite : bool, default=True Whether to overwrite the file if it already exists. If set to False, an exception is raised if the file exists. generatoronly : bool, default=False If True, the method returns the generated content string without saving to a file. onlyusedvariables : bool, default=True If True, local definitions are only saved if they are used within the template content. If False, all local definitions are saved, regardless of whether they are referenced in the template. Raises ------ FileExistsError If the file already exists and `overwrite` is set to False. Notes ----- - The script is saved in a plain text format, and each section (global parameters, definitions, template, and attributes) is written in a structured format with appropriate comments. - If `self.name` is used as the filename, it must be a valid string that can serve as a file name. - The file structure follows the format: # DSCRIPT SAVE FILE # generated on YYYY-MM-DD on user@hostname # GLOBAL PARAMETERS { ... } # DEFINITIONS (number of definitions=...) key=value # TEMPLATES (number of items=...) key: template_content # ATTRIBUTES (number of items with explicit attributes=...) key:{attr1=value1, attr2=value2, ...} """ # At the beginning of the save method start_time = time.time() # Start the timer if not generatoronly: # Use self.name if filename is not provided if filename is None: filename = span(self.name, sep="\n") # Ensure the filename ends with '.txt' if not filename.endswith('.txt'): filename += '.txt' # Construct the full path if foldername in [None, ""]: # Handle cases where foldername is None or an empty string filepath = os.path.abspath(filename) else: filepath = os.path.join(foldername, filename) # Check if the file already exists, and raise an exception if it does and overwrite is False if os.path.exists(filepath) and not overwrite: raise FileExistsError(f"The file '{filepath}' already exists.") # Header with current date, username, and host header = "# DSCRIPT SAVE FILE\n" header += "\n"*2 if generatoronly: header += dscript.header(verbose=True,filepath='dynamic code generation (no file)', name = self.name, version=self.version, license=self.license, email=self.email) else: header += dscript.header(verbose=True,filepath=filepath, name = self.name, version=self.version, license=self.license, email=self.email) header += "\n"*2 # Global parameters in strict Python syntax global_params = "# GLOBAL PARAMETERS (8 parameters)\n" global_params += "{\n" global_params += f" SECTIONS = {self.SECTIONS},\n" global_params += f" section = {self.section},\n" global_params += f" position = {self.position},\n" global_params += f" role = {self.role!r},\n" global_params += f" description = {self.description!r},\n" global_params += f" userid = {self.userid!r},\n" global_params += f" version = {self.version},\n" global_params += f" verbose = {self.verbose}\n" global_params += "}\n" # Initialize definitions with self.DEFINITIONS allvars = self.DEFINITIONS # Temporary dictionary to track global variable information global_var_info = {} # Loop over each template item to detect and record variable usage and overrides for template_index, (key, script_template) in enumerate(self.TEMPLATE.items()): # Detect variables used in this template used_variables = script_template.detect_variables() # Loop over each template item to detect and record variable usage and overrides for template_index, (key, script_template) in enumerate(self.TEMPLATE.items()): # Detect variables used in this template used_variables = script_template.detect_variables() # Check each variable used in this template for var in used_variables: # Get global and local values for the variable global_value = getattr(allvars, var, None) local_value = getattr(script_template.definitions, var, None) is_global = var in allvars # Check if the variable originates in global space is_default = is_global and ( global_value is None or global_value == "" or global_value == f"${{{var}}}" ) # If the variable is not yet tracked, initialize its info if var not in global_var_info: global_var_info[var] = { "value": global_value, # Initial value from allvars if exists "updatedvalue": global_value,# Initial value from allvars if exists "is_default": is_default, # Check if it’s set to a default value "first_def": None, # First definition (to be updated later) "first_use": template_index, # First time the variable is used "first_val": global_value, "override_index": template_index if local_value is not None else None, # Set override if defined locally "is_global": is_global # Track if the variable originates as global } else: # Update `override_index` if the variable is defined locally and its value changes if local_value is not None: # Check if the local value differs from the tracked value in global_var_info current_value = global_var_info[var]["value"] if current_value != local_value: global_var_info[var]["override_index"] = template_index global_var_info[var]["updatedvalue"] = local_value # Update the tracked value # Second loop: Update `first_def` for all variables for template_index, (key, script_template) in enumerate(self.TEMPLATE.items()): local_definitions = script_template.definitions.keys() for var in local_definitions: if var in global_var_info and global_var_info[var]["first_def"] is None: global_var_info[var]["first_def"] = template_index global_var_info[var]["first_val"] = getattr(script_template.definitions, var) # Filter global definitions based on usage, overrides, and first_def filtered_globals = { var: info for var, info in global_var_info.items() if (info["is_global"] or info["is_default"]) and (info["override_index"] is None) } # Generate the definitions output based on filtered globals definitions = f"\n# GLOBAL DEFINITIONS (number of definitions={len(filtered_globals)})\n" for var, info in filtered_globals.items(): if info["is_default"] and (info["first_def"]>info["first_use"] if info["first_def"] else True): definitions += f"{var} = ${{{var}}} # value assumed to be defined outside this DSCRIPT file\n" else: value = info["first_val"] #info["value"] if value in ["", None]: definitions += f'{var} = ""\n' elif isinstance(value, str): safe_value = value.replace('\\', '\\\\').replace('\n', '\\n') definitions += f"{var} = {safe_value}\n" else: definitions += f"{var} = {value}\n" # Template (number of lines/items) printsinglecontent = False template = f"\n# TEMPLATES (number of items={len(self.TEMPLATE)})\n" for key, script_template in self.TEMPLATE.items(): # Get local template definitions and detected variables template_vars = script_template.definitions used_variables = script_template.detect_variables() islocal = False # Temporary dictionary to accumulate variables to add to allvars valid_local_vars = lambdaScriptdata() # Write template-specific definitions only if they meet the updated conditions for var in template_vars.keys(): # Conditions for adding a variable to the local template and to `allvars` if (var in used_variables or not onlyusedvariables) and ( script_template.is_variable_set_value_only(var) and (var not in allvars or getattr(template_vars, var) != getattr(allvars, var)) ): # Start local definitions section if this is the first local variable for the template if not islocal: template += f"\n# LOCAL DEFINITIONS for key '{key}'\n" islocal = True # Retrieve and process the variable value value = getattr(template_vars, var) if value in ["", None]: template += f'{var} = ""\n' # Set empty or None values as "" elif isinstance(value, str): safe_value = value.replace('\\', '\\\\').replace('\n', '\\n') template += f"{var} = {safe_value}\n" else: template += f"{var} = {value}\n" # Add the variable to valid_local_vars for selective update of allvars valid_local_vars.setattr(var, value) # Update allvars only with filtered, valid local variables allvars += valid_local_vars # Write the template content if isinstance(script_template.content, list): if len(script_template.content) == 1: # Single-line template saved as a single line content_str = script_template.content[0].strip() template += "" if printsinglecontent else "\n" template += f"{key}: {content_str}\n" printsinglecontent = True else: content_str = '\n '.join(script_template.content) template += f"\n{key}: [\n {content_str}\n ]\n" printsinglecontent = False else: template += "" if printsinglecontent else "\n" template += f"{key}: {script_template.content}\n" printsinglecontent = True # Attributes (number of lines/items with explicit attributes) attributes = f"# ATTRIBUTES (number of items with explicit attributes={len(self.TEMPLATE)})\n" for key, script_template in self.TEMPLATE.items(): attr_str = ", ".join(f"{attr_name}={repr(attr_value)}" for attr_name, attr_value in script_template.attributes.items()) attributes += f"{key}:{{{attr_str}}}\n" # Combine all sections into one content content = header + "\n" + global_params + "\n" + definitions + "\n" + template + "\n" + attributes + "\n" # Append footer information to the content non_empty_lines = sum(1 for line in content.splitlines() if line.strip()) # Count non-empty lines execution_time = time.time() - start_time # Calculate the execution time in seconds # Prepare the footer content footer_lines = [ ["Non-empty lines", str(non_empty_lines)], ["Execution time (seconds)", f"{execution_time:.4f}"], ] # Format footer into tabular style footer_content = [ f"{row[0]:<25} {row[1]:<15}" for row in footer_lines ] # Use frame_header to format footer footer = frame_header( lines=["DSCRIPT SAVE FILE generator"] + footer_content, style=1 ) # Append footer to the content content += f"\n{footer}" if generatoronly: return content else: # Write the content to the file with open(filepath, 'w') as f: f.write(content) print(f"\nScript saved to {filepath}") return filepath # Write Method -- added on 2024-09-05 # ------------ @staticmethod @staticmethod def write(scriptcontent, filename=None, foldername=None, overwrite=False): """ Writes the provided script content to a specified file in a given folder, with a header if necessary. Parameters ---------- scriptcontent : str The content to be written to the file. filename : str, optional The name of the file. If not provided, a random name will be generated. The extension `.txt` will be appended if not already present. foldername : str, optional The folder where the file will be saved. If not provided, the current working directory is used. overwrite : bool, optional If False (default), raises a `FileExistsError` if the file already exists. If True, the file will be overwritten if it exists. Returns ------- str The full path to the written file. Raises ------ FileExistsError If the file already exists and `overwrite` is set to False. Notes ----- - A header is prepended to the content if it does not already exist, using the `header` method. - The header includes metadata such as the current date, username, hostname, and file details. """ # Generate a random name if filename is not provided if filename is None: filename = autoname(8) # Generates a random name of 8 letters # Ensure the filename ends with '.txt' if not filename.endswith('.txt'): filename += '.txt' # Handle foldername and relative paths if foldername is None or foldername == "": # If foldername is empty or None, use current working directory for relative paths if not os.path.isabs(filename): filepath = os.path.join(os.getcwd(), filename) else: filepath = filename # If filename is absolute, use it directly else: # If foldername is provided and filename is not absolute, use foldername if not os.path.isabs(filename): filepath = os.path.join(foldername, filename) else: filepath = filename # Check if file already exists, raise exception if it does and overwrite is False if os.path.exists(filepath) and not overwrite: raise FileExistsError(f"The file '{filepath}' already exists.") # Count total and non-empty lines in the content total_lines = len(scriptcontent.splitlines()) non_empty_lines = sum(1 for line in scriptcontent.splitlines() if line.strip()) # Prepare header if not already present if not scriptcontent.startswith("# DSCRIPT SAVE FILE"): fname = os.path.basename(filepath) # Extracts the filename (e.g., "myscript.txt") name, _ = os.path.splitext(fname) # Removes the extension, e.g., "myscript" metadata = get_metadata() # retrieve all metadata (statically) header = dscript.header(name=name, verbosity=True,style=1,filepath=filepath, version = metadata["version"], license = metadata["license"], email = metadata["email"]) # Add line count information to the header footer = frame_header( lines=[ f"Total lines written: {total_lines}", f"Non-empty lines: {non_empty_lines}" ], style=1 ) scriptcontent = header + "\n" + scriptcontent + "\n" + footer # Write the content to the file with open(filepath, 'w') as file: file.write(scriptcontent) return filepath # Load Method and its Parsing Rules -- added on 2024-09-04 # --------------------------------- @classmethod def load(cls, filename, foldername=None, numerickeys=True): """ Load a script instance from a text file. Parameters ---------- filename : str The name of the file to load the script from. If the filename does not end with ".txt", the extension is automatically appended. foldername : str, optional The directory where the file is located. If not provided, it defaults to the system's temporary directory. If the filename does not include a full path, this folder will be used. numerickeys : bool, default=True If True, numeric string keys in the template section are automatically converted into integers. For example, the key "0" would be converted into the integer 0. Returns ------- dscript A new `dscript` instance populated with the content of the loaded file. Raises ------ ValueError If the file does not start with the correct DSCRIPT header or the file format is invalid. FileNotFoundError If the specified file does not exist. Notes ----- - The file is expected to follow the same structured format as the one produced by the `save()` method. - The method processes global parameters, definitions, template lines/items, and attributes. If the file includes numeric keys as strings (e.g., "0", "1"), they can be automatically converted into integers if `numerickeys=True`. - The script structure is dynamically rebuilt, and each section (global parameters, definitions, template, and attributes) is correctly parsed and assigned to the corresponding parts of the `dscript` instance. """ # Step 0 validate filepath if not filename.endswith('.txt'): filename += '.txt' # Handle foldername and relative paths if foldername is None or foldername == "": # If the foldername is empty or None, use current working directory for relative paths if not os.path.isabs(filename): filepath = os.path.join(os.getcwd(), filename) else: filepath = filename # If filename is absolute, use it directly else: # If foldername is provided and filename is not absolute, use foldername if not os.path.isabs(filename): filepath = os.path.join(foldername, filename) else: filepath = filename if not os.path.exists(filepath): raise FileExistsError(f"The file '{filepath}' does not exist.") # Read the file contents with open(filepath, 'r') as f: content = f.read() # Call parsesyntax to parse the file content fname = os.path.basename(filepath) # Extracts the filename (e.g., "myscript.txt") name, _ = os.path.splitext(fname) # Removes the extension, e.g., "myscript return cls.parsesyntax(content, name, numerickeys) # Load Method and its Parsing Rules -- added on 2024-09-04 # --------------------------------- @classmethod def parsesyntax(cls, content, name=None, numerickeys=True, verbose=False, authentification=True, comment_chars="#%",continuation_marker="..."): """ Parse a DSCRIPT script from a string content. Parameters ---------- content : str The string content of the DSCRIPT script to be parsed. name : str, optional The name of the dscript project. If `None`, a random name is generated. numerickeys : bool, default=True If `True`, numeric string keys in the template section are automatically converted into integers. verbose : bool, default=False If `True`, the parser will output warnings for unrecognized lines outside of blocks. authentification : bool, default=True If `True`, the parser is expected that the first non empty line is # DSCRIPT SAVE FILE comment_chars : str, optional (default: "#%") A string containing characters to identify the start of a comment. Any of these characters will mark the beginning of a comment unless within quotes. continuation_marker : str, optional (default: "...") A string containing characters to indicate line continuation Any characters after the continuation marker are considered comment and are theorefore ignored Returns ------- dscript A new `dscript` instance populated with the content of the loaded file. Raises ------ ValueError If content does not start with the correct DSCRIPT header or the file format is invalid. Notes ----- **DSCRIPT SAVE FILE FORMAT** This script syntax is designed for creating dynamic and customizable input files, where variables, templates, and control attributes can be defined in a flexible manner. **Mandatory First Line:** Every DSCRIPT file must begin with the following line: ```plaintext # DSCRIPT SAVE FILE ``` **Structure Overview:** 1. **Global Parameters Section (Optional):** - This section defines global script settings, enclosed within curly braces `{}`. - Properties include: - `SECTIONS`: List of section names to be considered (e.g., `["DYNAMIC"]`). - `section`: Current section index (e.g., `0`). - `position`: Current script position in the order. - `role`: Defines the role of the script instance (e.g., `"dscript instance"`). - `description`: A short description of the script (e.g., `"dynamic script"`). - `userid`: Identifier for the user (e.g., `"dscript"`). - `version`: Script version (e.g., `0.1`). - `verbose`: Verbosity flag, typically a boolean (e.g., `False`). **Example:** ```plaintext { SECTIONS = ['INITIALIZATION', 'SIMULATION'] # Global script parameters } ``` 2. **Definitions Section:** - Variables are defined in Python-like syntax, allowing for dynamic variable substitution. - Variables can be numbers, strings, or lists, and they can include placeholders using `$` to delay execution or substitution. **Example:** ```plaintext d = 3 # Define a number periodic = "$p" # '$' prevents immediate evaluation of 'p' units = "$metal" # '$' prevents immediate evaluation of 'metal' dimension = "${d}" # Variable substitution boundary = ['p', 'p', 'p'] # List with a mix of variables and values atom_style = "$atomic" # String variable with delayed evaluation ``` 3. **Templates Section:** - This section provides a mapping between keys and their corresponding commands or instructions. - Each template can reference variables defined in the **Definitions** section or elsewhere, typically using the `${variable}` syntax. - **Syntax Variations**: Templates can be defined in several ways, including without blocks, with single- or multi-line blocks, and with ellipsis (`...`) as a line continuation marker. - **Single-line Template Without Block**: ```plaintext KEY: INSTRUCTION ``` - `KEY` is the identifier for the template (numeric or alphanumeric). - `INSTRUCTION` is the command or template text, which may reference variables. - **Single-line Template With Block**: ```plaintext KEY: [INSTRUCTION] ``` - Uses square brackets (`[ ]`) around the `INSTRUCTION`, indicating that all instructions are part of the block. - **Multi-line Template With Block**: ```plaintext KEY: [ INSTRUCTION1 INSTRUCTION2 ... ] ``` - Begins with `KEY: [` and ends with a standalone `]` on a new line. - Instructions within the block can span multiple lines, and ellipses (`...`) at the end of a line are used to indicate that the line continues, ignoring any content following the ellipsis as comments. - Comments following ellipses are removed after parsing and do not become part of the block, preserving only the instructions. - **Multi-line Template With Continuation Marker (Ellipsis)**: - For templates with complex code containing square brackets (`[ ]`), the ellipsis (`...`) can be used to prevent `]` from prematurely closing the block. The ellipsis will keep the line open across multiple lines, allowing brackets in the instructions. **Example:** ```plaintext example1: command ${value} # Single-line template without block example2: [command ${value}] # Single-line template with block # Multi-line template with block example3: [ command1 ${var1} command2 ${var2} ... # Line continues after ellipsis command3 ${var3} ... # Additional instruction continues ] # Multi-line template with ellipsis (handling square brackets) example4: [ A[0][1] ... # Ellipsis allows [ ] within instructions B[2][3] ... # Another instruction in the block ] ``` - **Key Points**: - **Blocks** allow grouping of multiple instructions for a single key, enclosed in square brackets. - **Ellipsis (`...`)** at the end of a line keeps the line open, preventing premature closing by `]`, especially useful if the template code includes square brackets (`[ ]`). - **Comments** placed after the ellipsis are removed after parsing and are not part of the final block content. This flexibility supports both simple and complex template structures, allowing instructions to be grouped logically while keeping code and comments distinct. 4. **Attributes Section:** - Each template line can have customizable attributes to control behavior and conditions. - Default attributes include: - `facultative`: If `True`, the line is optional and can be removed if needed. - `eval`: If `True`, the line will be evaluated with Python's `eval()` function. - `readonly`: If `True`, the line cannot be modified later in the script. - `condition`: An expression that must be satisfied for the line to be included. - `condeval`: If `True`, the condition will be evaluated using `eval()`. - `detectvar`: If `True`, this creates variables in the **Definitions** section if they do not exist. **Example:** ```plaintext units: {facultative=False, eval=False, readonly=False, condition="${units}", condeval=False, detectvar=True} dim: {facultative=False, eval=False, readonly=False, condition=None, condeval=False, detectvar=True} ``` **Note on Multiple Definitions** This example demonstrates how variables defined in the **Definitions** section are handled for each template. Each template retains its own snapshot of the variable definitions at the time it is created, ensuring that templates can use different values for the same variable if redefined. **Example:** ```plaintext # DSCRIPT SAVE FILE # Definitions var = 10 # Template key1 key1: Template content with ${var} # Definitions var = 20 # Template key2 key2: Template content with ${var} # Template key3 key3:[ this is an undefined variable ${var31} this is another undefined variable ${var32} this variable is defined ${var} ] ``` **Parsing and Usage:** ```python # Parse content using parsesyntax() ds = dscript.parsesyntax(content) # Accessing templates and their variables print(ds.TEMPLATE['key1'].text) # Output: Template content with 10 print(ds.TEMPLATE['key2'].text) # Output: Template content with 20 ``` **Handling Undefined Variables:** Variables like `${var31}` and `${var32}` in `key3` are undefined. The parser will handle them based on your substitution logic or raise an error if they are required. **Important Notes:** - The parser processes the script sequentially. Definitions must appear before the templates that use them. - Templates capture the variable definitions at the time they are parsed. Redefining a variable affects only subsequent templates. - Comments outside of blocks are allowed and ignored by the parser. - Content within templates is treated as-is, allowing for any syntax required by the target system (e.g., LAMMPS commands). **Advanced Example** Here's a more advanced example demonstrating the use of global definitions, local definitions, templates, and how to parse and render the template content. ```python content = ''' # GLOBAL DEFINITIONS dumpfile = $dump.LAMMPS dumpdt = 50 thermodt = 100 runtime = 5000 # LOCAL DEFINITIONS for step '0' dimension = 3 units = $si boundary = ['f', 'f', 'f'] atom_style = $smd atom_modify = ['map', 'array'] comm_modify = ['vel', 'yes'] neigh_modify = ['every', 10, 'delay', 0, 'check', 'yes'] newton = $off name = $SimulationBox # This is a comment line outside of blocks # ------------------------------------------ 0: [ % --------------[ Initialization Header (helper) for "${name}" ]-------------- # set a parameter to None or "" to remove the definition dimension ${dimension} units ${units} boundary ${boundary} atom_style ${atom_style} atom_modify ${atom_modify} comm_modify ${comm_modify} neigh_modify ${neigh_modify} newton ${newton} # ------------------------------------------ ] ''' # Parse the content ds = dscript.parsesyntax(content, verbose=True, authentification=False) # Access and print the rendered template print("Template 0 content:") print(ds.TEMPLATE[0].do()) ``` **Explanation:** - **Global Definitions:** Define variables that are accessible throughout the script. - **Local Definitions for Step '0':** Define variables specific to a particular step or template. - **Template Block:** Identified by `0: [ ... ]`, it contains the content where variables will be substituted. - **Comments:** Lines starting with `#` are comments and are ignored by the parser outside of template blocks. **Expected Output:** ``` Template 0 content: # --------------[ Initialization Header (helper) for "SimulationBox" ]-------------- # set a parameter to None or "" to remove the definition dimension 3 units si boundary ['f', 'f', 'f'] atom_style smd atom_modify ['map', 'array'] comm_modify ['vel', 'yes'] neigh_modify ['every', 10, 'delay', 0, 'check', 'yes'] newton off # ------------------------------------------ ``` **Notes:** - The `do()` method renders the template, substituting variables with their defined values. - Variables like `${dimension}` are replaced with their corresponding values defined in the local or global definitions. - The parser handles comments and blank lines appropriately, ensuring they don't interfere with the parsing logic. """ # Split the content into lines lines = content.splitlines() if not lines: raise ValueError("File/Content is empty or only contains blank lines.") # Initialize containers global_params = {} GLOBALdefinitions = lambdaScriptdata() LOCALdefinitions = lambdaScriptdata() template = {} attributes = {} # State variables inside_global_params = False global_params_content = "" inside_template_block = False current_template_key = None current_template_content = [] current_var_value = lambdaScriptdata() # Initialize line number line_number = 0 last_successful_line = 0 # Step 1: Authenticate the file if authentification: auth_line_found = False max_header_lines = 10 header_end_idx = -1 for idx, line in enumerate(lines[:max_header_lines]): stripped_line = line.strip() if not stripped_line: continue if stripped_line.startswith("# DSCRIPT SAVE FILE"): auth_line_found = True header_end_idx = idx break elif stripped_line.startswith("#") or stripped_line.startswith("%"): continue else: raise ValueError(f"Unexpected content before authentication line (# DSCRIPT SAVE FILE) at line {idx + 1}:\n{line}") if not auth_line_found: raise ValueError("File/Content is not a valid DSCRIPT file.") # Remove header lines lines = lines[header_end_idx + 1:] line_number = header_end_idx + 1 last_successful_line = line_number - 1 else: line_number = 0 last_successful_line = 0 # Process each line for idx, line in enumerate(lines): line_number += 1 line_content = line.rstrip('\n') # Determine if we're inside a template block if inside_template_block: # Extract the code with its eventual continuation_marker code_line = remove_comments( line_content, comment_chars=comment_chars, continuation_marker=continuation_marker, remove_continuation_marker=False, ).rstrip() # Check if line should continue if code_line.endswith(continuation_marker): # Append line up to the continuation marker endofline_index = line_content.rindex(continuation_marker) trimmed_content = line_content[:endofline_index].rstrip() if trimmed_content: current_template_content.append(trimmed_content) continue elif code_line.endswith("]"): # End of multi-line block closing_index = code_line.rindex(']') trimmed_content = code_line[:closing_index].rstrip() # Append any valid content before `]`, if non-empty if trimmed_content: current_template_content.append(trimmed_content) # End of template block content = '\n'.join(current_template_content) template[current_template_key] = ScriptTemplate( content=content, autorefresh=False, definitions=LOCALdefinitions, verbose=verbose, userid=current_template_key) # Refresh variables definitions template[current_template_key].refreshvar(globaldefinitions=GLOBALdefinitions) LOCALdefinitions = lambdaScriptdata() # Reset state for next block inside_template_block = False current_template_key = None current_template_content = [] last_successful_line = line_number continue else: # Append the entire original line content if not ending with `...` or `]` current_template_content.append(line_content) continue # Not inside a template block stripped_no_comments = remove_comments(line_content) # Ignore empty lines after removing comments if not stripped_no_comments.strip(): continue # If the original line is a comment line, skip it if line_content.strip().startswith("#") or line_content.strip().startswith("%"): continue stripped = stripped_no_comments.strip() # Handle start of a new template block template_block_match = re.match(r'^(\w+)\s*:\s*\[', stripped) if template_block_match: current_template_key = template_block_match.group(1) if inside_template_block: # Collect error context context_start = max(0, last_successful_line - 3) context_end = min(len(lines), line_number + 2) error_context_lines = lines[context_start:context_end] error_context = "" for i, error_line in enumerate(error_context_lines): line_num = context_start + i + 1 indicator = ">" if line_num == line_number else "*" if line_num == last_successful_line else " " error_context += f"{indicator} {line_num}: {error_line}\n" raise ValueError( f"Template block '{current_template_key}' starting at line {last_successful_line} (*) was not properly closed before starting a new one at line {line_number} (>).\n\n" f"Error context:\n{error_context}" ) else: inside_template_block = True idx_open_bracket = line_content.index('[') remainder = line_content[idx_open_bracket + 1:].strip() if remainder: remainder_code = remove_comments(remainder, comment_chars=comment_chars).rstrip() if remainder_code.endswith("]"): closing_index = remainder_code.rindex(']') content_line = remainder_code[:closing_index].strip() if content_line: current_template_content.append(content_line) content = '\n'.join(current_template_content) template[current_template_key] = ScriptTemplate( content=content, autorefresh=False, definitions=LOCALdefinitions, verbose=verbose, userid=current_template_key) template[current_template_key].refreshvar(globaldefinitions=GLOBALdefinitions) LOCALdefinitions = lambdaScriptdata() inside_template_block = False current_template_key = None current_template_content = [] last_successful_line = line_number continue else: current_template_content.append(remainder) last_successful_line = line_number continue # Handle start of global parameters if stripped.startswith('{') and not inside_global_params: if '}' in stripped: global_params_content = stripped cls._parse_global_params(global_params_content.strip(), global_params) global_params_content = "" last_successful_line = line_number else: inside_global_params = True global_params_content = stripped continue # Handle global parameters inside {...} if inside_global_params: global_params_content += ' ' + stripped if '}' in stripped: inside_global_params = False cls._parse_global_params(global_params_content.strip(), global_params) global_params_content = "" last_successful_line = line_number continue # Handle attributes attribute_match = re.match(r'^(\w+)\s*:\s*\{(.+)\}', stripped) if attribute_match: key, attr_content = attribute_match.groups() attributes[key] = {} cls._parse_attributes(attributes[key], attr_content.strip()) last_successful_line = line_number continue # Handle definitions definition_match = re.match(r'^(\w+)\s*=\s*(.+)', stripped) if definition_match: key, value = definition_match.groups() convertedvalue = cls._convert_value(value) if key in GLOBALdefinitions: if (GLOBALdefinitions.getattr(key) != convertedvalue) or \ (getattr(current_var_value, key) != convertedvalue): LOCALdefinitions.setattr(key, convertedvalue) else: GLOBALdefinitions.setattr(key, convertedvalue) last_successful_line = line_number setattr(current_var_value, key, convertedvalue) continue # Handle single-line templates template_match = re.match(r'^(\w+)\s*:\s*(.+)', stripped) if template_match: key, content = template_match.groups() template[key] = ScriptTemplate( content = content.strip(), autorefresh = False, definitions=LOCALdefinitions, verbose=verbose, userid=key) template[key].refreshvar(globaldefinitions=GLOBALdefinitions) LOCALdefinitions = lambdaScriptdata() last_successful_line = line_number continue # Unrecognized line if verbose: print(f"Warning: Unrecognized line at {line_number}: {line_content}") last_successful_line = line_number continue # At the end, check if any template block was left unclosed if inside_template_block: # Collect error context context_start = max(0, last_successful_line - 3) context_end = min(len(lines), last_successful_line + 3) error_context_lines = lines[context_start:context_end] error_context = "" for i, error_line in enumerate(error_context_lines): line_num = context_start + i indicator = ">" if line_num == last_successful_line else " " error_context += f"{indicator} {line_num}: {error_line}\n" raise ValueError( f"Template block '{current_template_key}' starting at line {last_successful_line} was not properly closed.\n\n" f"Error context:\n{error_context}" ) # Apply attributes to templates for key in attributes: if key in template: for attr_name, attr_value in attributes[key].items(): setattr(template[key], attr_name, attr_value) template[key]._autorefresh = True # restore the default behavior for the end-user else: raise ValueError(f"Attributes found for undefined template key: {key}") # Create and return new instance if name is None: name = autoname(8) instance = cls( name=name, SECTIONS=global_params.get('SECTIONS', ['DYNAMIC']), section=global_params.get('section', 0), position=global_params.get('position', 0), role=global_params.get('role', 'dscript instance'), description=global_params.get('description', 'dynamic script'), userid=global_params.get('userid', 'dscript'), version=global_params.get('version', 0.1), verbose=global_params.get('verbose', False) ) # Convert numeric string keys to integers if numerickeys is True if numerickeys: numeric_template = {} for key, value in template.items(): if key.isdigit(): numeric_template[int(key)] = value else: numeric_template[key] = value template = numeric_template # Set definitions and template instance.DEFINITIONS = GLOBALdefinitions instance.TEMPLATE = template # Refresh variables instance.set_all_variables() # Check variables instance.check_all_variables(verbose=False) return instance @classmethod def parsesyntax_legacy(cls, content, name=None, numerickeys=True): """ Parse a script from a string content. [ ------------------------------------------------------] [ Legacy parsesyntax method for backward compatibility. ] [ ------------------------------------------------------] Parameters ---------- content : str The string content of the script to be parsed. name : str The name of the dscript project (if None, it is set randomly) numerickeys : bool, default=True If True, numeric string keys in the template section are automatically converted into integers. Returns ------- dscript A new `dscript` instance populated with the content of the loaded file. Raises ------ ValueError If content does not start with the correct DSCRIPT header or the file format is invalid. Notes ----- - The file is expected to follow the same structured format as the one produced by the `save()` method. - The method processes global parameters, definitions, template lines/items, and attributes. If the file includes numeric keys as strings (e.g., "0", "1"), they can be automatically converted into integers if `numerickeys=True`. - The script structure is dynamically rebuilt, and each section (global parameters, definitions, template, and attributes) is correctly parsed and assigned to the corresponding parts of the `dscript` instance. PIZZA.DSCRIPT SAVE FILE FORMAT ------------------------------- This script syntax is designed for creating dynamic and customizable input files, where variables, templates, and control attributes can be defined in a flexible manner. ### Mandatory First Line: Every DSCRIPT file must begin with the following line: # DSCRIPT SAVE FILE ### Structure Overview: 1. **Global Parameters Section (Optional):** - This section defines global script settings, enclosed within curly braces `{ }`. - Properties include: - `SECTIONS`: List of section names to be considered (e.g., `["DYNAMIC"]`). - `section`: Current section index (e.g., `0`). - `position`: Current script position in the order. - `role`: Defines the role of the script instance (e.g., `"dscript instance"`). - `description`: A short description of the script (e.g., `"dynamic script"`). - `userid`: Identifier for the user (e.g., `"dscript"`). - `version`: Script version (e.g., `0.1`). - `verbose`: Verbosity flag, typically a boolean (e.g., `False`). Example: ``` { SECTIONS = ['INITIALIZATION', 'SIMULATION'] # Global script parameters } ``` 2. **Definitions Section:** - Variables are defined in Python-like syntax, allowing for dynamic variable substitution. - Variables can be numbers, strings, or lists, and they can include placeholders using `$` to delay execution or substitution. Example: ``` d = 3 # Define a number periodic = "$p" # '$' prevents immediate evaluation of 'p' units = "$metal" # '$' prevents immediate evaluation of 'metal' dimension = "${d}" # Variable substitution boundary = ['p', 'p', 'p'] # List with a mix of variables and values atom_style = "$atomic" # String variable with delayed evaluation ``` 3. **Templates Section:** - This section provides a mapping between keys and their corresponding commands or instructions. - The templates reference variables defined in the **Definitions** section or elsewhere. - Syntax: ``` KEY: INSTRUCTION ``` where: - `KEY` can be numeric or alphanumeric. - `INSTRUCTION` represents a command template, often referring to variables using `${variable}` notation. Example: ``` units: units ${units} # Template uses the 'units' variable dim: dimension ${dimension} # Template for setting the dimension bound: boundary ${boundary} # Template for boundary settings lattice: lattice ${lattice} # Lattice template ``` 4. **Attributes Section:** - Each template line can have customizable attributes to control behavior and conditions. - Default attributes include: - `facultative`: If `True`, the line is optional and can be removed if needed. - `eval`: If `True`, the line will be evaluated with Python's `eval()` function. - `readonly`: If `True`, the line cannot be modified later in the script. - `condition`: An expression that must be satisfied for the line to be included. - `condeval`: If `True`, the condition will be evaluated using `eval()`. - `detectvar`: If `True`, this creates variables in the **Definitions** section if they do not exist. Example: ``` units: {facultative=False, eval=False, readonly=False, condition="${units}", condeval=False, detectvar=True} dim: {facultative=False, eval=False, readonly=False, condition=None, condeval=False, detectvar=True} ``` Note on multiple definitions ----------------------------- This example demonstrates how variables defined in the `Definitions` section are handled for each template. Each template retains its own snapshot of the variable definitions at the time it is created, ensuring that templates can use different values for the same variable if redefined. content = "" " # DSCRIPT SAVE FILE # Definitions var = 10 # Template ky1 key1: Template content with ${var} # Definitions var = 20 # Template key2 key2: Template content with ${var} # Template key3 key3:[ this is an underfined variable ${var31} this is an another underfined variable ${var32} this variables is defined ${var} ] "" " # Parse content using parsesyntax() ds = dscript.parsesyntax(content) # Key1 should use the first definition of 'var' (10) print(ds.key1.definitions.var) # Output: Template content with 10 # Key2 should use the updated definition of 'var' (20) print(ds.key2.definitions.var) # Output: Template content with 10 """ # Split the content into lines lines = content.splitlines() lines = [line for line in lines if line.strip()] # Remove blank or empty lines # Raise an error if no content is left after removing blank lines if not lines: raise ValueError("File/Content is empty or only contains blank lines.") # Initialize containers for global parameters, definitions, templates, and attributes global_params = {} definitions = lambdaScriptdata() template = {} attributes = {} # State variables to handle multi-line global parameters and attributes inside_global_params = False inside_attributes = False current_attr_key = None # Ensure this is properly initialized global_params_content = "" inside_template_block = False # Track if we are inside a multi-line template current_template_key = None # Track the current template key current_template_content = [] # Store lines for the current template content # Step 1: Authenticate the file if not lines[0].strip().startswith("# DSCRIPT SAVE FILE"): raise ValueError("File/Content is not a valid DSCRIPT file.") # Step 2: Process each line dynamically for line in lines[1:]: stripped = line.strip() # Ignore empty lines and comments if not stripped or stripped.startswith("#"): continue # Remove trailing comments stripped = remove_comments(stripped) # Step 3: Handle global parameters inside {...} if stripped.startswith("{"): # Found the opening {, start accumulating global parameters inside_global_params = True # Remove the opening { and accumulate the remaining content global_params_content = stripped[stripped.index('{') + 1:].strip() # Check if the closing } is also on the same line if '}' in global_params_content: global_params_content = global_params_content[:global_params_content.index('}')].strip() inside_global_params = False # We found the closing } on the same line # Now parse the global parameters block cls._parse_global_params(global_params_content.strip(), global_params) global_params_content = "" # Reset for the next block continue if inside_global_params: # Accumulate content until the closing } is found if stripped.endswith("}"): # Found the closing }, accumulate and process the entire block global_params_content += " " + stripped[:stripped.index('}')].strip() inside_global_params = False # Finished reading global parameters block # Now parse the entire global parameters block cls._parse_global_params(global_params_content.strip(), global_params) global_params_content = "" # Reset for the next block if necessary else: # Continue accumulating if } is not found global_params_content += " " + stripped continue # Step 4: Detect the start of a multi-line template block inside [...] if not inside_template_block: template_match = re.match(r'(\w+)\s*:\s*\[', stripped) if template_match: current_template_key = template_match.group(1) # Capture the key inside_template_block = True current_template_content = [] # Reset content list continue # If inside a template block, accumulate lines until we find the closing ] if inside_template_block: if stripped == "]": # End of the template block, join the content and store it template[current_template_key] = ScriptTemplate( current_template_content, definitions=lambdaScriptdata(**definitions), # Clone current global definitions verbose=True, userid=current_template_key ) template[current_template_key].refreshvar() inside_template_block = False current_template_key = None current_template_content = [] else: # Accumulate the current line (without surrounding spaces) current_template_content.append(stripped) continue # Step 5: Handle attributes inside {...} if inside_attributes and stripped.endswith("}"): # Finish processing attributes for the current key cls._parse_attributes(attributes[current_attr_key], stripped[:-1]) # Remove trailing } inside_attributes = False current_attr_key = None continue if inside_attributes: # Continue accumulating attributes cls._parse_attributes(attributes[current_attr_key], stripped) continue # Step 6: Determine if the line is a definition, template, or attribute definition_match = re.match(r'(\w+)\s*=\s*(.+)', stripped) template_match = re.match(r'(\w+)\s*:\s*(?!\s*\{.*\}\s*$)(.+)', stripped) # template_match = re.match(r'(\w+)\s*:\s*(?!\{)(.+)', stripped) attribute_match = re.match(r'(\w+)\s*:\s*\{\s*(.+)\s*\}', stripped) # attribute_match = re.match(r'(\w+)\s*:\s*\{(.+)\}', stripped) if definition_match: # Line is a definition (key=value) key, value = definition_match.groups() definitions.setattr(key,cls._convert_value(value)) elif template_match and not inside_template_block: # Line is a template (key: content) key, content = template_match.groups() template[key] = ScriptTemplate( content, definitions=lambdaScriptdata(**definitions), # Clone current definitions verbose=True, userid=current_template_key) template[key].refreshvar() elif attribute_match: # Line is an attribute (key:{attributes...}) current_attr_key, attr_content = attribute_match.groups() attributes[current_attr_key] = {} cls._parse_attributes(attributes[current_attr_key], attr_content) inside_attributes = not stripped.endswith("}") # Step 7: Validation and Reconstruction # Make sure there are no attributes without a template entry for key in attributes: if key not in template: raise ValueError(f"Attributes found for undefined template key: {key}") # Apply attributes to the corresponding template object for attr_name, attr_value in attributes[key].items(): setattr(template[key], attr_name, attr_value) # Step 7: Create and return a new dscript instance if name is None: name = autoname(8) instance = cls( name = name, SECTIONS=global_params.get('SECTIONS', ['DYNAMIC']), section=global_params.get('section', 0), position=global_params.get('position', 0), role=global_params.get('role', 'dscript instance'), description=global_params.get('description', 'dynamic script'), userid=global_params.get('userid', 'dscript'), version=global_params.get('version', 0.1), verbose=global_params.get('verbose', False) ) # Convert numeric string keys to integers if numerickeys is True if numerickeys: numeric_template = {} for key, value in template.items(): # Check if the key is a numeric string if key.isdigit(): numeric_template[int(key)] = value else: numeric_template[key] = value template = numeric_template # Set definitions and template instance.DEFINITIONS = definitions instance.TEMPLATE = template # Refresh variables (ensure that variables are detected and added to definitions) instance.set_all_variables() # Check eval instance.check_all_variables(verbose=False) # return the new instance return instance @classmethod def _parse_global_params(cls, content, global_params): """ Parses global parameters from the accumulated content enclosed in `{}`. ### Parameters: content (str): The content string containing global parameters. global_params (dict): A dictionary to populate with parsed parameters. ### Raises: ValueError: If invalid lines or key-value pairs are encountered. """ # Remove braces from the content content = content.strip().strip("{}") # Split the content into lines by commas lines = re.split(r',(?![^(){}\[\]]*[\)\}\]])', content.strip()) for line in lines: line = line.strip() # Match key-value pairs match = re.match(r'([\w_]+)\s*=\s*(.+)', line) if match: key, value = match.groups() key = key.strip() value = value.strip() # Convert the value to the appropriate Python type and store it global_params[key] = cls._convert_value(value) else: raise ValueError(f"Invalid parameter line: '{line}'") @classmethod def _parse_attributes(cls, attr_dict, content): """Parses attributes from the content inside {attribute=value,...}.""" attr_pairs = re.findall(r'(\w+)\s*=\s*([^,]+)', content) for attr_name, attr_value in attr_pairs: attr_dict[attr_name] = cls._convert_value(attr_value) @classmethod def _convert_value(cls, value): """Converts a string representation of a value to the appropriate Python type.""" value = value.strip() # Boolean and None conversion if value.lower() == 'true': return True elif value.lower() == 'false': return False elif value.lower() == 'none': return None # Handle quoted strings if (value.startswith('"') and value.endswith('"')) or (value.startswith("'") and value.endswith("'")): return value[1:-1] # Handle lists (Python syntax inside the file) if value.startswith('[') and value.endswith(']'): return eval(value) # Using eval to parse lists safely in this controlled scenario # Handle numbers try: if '.' in value: return float(value) return int(value) except ValueError: # Return the value as-is if it doesn't match other types return value def __copy__(self): """ copy method """ cls = self.__class__ copie = cls.__new__(cls) copie.__dict__.update(self.__dict__) return copie def __deepcopy__(self, memo): """ deep copy method """ cls = self.__class__ copie = cls.__new__(cls) memo[id(self)] = copie for k, v in self.__dict__.items(): setattr(copie, k, copy.deepcopy(v, memo)) return copie def detect_all_variables(self): """ Detects all variables across all templates in the dscript object. This method iterates through all ScriptTemplate objects in the dscript and collects variables from each template using the detect_variables method. Returns: -------- list A sorted list of unique variables detected in all templates. """ all_variables = set() # Use a set to avoid duplicates # Iterate through all templates in the dscript object for template_key, template in self.TEMPLATE.items(): # Ensure the template is a ScriptTemplate and has the detect_variables method if isinstance(template, ScriptTemplate): detected_vars = template.detect_variables() all_variables.update(detected_vars) # Add the detected variables to the set return sorted(all_variables) # Return a sorted list of unique variables def add_dynamic_script(self, key, content="", userid=None, definitions=None, verbose=None, **USER): """ Add a dynamic script step to the dscript object. Parameters: ----------- key : str The key for the dynamic script (usually an index or step identifier). content : str or list of str, optional The content (template) of the script step. definitions : lambdaScriptdata, optional The merged variable space (STATIC + GLOBAL + LOCAL). verbose : bool, optional If None, self.verbose will be used. Controls verbosity of the template. USER : dict Additional user variables that override the definitions for this step. """ if definitions is None: definitions = lambdaScriptdata() if verbose is None: verbose = self.verbose # Create a new ScriptTemplate and add it to the TEMPLATE self.TEMPLATE[key] = ScriptTemplate( content=content, definitions=self.DEFINITIONS+definitions, verbose=verbose, userid = key if userid is None else userid, **USER ) def check_all_variables(self, verbose=True, seteval=True, output=False): """ Checks for undefined variables for each TEMPLATE key in the dscript object. Parameters: ----------- verbose : bool, optional, default=True If True, prints information about variables for each TEMPLATE key. Shows [-] if the variable is set to its default value, [+] if it is defined, and [ ] if it is undefined. seteval : bool, optional, default=True If True, sets the `eval` attribute to True if at least one variable is defined or set to its default value. output : bool, optional, default=False If True, returns a dictionary with lists of default variables, set variables, and undefined variables. Returns: -------- out : dict, optional If `output=True`, returns a dictionary with the following structure: - "defaultvalues": List of variables set to their default value (${varname}). - "setvalues": List of variables defined with values other than their default. - "undefined": List of variables that are undefined. """ out = {"defaultvalues": [], "setvalues": [], "undefined": []} for key in self.TEMPLATE: template = self.TEMPLATE[key] # Call the check_variables method of ScriptTemplate for each TEMPLATE key result = template.check_variables(verbose=verbose, seteval=seteval) # Update the output dictionary if needed out["defaultvalues"].extend(result["defaultvalues"]) out["setvalues"].extend(result["setvalues"]) out["undefined"].extend(result["undefined"]) if output: return out def set_all_variables(self): """ Ensures that all variables in the templates are added to the global definitions with default values if they are not already defined. """ for key, script_template in self.TEMPLATE.items(): # Check and update the global definitions with template-specific variables for var in script_template.detect_variables(): if var not in self.DEFINITIONS: # Add undefined variables with their default value self.DEFINITIONS.setattr(var, f"${{{var}}}") # Set default as ${varname}
Class variables
var construction_attributes
Static methods
def header(name=None, verbose=True, verbosity=None, style=2, filepath=None, version=None, license=None, email=None)
-
Generate a formatted header for the DSCRIPT file.
Parameters:
name (str, optional): The name of the script. If None, "Unnamed" is used. verbose (bool, optional): Whether to include the header. Default is True. verbosity (int, optional): Verbosity level. Overrides <code>verbose</code> if specified. style (int, optional): ASCII style for the header (default=2). filepath (str, optional): Full path to the file being saved. If None, the line mentioning the file path is excluded. version (str, optional): DSCRIPT version. If None, it is omitted from the header. license (str, optional): License type. If None, it is omitted from the header. email (str, optional): Contact email. If None, it is omitted from the header.
Returns:
str: A formatted string representing the script's metadata and initialization details. Returns an empty string if <code>verbose</code> is False.
The header includes:
- DSCRIPT version, license, and contact email, if provided. - The name of the script. - Filepath, if provided. - Information on where and when the script was generated.
Notes:
- If <code>verbosity</code> is specified, it overrides <code>verbose</code>. - Omits metadata lines if <code>version</code>, <code>license</code>, or <code>email</code> are not provided.
Expand source code
@staticmethod def header(name=None, verbose=True, verbosity=None, style=2, filepath=None, version=None, license=None, email=None): """ Generate a formatted header for the DSCRIPT file. ### Parameters: name (str, optional): The name of the script. If None, "Unnamed" is used. verbose (bool, optional): Whether to include the header. Default is True. verbosity (int, optional): Verbosity level. Overrides `verbose` if specified. style (int, optional): ASCII style for the header (default=2). filepath (str, optional): Full path to the file being saved. If None, the line mentioning the file path is excluded. version (str, optional): DSCRIPT version. If None, it is omitted from the header. license (str, optional): License type. If None, it is omitted from the header. email (str, optional): Contact email. If None, it is omitted from the header. ### Returns: str: A formatted string representing the script's metadata and initialization details. Returns an empty string if `verbose` is False. ### The header includes: - DSCRIPT version, license, and contact email, if provided. - The name of the script. - Filepath, if provided. - Information on where and when the script was generated. ### Notes: - If `verbosity` is specified, it overrides `verbose`. - Omits metadata lines if `version`, `license`, or `email` are not provided. """ # Resolve verbosity verbose = verbosity > 0 if verbosity is not None else verbose if not verbose: return "" # Validate inputs if name is None: name = "Unnamed" # Prepare metadata line metadata = [] if version: metadata.append(f"v{version}") if license: metadata.append(f"License: {license}") if email: metadata.append(f"Email: {email}") metadata_line = " | ".join(metadata) # Prepare the framed header content lines = [] if metadata_line: lines.append(f"PIZZA.DSCRIPT FILE {metadata_line}") lines += [ "", f"Name: {name}", ] # Add the filepath line if filepath is not None if filepath: lines.append(f"Path: {filepath}") lines += [ "", f"Generated on: {getpass.getuser()}@{socket.gethostname()}:{os.getcwd()}", f"{datetime.datetime.now().strftime('%A, %B %d, %Y at %H:%M:%S')}", ] # Use the shared frame_header function to format the framed content return frame_header(lines, style=style)
def load(filename, foldername=None, numerickeys=True)
-
Load a script instance from a text file.
Parameters
filename
:str
- The name of the file to load the script from. If the filename does not end with ".txt", the extension is automatically appended.
foldername
:str
, optional- The directory where the file is located. If not provided, it defaults to the system's temporary directory. If the filename does not include a full path, this folder will be used.
numerickeys
:bool
, default=True
- If True, numeric string keys in the template section are automatically converted into integers. For example, the key "0" would be converted into the integer 0.
Returns
Raises
ValueError
- If the file does not start with the correct DSCRIPT header or the file format is invalid.
FileNotFoundError
- If the specified file does not exist.
Notes
- The file is expected to follow the same structured format as the one produced by the
save()
method. - The method processes global parameters, definitions, template lines/items, and attributes. If the file
includes numeric keys as strings (e.g., "0", "1"), they can be automatically converted into integers
if
numerickeys=True
. - The script structure is dynamically rebuilt, and each section (global parameters, definitions,
template, and attributes) is correctly parsed and assigned to the corresponding parts of the
dscript
instance.
Expand source code
@classmethod def load(cls, filename, foldername=None, numerickeys=True): """ Load a script instance from a text file. Parameters ---------- filename : str The name of the file to load the script from. If the filename does not end with ".txt", the extension is automatically appended. foldername : str, optional The directory where the file is located. If not provided, it defaults to the system's temporary directory. If the filename does not include a full path, this folder will be used. numerickeys : bool, default=True If True, numeric string keys in the template section are automatically converted into integers. For example, the key "0" would be converted into the integer 0. Returns ------- dscript A new `dscript` instance populated with the content of the loaded file. Raises ------ ValueError If the file does not start with the correct DSCRIPT header or the file format is invalid. FileNotFoundError If the specified file does not exist. Notes ----- - The file is expected to follow the same structured format as the one produced by the `save()` method. - The method processes global parameters, definitions, template lines/items, and attributes. If the file includes numeric keys as strings (e.g., "0", "1"), they can be automatically converted into integers if `numerickeys=True`. - The script structure is dynamically rebuilt, and each section (global parameters, definitions, template, and attributes) is correctly parsed and assigned to the corresponding parts of the `dscript` instance. """ # Step 0 validate filepath if not filename.endswith('.txt'): filename += '.txt' # Handle foldername and relative paths if foldername is None or foldername == "": # If the foldername is empty or None, use current working directory for relative paths if not os.path.isabs(filename): filepath = os.path.join(os.getcwd(), filename) else: filepath = filename # If filename is absolute, use it directly else: # If foldername is provided and filename is not absolute, use foldername if not os.path.isabs(filename): filepath = os.path.join(foldername, filename) else: filepath = filename if not os.path.exists(filepath): raise FileExistsError(f"The file '{filepath}' does not exist.") # Read the file contents with open(filepath, 'r') as f: content = f.read() # Call parsesyntax to parse the file content fname = os.path.basename(filepath) # Extracts the filename (e.g., "myscript.txt") name, _ = os.path.splitext(fname) # Removes the extension, e.g., "myscript return cls.parsesyntax(content, name, numerickeys)
def parsesyntax(content, name=None, numerickeys=True, verbose=False, authentification=True, comment_chars='#%', continuation_marker='...')
-
Parse a DSCRIPT script from a string content.
Parameters ---------- content : str The string content of the DSCRIPT script to be parsed. name : str, optional The name of the dscript project. If <code>None</code>, a random name is generated. numerickeys : bool, default=True If <code>True</code>, numeric string keys in the template section are automatically converted into integers. verbose : bool, default=False If <code>True</code>, the parser will output warnings for unrecognized lines outside of blocks. authentification : bool, default=True If <code>True</code>, the parser is expected that the first non empty line is # DSCRIPT SAVE FILE comment_chars : str, optional (default: "#%") A string containing characters to identify the start of a comment. Any of these characters will mark the beginning of a comment unless within quotes. continuation_marker : str, optional (default: "...") A string containing characters to indicate line continuation Any characters after the continuation marker are considered comment and are theorefore ignored Returns ------- dscript A new <code><a title="group.dscript" href="#group.dscript">dscript</a></code> instance populated with the content of the loaded file. Raises ------ ValueError If content does not start with the correct DSCRIPT header or the file format is invalid. Notes ----- **DSCRIPT SAVE FILE FORMAT** This script syntax is designed for creating dynamic and customizable input files, where variables, templates, and control attributes can be defined in a flexible manner. **Mandatory First Line:** Every DSCRIPT file must begin with the following line: ```plaintext # DSCRIPT SAVE FILE ``` **Structure Overview:** 1. **Global Parameters Section (Optional):** - This section defines global script settings, enclosed within curly braces `{}`. - Properties include: - <code>SECTIONS</code>: List of section names to be considered (e.g., `["DYNAMIC"]`). - <code>section</code>: Current section index (e.g., <code>0</code>). - <code>position</code>: Current script position in the order. - <code>role</code>: Defines the role of the script instance (e.g., `"dscript instance"`). - <code>description</code>: A short description of the script (e.g., `"dynamic script"`). - <code>userid</code>: Identifier for the user (e.g., `"dscript"`). - <code>version</code>: Script version (e.g., <code>0.1</code>). - <code>verbose</code>: Verbosity flag, typically a boolean (e.g., <code>False</code>). **Example:** ```plaintext { SECTIONS = ['INITIALIZATION', 'SIMULATION'] # Global script parameters } ``` 2. **Definitions Section:** - Variables are defined in Python-like syntax, allowing for dynamic variable substitution. - Variables can be numbers, strings, or lists, and they can include placeholders using `$` to delay execution or substitution. **Example:** ```plaintext d = 3 # Define a number periodic = "$p" # '$' prevents immediate evaluation of 'p' units = "$metal" # '$' prevents immediate evaluation of 'metal' dimension = "${d}" # Variable substitution boundary = ['p', 'p', 'p'] # List with a mix of variables and values atom_style = "$atomic" # String variable with delayed evaluation ``` 3. **Templates Section:** - This section provides a mapping between keys and their corresponding commands or instructions. - Each template can reference variables defined in the **Definitions** section or elsewhere, typically using the `${variable}` syntax. - **Syntax Variations**: Templates can be defined in several ways, including without blocks, with single- or multi-line blocks, and with ellipsis (<code>...</code>) as a line continuation marker. - **Single-line Template Without Block**: ```plaintext KEY: INSTRUCTION ``` - <code>KEY</code> is the identifier for the template (numeric or alphanumeric). - <code>INSTRUCTION</code> is the command or template text, which may reference variables. - **Single-line Template With Block**: ```plaintext KEY: [INSTRUCTION] ``` - Uses square brackets (<code>\[ ]</code>) around the <code>INSTRUCTION</code>, indicating that all instructions are part of the block. - **Multi-line Template With Block**: ```plaintext KEY: [ INSTRUCTION1 INSTRUCTION2 ... ] ``` - Begins with `KEY: [` and ends with a standalone <code>]</code> on a new line. - Instructions within the block can span multiple lines, and ellipses (<code>...</code>) at the end of a line are used to indicate that the line continues, ignoring any content following the ellipsis as comments. - Comments following ellipses are removed after parsing and do not become part of the block, preserving only the instructions. - **Multi-line Template With Continuation Marker (Ellipsis)**: - For templates with complex code containing square brackets (<code>\[ ]</code>), the ellipsis (<code>...</code>) can be used to prevent <code>]</code> from prematurely closing the block. The ellipsis will keep the line open across multiple lines, allowing brackets in the instructions. **Example:** ```plaintext example1: command ${value} # Single-line template without block example2: [command ${value}] # Single-line template with block # Multi-line template with block example3: [ command1 ${var1} command2 ${var2} ... # Line continues after ellipsis command3 ${var3} ... # Additional instruction continues ] # Multi-line template with ellipsis (handling square brackets) example4: [ A[0][1] ... # Ellipsis allows [ ] within instructions B[2][3] ... # Another instruction in the block ] ``` - **Key Points**: - **Blocks** allow grouping of multiple instructions for a single key, enclosed in square brackets. - **Ellipsis (<code>...</code>)** at the end of a line keeps the line open, preventing premature closing by <code>]</code>, especially useful if the template code includes square brackets (<code>\[ ]</code>). - **Comments** placed after the ellipsis are removed after parsing and are not part of the final block content. This flexibility supports both simple and complex template structures, allowing instructions to be grouped logically while keeping code and comments distinct. 4. **Attributes Section:** - Each template line can have customizable attributes to control behavior and conditions. - Default attributes include: - <code>facultative</code>: If <code>True</code>, the line is optional and can be removed if needed. - <code>eval</code>: If <code>True</code>, the line will be evaluated with Python's <code>eval()</code> function. - <code>readonly</code>: If <code>True</code>, the line cannot be modified later in the script. - <code>condition</code>: An expression that must be satisfied for the line to be included. - <code>condeval</code>: If <code>True</code>, the condition will be evaluated using <code>eval()</code>. - <code>detectvar</code>: If <code>True</code>, this creates variables in the **Definitions** section if they do not exist. **Example:** ```plaintext units: {facultative=False, eval=False, readonly=False, condition="${units}", condeval=False, detectvar=True} dim: {facultative=False, eval=False, readonly=False, condition=None, condeval=False, detectvar=True} ``` **Note on Multiple Definitions** This example demonstrates how variables defined in the **Definitions** section are handled for each template. Each template retains its own snapshot of the variable definitions at the time it is created, ensuring that templates can use different values for the same variable if redefined. **Example:** ```plaintext # DSCRIPT SAVE FILE # Definitions var = 10 # Template key1 key1: Template content with ${var} # Definitions var = 20 # Template key2 key2: Template content with ${var} # Template key3 key3:[ this is an undefined variable ${var31} this is another undefined variable ${var32} this variable is defined ${var} ] ``` **Parsing and Usage:** ```python # Parse content using parsesyntax() ds = dscript.parsesyntax(content) # Accessing templates and their variables print(ds.TEMPLATE['key1'].text) # Output: Template content with 10 print(ds.TEMPLATE['key2'].text) # Output: Template content with 20 ``` **Handling Undefined Variables:** Variables like `${var31}` and `${var32}` in <code>key3</code> are undefined. The parser will handle them based on your substitution logic or raise an error if they are required. **Important Notes:** - The parser processes the script sequentially. Definitions must appear before the templates that use them. - Templates capture the variable definitions at the time they are parsed. Redefining a variable affects only subsequent templates. - Comments outside of blocks are allowed and ignored by the parser. - Content within templates is treated as-is, allowing for any syntax required by the target system (e.g., LAMMPS commands).
Advanced Example
Here's a more advanced example demonstrating the use of global definitions, local definitions, templates, and how to parse and render the template content. ```python content = ''' # GLOBAL DEFINITIONS dumpfile = $dump.LAMMPS dumpdt = 50 thermodt = 100 runtime = 5000 # LOCAL DEFINITIONS for step '0' dimension = 3 units = $si boundary = ['f', 'f', 'f'] atom_style = $smd atom_modify = ['map', 'array'] comm_modify = ['vel', 'yes'] neigh_modify = ['every', 10, 'delay', 0, 'check', 'yes'] newton = $off name = $SimulationBox # This is a comment line outside of blocks # ------------------------------------------ 0: [ % --------------[ Initialization Header (helper) for "${name}" ]-------------- # set a parameter to None or "" to remove the definition dimension ${dimension} units ${units} boundary ${boundary} atom_style ${atom_style} atom_modify ${atom_modify} comm_modify ${comm_modify} neigh_modify ${neigh_modify} newton ${newton} # ------------------------------------------ ] ''' # Parse the content ds = dscript.parsesyntax(content, verbose=True, authentification=False) # Access and print the rendered template print("Template 0 content:") print(ds.TEMPLATE[0].do()) ``` **Explanation:** - **Global Definitions:** Define variables that are accessible throughout the script. - **Local Definitions for Step '0':** Define variables specific to a particular step or template. - **Template Block:** Identified by `0: [ ... ]`, it contains the content where variables will be substituted. - **Comments:** Lines starting with `#` are comments and are ignored by the parser outside of template blocks. **Expected Output:** ``` Template 0 content: # --------------[ Initialization Header (helper) for "SimulationBox" ]-------------- # set a parameter to None or "" to remove the definition dimension 3 units si boundary ['f', 'f', 'f'] atom_style smd atom_modify ['map', 'array'] comm_modify ['vel', 'yes'] neigh_modify ['every', 10, 'delay', 0, 'check', 'yes'] newton off # ------------------------------------------ ``` **Notes:** - The <code>do()</code> method renders the template, substituting variables with their defined values. - Variables like `${dimension}` are replaced with their corresponding values defined in the local or global definitions. - The parser handles comments and blank lines appropriately, ensuring they don't interfere with the parsing logic.
Expand source code
@classmethod def parsesyntax(cls, content, name=None, numerickeys=True, verbose=False, authentification=True, comment_chars="#%",continuation_marker="..."): """ Parse a DSCRIPT script from a string content. Parameters ---------- content : str The string content of the DSCRIPT script to be parsed. name : str, optional The name of the dscript project. If `None`, a random name is generated. numerickeys : bool, default=True If `True`, numeric string keys in the template section are automatically converted into integers. verbose : bool, default=False If `True`, the parser will output warnings for unrecognized lines outside of blocks. authentification : bool, default=True If `True`, the parser is expected that the first non empty line is # DSCRIPT SAVE FILE comment_chars : str, optional (default: "#%") A string containing characters to identify the start of a comment. Any of these characters will mark the beginning of a comment unless within quotes. continuation_marker : str, optional (default: "...") A string containing characters to indicate line continuation Any characters after the continuation marker are considered comment and are theorefore ignored Returns ------- dscript A new `dscript` instance populated with the content of the loaded file. Raises ------ ValueError If content does not start with the correct DSCRIPT header or the file format is invalid. Notes ----- **DSCRIPT SAVE FILE FORMAT** This script syntax is designed for creating dynamic and customizable input files, where variables, templates, and control attributes can be defined in a flexible manner. **Mandatory First Line:** Every DSCRIPT file must begin with the following line: ```plaintext # DSCRIPT SAVE FILE ``` **Structure Overview:** 1. **Global Parameters Section (Optional):** - This section defines global script settings, enclosed within curly braces `{}`. - Properties include: - `SECTIONS`: List of section names to be considered (e.g., `["DYNAMIC"]`). - `section`: Current section index (e.g., `0`). - `position`: Current script position in the order. - `role`: Defines the role of the script instance (e.g., `"dscript instance"`). - `description`: A short description of the script (e.g., `"dynamic script"`). - `userid`: Identifier for the user (e.g., `"dscript"`). - `version`: Script version (e.g., `0.1`). - `verbose`: Verbosity flag, typically a boolean (e.g., `False`). **Example:** ```plaintext { SECTIONS = ['INITIALIZATION', 'SIMULATION'] # Global script parameters } ``` 2. **Definitions Section:** - Variables are defined in Python-like syntax, allowing for dynamic variable substitution. - Variables can be numbers, strings, or lists, and they can include placeholders using `$` to delay execution or substitution. **Example:** ```plaintext d = 3 # Define a number periodic = "$p" # '$' prevents immediate evaluation of 'p' units = "$metal" # '$' prevents immediate evaluation of 'metal' dimension = "${d}" # Variable substitution boundary = ['p', 'p', 'p'] # List with a mix of variables and values atom_style = "$atomic" # String variable with delayed evaluation ``` 3. **Templates Section:** - This section provides a mapping between keys and their corresponding commands or instructions. - Each template can reference variables defined in the **Definitions** section or elsewhere, typically using the `${variable}` syntax. - **Syntax Variations**: Templates can be defined in several ways, including without blocks, with single- or multi-line blocks, and with ellipsis (`...`) as a line continuation marker. - **Single-line Template Without Block**: ```plaintext KEY: INSTRUCTION ``` - `KEY` is the identifier for the template (numeric or alphanumeric). - `INSTRUCTION` is the command or template text, which may reference variables. - **Single-line Template With Block**: ```plaintext KEY: [INSTRUCTION] ``` - Uses square brackets (`[ ]`) around the `INSTRUCTION`, indicating that all instructions are part of the block. - **Multi-line Template With Block**: ```plaintext KEY: [ INSTRUCTION1 INSTRUCTION2 ... ] ``` - Begins with `KEY: [` and ends with a standalone `]` on a new line. - Instructions within the block can span multiple lines, and ellipses (`...`) at the end of a line are used to indicate that the line continues, ignoring any content following the ellipsis as comments. - Comments following ellipses are removed after parsing and do not become part of the block, preserving only the instructions. - **Multi-line Template With Continuation Marker (Ellipsis)**: - For templates with complex code containing square brackets (`[ ]`), the ellipsis (`...`) can be used to prevent `]` from prematurely closing the block. The ellipsis will keep the line open across multiple lines, allowing brackets in the instructions. **Example:** ```plaintext example1: command ${value} # Single-line template without block example2: [command ${value}] # Single-line template with block # Multi-line template with block example3: [ command1 ${var1} command2 ${var2} ... # Line continues after ellipsis command3 ${var3} ... # Additional instruction continues ] # Multi-line template with ellipsis (handling square brackets) example4: [ A[0][1] ... # Ellipsis allows [ ] within instructions B[2][3] ... # Another instruction in the block ] ``` - **Key Points**: - **Blocks** allow grouping of multiple instructions for a single key, enclosed in square brackets. - **Ellipsis (`...`)** at the end of a line keeps the line open, preventing premature closing by `]`, especially useful if the template code includes square brackets (`[ ]`). - **Comments** placed after the ellipsis are removed after parsing and are not part of the final block content. This flexibility supports both simple and complex template structures, allowing instructions to be grouped logically while keeping code and comments distinct. 4. **Attributes Section:** - Each template line can have customizable attributes to control behavior and conditions. - Default attributes include: - `facultative`: If `True`, the line is optional and can be removed if needed. - `eval`: If `True`, the line will be evaluated with Python's `eval()` function. - `readonly`: If `True`, the line cannot be modified later in the script. - `condition`: An expression that must be satisfied for the line to be included. - `condeval`: If `True`, the condition will be evaluated using `eval()`. - `detectvar`: If `True`, this creates variables in the **Definitions** section if they do not exist. **Example:** ```plaintext units: {facultative=False, eval=False, readonly=False, condition="${units}", condeval=False, detectvar=True} dim: {facultative=False, eval=False, readonly=False, condition=None, condeval=False, detectvar=True} ``` **Note on Multiple Definitions** This example demonstrates how variables defined in the **Definitions** section are handled for each template. Each template retains its own snapshot of the variable definitions at the time it is created, ensuring that templates can use different values for the same variable if redefined. **Example:** ```plaintext # DSCRIPT SAVE FILE # Definitions var = 10 # Template key1 key1: Template content with ${var} # Definitions var = 20 # Template key2 key2: Template content with ${var} # Template key3 key3:[ this is an undefined variable ${var31} this is another undefined variable ${var32} this variable is defined ${var} ] ``` **Parsing and Usage:** ```python # Parse content using parsesyntax() ds = dscript.parsesyntax(content) # Accessing templates and their variables print(ds.TEMPLATE['key1'].text) # Output: Template content with 10 print(ds.TEMPLATE['key2'].text) # Output: Template content with 20 ``` **Handling Undefined Variables:** Variables like `${var31}` and `${var32}` in `key3` are undefined. The parser will handle them based on your substitution logic or raise an error if they are required. **Important Notes:** - The parser processes the script sequentially. Definitions must appear before the templates that use them. - Templates capture the variable definitions at the time they are parsed. Redefining a variable affects only subsequent templates. - Comments outside of blocks are allowed and ignored by the parser. - Content within templates is treated as-is, allowing for any syntax required by the target system (e.g., LAMMPS commands). **Advanced Example** Here's a more advanced example demonstrating the use of global definitions, local definitions, templates, and how to parse and render the template content. ```python content = ''' # GLOBAL DEFINITIONS dumpfile = $dump.LAMMPS dumpdt = 50 thermodt = 100 runtime = 5000 # LOCAL DEFINITIONS for step '0' dimension = 3 units = $si boundary = ['f', 'f', 'f'] atom_style = $smd atom_modify = ['map', 'array'] comm_modify = ['vel', 'yes'] neigh_modify = ['every', 10, 'delay', 0, 'check', 'yes'] newton = $off name = $SimulationBox # This is a comment line outside of blocks # ------------------------------------------ 0: [ % --------------[ Initialization Header (helper) for "${name}" ]-------------- # set a parameter to None or "" to remove the definition dimension ${dimension} units ${units} boundary ${boundary} atom_style ${atom_style} atom_modify ${atom_modify} comm_modify ${comm_modify} neigh_modify ${neigh_modify} newton ${newton} # ------------------------------------------ ] ''' # Parse the content ds = dscript.parsesyntax(content, verbose=True, authentification=False) # Access and print the rendered template print("Template 0 content:") print(ds.TEMPLATE[0].do()) ``` **Explanation:** - **Global Definitions:** Define variables that are accessible throughout the script. - **Local Definitions for Step '0':** Define variables specific to a particular step or template. - **Template Block:** Identified by `0: [ ... ]`, it contains the content where variables will be substituted. - **Comments:** Lines starting with `#` are comments and are ignored by the parser outside of template blocks. **Expected Output:** ``` Template 0 content: # --------------[ Initialization Header (helper) for "SimulationBox" ]-------------- # set a parameter to None or "" to remove the definition dimension 3 units si boundary ['f', 'f', 'f'] atom_style smd atom_modify ['map', 'array'] comm_modify ['vel', 'yes'] neigh_modify ['every', 10, 'delay', 0, 'check', 'yes'] newton off # ------------------------------------------ ``` **Notes:** - The `do()` method renders the template, substituting variables with their defined values. - Variables like `${dimension}` are replaced with their corresponding values defined in the local or global definitions. - The parser handles comments and blank lines appropriately, ensuring they don't interfere with the parsing logic. """ # Split the content into lines lines = content.splitlines() if not lines: raise ValueError("File/Content is empty or only contains blank lines.") # Initialize containers global_params = {} GLOBALdefinitions = lambdaScriptdata() LOCALdefinitions = lambdaScriptdata() template = {} attributes = {} # State variables inside_global_params = False global_params_content = "" inside_template_block = False current_template_key = None current_template_content = [] current_var_value = lambdaScriptdata() # Initialize line number line_number = 0 last_successful_line = 0 # Step 1: Authenticate the file if authentification: auth_line_found = False max_header_lines = 10 header_end_idx = -1 for idx, line in enumerate(lines[:max_header_lines]): stripped_line = line.strip() if not stripped_line: continue if stripped_line.startswith("# DSCRIPT SAVE FILE"): auth_line_found = True header_end_idx = idx break elif stripped_line.startswith("#") or stripped_line.startswith("%"): continue else: raise ValueError(f"Unexpected content before authentication line (# DSCRIPT SAVE FILE) at line {idx + 1}:\n{line}") if not auth_line_found: raise ValueError("File/Content is not a valid DSCRIPT file.") # Remove header lines lines = lines[header_end_idx + 1:] line_number = header_end_idx + 1 last_successful_line = line_number - 1 else: line_number = 0 last_successful_line = 0 # Process each line for idx, line in enumerate(lines): line_number += 1 line_content = line.rstrip('\n') # Determine if we're inside a template block if inside_template_block: # Extract the code with its eventual continuation_marker code_line = remove_comments( line_content, comment_chars=comment_chars, continuation_marker=continuation_marker, remove_continuation_marker=False, ).rstrip() # Check if line should continue if code_line.endswith(continuation_marker): # Append line up to the continuation marker endofline_index = line_content.rindex(continuation_marker) trimmed_content = line_content[:endofline_index].rstrip() if trimmed_content: current_template_content.append(trimmed_content) continue elif code_line.endswith("]"): # End of multi-line block closing_index = code_line.rindex(']') trimmed_content = code_line[:closing_index].rstrip() # Append any valid content before `]`, if non-empty if trimmed_content: current_template_content.append(trimmed_content) # End of template block content = '\n'.join(current_template_content) template[current_template_key] = ScriptTemplate( content=content, autorefresh=False, definitions=LOCALdefinitions, verbose=verbose, userid=current_template_key) # Refresh variables definitions template[current_template_key].refreshvar(globaldefinitions=GLOBALdefinitions) LOCALdefinitions = lambdaScriptdata() # Reset state for next block inside_template_block = False current_template_key = None current_template_content = [] last_successful_line = line_number continue else: # Append the entire original line content if not ending with `...` or `]` current_template_content.append(line_content) continue # Not inside a template block stripped_no_comments = remove_comments(line_content) # Ignore empty lines after removing comments if not stripped_no_comments.strip(): continue # If the original line is a comment line, skip it if line_content.strip().startswith("#") or line_content.strip().startswith("%"): continue stripped = stripped_no_comments.strip() # Handle start of a new template block template_block_match = re.match(r'^(\w+)\s*:\s*\[', stripped) if template_block_match: current_template_key = template_block_match.group(1) if inside_template_block: # Collect error context context_start = max(0, last_successful_line - 3) context_end = min(len(lines), line_number + 2) error_context_lines = lines[context_start:context_end] error_context = "" for i, error_line in enumerate(error_context_lines): line_num = context_start + i + 1 indicator = ">" if line_num == line_number else "*" if line_num == last_successful_line else " " error_context += f"{indicator} {line_num}: {error_line}\n" raise ValueError( f"Template block '{current_template_key}' starting at line {last_successful_line} (*) was not properly closed before starting a new one at line {line_number} (>).\n\n" f"Error context:\n{error_context}" ) else: inside_template_block = True idx_open_bracket = line_content.index('[') remainder = line_content[idx_open_bracket + 1:].strip() if remainder: remainder_code = remove_comments(remainder, comment_chars=comment_chars).rstrip() if remainder_code.endswith("]"): closing_index = remainder_code.rindex(']') content_line = remainder_code[:closing_index].strip() if content_line: current_template_content.append(content_line) content = '\n'.join(current_template_content) template[current_template_key] = ScriptTemplate( content=content, autorefresh=False, definitions=LOCALdefinitions, verbose=verbose, userid=current_template_key) template[current_template_key].refreshvar(globaldefinitions=GLOBALdefinitions) LOCALdefinitions = lambdaScriptdata() inside_template_block = False current_template_key = None current_template_content = [] last_successful_line = line_number continue else: current_template_content.append(remainder) last_successful_line = line_number continue # Handle start of global parameters if stripped.startswith('{') and not inside_global_params: if '}' in stripped: global_params_content = stripped cls._parse_global_params(global_params_content.strip(), global_params) global_params_content = "" last_successful_line = line_number else: inside_global_params = True global_params_content = stripped continue # Handle global parameters inside {...} if inside_global_params: global_params_content += ' ' + stripped if '}' in stripped: inside_global_params = False cls._parse_global_params(global_params_content.strip(), global_params) global_params_content = "" last_successful_line = line_number continue # Handle attributes attribute_match = re.match(r'^(\w+)\s*:\s*\{(.+)\}', stripped) if attribute_match: key, attr_content = attribute_match.groups() attributes[key] = {} cls._parse_attributes(attributes[key], attr_content.strip()) last_successful_line = line_number continue # Handle definitions definition_match = re.match(r'^(\w+)\s*=\s*(.+)', stripped) if definition_match: key, value = definition_match.groups() convertedvalue = cls._convert_value(value) if key in GLOBALdefinitions: if (GLOBALdefinitions.getattr(key) != convertedvalue) or \ (getattr(current_var_value, key) != convertedvalue): LOCALdefinitions.setattr(key, convertedvalue) else: GLOBALdefinitions.setattr(key, convertedvalue) last_successful_line = line_number setattr(current_var_value, key, convertedvalue) continue # Handle single-line templates template_match = re.match(r'^(\w+)\s*:\s*(.+)', stripped) if template_match: key, content = template_match.groups() template[key] = ScriptTemplate( content = content.strip(), autorefresh = False, definitions=LOCALdefinitions, verbose=verbose, userid=key) template[key].refreshvar(globaldefinitions=GLOBALdefinitions) LOCALdefinitions = lambdaScriptdata() last_successful_line = line_number continue # Unrecognized line if verbose: print(f"Warning: Unrecognized line at {line_number}: {line_content}") last_successful_line = line_number continue # At the end, check if any template block was left unclosed if inside_template_block: # Collect error context context_start = max(0, last_successful_line - 3) context_end = min(len(lines), last_successful_line + 3) error_context_lines = lines[context_start:context_end] error_context = "" for i, error_line in enumerate(error_context_lines): line_num = context_start + i indicator = ">" if line_num == last_successful_line else " " error_context += f"{indicator} {line_num}: {error_line}\n" raise ValueError( f"Template block '{current_template_key}' starting at line {last_successful_line} was not properly closed.\n\n" f"Error context:\n{error_context}" ) # Apply attributes to templates for key in attributes: if key in template: for attr_name, attr_value in attributes[key].items(): setattr(template[key], attr_name, attr_value) template[key]._autorefresh = True # restore the default behavior for the end-user else: raise ValueError(f"Attributes found for undefined template key: {key}") # Create and return new instance if name is None: name = autoname(8) instance = cls( name=name, SECTIONS=global_params.get('SECTIONS', ['DYNAMIC']), section=global_params.get('section', 0), position=global_params.get('position', 0), role=global_params.get('role', 'dscript instance'), description=global_params.get('description', 'dynamic script'), userid=global_params.get('userid', 'dscript'), version=global_params.get('version', 0.1), verbose=global_params.get('verbose', False) ) # Convert numeric string keys to integers if numerickeys is True if numerickeys: numeric_template = {} for key, value in template.items(): if key.isdigit(): numeric_template[int(key)] = value else: numeric_template[key] = value template = numeric_template # Set definitions and template instance.DEFINITIONS = GLOBALdefinitions instance.TEMPLATE = template # Refresh variables instance.set_all_variables() # Check variables instance.check_all_variables(verbose=False) return instance
def parsesyntax_legacy(content, name=None, numerickeys=True)
-
Parse a script from a string content. [ ------------------------------------------------------] [ Legacy parsesyntax method for backward compatibility. ] [ ------------------------------------------------------]
Parameters
content
:str
- The string content of the script to be parsed.
name
:str
- The name of the dscript project (if None, it is set randomly)
numerickeys
:bool
, default=True
- If True, numeric string keys in the template section are automatically converted into integers.
Returns
Raises
ValueError
- If content does not start with the correct DSCRIPT header or the file format is invalid.
Notes
- The file is expected to follow the same structured format as the one produced by the
save()
method. - The method processes global parameters, definitions, template lines/items, and attributes. If the file
includes numeric keys as strings (e.g., "0", "1"), they can be automatically converted into integers
if
numerickeys=True
. - The script structure is dynamically rebuilt, and each section (global parameters, definitions,
template, and attributes) is correctly parsed and assigned to the corresponding parts of the
dscript
instance.
PIZZA.DSCRIPT SAVE FILE FORMAT
This script syntax is designed for creating dynamic and customizable input files, where variables, templates, and control attributes can be defined in a flexible manner.
Mandatory First Line:
Every DSCRIPT file must begin with the following line: # DSCRIPT SAVE FILE
Structure Overview:
-
Global Parameters Section (Optional):
- This section defines global script settings, enclosed within curly braces
{ }
. - Properties include:
SECTIONS
: List of section names to be considered (e.g.,["DYNAMIC"]
).section
: Current section index (e.g.,0
).position
: Current script position in the order.role
: Defines the role of the script instance (e.g.,"dscript instance"
).description
: A short description of the script (e.g.,"dynamic script"
).userid
: Identifier for the user (e.g.,"dscript"
).version
: Script version (e.g.,0.1
).verbose
: Verbosity flag, typically a boolean (e.g.,False
).
Example:
{ SECTIONS = ['INITIALIZATION', 'SIMULATION'] # Global script parameters }
- This section defines global script settings, enclosed within curly braces
-
Definitions Section:
- Variables are defined in Python-like syntax, allowing for dynamic variable substitution.
- Variables can be numbers, strings, or lists, and they can include placeholders using
$
to delay execution or substitution.
Example:
d = 3 # Define a number periodic = "$p" # '$' prevents immediate evaluation of 'p' units = "$metal" # '$' prevents immediate evaluation of 'metal' dimension = "${d}" # Variable substitution boundary = ['p', 'p', 'p'] # List with a mix of variables and values atom_style = "$atomic" # String variable with delayed evaluation
-
Templates Section:
- This section provides a mapping between keys and their corresponding commands or instructions.
- The templates reference variables defined in the Definitions section or elsewhere.
- Syntax:
KEY: INSTRUCTION
where:KEY
can be numeric or alphanumeric.INSTRUCTION
represents a command template, often referring to variables using${variable}
notation.
Example:
units: units ${units} # Template uses the 'units' variable dim: dimension ${dimension} # Template for setting the dimension bound: boundary ${boundary} # Template for boundary settings lattice: lattice ${lattice} # Lattice template
-
Attributes Section:
- Each template line can have customizable attributes to control behavior and conditions.
- Default attributes include:
facultative
: IfTrue
, the line is optional and can be removed if needed.eval
: IfTrue
, the line will be evaluated with Python'seval()
function.readonly
: IfTrue
, the line cannot be modified later in the script.condition
: An expression that must be satisfied for the line to be included.condeval
: IfTrue
, the condition will be evaluated usingeval()
.detectvar
: IfTrue
, this creates variables in the Definitions section if they do not exist.
Example:
units: {facultative=False, eval=False, readonly=False, condition="${units}", condeval=False, detectvar=True} dim: {facultative=False, eval=False, readonly=False, condition=None, condeval=False, detectvar=True}
Note On Multiple Definitions
This example demonstrates how variables defined in the
Definitions
section are handled for each template. Each template retains its own snapshot of the variable definitions at the time it is created, ensuring that templates can use different values for the same variable if redefined.content = "" "
DSCRIPT SAVE FILE
Definitions
var = 10
Template ky1
key1: Template content with ${var}
Definitions
var = 20
Template key2
key2: Template content with ${var}
Template key3
key3:[ this is an underfined variable ${var31} this is an another underfined variable ${var32} this variables is defined ${var} ]
"" "
Parse content using parsesyntax()
ds = dscript.parsesyntax(content)
Key1 should use the first definition of 'var' (10)
print(ds.key1.definitions.var) # Output: Template content with 10
Key2 should use the updated definition of 'var' (20)
print(ds.key2.definitions.var) # Output: Template content with 10
Expand source code
@classmethod def parsesyntax_legacy(cls, content, name=None, numerickeys=True): """ Parse a script from a string content. [ ------------------------------------------------------] [ Legacy parsesyntax method for backward compatibility. ] [ ------------------------------------------------------] Parameters ---------- content : str The string content of the script to be parsed. name : str The name of the dscript project (if None, it is set randomly) numerickeys : bool, default=True If True, numeric string keys in the template section are automatically converted into integers. Returns ------- dscript A new `dscript` instance populated with the content of the loaded file. Raises ------ ValueError If content does not start with the correct DSCRIPT header or the file format is invalid. Notes ----- - The file is expected to follow the same structured format as the one produced by the `save()` method. - The method processes global parameters, definitions, template lines/items, and attributes. If the file includes numeric keys as strings (e.g., "0", "1"), they can be automatically converted into integers if `numerickeys=True`. - The script structure is dynamically rebuilt, and each section (global parameters, definitions, template, and attributes) is correctly parsed and assigned to the corresponding parts of the `dscript` instance. PIZZA.DSCRIPT SAVE FILE FORMAT ------------------------------- This script syntax is designed for creating dynamic and customizable input files, where variables, templates, and control attributes can be defined in a flexible manner. ### Mandatory First Line: Every DSCRIPT file must begin with the following line: # DSCRIPT SAVE FILE ### Structure Overview: 1. **Global Parameters Section (Optional):** - This section defines global script settings, enclosed within curly braces `{ }`. - Properties include: - `SECTIONS`: List of section names to be considered (e.g., `["DYNAMIC"]`). - `section`: Current section index (e.g., `0`). - `position`: Current script position in the order. - `role`: Defines the role of the script instance (e.g., `"dscript instance"`). - `description`: A short description of the script (e.g., `"dynamic script"`). - `userid`: Identifier for the user (e.g., `"dscript"`). - `version`: Script version (e.g., `0.1`). - `verbose`: Verbosity flag, typically a boolean (e.g., `False`). Example: ``` { SECTIONS = ['INITIALIZATION', 'SIMULATION'] # Global script parameters } ``` 2. **Definitions Section:** - Variables are defined in Python-like syntax, allowing for dynamic variable substitution. - Variables can be numbers, strings, or lists, and they can include placeholders using `$` to delay execution or substitution. Example: ``` d = 3 # Define a number periodic = "$p" # '$' prevents immediate evaluation of 'p' units = "$metal" # '$' prevents immediate evaluation of 'metal' dimension = "${d}" # Variable substitution boundary = ['p', 'p', 'p'] # List with a mix of variables and values atom_style = "$atomic" # String variable with delayed evaluation ``` 3. **Templates Section:** - This section provides a mapping between keys and their corresponding commands or instructions. - The templates reference variables defined in the **Definitions** section or elsewhere. - Syntax: ``` KEY: INSTRUCTION ``` where: - `KEY` can be numeric or alphanumeric. - `INSTRUCTION` represents a command template, often referring to variables using `${variable}` notation. Example: ``` units: units ${units} # Template uses the 'units' variable dim: dimension ${dimension} # Template for setting the dimension bound: boundary ${boundary} # Template for boundary settings lattice: lattice ${lattice} # Lattice template ``` 4. **Attributes Section:** - Each template line can have customizable attributes to control behavior and conditions. - Default attributes include: - `facultative`: If `True`, the line is optional and can be removed if needed. - `eval`: If `True`, the line will be evaluated with Python's `eval()` function. - `readonly`: If `True`, the line cannot be modified later in the script. - `condition`: An expression that must be satisfied for the line to be included. - `condeval`: If `True`, the condition will be evaluated using `eval()`. - `detectvar`: If `True`, this creates variables in the **Definitions** section if they do not exist. Example: ``` units: {facultative=False, eval=False, readonly=False, condition="${units}", condeval=False, detectvar=True} dim: {facultative=False, eval=False, readonly=False, condition=None, condeval=False, detectvar=True} ``` Note on multiple definitions ----------------------------- This example demonstrates how variables defined in the `Definitions` section are handled for each template. Each template retains its own snapshot of the variable definitions at the time it is created, ensuring that templates can use different values for the same variable if redefined. content = "" " # DSCRIPT SAVE FILE # Definitions var = 10 # Template ky1 key1: Template content with ${var} # Definitions var = 20 # Template key2 key2: Template content with ${var} # Template key3 key3:[ this is an underfined variable ${var31} this is an another underfined variable ${var32} this variables is defined ${var} ] "" " # Parse content using parsesyntax() ds = dscript.parsesyntax(content) # Key1 should use the first definition of 'var' (10) print(ds.key1.definitions.var) # Output: Template content with 10 # Key2 should use the updated definition of 'var' (20) print(ds.key2.definitions.var) # Output: Template content with 10 """ # Split the content into lines lines = content.splitlines() lines = [line for line in lines if line.strip()] # Remove blank or empty lines # Raise an error if no content is left after removing blank lines if not lines: raise ValueError("File/Content is empty or only contains blank lines.") # Initialize containers for global parameters, definitions, templates, and attributes global_params = {} definitions = lambdaScriptdata() template = {} attributes = {} # State variables to handle multi-line global parameters and attributes inside_global_params = False inside_attributes = False current_attr_key = None # Ensure this is properly initialized global_params_content = "" inside_template_block = False # Track if we are inside a multi-line template current_template_key = None # Track the current template key current_template_content = [] # Store lines for the current template content # Step 1: Authenticate the file if not lines[0].strip().startswith("# DSCRIPT SAVE FILE"): raise ValueError("File/Content is not a valid DSCRIPT file.") # Step 2: Process each line dynamically for line in lines[1:]: stripped = line.strip() # Ignore empty lines and comments if not stripped or stripped.startswith("#"): continue # Remove trailing comments stripped = remove_comments(stripped) # Step 3: Handle global parameters inside {...} if stripped.startswith("{"): # Found the opening {, start accumulating global parameters inside_global_params = True # Remove the opening { and accumulate the remaining content global_params_content = stripped[stripped.index('{') + 1:].strip() # Check if the closing } is also on the same line if '}' in global_params_content: global_params_content = global_params_content[:global_params_content.index('}')].strip() inside_global_params = False # We found the closing } on the same line # Now parse the global parameters block cls._parse_global_params(global_params_content.strip(), global_params) global_params_content = "" # Reset for the next block continue if inside_global_params: # Accumulate content until the closing } is found if stripped.endswith("}"): # Found the closing }, accumulate and process the entire block global_params_content += " " + stripped[:stripped.index('}')].strip() inside_global_params = False # Finished reading global parameters block # Now parse the entire global parameters block cls._parse_global_params(global_params_content.strip(), global_params) global_params_content = "" # Reset for the next block if necessary else: # Continue accumulating if } is not found global_params_content += " " + stripped continue # Step 4: Detect the start of a multi-line template block inside [...] if not inside_template_block: template_match = re.match(r'(\w+)\s*:\s*\[', stripped) if template_match: current_template_key = template_match.group(1) # Capture the key inside_template_block = True current_template_content = [] # Reset content list continue # If inside a template block, accumulate lines until we find the closing ] if inside_template_block: if stripped == "]": # End of the template block, join the content and store it template[current_template_key] = ScriptTemplate( current_template_content, definitions=lambdaScriptdata(**definitions), # Clone current global definitions verbose=True, userid=current_template_key ) template[current_template_key].refreshvar() inside_template_block = False current_template_key = None current_template_content = [] else: # Accumulate the current line (without surrounding spaces) current_template_content.append(stripped) continue # Step 5: Handle attributes inside {...} if inside_attributes and stripped.endswith("}"): # Finish processing attributes for the current key cls._parse_attributes(attributes[current_attr_key], stripped[:-1]) # Remove trailing } inside_attributes = False current_attr_key = None continue if inside_attributes: # Continue accumulating attributes cls._parse_attributes(attributes[current_attr_key], stripped) continue # Step 6: Determine if the line is a definition, template, or attribute definition_match = re.match(r'(\w+)\s*=\s*(.+)', stripped) template_match = re.match(r'(\w+)\s*:\s*(?!\s*\{.*\}\s*$)(.+)', stripped) # template_match = re.match(r'(\w+)\s*:\s*(?!\{)(.+)', stripped) attribute_match = re.match(r'(\w+)\s*:\s*\{\s*(.+)\s*\}', stripped) # attribute_match = re.match(r'(\w+)\s*:\s*\{(.+)\}', stripped) if definition_match: # Line is a definition (key=value) key, value = definition_match.groups() definitions.setattr(key,cls._convert_value(value)) elif template_match and not inside_template_block: # Line is a template (key: content) key, content = template_match.groups() template[key] = ScriptTemplate( content, definitions=lambdaScriptdata(**definitions), # Clone current definitions verbose=True, userid=current_template_key) template[key].refreshvar() elif attribute_match: # Line is an attribute (key:{attributes...}) current_attr_key, attr_content = attribute_match.groups() attributes[current_attr_key] = {} cls._parse_attributes(attributes[current_attr_key], attr_content) inside_attributes = not stripped.endswith("}") # Step 7: Validation and Reconstruction # Make sure there are no attributes without a template entry for key in attributes: if key not in template: raise ValueError(f"Attributes found for undefined template key: {key}") # Apply attributes to the corresponding template object for attr_name, attr_value in attributes[key].items(): setattr(template[key], attr_name, attr_value) # Step 7: Create and return a new dscript instance if name is None: name = autoname(8) instance = cls( name = name, SECTIONS=global_params.get('SECTIONS', ['DYNAMIC']), section=global_params.get('section', 0), position=global_params.get('position', 0), role=global_params.get('role', 'dscript instance'), description=global_params.get('description', 'dynamic script'), userid=global_params.get('userid', 'dscript'), version=global_params.get('version', 0.1), verbose=global_params.get('verbose', False) ) # Convert numeric string keys to integers if numerickeys is True if numerickeys: numeric_template = {} for key, value in template.items(): # Check if the key is a numeric string if key.isdigit(): numeric_template[int(key)] = value else: numeric_template[key] = value template = numeric_template # Set definitions and template instance.DEFINITIONS = definitions instance.TEMPLATE = template # Refresh variables (ensure that variables are detected and added to definitions) instance.set_all_variables() # Check eval instance.check_all_variables(verbose=False) # return the new instance return instance
def write(scriptcontent, filename=None, foldername=None, overwrite=False)
-
Writes the provided script content to a specified file in a given folder, with a header if necessary.
Parameters
scriptcontent
:str
- The content to be written to the file.
filename
:str
, optional- The name of the file. If not provided, a random name will be generated.
The extension
.txt
will be appended if not already present. foldername
:str
, optional- The folder where the file will be saved. If not provided, the current working directory is used.
overwrite
:bool
, optional- If False (default), raises a
FileExistsError
if the file already exists. If True, the file will be overwritten if it exists.
Returns
str
- The full path to the written file.
Raises
FileExistsError
- If the file already exists and
overwrite
is set to False.
Notes
- A header is prepended to the content if it does not already exist, using the
header
method. - The header includes metadata such as the current date, username, hostname, and file details.
Expand source code
@staticmethod @staticmethod def write(scriptcontent, filename=None, foldername=None, overwrite=False): """ Writes the provided script content to a specified file in a given folder, with a header if necessary. Parameters ---------- scriptcontent : str The content to be written to the file. filename : str, optional The name of the file. If not provided, a random name will be generated. The extension `.txt` will be appended if not already present. foldername : str, optional The folder where the file will be saved. If not provided, the current working directory is used. overwrite : bool, optional If False (default), raises a `FileExistsError` if the file already exists. If True, the file will be overwritten if it exists. Returns ------- str The full path to the written file. Raises ------ FileExistsError If the file already exists and `overwrite` is set to False. Notes ----- - A header is prepended to the content if it does not already exist, using the `header` method. - The header includes metadata such as the current date, username, hostname, and file details. """ # Generate a random name if filename is not provided if filename is None: filename = autoname(8) # Generates a random name of 8 letters # Ensure the filename ends with '.txt' if not filename.endswith('.txt'): filename += '.txt' # Handle foldername and relative paths if foldername is None or foldername == "": # If foldername is empty or None, use current working directory for relative paths if not os.path.isabs(filename): filepath = os.path.join(os.getcwd(), filename) else: filepath = filename # If filename is absolute, use it directly else: # If foldername is provided and filename is not absolute, use foldername if not os.path.isabs(filename): filepath = os.path.join(foldername, filename) else: filepath = filename # Check if file already exists, raise exception if it does and overwrite is False if os.path.exists(filepath) and not overwrite: raise FileExistsError(f"The file '{filepath}' already exists.") # Count total and non-empty lines in the content total_lines = len(scriptcontent.splitlines()) non_empty_lines = sum(1 for line in scriptcontent.splitlines() if line.strip()) # Prepare header if not already present if not scriptcontent.startswith("# DSCRIPT SAVE FILE"): fname = os.path.basename(filepath) # Extracts the filename (e.g., "myscript.txt") name, _ = os.path.splitext(fname) # Removes the extension, e.g., "myscript" metadata = get_metadata() # retrieve all metadata (statically) header = dscript.header(name=name, verbosity=True,style=1,filepath=filepath, version = metadata["version"], license = metadata["license"], email = metadata["email"]) # Add line count information to the header footer = frame_header( lines=[ f"Total lines written: {total_lines}", f"Non-empty lines: {non_empty_lines}" ], style=1 ) scriptcontent = header + "\n" + scriptcontent + "\n" + footer # Write the content to the file with open(filepath, 'w') as file: file.write(scriptcontent) return filepath
Methods
def add_dynamic_script(self, key, content='', userid=None, definitions=None, verbose=None, **USER)
-
Add a dynamic script step to the dscript object.
Parameters:
key : str The key for the dynamic script (usually an index or step identifier). content : str or list of str, optional The content (template) of the script step. definitions : lambdaScriptdata, optional The merged variable space (STATIC + GLOBAL + LOCAL). verbose : bool, optional If None, self.verbose will be used. Controls verbosity of the template. USER : dict Additional user variables that override the definitions for this step.
Expand source code
def add_dynamic_script(self, key, content="", userid=None, definitions=None, verbose=None, **USER): """ Add a dynamic script step to the dscript object. Parameters: ----------- key : str The key for the dynamic script (usually an index or step identifier). content : str or list of str, optional The content (template) of the script step. definitions : lambdaScriptdata, optional The merged variable space (STATIC + GLOBAL + LOCAL). verbose : bool, optional If None, self.verbose will be used. Controls verbosity of the template. USER : dict Additional user variables that override the definitions for this step. """ if definitions is None: definitions = lambdaScriptdata() if verbose is None: verbose = self.verbose # Create a new ScriptTemplate and add it to the TEMPLATE self.TEMPLATE[key] = ScriptTemplate( content=content, definitions=self.DEFINITIONS+definitions, verbose=verbose, userid = key if userid is None else userid, **USER )
def check_all_variables(self, verbose=True, seteval=True, output=False)
-
Checks for undefined variables for each TEMPLATE key in the dscript object.
Parameters:
verbose : bool, optional, default=True If True, prints information about variables for each TEMPLATE key. Shows [-] if the variable is set to its default value, [+] if it is defined, and [ ] if it is undefined.
seteval : bool, optional, default=True If True, sets the
eval
attribute to True if at least one variable is defined or set to its default value.output : bool, optional, default=False If True, returns a dictionary with lists of default variables, set variables, and undefined variables.
Returns:
out : dict, optional If
output=True
, returns a dictionary with the following structure: - "defaultvalues": List of variables set to their default value (${varname}). - "setvalues": List of variables defined with values other than their default. - "undefined": List of variables that are undefined.Expand source code
def check_all_variables(self, verbose=True, seteval=True, output=False): """ Checks for undefined variables for each TEMPLATE key in the dscript object. Parameters: ----------- verbose : bool, optional, default=True If True, prints information about variables for each TEMPLATE key. Shows [-] if the variable is set to its default value, [+] if it is defined, and [ ] if it is undefined. seteval : bool, optional, default=True If True, sets the `eval` attribute to True if at least one variable is defined or set to its default value. output : bool, optional, default=False If True, returns a dictionary with lists of default variables, set variables, and undefined variables. Returns: -------- out : dict, optional If `output=True`, returns a dictionary with the following structure: - "defaultvalues": List of variables set to their default value (${varname}). - "setvalues": List of variables defined with values other than their default. - "undefined": List of variables that are undefined. """ out = {"defaultvalues": [], "setvalues": [], "undefined": []} for key in self.TEMPLATE: template = self.TEMPLATE[key] # Call the check_variables method of ScriptTemplate for each TEMPLATE key result = template.check_variables(verbose=verbose, seteval=seteval) # Update the output dictionary if needed out["defaultvalues"].extend(result["defaultvalues"]) out["setvalues"].extend(result["setvalues"]) out["undefined"].extend(result["undefined"]) if output: return out
def createEmptyVariables(self, vars)
-
Creates empty variables in DEFINITIONS if they don't already exist.
Parameters:
vars : str or list of str The variable name or list of variable names to be created in DEFINITIONS.
Expand source code
def createEmptyVariables(self, vars): """ Creates empty variables in DEFINITIONS if they don't already exist. Parameters: ----------- vars : str or list of str The variable name or list of variable names to be created in DEFINITIONS. """ if isinstance(vars, str): vars = [vars] # Convert single variable name to list for uniform processing for varname in vars: if varname not in self.DEFINITIONS: self.DEFINITIONS.setattr(varname,"${" + varname + "}")
def detect_all_variables(self)
-
Detects all variables across all templates in the dscript object.
This method iterates through all ScriptTemplate objects in the dscript and collects variables from each template using the detect_variables method.
Returns:
list A sorted list of unique variables detected in all templates.
Expand source code
def detect_all_variables(self): """ Detects all variables across all templates in the dscript object. This method iterates through all ScriptTemplate objects in the dscript and collects variables from each template using the detect_variables method. Returns: -------- list A sorted list of unique variables detected in all templates. """ all_variables = set() # Use a set to avoid duplicates # Iterate through all templates in the dscript object for template_key, template in self.TEMPLATE.items(): # Ensure the template is a ScriptTemplate and has the detect_variables method if isinstance(template, ScriptTemplate): detected_vars = template.detect_variables() all_variables.update(detected_vars) # Add the detected variables to the set return sorted(all_variables) # Return a sorted list of unique variables
def do(self, printflag=None, verbose=None, softrun=False, return_definitions=False, comment_chars='#%', **USER)
-
Executes or previews all
ScriptTemplate
instances inTEMPLATE
, concatenating their processed content. Allows for optional headers and footers based on verbosity settings, and offers a preliminary preview mode withsoftrun
. Accumulates definitions across all templates ifreturn_definitions=True
.Parameters
printflag
:bool
, optional- If
True
, enables print output during execution. Defaults to the instance's print flag ifNone
. verbose
:bool
, optional- If
True
, includes headers and footers in the output, providing additional detail. Defaults to the instance's verbosity setting ifNone
. softrun
:bool
, optional- If
True
, executes the script in a preliminary mode: - Bypasses full variable substitution for a preview of the content, useful for validating structure. - IfFalse
(default), performs full processing, including variable substitutions and evaluations. return_definitions
:bool
, optional- If
True
, returns a tuple where the second element contains accumulated definitions from all templates. IfFalse
(default), returns only the concatenated output. comment_chars
:str
, optional(default: "#%")
- A string containing characters to identify the start of a comment. Any of these characters will mark the beginning of a comment unless within quotes.
**USER
:keyword arguments
- Allows for the provision of additional user-defined definitions, where each keyword represents a definition key and the associated value represents the definition's content. These definitions can override or supplement template-level definitions during execution.
Returns
str
ortuple
-
- If
return_definitions=False
, returns the concatenated output of allScriptTemplate
instances, with optional headers, footers, and execution summary based on verbosity. - If
return_definitions=True
, returns a tuple of (output
,accumulated_definitions
), whereaccumulated_definitions
contains all definitions used across templates.
- If
Notes
- Each
ScriptTemplate
inTEMPLATE
is processed individually using its owndo()
method. - The
softrun
mode provides a preliminary content preview without full variable substitution, helpful for inspecting the script structure or gathering local definitions. - When
verbose
is enabled, the method includes detailed headers, footers, and a summary of processed and ignored items, providing insight into the script's construction and variable usage. - Accumulated definitions from each
ScriptTemplate
are combined ifreturn_definitions=True
, which can be useful for tracking all variables and definitions applied across the templates.
Example
>>> dscript_instance = dscript(name="ExampleScript") >>> dscript_instance.TEMPLATE[0] = ScriptTemplate( ... content=["units ${units}", "boundary ${boundary}"], ... definitions=lambdaScriptdata(units="lj", boundary="p p p"), ... attributes={'eval': True} ... ) >>> dscript_instance.do(verbose=True, units="real") # Output: # -------------- # TEMPLATE "ExampleScript" # -------------- units real boundary p p p # ---> Total items: 2 - Ignored items: 0
Expand source code
def do(self, printflag=None, verbose=None, softrun=False, return_definitions=False,comment_chars="#%", **USER): """ Executes or previews all `ScriptTemplate` instances in `TEMPLATE`, concatenating their processed content. Allows for optional headers and footers based on verbosity settings, and offers a preliminary preview mode with `softrun`. Accumulates definitions across all templates if `return_definitions=True`. Parameters ---------- printflag : bool, optional If `True`, enables print output during execution. Defaults to the instance's print flag if `None`. verbose : bool, optional If `True`, includes headers and footers in the output, providing additional detail. Defaults to the instance's verbosity setting if `None`. softrun : bool, optional If `True`, executes the script in a preliminary mode: - Bypasses full variable substitution for a preview of the content, useful for validating structure. - If `False` (default), performs full processing, including variable substitutions and evaluations. return_definitions : bool, optional If `True`, returns a tuple where the second element contains accumulated definitions from all templates. If `False` (default), returns only the concatenated output. comment_chars : str, optional (default: "#%") A string containing characters to identify the start of a comment. Any of these characters will mark the beginning of a comment unless within quotes. **USER : keyword arguments Allows for the provision of additional user-defined definitions, where each keyword represents a definition key and the associated value represents the definition's content. These definitions can override or supplement template-level definitions during execution. Returns ------- str or tuple - If `return_definitions=False`, returns the concatenated output of all `ScriptTemplate` instances, with optional headers, footers, and execution summary based on verbosity. - If `return_definitions=True`, returns a tuple of (`output`, `accumulated_definitions`), where `accumulated_definitions` contains all definitions used across templates. Notes ----- - Each `ScriptTemplate` in `TEMPLATE` is processed individually using its own `do()` method. - The `softrun` mode provides a preliminary content preview without full variable substitution, helpful for inspecting the script structure or gathering local definitions. - When `verbose` is enabled, the method includes detailed headers, footers, and a summary of processed and ignored items, providing insight into the script's construction and variable usage. - Accumulated definitions from each `ScriptTemplate` are combined if `return_definitions=True`, which can be useful for tracking all variables and definitions applied across the templates. Example ------- >>> dscript_instance = dscript(name="ExampleScript") >>> dscript_instance.TEMPLATE[0] = ScriptTemplate( ... content=["units ${units}", "boundary ${boundary}"], ... definitions=lambdaScriptdata(units="lj", boundary="p p p"), ... attributes={'eval': True} ... ) >>> dscript_instance.do(verbose=True, units="real") # Output: # -------------- # TEMPLATE "ExampleScript" # -------------- units real boundary p p p # ---> Total items: 2 - Ignored items: 0 """ printflag = self.printflag if printflag is None else printflag verbose = self.verbose if verbose is None else verbose header = f"# --------------[ TEMPLATE \"{self.name}\" ]--------------" if verbose else "" footer = "# --------------------------------------------" if verbose else "" # Initialize output, counters, and optional definitions accumulator output = [header] non_empty_lines = 0 ignored_lines = 0 accumulated_definitions = lambdaScriptdata() if return_definitions else None for key, template in self.TEMPLATE.items(): # Process each template with softrun if enabled, otherwise use full processing result = template.do(softrun=softrun,USER=lambdaScriptdata(**USER)) if result: # Apply comment removal based on verbosity final_result = result if verbose else remove_comments(result,comment_chars=comment_chars) if final_result or verbose: output.append(final_result) non_empty_lines += 1 else: ignored_lines += 1 # Accumulate definitions if return_definitions is enabled if return_definitions: accumulated_definitions += template.definitions else: ignored_lines += 1 # Add footer summary if verbose nel_word = 'items' if non_empty_lines > 1 else 'item' il_word = 'items' if ignored_lines > 1 else 'item' footer += f"\n# ---> Total {nel_word}: {non_empty_lines} - Ignored {il_word}: {ignored_lines}" if verbose else "" output.append(footer) # Concatenate output and determine return type based on return_definitions output_content = "\n".join(output) return (output_content, accumulated_definitions) if return_definitions else output_content
def generator(self)
-
Returns
STR
- generated code corresponding to dscript (using dscript syntax/language).
Expand source code
def generator(self): """ Returns ------- STR generated code corresponding to dscript (using dscript syntax/language). """ return self.save(generatoronly=True)
def get_attributes_by_index(self, index)
-
Returns the attributes of the ScriptTemplate at the specified index.
Expand source code
def get_attributes_by_index(self, index): """ Returns the attributes of the ScriptTemplate at the specified index.""" key = list(self.TEMPLATE.keys())[index] return self.TEMPLATE[key].attributes
def get_content_by_index(self, index, do=True, protected=True)
-
Returns the content of the ScriptTemplate at the specified index.
Parameters:
index : int The index of the template in the TEMPLATE dictionary. do : bool, optional (default=True) If True, the content will be processed based on conditions and evaluation flags. protected : bool, optional (default=True) Controls whether variable evaluation is protected (e.g., prevents overwriting certain definitions).
Returns:
str or list of str The content of the template after processing, or an empty string if conditions or evaluation flags block it.
Expand source code
def get_content_by_index(self, index, do=True, protected=True): """ Returns the content of the ScriptTemplate at the specified index. Parameters: ----------- index : int The index of the template in the TEMPLATE dictionary. do : bool, optional (default=True) If True, the content will be processed based on conditions and evaluation flags. protected : bool, optional (default=True) Controls whether variable evaluation is protected (e.g., prevents overwriting certain definitions). Returns: -------- str or list of str The content of the template after processing, or an empty string if conditions or evaluation flags block it. """ key = list(self.TEMPLATE.keys())[index] s = self.TEMPLATE[key].content att = self.TEMPLATE[key].attributes # Return an empty string if the facultative attribute is True and do is True if att["facultative"] and do: return "" # Evaluate the condition (if any) if att["condition"] is not None: cond = eval(self.DEFINITIONS.formateval(att["condition"], protected)) else: cond = True # If the condition is met, process the content if cond: # Apply formateval only if the eval attribute is True and do is True if att["eval"] and do: if isinstance(s, list): # Apply formateval to each item in the list if s is a list return [self.DEFINITIONS.formateval(line, protected) for line in s] else: # Apply formateval to the single string content return self.DEFINITIONS.formateval(s, protected) else: return s # Return the raw content if no evaluation is needed elif do: return "" # Return an empty string if the condition is not met and do is True else: return s # Return the raw content if do is False
def items(self)
-
Expand source code
def items(self): return ((key, s.content) for key, s in self.TEMPLATE.items())
def keys(self)
-
Return the keys of the TEMPLATE.
Expand source code
def keys(self): """Return the keys of the TEMPLATE.""" return self.TEMPLATE.keys()
def pipescript(self, *keys, printflag=None, verbose=None, verbosity=None, **USER)
-
Returns a pipescript object by combining script objects corresponding to the given keys.
Parameters:
keys : one or more keys that correspond to the
TEMPLATE
entries. printflag : bool, optional Whether to enable printing of additional information. verbose : bool, optional Whether to run in verbose mode for debugging or detailed output. *USER : dict, optional Additional user-defined variables to pass into the script.Returns:
A
pipescript
object that combines the script objects generated from the selected dscript subobjects.Expand source code
def pipescript(self, *keys, printflag=None, verbose=None, verbosity=None, **USER): """ Returns a pipescript object by combining script objects corresponding to the given keys. Parameters: ----------- *keys : one or more keys that correspond to the `TEMPLATE` entries. printflag : bool, optional Whether to enable printing of additional information. verbose : bool, optional Whether to run in verbose mode for debugging or detailed output. **USER : dict, optional Additional user-defined variables to pass into the script. Returns: -------- A `pipescript` object that combines the script objects generated from the selected dscript subobjects. """ # Start with an empty pipescript # combined_pipescript = None # # Iterate over the provided keys to extract corresponding subobjects # for key in keys: # # Extract the dscript subobject for the given key # sub_dscript = self(key) # # Convert the dscript subobject to a script object, passing USER, printflag, and verbose # script_obj = sub_dscript.script(printflag=printflag, verbose=verbose, **USER) # # Combine script objects into a pipescript object # if combined_pipescript is None: # combined_pipescript = pipescript(script_obj) # Initialize pipescript # else: # combined_pipescript = combined_pipescript | script_obj # Use pipe operator # if combined_pipescript is None: # ValueError('The conversion to pipescript from {type{self}} falled') # return combined_pipescript printflag = self.printflag if printflag is None else printflag verbose = verbosity > 0 if verbosity is not None else (self.verbose if verbose is None else verbose) verbosity = 0 if not verbose else verbosity # Loop over all keys in TEMPLATE and combine them combined_pipescript = None for key in self.keys(): # Create a new dscript object with only the current key in TEMPLATE focused_dscript = dscript(name=f"{self.name}:{key}") focused_dscript.TEMPLATE[key] = self.TEMPLATE[key] focused_dscript.TEMPLATE[key].definitions = scriptdata(**self.TEMPLATE[key].definitions) focused_dscript.DEFINITIONS = scriptdata(**self.DEFINITIONS) focused_dscript.SECTIONS = self.SECTIONS[:] focused_dscript.section = self.section focused_dscript.position = self.position focused_dscript.role = self.role focused_dscript.description = self.description focused_dscript.userid = self.userid focused_dscript.version = self.version focused_dscript.verbose = verbose focused_dscript.printflag = printflag # Convert the focused dscript object to a script object script_obj = focused_dscript.script(printflag=printflag, verbose=verbose, **USER) # Combine the script objects into a pipescript object using the pipe operator if combined_pipescript is None: combined_pipescript = pipescript(script_obj) # Initialize pipescript else: combined_pipescript = combined_pipescript | pipescript(script_obj) # Use pipe operator if combined_pipescript is None: ValueError('The conversion to pipescript from {type{self}} falled') return combined_pipescript
def reorder(self, order)
-
Reorder the TEMPLATE lines according to a list of indices.
Expand source code
def reorder(self, order): """Reorder the TEMPLATE lines according to a list of indices.""" # Get the original items as a list of (key, value) pairs original_items = list(self.TEMPLATE.items()) # Create a new dictionary with reordered scripts, preserving original keys new_scripts = {original_items[i][0]: original_items[i][1] for i in order} # Create a new dscript object with reordered scripts reordered_script = dscript() reordered_script.TEMPLATE = new_scripts return reordered_script
def save(self, filename=None, foldername=None, overwrite=False, generatoronly=False, onlyusedvariables=True)
-
Save the current script instance to a text file.
Parameters
filename
:str
, optional- The name of the file to save the script to. If not provided,
self.name
is used. The extension ".txt" is automatically appended if not included. foldername
:str
, optional- The directory where the file will be saved. If not provided, it defaults to the system's temporary directory. If the filename does not include a full path, this folder will be used.
overwrite
:bool
, default=True
- Whether to overwrite the file if it already exists. If set to False, an exception is raised if the file exists.
generatoronly
:bool
, default=False
- If True, the method returns the generated content string without saving to a file.
onlyusedvariables
:bool
, default=True
- If True, local definitions are only saved if they are used within the template content. If False, all local definitions are saved, regardless of whether they are referenced in the template.
Raises
FileExistsError
- If the file already exists and
overwrite
is set to False.
Notes
- The script is saved in a plain text format, and each section (global parameters, definitions, template, and attributes) is written in a structured format with appropriate comments.
- If
self.name
is used as the filename, it must be a valid string that can serve as a file name. -
The file structure follows the format: # DSCRIPT SAVE FILE # generated on YYYY-MM-DD on user@hostname
GLOBAL PARAMETERS
{ … }
DEFINITIONS (number of definitions=…)
key=value
TEMPLATES (number of items=…)
key: template_content
ATTRIBUTES (number of items with explicit attributes=…)
key:{attr1=value1, attr2=value2, …}
Expand source code
def save(self, filename=None, foldername=None, overwrite=False, generatoronly=False, onlyusedvariables=True): """ Save the current script instance to a text file. Parameters ---------- filename : str, optional The name of the file to save the script to. If not provided, `self.name` is used. The extension ".txt" is automatically appended if not included. foldername : str, optional The directory where the file will be saved. If not provided, it defaults to the system's temporary directory. If the filename does not include a full path, this folder will be used. overwrite : bool, default=True Whether to overwrite the file if it already exists. If set to False, an exception is raised if the file exists. generatoronly : bool, default=False If True, the method returns the generated content string without saving to a file. onlyusedvariables : bool, default=True If True, local definitions are only saved if they are used within the template content. If False, all local definitions are saved, regardless of whether they are referenced in the template. Raises ------ FileExistsError If the file already exists and `overwrite` is set to False. Notes ----- - The script is saved in a plain text format, and each section (global parameters, definitions, template, and attributes) is written in a structured format with appropriate comments. - If `self.name` is used as the filename, it must be a valid string that can serve as a file name. - The file structure follows the format: # DSCRIPT SAVE FILE # generated on YYYY-MM-DD on user@hostname # GLOBAL PARAMETERS { ... } # DEFINITIONS (number of definitions=...) key=value # TEMPLATES (number of items=...) key: template_content # ATTRIBUTES (number of items with explicit attributes=...) key:{attr1=value1, attr2=value2, ...} """ # At the beginning of the save method start_time = time.time() # Start the timer if not generatoronly: # Use self.name if filename is not provided if filename is None: filename = span(self.name, sep="\n") # Ensure the filename ends with '.txt' if not filename.endswith('.txt'): filename += '.txt' # Construct the full path if foldername in [None, ""]: # Handle cases where foldername is None or an empty string filepath = os.path.abspath(filename) else: filepath = os.path.join(foldername, filename) # Check if the file already exists, and raise an exception if it does and overwrite is False if os.path.exists(filepath) and not overwrite: raise FileExistsError(f"The file '{filepath}' already exists.") # Header with current date, username, and host header = "# DSCRIPT SAVE FILE\n" header += "\n"*2 if generatoronly: header += dscript.header(verbose=True,filepath='dynamic code generation (no file)', name = self.name, version=self.version, license=self.license, email=self.email) else: header += dscript.header(verbose=True,filepath=filepath, name = self.name, version=self.version, license=self.license, email=self.email) header += "\n"*2 # Global parameters in strict Python syntax global_params = "# GLOBAL PARAMETERS (8 parameters)\n" global_params += "{\n" global_params += f" SECTIONS = {self.SECTIONS},\n" global_params += f" section = {self.section},\n" global_params += f" position = {self.position},\n" global_params += f" role = {self.role!r},\n" global_params += f" description = {self.description!r},\n" global_params += f" userid = {self.userid!r},\n" global_params += f" version = {self.version},\n" global_params += f" verbose = {self.verbose}\n" global_params += "}\n" # Initialize definitions with self.DEFINITIONS allvars = self.DEFINITIONS # Temporary dictionary to track global variable information global_var_info = {} # Loop over each template item to detect and record variable usage and overrides for template_index, (key, script_template) in enumerate(self.TEMPLATE.items()): # Detect variables used in this template used_variables = script_template.detect_variables() # Loop over each template item to detect and record variable usage and overrides for template_index, (key, script_template) in enumerate(self.TEMPLATE.items()): # Detect variables used in this template used_variables = script_template.detect_variables() # Check each variable used in this template for var in used_variables: # Get global and local values for the variable global_value = getattr(allvars, var, None) local_value = getattr(script_template.definitions, var, None) is_global = var in allvars # Check if the variable originates in global space is_default = is_global and ( global_value is None or global_value == "" or global_value == f"${{{var}}}" ) # If the variable is not yet tracked, initialize its info if var not in global_var_info: global_var_info[var] = { "value": global_value, # Initial value from allvars if exists "updatedvalue": global_value,# Initial value from allvars if exists "is_default": is_default, # Check if it’s set to a default value "first_def": None, # First definition (to be updated later) "first_use": template_index, # First time the variable is used "first_val": global_value, "override_index": template_index if local_value is not None else None, # Set override if defined locally "is_global": is_global # Track if the variable originates as global } else: # Update `override_index` if the variable is defined locally and its value changes if local_value is not None: # Check if the local value differs from the tracked value in global_var_info current_value = global_var_info[var]["value"] if current_value != local_value: global_var_info[var]["override_index"] = template_index global_var_info[var]["updatedvalue"] = local_value # Update the tracked value # Second loop: Update `first_def` for all variables for template_index, (key, script_template) in enumerate(self.TEMPLATE.items()): local_definitions = script_template.definitions.keys() for var in local_definitions: if var in global_var_info and global_var_info[var]["first_def"] is None: global_var_info[var]["first_def"] = template_index global_var_info[var]["first_val"] = getattr(script_template.definitions, var) # Filter global definitions based on usage, overrides, and first_def filtered_globals = { var: info for var, info in global_var_info.items() if (info["is_global"] or info["is_default"]) and (info["override_index"] is None) } # Generate the definitions output based on filtered globals definitions = f"\n# GLOBAL DEFINITIONS (number of definitions={len(filtered_globals)})\n" for var, info in filtered_globals.items(): if info["is_default"] and (info["first_def"]>info["first_use"] if info["first_def"] else True): definitions += f"{var} = ${{{var}}} # value assumed to be defined outside this DSCRIPT file\n" else: value = info["first_val"] #info["value"] if value in ["", None]: definitions += f'{var} = ""\n' elif isinstance(value, str): safe_value = value.replace('\\', '\\\\').replace('\n', '\\n') definitions += f"{var} = {safe_value}\n" else: definitions += f"{var} = {value}\n" # Template (number of lines/items) printsinglecontent = False template = f"\n# TEMPLATES (number of items={len(self.TEMPLATE)})\n" for key, script_template in self.TEMPLATE.items(): # Get local template definitions and detected variables template_vars = script_template.definitions used_variables = script_template.detect_variables() islocal = False # Temporary dictionary to accumulate variables to add to allvars valid_local_vars = lambdaScriptdata() # Write template-specific definitions only if they meet the updated conditions for var in template_vars.keys(): # Conditions for adding a variable to the local template and to `allvars` if (var in used_variables or not onlyusedvariables) and ( script_template.is_variable_set_value_only(var) and (var not in allvars or getattr(template_vars, var) != getattr(allvars, var)) ): # Start local definitions section if this is the first local variable for the template if not islocal: template += f"\n# LOCAL DEFINITIONS for key '{key}'\n" islocal = True # Retrieve and process the variable value value = getattr(template_vars, var) if value in ["", None]: template += f'{var} = ""\n' # Set empty or None values as "" elif isinstance(value, str): safe_value = value.replace('\\', '\\\\').replace('\n', '\\n') template += f"{var} = {safe_value}\n" else: template += f"{var} = {value}\n" # Add the variable to valid_local_vars for selective update of allvars valid_local_vars.setattr(var, value) # Update allvars only with filtered, valid local variables allvars += valid_local_vars # Write the template content if isinstance(script_template.content, list): if len(script_template.content) == 1: # Single-line template saved as a single line content_str = script_template.content[0].strip() template += "" if printsinglecontent else "\n" template += f"{key}: {content_str}\n" printsinglecontent = True else: content_str = '\n '.join(script_template.content) template += f"\n{key}: [\n {content_str}\n ]\n" printsinglecontent = False else: template += "" if printsinglecontent else "\n" template += f"{key}: {script_template.content}\n" printsinglecontent = True # Attributes (number of lines/items with explicit attributes) attributes = f"# ATTRIBUTES (number of items with explicit attributes={len(self.TEMPLATE)})\n" for key, script_template in self.TEMPLATE.items(): attr_str = ", ".join(f"{attr_name}={repr(attr_value)}" for attr_name, attr_value in script_template.attributes.items()) attributes += f"{key}:{{{attr_str}}}\n" # Combine all sections into one content content = header + "\n" + global_params + "\n" + definitions + "\n" + template + "\n" + attributes + "\n" # Append footer information to the content non_empty_lines = sum(1 for line in content.splitlines() if line.strip()) # Count non-empty lines execution_time = time.time() - start_time # Calculate the execution time in seconds # Prepare the footer content footer_lines = [ ["Non-empty lines", str(non_empty_lines)], ["Execution time (seconds)", f"{execution_time:.4f}"], ] # Format footer into tabular style footer_content = [ f"{row[0]:<25} {row[1]:<15}" for row in footer_lines ] # Use frame_header to format footer footer = frame_header( lines=["DSCRIPT SAVE FILE generator"] + footer_content, style=1 ) # Append footer to the content content += f"\n{footer}" if generatoronly: return content else: # Write the content to the file with open(filepath, 'w') as f: f.write(content) print(f"\nScript saved to {filepath}") return filepath
def script(self, printflag=None, verbose=None, verbosity=None, **USER)
-
returns the corresponding script
Expand source code
def script(self,printflag=None, verbose=None, verbosity=None, **USER): """ returns the corresponding script """ printflag = self.printflag if printflag is None else printflag verbose = verbosity > 0 if verbosity is not None else (self.verbose if verbose is None else verbose) verbosity = 0 if not verbose else verbosity return lamdaScript(self,persistentfile=True, persistentfolder=None, printflag=printflag, verbose=verbose, **USER)
def set_all_variables(self)
-
Ensures that all variables in the templates are added to the global definitions with default values if they are not already defined.
Expand source code
def set_all_variables(self): """ Ensures that all variables in the templates are added to the global definitions with default values if they are not already defined. """ for key, script_template in self.TEMPLATE.items(): # Check and update the global definitions with template-specific variables for var in script_template.detect_variables(): if var not in self.DEFINITIONS: # Add undefined variables with their default value self.DEFINITIONS.setattr(var, f"${{{var}}}") # Set default as ${varname}
def values(self)
-
Return the ScriptTemplate objects in TEMPLATE.
Expand source code
def values(self): """Return the ScriptTemplate objects in TEMPLATE.""" return self.TEMPLATE.values()
class group (name=None, groups=None, group_names=None, collection=None, printflag=False, verbose=True, verbosity=None)
-
A class for managing LAMMPS group operations and generating LAMMPS scripts.
Overview
The
group
class provides an object-oriented interface to define and manage groups of atoms in LAMMPS simulations. Groups in LAMMPS are collections of atoms that can be manipulated together, allowing users to apply fixes, compute properties, or perform operations on specific subsets of atoms.This class allows you to create, combine, and manipulate groups using algebraic operations (union, intersection, subtraction), and to generate the corresponding LAMMPS commands. It also provides methods to output the group commands as scripts, which can be integrated into LAMMPS input files.
Key Features
- Create and Manage Groups: Define new groups based on atom types, regions, IDs, or variables.
- Flexible Group Creation: Create multiple groups at once and define groups
using concise criteria through the
add_group_criteria
method. - Algebraic Operations: Combine groups using union (
+
), intersection (*
), and subtraction (-
) operations. - Subindexing with Callable Syntax: Retrieve multiple group operations
by calling the
group
instance with names or indices. - Script Generation: Generate LAMMPS script lines for group definitions, which can be output as scripts or pipelines.
- Dynamic Evaluation: Evaluate complex group expressions and store the resulting operations.
- Integration with Scripting Tools: Convert group operations into
dscript
orpipescript
objects for advanced script management.
LAMMPS Context
In LAMMPS (Large-scale Atomic/Molecular Massively Parallel Simulator), groups are fundamental for specifying subsets of atoms for applying operations like forces, fixes, and computes. The
group
command in LAMMPS allows users to define groups based on various criteria such as atom IDs, types, regions, and variables.This
group
class abstracts the complexity of managing group definitions and operations, providing a high-level interface to define and manipulate groups programmatically.Usage Examples
Creating Multiple Groups at Once
# Create groups 'o1', 'o2', 'o3', 'o4' upon instantiation G = group(group_names=['o1', 'o2', 'o3', 'o4'])
Defining Groups Based on Criteria
G = group() G.add_group_criteria('lower', type=[1]) G.add_group_criteria('central', region='central_cyl') G.add_group_criteria('new_group', create=True) G.add_group_criteria('upper', clear=True) G.add_group_criteria('subtract_group', subtract=['group1', 'group2'])
Defining a Group Based on a Variable
G = group() G.variable('myVar', 'x > 5') # Assign a variable G.byvariable('myGroup', 'myVar') # Define group based on the variable
Using
add_group_criteria
with VariableG.add_group_criteria('myGroup', variable={'name': 'myVar', 'expression': 'x > 5'})
Defining Groups Using a Dictionary
group_definitions = { 'group1': {'type': [1, 2]}, 'group2': {'region': 'my_region'}, 'group3': {'variable': {'name': 'var1', 'expression': 'x > 5'}}, 'group4': {'union': ['group1', 'group2']}, } G.add_group_criteria(group_definitions)
Creating a Group from a Collection of
groupobject
Instances# Import the classes # from groupobject import groupobject # from group import group # Create groupobject instances o1 = groupobject(beadtype=1, group=["all", "A"], mass=1.0) o2 = groupobject(beadtype=2, group=["all", "B", "C"]) o3 = groupobject(beadtype=3, group="C", mass=2.5) # Create a group instance with the collection G = group(name="mycollection", collection=[o1, o2, o3]) # or G = group(name=None, collection=[o1, o2, o3]) # Name will be generated as "1+2+3" # Generate the LAMMPS script script_content = G.code() print(script_content)
Expected Output:
group all type 1 2 3 group A type 1 group B type 2 group C type 2 3
Accessing Groups and Performing Operations
# Access groups via attribute-style or item-style access op1 = G.group1 op2 = G['group2'] # Combine groups using algebraic operations complex_op = op1 + op2 - G.group3 # Evaluate the operation and store the result G.evaluate('combined_group', complex_op)
Subindexing with Callable Syntax
# Retrieve multiple operations by names or indices subG = G('group1', 2, 'group3') # Retrieves 'group1', third operation, and 'group3' # Display the names of operations in subG operation_names = [op.name for op in subG._operations] print(operation_names) # Output: ['group1', 'group3', 'group3'] # Generate the LAMMPS script for the subgroup script_content = subG.code() print(script_content)
Generating LAMMPS Script
script_content = G.code() print(script_content)
Class Methods
Initialization and Setup
__init__(self, name=None, groups=None, group_names=None, printflag=False, verbose=True)
: Initializes a newgroup
instance.name
(str): Optional name for the group instance.groups
(dict): Dictionary of group definitions to create upon initialization.group_names
(list): List of group names to create empty groups upon initialization.-
collection
(list or tuple of groupobject, optional): Collection of groupobject instances. -
printflag
(bool): If True, enables printing of script generation. -
verbose
(bool): If True, enables verbose output. -
create_groups(self, *group_names)
: Creates multiple new groups with the given names. -
add_group_criteria(self, *args, **kwargs)
: Adds group(s) based on criteria. - Supports two usages:
add_group_criteria(group_name, **criteria)
: Adds a single group.add_group_criteria(group_definitions)
: Adds multiple groups from a dictionary.
Group Creation Methods
create(self, group_name)
: Creates a new empty group with the given name.bytype(self, group_name, type_values)
: Defines a group based on atom types.byid(self, group_name, id_values)
: Defines a group based on atom IDs.byregion(self, group_name, region_name)
: Defines a group based on a region.variable(self, variable_name, expression, style="atom")
: Assigns an expression to a LAMMPS variable.byvariable(self, group_name, variable_name)
: Defines a group based on a variable.clear(self, group_name)
: Clears an existing group.
Algebraic Operations
union(self, group_name, *groups)
: Performs a union of the specified groups.intersect(self, group_name, *groups)
: Performs an intersection of the specified groups.subtract(self, group_name, *groups)
: Subtracts groups from the first group.evaluate(self, group_name, group_op)
: Evaluates a group operation and stores the result.
Access and Manipulation
__getitem__(self, key)
: Allows accessing operations by name or index.__getattr__(self, operation_name)
: Enables attribute-style access to operations.__call__(self, *keys)
: Returns a newgroup
instance containing specified operations.- Parameters:
*keys
(str or int): One or more names or indices of operations to retrieve.
- Returns:
- Example:
python subG = G('group1', 1, 'group3') # Retrieves 'group1', second operation, and 'group3'
list(self)
: Returns a list of all operation names.find(self, name)
: Finds the index of an operation based on its name.disp(self, name)
: Displays the content of an operation.delete(self, name)
: Deletes an operation by name.copy(self, source_name, new_name)
: Copies an existing operation to a new name.rename(self, old_name, new_name)
: Renames an existing operation.reindex(self, name, new_idx)
: Changes the index of an operation.
Script Generation
code(self)
: Returns the generated LAMMPS commands as a string.dscript(self, name=None)
: Generates adscript
object containing the group's commands.script(self, name=None)
: Generates a script object containing the group's commands.pipescript(self)
: Generates apipescript
object containing each group's command as a separate script in a pipeline.
Operator Overloading
- Addition (
+
): Union of groups. - Subtraction (
-
): Subtraction of groups. - Multiplication (
*
): Intersection of groups.
These operators are overloaded in the
Operation
class and can be used to combine group operations.Internal Functionality
The class maintains a list
_operations
, which stores all the group operations defined. Each operation is an instance of theOperation
class, which represents a LAMMPS group command along with its operands and operator.Subindexing with Callable Syntax
The
group
class allows you to retrieve multiple operations by calling the instance with names or indices:__call__(self, *keys)
: Returns a newgroup
instance containing the specified operations.- Parameters:
*keys
(str or int): One or more names or indices of operations to retrieve.
- Returns:
- Example:
python subG = G('group1', 1, 'group3') # Retrieves 'group1', second operation, and 'group3'
- Notes:
- Indices are 0-based integers.
- Duplicate operations are avoided in the new
group
instance.
Integration with Scripts
The
group
class integrates withdscript
andpipescript
classes to allow advanced script management:dscript
: A dynamic script management class that handles script lines with variable substitution and conditional execution.pipescript
: Manages a pipeline of scripts, allowing sequential execution and advanced variable space management.
Important Notes
- Operator Overloading: The class overloads the
+
,-
, and*
operators for theOperation
class to perform union, subtraction, and intersection respectively. - Flexible Group Creation: Groups can be created upon instantiation or added later using concise methods.
- Variable Assignment and Group Definition: When defining groups based on variables, variable assignment and group creation are handled separately.
- Subindexing with
__call__
: The__call__
method allows you to retrieve multiple operations and create subgroups. - Error Handling: The class includes robust error checking and provides informative error messages.
- Lazy Evaluation: Group operations are stored and only evaluated when
the
evaluate
method is called. - Name Management: The class ensures that group names are unique within the instance to prevent conflicts.
Conclusion
The
group
class simplifies the management of groups in LAMMPS simulations, allowing for clear and maintainable code when dealing with complex group operations. By providing high-level abstractions, operator overloading, subindexing capabilities, and integration with script management tools, it enhances productivity and reduces the potential for errors in simulation setup.Initializes a new instance of the group class.
Parameters:
name (str, optional): Name for the group instance. If <code>None</code> or empty and <code>collection</code> is provided, generates a name based on beadtypes (e.g., "1+2+3"). groups (dict, optional): Dictionary of group definitions to create upon initialization. group_names (list or tuple, optional): List of group names to create empty groups upon initialization. collection (list, tuple, or groupcollection, optional): - If a list or tuple, it should contain <code><a title="group.groupobject" href="#group.groupobject">groupobject</a></code> instances. - If a <code><a title="group.groupcollection" href="#group.groupcollection">groupcollection</a></code> object, it will extract the <code><a title="group.groupobject" href="#group.groupobject">groupobject</a></code> instances from it. printflag (bool, optional): If <code>True</code>, enables printing of script generation. verbose (bool, optional): If <code>True</code>, enables verbose output.
Raises:
TypeError: - If <code>groups</code> is not a dictionary. - If <code>group\_names</code> is not a list or tuple. - If <code>collection</code> is not a list, tuple, or <code><a title="group.groupcollection" href="#group.groupcollection">groupcollection</a></code> object. - If any item in <code>collection</code> (when it's a list or tuple) is not a <code><a title="group.groupobject" href="#group.groupobject">groupobject</a></code> instance.
Expand source code
class group: """ A class for managing LAMMPS group operations and generating LAMMPS scripts. ### Overview The `group` class provides an object-oriented interface to define and manage groups of atoms in LAMMPS simulations. Groups in LAMMPS are collections of atoms that can be manipulated together, allowing users to apply fixes, compute properties, or perform operations on specific subsets of atoms. This class allows you to create, combine, and manipulate groups using algebraic operations (union, intersection, subtraction), and to generate the corresponding LAMMPS commands. It also provides methods to output the group commands as scripts, which can be integrated into LAMMPS input files. ### Key Features - **Create and Manage Groups**: Define new groups based on atom types, regions, IDs, or variables. - **Flexible Group Creation**: Create multiple groups at once and define groups using concise criteria through the `add_group_criteria` method. - **Algebraic Operations**: Combine groups using union (`+`), intersection (`*`), and subtraction (`-`) operations. - **Subindexing with Callable Syntax**: Retrieve multiple group operations by calling the `group` instance with names or indices. - **Script Generation**: Generate LAMMPS script lines for group definitions, which can be output as scripts or pipelines. - **Dynamic Evaluation**: Evaluate complex group expressions and store the resulting operations. - **Integration with Scripting Tools**: Convert group operations into `dscript` or `pipescript` objects for advanced script management. ### LAMMPS Context In LAMMPS (Large-scale Atomic/Molecular Massively Parallel Simulator), groups are fundamental for specifying subsets of atoms for applying operations like forces, fixes, and computes. The `group` command in LAMMPS allows users to define groups based on various criteria such as atom IDs, types, regions, and variables. This `group` class abstracts the complexity of managing group definitions and operations, providing a high-level interface to define and manipulate groups programmatically. ### Usage Examples **Creating Multiple Groups at Once** ```python # Create groups 'o1', 'o2', 'o3', 'o4' upon instantiation G = group(group_names=['o1', 'o2', 'o3', 'o4']) ``` **Defining Groups Based on Criteria** ```python G = group() G.add_group_criteria('lower', type=[1]) G.add_group_criteria('central', region='central_cyl') G.add_group_criteria('new_group', create=True) G.add_group_criteria('upper', clear=True) G.add_group_criteria('subtract_group', subtract=['group1', 'group2']) ``` **Defining a Group Based on a Variable** ```python G = group() G.variable('myVar', 'x > 5') # Assign a variable G.byvariable('myGroup', 'myVar') # Define group based on the variable ``` **Using `add_group_criteria` with Variable** ```python G.add_group_criteria('myGroup', variable={'name': 'myVar', 'expression': 'x > 5'}) ``` **Defining Groups Using a Dictionary** ```python group_definitions = { 'group1': {'type': [1, 2]}, 'group2': {'region': 'my_region'}, 'group3': {'variable': {'name': 'var1', 'expression': 'x > 5'}}, 'group4': {'union': ['group1', 'group2']}, } G.add_group_criteria(group_definitions) ``` **Creating a Group from a Collection of `groupobject` Instances** ```python # Import the classes # from groupobject import groupobject # from group import group # Create groupobject instances o1 = groupobject(beadtype=1, group=["all", "A"], mass=1.0) o2 = groupobject(beadtype=2, group=["all", "B", "C"]) o3 = groupobject(beadtype=3, group="C", mass=2.5) # Create a group instance with the collection G = group(name="mycollection", collection=[o1, o2, o3]) # or G = group(name=None, collection=[o1, o2, o3]) # Name will be generated as "1+2+3" # Generate the LAMMPS script script_content = G.code() print(script_content) ``` **Expected Output:** ``` group all type 1 2 3 group A type 1 group B type 2 group C type 2 3 ``` **Accessing Groups and Performing Operations** ```python # Access groups via attribute-style or item-style access op1 = G.group1 op2 = G['group2'] # Combine groups using algebraic operations complex_op = op1 + op2 - G.group3 # Evaluate the operation and store the result G.evaluate('combined_group', complex_op) ``` **Subindexing with Callable Syntax** ```python # Retrieve multiple operations by names or indices subG = G('group1', 2, 'group3') # Retrieves 'group1', third operation, and 'group3' # Display the names of operations in subG operation_names = [op.name for op in subG._operations] print(operation_names) # Output: ['group1', 'group3', 'group3'] # Generate the LAMMPS script for the subgroup script_content = subG.code() print(script_content) ``` **Generating LAMMPS Script** ```python script_content = G.code() print(script_content) ``` ### Class Methods #### Initialization and Setup - `__init__(self, name=None, groups=None, group_names=None, printflag=False, verbose=True)`: Initializes a new `group` instance. - `name` (str): Optional name for the group instance. - `groups` (dict): Dictionary of group definitions to create upon initialization. - `group_names` (list): List of group names to create empty groups upon initialization. - `collection` (list or tuple of groupobject, optional): Collection of groupobject instances. - `printflag` (bool): If True, enables printing of script generation. - `verbose` (bool): If True, enables verbose output. - `create_groups(self, *group_names)`: Creates multiple new groups with the given names. - `add_group_criteria(self, *args, **kwargs)`: Adds group(s) based on criteria. - Supports two usages: - `add_group_criteria(group_name, **criteria)`: Adds a single group. - `add_group_criteria(group_definitions)`: Adds multiple groups from a dictionary. #### Group Creation Methods - `create(self, group_name)`: Creates a new empty group with the given name. - `bytype(self, group_name, type_values)`: Defines a group based on atom types. - `byid(self, group_name, id_values)`: Defines a group based on atom IDs. - `byregion(self, group_name, region_name)`: Defines a group based on a region. - `variable(self, variable_name, expression, style="atom")`: Assigns an expression to a LAMMPS variable. - `byvariable(self, group_name, variable_name)`: Defines a group based on a variable. - `clear(self, group_name)`: Clears an existing group. #### Algebraic Operations - `union(self, group_name, *groups)`: Performs a union of the specified groups. - `intersect(self, group_name, *groups)`: Performs an intersection of the specified groups. - `subtract(self, group_name, *groups)`: Subtracts groups from the first group. - `evaluate(self, group_name, group_op)`: Evaluates a group operation and stores the result. #### Access and Manipulation - `__getitem__(self, key)`: Allows accessing operations by name or index. - `__getattr__(self, operation_name)`: Enables attribute-style access to operations. - `__call__(self, *keys)`: Returns a new `group` instance containing specified operations. - **Parameters**: - `*keys` (str or int): One or more names or indices of operations to retrieve. - **Returns**: - `group`: A new `group` instance containing the specified operations. - **Example**: ```python subG = G('group1', 1, 'group3') # Retrieves 'group1', second operation, and 'group3' ``` - `list(self)`: Returns a list of all operation names. - `find(self, name)`: Finds the index of an operation based on its name. - `disp(self, name)`: Displays the content of an operation. - `delete(self, name)`: Deletes an operation by name. - `copy(self, source_name, new_name)`: Copies an existing operation to a new name. - `rename(self, old_name, new_name)`: Renames an existing operation. - `reindex(self, name, new_idx)`: Changes the index of an operation. #### Script Generation - `code(self)`: Returns the generated LAMMPS commands as a string. - `dscript(self, name=None)`: Generates a `dscript` object containing the group's commands. - `script(self, name=None)`: Generates a script object containing the group's commands. - `pipescript(self)`: Generates a `pipescript` object containing each group's command as a separate script in a pipeline. ### Operator Overloading - **Addition (`+`)**: Union of groups. - **Subtraction (`-`)**: Subtraction of groups. - **Multiplication (`*`)**: Intersection of groups. These operators are overloaded in the `Operation` class and can be used to combine group operations. ### Internal Functionality The class maintains a list `_operations`, which stores all the group operations defined. Each operation is an instance of the `Operation` class, which represents a LAMMPS group command along with its operands and operator. ### Subindexing with Callable Syntax The `group` class allows you to retrieve multiple operations by calling the instance with names or indices: - `__call__(self, *keys)`: Returns a new `group` instance containing the specified operations. - **Parameters**: - `*keys` (str or int): One or more names or indices of operations to retrieve. - **Returns**: - `group`: A new `group` instance containing the specified operations. - **Example**: ```python subG = G('group1', 1, 'group3') # Retrieves 'group1', second operation, and 'group3' ``` - **Notes**: - Indices are 0-based integers. - Duplicate operations are avoided in the new `group` instance. ### Integration with Scripts The `group` class integrates with `dscript` and `pipescript` classes to allow advanced script management: - **`dscript`**: A dynamic script management class that handles script lines with variable substitution and conditional execution. - **`pipescript`**: Manages a pipeline of scripts, allowing sequential execution and advanced variable space management. ### Important Notes - **Operator Overloading**: The class overloads the `+`, `-`, and `*` operators for the `Operation` class to perform union, subtraction, and intersection respectively. - **Flexible Group Creation**: Groups can be created upon instantiation or added later using concise methods. - **Variable Assignment and Group Definition**: When defining groups based on variables, variable assignment and group creation are handled separately. - **Subindexing with `__call__`**: The `__call__` method allows you to retrieve multiple operations and create subgroups. - **Error Handling**: The class includes robust error checking and provides informative error messages. - **Lazy Evaluation**: Group operations are stored and only evaluated when the `evaluate` method is called. - **Name Management**: The class ensures that group names are unique within the instance to prevent conflicts. ### Conclusion The `group` class simplifies the management of groups in LAMMPS simulations, allowing for clear and maintainable code when dealing with complex group operations. By providing high-level abstractions, operator overloading, subindexing capabilities, and integration with script management tools, it enhances productivity and reduces the potential for errors in simulation setup. """ def __init__(self, name=None, groups=None, group_names=None, collection=None, printflag=False, verbose=True, verbosity=None): """ Initializes a new instance of the group class. ### Parameters: name (str, optional): Name for the group instance. If `None` or empty and `collection` is provided, generates a name based on beadtypes (e.g., "1+2+3"). groups (dict, optional): Dictionary of group definitions to create upon initialization. group_names (list or tuple, optional): List of group names to create empty groups upon initialization. collection (list, tuple, or groupcollection, optional): - If a list or tuple, it should contain `groupobject` instances. - If a `groupcollection` object, it will extract the `groupobject` instances from it. printflag (bool, optional): If `True`, enables printing of script generation. verbose (bool, optional): If `True`, enables verbose output. ### Raises: TypeError: - If `groups` is not a dictionary. - If `group_names` is not a list or tuple. - If `collection` is not a list, tuple, or `groupcollection` object. - If any item in `collection` (when it's a list or tuple) is not a `groupobject` instance. """ self._in_construction = True # Indicate that the object is under construction # Handle 'name' parameter if not name: name = generate_random_name() self._name = name # Initialize other attributes self._operations = [] self.printflag = printflag self.verbose = verbose if verbosity is None else verbosity>0 self.verbosity = verbosity self._in_construction = False # Set the flag to indicate construction is finished # Handle 'groups' parameter if groups: if not isinstance(groups, dict): raise TypeError("Parameter 'groups' must be a dictionary.") self.add_group_criteria(groups) # Handle 'group_names' parameter if group_names: if not isinstance(group_names, (list, tuple)): raise TypeError("Parameter 'group_names' must be a list or tuple of group names.") self.create_groups(*group_names) # Handle 'collection' parameter if collection: if isinstance(collection, groupcollection): # Extract the list of groupobject instances from the groupcollection collection = collection.collection elif not isinstance(collection, (list, tuple)): raise TypeError("Parameter 'collection' must be a list, tuple, or `groupcollection` object.") # If collection is a list or tuple, validate its items if isinstance(collection, (list, tuple)): for obj in collection: if not isinstance(obj, groupobject): raise TypeError("All items in 'collection' must be `groupobject` instances.") self.generate_group_definitions_from_collection(collection) def create_groups(self, *group_names): for group_name in group_names: if not isinstance(group_name, str): raise TypeError(f"Group name must be a string, got {type(group_name)}") self.create(group_name) def __str__(self): return f'Group "{self._name}" with {len(self._operations)} operations\n' def format_cell_content(self, content, max_width): content = str(content) if content is not None else '' if len(content) > max_width: start = content[: (max_width - 5) // 2] end = content[-((max_width - 5) // 2):] content = f"{start} ... {end}" return content def __repr__(self): """ Returns a neatly formatted table representation of the group's operations. Each row represents an operation in the group, displaying its index, name, operator, and operands. The table adjusts column widths dynamically and truncates content based on maximum column widths. ### Returns: str: A formatted string representation of the group operations. """ # Define headers for the table headers = ["Idx", "Name", "Operator", "Operands"] col_max_widths = [5, 20, 20, 40] align = ["R", "C", "C", "L"] # Prepare rows by iterating over the operations rows = [] for idx, op in enumerate(self._operations): rows.append([ str(idx), # Index str(op.name), # Name str(op.operator), # Operator str(span(op.operands)), # Operands ]) # Use the helper to format the table table = format_table(headers, rows, col_max_widths, align) # Append the string representation of the group itself return f"{table}\n\n{str(self)}" def __len__(self): """ return the number of stored operations """ return len(self._operations) def list(self): """ return the list of all operations """ return [op.name for op in self._operations] def code(self): """ Joins the `code` attributes of all stored `operation` objects with '\n'. """ return '\n'.join([op.code for op in self._operations]) def find(self, name): """Returns the index of an operation based on its name.""" if '_operations' in self.__dict__: for i, op in enumerate(self._operations): if op.name == name: return i return None def disp(self, name): """ display the content of an operation """ idx = self.find(name) if idx is not None: return self._operations[idx].__repr__() else: return "Operation not found" def clearall(self): """ clear all operations """ self._operations = [] def delete(self, name): """ Deletes one or more stored operations based on their names. Parameters: ----------- name : str, list, or tuple The name(s) of the operation(s) to delete. If a list or tuple is provided, all specified operations will be deleted. Usage: ------ G.delete('operation_name') G.delete(['operation1', 'operation2']) G.delete(('operation1', 'operation2')) Raises: ------- ValueError If any of the specified operations are not found. """ # Handle a single string, list, or tuple if isinstance(name, (list, tuple)): not_found = [] for n in name: idx = self.find(n) if idx is not None: del self._operations[idx] else: not_found.append(n) # If any names were not found, raise an exception if not_found: raise ValueError(f"Operation(s) {', '.join(not_found)} not found.") elif isinstance(name, str): idx = self.find(name) if idx is not None: del self._operations[idx] else: raise ValueError(f"Operation {name} not found.") else: raise TypeError("The 'name' parameter must be a string, list, or tuple.") def copy(self, source_name, new_name): """ Copies a stored operation to a new operation with a different name. Parameters: source_name: str Name of the source operation to copy new_name: str Name of the new operation Usage: G.copy('source_operation', 'new_operation') """ idx = self.find(source_name) if idx is not None: copied_operation = self._operations[idx].clone() copied_operation.name = new_name self.add_operation(copied_operation) else: raise ValueError(f"Operation {source_name} not found.") def rename(self, old_name, new_name): """ Rename a stored operation. Parameters: old_name: str Current name of the operation new_name: str New name to assign to the operation Usage: G.rename('old_operation', 'new_operation') """ idx = self.find(old_name) if idx is not None: if new_name == old_name: raise ValueError("The new name should be different from the previous one.") elif new_name in self.list(): raise ValueError("Operation name must be unique.") self._operations[idx].name = new_name else: raise ValueError(f"Operation '{old_name}' not found.") def reindex(self, name, new_idx): """ Change the index of a stored operation. Parameters: name: str Name of the operation to reindex new_idx: int New index for the operation Usage: G.reindex('operation_name', 2) """ idx = self.find(name) if idx is not None and 0 <= new_idx < len(self._operations): op = self._operations.pop(idx) self._operations.insert(new_idx, op) else: raise ValueError(f"Operation '{name}' not found or new index {new_idx} out of range.") # ------------- indexing and attribute overloading def __getitem__(self, key): """ Enable shorthand for G.operations[G.find(operation_name)] using G[operation_name], or accessing operation by index using G[index]. """ if isinstance(key, str): idx = self.find(key) if idx is not None: return self._operations[idx] else: raise KeyError(f"Operation '{key}' not found.") elif isinstance(key, int): if -len(self._operations) <= key < len(self._operations): return self._operations[key] else: raise IndexError("Operation index out of range.") else: raise TypeError("Key must be an operation name (string) or index (integer).") def __getattr__(self, name): """ Allows accessing operations via attribute-style notation. If the attribute is one of the core attributes, returns it directly. For other attributes, searches for an operation with a matching name in the _operations list. Parameters: ----------- name : str The name of the attribute or operation to access. Returns: -------- The value of the attribute if it's a core attribute, or the operation associated with the specified name if found in _operations. Raises: ------- AttributeError If the attribute or operation is not found. """ # Handle core attributes directly if name in {'_name', '_operations', 'printflag', 'verbose'}: # Use object.__getattribute__ to avoid recursion return object.__getattribute__(self, name) # Search for the operation in _operations elif '_operations' in self.__dict__: for op in self._operations: if op.name == name: return op # If not found, raise an AttributeError raise AttributeError(f"Attribute or operation '{name}' not found.") def __setattr__(self, name, value): """ Allows deletion of an operation via 'G.operation_name = []' after construction. During construction, attributes are set normally. """ if getattr(self, '_in_construction', True): # During construction, set attributes normally super().__setattr__(name, value) else: # After construction if isinstance(value, list) and len(value) == 0: # Handle deletion syntax idx = self.find(name) if idx is not None: del self._operations[idx] else: raise AttributeError(f"Operation '{name}' not found for deletion.") else: # Set attribute normally super().__setattr__(name, value) def _get_subobject(self, key): """ Retrieves a subobject based on the provided key. Parameters: ----------- key : str or int The key used to retrieve the subobject. Returns: -------- Operation The operation corresponding to the key. Raises: ------- KeyError If the key is not found. IndexError If the index is out of range. TypeError If the key is not a string or integer. """ if isinstance(key, str): # If key is a string, treat it as a group name for operation in self._operations: if operation.name == key: return operation raise KeyError(f"No operation found with name '{key}'.") elif isinstance(key, int): # If key is an integer, treat it as an index (0-based) if 0 <= key < len(self._operations): return self._operations[key] else: raise IndexError(f"Index {key} is out of range.") else: raise TypeError("Key must be a string or integer.") def __call__(self, *keys): """ Allows subindexing of the group object using callable syntax with multiple keys. Parameters: ----------- *keys : str or int One or more keys used to retrieve subobjects or perform subindexing. Returns: -------- group A new group instance containing the specified operations. Example: -------- subG = G('a', 1, 'c') # Retrieves operations 'a', second operation, and 'c' """ selected_operations = [] for key in keys: operation = self._get_subobject(key) selected_operations.append(operation) # Create a new group instance with the selected operations new_group = group(name=f"{self._name}_subgroup", printflag=self.printflag, verbose=self.verbose) new_group._operations = selected_operations return new_group def operation_exists(self,operation_name): """ Returns true if "operation_name" exists To be used by Operation, not by end-user, which should prefer find() """ return any(op.name == operation_name for op in self._operations) def get_by_name(self,operation_name): """ Returns the operation matching "operation_name" Usage: group.get_by_name("operation_name") To be used by Operation, not by end-user, which should prefer getattr() """ for op in self._operations: if op.name == operation_name: return op raise AttributeError(f"Operation with name '{operation_name}' not found.") def add_operation(self,operation): """ add an operation """ if operation.name in self.list(): raise ValueError(f"The operation '{operation.name}' already exists.") else: self._operations.append(operation) # --------- LAMMPS methods def variable(self, variable_name, expression, style="atom"): """ Assigns an expression to a LAMMPS variable. Parameters: - variable_name (str): The name of the variable to be assigned. - expression (str): The expression to assign to the variable. - style (str): The type of variable (default is "atom"). """ if not isinstance(variable_name, str): raise TypeError(f"Variable name must be a string, got {type(variable_name)}") if not isinstance(expression, str): raise TypeError(f"Expression must be a string, got {type(expression)}") if not isinstance(style, str): raise TypeError(f"Style must be a string, got {type(style)}") lammps_code = f"variable {variable_name} {style} \"{expression}\"" op = Operation("variable", [variable_name, expression], code=lammps_code) self.add_operation(op) def byvariable(self, group_name, variable_name): """ Sets a group of atoms based on a variable. Parameters: - group_name: str, the name of the group. - variable_name: str, the name of the variable to define the group. """ if not isinstance(group_name, str): raise TypeError(f"Group name must be a string, got {type(group_name)}") if not isinstance(variable_name, str): raise TypeError(f"Variable name must be a string, got {type(variable_name)}") lammps_code = f"group {group_name} variable {variable_name}" op = Operation("byvariable", [variable_name], name=group_name, code=lammps_code) self.add_operation(op) def byregion(self, group_name, region_name): """ set a group of atoms based on a regionID G.region(group_name,regionID) """ lammps_code = f"group {group_name} region {region_name}" criteria = {"region": region_name} op = Operation("byregion", [region_name], name=group_name, code=lammps_code, criteria=criteria) self.add_operation(op) def bytype(self, group_name, type_values): """ select atoms by type and store them in group G.type(group_name,type_values) """ if not isinstance(type_values, (list, tuple)): type_values = [type_values] lammps_code = f"group {group_name} type {span(type_values)}" criteria = {"type": type_values} op = Operation("bytype", type_values, name=group_name, code=lammps_code, criteria=criteria) self.add_operation(op) def byid(self, group_name, id_values): """ select atoms by id and store them in group G.id(group_name,id_values) """ if not isinstance(id_values, (list, tuple)): id_values = [id_values] lammps_code = f"group {group_name} id {span(id_values)}" criteria = {"id": id_values} op = Operation("byid", id_values, name=group_name, code=lammps_code, criteria=criteria) self.add_operation(op) def create(self, group_name): """ create group G.create(group_name) """ lammps_code = f"group {group_name} clear" criteria = {"clear": True} op = Operation("create", [], name=group_name, code=lammps_code, criteria=criteria) self.add_operation(op) def clear(self, group_name): """ clear group G.clear(group_name) """ lammps_code = f"group {group} clear" criteria = {"clear": True} op = Operation("clear", [], name=group_name, code=lammps_code, criteria=criteria) self.add_operation(op) def union(self,group_name, *groups): """ Union group1, group2, group3 and store the result in group_name. Example usage: group.union(group_name, group1, group2, group3,...) """ lammps_code = f"group {group_name} union {span(groups)}" criteria = {"union": groups} op = Operation("union", groups, name=group_name, code=lammps_code, criteria=criteria) self.add_operation(op) def intersect(self,group_name, *groups): """ Intersect group1, group2, group3 and store the result in group_name. Example usage: group.intersect(group_name, group1, group2, group3,...) """ lammps_code = f"group {group_name} intersect {span(groups)}" criteria = {"intersect": groups} op = Operation("intersect", groups, name=group_name, code=lammps_code, criteria=criteria) self.add_operation(op) def subtract(self,group_name, *groups): """ Subtract group2, group3 from group1 and store the result in group_name. Example usage: group.subtract(group_name, group1, group2, group3,...) """ lammps_code = f"group {group_name} subtract {span(groups)}" criteria = {"subtract": groups} op = Operation("subtract", groups, name=group_name, code=lammps_code, criteria=criteria) self.add_operation(op) def evaluate(self, group_name, group_op): """ Evaluates the operation and stores the result in a new group. Expressions could combine +, - and * like o1+o2+o3-o4+o5+o6 Parameters: ----------- groupname : str The name of the group that will store the result. group_op : Operation The operation to evaluate. """ if not isinstance(group_op, Operation): raise TypeError("Expected an instance of Operation.") if group_name in self.list(): raise ValueError(f"The operation '{group_name}' already exists.") # If the operation is already finalized, no need to evaluate if group_op.isfinalized(): if group_op.name != group_name: # If names differ, create a copy with the new name self.copy(group_op.name, group_name) return # Recursively evaluate operands operand_names = [] for op in group_op.operands: if isinstance(op, Operation): if op.isfinalized(): # Use existing finalized operation name operand_names.append(op.name) else: # Generate a unique name if the operation doesn't have one if not op.name: op.name = op.generate_hashname() # Recursively evaluate the operand operation self.evaluate(op.name, op) operand_names.append(op.name) else: operand_names.append(str(op)) # Call the appropriate method based on the operator if group_op.operator == '+': self.union(group_name, *operand_names) group_op.operator = 'union' elif group_op.operator == '-': self.subtract(group_name, *operand_names) group_op.operator = 'subtract' elif group_op.operator == '*': self.intersect(group_name, *operand_names) group_op.operator = 'intersect' else: raise ValueError(f"Unknown operator: {group_op.operator}") # Update the operation group_op.name = group_name # Get the last added operation finalized_op = self._operations[-1] group_op.code = finalized_op.code group_op.operands = operand_names # Add the operation to the group's _operations if not already added if group_op.name not in self.list(): self._operations.append(group_op) def add_group_criteria(self, *args, **kwargs): """ Adds group(s) using existing methods based on key-value pairs. Supports two usages: 1. add_group_criteria(group_name, **criteria) 2. add_group_criteria(group_definitions) Parameters: - group_name (str): The name of the group. - **criteria: Criteria for group creation. OR - group_definitions (dict): A dictionary where keys are group names and values are criteria dictionaries. Raises: - TypeError: If arguments are invalid. Usage: - G.add_group_criteria('group_name', type=[1,2]) - G.add_group_criteria({'group1': {'type': [1]}, 'group2': {'region': 'regionID'}}) """ if len(args) == 1 and isinstance(args[0], dict): # Called with group_definitions dict group_definitions = args[0] for group_name, criteria in group_definitions.items(): self.add_group_criteria_single(group_name, **criteria) elif len(args) == 1 and isinstance(args[0], str): # Called with group_name and criteria group_name = args[0] if not kwargs: raise ValueError(f"No criteria provided for group '{group_name}'.") self.add_group_criteria_single(group_name, **kwargs) else: raise TypeError("Invalid arguments. Use add_group_criteria(group_name, **criteria) or add_group_criteria(group_definitions).") def add_group_criteria_single(self, group_name, **criteria): """ Adds a single group based on criteria. Parameters: - group_name (str): The name of the group. - **criteria: Criteria for group creation. Raises: - TypeError: If group_name is not a string. - ValueError: If no valid criteria are provided or if criteria are invalid. Example (advanced): G = group() group_definitions = { 'myGroup': { 'variable': { 'name': 'myVar', 'expression': 'x > 5', 'style': 'atom' } } } G.add_group_criteria(group_definitions) print(G.code()) Expected output variable myVar atom "x > 5" group myGroup variable myVar """ if not isinstance(group_name, str): raise TypeError(f"Group name must be a string, got {type(group_name)}") if not criteria: raise ValueError(f"No criteria provided for group '{group_name}'.") if "type" in criteria: type_values = criteria["type"] if not isinstance(type_values, (list, tuple, int)): raise TypeError("Type values must be an integer or a list/tuple of integers.") self.bytype(group_name, type_values) elif "region" in criteria: region_name = criteria["region"] if not isinstance(region_name, str): raise TypeError("Region name must be a string.") self.byregion(group_name, region_name) elif "id" in criteria: id_values = criteria["id"] if not isinstance(id_values, (list, tuple, int)): raise TypeError("ID values must be an integer or a list/tuple of integers.") self.byid(group_name, id_values) elif "variable" in criteria: var_info = criteria["variable"] if not isinstance(var_info, dict): raise TypeError("Variable criteria must be a dictionary.") required_keys = {'name', 'expression'} if not required_keys.issubset(var_info.keys()): missing = required_keys - var_info.keys() raise ValueError(f"Variable criteria missing keys: {missing}") var_name = var_info['name'] expression = var_info['expression'] style = var_info.get('style', 'atom') # First, assign the variable self.variable(var_name, expression, style) # Then, create the group based on the variable self.byvariable(group_name, var_name) elif "union" in criteria: groups = criteria["union"] if not isinstance(groups, (list, tuple)): raise TypeError("Union groups must be a list or tuple of group names.") self.union(group_name, *groups) elif "intersect" in criteria: groups = criteria["intersect"] if not isinstance(groups, (list, tuple)): raise TypeError("Intersect groups must be a list or tuple of group names.") self.intersect(group_name, *groups) elif "subtract" in criteria: groups = criteria["subtract"] if not isinstance(groups, (list, tuple)): raise TypeError("Subtract groups must be a list or tuple of group names.") self.subtract(group_name, *groups) elif "create" in criteria and criteria["create"]: self.create(group_name) elif "clear" in criteria and criteria["clear"]: self.clear(group_name) else: raise ValueError(f"No valid criterion provided for group '{group_name}'.") def get_group_criteria(self, group_name): """ Retrieve the criteria that define a group. Handles group_name as a string or number. Parameters: - group_name: str or int, the name or number of the group. Returns: - dict or str: The criteria used to define the group, or a message if defined by multiple criteria. Raises: - ValueError: If the group does not exist. """ # Check if group_name exists in _operations if group_name not in self._operations: raise ValueError(f"Group '{group_name}' does not exist.") # Retrieve all operations related to the group directly from _operations operations = self._operations[group_name] if not operations: raise ValueError(f"No operations found for group '{group_name}'.") criteria = {} for op in operations: if op.criteria: criteria.update(op.criteria) # If multiple criteria, return a message if len(criteria) > 1: return f"Group '{group_name}' is defined by multiple criteria: {criteria}" return criteria def generate_group_definitions_from_collection(self,collection): """ Generates group definitions based on the collection of groupobject instances. This method populates the groups based on beadtypes and associated group names. """ group_defs = {} for obj in collection: for group_name in obj.group: if group_name not in group_defs: group_defs[group_name] = {'type': []} if obj.beadtype not in group_defs[group_name]['type']: group_defs[group_name]['type'].append(obj.beadtype) # Now, add these group definitions self.add_group_criteria(group_defs) def dscript(self, name=None, printflag=None, verbose=None, verbosity=None): """ Generates a dscript object containing the group's LAMMPS commands. Parameters: - name (str): Optional name for the script object. - printflag (bool, default=False): print on the current console if True - verbose (bool, default=True): keep comments if True Returns: - dscript: A dscript object containing the group's code. """ if name is None: name = self._name 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 # Create a new dscript object dscript_obj = dscript(name=name,printflag=printflag, verbose=verbose, verbosity=verbosity) # Add each line of the group's code to the script object for idx, op in enumerate(self._operations): # Use the index as the key for the script line dscript_obj[idx] = op.code return dscript_obj def script(self, name=None, printflag=None, verbose=None, verbosity=None): """ Generates a script object containing the group's LAMMPS commands. Parameters: - name (str): Optional name for the script object. - printflag (bool, default=False): print on the current console if True - verbose (bool, default=True): keep comments if True Returns: - script: A script object containing the group's code. """ if name is None: name = self._name 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 script_obj = self.dscript(name=name,printflag=printflag, verbose=verbose, verbosity=verbosity).script(printflag=printflag, verbose=verbose, verbosity=verbosity) return script_obj def pipescript(self, printflag=None, verbose=None, verbosity=None): """ Generates a pipescript object containing the group's LAMMPS commands. Parameters: - printflag (bool, default=False): print on the current console if True - verbose (bool, default=True): keep comments if True Returns: - pipescript: A pipescript object containing the group's code lines as individual scripts in the pipeline. """ 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 # Create a list to hold script objects script_list = [] # For each operation, create a script object and add it to the list for op in self._operations: # Create a dscript object with the code line dscript_obj = dscript(printflag=printflag, verbose=verbose, verbosity=verbosity) dscript_obj["dummy"] = op.code # Convert the dscript to a script object script_obj = dscript_obj.script(printflag=printflag, verbose=verbose, verbosity=verbosity) # Add the script object to the list script_list.append(script_obj) # Use the static method 'join' to create a pipescript from the list if script_list: pipe_obj = pipescript.join(script_list) else: pipe_obj = pipescript() return pipe_obj # 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, copy.deepcopy(v, memo)) return copie def count(self,name=None, selection: Optional[List[str]] = None) -> 'dscript': """ Generates DSCRIPT counters for specified groups with LAMMPS variable definitions and print commands. The method retrieves the list of group names using `self.list()`. If `selection` is provided, it filters the groups to include only those specified. It then creates a variable for each selected group that counts the number of atoms in that group and generates corresponding print commands. The commands are encapsulated within a `dscript` object for execution. ### Parameters: selection (list of str, optional): - List of group names to be numbered. - If `None`, all groups in the collection are numbered. ### Returns: dscript: A `dscript` object containing the variable definitions and print commands, formatted as follows: ``` variable n_lower equal "count(lower)" variable n_middle equal "count(middle)" variable n_upper equal "count(upper)" print "Number of atoms in lower: ${n_lower}" print "Number of atoms in middle: ${n_middle}" print "Number of atoms in upper: ${n_upper}" ``` The `variables` attribute holds the variable definitions, and the `printvariables` attribute holds the print commands, each separated by a newline. ### Raises: ValueError: - If any group specified in `selection` does not exist in the collection. - If `selection` contains duplicate group names. TypeError: - If `selection` is not a list of strings. ### Example: ```python # Create groupobject instances g1 = groupobject(beadtype=1,group = 'lower') g2 = groupobject(beadtype=2,group = 'middle') g3 = groupobject(beadtype=3,group = 'upper') # Initialize a group with the groupobjects G = group(name="1+2+3",collection=(g1,g2,g3)) or collection=g1+g2+g3 # add other groups G.evaluate("all", G.lower + G.middle + G.upper) G.evaluate("external", G.all - G.middle) # Generate number commands for all groups N = G.count(selection=["all","lower","middle","lower"]) print(N.do()) ``` **Output:** ``` variable n_all equal "count(all)" variable n_lower equal "count(lower)" variable n_middle equal "count(middle)" variable n_upper equal "count(upper)" print "Number of atoms in all: ${n_all}" print "Number of atoms in lower: ${n_lower}" print "Number of atoms in middle: ${n_middle}" print "Number of atoms in upper: ${n_upper}" ``` """ # Retrieve the list of all group names all_group_names = self.list() # If selection is provided, validate it if selection is not None: if not isinstance(selection, (list, tuple)): raise TypeError("Parameter 'selection' must be a list of group names.") if not all(isinstance(gitem, str) for gitem in selection): raise TypeError("All items in 'selection' must be strings representing group names.") if len(selection) != len(set(selection)): raise ValueError("Duplicate group names found in 'selection'. Each group should be unique.") # Check that all selected groups exist missing_groups = set(selection) - set(all_group_names) if missing_groups: raise ValueError(f"The following groups are not present in the collection: {', '.join(missing_groups)}") # Use the selected groups target_groups = selection else: # If no selection, target all groups target_groups = all_group_names # Initialize lists to hold variable definitions and print commands variable_definitions = [] print_commands = [] for gitem in target_groups: # Validate group name if not gitem: raise ValueError("Group names must be non-empty strings.") # Create a valid variable name by replacing any non-alphanumeric characters with underscores variable_name = f"n_{gitem}".replace(" ", "_").replace("-", "_") # Define the variable to count the number of atoms in the group variable_definitions.append(f'variable {variable_name} equal "count({gitem})"') # Define the corresponding print command print_commands.append(f'print "Number of atoms in {gitem}: ${{{variable_name}}}"') # Create a dscript object with the generated commands idD = f"<dscript:group:{self._name}:count>" D = dscript( name=idD if name is None else name, description=f"{idD} for group counts" ) D.variables = "\n".join(variable_definitions) D.printvariables = "\n".join(print_commands) D.set_all_variables() return D
Methods
def add_group_criteria(self, *args, **kwargs)
-
Adds group(s) using existing methods based on key-value pairs.
Supports two usages: 1. add_group_criteria(group_name, **criteria) 2. add_group_criteria(group_definitions)
Parameters: - group_name (str): The name of the group. - **criteria: Criteria for group creation.
OR
- group_definitions (dict): A dictionary where keys are group names and values are criteria dictionaries.
Raises: - TypeError: If arguments are invalid.
Usage: - G.add_group_criteria('group_name', type=[1,2]) - G.add_group_criteria({'group1': {'type': [1]}, 'group2': {'region': 'regionID'}})
Expand source code
def add_group_criteria(self, *args, **kwargs): """ Adds group(s) using existing methods based on key-value pairs. Supports two usages: 1. add_group_criteria(group_name, **criteria) 2. add_group_criteria(group_definitions) Parameters: - group_name (str): The name of the group. - **criteria: Criteria for group creation. OR - group_definitions (dict): A dictionary where keys are group names and values are criteria dictionaries. Raises: - TypeError: If arguments are invalid. Usage: - G.add_group_criteria('group_name', type=[1,2]) - G.add_group_criteria({'group1': {'type': [1]}, 'group2': {'region': 'regionID'}}) """ if len(args) == 1 and isinstance(args[0], dict): # Called with group_definitions dict group_definitions = args[0] for group_name, criteria in group_definitions.items(): self.add_group_criteria_single(group_name, **criteria) elif len(args) == 1 and isinstance(args[0], str): # Called with group_name and criteria group_name = args[0] if not kwargs: raise ValueError(f"No criteria provided for group '{group_name}'.") self.add_group_criteria_single(group_name, **kwargs) else: raise TypeError("Invalid arguments. Use add_group_criteria(group_name, **criteria) or add_group_criteria(group_definitions).")
def add_group_criteria_single(self, group_name, **criteria)
-
Adds a single group based on criteria.
Parameters: - group_name (str): The name of the group. - **criteria: Criteria for group creation.
Raises: - TypeError: If group_name is not a string. - ValueError: If no valid criteria are provided or if criteria are invalid.
Example (advanced): G = group() group_definitions = { 'myGroup': { 'variable': { 'name': 'myVar', 'expression': 'x > 5', 'style': 'atom' } } } G.add_group_criteria(group_definitions) print(G.code())
Expected output variable myVar atom "x > 5" group myGroup variable myVar
Expand source code
def add_group_criteria_single(self, group_name, **criteria): """ Adds a single group based on criteria. Parameters: - group_name (str): The name of the group. - **criteria: Criteria for group creation. Raises: - TypeError: If group_name is not a string. - ValueError: If no valid criteria are provided or if criteria are invalid. Example (advanced): G = group() group_definitions = { 'myGroup': { 'variable': { 'name': 'myVar', 'expression': 'x > 5', 'style': 'atom' } } } G.add_group_criteria(group_definitions) print(G.code()) Expected output variable myVar atom "x > 5" group myGroup variable myVar """ if not isinstance(group_name, str): raise TypeError(f"Group name must be a string, got {type(group_name)}") if not criteria: raise ValueError(f"No criteria provided for group '{group_name}'.") if "type" in criteria: type_values = criteria["type"] if not isinstance(type_values, (list, tuple, int)): raise TypeError("Type values must be an integer or a list/tuple of integers.") self.bytype(group_name, type_values) elif "region" in criteria: region_name = criteria["region"] if not isinstance(region_name, str): raise TypeError("Region name must be a string.") self.byregion(group_name, region_name) elif "id" in criteria: id_values = criteria["id"] if not isinstance(id_values, (list, tuple, int)): raise TypeError("ID values must be an integer or a list/tuple of integers.") self.byid(group_name, id_values) elif "variable" in criteria: var_info = criteria["variable"] if not isinstance(var_info, dict): raise TypeError("Variable criteria must be a dictionary.") required_keys = {'name', 'expression'} if not required_keys.issubset(var_info.keys()): missing = required_keys - var_info.keys() raise ValueError(f"Variable criteria missing keys: {missing}") var_name = var_info['name'] expression = var_info['expression'] style = var_info.get('style', 'atom') # First, assign the variable self.variable(var_name, expression, style) # Then, create the group based on the variable self.byvariable(group_name, var_name) elif "union" in criteria: groups = criteria["union"] if not isinstance(groups, (list, tuple)): raise TypeError("Union groups must be a list or tuple of group names.") self.union(group_name, *groups) elif "intersect" in criteria: groups = criteria["intersect"] if not isinstance(groups, (list, tuple)): raise TypeError("Intersect groups must be a list or tuple of group names.") self.intersect(group_name, *groups) elif "subtract" in criteria: groups = criteria["subtract"] if not isinstance(groups, (list, tuple)): raise TypeError("Subtract groups must be a list or tuple of group names.") self.subtract(group_name, *groups) elif "create" in criteria and criteria["create"]: self.create(group_name) elif "clear" in criteria and criteria["clear"]: self.clear(group_name) else: raise ValueError(f"No valid criterion provided for group '{group_name}'.")
def add_operation(self, operation)
-
add an operation
Expand source code
def add_operation(self,operation): """ add an operation """ if operation.name in self.list(): raise ValueError(f"The operation '{operation.name}' already exists.") else: self._operations.append(operation)
def byid(self, group_name, id_values)
-
select atoms by id and store them in group G.id(group_name,id_values)
Expand source code
def byid(self, group_name, id_values): """ select atoms by id and store them in group G.id(group_name,id_values) """ if not isinstance(id_values, (list, tuple)): id_values = [id_values] lammps_code = f"group {group_name} id {span(id_values)}" criteria = {"id": id_values} op = Operation("byid", id_values, name=group_name, code=lammps_code, criteria=criteria) self.add_operation(op)
def byregion(self, group_name, region_name)
-
set a group of atoms based on a regionID G.region(group_name,regionID)
Expand source code
def byregion(self, group_name, region_name): """ set a group of atoms based on a regionID G.region(group_name,regionID) """ lammps_code = f"group {group_name} region {region_name}" criteria = {"region": region_name} op = Operation("byregion", [region_name], name=group_name, code=lammps_code, criteria=criteria) self.add_operation(op)
def bytype(self, group_name, type_values)
-
select atoms by type and store them in group G.type(group_name,type_values)
Expand source code
def bytype(self, group_name, type_values): """ select atoms by type and store them in group G.type(group_name,type_values) """ if not isinstance(type_values, (list, tuple)): type_values = [type_values] lammps_code = f"group {group_name} type {span(type_values)}" criteria = {"type": type_values} op = Operation("bytype", type_values, name=group_name, code=lammps_code, criteria=criteria) self.add_operation(op)
def byvariable(self, group_name, variable_name)
-
Sets a group of atoms based on a variable.
Parameters: - group_name: str, the name of the group. - variable_name: str, the name of the variable to define the group.
Expand source code
def byvariable(self, group_name, variable_name): """ Sets a group of atoms based on a variable. Parameters: - group_name: str, the name of the group. - variable_name: str, the name of the variable to define the group. """ if not isinstance(group_name, str): raise TypeError(f"Group name must be a string, got {type(group_name)}") if not isinstance(variable_name, str): raise TypeError(f"Variable name must be a string, got {type(variable_name)}") lammps_code = f"group {group_name} variable {variable_name}" op = Operation("byvariable", [variable_name], name=group_name, code=lammps_code) self.add_operation(op)
def clear(self, group_name)
-
clear group G.clear(group_name)
Expand source code
def clear(self, group_name): """ clear group G.clear(group_name) """ lammps_code = f"group {group} clear" criteria = {"clear": True} op = Operation("clear", [], name=group_name, code=lammps_code, criteria=criteria) self.add_operation(op)
def clearall(self)
-
clear all operations
Expand source code
def clearall(self): """ clear all operations """ self._operations = []
def code(self)
-
Joins the
code
attributes of all storedoperation
objects with ' '.Expand source code
def code(self): """ Joins the `code` attributes of all stored `operation` objects with '\n'. """ return '\n'.join([op.code for op in self._operations])
def copy(self, source_name, new_name)
-
Copies a stored operation to a new operation with a different name.
Parameters: source_name: str Name of the source operation to copy new_name: str Name of the new operation
Usage: G.copy('source_operation', 'new_operation')
Expand source code
def copy(self, source_name, new_name): """ Copies a stored operation to a new operation with a different name. Parameters: source_name: str Name of the source operation to copy new_name: str Name of the new operation Usage: G.copy('source_operation', 'new_operation') """ idx = self.find(source_name) if idx is not None: copied_operation = self._operations[idx].clone() copied_operation.name = new_name self.add_operation(copied_operation) else: raise ValueError(f"Operation {source_name} not found.")
def count(self, name=None, selection: Optional[List[str]] = None) ‑> pizza.dscript.dscript
-
Generates DSCRIPT counters for specified groups with LAMMPS variable definitions and print commands.
The method retrieves the list of group names using
self.list()
. Ifselection
is provided, it filters the groups to include only those specified. It then creates a variable for each selected group that counts the number of atoms in that group and generates corresponding print commands. The commands are encapsulated within adscript
object for execution.Parameters:
selection (list of str, optional): - List of group names to be numbered. - If <code>None</code>, all groups in the collection are numbered.
Returns:
dscript: A <code><a title="group.dscript" href="#group.dscript">dscript</a></code> object containing the variable definitions and print commands, formatted as follows: ``` variable n_lower equal "count(lower)" variable n_middle equal "count(middle)" variable n_upper equal "count(upper)" print "Number of atoms in lower: ${n_lower}" print "Number of atoms in middle: ${n_middle}" print "Number of atoms in upper: ${n_upper}" ``` The <code>variables</code> attribute holds the variable definitions, and the <code>printvariables</code> attribute holds the print commands, each separated by a newline.
Raises:
ValueError: - If any group specified in <code>selection</code> does not exist in the collection. - If <code>selection</code> contains duplicate group names. TypeError: - If <code>selection</code> is not a list of strings.
Example:
```python # Create groupobject instances g1 = groupobject(beadtype=1,group = 'lower') g2 = groupobject(beadtype=2,group = 'middle') g3 = groupobject(beadtype=3,group = 'upper') # Initialize a group with the groupobjects G = group(name="1+2+3",collection=(g1,g2,g3)) or collection=g1+g2+g3 # add other groups G.evaluate("all", G.lower + G.middle + G.upper) G.evaluate("external", G.all - G.middle) # Generate number commands for all groups N = G.count(selection=["all","lower","middle","lower"]) print(N.do()) ``` **Output:** ``` variable n_all equal "count(all)" variable n_lower equal "count(lower)" variable n_middle equal "count(middle)" variable n_upper equal "count(upper)" print "Number of atoms in all: ${n_all}" print "Number of atoms in lower: ${n_lower}" print "Number of atoms in middle: ${n_middle}" print "Number of atoms in upper: ${n_upper}" ```
Expand source code
def count(self,name=None, selection: Optional[List[str]] = None) -> 'dscript': """ Generates DSCRIPT counters for specified groups with LAMMPS variable definitions and print commands. The method retrieves the list of group names using `self.list()`. If `selection` is provided, it filters the groups to include only those specified. It then creates a variable for each selected group that counts the number of atoms in that group and generates corresponding print commands. The commands are encapsulated within a `dscript` object for execution. ### Parameters: selection (list of str, optional): - List of group names to be numbered. - If `None`, all groups in the collection are numbered. ### Returns: dscript: A `dscript` object containing the variable definitions and print commands, formatted as follows: ``` variable n_lower equal "count(lower)" variable n_middle equal "count(middle)" variable n_upper equal "count(upper)" print "Number of atoms in lower: ${n_lower}" print "Number of atoms in middle: ${n_middle}" print "Number of atoms in upper: ${n_upper}" ``` The `variables` attribute holds the variable definitions, and the `printvariables` attribute holds the print commands, each separated by a newline. ### Raises: ValueError: - If any group specified in `selection` does not exist in the collection. - If `selection` contains duplicate group names. TypeError: - If `selection` is not a list of strings. ### Example: ```python # Create groupobject instances g1 = groupobject(beadtype=1,group = 'lower') g2 = groupobject(beadtype=2,group = 'middle') g3 = groupobject(beadtype=3,group = 'upper') # Initialize a group with the groupobjects G = group(name="1+2+3",collection=(g1,g2,g3)) or collection=g1+g2+g3 # add other groups G.evaluate("all", G.lower + G.middle + G.upper) G.evaluate("external", G.all - G.middle) # Generate number commands for all groups N = G.count(selection=["all","lower","middle","lower"]) print(N.do()) ``` **Output:** ``` variable n_all equal "count(all)" variable n_lower equal "count(lower)" variable n_middle equal "count(middle)" variable n_upper equal "count(upper)" print "Number of atoms in all: ${n_all}" print "Number of atoms in lower: ${n_lower}" print "Number of atoms in middle: ${n_middle}" print "Number of atoms in upper: ${n_upper}" ``` """ # Retrieve the list of all group names all_group_names = self.list() # If selection is provided, validate it if selection is not None: if not isinstance(selection, (list, tuple)): raise TypeError("Parameter 'selection' must be a list of group names.") if not all(isinstance(gitem, str) for gitem in selection): raise TypeError("All items in 'selection' must be strings representing group names.") if len(selection) != len(set(selection)): raise ValueError("Duplicate group names found in 'selection'. Each group should be unique.") # Check that all selected groups exist missing_groups = set(selection) - set(all_group_names) if missing_groups: raise ValueError(f"The following groups are not present in the collection: {', '.join(missing_groups)}") # Use the selected groups target_groups = selection else: # If no selection, target all groups target_groups = all_group_names # Initialize lists to hold variable definitions and print commands variable_definitions = [] print_commands = [] for gitem in target_groups: # Validate group name if not gitem: raise ValueError("Group names must be non-empty strings.") # Create a valid variable name by replacing any non-alphanumeric characters with underscores variable_name = f"n_{gitem}".replace(" ", "_").replace("-", "_") # Define the variable to count the number of atoms in the group variable_definitions.append(f'variable {variable_name} equal "count({gitem})"') # Define the corresponding print command print_commands.append(f'print "Number of atoms in {gitem}: ${{{variable_name}}}"') # Create a dscript object with the generated commands idD = f"<dscript:group:{self._name}:count>" D = dscript( name=idD if name is None else name, description=f"{idD} for group counts" ) D.variables = "\n".join(variable_definitions) D.printvariables = "\n".join(print_commands) D.set_all_variables() return D
def create(self, group_name)
-
create group G.create(group_name)
Expand source code
def create(self, group_name): """ create group G.create(group_name) """ lammps_code = f"group {group_name} clear" criteria = {"clear": True} op = Operation("create", [], name=group_name, code=lammps_code, criteria=criteria) self.add_operation(op)
def create_groups(self, *group_names)
-
Expand source code
def create_groups(self, *group_names): for group_name in group_names: if not isinstance(group_name, str): raise TypeError(f"Group name must be a string, got {type(group_name)}") self.create(group_name)
def delete(self, name)
-
Deletes one or more stored operations based on their names.
Parameters:
name : str, list, or tuple The name(s) of the operation(s) to delete. If a list or tuple is provided, all specified operations will be deleted.
Usage:
G.delete('operation_name') G.delete(['operation1', 'operation2']) G.delete(('operation1', 'operation2'))
Raises:
ValueError If any of the specified operations are not found.
Expand source code
def delete(self, name): """ Deletes one or more stored operations based on their names. Parameters: ----------- name : str, list, or tuple The name(s) of the operation(s) to delete. If a list or tuple is provided, all specified operations will be deleted. Usage: ------ G.delete('operation_name') G.delete(['operation1', 'operation2']) G.delete(('operation1', 'operation2')) Raises: ------- ValueError If any of the specified operations are not found. """ # Handle a single string, list, or tuple if isinstance(name, (list, tuple)): not_found = [] for n in name: idx = self.find(n) if idx is not None: del self._operations[idx] else: not_found.append(n) # If any names were not found, raise an exception if not_found: raise ValueError(f"Operation(s) {', '.join(not_found)} not found.") elif isinstance(name, str): idx = self.find(name) if idx is not None: del self._operations[idx] else: raise ValueError(f"Operation {name} not found.") else: raise TypeError("The 'name' parameter must be a string, list, or tuple.")
def disp(self, name)
-
display the content of an operation
Expand source code
def disp(self, name): """ display the content of an operation """ idx = self.find(name) if idx is not None: return self._operations[idx].__repr__() else: return "Operation not found"
def dscript(self, name=None, printflag=None, verbose=None, verbosity=None)
-
Generates a dscript object containing the group's LAMMPS commands.
Parameters: - name (str): Optional name for the script object. - printflag (bool, default=False): print on the current console if True - verbose (bool, default=True): keep comments if True
Returns: - dscript: A dscript object containing the group's code.
Expand source code
def dscript(self, name=None, printflag=None, verbose=None, verbosity=None): """ Generates a dscript object containing the group's LAMMPS commands. Parameters: - name (str): Optional name for the script object. - printflag (bool, default=False): print on the current console if True - verbose (bool, default=True): keep comments if True Returns: - dscript: A dscript object containing the group's code. """ if name is None: name = self._name 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 # Create a new dscript object dscript_obj = dscript(name=name,printflag=printflag, verbose=verbose, verbosity=verbosity) # Add each line of the group's code to the script object for idx, op in enumerate(self._operations): # Use the index as the key for the script line dscript_obj[idx] = op.code return dscript_obj
def evaluate(self, group_name, group_op)
-
Evaluates the operation and stores the result in a new group. Expressions could combine +, - and * like o1+o2+o3-o4+o5+o6
Parameters:
groupname : str The name of the group that will store the result. group_op : Operation The operation to evaluate.
Expand source code
def evaluate(self, group_name, group_op): """ Evaluates the operation and stores the result in a new group. Expressions could combine +, - and * like o1+o2+o3-o4+o5+o6 Parameters: ----------- groupname : str The name of the group that will store the result. group_op : Operation The operation to evaluate. """ if not isinstance(group_op, Operation): raise TypeError("Expected an instance of Operation.") if group_name in self.list(): raise ValueError(f"The operation '{group_name}' already exists.") # If the operation is already finalized, no need to evaluate if group_op.isfinalized(): if group_op.name != group_name: # If names differ, create a copy with the new name self.copy(group_op.name, group_name) return # Recursively evaluate operands operand_names = [] for op in group_op.operands: if isinstance(op, Operation): if op.isfinalized(): # Use existing finalized operation name operand_names.append(op.name) else: # Generate a unique name if the operation doesn't have one if not op.name: op.name = op.generate_hashname() # Recursively evaluate the operand operation self.evaluate(op.name, op) operand_names.append(op.name) else: operand_names.append(str(op)) # Call the appropriate method based on the operator if group_op.operator == '+': self.union(group_name, *operand_names) group_op.operator = 'union' elif group_op.operator == '-': self.subtract(group_name, *operand_names) group_op.operator = 'subtract' elif group_op.operator == '*': self.intersect(group_name, *operand_names) group_op.operator = 'intersect' else: raise ValueError(f"Unknown operator: {group_op.operator}") # Update the operation group_op.name = group_name # Get the last added operation finalized_op = self._operations[-1] group_op.code = finalized_op.code group_op.operands = operand_names # Add the operation to the group's _operations if not already added if group_op.name not in self.list(): self._operations.append(group_op)
def find(self, name)
-
Returns the index of an operation based on its name.
Expand source code
def find(self, name): """Returns the index of an operation based on its name.""" if '_operations' in self.__dict__: for i, op in enumerate(self._operations): if op.name == name: return i return None
def format_cell_content(self, content, max_width)
-
Expand source code
def format_cell_content(self, content, max_width): content = str(content) if content is not None else '' if len(content) > max_width: start = content[: (max_width - 5) // 2] end = content[-((max_width - 5) // 2):] content = f"{start} ... {end}" return content
def generate_group_definitions_from_collection(self, collection)
-
Generates group definitions based on the collection of groupobject instances.
This method populates the groups based on beadtypes and associated group names.
Expand source code
def generate_group_definitions_from_collection(self,collection): """ Generates group definitions based on the collection of groupobject instances. This method populates the groups based on beadtypes and associated group names. """ group_defs = {} for obj in collection: for group_name in obj.group: if group_name not in group_defs: group_defs[group_name] = {'type': []} if obj.beadtype not in group_defs[group_name]['type']: group_defs[group_name]['type'].append(obj.beadtype) # Now, add these group definitions self.add_group_criteria(group_defs)
def get_by_name(self, operation_name)
-
Returns the operation matching "operation_name" Usage: group.get_by_name("operation_name") To be used by Operation, not by end-user, which should prefer getattr()
Expand source code
def get_by_name(self,operation_name): """ Returns the operation matching "operation_name" Usage: group.get_by_name("operation_name") To be used by Operation, not by end-user, which should prefer getattr() """ for op in self._operations: if op.name == operation_name: return op raise AttributeError(f"Operation with name '{operation_name}' not found.")
def get_group_criteria(self, group_name)
-
Retrieve the criteria that define a group. Handles group_name as a string or number.
Parameters: - group_name: str or int, the name or number of the group.
Returns: - dict or str: The criteria used to define the group, or a message if defined by multiple criteria.
Raises: - ValueError: If the group does not exist.
Expand source code
def get_group_criteria(self, group_name): """ Retrieve the criteria that define a group. Handles group_name as a string or number. Parameters: - group_name: str or int, the name or number of the group. Returns: - dict or str: The criteria used to define the group, or a message if defined by multiple criteria. Raises: - ValueError: If the group does not exist. """ # Check if group_name exists in _operations if group_name not in self._operations: raise ValueError(f"Group '{group_name}' does not exist.") # Retrieve all operations related to the group directly from _operations operations = self._operations[group_name] if not operations: raise ValueError(f"No operations found for group '{group_name}'.") criteria = {} for op in operations: if op.criteria: criteria.update(op.criteria) # If multiple criteria, return a message if len(criteria) > 1: return f"Group '{group_name}' is defined by multiple criteria: {criteria}" return criteria
def intersect(self, group_name, *groups)
-
Intersect group1, group2, group3 and store the result in group_name. Example usage: group.intersect(group_name, group1, group2, group3,…)
Expand source code
def intersect(self,group_name, *groups): """ Intersect group1, group2, group3 and store the result in group_name. Example usage: group.intersect(group_name, group1, group2, group3,...) """ lammps_code = f"group {group_name} intersect {span(groups)}" criteria = {"intersect": groups} op = Operation("intersect", groups, name=group_name, code=lammps_code, criteria=criteria) self.add_operation(op)
def list(self)
-
return the list of all operations
Expand source code
def list(self): """ return the list of all operations """ return [op.name for op in self._operations]
def operation_exists(self, operation_name)
-
Returns true if "operation_name" exists To be used by Operation, not by end-user, which should prefer find()
Expand source code
def operation_exists(self,operation_name): """ Returns true if "operation_name" exists To be used by Operation, not by end-user, which should prefer find() """ return any(op.name == operation_name for op in self._operations)
def pipescript(self, printflag=None, verbose=None, verbosity=None)
-
Generates a pipescript object containing the group's LAMMPS commands.
Parameters: - printflag (bool, default=False): print on the current console if True - verbose (bool, default=True): keep comments if True
Returns: - pipescript: A pipescript object containing the group's code lines as individual scripts in the pipeline.
Expand source code
def pipescript(self, printflag=None, verbose=None, verbosity=None): """ Generates a pipescript object containing the group's LAMMPS commands. Parameters: - printflag (bool, default=False): print on the current console if True - verbose (bool, default=True): keep comments if True Returns: - pipescript: A pipescript object containing the group's code lines as individual scripts in the pipeline. """ 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 # Create a list to hold script objects script_list = [] # For each operation, create a script object and add it to the list for op in self._operations: # Create a dscript object with the code line dscript_obj = dscript(printflag=printflag, verbose=verbose, verbosity=verbosity) dscript_obj["dummy"] = op.code # Convert the dscript to a script object script_obj = dscript_obj.script(printflag=printflag, verbose=verbose, verbosity=verbosity) # Add the script object to the list script_list.append(script_obj) # Use the static method 'join' to create a pipescript from the list if script_list: pipe_obj = pipescript.join(script_list) else: pipe_obj = pipescript() return pipe_obj
def reindex(self, name, new_idx)
-
Change the index of a stored operation.
Parameters: name: str Name of the operation to reindex new_idx: int New index for the operation
Usage: G.reindex('operation_name', 2)
Expand source code
def reindex(self, name, new_idx): """ Change the index of a stored operation. Parameters: name: str Name of the operation to reindex new_idx: int New index for the operation Usage: G.reindex('operation_name', 2) """ idx = self.find(name) if idx is not None and 0 <= new_idx < len(self._operations): op = self._operations.pop(idx) self._operations.insert(new_idx, op) else: raise ValueError(f"Operation '{name}' not found or new index {new_idx} out of range.")
def rename(self, old_name, new_name)
-
Rename a stored operation.
Parameters: old_name: str Current name of the operation new_name: str New name to assign to the operation
Usage: G.rename('old_operation', 'new_operation')
Expand source code
def rename(self, old_name, new_name): """ Rename a stored operation. Parameters: old_name: str Current name of the operation new_name: str New name to assign to the operation Usage: G.rename('old_operation', 'new_operation') """ idx = self.find(old_name) if idx is not None: if new_name == old_name: raise ValueError("The new name should be different from the previous one.") elif new_name in self.list(): raise ValueError("Operation name must be unique.") self._operations[idx].name = new_name else: raise ValueError(f"Operation '{old_name}' not found.")
def script(self, name=None, printflag=None, verbose=None, verbosity=None)
-
Generates a script object containing the group's LAMMPS commands.
Parameters: - name (str): Optional name for the script object. - printflag (bool, default=False): print on the current console if True - verbose (bool, default=True): keep comments if True
Returns: - script: A script object containing the group's code.
Expand source code
def script(self, name=None, printflag=None, verbose=None, verbosity=None): """ Generates a script object containing the group's LAMMPS commands. Parameters: - name (str): Optional name for the script object. - printflag (bool, default=False): print on the current console if True - verbose (bool, default=True): keep comments if True Returns: - script: A script object containing the group's code. """ if name is None: name = self._name 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 script_obj = self.dscript(name=name,printflag=printflag, verbose=verbose, verbosity=verbosity).script(printflag=printflag, verbose=verbose, verbosity=verbosity) return script_obj
def subtract(self, group_name, *groups)
-
Subtract group2, group3 from group1 and store the result in group_name. Example usage: group.subtract(group_name, group1, group2, group3,…)
Expand source code
def subtract(self,group_name, *groups): """ Subtract group2, group3 from group1 and store the result in group_name. Example usage: group.subtract(group_name, group1, group2, group3,...) """ lammps_code = f"group {group_name} subtract {span(groups)}" criteria = {"subtract": groups} op = Operation("subtract", groups, name=group_name, code=lammps_code, criteria=criteria) self.add_operation(op)
def union(self, group_name, *groups)
-
Union group1, group2, group3 and store the result in group_name. Example usage: group.union(group_name, group1, group2, group3,…)
Expand source code
def union(self,group_name, *groups): """ Union group1, group2, group3 and store the result in group_name. Example usage: group.union(group_name, group1, group2, group3,...) """ lammps_code = f"group {group_name} union {span(groups)}" criteria = {"union": groups} op = Operation("union", groups, name=group_name, code=lammps_code, criteria=criteria) self.add_operation(op)
def variable(self, variable_name, expression, style='atom')
-
Assigns an expression to a LAMMPS variable.
Parameters: - variable_name (str): The name of the variable to be assigned. - expression (str): The expression to assign to the variable. - style (str): The type of variable (default is "atom").
Expand source code
def variable(self, variable_name, expression, style="atom"): """ Assigns an expression to a LAMMPS variable. Parameters: - variable_name (str): The name of the variable to be assigned. - expression (str): The expression to assign to the variable. - style (str): The type of variable (default is "atom"). """ if not isinstance(variable_name, str): raise TypeError(f"Variable name must be a string, got {type(variable_name)}") if not isinstance(expression, str): raise TypeError(f"Expression must be a string, got {type(expression)}") if not isinstance(style, str): raise TypeError(f"Style must be a string, got {type(style)}") lammps_code = f"variable {variable_name} {style} \"{expression}\"" op = Operation("variable", [variable_name, expression], code=lammps_code) self.add_operation(op)
class groupcollection (collection: List[ForwardRef('groupobject')], name: Optional[str] = None)
-
Represents a collection of
groupobject
instances, typically formed by combining them.Attributes:
collection (List[groupobject]): The list of <code><a title="group.groupobject" href="#group.groupobject">groupobject</a></code> instances in the collection. name (str): The name of the collection, generated automatically if not provided.
Examples:
>>> o1 = groupobject(beadtype=1, group=["all", "A"], mass=1.0) >>> o2 = groupobject(beadtype=2, group=["all", "B", "C"]) >>> G = groupcollection([o1, o2]) >>> print(G.name) beadtype=1,2
Initializes a new instance of the groupcollection class.
Parameters:
collection (List[groupobject]): A list of <code><a title="group.groupobject" href="#group.groupobject">groupobject</a></code> instances to include in the collection. name (str, optional): The name of the collection. If not provided, a default name is generated.
Raises:
ValueError: If the <code>collection</code> is empty or contains invalid elements.
Expand source code
class groupcollection: """ Represents a collection of `groupobject` instances, typically formed by combining them. ### Attributes: collection (List[groupobject]): The list of `groupobject` instances in the collection. name (str): The name of the collection, generated automatically if not provided. ### Examples: >>> o1 = groupobject(beadtype=1, group=["all", "A"], mass=1.0) >>> o2 = groupobject(beadtype=2, group=["all", "B", "C"]) >>> G = groupcollection([o1, o2]) >>> print(G.name) beadtype=1,2 """ def __init__(self, collection: List['groupobject'], name: Optional[str] = None): """ Initializes a new instance of the groupcollection class. ### Parameters: collection (List[groupobject]): A list of `groupobject` instances to include in the collection. name (str, optional): The name of the collection. If not provided, a default name is generated. ### Raises: ValueError: If the `collection` is empty or contains invalid elements. """ if not isinstance(collection, list) or not all(isinstance(obj, groupobject) for obj in collection): raise ValueError("`collection` must be a list of `groupobject` instances.") if not collection: raise ValueError("`collection` cannot be empty.") self.collection = collection # Automatically assign a name if not provided if name is None: # Extract names or "beadtype=X" patterns beadtype_names = [ obj.name if obj.name.startswith("beadtype=") else None for obj in collection ] # If all objects have beadtype names, use "beadtype=1,2,3" pattern if all(beadtype_names): beadtypes = [obj.beadtype for obj in collection] self.name = "beadtype={}".format(",".join(map(str, sorted(beadtypes)))) else: # Fall back to concatenating names self.name = ",".join(obj.name for obj in collection) else: self.name = name def __add__(self, other: Union[groupobject, 'groupcollection']) -> 'groupcollection': """ Adds a `groupobject` or another `groupcollection` to this collection. ### Parameters: other (groupobject or groupcollection): The object to add. ### Returns: groupcollection: A new `groupcollection` instance containing the combined objects. ### Raises: TypeError: If `other` is neither a `groupobject` nor a `groupcollection`. """ if isinstance(other, groupobject): return groupcollection(self.collection + [other]) elif isinstance(other, groupcollection): return groupcollection(self.collection + other.collection) else: raise TypeError("Addition only supported between `groupobject` or `groupcollection` instances.") def __iadd__(self, other: Union[groupobject, List[groupobject], Tuple[groupobject, ...]]) -> 'groupcollection': """ In-place addition of a `groupobject` or a list/tuple of `groupobject` instances. ### Parameters: other (groupobject or list/tuple of groupobject): The object(s) to add. ### Returns: groupcollection: The updated `groupcollection` instance. ### Raises: TypeError: If `other` is not a `groupobject` or a list/tuple of `groupobject` instances. """ if isinstance(other, groupobject): self.collection.append(other) elif isinstance(other, (list, tuple)): for obj in other: if not isinstance(obj, groupobject): raise TypeError("All items to add must be `groupobject` instances.") self.collection.extend(other) else: raise TypeError("In-place addition only supported with `groupobject` instances or lists/tuples of `groupobject` instances.") return self def __repr__(self) -> str: """ Returns a neatly formatted string representation of the groupcollection. The representation includes a table of `beadtype`, `group` (max width 50 characters), and `mass`. It also provides a summary of the total number of `groupobject` instances. ### Returns: str: Formatted string representation of the collection. """ headers = ["Beadtype", 'Name', "Group", "Mass"] col_max_widths = [10, 20, 30, 10] # Prepare rows rows = [] for obj in self.collection: beadtype_str = str(obj.beadtype) group_str = ", ".join(obj.group) mass_str = f"{obj.mass}" if obj.mass is not None else "None" rows.append([beadtype_str, obj.name, group_str, mass_str]) # Generate the table table = format_table(headers, rows, col_max_widths) # Add summary summary = f"Total groupobjects: {len(self)}" return f"{table}\n{summary}" def __str__(self) -> str: """ Returns the same representation as `__repr__`. ### Returns: str: Formatted string representation of the collection. """ return f"groupcollection including {len(self)} groupobjects" def __len__(self) -> int: """ Returns the number of `groupobject` instances in the collection. ### Returns: int: Number of objects in the collection. """ return len(self.collection) def __getitem__(self, index: int) -> groupobject: """ Retrieves a `groupobject` by its index. ### Parameters: index (int): The index of the `groupobject` to retrieve. ### Returns: groupobject: The `groupobject` at the specified index. ### Raises: IndexError: If the index is out of range. """ return self.collection[index] def __iter__(self): """ Returns an iterator over the `groupobject` instances in the collection. ### Returns: Iterator[groupobject]: An iterator over the collection. """ return iter(self.collection) def append(self, obj: groupobject): """ Appends a `groupobject` to the collection. ### Parameters: obj (groupobject): The object to append. ### Raises: TypeError: If `obj` is not a `groupobject` instance. """ if not isinstance(obj, groupobject): raise TypeError("Only `groupobject` instances can be appended.") self.collection.append(obj) def extend(self, objs: Union[List[groupobject], Tuple[groupobject, ...]]): """ Extends the collection with a list or tuple of `groupobject` instances. ### Parameters: objs (list or tuple of groupobject): The objects to extend the collection with. ### Raises: TypeError: If `objs` is not a list or tuple. TypeError: If any item in `objs` is not a `groupobject` instance. """ if not isinstance(objs, (list, tuple)): raise TypeError("`objs` must be a list or tuple of `groupobject` instances.") for obj in objs: if not isinstance(obj, groupobject): raise TypeError("All items to extend must be `groupobject` instances.") self.collection.extend(objs) def remove(self, obj: groupobject): """ Removes a `groupobject` from the collection. ### Parameters: obj (groupobject): The object to remove. ### Raises: ValueError: If `obj` is not found in the collection. """ self.collection.remove(obj) def clear(self): """ Clears all `groupobject` instances from the collection. """ self.collection.clear() def mass(self, name: Optional[str] = None, default_mass: Optional[Union[str, int, float]] = "${mass}", verbose: Optional[bool] = True) -> 'dscript': """ Generates LAMMPS mass commands for each unique beadtype in the collection. The method iterates through all `groupobject` instances in the collection, collects unique beadtypes, and ensures that each beadtype has a consistent mass. If a beadtype has `mass=None`, it assigns a default mass as specified by `default_mass`. ### Parameters: name (str, optional): The name to assign to the resulting `dscript` object. Defaults to a generated name. default_mass (str, optional): The default mass value to assign when a beadtype's mass is `None`. Defaults to `"${mass}"`. verbose (bool, optional): If `True`, includes a comment header in the output. Defaults to `True`. ### Returns: dscript: A `dscript` object containing the mass commands for each beadtype, formatted as follows: ``` mass 1 1.0 mass 2 1.0 mass 3 2.5 ``` The `collection` attribute of the `dscript` object holds the formatted mass commands as a single string. ### Raises: ValueError: If a beadtype has inconsistent mass values across different `groupobject` instances. ### Example: ```python # Create groupobject instances o1 = groupobject(beadtype=1, group=["all", "A"], mass=1.0) o2 = groupobject(beadtype=2, group=["all", "B", "C"]) o3 = groupobject(beadtype=3, group="C", mass=2.5) # Initialize a groupcollection with the groupobjects G = groupcollection([o1, o2, o3]) # Generate mass commands M = G.mass() print(M.do()) ``` **Output:** ``` mass 1 1.0 mass 2 ${mass} mass 3 2.5 ``` """ beadtype_mass = {} for obj in self.collection: bt = obj.beadtype mass = obj.mass if obj.mass is not None else "${mass}" if bt in beadtype_mass: if beadtype_mass[bt] != mass: raise ValueError( f"Inconsistent masses for beadtype {bt}: {beadtype_mass[bt]} vs {mass}" ) else: beadtype_mass[bt] = mass # Sort beadtypes for consistent ordering sorted_beadtypes = sorted(beadtype_mass.keys()) # Generate mass commands lines = [f"mass {bt} {beadtype_mass[bt]}" for bt in sorted_beadtypes] # return a dscript object idD = f"<dscript:group:{self.name}:mass>" description = f"{idD} definitions for {len(self)} beads" if verbose: lines.insert(0, "# "+description) D = dscript(name=idD if name is None else name, description=f"{idD} with {len(self)} beads") D.collection = "\n".join(lines) D.DEFINITIONS.mass = default_mass return D
Methods
def append(self, obj: groupobject)
-
Appends a
groupobject
to the collection.Parameters:
obj (groupobject): The object to append.
Raises:
TypeError: If <code>obj</code> is not a <code><a title="group.groupobject" href="#group.groupobject">groupobject</a></code> instance.
Expand source code
def append(self, obj: groupobject): """ Appends a `groupobject` to the collection. ### Parameters: obj (groupobject): The object to append. ### Raises: TypeError: If `obj` is not a `groupobject` instance. """ if not isinstance(obj, groupobject): raise TypeError("Only `groupobject` instances can be appended.") self.collection.append(obj)
def clear(self)
-
Clears all
groupobject
instances from the collection.Expand source code
def clear(self): """ Clears all `groupobject` instances from the collection. """ self.collection.clear()
def extend(self, objs: Union[List[groupobject], Tuple[groupobject, ...]])
-
Extends the collection with a list or tuple of
groupobject
instances.Parameters:
objs (list or tuple of groupobject): The objects to extend the collection with.
Raises:
TypeError: If <code>objs</code> is not a list or tuple. TypeError: If any item in <code>objs</code> is not a <code><a title="group.groupobject" href="#group.groupobject">groupobject</a></code> instance.
Expand source code
def extend(self, objs: Union[List[groupobject], Tuple[groupobject, ...]]): """ Extends the collection with a list or tuple of `groupobject` instances. ### Parameters: objs (list or tuple of groupobject): The objects to extend the collection with. ### Raises: TypeError: If `objs` is not a list or tuple. TypeError: If any item in `objs` is not a `groupobject` instance. """ if not isinstance(objs, (list, tuple)): raise TypeError("`objs` must be a list or tuple of `groupobject` instances.") for obj in objs: if not isinstance(obj, groupobject): raise TypeError("All items to extend must be `groupobject` instances.") self.collection.extend(objs)
def mass(self, name: Optional[str] = None, default_mass: Union[str, int, float, NoneType] = '${mass}', verbose: Optional[bool] = True) ‑> pizza.dscript.dscript
-
Generates LAMMPS mass commands for each unique beadtype in the collection.
The method iterates through all
groupobject
instances in the collection, collects unique beadtypes, and ensures that each beadtype has a consistent mass. If a beadtype hasmass=None
, it assigns a default mass as specified bydefault_mass
.Parameters:
name (str, optional): The name to assign to the resulting <code><a title="group.dscript" href="#group.dscript">dscript</a></code> object. Defaults to a generated name. default_mass (str, optional): The default mass value to assign when a beadtype's mass is <code>None</code>. Defaults to `"${mass}"`. verbose (bool, optional): If <code>True</code>, includes a comment header in the output. Defaults to <code>True</code>.
Returns:
dscript: A <code><a title="group.dscript" href="#group.dscript">dscript</a></code> object containing the mass commands for each beadtype, formatted as follows: ``` mass 1 1.0 mass 2 1.0 mass 3 2.5 ``` The <code>collection</code> attribute of the <code><a title="group.dscript" href="#group.dscript">dscript</a></code> object holds the formatted mass commands as a single string.
Raises:
ValueError: If a beadtype has inconsistent mass values across different <code><a title="group.groupobject" href="#group.groupobject">groupobject</a></code> instances.
Example:
```python # Create groupobject instances o1 = groupobject(beadtype=1, group=["all", "A"], mass=1.0) o2 = groupobject(beadtype=2, group=["all", "B", "C"]) o3 = groupobject(beadtype=3, group="C", mass=2.5) # Initialize a groupcollection with the groupobjects G = groupcollection([o1, o2, o3]) # Generate mass commands M = G.mass() print(M.do()) ``` **Output:** ``` mass 1 1.0 mass 2 ${mass} mass 3 2.5 ```
Expand source code
def mass(self, name: Optional[str] = None, default_mass: Optional[Union[str, int, float]] = "${mass}", verbose: Optional[bool] = True) -> 'dscript': """ Generates LAMMPS mass commands for each unique beadtype in the collection. The method iterates through all `groupobject` instances in the collection, collects unique beadtypes, and ensures that each beadtype has a consistent mass. If a beadtype has `mass=None`, it assigns a default mass as specified by `default_mass`. ### Parameters: name (str, optional): The name to assign to the resulting `dscript` object. Defaults to a generated name. default_mass (str, optional): The default mass value to assign when a beadtype's mass is `None`. Defaults to `"${mass}"`. verbose (bool, optional): If `True`, includes a comment header in the output. Defaults to `True`. ### Returns: dscript: A `dscript` object containing the mass commands for each beadtype, formatted as follows: ``` mass 1 1.0 mass 2 1.0 mass 3 2.5 ``` The `collection` attribute of the `dscript` object holds the formatted mass commands as a single string. ### Raises: ValueError: If a beadtype has inconsistent mass values across different `groupobject` instances. ### Example: ```python # Create groupobject instances o1 = groupobject(beadtype=1, group=["all", "A"], mass=1.0) o2 = groupobject(beadtype=2, group=["all", "B", "C"]) o3 = groupobject(beadtype=3, group="C", mass=2.5) # Initialize a groupcollection with the groupobjects G = groupcollection([o1, o2, o3]) # Generate mass commands M = G.mass() print(M.do()) ``` **Output:** ``` mass 1 1.0 mass 2 ${mass} mass 3 2.5 ``` """ beadtype_mass = {} for obj in self.collection: bt = obj.beadtype mass = obj.mass if obj.mass is not None else "${mass}" if bt in beadtype_mass: if beadtype_mass[bt] != mass: raise ValueError( f"Inconsistent masses for beadtype {bt}: {beadtype_mass[bt]} vs {mass}" ) else: beadtype_mass[bt] = mass # Sort beadtypes for consistent ordering sorted_beadtypes = sorted(beadtype_mass.keys()) # Generate mass commands lines = [f"mass {bt} {beadtype_mass[bt]}" for bt in sorted_beadtypes] # return a dscript object idD = f"<dscript:group:{self.name}:mass>" description = f"{idD} definitions for {len(self)} beads" if verbose: lines.insert(0, "# "+description) D = dscript(name=idD if name is None else name, description=f"{idD} with {len(self)} beads") D.collection = "\n".join(lines) D.DEFINITIONS.mass = default_mass return D
def remove(self, obj: groupobject)
-
Removes a
groupobject
from the collection.Parameters:
obj (groupobject): The object to remove.
Raises:
ValueError: If <code>obj</code> is not found in the collection.
Expand source code
def remove(self, obj: groupobject): """ Removes a `groupobject` from the collection. ### Parameters: obj (groupobject): The object to remove. ### Raises: ValueError: If `obj` is not found in the collection. """ self.collection.remove(obj)
class groupobject (beadtype: Union[int, float], group: Union[str, List[str], Tuple[str, ...]], mass: Union[int, float, NoneType] = None, name: Optional[str] = None)
-
Represents an object with a bead type, associated groups, and an optional mass.
Attributes:
beadtype (int or float): The bead type identifier. Must be an integer or a real scalar. group (List[str]): List of group names the object belongs to. mass (Optional[float]): The mass of the bead. Must be a real scalar or None. name (Optional[str]): Name of the object
Examples:
>>> o1 = groupobject(beadtype=1, group=["all", "A"], mass=1.0) >>> o2 = groupobject(beadtype=2, group=["all", "B", "C"]) >>> o3 = groupobject(beadtype=3, group="C", mass=2.5)
Initializes a new instance of the groupobject class.
Parameters:
beadtype (int or float): The bead type identifier. group (str, list of str, or tuple of str): Group names the object belongs to. mass (float or int, optional): The mass of the bead.
Raises:
TypeError: If <code>beadtype</code> is not an int or float. TypeError: If <code><a title="group.group" href="#group.group">group</a></code> is not a str, list of str, or tuple of str. TypeError: If <code>mass</code> is not a float, int, or None.
Expand source code
class groupobject: """ Represents an object with a bead type, associated groups, and an optional mass. ### Attributes: beadtype (int or float): The bead type identifier. Must be an integer or a real scalar. group (List[str]): List of group names the object belongs to. mass (Optional[float]): The mass of the bead. Must be a real scalar or None. name (Optional[str]): Name of the object ### Examples: >>> o1 = groupobject(beadtype=1, group=["all", "A"], mass=1.0) >>> o2 = groupobject(beadtype=2, group=["all", "B", "C"]) >>> o3 = groupobject(beadtype=3, group="C", mass=2.5) """ def __init__(self, beadtype: Union[int, float], group: Union[str, List[str], Tuple[str, ...]], \ mass: Optional[Union[int, float]] = None, name: Optional[str]=None, ): """ Initializes a new instance of the groupobject class. ### Parameters: beadtype (int or float): The bead type identifier. group (str, list of str, or tuple of str): Group names the object belongs to. mass (float or int, optional): The mass of the bead. ### Raises: TypeError: If `beadtype` is not an int or float. TypeError: If `group` is not a str, list of str, or tuple of str. TypeError: If `mass` is not a float, int, or None. """ # Validate beadtype if not isinstance(beadtype, (int, float)): raise TypeError(f"'beadtype' must be an integer or float, got {type(beadtype).__name__} instead.") self.beadtype = beadtype # Validate group if isinstance(group, str): self.group = [group] elif isinstance(group, (list, tuple)): if not all(isinstance(g, str) for g in group): raise TypeError("All elements in 'group' must be strings.") self.group = list(group) else: raise TypeError(f"'group' must be a string, list of strings, or tuple of strings, got {type(group).__name__} instead.") # Validate mass if mass is not None and not isinstance(mass, (int, float)): raise TypeError(f"'mass' must be a real scalar (int or float) or None, got {type(mass).__name__} instead.") self.mass = mass # Validate name self.name = f"beadtype={beadtype}" if name is None else name def __str__(self) -> str: """ Returns a readable string representation of the groupobject. """ return f"groupobject | type={self.beadtype} | name={self.group} | mass={self.mass}" def __repr__(self) -> str: """ Returns an unambiguous string representation of the groupobject. ### Returns: str: String representation including beadtype, group, and mass (if not None). """ return f"groupobject(beadtype={self.beadtype}, group={span(self.group,',','[',']')}, name={self.name}, mass={self.mass})" def __add__(self, other: Union['groupobject', 'groupcollection']) -> 'groupcollection': """ Adds this groupobject to another groupobject or a groupcollection. ### Parameters: other (groupobject or groupcollection): The object to add. ### Returns: groupcollection: A new groupcollection instance containing the combined objects. ### Raises: TypeError: If `other` is neither a `groupobject` nor a `groupcollection`. """ if isinstance(other, groupobject): return groupcollection([self, other]) elif isinstance(other, groupcollection): return groupcollection([self] + other.collection) else: raise TypeError("Addition only supported between `groupobject` or `groupcollection` instances.") def __radd__(self, other: Union['groupobject', 'groupcollection']) -> 'groupcollection': """ Adds this groupobject to another groupobject or a groupcollection from the right. ### Parameters: other (groupobject or groupcollection): The object to add. ### Returns: groupcollection: A new groupcollection instance containing the combined objects. ### Raises: TypeError: If `other` is neither a `groupobject` nor a `groupcollection`. """ if isinstance(other, groupobject): return groupcollection([other, self]) elif isinstance(other, groupcollection): return groupcollection(other.collection + [self]) else: raise TypeError("Addition only supported between `groupobject` or `groupcollection` instances.")
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
, orscriptobjectgroup
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
andpipescript
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 withcmd = p.do()
. - Reordering pipelines withp[[2, 0, 1]]
. - Deleting steps withp[[0, 1]] = []
. - Accessing local and global user space variables viap.USER[idx].var
andp.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. TheUSER
space andDEFINITIONS
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:
- STATIC: Definitions specific to each script in the pipescript.
- GLOBAL: User variables shared across steps from a specific point onwards.
- 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'sverbose
. 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 is4
(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)
- Pipeline Construction: Create pipelines of scripts, combining multiple
script objects,