Module raster
raster.py
provides methods to generate and manipulate raster-based geometries for LAMMPS input files. It includes tools for creating, plotting, and exporting geometric objects, with advanced features for particle systems such as emulsions and core-shell dispersions. This module is part of the Pizza3 toolkit.
Features
1. Raster Area Creation
Create a raster object with customizable dimensions and properties:
R = raster(width=200, height=200, dpi=300)
2. Geometric Object Insertion
Define and manipulate objects such as rectangles, circles, triangles, diamonds, and polygons:
R.rectangle(10, 20, 15, 30, name='rect1', beadtype=1)
R.circle(50, 50, 10, name='circle1', beadtype=2, angle=45)
R.triangle(30, 40, 10, name='triangle1', beadtype=3, angle=30)
3. Collections and Paths
Group objects into collections or copy them along specified paths:
R.collection(object1, object2, name='my_collection', beadtype=1)
R.copyalongpath(object1, name='path_collection', path=linear, n=5)
4. Scatter and Emulsions
Generate random distributions of particles: - Emulsions: Place circular particles within defined boundaries. - Core-Shell Particles: Insert particles with inner and outer radii.
e = emulsion(xmin=10, ymin=10, xmax=100, ymax=100)
e.insertion([10, 20, 30], beadtype=1)
5. Visualization
Preview objects and raster layouts:
R.plot()
R.show(extra="label", contour=True)
6. Exporting to LAMMPS
Generate a pizza.data3.data
object for exporting to LAMMPS:
data_obj = R.data(scale=(1, 1), center=(0, 0))
data_obj.write("output_file.lmp")
Advanced Features
- Overlay Images: Import and convert images to raster objects with beads:
python R.overlay(50, 50, filename="image.jpg", ncolors=4, beadtype=2)
- Hexagonal Packing: Generate hex-packed data for particle arrangements.
- Labeling and Masking: Add labels to objects or define masked regions.
Usage Examples
Basic Example
R = raster(width=100, height=100)
R.rectangle(10, 20, 15, 30, name='rect1', beadtype=1)
R.circle(50, 50, 10, name='circle1', beadtype=2)
R.plot()
R.show(extra="label")
Emulsion Generation
E = raster(width=400, height=400)
e = emulsion(xmin=10, ymin=10, xmax=390, ymax=390)
e.insertion([60, 50, 40, 30], beadtype=1)
E.scatter(e, name="emulsion")
E.plot()
E.show()
Core-Shell Dispersion
C = raster(width=400, height=400)
cs = coreshell(xmin=10, ymin=10, xmax=390, ymax=390)
cs.insertion([60, 50, 40], beadtype=(1, 2), thickness=4)
C.scatter(cs, name="core-shell")
C.plot()
C.show()
Requirements
Since version 0.40, image overlays and live previews depend on the Pizza3.pizza.private.PIL
library. For compatibility, the customized version of PIL must be compiled:
cd Pizza3/pizza/private/PIL
python3 setup.py install
Authors and Credits
- Author: Olivier Vitrac
- Email: olivier.vitrac@agroparistech.fr
- License: GPLv3
- Credits: [Olivier Vitrac, Pizza3 Development Team]
Old help
RASTER method to generate LAMMPS input files (in 2D for this version)
Generate a raster area
R = raster()
R = raster(width=200, height=200, dpi=300)
Set objects (rectangle, circle, triangle, diamond...)
R.rectangle(1,24,2,20,name='rect1')
R.rectangle(60,80,50,81,name='rect2',beadtype=2,angle=40)
R.rectangle(50,50,10,10,mode="center",angle=45,beadtype=1)
R.circle(45,20,5,name='C1',beadtype=3)
R.circle(35,10,5,name='C2',beadtype=3)
R.circle(15,30,10,name='p1',beadtype=4,shaperatio=0.2,angle=-30)
R.circle(12,40,8,name='p2',beadtype=4,shaperatio=0.2,angle=20)
R.circle(12,80,22,name='p3',beadtype=4,shaperatio=1.3,angle=20)
R.triangle(85,20,10,name='T1',beadtype=5,angle=20)
R.diamond(85,35,5,name='D1',beadtype=5,angle=20)
R.pentagon(50,35,5,name='P1',beadtype=5,angle=90)
R.hexagon(47,85,12,name='H1',beadtype=5,angle=90)
List simple objects
R.list()
R.get("p1")
R.p1
R.C1
List objects in a collection
R.C1.get("p1")
R.C1.p1 shows the object p1 in the collection C1
Build objects and show them
R.plot()
R.show()
Show and manage labels
R.show(extra="label",contour=True)
R.label("rect003")
R.unlabel('rect1')
Manage objects, update and show
Get the image and convert the image to text
I = R.numeric()
T = R.string()
R.print()
Create a pizza.dump3.dump object
X = R.data()
X=R.data(scale=(1,1),center=(0,0))
X.write("/tmp/myfile")
Build an emulsion/suspension
C = raster(width=400,height=400)
e = emulsion(xmin=10, ymin=10, xmax=390, ymax=390)
e.insertion([60,50,40,30,20,15,15,10,8,20,12,8,6,4,11,13],beadtype=1)
C.scatter(e,name="emulsion")
C.plot()
C.show()
Build a core-shell dispersion
D = raster(width=400,height=400)
cs = coreshell(xmin=10, ymin=10, xmax=390, ymax=390)
cs.insertion([60,50,40,30,20,15,15,10,8,20,12,8,11,13],beadtype=(1,2),thickness = 4)
D.scatter(cs,name="core-shell")
D.plot()
D.show()
More advanced features enable object copy, duplication along a path
Contruction of scattered particles
See: copyalongpath(), scatter(), emulsion(), coreshell()
Examples follow in the __main__ section
--------------------------------------------------------------------
BUILDING REQUIREMENTS:
Since version 0.40, overlay(), torgb() and live previews
use Pizza3.pizza.private.PIL library
The customized version of PIL needs to be compiled for your system
By assuming that anaconda is used:
condainit
cd Pizza3/pizza/private/PIL
python3 setup.py install
unzip -l dist/UNKNOWN-9.1.0-py3.9-linux-x86_64.egg
unzip -j "dist/UNKNOWN-9.1.0-py3.9-linux-x86_64.egg" "PIL/_imaging.cpython-39-x86_64-linux-gnu.so" .
rm -rf dist/
rm -rf build/
rm -rf ../UNKNOWN.egg-info
--------------------------------------------------------------------
Expand source code
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
`raster.py` provides methods to generate and manipulate raster-based geometries for LAMMPS input files. It includes tools for creating, plotting, and exporting geometric objects, with advanced features for particle systems such as emulsions and core-shell dispersions. This module is part of the **Pizza3** toolkit.
---
## Features
### 1. **Raster Area Creation**
Create a raster object with customizable dimensions and properties:
```python
R = raster(width=200, height=200, dpi=300)
```
### 2. **Geometric Object Insertion**
Define and manipulate objects such as rectangles, circles, triangles, diamonds, and polygons:
```python
R.rectangle(10, 20, 15, 30, name='rect1', beadtype=1)
R.circle(50, 50, 10, name='circle1', beadtype=2, angle=45)
R.triangle(30, 40, 10, name='triangle1', beadtype=3, angle=30)
```
### 3. **Collections and Paths**
Group objects into collections or copy them along specified paths:
```python
R.collection(object1, object2, name='my_collection', beadtype=1)
R.copyalongpath(object1, name='path_collection', path=linear, n=5)
```
### 4. **Scatter and Emulsions**
Generate random distributions of particles:
- **Emulsions**: Place circular particles within defined boundaries.
- **Core-Shell Particles**: Insert particles with inner and outer radii.
```python
e = emulsion(xmin=10, ymin=10, xmax=100, ymax=100)
e.insertion([10, 20, 30], beadtype=1)
```
### 5. **Visualization**
Preview objects and raster layouts:
```python
R.plot()
R.show(extra="label", contour=True)
```
### 6. **Exporting to LAMMPS**
Generate a `pizza.data3.data` object for exporting to LAMMPS:
```python
data_obj = R.data(scale=(1, 1), center=(0, 0))
data_obj.write("output_file.lmp")
```
---
## Advanced Features
- **Overlay Images**: Import and convert images to raster objects with beads:
```python
R.overlay(50, 50, filename="image.jpg", ncolors=4, beadtype=2)
```
- **Hexagonal Packing**: Generate hex-packed data for particle arrangements.
- **Labeling and Masking**: Add labels to objects or define masked regions.
---
## Usage Examples
### Basic Example
```python
R = raster(width=100, height=100)
R.rectangle(10, 20, 15, 30, name='rect1', beadtype=1)
R.circle(50, 50, 10, name='circle1', beadtype=2)
R.plot()
R.show(extra="label")
```
### Emulsion Generation
```python
E = raster(width=400, height=400)
e = emulsion(xmin=10, ymin=10, xmax=390, ymax=390)
e.insertion([60, 50, 40, 30], beadtype=1)
E.scatter(e, name="emulsion")
E.plot()
E.show()
```
### Core-Shell Dispersion
```python
C = raster(width=400, height=400)
cs = coreshell(xmin=10, ymin=10, xmax=390, ymax=390)
cs.insertion([60, 50, 40], beadtype=(1, 2), thickness=4)
C.scatter(cs, name="core-shell")
C.plot()
C.show()
```
---
## Requirements
Since version 0.40, image overlays and live previews depend on the `Pizza3.pizza.private.PIL` library. For compatibility, the customized version of PIL must be compiled:
```bash
cd Pizza3/pizza/private/PIL
python3 setup.py install
```
---
## Authors and Credits
- **Author**: Olivier Vitrac
- **Email**: olivier.vitrac@agroparistech.fr
- **License**: GPLv3
- **Credits**: [Olivier Vitrac, Pizza3 Development Team]
---
## Old help
RASTER method to generate LAMMPS input files (in 2D for this version)
Generate a raster area
R = raster()
R = raster(width=200, height=200, dpi=300)
Set objects (rectangle, circle, triangle, diamond...)
R.rectangle(1,24,2,20,name='rect1')
R.rectangle(60,80,50,81,name='rect2',beadtype=2,angle=40)
R.rectangle(50,50,10,10,mode="center",angle=45,beadtype=1)
R.circle(45,20,5,name='C1',beadtype=3)
R.circle(35,10,5,name='C2',beadtype=3)
R.circle(15,30,10,name='p1',beadtype=4,shaperatio=0.2,angle=-30)
R.circle(12,40,8,name='p2',beadtype=4,shaperatio=0.2,angle=20)
R.circle(12,80,22,name='p3',beadtype=4,shaperatio=1.3,angle=20)
R.triangle(85,20,10,name='T1',beadtype=5,angle=20)
R.diamond(85,35,5,name='D1',beadtype=5,angle=20)
R.pentagon(50,35,5,name='P1',beadtype=5,angle=90)
R.hexagon(47,85,12,name='H1',beadtype=5,angle=90)
List simple objects
R.list()
R.get("p1")
R.p1
R.C1
List objects in a collection
R.C1.get("p1")
R.C1.p1 shows the object p1 in the collection C1
Build objects and show them
R.plot()
R.show()
Show and manage labels
R.show(extra="label",contour=True)
R.label("rect003")
R.unlabel('rect1')
Manage objects, update and show
Get the image and convert the image to text
I = R.numeric()
T = R.string()
R.print()
Create a pizza.dump3.dump object
X = R.data()
X=R.data(scale=(1,1),center=(0,0))
X.write("/tmp/myfile")
Build an emulsion/suspension
C = raster(width=400,height=400)
e = emulsion(xmin=10, ymin=10, xmax=390, ymax=390)
e.insertion([60,50,40,30,20,15,15,10,8,20,12,8,6,4,11,13],beadtype=1)
C.scatter(e,name="emulsion")
C.plot()
C.show()
Build a core-shell dispersion
D = raster(width=400,height=400)
cs = coreshell(xmin=10, ymin=10, xmax=390, ymax=390)
cs.insertion([60,50,40,30,20,15,15,10,8,20,12,8,11,13],beadtype=(1,2),thickness = 4)
D.scatter(cs,name="core-shell")
D.plot()
D.show()
More advanced features enable object copy, duplication along a path
Contruction of scattered particles
See: copyalongpath(), scatter(), emulsion(), coreshell()
Examples follow in the __main__ section
--------------------------------------------------------------------
BUILDING REQUIREMENTS:
Since version 0.40, overlay(), torgb() and live previews
use Pizza3.pizza.private.PIL library
The customized version of PIL needs to be compiled for your system
By assuming that anaconda is used:
condainit
cd Pizza3/pizza/private/PIL
python3 setup.py install
unzip -l dist/UNKNOWN-9.1.0-py3.9-linux-x86_64.egg
unzip -j "dist/UNKNOWN-9.1.0-py3.9-linux-x86_64.egg" "PIL/_imaging.cpython-39-x86_64-linux-gnu.so" .
rm -rf dist/
rm -rf build/
rm -rf ../UNKNOWN.egg-info
--------------------------------------------------------------------
"""
__project__ = "Pizza3"
__author__ = "Olivier Vitrac"
__copyright__ = "Copyright 2022"
__credits__ = ["Olivier Vitrac"]
__license__ = "GPLv3"
__maintainer__ = "Olivier Vitrac"
__email__ = "olivier.vitrac@agroparistech.fr"
__version__ = "0.99991"
# INRAE\Olivier Vitrac - rev. 2024-12-08
# contact: olivier.vitrac@agroparistech.fr, han.chen@inrae.fr
# History
# 2022-02-05 first alpha version
# 2022-02-06 RC for 2D
# 2022-02-08 add count(), update the display method
# 2022-02-10 add figure(), newfigure(), count()
# 2022-02-11 improve display, add data()
# 2022-02-12 major release, fully compatible with pizza.data3.data
# 2022-02-13 the example (<F5>) has been modified R.plot() should precedes R.list()
# 2022-02-28 update write files for SMD, add scale and center to R.data()
# 2022-03-02 fix data(): xlo and ylow (beads should not overlap the boundary), scale radii, volumes
# 2022-03-20 major update, add collection, duplication, translation, scatter(), emulsion()
# 2022-03-22 update raster to insert customized beadtypes
# 2022-03-23 add coreshell()
# 2022-03-23 fix nattempt, add arc
# 2022-04-01 add maxtype to raster.data(), e.g. raster.data(maxtype=4)
# 2022-04-08 add beadtype2(alternative beadtype, ratio) to salt objects
# 2022-04-13 descale volume in data() for stability reason
# 2022-04-23 very first overlay implementation (alpha version) -- version 0.40
# 2022-04-24 full implementation of overlay (not fully tested yet, intended to behave has a regular object)
# 2022-04-25 full integration of PIL
# 2022-04-26 add torgb(), thumbnails, add angle, scale=(scalex,scaley) to overlay()
# 2022-04-26 add building instructions, version 0.421
# 2022-04-27 add scale to the representation of overlay objects (0.422)
# 2022-04-28 fix len(raster object) - typo error (0.4221)
# 2022-05-03 add hexpacking to data(), enables you to reproduces an hexgonal packaging
# 2023-01-03 workaround to have raster working on Windows without restrictions
# 2024-12-08 improved help
# %% Imports and private library
import os
from platform import system as sys
from copy import copy as duplicate
from copy import deepcopy as deepduplicate
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.path as path
import matplotlib.patches as patches
import matplotlib.cm as cmap
from IPython.display import display
from pizza.data3 import data as data3
from pizza.private.mstruct import struct
if sys()=="Windows":
try:
from PIL import Image, ImagePalette
PILavailable = True
except ImportError:
print("WARNING: no image capabilities in Windows")
PILavailable = False
else:
from pizza.private.PIL import Image, ImagePalette
PILavailable = True
__all__ = ['Circle', 'Collection', 'Diamond', 'Hexagon', 'Pentagon', 'Rectangle', 'Triangle', 'arc', 'collection', 'coregeometry', 'coreshell', 'data3', 'emulsion', 'genericpolygon', 'imagesc', 'ind2rgb', 'linear', 'overlay', 'raster', 'scatter', 'struct']
def _rotate(x0,y0,xc,yc,angle):
angle = np.pi * angle / 180.0
x1 = (x0 - xc)*np.cos(angle) - (y0 - yc)*np.sin(angle) + xc
y1 = (x0 - xc)*np.sin(angle) + (y0 - yc)*np.cos(angle) + yc
return x1, y1
def _extents(f):
halftick = ( f[1] - f[0] ) / 2
return [f[0] - halftick, f[-1] + halftick]
# wrapper of imagesc (note that the origin is bottom left)
# usage: data = np.random.randn(5,10)
# imagesc(data)
def imagesc(im,x=None,y=None):
""" imagesc à la Matlab
imagesc(np2array) """
if x==None: x=np.arange(1,np.shape(im)[1]+1)
if y==None: y=np.arange(1,np.shape(im)[0]+1)
plt.imshow(im, extent=_extents(x) + _extents(y),
aspect="auto", origin="lower", interpolation="none")
# convert indexed image to RGB (using PIL)
# rgbim = ind2rgb(im,ncolors=number of colors)
def ind2rgb(im,ncolors=64):
""" Convert indexed image (NumPy array) to RGB
rgb = ind2rgb(np2array,ncolors=nc)
use rgb.save("/path/filename.png") for saving
"""
raw = Image.fromarray(np.flipud(im),"P")
col0 = np.array(np.round(255*cmap.get_cmap("viridis",ncolors).colors[:,:3]),dtype="uint8")
col = bytearray(np.resize(col0,(256,3)).flatten())
pal = ImagePalette.ImagePalette(mode="RGB",palette=col)
#Image.convert(mode=None, matrix=None, dither=None, palette=Palette.WEB, colors=256)
raw.putpalette(pal)
return raw
# helper for parametric functions
def linear(xmin=10,ymin=10,xmax=80,ymax=80,n=5,USER=struct()):
""" Equispaced points along a trajectory
X,Y = linear(xmin=value,ymin=value,xmax=value,ymax=value,n=int)
"""
return np.linspace(xmin,xmax,n), np.linspace(ymin,ymax,n)
def arc(xmin=10,ymin=50,xmax=80,ymax=50,n=5,USER=struct(radius=20,direction=1)):
""" Point distributed along an arc
X,Y = arc(xmin=value,ymin=value,xmax=value,ymax=value,n=int,
USER=struct(radius=value,direction=1))
Use direction to choose the upward +1 or downward -1 circle
see: https://rosettacode.org/wiki/Circles_of_given_radius_through_two_points
"""
R = 0 if "radius" not in USER else USER.radius
direction = +1 if "direction" not in USER else USER.direction
dx,dy = xmax-xmin, ymax-ymin
q = np.sqrt(dx**2+dy**2) # distance
R = max(R,q/2) # radius constraint
d = np.sqrt(R**2-(q/2)**2) # distance along the mirror line
xc = (xmin+xmax)/2 - direction * d*dy/q
yc = (ymin+ymax)/2 + direction * d*dx/q
thmin,thmax = np.arctan((ymin-yc)/(xmin-xc)), np.arctan((ymax-yc)/(xmax-xc))
if d==0: thmax = thmin + np.pi
th = np.linspace(thmin,thmax,n)
return xc+np.cos(th)*R,yc+np.sin(th)*R
# %% raster class
class raster:
""" raster class for LAMMPS SMD
Constructor
R = raster(width=100,height=100...)
Extra properties
dpi, fontsize
additional properties for R.data()
scale, center : full scaling
mass, volume, radius, contactradius, velocities, forces: bead scaling
filename
List of available properties = default values
name = "default raster"
width = 100
height = 100
dpi = 200
fontsize = 10
mass = 1
volume = 1
radius = 1.5
contactradius = 0.5
velocities = [0, 0, 0]
forces = [0, 0, 0]
preview = True
previewthumb = (128,128)
filename = ["%dx%d raster (%s)" % (self.width,self.height,self.name)]
Graphical objects
R.rectangle(xleft,xright,ybottom,ytop [, beadtype=1,mode="lower", angle=0, ismask=False])
R.rectangle(xcenter,ycenter,width,height [, beadtype=1,mode="center", angle=0, ismask=False])
R.circle(xcenter,ycenter,radius [, beadtype=1,shaperatio=1, angle=0, ismask=False], resolution=20, shiftangle=0)
R.triangle(...)
R.diamond(...)
R.pentagon(...)
R.hexagon(...)
R.overlay(xleft,xright,filename=="valid/image.ext",color=2,beadtype=1)
note: use fake=True to generate an object without inserting it
R.collection(...) generates collection of existing or fake objects
R.object.copy(...) enables to copy an object
Display methods (precedence affects the result)
R.plot()
R.show(), R.show(extra="label",contour=True,what="beadtype" or "objindex")
R.show(extra="labels")
R.list()
R.get("object")
R.print()
R.label("object")
R.unlabel("object")
R.figure()
R.newfigure(dpi=300)
R.numeric()
R.string(), R.string(what="beadtype" or "objindex"))
R.names()
R.print()
Clear and delete
R.clear()
R.clear("all")
R.delete("object")
Copy objects
R.copyalongpath(....)
R.scatter()
Generate an input data object
X = R.data() or X=R.data(scale=(1,1),center=(0,0))
X.write("/tmp/myfile")
"""
# CONSTRUCTOR ----------------------------
def __init__(self,
# raster properties
name="default raster",
width=100,
height=100,
# printing and display
dpi=200,
fontsize=10,
# for data conversion
mass=1,
volume=1,
radius=1.5,
contactradius=0.5,
velocities=[0,0,0],
forces=[0,0,0],
preview=True,
previewthumb = (128,128),
filename=""
):
""" initialize raster """
self.name = name
self.width = width
self.height = height
self.xcenter= width/2
self.ycenter = height/2
self.objects = {}
self.nobjects = 0 # total number of objects (alive)
self.nbeads = 0
self.counter = { "triangle":0,
"diamond":0,
"rectangle":0,
"pentagon":0,
"hexagon":0,
"circle":0,
"overlay":0,
"collection":0,
"all":0
}
self.fontsize = 10 # font size for labels
self.imbead = np.zeros((height,width),dtype=np.int8)
self.imobj = np.zeros((height,width),dtype=np.int8)
self.hfig = [] # figure handle
self.dpi = dpi
# generic SMD properties (to be rescaled)
self.volume = volume
self.mass = mass
self.radius = radius
self.contactradius = contactradius
self.velocities = velocities
self.forces =forces
self.preview = preview
self.previewthumb = previewthumb
if filename == "":
self.filename = ["%dx%d raster (%s)" % (self.width,self.height,self.name)]
else:
self.filename = filename
# DATA ----------------------------
def data(self,scale=(1,1),center=(0,0),maxtype=None,hexpacking=None):
"""
return a pizza.data object
data()
data(scale=(scalex,scaley),
center=(centerx,centery),
maxtype=number,
hexpacking=(0.5,0))
"""
if not isinstance(scale,tuple) or len(scale)!=2:
raise ValueError("scale must be tuple (scalex,scaley)")
if not isinstance(center,tuple) or len(scale)!=2:
raise ValueError("center must be tuple (centerx,centery)")
scalez = np.sqrt(scale[0]*scale[1])
scalevol = scale[0]*scale[1] #*scalez
maxtypeheader = self.count()[-1][0] if maxtype is None else maxtype
n = self.length()
i,j = self.imbead.nonzero() # x=j+0.5 y=i+0.5
x = (j+0.5-center[0])*scale[0]
y = (i+0.5-center[1])*scale[1]
if hexpacking is not None:
if isinstance(hexpacking,tuple) and len(hexpacking)==2:
for k in range(len(i)):
if i[k] % 2:
x[k] = (j[k]+0.5+hexpacking[1]-center[0])*scale[0]
else:
x[k] = (j[k]+0.5+hexpacking[0]-center[0])*scale[0]
else:
raise ValueError("hexpacking should be a tuple (shiftodd,shifteven)")
X = data3() # empty pizza.data3.data object
X.title = self.name + "(raster)"
X.headers = {'atoms': n,
'atom types': maxtypeheader,
'xlo xhi': ((0.0-center[0])*scale[0], (self.width-0.0-center[0])*scale[0]),
'ylo yhi': ((0.0-center[1])*scale[1], (self.height-0.0-center[1])*scale[1]),
'zlo zhi': (0, scalez)}
# [ATOMS] section
X.append('Atoms',list(range(1,n+1)),True,"id") # id
X.append('Atoms',self.imbead[i,j],True,"type") # Type
X.append('Atoms',1,True,"mol") # mol
X.append('Atoms',self.volume*scalevol,False,"c_vol") # c_vol
X.append('Atoms',self.mass*scalevol,False,"mass") # mass
X.append('Atoms',self.radius*scalez,False,"radius") # radius
X.append('Atoms',self.contactradius*scalez,False,"c_contact_radius") # c_contact_radius
X.append('Atoms',x,False,"x") # x
X.append('Atoms',y,False,"y") # y
X.append('Atoms',0,False,"z") # z
X.append('Atoms',x,False,"x0") # x0
X.append('Atoms',y,False,"y0") # y0
X.append('Atoms',0,False,"z0") # z0
# [VELOCITIES] section
X.append('Velocities',list(range(1,n+1)),True,"id") # id
X.append('Velocities',self.velocities[0],False,"vx") # vx
X.append('Velocities',self.velocities[1],False,"vy") # vy
X.append('Velocities',self.velocities[2],False,"vz") # vz
# pseudo-filename
X.flist = self.filename
return X
# LENGTH ----------------------------
def length(self,t=None,what="beadtype"):
""" returns the total number of beads length(type,"beadtype") """
if what == "beadtype":
num = self.imbead
elif what == "objindex":
num = self.imobj
else:
raise ValueError('"beadtype" and "objindex" are the only acceptable values')
if t==None:
return np.count_nonzero(num>0)
else:
return np.count_nonzero(num==t)
# NUMERIC ----------------------------
def numeric(self):
""" retrieve the image as a numpy.array """
return self.imbead, self.imobj
# STRING ----------------------------
def string(self,what="beadtype"):
""" convert the image as ASCII strings """
if what == "beadtype":
num = np.flipud(duplicate(self.imbead))
elif what == "objindex":
num = np.flipud(duplicate(self.imobj))
else:
raise ValueError('"beadtype" and "objindex" are the only acceptable values')
num[num>0] = num[num>0] + 65
num[num==0] = 32
num = list(num)
return ["".join(map(chr,x)) for x in num]
# GET -----------------------------
def get(self,name):
""" returns the object """
if name in self.objects:
return self.objects[name]
else:
raise ValueError('the object "%s" does not exist, use list()' % name)
# GETATTR --------------------------
def __getattr__(self,key):
""" get attribute override """
return self.get(key)
# CLEAR ----------------------------
def clear(self,what="nothing"):
""" clear the plotting area, use clear("all")) to remove all objects """
self.imbead = np.zeros((self.height,self.width),dtype=np.int8)
self.imobj = np.zeros((self.height,self.width),dtype=np.int8)
for o in self.names():
if what=="all":
self.delete(o)
else:
self.objects[o].isplotted = False
self.objects[o].islabelled = False
if not self.objects[o].ismask:
self.nbeads -= self.objects[o].nbeads
self.objects[o].nbeads = 0 # number of beads (plotted)
self.figure()
plt.cla()
self.show()
# DISP method ----------------------------
def __repr__(self):
""" display method """
ctyp = self.count() # count objects (not beads)
print("-"*40)
print('RASTER area "%s" with %d objects' % (self.name,self.nobjects))
print("-"*40)
print("<- grid size ->")
print("\twidth: %d" % self.width)
print("\theight: %d" % self.height)
print("<- bead types ->")
nbt = 0
if len(ctyp):
for i,c in enumerate(ctyp):
nb = self.length(c[0])
nbt += nb
print("\t type=%d (%d objects, %d beads)" % (c[0],c[1],nb))
else:
print("\tno bead assigned")
print("-"*40)
if self.preview and len(self)>0 and self.max>0:
if PILavailable:
display(self.torgb("beadtype",self.previewthumb))
display(self.torgb("objindex",self.previewthumb))
else:
print("no PIL image")
return "RASTER AREA %d x %d with %d objects (%d types, %d beads)." % \
(self.width,self.height,self.nobjects,len(ctyp),nbt)
# TORGB method ----------------------------
def torgb(self,what="beadtype",thumb=None):
""" converts bead raster to image
rgb = raster.torgb(what="beadtype")
thumbnail = raster.torgb(what="beadtype",(128,128))
use: rgb.save("/path/filename.png") for saving
what = "beadtype" or "objindex"
"""
if what=="beadtype":
rgb = ind2rgb(self.imbead,ncolors=self.max+1)
elif what == "objindex":
rgb = ind2rgb(self.imobj,ncolors=len(self)+1)
else:
raise ValueError('"beadtype" and "objindex" are the only acceptable values')
if thumb is not None: rgb.thumbnail(thumb)
return rgb
# COUNT method ----------------------------
def count(self):
""" count objects by type """
typlist = []
for o in self.names():
if isinstance(self.objects[o].beadtype,list):
typlist += self.objects[o].beadtype
else:
typlist.append(self.objects[o].beadtype)
utypes = list(set(typlist))
c = []
for t in utypes:
c.append((t,typlist.count(t)))
return c
# max method ------------------------------
@property
def max(self):
""" max bead type """
typlist = []
for o in self.names():
if isinstance(self.objects[o].beadtype,list):
typlist += self.objects[o].beadtype
else:
typlist.append(self.objects[o].beadtype)
return max(typlist)
# len method ------------------------------
def __len__(self):
""" len method """
return len(self.objects)
# NAMES method ----------------------------
def names(self):
""" return the names of objects sorted as index """
namesunsorted=namessorted=list(self.objects.keys())
nobj = len(namesunsorted)
for iobj in range(nobj):
namessorted[self.objects[namesunsorted[iobj]].index-1] = namesunsorted[iobj]
return namessorted
# LIST method ----------------------------
def list(self):
""" list objects """
fmt = "%%%ss:" % max(10,max([len(n) for n in self.names()])+2)
print("RASTER with %d objects" % self.nobjects)
for o in self.objects.keys():
print(fmt % self.objects[o].name,"%-10s" % self.objects[o].kind,
"(beadtype=%d,object index=[%d,%d], n=%d)" % \
(self.objects[o].beadtype,
self.objects[o].index,
self.objects[o].subindex,
self.objects[o].nbeads))
# EXIST method ----------------------------
def exist(self,name):
""" exist object """
return name in self.objects
# DELETE method ----------------------------
def delete(self,name):
""" delete object """
if name in self.objects:
if not self.objects[name].ismask:
self.nbeads -= self.objects[name].nbeads
del self.objects[name]
self.nobjects -= 1
else:
raise ValueError("%d is not a valid name (use list()) to list valid objects" % name)
self.clear()
self.plot()
self.show(extra="label")
# VALID method
def valid(self,x,y):
""" validation of coordinates """
return min(self.width,max(0,round(x))),min(self.height,max(0,round(y)))
# frameobj method
def frameobj(self,obj):
""" frame coordinates by taking into account translation """
if obj.hasclosefit:
envelope = 0
else:
envelope = 1
xmin, ymin = self.valid(obj.xmin-envelope, obj.ymin-envelope)
xmax, ymax = self.valid(obj.xmax+envelope, obj.ymax+envelope)
return xmin, ymin, xmax, ymax
# RECTANGLE ----------------------------
def rectangle(self,a,b,c,d,
mode="lowerleft",name=None,angle=0,
beadtype=None,ismask=False,fake=False,beadtype2=None):
"""
rectangle object
rectangle(xleft,xright,ybottom,ytop [, beadtype=1,mode="lower", angle=0, ismask=False])
rectangle(xcenter,ycenter,width,height [, beadtype=1,mode="center", angle=0, ismask=False])
use rectangle(...,beadtype2=(type,ratio)) to salt an object with beads
from another type and with a given ratio
"""
# object creation
self.counter["all"] += 1
self.counter["rectangle"] += 1
R = Rectangle((self.counter["all"],self.counter["rectangle"]))
if (name != None) and (name != ""):
if self.exist(name):
print('RASTER:: the object "%s" is overwritten',name)
self.delete(name)
R.name = name
else:
name = R.name
if beadtype is not None: R.beadtype = int(np.floor(beadtype))
if beadtype2 is not None:
if not isinstance(beadtype2,tuple) or len(beadtype2)!=2:
raise AttributeError("beadtype2 must be a tuple (beadtype,ratio)")
R.beadtype2 = beadtype2
if ismask: R.beadtype = 0
R.ismask = R.beadtype==0
# build vertices
if mode == "lowerleft":
R.xcenter0 = (a+b)/2
R.ycenter0 = (c+d)/2
R.vertices = [
_rotate(a,c,R.xcenter0,R.ycenter0,angle),
_rotate(b,c,R.xcenter0,R.ycenter0,angle),
_rotate(b,d,R.xcenter0,R.ycenter0,angle),
_rotate(a,d,R.xcenter0,R.ycenter0,angle),
_rotate(a,c,R.xcenter0,R.ycenter0,angle)
] # anti-clockwise, closed (last point repeated)
elif mode == "center":
R.xcenter0 = a
R.ycenter0 = b
R.vertices = [
_rotate(a-c/2,b-d/2,R.xcenter0,R.ycenter0,angle),
_rotate(a+c/2,b-d/2,R.xcenter0,R.ycenter0,angle),
_rotate(a+c/2,b+d/2,R.xcenter0,R.ycenter0,angle),
_rotate(a-c/2,b+d/2,R.xcenter0,R.ycenter0,angle),
_rotate(a-c/2,b-d/2,R.xcenter0,R.ycenter0,angle)
]
else:
raise ValueError('"%s" is not a recognized mode, use "lowerleft" (default) and "center" instead')
# build path object and range
R.codes = [ path.Path.MOVETO,
path.Path.LINETO,
path.Path.LINETO,
path.Path.LINETO,
path.Path.CLOSEPOLY
]
R.nvertices = len(R.vertices)-1
R.xmin0, R.ymin0, R.xmax0, R.ymax0 = R.corners()
R.xmin0, R.ymin0 = self.valid(R.xmin0,R.ymin0)
R.xmax0, R.ymax0 = self.valid(R.xmax0,R.ymax0)
R.angle = angle
# store the object (if not fake)
if fake:
self.counter["all"] -= 1
self.counter["rectangle"] -= 1
return R
else:
self.objects[name] = R
self.nobjects += 1
return None
# CIRCLE ----------------------------
def circle(self,xc,yc,radius,
name=None,shaperatio=1,angle=0,beadtype=None,ismask=False,
resolution=20,shiftangle=0,fake=False,beadtype2=None):
"""
circle object (or any regular polygon)
circle(xcenter,ycenter,radius [, beadtype=1,shaperatio=1, angle=0, ismask=False], resolution=20, shiftangle=0)
use circle(...,beadtype2=(type,ratio)) to salt an object with beads from another type and with a given ratio
"""
# object creation
self.counter["all"] += 1
if resolution==3:
typ = "triangle"
self.counter["triangle"] += 1
G = Triangle((self.counter["all"],self.counter["triangle"]))
elif resolution==4:
typ = "diamond"
self.counter["diamond"] += 1
G = Diamond((self.counter["all"],self.counter["diamond"]))
elif resolution==5:
typ = "pentagon"
self.counter["pentagon"] += 1
G = Pentagon((self.counter["all"],self.counter["pentagon"]))
elif resolution==6:
typ = "hexagon"
self.counter["hexagon"] += 1
G = Hexagon((self.counter["all"],self.counter["hexagon"]))
else:
typ = "circle"
self.counter["circle"] += 1
G = Circle((self.counter["all"],self.counter["circle"]),resolution=resolution)
if (name != None) and (name != ""):
if self.exist(name):
print('RASTER:: the object "%s" is overwritten',name)
self.delete(name)
G.name = name
else:
name = G.name
if beadtype is not None: G.beadtype = int(np.floor(beadtype))
if beadtype2 is not None:
if not isinstance(beadtype2,tuple) or len(beadtype2)!=2:
raise AttributeError("beadtype2 must be a tuple (beadtype,ratio)")
G.beadtype2 = beadtype2
if ismask: G.beadtype = 0
G.ismask = G.beadtype==0
# build vertices
th = np.linspace(0,2*np.pi,G.resolution+1) +shiftangle*np.pi/180
xgen = xc + radius * np.cos(th)
ygen = yc + radius * shaperatio * np.sin(th)
G.xcenter0, G.ycenter0, G.radius = xc, yc, radius
G.vertices, G.codes = [], []
for i in range(G.resolution+1):
G.vertices.append(_rotate(xgen[i],ygen[i],xc,yc,angle))
if i==0:
G.codes.append(path.Path.MOVETO)
elif i==G.resolution:
G.codes.append(path.Path.CLOSEPOLY)
else:
G.codes.append(path.Path.LINETO)
G.nvertices = len(G.vertices)-1
# build path object and range
G.xmin0, G.ymin0, G.xmax0, G.ymax0 = G.corners()
G.xmin0, G.ymin0 = self.valid(G.xmin0,G.ymin0)
G.xmax0, G.ymax0 = self.valid(G.xmax0,G.ymax0)
G.angle, G.shaperatio = angle, shaperatio
# store the object
if fake:
self.counter["all"] -= 1
self.counter[typ] -= 1
return G
else:
self.objects[name] = G
self.nobjects += 1
return None
# OVERLAY -------------------------------
def overlay(self,x0,y0,
name = None,
filename = None,
color = 1,
colormax = None,
ncolors = 4,
beadtype = None,
beadtype2 = None,
ismask = False,
fake = False,
flipud = True,
angle = 0,
scale= (1,1)
):
"""
overlay object: made from an image converted to nc colors
the object is made from the level ranged between ic and jc (bounds included)
note: if palette found, no conversion is applied
O = overlay(x0,y0,filename="/this/is/my/image.png",ncolors=nc,color=ic,colormax=jc,beadtype=b)
O = overlay(...angle=0,scale=(1,1)) to induce rotation and change of scale
O = overlay(....ismask=False,fake=False)
note use overlay(...flipud=False) to prevent image fliping (standard)
Outputs:
O.original original image (PIL)
O.raw image converted to ncolors if needed
"""
if filename is None or filename=="":
raise ValueError("filename is required (valid image)")
O = overlay(counter=(self.counter["all"]+1,self.counter["overlay"]+1),
filename = filename,
xmin = x0,
ymin = y0,
ncolors = ncolors,
flipud = flipud,
angle = angle,
scale = scale
)
O.select(color=color, colormax=colormax)
if (name is not None) and (name !=""):
if self.exist(name):
print('RASTER:: the object "%s" is overwritten',name)
self.delete(name)
O.name = name
else:
name = O.name
if beadtype is not None: O.beadtype = int(np.floor(beadtype))
if beadtype2 is not None:
if not isinstance(beadtype2,tuple) or len(beadtype2)!=2:
raise AttributeError("beadtype2 must be a tuple (beadtype,ratio)")
O.beadtype2 = beadtype2
if ismask: O.beadtype = 0
O.ismask = O.beadtype==0
self.counter["all"] += 1
self.counter["overlay"] += 1
if fake:
self.counter["all"] -= 1
self.counter["overlay"] -= 1
return O
else:
self.objects[name] = O
self.nobjects += 1
return None
# COLLECTION ----------------------------
def collection(self,*obj,
name=None,
beadtype=None,
ismask=None,
translate = [0.0,0.0],
fake = False,
**kwobj):
"""
collection of objects:
collection(draftraster,name="mycollect" [,beadtype=1,ismask=True]
collection(name="mycollect",newobjname1 = obj1, newobjname2 = obj2...)
"""
self.counter["all"] += 1
self.counter["collection"] += 1
C = Collection((self.counter["all"],self.counter["collection"]))
# name
if name != None:
if self.exist(name):
print('RASTER:: the object "%s" is overwritten',name)
self.delete(name)
C.name = name
else:
name = C.name
# build the collection
C.collection = collection(*obj,**kwobj)
xmin = ymin = +1e99
xmax = ymax = -1e99
# apply modifications (beadtype, ismask)
for o in C.collection.keys():
tmp = C.collection.getattr(o)
tmp.translate[0] += translate[0]
tmp.translate[1] += translate[1]
xmin, xmax = min(xmin,tmp.xmin), max(xmax,tmp.xmax)
ymin, ymax = min(ymin,tmp.ymin), max(ymax,tmp.ymax)
if beadtype != None: tmp.beadtype = beadtype
if ismask != None: tmp.ismask = ismask
C.collection.setattr(o,tmp)
C.xmin, C.xmax, C.ymin, C.ymax = xmin, xmax, ymin, ymax
C.width, C.height = xmax-xmin, ymax-ymin
if fake:
return C
else:
self.objects[name] = C
self.nobjects += 1
return None
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# =========== pseudo methods connected to circle() ===========
# TRIANGLE, DIAMOND, PENTAGON, HEXAGON, -----------------------
def triangle(self,xc,yc,radius,name=None,
shaperatio=1,angle=0,beadtype=None,ismask=False,shiftangle=0,fake=False,beadtype2=None):
"""
triangle object
triangle(xcenter,ycenter,radius [, beadtype=1,shaperatio=1, angle=0, ismask=False]
use triangle(...,beadtype2=(type,ratio)) to salt an object with beads from another type and with a given ratio
"""
self.circle(xc,yc,radius,name=name,shaperatio=shaperatio,resolution=3,
angle=angle,beadtype=beadtype,ismask=ismask,shiftangle=0,fake=fake,beadtype2=beadtype2)
def diamond(self,xc,yc,radius,name=None,
shaperatio=1,angle=0,beadtype=None,ismask=False,shiftangle=0,fake=False,beadtype2=None):
"""
diamond object
diamond(xcenter,ycenter,radius [, beadtype=1,shaperatio=1, angle=0, ismask=False]
use diamond(...,beadtype2=(type,ratio)) to salt an object with beads from another type and with a given ratio
"""
self.circle(xc,yc,radius,name=name,shaperatio=shaperatio,resolution=4,
angle=angle,beadtype=beadtype,ismask=ismask,shiftangle=0,fake=fake,beadtype2=beadtype2)
def pentagon(self,xc,yc,radius,name=None,
shaperatio=1,angle=0,beadtype=None,ismask=False,shiftangle=0,fake=False,beadtype2=None):
"""
pentagon object
pentagon(xcenter,ycenter,radius [, beadtype=1,shaperatio=1, angle=0, ismask=False]
use pentagon(...,beadtype2=(type,ratio)) to salt an object with beads from another type and with a given ratio
"""
self.circle(xc,yc,radius,name=name,shaperatio=shaperatio,resolution=5,
angle=angle,beadtype=beadtype,ismask=ismask,shiftangle=0,fake=fake,beadtype2=beadtype2)
def hexagon(self,xc,yc,radius,name=None,
shaperatio=1,angle=0,beadtype=None,ismask=False,shiftangle=0,fake=False,beadtype2=None):
"""
hexagon object
hexagon(xcenter,ycenter,radius [, beadtype=1,shaperatio=1, angle=0, ismask=False]
use hexagon(...,beadtype2=(type,ratio)) to salt an object with beads from another type and with a given ratio
"""
self.circle(xc,yc,radius,name=name,shaperatio=shaperatio,resolution=6,
angle=angle,beadtype=beadtype,ismask=ismask,shiftangle=0,fake=fake,beadtype2=beadtype2)
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# label method ----------------------------
def label(self,name,**fmt):
"""
label:
label(name [, contour=True,edgecolor="orange",facecolor="none",linewidth=2, ax=plt.gca()])
"""
self.figure()
if name in self.objects:
if not self.objects[name].islabelled:
if self.objects[name].alike == "mixed":
for o in self.objects[name].collection:
self.labelobj(o,**fmt)
else:
self.labelobj(self.objects[name],**fmt)
plt.show()
self.objects[name].islabelled = True
else:
raise ValueError("%d is not a valid name (use list()) to list valid objects" % name)
# label object method -----------------------------
def labelobj(self,obj,contour=True,edgecolor="orange",facecolor="none",linewidth=2,ax=plt.gca()):
"""
labelobj:
labelobj(obj [, contour=True,edgecolor="orange",facecolor="none",linewidth=2, ax=plt.gca()])
"""
if contour: contour = obj.hascontour # e.g. overlays do not have contour
if contour:
patch = patches.PathPatch(obj.polygon2plot,
facecolor=facecolor,
edgecolor=edgecolor,
lw=linewidth)
obj.hlabel["contour"] = ax.add_patch(patch)
else:
obj.hlabel["contour"] = None
obj.hlabel["text"] = \
plt.text(obj.xcenter,
obj.ycenter,
"%s\n(t=$%d$,$n_p$=%d)" % (obj.name, obj.beadtype,obj.nbeads),
horizontalalignment = "center",
verticalalignment = "center_baseline",
fontsize=self.fontsize
)
def unlabel(self,name):
""" unlabel """
if name in self.objects:
if self.objects[name].islabelled:
self.objects[name].hlabel["contour"].remove()
self.objects[name].hlabel["text"].remove()
self.objects[name].hlabel = {'contour':[], 'text':[]}
self.objects[name].islabelled = False
else:
raise ValueError("%d is not a valid name (use list()) to list valid objects" % name)
# PLOT method ----------------------------
def plot(self):
""" plot """
for o in self.objects:
if not self.objects[o].isplotted:
if self.objects[o].alike == "mixed":
for o2 in self.objects[o].collection:
self.plotobj(o2)
else:
self.plotobj(self.objects[o])
# store it as plotted
self.objects[o].isplotted = True
if not self.objects[o].ismask:
self.nbeads += self.objects[o].nbeads
# PLOTobj method -----------------------
def plotobj(self,obj):
""" plotobj(obj) """
if obj.alike == "circle":
xmin, ymin, xmax, ymax = self.frameobj(obj)
j,i = np.meshgrid(range(xmin,xmax), range(ymin,ymax))
points = np.vstack((j.flatten(),i.flatten())).T
npoints = points.shape[0]
inside = obj.polygon.contains_points(points)
if obj.beadtype2 is None: # -- no salting --
for k in range(npoints):
if inside[k] and \
points[k,0]>=0 and \
points[k,0]<self.width and \
points[k,1]>=0 and \
points[k,1]<self.height:
self.imbead[points[k,1],points[k,0]] = obj.beadtype
self.imobj[points[k,1],points[k,0]] = obj.index
obj.nbeads += 1
else:
for k in range(npoints): # -- salting --
if inside[k] and \
points[k,0]>=0 and \
points[k,0]<self.width and \
points[k,1]>=0 and \
points[k,1]<self.height:
if np.random.rand()<obj.beadtype2[1]:
self.imbead[points[k,1],points[k,0]] = obj.beadtype2[0]
else:
self.imbead[points[k,1],points[k,0]] = obj.beadtype
self.imobj[points[k,1],points[k,0]] = obj.index
obj.nbeads += 1
elif obj.alike == "overlay":
xmin, ymin, xmax, ymax = self.frameobj(obj)
j,i = np.meshgrid(range(xmin,xmax), range(ymin,ymax))
points = np.vstack((j.flatten(),i.flatten())).T
npoints = points.shape[0]
inside = obj.select()
if obj.beadtype2 is None: # -- no salting --
for k in range(npoints):
if inside[ points[k,1]-ymin, points[k,0]-xmin ] and \
points[k,0]>=0 and \
points[k,0]<self.width and \
points[k,1]>=0 and \
points[k,1]<self.height:
self.imbead[points[k,1],points[k,0]] = obj.beadtype
self.imobj[points[k,1],points[k,0]] = obj.index
obj.nbeads += 1
else:
for k in range(npoints): # -- salting --
if inside[ points[k,0]-ymin, points[k,0]-xmin ] and \
points[k,0]>=0 and \
points[k,0]<self.width and \
points[k,1]>=0 and \
points[k,1]<self.height:
if np.random.rand()<obj.beadtype2[1]:
self.imbead[points[k,1],points[k,0]] = obj.beadtype2[0]
else:
self.imbead[points[k,1],points[k,0]] = obj.beadtype
self.imobj[points[k,1],points[k,0]] = obj.index
obj.nbeads += 1
else:
raise ValueError("This object type is notimplemented")
# SHOW method ----------------------------
def show(self,extra="none",contour=True,what="beadtype"):
""" show method: show(extra="label",contour=True,what="beadtype") """
self.figure()
if what=="beadtype":
imagesc(self.imbead)
elif what == "objindex":
imagesc(self.imobj)
else:
raise ValueError('"beadtype" and "objindex" are the only acceptable values')
if extra == "label":
ax = plt.gca()
for o in self.names():
if not self.objects[o].ismask:
self.label(o,ax=ax,contour=contour)
ax.set_title("raster area: %s \n (n=%d, $n_p$=%d)" %\
(self.name,self.length(),self.nbeads) )
plt.show()
# SHOW method ----------------------------
def print(self,what="beadtype"):
""" print method """
txt = self.string(what=what)
for i in range(len(txt)):
print(txt[i],end="\n")
# FIGURE method ----------------------------
def figure(self):
""" set the current figure """
if self.hfig==[] or not plt.fignum_exists(self.hfig.number):
self.newfigure()
plt.figure(self.hfig.number)
# NEWFIGURE method ----------------------------
def newfigure(self):
""" create a new figure (dpi=200) """
self.hfig = plt.figure(dpi=self.dpi)
# COPY OBJECT ALONG a contour -----------------
def copyalongpath(self,obj,
name="path",
beadtype=None,
path=linear,
xmin=10,
ymin=10,
xmax=70,
ymax=90,
n=7,
USER=struct()):
"""
The method enable to copy an existing object (from the current raster,
from another raster or a fake object) amp,g
Parameters
----------
obj : real or fake object
the object to be copied.
name : string, optional
the name of the object collection. The default is "path".
beadtype : integet, optional
type of bead (can override existing value). The default is None.
path : function, optional
parametric function returning x,y. The default is linear.
x is between xmin and xmax, and y between ymin, ymax
xmin : int64 or float, optional
left x corner position. The default is 10.
ymin : int64 or float, optional
bottom y corner position. The default is 10.
xmax : int64 or float, optional
right x corner position. The default is 70.
ymax : int64 or float, optional
top y corner position. The default is 90.
n : integet, optional
number of copies. The default is 7.
USER : structure to pass specific parameters
Returns
-------
None.
"""
if not isinstance(USER,struct):
raise TypeError("USER should be a structure")
x,y = path(xmin=xmin,ymin=ymin,xmax=xmax,ymax=ymax,n=n,USER=USER)
btyp = obj.beadtype if beadtype == None else beadtype
collect = {}
for i in range(n):
nameobj = "%s_%s_%02d" % (name,obj.name,i)
x[i], y[i] = self.valid(x[i], y[i])
translate = [ x[i]-obj.xcenter, y[i]-obj.ycenter ]
collect[nameobj] = obj.copy(translate=translate,
name=nameobj,
beadtype=btyp)
self.collection(**collect,name=name)
# SCATTER -------------------------------
def scatter(self,
E,
name="emulsion",
beadtype=None,
ismask = False
):
"""
Parameters
----------
E : scatter or emulsion object
codes for x,y and r.
name : string, optional
name of the collection. The default is "emulsion".
beadtype : integer, optional
for all objects. The default is 1.
ismask : logical, optional
Set it to true to force a mask. The default is False.
Raises
------
TypeError
Return an error of the object is not a scatter type.
Returns
-------
None.
"""
if isinstance(E,scatter):
collect = {}
for i in range(E.n):
b = E.beadtype[i] if beadtype==None else beadtype
nameobj = "glob%02d" % i
collect[nameobj] = self.circle(E.x[i],E.y[i],E.r[i],
name=nameobj,beadtype=b,ismask=ismask,fake=True)
self.collection(**collect,name=name)
else:
raise TypeError("the first argument must be an emulsion object")
# %% PRIVATE SUB-CLASSES
# Use the equivalent methods of raster() to call these constructors
# raster.rectangle, raster.circle, raster.triangle... raster.collection
#
# Two counters are used for automatic naming
# counter[0] is the overall index (total number of objects created)
# counter[1] is the index of objects of this type (total number of objects created for this class)
#
# Overview:
# genericpolygon --> Rectancle, Circle
# Circle --> Triangle, Diamond, Pentagon, Hexagon
# Collection --> graphical object for collections (many properties are dynamic)
# struct --> collection is the low-level class container of Collection
class coregeometry:
""" core geometry object"""
@property
def xcenter(self):
""" xcenter with translate """
return self.xcenter0 + self.translate[0]
@property
def ycenter(self):
""" xcenter with translate """
return self.ycenter0 + self.translate[1]
@property
def xmin(self):
""" xleft position """
return self.xmin0 + self.translate[0]
@property
def xmax(self):
""" xright position """
return self.xmax0 + self.translate[0]
@property
def ymin(self):
""" yleft position """
return self.ymin0 + self.translate[1]
@property
def ymax(self):
""" yright position """
return self.ymax0 + self.translate[1]
@property
def width(self):
""" oibject width range """
return self.xmax - self.xmin
@property
def height(self):
""" oibject height range """
return self.ymax - self.ymin
def copy(self,translate=None,beadtype=None,name=""):
""" returns a copy of the graphical object """
if self.alike != "mixed":
dup = deepduplicate(self)
if translate != None: # applies translation
dup.translate[0] += translate[0]
dup.translate[1] += translate[1]
if beadtype != None: # update beadtype
dup.beadtype = beadtype
if name != "": # update name
dup.name = name
return dup
else:
raise ValueError("collections cannot be copied, regenerate the collection instead")
class overlay(coregeometry):
""" generic overlay class """
hascontour = False
hasclosefit = True
def __init__(self,
counter = (0,0),
filename="./sandbox/image.jpg",
xmin = 0,
ymin = 0,
ncolors = 4,
flipud = True,
angle = 0,
scale = (1,1)
):
""" generate an overlay from file
overlay(counter=(c1,c2),filename="this/is/myimage.jpg",xmin=x0,ymin=y0,colors=4)
additional options
overlay(...,flipud=True,angle=0,scale=(1,1))
"""
self.name = "over%03d" % counter[1]
self.kind = "overlay" # kind of object
self.alike = "overlay" # similar object for plotting
self.beadtype = 1 # bead type
self.beadtype2 = None # bead type 2 (alternative beadtype, ratio)
self.nbeads = 0 # number of beads
self.ismask = False # True if beadtype == 0
self.isplotted = False # True if plotted
self.islabelled = False # True if labelled
self.resolution = None # resolution is undefined
self.hlabel = {'contour':[], 'text':[]}
self.index = counter[0]
self.subindex = counter[1]
self.translate = [0.0,0.0] # modification used when an object is duplicated
if scale is None: scale = 1
if not isinstance(scale,(tuple,list)): scale = (scale,scale)
self.scale = scale
if angle is None: angle = 0
self.angle = angle
self.flipud = flipud
if not os.path.isfile(filename):
raise IOError(f'the file "{filename}" does not exist')
self.filename = filename
self.ncolors = ncolors
self.color = None
self.colormax = None
self.original,self.raw,self.im,self.map = self.load()
self.xmin0 = xmin
self.ymin0 = ymin
self.xmax0 = xmin + self.im.shape[1]
self.ymax0 = ymin + self.im.shape[0]
self.xcenter0 = (self.xmin+self.xmax)/2
self.ycenter0 = (self.ymin+self.ymax)/2
def select(self,color=None,colormax=None,scale=None,angle=None):
""" select the color index:
select(color = c) peeks pixels = c
select(color = c, colormax = cmax) peeks pixels>=c and pixels<=cmax
"""
if color is None:
color = self.color
else:
self.color = color
if (colormax is None) and (self.colormax is not None) and (self.colormax > self.color):
colormax = self.colormax
else:
colormax = self.colormax = color
if isinstance(color,int) and color<len(self.map):
S = np.logical_and(self.im>=color,self.im<=colormax)
self.nbeads = np.count_nonzero(S)
return np.flipud(S) if self.flipud else S
raise ValueError("color must be an integer lower than %d" % len(self.map))
def load(self):
""" load image and process it
returns the image, the indexed image and its color map (à la Matlab, such as imread)
note: if the image contains a palette it is used, if not the
image is converted to an indexed image without dihtering
"""
I = Image.open(self.filename)
if self.angle != 0:
I= I.rotate(self.angle)
if self.scale[0] * self.scale[1] != 1:
I = I.resize((round(I.size[0]*self.scale[0]),round(I.size[1]*self.scale[1])))
palette = I.getpalette()
if palette is None:
J=I.convert(mode="P",colors=self.ncolors,palette=Image.Palette.ADAPTIVE)
palette = J.getpalette()
else:
J = I
p = np.array(palette,dtype="uint8").reshape((int(len(palette)/3),3))
ncolors = len(p.sum(axis=1).nonzero()[0]);
if ncolors<self.ncolors:
print(f"only {ncolors} are available")
return I,J, np.array(J,dtype="uint8"), p[:ncolors,:]
def __repr__(self):
""" display for rectangle class """
print("%s - %s object" % (self.name, self.kind))
print(f'\tfilename: "{self.filename}"')
print("\tangle = %0.4g (original image)" % self.angle)
print("\tscale = [%0.4g, %0.4g] (original image)" % self.scale)
print(f"\tncolors = {self.ncolors} (selected={self.color})")
print("\trange x = [%0.4g %0.4g]" % (self.xmin,self.xmax))
print("\trange y = [%0.4g %0.4g]" % (self.ymin,self.ymax))
print("\tcenter = [%0.4g %0.4g]" % (self.xcenter,self.ycenter))
print("\ttranslate = [%0.4g %0.4g]" % (self.translate[0],self.translate[1]))
print("note: use the attribute origina,raw to see the raw image")
return "%s object: %s (beadtype=%d)" % (self.kind,self.name,self.beadtype)
class genericpolygon(coregeometry):
""" generic polygon methods """
hascontour = True
hasclosefit = False
@property
def polygon(self):
"""
R.polygon = path.Path(R.vertices,R.codes,closed=True)
"""
v = self.vertices
if self.translate != None:
vtmp = list(map(list,zip(*v)))
for i in range(len(vtmp[0])):
vtmp[0][i] += self.translate[0]
vtmp[1][i] += self.translate[1]
v = list(zip(*vtmp))
return path.Path(v,self.codes,closed=True)
@property
def polygon2plot(self):
"""
R.polygon2plot = path.Path(R.polygon.vertices+ np.array([1,1]),R.codes,closed=True)
"""
return path.Path(self.polygon.vertices+ np.array([1,1]),self.codes,closed=True)
def corners(self):
""" returns xmin, ymin, xmax, ymax """
return min([self.vertices[k][0] for k in range(self.nvertices)])+self.translate[0], \
min([self.vertices[k][1] for k in range(self.nvertices)])+self.translate[1], \
max([self.vertices[k][0] for k in range(self.nvertices)])+self.translate[0], \
max([self.vertices[k][1] for k in range(self.nvertices)])+self.translate[1]
class Rectangle(genericpolygon):
""" Rectangle class """
def __init__(self,counter):
self.name = "rect%03d" % counter[1]
self.kind = "rectangle" # kind of object
self.alike = "circle" # similar object for plotting
self.beadtype = 1 # bead type
self.beadtype2 = None # bead type 2 (alternative beadtype, ratio)
self.nbeads = 0 # number of beads
self.ismask = False # True if beadtype == 0
self.isplotted = False # True if plotted
self.islabelled = False # True if labelled
self.resolution = None # resolution is undefined
self.hlabel = {'contour':[], 'text':[]}
self.index = counter[0]
self.subindex = counter[1]
self.translate = [0.0,0.0] # modification used when an object is duplicated
def __repr__(self):
""" display for rectangle class """
print("%s - %s object" % (self.name, self.kind))
print("\trange x = [%0.4g %0.4g]" % (self.xmin,self.xmax))
print("\trange y = [%0.4g %0.4g]" % (self.ymin,self.ymax))
print("\tcenter = [%0.4g %0.4g]" % (self.xcenter,self.ycenter))
print("\tangle = %0.4g" % self.angle)
print("\ttranslate = [%0.4g %0.4g]" % (self.translate[0],self.translate[1]))
return "%s object: %s (beadtype=%d)" % (self.kind,self.name,self.beadtype)
class Circle(genericpolygon):
""" Circle class """
def __init__(self,counter,resolution=20):
self.name = "circ%03d" % counter[1]
self.kind = "circle" # kind of object
self.alike = "circle" # similar object for plotting
self.resolution = resolution # default resolution
self.beadtype = 1 # bead type
self.beadtype2 = None # bead type 2 (alternative beadtype, ratio)
self.nbeads = 0 # number of beads
self.ismask = False # True if beadtype == 0
self.isplotted = False # True if plotted
self.islabelled = False # True if labelled
self.hlabel = {'contour':[], 'text':[]}
self.index = counter[0]
self.subindex = counter[1]
self.translate = [0.0,0.0] # modification used when an object is duplicated
def __repr__(self):
""" display circle """
print("%s - %s object" % (self.name,self.kind) )
print("\trange x = [%0.4g %0.4g]" % (self.xmin,self.xmax))
print("\trange y = [%0.4g %0.4g]" % (self.ymin,self.ymax))
print("\tcenter = [%0.4g %0.4g]" % (self.xcenter,self.ycenter))
print("\tradius = %0.4g" % self.radius)
print("\tshaperatio = %0.4g" % self.shaperatio)
print("\tangle = %0.4g" % self.angle)
print("\ttranslate = [%0.4g %0.4g]" % (self.translate[0],self.translate[1]))
return "%s object: %s (beadtype=%d)" % (self.kind, self.name,self.beadtype)
class Triangle(Circle):
""" Triangle class """
def __init__(self,counter):
super().__init__(counter,resolution=3)
self.name = "tri%03d" % counter[1]
self.kind = "triangle" # kind of object
class Diamond(Circle):
""" Diamond class """
def __init__(self,counter):
super().__init__(counter,resolution=4)
self.name = "diam%03d" % counter[1]
self.kind = "diamond" # kind of object
class Pentagon(Circle):
""" Pentagon class """
def __init__(self,counter):
super().__init__(counter,resolution=5)
self.name = "penta%03d" % counter[1]
self.kind = "pentagon" # kind of object
class Hexagon(Circle):
""" Hexagon class """
def __init__(self,counter):
super().__init__(counter,resolution=6)
self.name = "hex%03d" % counter[1]
self.kind = "Hexagon" # kind of object
class collection(struct):
""" collection class container (not to be called directly) """
_type = "collect" # object type
_fulltype = "Collections" # full name
_ftype = "collection" # field name
def __init__(self,*obj,**kwobj):
# store the objects with their alias
super().__init__(**kwobj)
# store objects with their real names
for o in obj:
if isinstance(o,raster):
s = struct.dict2struct(o.objects)
list_s = s.keys()
for i in range(len(list_s)): self.setattr(list_s[i], s[i].copy())
elif o!=None:
self.setattr(o.name, o.copy())
class Collection:
""" Collection object """
def __init__(self,counter):
self.name = "collect%03d" % counter[1]
self.kind = "collection" # kind of object
self.alike = "mixed" # similar object for plotting
self.nbeads = 0 # number of beads
self.ismask = False # True if beadtype == 0
self.isplotted = False # True if plotted
self.islabelled = False # True if labelled
self.resolution = None # resolution is undefined
self.hlabel = {'contour':[], 'text':[]}
self.index = counter[0]
self.subindex = counter[1]
self.collection = collection()
self.translate = [0.0,0.0]
def __repr__(self):
keylengths = [len(key) for key in self.collection.keys()]
width = max(10,max(keylengths)+2)
fmt = "%%%ss:" % width
line = ( fmt % ('-'*(width-2)) ) + ( '-'*(min(40,width*5)) )
print("%s - %s object" % (self.name, self.kind))
print(line)
print("\trange x = [%0.4g %0.4g]" % (self.xmin,self.xmax))
print("\trange y = [%0.4g %0.4g]" % (self.ymin,self.ymax))
print("\tcenter = [%0.4g %0.4g]" % self.xycenter)
print("\ttranslate = [%0.4g %0.4g]" % (self.translate[0],self.translate[1]))
print(line,' name: type "original name" [centerx centery] [translatex translatey]',line,sep="\n")
for key,value in self.collection.items():
print(fmt % key,value.kind,
'"%s"' % value.name,
"[%0.4g %0.4g]" % (value.xcenter,value.ycenter),
"[%0.4g %0.4g]" % (value.translate[0],value.translate[1]))
print(line)
return "%s object: %s (beadtype=[%s])" % (self.kind,self.name,", ".join(map(str,self.beadtype)))
# GET -----------------------------
def get(self,name):
""" returns the object """
if name in self.collection:
return self.collection.getattr(name)
else:
raise ValueError('the object "%s" does not exist, use list()' % name)
# GETATTR --------------------------
def __getattr__(self,key):
""" get attribute override """
return self.get(key)
@property
def xycenter(self):
""" returns the xcenter and ycenter of the collection """
sx = sy = 0
n = len(self.collection)
for o in self.collection:
sx += o.xcenter
sy += o.ycenter
return sx/n, sy/n
@property
def xcenter(self):
""" returns xcenter """
xc,_ = self.xycenter
@property
def ycenter(self):
""" returns ycenter """
_,yc = self.xycenter
@property
def beadtype(self):
""" returns the xcenter and ycenter of the collection """
b = []
for o in self.collection:
if o.beadtype not in b:
b.append(o.beadtype)
if len(b)==0:
return 1
else:
return b
# %% scatter class and emulsion class
# Simplified scatter and emulsion generator
class scatter():
""" generic top scatter class """
def __init__(self):
"""
The scatter class provides an easy constructor
to distribute in space objects according to their
positions x,y, size r (radius) and beadtype.
The class is used to derive emulsions.
Returns
-------
None.
"""
self.x = np.array([],dtype=int)
self.y = np.array([],dtype=int)
self.r = np.array([],dtype=int)
self.beadtype = []
@property
def n(self):
return len(self.x)
def pairdist(self,x,y):
""" pair distance to the surface of all disks/spheres """
if self.n==0:
return np.Inf
else:
return np.floor(np.sqrt((x-self.x)**2+(y-self.y)**2)-self.r)
class emulsion(scatter):
""" emulsion generator """
def __init__(self, xmin=10, ymin=10, xmax=90, ymax=90,
maxtrials=1000, beadtype=1, forcedinsertion=True):
"""
Parameters
----------
The insertions are performed between xmin,ymin and xmax,ymax
xmin : int64 or real, optional
x left corner. The default is 10.
ymin : int64 or real, optional
y bottom corner. The default is 10.
xmax : int64 or real, optional
x right corner. The default is 90.
ymax : int64 or real, optional
y top corner. The default is 90.
beadtype : default beadtype to apply if not precised at insertion
maxtrials : integer, optional
Maximum of attempts for an object. The default is 1000.
forcedinsertion : logical, optional
Set it to true to force the next insertion. The default is True.
Returns
-------
None.
"""
super().__init__()
self.xmin, self.xmax, self.ymin, self.ymax = xmin, xmax, ymin, ymax
self.lastinsertion = (None,None,None,None) # x,y,r, beadtype
self.width = xmax-xmin
self.height = ymax-ymin
self.defautbeadtype = beadtype
self.maxtrials = maxtrials
self.forcedinsertion = forcedinsertion
def __repr__(self):
print(f" Emulsion object\n\t{self.width}x{self.height} starting at x={self.xmin}, y={self.ymin}")
print(f"\tcontains {self.n} insertions")
print("\tmaximum insertion trials:", self.maxtrials)
print("\tforce next insertion if previous fails:", self.forcedinsertion)
return f"emulsion with {self.n} insertions"
def walldist(self,x,y):
""" shortest distance to the wall """
return min(abs(x-self.xmin),abs(y-self.ymin),abs(x-self.xmax),abs(y-self.ymax))
def dist(self,x,y):
""" shortest distance of the center (x,y) to the wall or any object"""
return np.minimum(np.min(self.pairdist(x,y)),self.walldist(x,y))
def accepted(self,x,y,r):
""" acceptation criterion """
return self.dist(x,y)>r
def rand(self):
""" random position x,y """
return np.round(np.random.uniform(low=self.xmin,high=self.xmax)), \
np.round(np.random.uniform(low=self.ymin,high=self.ymax))
def setbeadtype(self,beadtype):
""" set the default or the supplied beadtype """
if beadtype == None:
self.beadtype.append(self.defautbeadtype)
return self.defautbeadtype
else:
self.beadtype.append(beadtype)
return beadtype
def insertone(self,x=None,y=None,r=None,beadtype=None,overlap=False):
"""
insert one object of radius r
properties:
x,y coordinates (if missing, picked randomly from uniform distribution)
r radius (default = 2% of diagonal)
beadtype (default = defautbeadtype)
overlap = False (accept only if no overlap)
"""
attempt, success = 0, False
random = (x==None) or (y==None)
if r==None:
r = 0.02*np.sqrt(self.width**2+self.height**2)
while not success and attempt<self.maxtrials:
attempt += 1
if random: x,y = self.rand()
if overlap:
success = True
else:
success = self.accepted(x,y,r)
if success:
self.x = np.append(self.x,x)
self.y = np.append(self.y,y)
self.r = np.append(self.r,r)
b=self.setbeadtype(beadtype)
self.lastinsertion = (x,y,r,b)
return success
def insertion(self,rlist,beadtype=None):
"""
insert a list of objects
nsuccess=insertion(rlist,beadtype=None)
beadtype=b forces the value b
if None, defaultbeadtype is used instead
"""
rlist.sort(reverse=True)
ntodo = len(rlist)
n = nsuccess = 0
stop = False
while not stop:
n += 1
success = self.insertone(r=rlist[n-1],beadtype=beadtype)
if success: nsuccess += 1
stop = (n==ntodo) or (not success and not self.forcedinsertion)
if nsuccess==ntodo:
print(f"{nsuccess} objects inserted successfully")
else:
print(f"partial success: {nsuccess} of {ntodo} objects inserted")
return nsuccess
class coreshell(emulsion):
"""
coreshell generator
inherited from emulsion
the method insertion has been modified to integrate
thickess = shell thickness value
beadtype = (shell beadtype, core beadtype)
"""
def insertion(self,rlist,thickness=None, beadtype=(1,2)):
"""
insert a list of objects
nsuccess=insertion(...)
List of properties
rlist = [r1, r2,...]
thickness = shell thcikness value
beadtype = (shell beadtype, core beadtype)
"""
# check arguments
if thickness==None:
raise AttributeError("set a value for the shell thickness")
if not isinstance(beadtype,tuple):
raise TypeError("beadtype must be a turple")
# prepare the work
rlist.sort(reverse=True)
ntodo = len(rlist)
n = nsuccess = 0
stop = False
while not stop:
# next insertion and check rcore
n += 1
rshell = rlist[n-1]
rcore = rshell - thickness
if rcore<=0:
raise ValueError(
f"The external radius={rshell} cannot be smaller than the shell thickness={thickness}")
# do the insertion of the shell (largest radius)
success = self.insertone(r=rshell,beadtype=beadtype[0],overlap=False)
if success:
success = self.insertone(
x = self.lastinsertion[0],
y = self.lastinsertion[1],
r=rcore,
beadtype=beadtype[1],
overlap=True)
nsuccess += 1
stop = (n==ntodo) or (not success and not self.forcedinsertion)
if nsuccess==ntodo:
print(f"{nsuccess} objects inserted successfully")
else:
print(f"partial success: {nsuccess} of {ntodo} objects inserted")
return nsuccess
# %% debug section - generic code to test methods (press F5)
# ===================================================
# main()
# ===================================================
# for debugging purposes (code called as a script)
# the code is called from here
# ===================================================
if __name__ == '__main__':
# %% basic example
plt.close("all")
R = raster()
R.rectangle(1,24,2,20,name='rect1')
R.rectangle(60,80,50,81,name='rect2',beadtype=2,angle=40,beadtype2=(9,0.2))
R.rectangle(50,50,10,10,mode="center",angle=45,beadtype=1)
R.circle(45,20,5,name='C1',beadtype=3,beadtype2=(8,0.25))
R.circle(35,10,5,name='C2',beadtype=3)
R.circle(15,30,10,name='p1',beadtype=4,shaperatio=0.2,angle=-30)
R.circle(12,40,8,name='p2',beadtype=4,shaperatio=0.2,angle=20)
R.circle(12,80,22,name='p3',beadtype=4,shaperatio=1.3,angle=20,beadtype2=(9,0.1))
R.triangle(85,20,10,name='T1',beadtype=5,angle=20)
R.diamond(85,35,5,name='D1',beadtype=5,angle=20,beadtype2=(9,0.5))
R.pentagon(50,35,5,name='P1',beadtype=5,angle=90)
R.hexagon(47,85,12,name='H1',beadtype=5,angle=90)
R.label("rect003")
R.plot()
R.list()
R.show()
R.clear()
R.show()
R.plot()
R.show(extra="label")
R.label("rect003")
R.unlabel('rect1')
X=R.data()
# %% another example
S = raster(width=1000,height=1000)
S.rectangle(150,850,850,1000,name="top",beadtype=1)
S.rectangle(150,850,0,150,name="bottom",beadtype=2)
S.circle(500,500,480,name="mask",ismask=True,resolution=500)
S.triangle(250,880,80,name='tooth1',angle=60,beadtype=1)
S.triangle(750,880,80,name='tooth2',angle=-0,beadtype=1)
S.circle(500,200,300,name="tongue",beadtype=5,shaperatio=0.3,resolution=300)
S.rectangle(500,450,320,320,name="food",mode="center",beadtype=3)
S.plot()
S.show(extra="label",contour=False)
# %% advanced example
#plt.close("all")
draft = raster()
draft.rectangle(1,24,2,20,name='rect1'),
draft.rectangle(60,80,50,81,name='rect2',beadtype=2,angle=40),
draft.rectangle(50,50,10,10,mode="center",angle=45,beadtype=1),
draft.circle(45,20,5,name='C1',beadtype=3),
draft.circle(35,10,5,name='C2',beadtype=3),
draft.circle(10,10,2,name="X",beadtype=4)
A = raster()
A.collection(draft,name="C1",beadtype=1,translate=[10,30])
repr(A)
A.objects
A.plot()
A.show(extra="label")
A.objects
B = raster()
#B.collection(X=draft.X,beadtype=1,translate=[50,50])
B.copyalongpath(draft.X,name="PX",beadtype=2,
path=arc,
xmin=10,
ymin=10,
xmax=90,
ymax=50,
n=12)
B.plot()
B.show(extra="label")
# %% emulsion example
C = raster(width=400,height=400)
e = emulsion(xmin=10, ymin=10, xmax=390, ymax=390)
e.insertion([60,50,40,30,20,15,15,10,8,20,12,8,6,4,11,13],beadtype=1)
e.insertion([30,10,20,2,4,5,5,10,12,20,25,12,14,16,17],beadtype=2)
e.insertion([40,2,8,6,6,5,5,2,3,4,4,4,4,4,10,16,12,14,13],beadtype=3)
C.scatter(e,name="emulsion")
C.plot()
C.show()
# %% core-shell example
D = raster(width=400,height=400)
cs = coreshell(xmin=10, ymin=10, xmax=390, ymax=390)
cs.insertion([60,50,40,30,20,15,15,10,8,20,12,8,11,13],beadtype=(1,2),thickness = 4)
D.scatter(cs,name="core-shell")
D.plot()
D.show()
# %% overlay example
I = raster(width=600,height=600)
I.overlay(30,100,name="pix0",filename="./sandbox/image.jpg",ncolors=4,color=0,beadtype=1,angle=10,scale=(1.1,1.1))
I.overlay(30,100,name="pix2",filename="./sandbox/image.jpg",ncolors=4,color=2,beadtype=2,angle=10,scale=(1.1,1.1))
I.label("pix0")
I.plot()
I.show(extra="label")
I.pix0.original
I.pix0.raw
a = I.torgb("objindex",(512,512))
a.show()
a.save("./tmp/preview.png")
Functions
def arc(xmin=10, ymin=50, xmax=80, ymax=50, n=5, USER=structure (struct object) with 2 fields)
-
Point distributed along an arc X,Y = arc(xmin=value,ymin=value,xmax=value,ymax=value,n=int, USER=struct(radius=value,direction=1)) Use direction to choose the upward +1 or downward -1 circle see: https://rosettacode.org/wiki/Circles_of_given_radius_through_two_points
Expand source code
def arc(xmin=10,ymin=50,xmax=80,ymax=50,n=5,USER=struct(radius=20,direction=1)): """ Point distributed along an arc X,Y = arc(xmin=value,ymin=value,xmax=value,ymax=value,n=int, USER=struct(radius=value,direction=1)) Use direction to choose the upward +1 or downward -1 circle see: https://rosettacode.org/wiki/Circles_of_given_radius_through_two_points """ R = 0 if "radius" not in USER else USER.radius direction = +1 if "direction" not in USER else USER.direction dx,dy = xmax-xmin, ymax-ymin q = np.sqrt(dx**2+dy**2) # distance R = max(R,q/2) # radius constraint d = np.sqrt(R**2-(q/2)**2) # distance along the mirror line xc = (xmin+xmax)/2 - direction * d*dy/q yc = (ymin+ymax)/2 + direction * d*dx/q thmin,thmax = np.arctan((ymin-yc)/(xmin-xc)), np.arctan((ymax-yc)/(xmax-xc)) if d==0: thmax = thmin + np.pi th = np.linspace(thmin,thmax,n) return xc+np.cos(th)*R,yc+np.sin(th)*R
def imagesc(im, x=None, y=None)
-
imagesc à la Matlab imagesc(np2array)
Expand source code
def imagesc(im,x=None,y=None): """ imagesc à la Matlab imagesc(np2array) """ if x==None: x=np.arange(1,np.shape(im)[1]+1) if y==None: y=np.arange(1,np.shape(im)[0]+1) plt.imshow(im, extent=_extents(x) + _extents(y), aspect="auto", origin="lower", interpolation="none")
def ind2rgb(im, ncolors=64)
-
Convert indexed image (NumPy array) to RGB rgb = ind2rgb(np2array,ncolors=nc) use rgb.save("/path/filename.png") for saving
Expand source code
def ind2rgb(im,ncolors=64): """ Convert indexed image (NumPy array) to RGB rgb = ind2rgb(np2array,ncolors=nc) use rgb.save("/path/filename.png") for saving """ raw = Image.fromarray(np.flipud(im),"P") col0 = np.array(np.round(255*cmap.get_cmap("viridis",ncolors).colors[:,:3]),dtype="uint8") col = bytearray(np.resize(col0,(256,3)).flatten()) pal = ImagePalette.ImagePalette(mode="RGB",palette=col) #Image.convert(mode=None, matrix=None, dither=None, palette=Palette.WEB, colors=256) raw.putpalette(pal) return raw
def linear(xmin=10, ymin=10, xmax=80, ymax=80, n=5, USER=structure (struct object) with 0 fields)
-
Equispaced points along a trajectory X,Y = linear(xmin=value,ymin=value,xmax=value,ymax=value,n=int)
Expand source code
def linear(xmin=10,ymin=10,xmax=80,ymax=80,n=5,USER=struct()): """ Equispaced points along a trajectory X,Y = linear(xmin=value,ymin=value,xmax=value,ymax=value,n=int) """ return np.linspace(xmin,xmax,n), np.linspace(ymin,ymax,n)
Classes
class Circle (counter, resolution=20)
-
Circle class
Expand source code
class Circle(genericpolygon): """ Circle class """ def __init__(self,counter,resolution=20): self.name = "circ%03d" % counter[1] self.kind = "circle" # kind of object self.alike = "circle" # similar object for plotting self.resolution = resolution # default resolution self.beadtype = 1 # bead type self.beadtype2 = None # bead type 2 (alternative beadtype, ratio) self.nbeads = 0 # number of beads self.ismask = False # True if beadtype == 0 self.isplotted = False # True if plotted self.islabelled = False # True if labelled self.hlabel = {'contour':[], 'text':[]} self.index = counter[0] self.subindex = counter[1] self.translate = [0.0,0.0] # modification used when an object is duplicated def __repr__(self): """ display circle """ print("%s - %s object" % (self.name,self.kind) ) print("\trange x = [%0.4g %0.4g]" % (self.xmin,self.xmax)) print("\trange y = [%0.4g %0.4g]" % (self.ymin,self.ymax)) print("\tcenter = [%0.4g %0.4g]" % (self.xcenter,self.ycenter)) print("\tradius = %0.4g" % self.radius) print("\tshaperatio = %0.4g" % self.shaperatio) print("\tangle = %0.4g" % self.angle) print("\ttranslate = [%0.4g %0.4g]" % (self.translate[0],self.translate[1])) return "%s object: %s (beadtype=%d)" % (self.kind, self.name,self.beadtype)
Ancestors
Subclasses
Inherited members
class Collection (counter)
-
Collection object
Expand source code
class Collection: """ Collection object """ def __init__(self,counter): self.name = "collect%03d" % counter[1] self.kind = "collection" # kind of object self.alike = "mixed" # similar object for plotting self.nbeads = 0 # number of beads self.ismask = False # True if beadtype == 0 self.isplotted = False # True if plotted self.islabelled = False # True if labelled self.resolution = None # resolution is undefined self.hlabel = {'contour':[], 'text':[]} self.index = counter[0] self.subindex = counter[1] self.collection = collection() self.translate = [0.0,0.0] def __repr__(self): keylengths = [len(key) for key in self.collection.keys()] width = max(10,max(keylengths)+2) fmt = "%%%ss:" % width line = ( fmt % ('-'*(width-2)) ) + ( '-'*(min(40,width*5)) ) print("%s - %s object" % (self.name, self.kind)) print(line) print("\trange x = [%0.4g %0.4g]" % (self.xmin,self.xmax)) print("\trange y = [%0.4g %0.4g]" % (self.ymin,self.ymax)) print("\tcenter = [%0.4g %0.4g]" % self.xycenter) print("\ttranslate = [%0.4g %0.4g]" % (self.translate[0],self.translate[1])) print(line,' name: type "original name" [centerx centery] [translatex translatey]',line,sep="\n") for key,value in self.collection.items(): print(fmt % key,value.kind, '"%s"' % value.name, "[%0.4g %0.4g]" % (value.xcenter,value.ycenter), "[%0.4g %0.4g]" % (value.translate[0],value.translate[1])) print(line) return "%s object: %s (beadtype=[%s])" % (self.kind,self.name,", ".join(map(str,self.beadtype))) # GET ----------------------------- def get(self,name): """ returns the object """ if name in self.collection: return self.collection.getattr(name) else: raise ValueError('the object "%s" does not exist, use list()' % name) # GETATTR -------------------------- def __getattr__(self,key): """ get attribute override """ return self.get(key) @property def xycenter(self): """ returns the xcenter and ycenter of the collection """ sx = sy = 0 n = len(self.collection) for o in self.collection: sx += o.xcenter sy += o.ycenter return sx/n, sy/n @property def xcenter(self): """ returns xcenter """ xc,_ = self.xycenter @property def ycenter(self): """ returns ycenter """ _,yc = self.xycenter @property def beadtype(self): """ returns the xcenter and ycenter of the collection """ b = [] for o in self.collection: if o.beadtype not in b: b.append(o.beadtype) if len(b)==0: return 1 else: return b
Instance variables
var beadtype
-
returns the xcenter and ycenter of the collection
Expand source code
@property def beadtype(self): """ returns the xcenter and ycenter of the collection """ b = [] for o in self.collection: if o.beadtype not in b: b.append(o.beadtype) if len(b)==0: return 1 else: return b
var xcenter
-
returns xcenter
Expand source code
@property def xcenter(self): """ returns xcenter """ xc,_ = self.xycenter
var xycenter
-
returns the xcenter and ycenter of the collection
Expand source code
@property def xycenter(self): """ returns the xcenter and ycenter of the collection """ sx = sy = 0 n = len(self.collection) for o in self.collection: sx += o.xcenter sy += o.ycenter return sx/n, sy/n
var ycenter
-
returns ycenter
Expand source code
@property def ycenter(self): """ returns ycenter """ _,yc = self.xycenter
Methods
def get(self, name)
-
returns the object
Expand source code
def get(self,name): """ returns the object """ if name in self.collection: return self.collection.getattr(name) else: raise ValueError('the object "%s" does not exist, use list()' % name)
class Diamond (counter)
-
Diamond class
Expand source code
class Diamond(Circle): """ Diamond class """ def __init__(self,counter): super().__init__(counter,resolution=4) self.name = "diam%03d" % counter[1] self.kind = "diamond" # kind of object
Ancestors
Inherited members
class Hexagon (counter)
-
Hexagon class
Expand source code
class Hexagon(Circle): """ Hexagon class """ def __init__(self,counter): super().__init__(counter,resolution=6) self.name = "hex%03d" % counter[1] self.kind = "Hexagon" # kind of object
Ancestors
Inherited members
class Pentagon (counter)
-
Pentagon class
Expand source code
class Pentagon(Circle): """ Pentagon class """ def __init__(self,counter): super().__init__(counter,resolution=5) self.name = "penta%03d" % counter[1] self.kind = "pentagon" # kind of object
Ancestors
Inherited members
class Rectangle (counter)
-
Rectangle class
Expand source code
class Rectangle(genericpolygon): """ Rectangle class """ def __init__(self,counter): self.name = "rect%03d" % counter[1] self.kind = "rectangle" # kind of object self.alike = "circle" # similar object for plotting self.beadtype = 1 # bead type self.beadtype2 = None # bead type 2 (alternative beadtype, ratio) self.nbeads = 0 # number of beads self.ismask = False # True if beadtype == 0 self.isplotted = False # True if plotted self.islabelled = False # True if labelled self.resolution = None # resolution is undefined self.hlabel = {'contour':[], 'text':[]} self.index = counter[0] self.subindex = counter[1] self.translate = [0.0,0.0] # modification used when an object is duplicated def __repr__(self): """ display for rectangle class """ print("%s - %s object" % (self.name, self.kind)) print("\trange x = [%0.4g %0.4g]" % (self.xmin,self.xmax)) print("\trange y = [%0.4g %0.4g]" % (self.ymin,self.ymax)) print("\tcenter = [%0.4g %0.4g]" % (self.xcenter,self.ycenter)) print("\tangle = %0.4g" % self.angle) print("\ttranslate = [%0.4g %0.4g]" % (self.translate[0],self.translate[1])) return "%s object: %s (beadtype=%d)" % (self.kind,self.name,self.beadtype)
Ancestors
Inherited members
class Triangle (counter)
-
Triangle class
Expand source code
class Triangle(Circle): """ Triangle class """ def __init__(self,counter): super().__init__(counter,resolution=3) self.name = "tri%03d" % counter[1] self.kind = "triangle" # kind of object
Ancestors
Inherited members
class collection (*obj, **kwobj)
-
collection class container (not to be called directly)
constructor, use debug=True to report eval errors
Expand source code
class collection(struct): """ collection class container (not to be called directly) """ _type = "collect" # object type _fulltype = "Collections" # full name _ftype = "collection" # field name def __init__(self,*obj,**kwobj): # store the objects with their alias super().__init__(**kwobj) # store objects with their real names for o in obj: if isinstance(o,raster): s = struct.dict2struct(o.objects) list_s = s.keys() for i in range(len(list_s)): self.setattr(list_s[i], s[i].copy()) elif o!=None: self.setattr(o.name, o.copy())
Ancestors
- pizza.private.mstruct.struct
class coregeometry
-
core geometry object
Expand source code
class coregeometry: """ core geometry object""" @property def xcenter(self): """ xcenter with translate """ return self.xcenter0 + self.translate[0] @property def ycenter(self): """ xcenter with translate """ return self.ycenter0 + self.translate[1] @property def xmin(self): """ xleft position """ return self.xmin0 + self.translate[0] @property def xmax(self): """ xright position """ return self.xmax0 + self.translate[0] @property def ymin(self): """ yleft position """ return self.ymin0 + self.translate[1] @property def ymax(self): """ yright position """ return self.ymax0 + self.translate[1] @property def width(self): """ oibject width range """ return self.xmax - self.xmin @property def height(self): """ oibject height range """ return self.ymax - self.ymin def copy(self,translate=None,beadtype=None,name=""): """ returns a copy of the graphical object """ if self.alike != "mixed": dup = deepduplicate(self) if translate != None: # applies translation dup.translate[0] += translate[0] dup.translate[1] += translate[1] if beadtype != None: # update beadtype dup.beadtype = beadtype if name != "": # update name dup.name = name return dup else: raise ValueError("collections cannot be copied, regenerate the collection instead")
Subclasses
Instance variables
var height
-
oibject height range
Expand source code
@property def height(self): """ oibject height range """ return self.ymax - self.ymin
var width
-
oibject width range
Expand source code
@property def width(self): """ oibject width range """ return self.xmax - self.xmin
var xcenter
-
xcenter with translate
Expand source code
@property def xcenter(self): """ xcenter with translate """ return self.xcenter0 + self.translate[0]
var xmax
-
xright position
Expand source code
@property def xmax(self): """ xright position """ return self.xmax0 + self.translate[0]
var xmin
-
xleft position
Expand source code
@property def xmin(self): """ xleft position """ return self.xmin0 + self.translate[0]
var ycenter
-
xcenter with translate
Expand source code
@property def ycenter(self): """ xcenter with translate """ return self.ycenter0 + self.translate[1]
var ymax
-
yright position
Expand source code
@property def ymax(self): """ yright position """ return self.ymax0 + self.translate[1]
var ymin
-
yleft position
Expand source code
@property def ymin(self): """ yleft position """ return self.ymin0 + self.translate[1]
Methods
def copy(self, translate=None, beadtype=None, name='')
-
returns a copy of the graphical object
Expand source code
def copy(self,translate=None,beadtype=None,name=""): """ returns a copy of the graphical object """ if self.alike != "mixed": dup = deepduplicate(self) if translate != None: # applies translation dup.translate[0] += translate[0] dup.translate[1] += translate[1] if beadtype != None: # update beadtype dup.beadtype = beadtype if name != "": # update name dup.name = name return dup else: raise ValueError("collections cannot be copied, regenerate the collection instead")
class coreshell (xmin=10, ymin=10, xmax=90, ymax=90, maxtrials=1000, beadtype=1, forcedinsertion=True)
-
coreshell generator inherited from emulsion the method insertion has been modified to integrate thickess = shell thickness value beadtype = (shell beadtype, core beadtype)
Parameters
- The insertions are performed between xmin,ymin and xmax,ymax
xmin
:int64
orreal
, optional- x left corner. The default is 10.
ymin
:int64
orreal
, optional- y bottom corner. The default is 10.
xmax
:int64
orreal
, optional- x right corner. The default is 90.
ymax
:int64
orreal
, optional- y top corner. The default is 90.
beadtype
:default beadtype to apply if not precised at insertion
maxtrials
:integer
, optional- Maximum of attempts for an object. The default is 1000.
forcedinsertion
:logical
, optional- Set it to true to force the next insertion. The default is True.
Returns
None.
Expand source code
class coreshell(emulsion): """ coreshell generator inherited from emulsion the method insertion has been modified to integrate thickess = shell thickness value beadtype = (shell beadtype, core beadtype) """ def insertion(self,rlist,thickness=None, beadtype=(1,2)): """ insert a list of objects nsuccess=insertion(...) List of properties rlist = [r1, r2,...] thickness = shell thcikness value beadtype = (shell beadtype, core beadtype) """ # check arguments if thickness==None: raise AttributeError("set a value for the shell thickness") if not isinstance(beadtype,tuple): raise TypeError("beadtype must be a turple") # prepare the work rlist.sort(reverse=True) ntodo = len(rlist) n = nsuccess = 0 stop = False while not stop: # next insertion and check rcore n += 1 rshell = rlist[n-1] rcore = rshell - thickness if rcore<=0: raise ValueError( f"The external radius={rshell} cannot be smaller than the shell thickness={thickness}") # do the insertion of the shell (largest radius) success = self.insertone(r=rshell,beadtype=beadtype[0],overlap=False) if success: success = self.insertone( x = self.lastinsertion[0], y = self.lastinsertion[1], r=rcore, beadtype=beadtype[1], overlap=True) nsuccess += 1 stop = (n==ntodo) or (not success and not self.forcedinsertion) if nsuccess==ntodo: print(f"{nsuccess} objects inserted successfully") else: print(f"partial success: {nsuccess} of {ntodo} objects inserted") return nsuccess
Ancestors
Methods
def insertion(self, rlist, thickness=None, beadtype=(1, 2))
-
insert a list of objects nsuccess=insertion(…)
List of properties rlist = [r1, r2,...] thickness = shell thcikness value beadtype = (shell beadtype, core beadtype)
Expand source code
def insertion(self,rlist,thickness=None, beadtype=(1,2)): """ insert a list of objects nsuccess=insertion(...) List of properties rlist = [r1, r2,...] thickness = shell thcikness value beadtype = (shell beadtype, core beadtype) """ # check arguments if thickness==None: raise AttributeError("set a value for the shell thickness") if not isinstance(beadtype,tuple): raise TypeError("beadtype must be a turple") # prepare the work rlist.sort(reverse=True) ntodo = len(rlist) n = nsuccess = 0 stop = False while not stop: # next insertion and check rcore n += 1 rshell = rlist[n-1] rcore = rshell - thickness if rcore<=0: raise ValueError( f"The external radius={rshell} cannot be smaller than the shell thickness={thickness}") # do the insertion of the shell (largest radius) success = self.insertone(r=rshell,beadtype=beadtype[0],overlap=False) if success: success = self.insertone( x = self.lastinsertion[0], y = self.lastinsertion[1], r=rcore, beadtype=beadtype[1], overlap=True) nsuccess += 1 stop = (n==ntodo) or (not success and not self.forcedinsertion) if nsuccess==ntodo: print(f"{nsuccess} objects inserted successfully") else: print(f"partial success: {nsuccess} of {ntodo} objects inserted") return nsuccess
Inherited members
class data3 (*args: Any)
-
The
data
class provides tools to read, write, and manipulate LAMMPS data files, enabling seamless integration with thedump
class for restart generation and simulation data management.Initialize a data object.
Parameters
*args: Variable length argument list. - No arguments: Creates an empty data object. - One argument (filename or dump object): Initializes from a file or dump object. - Two arguments (dump object, timestep): Initializes from a dump object at a specific timestep.
Expand source code
class data: """ The `data` class provides tools to read, write, and manipulate LAMMPS data files, enabling seamless integration with the `dump` class for restart generation and simulation data management. """ # Class-level keywords for headers and sections HKEYWORDS = [ "atoms", "ellipsoids", "lines", "triangles", "bodies", "bonds", "angles", "dihedrals", "impropers", "atom types", "bond types", "angle types", "dihedral types", "improper types", "xlo xhi", "ylo yhi", "zlo zhi", "xy xz yz", ] SKEYWORDS = [ ["Masses", "atom types"], ["Atoms", "atoms"], ["Ellipsoids", "ellipsoids"], ["Lines", "lines"], ["Triangles", "triangles"], ["Bodies", "bodies"], ["Bonds", "bonds"], ["Angles", "angles"], ["Dihedrals", "dihedrals"], ["Impropers", "impropers"], ["Velocities", "atoms"], ["Pair Coeffs", "atom types"], ["Bond Coeffs", "bond types"], ["Angle Coeffs", "angle types"], ["Dihedral Coeffs", "dihedral types"], ["Improper Coeffs", "improper types"], ["BondBond Coeffs", "angle types"], ["BondAngle Coeffs", "angle types"], ["MiddleBondTorsion Coeffs", "dihedral types"], ["EndBondTorsion Coeffs", "dihedral types"], ["AngleTorsion Coeffs", "dihedral types"], ["AngleAngleTorsion Coeffs", "dihedral types"], ["BondBond13 Coeffs", "dihedral types"], ["AngleAngle Coeffs", "improper types"], ["Molecules", "atoms"], ["Tinker Types", "atoms"], ] def __init__(self, *args: Any): """ Initialize a data object. Parameters: *args: Variable length argument list. - No arguments: Creates an empty data object. - One argument (filename or dump object): Initializes from a file or dump object. - Two arguments (dump object, timestep): Initializes from a dump object at a specific timestep. """ self.nselect = 1 self.names: Dict[str, int] = {} self.headers: Dict[str, Union[int, Tuple[float, float], Tuple[float, float, float]]] = {} self.sections: Dict[str, List[str]] = {} self.flist: List[str] = [] self.restart: bool = False if not args: # Default Constructor (empty object) self.title = "LAMMPS data file" logger.debug("Initialized empty data object.") return first_arg = args[0] if isinstance(first_arg, dump): # Constructor from an existing dump object self._init_from_dump(first_arg, *args[1:]) elif isinstance(first_arg, str): # Constructor from a DATA file self._init_from_file(*args) else: raise TypeError("Invalid argument type for data constructor.") def _init_from_dump(self, dump_obj: dump, timestep: Optional[int] = None) -> None: """ Initialize the data object from a dump object. Parameters: dump_obj (dump): The dump object to initialize from. timestep (Optional[int]): The specific timestep to use. If None, the last timestep is used. """ times = dump_obj.time() num_timesteps = len(times) if timestep is not None: if timestep not in times: raise ValueError("The input timestep is not available in the dump object.") selected_time = timestep else: selected_time = times[-1] try: index = times.index(selected_time) except ValueError: raise ValueError("Selected timestep not found in dump object.") self.title = (f'LAMMPS data file (restart from "{dump_obj.flist[0]}" ' f't = {selected_time:.5g} (frame {index + 1} of {num_timesteps}))') logger.debug(f"Set title: {self.title}") # Set headers snap = dump_obj.snaps[index] self.headers = { 'atoms': snap.natoms, 'atom types': dump_obj.minmax("type")[1], 'xlo xhi': (snap.xlo, snap.xhi), 'ylo yhi': (snap.ylo, snap.yhi), 'zlo zhi': (snap.zlo, snap.zhi) } logger.debug(f"Set headers: {self.headers}") # Initialize sections self.sections = {} template_atoms = { "smd": ["id", "type", "mol", "c_vol", "mass", "radius", "c_contact_radius", "x", "y", "z", "f_1[1]", "f_1[2]", "f_1[3]"] } if dump_obj.kind(template_atoms["smd"]): for col in template_atoms["smd"]: vector = dump_obj.vecs(selected_time, col) is_id_type_mol = col in ["id", "type", "mol"] self.append("Atoms", vector, force_integer=is_id_type_mol, property_name=col) else: raise ValueError("Please add your ATOMS section in the constructor.") # Set velocities if required template_velocities = {"smd": ["id", "vx", "vy", "vz"]} if dump_obj.kind(template_atoms["smd"]): if dump_obj.kind(template_velocities["smd"]): for col in template_velocities["smd"]: vector = dump_obj.vecs(selected_time, col) is_id = col == "id" self.append("Velocities", vector, force_integer=is_id, property_name=col) else: raise ValueError("The velocities are missing for the style SMD.") # Store filename self.flist = dump_obj.flist.copy() self.restart = True logger.debug("Initialized data object from dump.") def _init_from_file(self, filename: str) -> None: """ Initialize the data object from a LAMMPS data file. Parameters: filename (str): Path to the LAMMPS data file. """ flist = [filename] is_gzipped = filename.endswith(".gz") try: if is_gzipped: with subprocess.Popen([PIZZA_GUNZIP, "-c", filename], stdout=subprocess.PIPE, text=True) as proc: file_handle = proc.stdout logger.debug(f"Opened gzipped file: {filename}") else: file_handle = open(filename, 'r') logger.debug(f"Opened file: {filename}") with file_handle: self.title = file_handle.readline().strip() logger.debug(f"Read title: {self.title}") # Read headers while True: line = file_handle.readline() if not line: break line = line.strip() if not line: continue found = False for keyword in self.HKEYWORDS: if keyword in line: found = True words = line.split() if keyword in ["xlo xhi", "ylo yhi", "zlo zhi"]: self.headers[keyword] = (float(words[0]), float(words[1])) elif keyword == "xy xz yz": self.headers[keyword] = (float(words[0]), float(words[1]), float(words[2])) else: self.headers[keyword] = int(words[0]) logger.debug(f"Set header '{keyword}': {self.headers[keyword]}") break if not found: break # Reached the end of headers # Read sections while line: found_section = False for pair in self.SKEYWORDS: keyword, length_key = pair if keyword == line: found_section = True if length_key not in self.headers: raise ValueError(f"Data section '{keyword}' has no matching header value.") count = self.headers[length_key] file_handle.readline() # Read the blank line after section keyword section_lines = [file_handle.readline() for _ in range(count)] self.sections[keyword] = section_lines logger.debug(f"Read section '{keyword}' with {count} entries.") break if not found_section: raise ValueError(f"Invalid section '{line}' in data file.") # Read next section keyword line = file_handle.readline() if line: line = line.strip() self.flist = flist self.restart = False logger.info(f"Initialized data object from file '{filename}'.") except subprocess.CalledProcessError as e: logger.error(f"Error decompressing file '{filename}': {e}") raise except FileNotFoundError: logger.error(f"File '{filename}' not found.") raise except Exception as e: logger.error(f"Error reading file '{filename}': {e}") raise def __repr__(self) -> str: """ Return a string representation of the data object. Returns: str: Description of the data object. """ if not self.sections or not self.headers: ret = f"empty {self.title}" logger.info(ret) return ret kind = "restart" if self.restart else "source" header_info = (f"Data file: {self.flist[0]}\n" f"\tcontains {self.headers.get('atoms', 0)} atoms from {self.headers.get('atom types', 0)} atom types\n" f"\twith box = [{self.headers.get('xlo xhi', (0, 0))[0]} " f"{self.headers.get('xlo xhi', (0, 0))[1]} " f"{self.headers.get('ylo yhi', (0, 0))[0]} " f"{self.headers.get('ylo yhi', (0, 0))[1]} " f"{self.headers.get('zlo zhi', (0, 0))[0]} " f"{self.headers.get('zlo zhi', (0, 0))[1]}]") logger.info(header_info) section_info = "\twith the following sections:" logger.info(section_info) for section_name in self.sections.keys(): section_details = f"\t\t{self.dispsection(section_name, False)}" logger.info(section_details) ret = (f'LAMMPS data object including {self.headers.get("atoms", 0)} atoms ' f'({self.maxtype()} types, {kind}="{self.flist[0]}")') return ret def map(self, *pairs: Any) -> None: """ Assign names to atom columns. Parameters: *pairs (Any): Pairs of column indices and names. Raises: ValueError: If an odd number of arguments is provided. """ if len(pairs) % 2 != 0: raise ValueError("data.map() requires pairs of mappings.") for i in range(0, len(pairs), 2): column_index = pairs[i] - 1 name = pairs[i + 1] self.names[name] = column_index logger.debug(f"Mapped column '{name}' to index {column_index + 1}.") def get(self, *args: Any) -> Union[List[List[float]], List[float]]: """ Extract information from data file fields. Parameters: *args: Variable length argument list. - One argument: Returns all columns as a 2D list of floats. - Two arguments: Returns the specified column as a list of floats. Returns: Union[List[List[float]], List[float]]: Extracted data. Raises: ValueError: If invalid number of arguments is provided. KeyError: If the specified field is not found. """ if len(args) == 1: field = args[0] array = [] lines = self.sections.get(field, []) for line in lines: words = line.split() values = [float(word) for word in words] array.append(values) logger.debug(f"Extracted all columns from field '{field}'.") return array elif len(args) == 2: field, column = args column_index = column - 1 vec = [] lines = self.sections.get(field, []) for line in lines: words = line.split() vec.append(float(words[column_index])) logger.debug(f"Extracted column {column} from field '{field}'.") return vec else: raise ValueError("Invalid arguments for data.get().") def reorder(self, section: str, *order: int) -> None: """ Reorder columns in a data file section. Parameters: section (str): The name of the section to reorder. *order (int): The new order of column indices. Raises: ValueError: If the section name is invalid. """ if section not in self.sections: raise ValueError(f'"{section}" is not a valid section name.') num_columns = len(order) logger.info(f">> Reordering {num_columns} columns in section '{section}'.") old_lines = self.sections[section] new_lines = [] for line in old_lines: words = line.split() try: reordered = " ".join(words[i - 1] for i in order) + "\n" except IndexError: raise ValueError("Column index out of range during reorder.") new_lines.append(reordered) self.sections[section] = new_lines logger.debug(f"Reordered columns in section '{section}'.") def replace(self, section: str, column: int, vector: Union[List[float], float]) -> None: """ Replace a column in a named section with a vector of values. Parameters: section (str): The name of the section. column (int): The column index to replace (1-based). vector (Union[List[float], float]): The new values or a single scalar value. Raises: ValueError: If the section is invalid or vector length mismatch. """ if section not in self.sections: raise ValueError(f'"{section}" is not a valid section name.') lines = self.sections[section] num_lines = len(lines) if not isinstance(vector, list): vector = [vector] if len(vector) == 1: vector = vector * num_lines if len(vector) != num_lines: raise ValueError(f'The length of new data ({len(vector)}) in section "{section}" does not match the number of rows {num_lines}.') new_lines = [] column_index = column - 1 for i, line in enumerate(lines): words = line.split() if column_index >= len(words): raise ValueError(f"Column index {column} out of range for section '{section}'.") words[column_index] = str(vector[i]) new_line = " ".join(words) + "\n" new_lines.append(new_line) self.sections[section] = new_lines logger.debug(f"Replaced column {column} in section '{section}' with new data.") def append(self, section: str, vector: Union[List[float], np.ndarray, float], force_integer: bool = False, property_name: Optional[str] = None) -> None: """ Append a new column to a named section. Parameters: section (str): The name of the section. vector (Union[List[float], np.ndarray, float]): The values to append. force_integer (bool): If True, values are converted to integers. property_name (Optional[str]): The name of the property being appended. Raises: ValueError: If vector length mismatch occurs. """ if section not in self.sections: self.sections[section] = [] logger.info(f'Added new section [{section}] - file="{self.title}".') lines = self.sections[section] num_lines = len(lines) if not isinstance(vector, (list, np.ndarray)): vector = [vector] if property_name: logger.info(f'\t> Adding property "{property_name}" with {len(vector)} values to [{section}].') else: logger.info(f'\t> Adding {len(vector)} values to [{section}] (no name).') new_lines = [] if num_lines == 0: # Empty section, create initial lines num_entries = len(vector) for i in range(num_entries): value = int(vector[i]) if force_integer else vector[i] new_line = f"{int(value) if force_integer else value}\n" new_lines.append(new_line) logger.debug(f"Initialized empty section '{section}' with new column.") else: if len(vector) == 1: vector = vector * num_lines if len(vector) != num_lines: raise ValueError(f'The length of new data ({len(vector)}) in section "{section}" does not match the number of rows {num_lines}.') for i, line in enumerate(lines): value = int(vector[i]) if force_integer else vector[i] new_word = str(value) new_line = line.rstrip('\n') + f" {new_word}\n" new_lines.append(new_line) self.sections[section] = new_lines logger.debug(f"Appended new column to section '{section}'.") def dispsection(self, section: str, include_header: bool = True) -> str: """ Display information about a section. Parameters: section (str): The name of the section. include_header (bool): Whether to include "LAMMPS data section" in the output. Returns: str: Description of the section. """ if section not in self.sections: raise ValueError(f"Section '{section}' not found in data object.") lines = self.sections[section] num_lines = len(lines) num_columns = len(lines[0].split()) if lines else 0 ret = f'"{section}": {num_lines} x {num_columns} values' if include_header: ret = f"LAMMPS data section {ret}" return ret def newxyz(self, dm: dump, ntime: int) -> None: """ Replace x, y, z coordinates in the Atoms section with those from a dump object. Parameters: dm (dump): The dump object containing new coordinates. ntime (int): The timestep to extract coordinates from. Raises: ValueError: If required columns are not defined. """ nsnap = dm.findtime(ntime) logger.info(f">> Replacing XYZ for {nsnap} snapshots.") dm.sort(ntime) x, y, z = dm.vecs(ntime, "x", "y", "z") self.replace("Atoms", self.names.get("x", 0) + 1, x) self.replace("Atoms", self.names.get("y", 0) + 1, y) self.replace("Atoms", self.names.get("z", 0) + 1, z) if "ix" in dm.names and "ix" in self.names: ix, iy, iz = dm.vecs(ntime, "ix", "iy", "iz") self.replace("Atoms", self.names.get("ix", 0) + 1, ix) self.replace("Atoms", self.names.get("iy", 0) + 1, iy) self.replace("Atoms", self.names.get("iz", 0) + 1, iz) logger.debug(f"Replaced XYZ coordinates at timestep {ntime}.") def delete(self, keyword: str) -> None: """ Delete a header value or section from the data object. Parameters: keyword (str): The header or section name to delete. Raises: ValueError: If the keyword is not found. """ if keyword in self.headers: del self.headers[keyword] logger.debug(f"Deleted header '{keyword}'.") elif keyword in self.sections: del self.sections[keyword] logger.debug(f"Deleted section '{keyword}'.") else: raise ValueError("Keyword not found in data object.") def write(self, filename: str) -> None: """ Write the data object to a LAMMPS data file. Parameters: filename (str): The output file path. """ try: with open(filename, "w") as f: f.write(f"{self.title}\n") logger.debug(f"Wrote title to file '{filename}'.") # Write headers for keyword in self.HKEYWORDS: if keyword in self.headers: value = self.headers[keyword] if keyword in ["xlo xhi", "ylo yhi", "zlo zhi"]: f.write(f"{value[0]} {value[1]} {keyword}\n") elif keyword == "xy xz yz": f.write(f"{value[0]} {value[1]} {value[2]} {keyword}\n") else: f.write(f"{value} {keyword}\n") logger.debug(f"Wrote header '{keyword}' to file.") # Write sections for pair in self.SKEYWORDS: keyword = pair[0] if keyword in self.sections: f.write(f"\n{keyword}\n\n") for line in self.sections[keyword]: f.write(line) logger.debug(f"Wrote section '{keyword}' to file.") logger.info(f"Data object written to '{filename}'.") except IOError as e: logger.error(f"Error writing to file '{filename}': {e}") raise def iterator(self, flag: int) -> Tuple[int, int, int]: """ Iterator method compatible with other tools. Parameters: flag (int): 0 for the first call, 1 for subsequent calls. Returns: Tuple[int, int, int]: (index, time, flag) """ if flag == 0: return 0, 0, 1 return 0, 0, -1 def findtime(self, n: int) -> int: """ Find the index of a given timestep. Parameters: n (int): The timestep to find. Returns: int: The index of the timestep. Raises: ValueError: If the timestep does not exist. """ if n == 0: return 0 raise ValueError(f"No step {n} exists.") def viz(self, isnap: int) -> Tuple[int, List[float], List[List[Union[int, float]]], List[List[Union[int, float]]], List[Any], List[Any]]: """ Return visualization data for a specified snapshot. Parameters: isnap (int): Snapshot index (must be 0 for data object). Returns: Tuple containing time, box dimensions, atoms, bonds, tris, and lines. Raises: ValueError: If isnap is not 0. """ if isnap: raise ValueError("Cannot call data.viz() with isnap != 0.") id_idx = self.names.get("id") type_idx = self.names.get("type") x_idx = self.names.get("x") y_idx = self.names.get("y") z_idx = self.names.get("z") if None in [id_idx, type_idx, x_idx, y_idx, z_idx]: raise ValueError("One or more required columns (id, type, x, y, z) are not defined.") xlohi = self.headers.get("xlo xhi", (0.0, 0.0)) ylohi = self.headers.get("ylo yhi", (0.0, 0.0)) zlohi = self.headers.get("zlo zhi", (0.0, 0.0)) box = [xlohi[0], ylohi[0], zlohi[0], xlohi[1], ylohi[1], zlohi[1]] # Create atom list needed by viz from id, type, x, y, z atoms = [] atom_lines = self.sections.get("Atoms", []) for line in atom_lines: words = line.split() atoms.append([ int(words[id_idx]), int(words[type_idx]), float(words[x_idx]), float(words[y_idx]), float(words[z_idx]), ]) # Create list of current bond coords from list of bonds bonds = [] if "Bonds" in self.sections: bond_lines = self.sections["Bonds"] for line in bond_lines: words = line.split() bid = int(words[0]) btype = int(words[1]) atom1 = int(words[2]) atom2 = int(words[3]) if atom1 - 1 >= len(atom_lines) or atom2 - 1 >= len(atom_lines): raise ValueError("Atom index in Bonds section out of range.") atom1_words = self.sections["Atoms"][atom1 - 1].split() atom2_words = self.sections["Atoms"][atom2 - 1].split() bonds.append([ bid, btype, float(atom1_words[x_idx]), float(atom1_words[y_idx]), float(atom1_words[z_idx]), float(atom2_words[x_idx]), float(atom2_words[y_idx]), float(atom2_words[z_idx]), int(atom1_words[type_idx]), int(atom2_words[type_idx]), ]) tris = [] lines = [] logger.debug("Prepared visualization data.") return 0, box, atoms, bonds, tris, lines def maxbox(self) -> List[float]: """ Return the box dimensions. Returns: List[float]: [xlo, ylo, zlo, xhi, yhi, zhi] """ xlohi = self.headers.get("xlo xhi", (0.0, 0.0)) ylohi = self.headers.get("ylo yhi", (0.0, 0.0)) zlohi = self.headers.get("zlo zhi", (0.0, 0.0)) box = [xlohi[0], ylohi[0], zlohi[0], xlohi[1], ylohi[1], zlohi[1]] logger.debug(f"Box dimensions: {box}") return box def maxtype(self) -> int: """ Return the number of atom types. Returns: int: Number of atom types. """ maxtype = self.headers.get("atom types", 0) logger.debug(f"Number of atom types: {maxtype}") return maxtype
Class variables
var HKEYWORDS
var SKEYWORDS
Methods
def append(self, section: str, vector: Union[List[float], numpy.ndarray, float], force_integer: bool = False, property_name: Optional[str] = None) ‑> NoneType
-
Append a new column to a named section.
Parameters
section (str): The name of the section. vector (Union[List[float], np.ndarray, float]): The values to append. force_integer (bool): If True, values are converted to integers. property_name (Optional[str]): The name of the property being appended.
Raises
ValueError
- If vector length mismatch occurs.
Expand source code
def append(self, section: str, vector: Union[List[float], np.ndarray, float], force_integer: bool = False, property_name: Optional[str] = None) -> None: """ Append a new column to a named section. Parameters: section (str): The name of the section. vector (Union[List[float], np.ndarray, float]): The values to append. force_integer (bool): If True, values are converted to integers. property_name (Optional[str]): The name of the property being appended. Raises: ValueError: If vector length mismatch occurs. """ if section not in self.sections: self.sections[section] = [] logger.info(f'Added new section [{section}] - file="{self.title}".') lines = self.sections[section] num_lines = len(lines) if not isinstance(vector, (list, np.ndarray)): vector = [vector] if property_name: logger.info(f'\t> Adding property "{property_name}" with {len(vector)} values to [{section}].') else: logger.info(f'\t> Adding {len(vector)} values to [{section}] (no name).') new_lines = [] if num_lines == 0: # Empty section, create initial lines num_entries = len(vector) for i in range(num_entries): value = int(vector[i]) if force_integer else vector[i] new_line = f"{int(value) if force_integer else value}\n" new_lines.append(new_line) logger.debug(f"Initialized empty section '{section}' with new column.") else: if len(vector) == 1: vector = vector * num_lines if len(vector) != num_lines: raise ValueError(f'The length of new data ({len(vector)}) in section "{section}" does not match the number of rows {num_lines}.') for i, line in enumerate(lines): value = int(vector[i]) if force_integer else vector[i] new_word = str(value) new_line = line.rstrip('\n') + f" {new_word}\n" new_lines.append(new_line) self.sections[section] = new_lines logger.debug(f"Appended new column to section '{section}'.")
def delete(self, keyword: str) ‑> NoneType
-
Delete a header value or section from the data object.
Parameters
keyword (str): The header or section name to delete.
Raises
ValueError
- If the keyword is not found.
Expand source code
def delete(self, keyword: str) -> None: """ Delete a header value or section from the data object. Parameters: keyword (str): The header or section name to delete. Raises: ValueError: If the keyword is not found. """ if keyword in self.headers: del self.headers[keyword] logger.debug(f"Deleted header '{keyword}'.") elif keyword in self.sections: del self.sections[keyword] logger.debug(f"Deleted section '{keyword}'.") else: raise ValueError("Keyword not found in data object.")
def dispsection(self, section: str, include_header: bool = True) ‑> str
-
Display information about a section.
Parameters
section (str): The name of the section. include_header (bool): Whether to include "LAMMPS data section" in the output.
Returns
str
- Description of the section.
Expand source code
def dispsection(self, section: str, include_header: bool = True) -> str: """ Display information about a section. Parameters: section (str): The name of the section. include_header (bool): Whether to include "LAMMPS data section" in the output. Returns: str: Description of the section. """ if section not in self.sections: raise ValueError(f"Section '{section}' not found in data object.") lines = self.sections[section] num_lines = len(lines) num_columns = len(lines[0].split()) if lines else 0 ret = f'"{section}": {num_lines} x {num_columns} values' if include_header: ret = f"LAMMPS data section {ret}" return ret
def findtime(self, n: int) ‑> int
-
Find the index of a given timestep.
Parameters
n (int): The timestep to find.
Returns
int
- The index of the timestep.
Raises
ValueError
- If the timestep does not exist.
Expand source code
def findtime(self, n: int) -> int: """ Find the index of a given timestep. Parameters: n (int): The timestep to find. Returns: int: The index of the timestep. Raises: ValueError: If the timestep does not exist. """ if n == 0: return 0 raise ValueError(f"No step {n} exists.")
def get(self, *args: Any) ‑> Union[List[List[float]], List[float]]
-
Extract information from data file fields.
Parameters
*args: Variable length argument list. - One argument: Returns all columns as a 2D list of floats. - Two arguments: Returns the specified column as a list of floats.
Returns
Union[List[List[float]], List[float]]
- Extracted data.
Raises
ValueError
- If invalid number of arguments is provided.
KeyError
- If the specified field is not found.
Expand source code
def get(self, *args: Any) -> Union[List[List[float]], List[float]]: """ Extract information from data file fields. Parameters: *args: Variable length argument list. - One argument: Returns all columns as a 2D list of floats. - Two arguments: Returns the specified column as a list of floats. Returns: Union[List[List[float]], List[float]]: Extracted data. Raises: ValueError: If invalid number of arguments is provided. KeyError: If the specified field is not found. """ if len(args) == 1: field = args[0] array = [] lines = self.sections.get(field, []) for line in lines: words = line.split() values = [float(word) for word in words] array.append(values) logger.debug(f"Extracted all columns from field '{field}'.") return array elif len(args) == 2: field, column = args column_index = column - 1 vec = [] lines = self.sections.get(field, []) for line in lines: words = line.split() vec.append(float(words[column_index])) logger.debug(f"Extracted column {column} from field '{field}'.") return vec else: raise ValueError("Invalid arguments for data.get().")
def iterator(self, flag: int) ‑> Tuple[int, int, int]
-
Iterator method compatible with other tools.
Parameters
flag (int): 0 for the first call, 1 for subsequent calls.
Returns
Tuple[int, int, int]
- (index, time, flag)
Expand source code
def iterator(self, flag: int) -> Tuple[int, int, int]: """ Iterator method compatible with other tools. Parameters: flag (int): 0 for the first call, 1 for subsequent calls. Returns: Tuple[int, int, int]: (index, time, flag) """ if flag == 0: return 0, 0, 1 return 0, 0, -1
def map(self, *pairs: Any) ‑> NoneType
-
Assign names to atom columns.
Parameters
*pairs (Any): Pairs of column indices and names.
Raises
ValueError
- If an odd number of arguments is provided.
Expand source code
def map(self, *pairs: Any) -> None: """ Assign names to atom columns. Parameters: *pairs (Any): Pairs of column indices and names. Raises: ValueError: If an odd number of arguments is provided. """ if len(pairs) % 2 != 0: raise ValueError("data.map() requires pairs of mappings.") for i in range(0, len(pairs), 2): column_index = pairs[i] - 1 name = pairs[i + 1] self.names[name] = column_index logger.debug(f"Mapped column '{name}' to index {column_index + 1}.")
def maxbox(self) ‑> List[float]
-
Return the box dimensions.
Returns
List[float]
- [xlo, ylo, zlo, xhi, yhi, zhi]
Expand source code
def maxbox(self) -> List[float]: """ Return the box dimensions. Returns: List[float]: [xlo, ylo, zlo, xhi, yhi, zhi] """ xlohi = self.headers.get("xlo xhi", (0.0, 0.0)) ylohi = self.headers.get("ylo yhi", (0.0, 0.0)) zlohi = self.headers.get("zlo zhi", (0.0, 0.0)) box = [xlohi[0], ylohi[0], zlohi[0], xlohi[1], ylohi[1], zlohi[1]] logger.debug(f"Box dimensions: {box}") return box
def maxtype(self) ‑> int
-
Return the number of atom types.
Returns
int
- Number of atom types.
Expand source code
def maxtype(self) -> int: """ Return the number of atom types. Returns: int: Number of atom types. """ maxtype = self.headers.get("atom types", 0) logger.debug(f"Number of atom types: {maxtype}") return maxtype
def newxyz(self, dm: pizza.dump3.dump, ntime: int) ‑> NoneType
-
Replace x, y, z coordinates in the Atoms section with those from a dump object.
Parameters
dm (dump): The dump object containing new coordinates. ntime (int): The timestep to extract coordinates from.
Raises
ValueError
- If required columns are not defined.
Expand source code
def newxyz(self, dm: dump, ntime: int) -> None: """ Replace x, y, z coordinates in the Atoms section with those from a dump object. Parameters: dm (dump): The dump object containing new coordinates. ntime (int): The timestep to extract coordinates from. Raises: ValueError: If required columns are not defined. """ nsnap = dm.findtime(ntime) logger.info(f">> Replacing XYZ for {nsnap} snapshots.") dm.sort(ntime) x, y, z = dm.vecs(ntime, "x", "y", "z") self.replace("Atoms", self.names.get("x", 0) + 1, x) self.replace("Atoms", self.names.get("y", 0) + 1, y) self.replace("Atoms", self.names.get("z", 0) + 1, z) if "ix" in dm.names and "ix" in self.names: ix, iy, iz = dm.vecs(ntime, "ix", "iy", "iz") self.replace("Atoms", self.names.get("ix", 0) + 1, ix) self.replace("Atoms", self.names.get("iy", 0) + 1, iy) self.replace("Atoms", self.names.get("iz", 0) + 1, iz) logger.debug(f"Replaced XYZ coordinates at timestep {ntime}.")
def reorder(self, section: str, *order: int) ‑> NoneType
-
Reorder columns in a data file section.
Parameters
section (str): The name of the section to reorder. *order (int): The new order of column indices.
Raises
ValueError
- If the section name is invalid.
Expand source code
def reorder(self, section: str, *order: int) -> None: """ Reorder columns in a data file section. Parameters: section (str): The name of the section to reorder. *order (int): The new order of column indices. Raises: ValueError: If the section name is invalid. """ if section not in self.sections: raise ValueError(f'"{section}" is not a valid section name.') num_columns = len(order) logger.info(f">> Reordering {num_columns} columns in section '{section}'.") old_lines = self.sections[section] new_lines = [] for line in old_lines: words = line.split() try: reordered = " ".join(words[i - 1] for i in order) + "\n" except IndexError: raise ValueError("Column index out of range during reorder.") new_lines.append(reordered) self.sections[section] = new_lines logger.debug(f"Reordered columns in section '{section}'.")
def replace(self, section: str, column: int, vector: Union[List[float], float]) ‑> NoneType
-
Replace a column in a named section with a vector of values.
Parameters
section (str): The name of the section. column (int): The column index to replace (1-based). vector (Union[List[float], float]): The new values or a single scalar value.
Raises
ValueError
- If the section is invalid or vector length mismatch.
Expand source code
def replace(self, section: str, column: int, vector: Union[List[float], float]) -> None: """ Replace a column in a named section with a vector of values. Parameters: section (str): The name of the section. column (int): The column index to replace (1-based). vector (Union[List[float], float]): The new values or a single scalar value. Raises: ValueError: If the section is invalid or vector length mismatch. """ if section not in self.sections: raise ValueError(f'"{section}" is not a valid section name.') lines = self.sections[section] num_lines = len(lines) if not isinstance(vector, list): vector = [vector] if len(vector) == 1: vector = vector * num_lines if len(vector) != num_lines: raise ValueError(f'The length of new data ({len(vector)}) in section "{section}" does not match the number of rows {num_lines}.') new_lines = [] column_index = column - 1 for i, line in enumerate(lines): words = line.split() if column_index >= len(words): raise ValueError(f"Column index {column} out of range for section '{section}'.") words[column_index] = str(vector[i]) new_line = " ".join(words) + "\n" new_lines.append(new_line) self.sections[section] = new_lines logger.debug(f"Replaced column {column} in section '{section}' with new data.")
def viz(self, isnap: int) ‑> Tuple[int, List[float], List[List[Union[int, float]]], List[List[Union[int, float]]], List[Any], List[Any]]
-
Return visualization data for a specified snapshot.
Parameters
isnap (int): Snapshot index (must be 0 for data object).
Returns
Tuple containing time, box dimensions, atoms, bonds, tris, and lines.
Raises
ValueError
- If isnap is not 0.
Expand source code
def viz(self, isnap: int) -> Tuple[int, List[float], List[List[Union[int, float]]], List[List[Union[int, float]]], List[Any], List[Any]]: """ Return visualization data for a specified snapshot. Parameters: isnap (int): Snapshot index (must be 0 for data object). Returns: Tuple containing time, box dimensions, atoms, bonds, tris, and lines. Raises: ValueError: If isnap is not 0. """ if isnap: raise ValueError("Cannot call data.viz() with isnap != 0.") id_idx = self.names.get("id") type_idx = self.names.get("type") x_idx = self.names.get("x") y_idx = self.names.get("y") z_idx = self.names.get("z") if None in [id_idx, type_idx, x_idx, y_idx, z_idx]: raise ValueError("One or more required columns (id, type, x, y, z) are not defined.") xlohi = self.headers.get("xlo xhi", (0.0, 0.0)) ylohi = self.headers.get("ylo yhi", (0.0, 0.0)) zlohi = self.headers.get("zlo zhi", (0.0, 0.0)) box = [xlohi[0], ylohi[0], zlohi[0], xlohi[1], ylohi[1], zlohi[1]] # Create atom list needed by viz from id, type, x, y, z atoms = [] atom_lines = self.sections.get("Atoms", []) for line in atom_lines: words = line.split() atoms.append([ int(words[id_idx]), int(words[type_idx]), float(words[x_idx]), float(words[y_idx]), float(words[z_idx]), ]) # Create list of current bond coords from list of bonds bonds = [] if "Bonds" in self.sections: bond_lines = self.sections["Bonds"] for line in bond_lines: words = line.split() bid = int(words[0]) btype = int(words[1]) atom1 = int(words[2]) atom2 = int(words[3]) if atom1 - 1 >= len(atom_lines) or atom2 - 1 >= len(atom_lines): raise ValueError("Atom index in Bonds section out of range.") atom1_words = self.sections["Atoms"][atom1 - 1].split() atom2_words = self.sections["Atoms"][atom2 - 1].split() bonds.append([ bid, btype, float(atom1_words[x_idx]), float(atom1_words[y_idx]), float(atom1_words[z_idx]), float(atom2_words[x_idx]), float(atom2_words[y_idx]), float(atom2_words[z_idx]), int(atom1_words[type_idx]), int(atom2_words[type_idx]), ]) tris = [] lines = [] logger.debug("Prepared visualization data.") return 0, box, atoms, bonds, tris, lines
def write(self, filename: str) ‑> NoneType
-
Write the data object to a LAMMPS data file.
Parameters
filename (str): The output file path.
Expand source code
def write(self, filename: str) -> None: """ Write the data object to a LAMMPS data file. Parameters: filename (str): The output file path. """ try: with open(filename, "w") as f: f.write(f"{self.title}\n") logger.debug(f"Wrote title to file '{filename}'.") # Write headers for keyword in self.HKEYWORDS: if keyword in self.headers: value = self.headers[keyword] if keyword in ["xlo xhi", "ylo yhi", "zlo zhi"]: f.write(f"{value[0]} {value[1]} {keyword}\n") elif keyword == "xy xz yz": f.write(f"{value[0]} {value[1]} {value[2]} {keyword}\n") else: f.write(f"{value} {keyword}\n") logger.debug(f"Wrote header '{keyword}' to file.") # Write sections for pair in self.SKEYWORDS: keyword = pair[0] if keyword in self.sections: f.write(f"\n{keyword}\n\n") for line in self.sections[keyword]: f.write(line) logger.debug(f"Wrote section '{keyword}' to file.") logger.info(f"Data object written to '{filename}'.") except IOError as e: logger.error(f"Error writing to file '{filename}': {e}") raise
class emulsion (xmin=10, ymin=10, xmax=90, ymax=90, maxtrials=1000, beadtype=1, forcedinsertion=True)
-
emulsion generator
Parameters
- The insertions are performed between xmin,ymin and xmax,ymax
xmin
:int64
orreal
, optional- x left corner. The default is 10.
ymin
:int64
orreal
, optional- y bottom corner. The default is 10.
xmax
:int64
orreal
, optional- x right corner. The default is 90.
ymax
:int64
orreal
, optional- y top corner. The default is 90.
beadtype
:default beadtype to apply if not precised at insertion
maxtrials
:integer
, optional- Maximum of attempts for an object. The default is 1000.
forcedinsertion
:logical
, optional- Set it to true to force the next insertion. The default is True.
Returns
None.
Expand source code
class emulsion(scatter): """ emulsion generator """ def __init__(self, xmin=10, ymin=10, xmax=90, ymax=90, maxtrials=1000, beadtype=1, forcedinsertion=True): """ Parameters ---------- The insertions are performed between xmin,ymin and xmax,ymax xmin : int64 or real, optional x left corner. The default is 10. ymin : int64 or real, optional y bottom corner. The default is 10. xmax : int64 or real, optional x right corner. The default is 90. ymax : int64 or real, optional y top corner. The default is 90. beadtype : default beadtype to apply if not precised at insertion maxtrials : integer, optional Maximum of attempts for an object. The default is 1000. forcedinsertion : logical, optional Set it to true to force the next insertion. The default is True. Returns ------- None. """ super().__init__() self.xmin, self.xmax, self.ymin, self.ymax = xmin, xmax, ymin, ymax self.lastinsertion = (None,None,None,None) # x,y,r, beadtype self.width = xmax-xmin self.height = ymax-ymin self.defautbeadtype = beadtype self.maxtrials = maxtrials self.forcedinsertion = forcedinsertion def __repr__(self): print(f" Emulsion object\n\t{self.width}x{self.height} starting at x={self.xmin}, y={self.ymin}") print(f"\tcontains {self.n} insertions") print("\tmaximum insertion trials:", self.maxtrials) print("\tforce next insertion if previous fails:", self.forcedinsertion) return f"emulsion with {self.n} insertions" def walldist(self,x,y): """ shortest distance to the wall """ return min(abs(x-self.xmin),abs(y-self.ymin),abs(x-self.xmax),abs(y-self.ymax)) def dist(self,x,y): """ shortest distance of the center (x,y) to the wall or any object""" return np.minimum(np.min(self.pairdist(x,y)),self.walldist(x,y)) def accepted(self,x,y,r): """ acceptation criterion """ return self.dist(x,y)>r def rand(self): """ random position x,y """ return np.round(np.random.uniform(low=self.xmin,high=self.xmax)), \ np.round(np.random.uniform(low=self.ymin,high=self.ymax)) def setbeadtype(self,beadtype): """ set the default or the supplied beadtype """ if beadtype == None: self.beadtype.append(self.defautbeadtype) return self.defautbeadtype else: self.beadtype.append(beadtype) return beadtype def insertone(self,x=None,y=None,r=None,beadtype=None,overlap=False): """ insert one object of radius r properties: x,y coordinates (if missing, picked randomly from uniform distribution) r radius (default = 2% of diagonal) beadtype (default = defautbeadtype) overlap = False (accept only if no overlap) """ attempt, success = 0, False random = (x==None) or (y==None) if r==None: r = 0.02*np.sqrt(self.width**2+self.height**2) while not success and attempt<self.maxtrials: attempt += 1 if random: x,y = self.rand() if overlap: success = True else: success = self.accepted(x,y,r) if success: self.x = np.append(self.x,x) self.y = np.append(self.y,y) self.r = np.append(self.r,r) b=self.setbeadtype(beadtype) self.lastinsertion = (x,y,r,b) return success def insertion(self,rlist,beadtype=None): """ insert a list of objects nsuccess=insertion(rlist,beadtype=None) beadtype=b forces the value b if None, defaultbeadtype is used instead """ rlist.sort(reverse=True) ntodo = len(rlist) n = nsuccess = 0 stop = False while not stop: n += 1 success = self.insertone(r=rlist[n-1],beadtype=beadtype) if success: nsuccess += 1 stop = (n==ntodo) or (not success and not self.forcedinsertion) if nsuccess==ntodo: print(f"{nsuccess} objects inserted successfully") else: print(f"partial success: {nsuccess} of {ntodo} objects inserted") return nsuccess
Ancestors
Subclasses
Methods
def accepted(self, x, y, r)
-
acceptation criterion
Expand source code
def accepted(self,x,y,r): """ acceptation criterion """ return self.dist(x,y)>r
def dist(self, x, y)
-
shortest distance of the center (x,y) to the wall or any object
Expand source code
def dist(self,x,y): """ shortest distance of the center (x,y) to the wall or any object""" return np.minimum(np.min(self.pairdist(x,y)),self.walldist(x,y))
def insertion(self, rlist, beadtype=None)
-
insert a list of objects nsuccess=insertion(rlist,beadtype=None) beadtype=b forces the value b if None, defaultbeadtype is used instead
Expand source code
def insertion(self,rlist,beadtype=None): """ insert a list of objects nsuccess=insertion(rlist,beadtype=None) beadtype=b forces the value b if None, defaultbeadtype is used instead """ rlist.sort(reverse=True) ntodo = len(rlist) n = nsuccess = 0 stop = False while not stop: n += 1 success = self.insertone(r=rlist[n-1],beadtype=beadtype) if success: nsuccess += 1 stop = (n==ntodo) or (not success and not self.forcedinsertion) if nsuccess==ntodo: print(f"{nsuccess} objects inserted successfully") else: print(f"partial success: {nsuccess} of {ntodo} objects inserted") return nsuccess
def insertone(self, x=None, y=None, r=None, beadtype=None, overlap=False)
-
insert one object of radius r properties: x,y coordinates (if missing, picked randomly from uniform distribution) r radius (default = 2% of diagonal) beadtype (default = defautbeadtype) overlap = False (accept only if no overlap)
Expand source code
def insertone(self,x=None,y=None,r=None,beadtype=None,overlap=False): """ insert one object of radius r properties: x,y coordinates (if missing, picked randomly from uniform distribution) r radius (default = 2% of diagonal) beadtype (default = defautbeadtype) overlap = False (accept only if no overlap) """ attempt, success = 0, False random = (x==None) or (y==None) if r==None: r = 0.02*np.sqrt(self.width**2+self.height**2) while not success and attempt<self.maxtrials: attempt += 1 if random: x,y = self.rand() if overlap: success = True else: success = self.accepted(x,y,r) if success: self.x = np.append(self.x,x) self.y = np.append(self.y,y) self.r = np.append(self.r,r) b=self.setbeadtype(beadtype) self.lastinsertion = (x,y,r,b) return success
def rand(self)
-
random position x,y
Expand source code
def rand(self): """ random position x,y """ return np.round(np.random.uniform(low=self.xmin,high=self.xmax)), \ np.round(np.random.uniform(low=self.ymin,high=self.ymax))
def setbeadtype(self, beadtype)
-
set the default or the supplied beadtype
Expand source code
def setbeadtype(self,beadtype): """ set the default or the supplied beadtype """ if beadtype == None: self.beadtype.append(self.defautbeadtype) return self.defautbeadtype else: self.beadtype.append(beadtype) return beadtype
def walldist(self, x, y)
-
shortest distance to the wall
Expand source code
def walldist(self,x,y): """ shortest distance to the wall """ return min(abs(x-self.xmin),abs(y-self.ymin),abs(x-self.xmax),abs(y-self.ymax))
Inherited members
class genericpolygon
-
generic polygon methods
Expand source code
class genericpolygon(coregeometry): """ generic polygon methods """ hascontour = True hasclosefit = False @property def polygon(self): """ R.polygon = path.Path(R.vertices,R.codes,closed=True) """ v = self.vertices if self.translate != None: vtmp = list(map(list,zip(*v))) for i in range(len(vtmp[0])): vtmp[0][i] += self.translate[0] vtmp[1][i] += self.translate[1] v = list(zip(*vtmp)) return path.Path(v,self.codes,closed=True) @property def polygon2plot(self): """ R.polygon2plot = path.Path(R.polygon.vertices+ np.array([1,1]),R.codes,closed=True) """ return path.Path(self.polygon.vertices+ np.array([1,1]),self.codes,closed=True) def corners(self): """ returns xmin, ymin, xmax, ymax """ return min([self.vertices[k][0] for k in range(self.nvertices)])+self.translate[0], \ min([self.vertices[k][1] for k in range(self.nvertices)])+self.translate[1], \ max([self.vertices[k][0] for k in range(self.nvertices)])+self.translate[0], \ max([self.vertices[k][1] for k in range(self.nvertices)])+self.translate[1]
Ancestors
Subclasses
Class variables
var hasclosefit
var hascontour
Instance variables
var polygon
-
R.polygon = path.Path(R.vertices,R.codes,closed=True)
Expand source code
@property def polygon(self): """ R.polygon = path.Path(R.vertices,R.codes,closed=True) """ v = self.vertices if self.translate != None: vtmp = list(map(list,zip(*v))) for i in range(len(vtmp[0])): vtmp[0][i] += self.translate[0] vtmp[1][i] += self.translate[1] v = list(zip(*vtmp)) return path.Path(v,self.codes,closed=True)
var polygon2plot
-
R.polygon2plot = path.Path(R.polygon.vertices+ np.array([1,1]),R.codes,closed=True)
Expand source code
@property def polygon2plot(self): """ R.polygon2plot = path.Path(R.polygon.vertices+ np.array([1,1]),R.codes,closed=True) """ return path.Path(self.polygon.vertices+ np.array([1,1]),self.codes,closed=True)
Methods
def corners(self)
-
returns xmin, ymin, xmax, ymax
Expand source code
def corners(self): """ returns xmin, ymin, xmax, ymax """ return min([self.vertices[k][0] for k in range(self.nvertices)])+self.translate[0], \ min([self.vertices[k][1] for k in range(self.nvertices)])+self.translate[1], \ max([self.vertices[k][0] for k in range(self.nvertices)])+self.translate[0], \ max([self.vertices[k][1] for k in range(self.nvertices)])+self.translate[1]
Inherited members
class overlay (counter=(0, 0), filename='./sandbox/image.jpg', xmin=0, ymin=0, ncolors=4, flipud=True, angle=0, scale=(1, 1))
-
generic overlay class
generate an overlay from file overlay(counter=(c1,c2),filename="this/is/myimage.jpg",xmin=x0,ymin=y0,colors=4) additional options overlay(…,flipud=True,angle=0,scale=(1,1))
Expand source code
class overlay(coregeometry): """ generic overlay class """ hascontour = False hasclosefit = True def __init__(self, counter = (0,0), filename="./sandbox/image.jpg", xmin = 0, ymin = 0, ncolors = 4, flipud = True, angle = 0, scale = (1,1) ): """ generate an overlay from file overlay(counter=(c1,c2),filename="this/is/myimage.jpg",xmin=x0,ymin=y0,colors=4) additional options overlay(...,flipud=True,angle=0,scale=(1,1)) """ self.name = "over%03d" % counter[1] self.kind = "overlay" # kind of object self.alike = "overlay" # similar object for plotting self.beadtype = 1 # bead type self.beadtype2 = None # bead type 2 (alternative beadtype, ratio) self.nbeads = 0 # number of beads self.ismask = False # True if beadtype == 0 self.isplotted = False # True if plotted self.islabelled = False # True if labelled self.resolution = None # resolution is undefined self.hlabel = {'contour':[], 'text':[]} self.index = counter[0] self.subindex = counter[1] self.translate = [0.0,0.0] # modification used when an object is duplicated if scale is None: scale = 1 if not isinstance(scale,(tuple,list)): scale = (scale,scale) self.scale = scale if angle is None: angle = 0 self.angle = angle self.flipud = flipud if not os.path.isfile(filename): raise IOError(f'the file "{filename}" does not exist') self.filename = filename self.ncolors = ncolors self.color = None self.colormax = None self.original,self.raw,self.im,self.map = self.load() self.xmin0 = xmin self.ymin0 = ymin self.xmax0 = xmin + self.im.shape[1] self.ymax0 = ymin + self.im.shape[0] self.xcenter0 = (self.xmin+self.xmax)/2 self.ycenter0 = (self.ymin+self.ymax)/2 def select(self,color=None,colormax=None,scale=None,angle=None): """ select the color index: select(color = c) peeks pixels = c select(color = c, colormax = cmax) peeks pixels>=c and pixels<=cmax """ if color is None: color = self.color else: self.color = color if (colormax is None) and (self.colormax is not None) and (self.colormax > self.color): colormax = self.colormax else: colormax = self.colormax = color if isinstance(color,int) and color<len(self.map): S = np.logical_and(self.im>=color,self.im<=colormax) self.nbeads = np.count_nonzero(S) return np.flipud(S) if self.flipud else S raise ValueError("color must be an integer lower than %d" % len(self.map)) def load(self): """ load image and process it returns the image, the indexed image and its color map (à la Matlab, such as imread) note: if the image contains a palette it is used, if not the image is converted to an indexed image without dihtering """ I = Image.open(self.filename) if self.angle != 0: I= I.rotate(self.angle) if self.scale[0] * self.scale[1] != 1: I = I.resize((round(I.size[0]*self.scale[0]),round(I.size[1]*self.scale[1]))) palette = I.getpalette() if palette is None: J=I.convert(mode="P",colors=self.ncolors,palette=Image.Palette.ADAPTIVE) palette = J.getpalette() else: J = I p = np.array(palette,dtype="uint8").reshape((int(len(palette)/3),3)) ncolors = len(p.sum(axis=1).nonzero()[0]); if ncolors<self.ncolors: print(f"only {ncolors} are available") return I,J, np.array(J,dtype="uint8"), p[:ncolors,:] def __repr__(self): """ display for rectangle class """ print("%s - %s object" % (self.name, self.kind)) print(f'\tfilename: "{self.filename}"') print("\tangle = %0.4g (original image)" % self.angle) print("\tscale = [%0.4g, %0.4g] (original image)" % self.scale) print(f"\tncolors = {self.ncolors} (selected={self.color})") print("\trange x = [%0.4g %0.4g]" % (self.xmin,self.xmax)) print("\trange y = [%0.4g %0.4g]" % (self.ymin,self.ymax)) print("\tcenter = [%0.4g %0.4g]" % (self.xcenter,self.ycenter)) print("\ttranslate = [%0.4g %0.4g]" % (self.translate[0],self.translate[1])) print("note: use the attribute origina,raw to see the raw image") return "%s object: %s (beadtype=%d)" % (self.kind,self.name,self.beadtype)
Ancestors
Class variables
var hasclosefit
var hascontour
Methods
def load(self)
-
load image and process it returns the image, the indexed image and its color map (à la Matlab, such as imread)
note: if the image contains a palette it is used, if not the image is converted to an indexed image without dihtering
Expand source code
def load(self): """ load image and process it returns the image, the indexed image and its color map (à la Matlab, such as imread) note: if the image contains a palette it is used, if not the image is converted to an indexed image without dihtering """ I = Image.open(self.filename) if self.angle != 0: I= I.rotate(self.angle) if self.scale[0] * self.scale[1] != 1: I = I.resize((round(I.size[0]*self.scale[0]),round(I.size[1]*self.scale[1]))) palette = I.getpalette() if palette is None: J=I.convert(mode="P",colors=self.ncolors,palette=Image.Palette.ADAPTIVE) palette = J.getpalette() else: J = I p = np.array(palette,dtype="uint8").reshape((int(len(palette)/3),3)) ncolors = len(p.sum(axis=1).nonzero()[0]); if ncolors<self.ncolors: print(f"only {ncolors} are available") return I,J, np.array(J,dtype="uint8"), p[:ncolors,:]
def select(self, color=None, colormax=None, scale=None, angle=None)
-
select the color index: select(color = c) peeks pixels = c select(color = c, colormax = cmax) peeks pixels>=c and pixels<=cmax
Expand source code
def select(self,color=None,colormax=None,scale=None,angle=None): """ select the color index: select(color = c) peeks pixels = c select(color = c, colormax = cmax) peeks pixels>=c and pixels<=cmax """ if color is None: color = self.color else: self.color = color if (colormax is None) and (self.colormax is not None) and (self.colormax > self.color): colormax = self.colormax else: colormax = self.colormax = color if isinstance(color,int) and color<len(self.map): S = np.logical_and(self.im>=color,self.im<=colormax) self.nbeads = np.count_nonzero(S) return np.flipud(S) if self.flipud else S raise ValueError("color must be an integer lower than %d" % len(self.map))
Inherited members
class raster (name='default raster', width=100, height=100, dpi=200, fontsize=10, mass=1, volume=1, radius=1.5, contactradius=0.5, velocities=[0, 0, 0], forces=[0, 0, 0], preview=True, previewthumb=(128, 128), filename='')
-
raster class for LAMMPS SMD
Constructor
R = raster(width=100,height=100...) Extra properties dpi, fontsize additional properties for R.data() scale, center : full scaling mass, volume, radius, contactradius, velocities, forces: bead scaling filename List of available properties = default values name = "default raster" width = 100 height = 100 dpi = 200 fontsize = 10 mass = 1 volume = 1 radius = 1.5 contactradius = 0.5 velocities = [0, 0, 0] forces = [0, 0, 0] preview = True previewthumb = (128,128) filename = ["%dx%d raster (%s)" % (self.width,self.height,self.name)]
Graphical objects
R.rectangle(xleft,xright,ybottom,ytop [, beadtype=1,mode="lower", angle=0, ismask=False]) R.rectangle(xcenter,ycenter,width,height [, beadtype=1,mode="center", angle=0, ismask=False]) R.circle(xcenter,ycenter,radius [, beadtype=1,shaperatio=1, angle=0, ismask=False], resolution=20, shiftangle=0) R.triangle(...) R.diamond(...) R.pentagon(...) R.hexagon(...) R.overlay(xleft,xright,filename=="valid/image.ext",color=2,beadtype=1) note: use fake=True to generate an object without inserting it R.collection(...) generates collection of existing or fake objects R.object.copy(...) enables to copy an object
Display methods (precedence affects the result) R.plot() R.show(), R.show(extra="label",contour=True,what="beadtype" or "objindex") R.show(extra="labels") R.list() R.get("object") R.print() R.label("object") R.unlabel("object") R.figure() R.newfigure(dpi=300)
R.numeric() R.string(), R.string(what="beadtype" or "objindex")) R.names() R.print()
Clear and delete R.clear() R.clear("all") R.delete("object")
Copy objects R.copyalongpath(....) R.scatter()
Generate an input data object X = R.data() or X=R.data(scale=(1,1),center=(0,0)) X.write("/tmp/myfile")
initialize raster
Expand source code
class raster: """ raster class for LAMMPS SMD Constructor R = raster(width=100,height=100...) Extra properties dpi, fontsize additional properties for R.data() scale, center : full scaling mass, volume, radius, contactradius, velocities, forces: bead scaling filename List of available properties = default values name = "default raster" width = 100 height = 100 dpi = 200 fontsize = 10 mass = 1 volume = 1 radius = 1.5 contactradius = 0.5 velocities = [0, 0, 0] forces = [0, 0, 0] preview = True previewthumb = (128,128) filename = ["%dx%d raster (%s)" % (self.width,self.height,self.name)] Graphical objects R.rectangle(xleft,xright,ybottom,ytop [, beadtype=1,mode="lower", angle=0, ismask=False]) R.rectangle(xcenter,ycenter,width,height [, beadtype=1,mode="center", angle=0, ismask=False]) R.circle(xcenter,ycenter,radius [, beadtype=1,shaperatio=1, angle=0, ismask=False], resolution=20, shiftangle=0) R.triangle(...) R.diamond(...) R.pentagon(...) R.hexagon(...) R.overlay(xleft,xright,filename=="valid/image.ext",color=2,beadtype=1) note: use fake=True to generate an object without inserting it R.collection(...) generates collection of existing or fake objects R.object.copy(...) enables to copy an object Display methods (precedence affects the result) R.plot() R.show(), R.show(extra="label",contour=True,what="beadtype" or "objindex") R.show(extra="labels") R.list() R.get("object") R.print() R.label("object") R.unlabel("object") R.figure() R.newfigure(dpi=300) R.numeric() R.string(), R.string(what="beadtype" or "objindex")) R.names() R.print() Clear and delete R.clear() R.clear("all") R.delete("object") Copy objects R.copyalongpath(....) R.scatter() Generate an input data object X = R.data() or X=R.data(scale=(1,1),center=(0,0)) X.write("/tmp/myfile") """ # CONSTRUCTOR ---------------------------- def __init__(self, # raster properties name="default raster", width=100, height=100, # printing and display dpi=200, fontsize=10, # for data conversion mass=1, volume=1, radius=1.5, contactradius=0.5, velocities=[0,0,0], forces=[0,0,0], preview=True, previewthumb = (128,128), filename="" ): """ initialize raster """ self.name = name self.width = width self.height = height self.xcenter= width/2 self.ycenter = height/2 self.objects = {} self.nobjects = 0 # total number of objects (alive) self.nbeads = 0 self.counter = { "triangle":0, "diamond":0, "rectangle":0, "pentagon":0, "hexagon":0, "circle":0, "overlay":0, "collection":0, "all":0 } self.fontsize = 10 # font size for labels self.imbead = np.zeros((height,width),dtype=np.int8) self.imobj = np.zeros((height,width),dtype=np.int8) self.hfig = [] # figure handle self.dpi = dpi # generic SMD properties (to be rescaled) self.volume = volume self.mass = mass self.radius = radius self.contactradius = contactradius self.velocities = velocities self.forces =forces self.preview = preview self.previewthumb = previewthumb if filename == "": self.filename = ["%dx%d raster (%s)" % (self.width,self.height,self.name)] else: self.filename = filename # DATA ---------------------------- def data(self,scale=(1,1),center=(0,0),maxtype=None,hexpacking=None): """ return a pizza.data object data() data(scale=(scalex,scaley), center=(centerx,centery), maxtype=number, hexpacking=(0.5,0)) """ if not isinstance(scale,tuple) or len(scale)!=2: raise ValueError("scale must be tuple (scalex,scaley)") if not isinstance(center,tuple) or len(scale)!=2: raise ValueError("center must be tuple (centerx,centery)") scalez = np.sqrt(scale[0]*scale[1]) scalevol = scale[0]*scale[1] #*scalez maxtypeheader = self.count()[-1][0] if maxtype is None else maxtype n = self.length() i,j = self.imbead.nonzero() # x=j+0.5 y=i+0.5 x = (j+0.5-center[0])*scale[0] y = (i+0.5-center[1])*scale[1] if hexpacking is not None: if isinstance(hexpacking,tuple) and len(hexpacking)==2: for k in range(len(i)): if i[k] % 2: x[k] = (j[k]+0.5+hexpacking[1]-center[0])*scale[0] else: x[k] = (j[k]+0.5+hexpacking[0]-center[0])*scale[0] else: raise ValueError("hexpacking should be a tuple (shiftodd,shifteven)") X = data3() # empty pizza.data3.data object X.title = self.name + "(raster)" X.headers = {'atoms': n, 'atom types': maxtypeheader, 'xlo xhi': ((0.0-center[0])*scale[0], (self.width-0.0-center[0])*scale[0]), 'ylo yhi': ((0.0-center[1])*scale[1], (self.height-0.0-center[1])*scale[1]), 'zlo zhi': (0, scalez)} # [ATOMS] section X.append('Atoms',list(range(1,n+1)),True,"id") # id X.append('Atoms',self.imbead[i,j],True,"type") # Type X.append('Atoms',1,True,"mol") # mol X.append('Atoms',self.volume*scalevol,False,"c_vol") # c_vol X.append('Atoms',self.mass*scalevol,False,"mass") # mass X.append('Atoms',self.radius*scalez,False,"radius") # radius X.append('Atoms',self.contactradius*scalez,False,"c_contact_radius") # c_contact_radius X.append('Atoms',x,False,"x") # x X.append('Atoms',y,False,"y") # y X.append('Atoms',0,False,"z") # z X.append('Atoms',x,False,"x0") # x0 X.append('Atoms',y,False,"y0") # y0 X.append('Atoms',0,False,"z0") # z0 # [VELOCITIES] section X.append('Velocities',list(range(1,n+1)),True,"id") # id X.append('Velocities',self.velocities[0],False,"vx") # vx X.append('Velocities',self.velocities[1],False,"vy") # vy X.append('Velocities',self.velocities[2],False,"vz") # vz # pseudo-filename X.flist = self.filename return X # LENGTH ---------------------------- def length(self,t=None,what="beadtype"): """ returns the total number of beads length(type,"beadtype") """ if what == "beadtype": num = self.imbead elif what == "objindex": num = self.imobj else: raise ValueError('"beadtype" and "objindex" are the only acceptable values') if t==None: return np.count_nonzero(num>0) else: return np.count_nonzero(num==t) # NUMERIC ---------------------------- def numeric(self): """ retrieve the image as a numpy.array """ return self.imbead, self.imobj # STRING ---------------------------- def string(self,what="beadtype"): """ convert the image as ASCII strings """ if what == "beadtype": num = np.flipud(duplicate(self.imbead)) elif what == "objindex": num = np.flipud(duplicate(self.imobj)) else: raise ValueError('"beadtype" and "objindex" are the only acceptable values') num[num>0] = num[num>0] + 65 num[num==0] = 32 num = list(num) return ["".join(map(chr,x)) for x in num] # GET ----------------------------- def get(self,name): """ returns the object """ if name in self.objects: return self.objects[name] else: raise ValueError('the object "%s" does not exist, use list()' % name) # GETATTR -------------------------- def __getattr__(self,key): """ get attribute override """ return self.get(key) # CLEAR ---------------------------- def clear(self,what="nothing"): """ clear the plotting area, use clear("all")) to remove all objects """ self.imbead = np.zeros((self.height,self.width),dtype=np.int8) self.imobj = np.zeros((self.height,self.width),dtype=np.int8) for o in self.names(): if what=="all": self.delete(o) else: self.objects[o].isplotted = False self.objects[o].islabelled = False if not self.objects[o].ismask: self.nbeads -= self.objects[o].nbeads self.objects[o].nbeads = 0 # number of beads (plotted) self.figure() plt.cla() self.show() # DISP method ---------------------------- def __repr__(self): """ display method """ ctyp = self.count() # count objects (not beads) print("-"*40) print('RASTER area "%s" with %d objects' % (self.name,self.nobjects)) print("-"*40) print("<- grid size ->") print("\twidth: %d" % self.width) print("\theight: %d" % self.height) print("<- bead types ->") nbt = 0 if len(ctyp): for i,c in enumerate(ctyp): nb = self.length(c[0]) nbt += nb print("\t type=%d (%d objects, %d beads)" % (c[0],c[1],nb)) else: print("\tno bead assigned") print("-"*40) if self.preview and len(self)>0 and self.max>0: if PILavailable: display(self.torgb("beadtype",self.previewthumb)) display(self.torgb("objindex",self.previewthumb)) else: print("no PIL image") return "RASTER AREA %d x %d with %d objects (%d types, %d beads)." % \ (self.width,self.height,self.nobjects,len(ctyp),nbt) # TORGB method ---------------------------- def torgb(self,what="beadtype",thumb=None): """ converts bead raster to image rgb = raster.torgb(what="beadtype") thumbnail = raster.torgb(what="beadtype",(128,128)) use: rgb.save("/path/filename.png") for saving what = "beadtype" or "objindex" """ if what=="beadtype": rgb = ind2rgb(self.imbead,ncolors=self.max+1) elif what == "objindex": rgb = ind2rgb(self.imobj,ncolors=len(self)+1) else: raise ValueError('"beadtype" and "objindex" are the only acceptable values') if thumb is not None: rgb.thumbnail(thumb) return rgb # COUNT method ---------------------------- def count(self): """ count objects by type """ typlist = [] for o in self.names(): if isinstance(self.objects[o].beadtype,list): typlist += self.objects[o].beadtype else: typlist.append(self.objects[o].beadtype) utypes = list(set(typlist)) c = [] for t in utypes: c.append((t,typlist.count(t))) return c # max method ------------------------------ @property def max(self): """ max bead type """ typlist = [] for o in self.names(): if isinstance(self.objects[o].beadtype,list): typlist += self.objects[o].beadtype else: typlist.append(self.objects[o].beadtype) return max(typlist) # len method ------------------------------ def __len__(self): """ len method """ return len(self.objects) # NAMES method ---------------------------- def names(self): """ return the names of objects sorted as index """ namesunsorted=namessorted=list(self.objects.keys()) nobj = len(namesunsorted) for iobj in range(nobj): namessorted[self.objects[namesunsorted[iobj]].index-1] = namesunsorted[iobj] return namessorted # LIST method ---------------------------- def list(self): """ list objects """ fmt = "%%%ss:" % max(10,max([len(n) for n in self.names()])+2) print("RASTER with %d objects" % self.nobjects) for o in self.objects.keys(): print(fmt % self.objects[o].name,"%-10s" % self.objects[o].kind, "(beadtype=%d,object index=[%d,%d], n=%d)" % \ (self.objects[o].beadtype, self.objects[o].index, self.objects[o].subindex, self.objects[o].nbeads)) # EXIST method ---------------------------- def exist(self,name): """ exist object """ return name in self.objects # DELETE method ---------------------------- def delete(self,name): """ delete object """ if name in self.objects: if not self.objects[name].ismask: self.nbeads -= self.objects[name].nbeads del self.objects[name] self.nobjects -= 1 else: raise ValueError("%d is not a valid name (use list()) to list valid objects" % name) self.clear() self.plot() self.show(extra="label") # VALID method def valid(self,x,y): """ validation of coordinates """ return min(self.width,max(0,round(x))),min(self.height,max(0,round(y))) # frameobj method def frameobj(self,obj): """ frame coordinates by taking into account translation """ if obj.hasclosefit: envelope = 0 else: envelope = 1 xmin, ymin = self.valid(obj.xmin-envelope, obj.ymin-envelope) xmax, ymax = self.valid(obj.xmax+envelope, obj.ymax+envelope) return xmin, ymin, xmax, ymax # RECTANGLE ---------------------------- def rectangle(self,a,b,c,d, mode="lowerleft",name=None,angle=0, beadtype=None,ismask=False,fake=False,beadtype2=None): """ rectangle object rectangle(xleft,xright,ybottom,ytop [, beadtype=1,mode="lower", angle=0, ismask=False]) rectangle(xcenter,ycenter,width,height [, beadtype=1,mode="center", angle=0, ismask=False]) use rectangle(...,beadtype2=(type,ratio)) to salt an object with beads from another type and with a given ratio """ # object creation self.counter["all"] += 1 self.counter["rectangle"] += 1 R = Rectangle((self.counter["all"],self.counter["rectangle"])) if (name != None) and (name != ""): if self.exist(name): print('RASTER:: the object "%s" is overwritten',name) self.delete(name) R.name = name else: name = R.name if beadtype is not None: R.beadtype = int(np.floor(beadtype)) if beadtype2 is not None: if not isinstance(beadtype2,tuple) or len(beadtype2)!=2: raise AttributeError("beadtype2 must be a tuple (beadtype,ratio)") R.beadtype2 = beadtype2 if ismask: R.beadtype = 0 R.ismask = R.beadtype==0 # build vertices if mode == "lowerleft": R.xcenter0 = (a+b)/2 R.ycenter0 = (c+d)/2 R.vertices = [ _rotate(a,c,R.xcenter0,R.ycenter0,angle), _rotate(b,c,R.xcenter0,R.ycenter0,angle), _rotate(b,d,R.xcenter0,R.ycenter0,angle), _rotate(a,d,R.xcenter0,R.ycenter0,angle), _rotate(a,c,R.xcenter0,R.ycenter0,angle) ] # anti-clockwise, closed (last point repeated) elif mode == "center": R.xcenter0 = a R.ycenter0 = b R.vertices = [ _rotate(a-c/2,b-d/2,R.xcenter0,R.ycenter0,angle), _rotate(a+c/2,b-d/2,R.xcenter0,R.ycenter0,angle), _rotate(a+c/2,b+d/2,R.xcenter0,R.ycenter0,angle), _rotate(a-c/2,b+d/2,R.xcenter0,R.ycenter0,angle), _rotate(a-c/2,b-d/2,R.xcenter0,R.ycenter0,angle) ] else: raise ValueError('"%s" is not a recognized mode, use "lowerleft" (default) and "center" instead') # build path object and range R.codes = [ path.Path.MOVETO, path.Path.LINETO, path.Path.LINETO, path.Path.LINETO, path.Path.CLOSEPOLY ] R.nvertices = len(R.vertices)-1 R.xmin0, R.ymin0, R.xmax0, R.ymax0 = R.corners() R.xmin0, R.ymin0 = self.valid(R.xmin0,R.ymin0) R.xmax0, R.ymax0 = self.valid(R.xmax0,R.ymax0) R.angle = angle # store the object (if not fake) if fake: self.counter["all"] -= 1 self.counter["rectangle"] -= 1 return R else: self.objects[name] = R self.nobjects += 1 return None # CIRCLE ---------------------------- def circle(self,xc,yc,radius, name=None,shaperatio=1,angle=0,beadtype=None,ismask=False, resolution=20,shiftangle=0,fake=False,beadtype2=None): """ circle object (or any regular polygon) circle(xcenter,ycenter,radius [, beadtype=1,shaperatio=1, angle=0, ismask=False], resolution=20, shiftangle=0) use circle(...,beadtype2=(type,ratio)) to salt an object with beads from another type and with a given ratio """ # object creation self.counter["all"] += 1 if resolution==3: typ = "triangle" self.counter["triangle"] += 1 G = Triangle((self.counter["all"],self.counter["triangle"])) elif resolution==4: typ = "diamond" self.counter["diamond"] += 1 G = Diamond((self.counter["all"],self.counter["diamond"])) elif resolution==5: typ = "pentagon" self.counter["pentagon"] += 1 G = Pentagon((self.counter["all"],self.counter["pentagon"])) elif resolution==6: typ = "hexagon" self.counter["hexagon"] += 1 G = Hexagon((self.counter["all"],self.counter["hexagon"])) else: typ = "circle" self.counter["circle"] += 1 G = Circle((self.counter["all"],self.counter["circle"]),resolution=resolution) if (name != None) and (name != ""): if self.exist(name): print('RASTER:: the object "%s" is overwritten',name) self.delete(name) G.name = name else: name = G.name if beadtype is not None: G.beadtype = int(np.floor(beadtype)) if beadtype2 is not None: if not isinstance(beadtype2,tuple) or len(beadtype2)!=2: raise AttributeError("beadtype2 must be a tuple (beadtype,ratio)") G.beadtype2 = beadtype2 if ismask: G.beadtype = 0 G.ismask = G.beadtype==0 # build vertices th = np.linspace(0,2*np.pi,G.resolution+1) +shiftangle*np.pi/180 xgen = xc + radius * np.cos(th) ygen = yc + radius * shaperatio * np.sin(th) G.xcenter0, G.ycenter0, G.radius = xc, yc, radius G.vertices, G.codes = [], [] for i in range(G.resolution+1): G.vertices.append(_rotate(xgen[i],ygen[i],xc,yc,angle)) if i==0: G.codes.append(path.Path.MOVETO) elif i==G.resolution: G.codes.append(path.Path.CLOSEPOLY) else: G.codes.append(path.Path.LINETO) G.nvertices = len(G.vertices)-1 # build path object and range G.xmin0, G.ymin0, G.xmax0, G.ymax0 = G.corners() G.xmin0, G.ymin0 = self.valid(G.xmin0,G.ymin0) G.xmax0, G.ymax0 = self.valid(G.xmax0,G.ymax0) G.angle, G.shaperatio = angle, shaperatio # store the object if fake: self.counter["all"] -= 1 self.counter[typ] -= 1 return G else: self.objects[name] = G self.nobjects += 1 return None # OVERLAY ------------------------------- def overlay(self,x0,y0, name = None, filename = None, color = 1, colormax = None, ncolors = 4, beadtype = None, beadtype2 = None, ismask = False, fake = False, flipud = True, angle = 0, scale= (1,1) ): """ overlay object: made from an image converted to nc colors the object is made from the level ranged between ic and jc (bounds included) note: if palette found, no conversion is applied O = overlay(x0,y0,filename="/this/is/my/image.png",ncolors=nc,color=ic,colormax=jc,beadtype=b) O = overlay(...angle=0,scale=(1,1)) to induce rotation and change of scale O = overlay(....ismask=False,fake=False) note use overlay(...flipud=False) to prevent image fliping (standard) Outputs: O.original original image (PIL) O.raw image converted to ncolors if needed """ if filename is None or filename=="": raise ValueError("filename is required (valid image)") O = overlay(counter=(self.counter["all"]+1,self.counter["overlay"]+1), filename = filename, xmin = x0, ymin = y0, ncolors = ncolors, flipud = flipud, angle = angle, scale = scale ) O.select(color=color, colormax=colormax) if (name is not None) and (name !=""): if self.exist(name): print('RASTER:: the object "%s" is overwritten',name) self.delete(name) O.name = name else: name = O.name if beadtype is not None: O.beadtype = int(np.floor(beadtype)) if beadtype2 is not None: if not isinstance(beadtype2,tuple) or len(beadtype2)!=2: raise AttributeError("beadtype2 must be a tuple (beadtype,ratio)") O.beadtype2 = beadtype2 if ismask: O.beadtype = 0 O.ismask = O.beadtype==0 self.counter["all"] += 1 self.counter["overlay"] += 1 if fake: self.counter["all"] -= 1 self.counter["overlay"] -= 1 return O else: self.objects[name] = O self.nobjects += 1 return None # COLLECTION ---------------------------- def collection(self,*obj, name=None, beadtype=None, ismask=None, translate = [0.0,0.0], fake = False, **kwobj): """ collection of objects: collection(draftraster,name="mycollect" [,beadtype=1,ismask=True] collection(name="mycollect",newobjname1 = obj1, newobjname2 = obj2...) """ self.counter["all"] += 1 self.counter["collection"] += 1 C = Collection((self.counter["all"],self.counter["collection"])) # name if name != None: if self.exist(name): print('RASTER:: the object "%s" is overwritten',name) self.delete(name) C.name = name else: name = C.name # build the collection C.collection = collection(*obj,**kwobj) xmin = ymin = +1e99 xmax = ymax = -1e99 # apply modifications (beadtype, ismask) for o in C.collection.keys(): tmp = C.collection.getattr(o) tmp.translate[0] += translate[0] tmp.translate[1] += translate[1] xmin, xmax = min(xmin,tmp.xmin), max(xmax,tmp.xmax) ymin, ymax = min(ymin,tmp.ymin), max(ymax,tmp.ymax) if beadtype != None: tmp.beadtype = beadtype if ismask != None: tmp.ismask = ismask C.collection.setattr(o,tmp) C.xmin, C.xmax, C.ymin, C.ymax = xmin, xmax, ymin, ymax C.width, C.height = xmax-xmin, ymax-ymin if fake: return C else: self.objects[name] = C self.nobjects += 1 return None # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # =========== pseudo methods connected to circle() =========== # TRIANGLE, DIAMOND, PENTAGON, HEXAGON, ----------------------- def triangle(self,xc,yc,radius,name=None, shaperatio=1,angle=0,beadtype=None,ismask=False,shiftangle=0,fake=False,beadtype2=None): """ triangle object triangle(xcenter,ycenter,radius [, beadtype=1,shaperatio=1, angle=0, ismask=False] use triangle(...,beadtype2=(type,ratio)) to salt an object with beads from another type and with a given ratio """ self.circle(xc,yc,radius,name=name,shaperatio=shaperatio,resolution=3, angle=angle,beadtype=beadtype,ismask=ismask,shiftangle=0,fake=fake,beadtype2=beadtype2) def diamond(self,xc,yc,radius,name=None, shaperatio=1,angle=0,beadtype=None,ismask=False,shiftangle=0,fake=False,beadtype2=None): """ diamond object diamond(xcenter,ycenter,radius [, beadtype=1,shaperatio=1, angle=0, ismask=False] use diamond(...,beadtype2=(type,ratio)) to salt an object with beads from another type and with a given ratio """ self.circle(xc,yc,radius,name=name,shaperatio=shaperatio,resolution=4, angle=angle,beadtype=beadtype,ismask=ismask,shiftangle=0,fake=fake,beadtype2=beadtype2) def pentagon(self,xc,yc,radius,name=None, shaperatio=1,angle=0,beadtype=None,ismask=False,shiftangle=0,fake=False,beadtype2=None): """ pentagon object pentagon(xcenter,ycenter,radius [, beadtype=1,shaperatio=1, angle=0, ismask=False] use pentagon(...,beadtype2=(type,ratio)) to salt an object with beads from another type and with a given ratio """ self.circle(xc,yc,radius,name=name,shaperatio=shaperatio,resolution=5, angle=angle,beadtype=beadtype,ismask=ismask,shiftangle=0,fake=fake,beadtype2=beadtype2) def hexagon(self,xc,yc,radius,name=None, shaperatio=1,angle=0,beadtype=None,ismask=False,shiftangle=0,fake=False,beadtype2=None): """ hexagon object hexagon(xcenter,ycenter,radius [, beadtype=1,shaperatio=1, angle=0, ismask=False] use hexagon(...,beadtype2=(type,ratio)) to salt an object with beads from another type and with a given ratio """ self.circle(xc,yc,radius,name=name,shaperatio=shaperatio,resolution=6, angle=angle,beadtype=beadtype,ismask=ismask,shiftangle=0,fake=fake,beadtype2=beadtype2) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # label method ---------------------------- def label(self,name,**fmt): """ label: label(name [, contour=True,edgecolor="orange",facecolor="none",linewidth=2, ax=plt.gca()]) """ self.figure() if name in self.objects: if not self.objects[name].islabelled: if self.objects[name].alike == "mixed": for o in self.objects[name].collection: self.labelobj(o,**fmt) else: self.labelobj(self.objects[name],**fmt) plt.show() self.objects[name].islabelled = True else: raise ValueError("%d is not a valid name (use list()) to list valid objects" % name) # label object method ----------------------------- def labelobj(self,obj,contour=True,edgecolor="orange",facecolor="none",linewidth=2,ax=plt.gca()): """ labelobj: labelobj(obj [, contour=True,edgecolor="orange",facecolor="none",linewidth=2, ax=plt.gca()]) """ if contour: contour = obj.hascontour # e.g. overlays do not have contour if contour: patch = patches.PathPatch(obj.polygon2plot, facecolor=facecolor, edgecolor=edgecolor, lw=linewidth) obj.hlabel["contour"] = ax.add_patch(patch) else: obj.hlabel["contour"] = None obj.hlabel["text"] = \ plt.text(obj.xcenter, obj.ycenter, "%s\n(t=$%d$,$n_p$=%d)" % (obj.name, obj.beadtype,obj.nbeads), horizontalalignment = "center", verticalalignment = "center_baseline", fontsize=self.fontsize ) def unlabel(self,name): """ unlabel """ if name in self.objects: if self.objects[name].islabelled: self.objects[name].hlabel["contour"].remove() self.objects[name].hlabel["text"].remove() self.objects[name].hlabel = {'contour':[], 'text':[]} self.objects[name].islabelled = False else: raise ValueError("%d is not a valid name (use list()) to list valid objects" % name) # PLOT method ---------------------------- def plot(self): """ plot """ for o in self.objects: if not self.objects[o].isplotted: if self.objects[o].alike == "mixed": for o2 in self.objects[o].collection: self.plotobj(o2) else: self.plotobj(self.objects[o]) # store it as plotted self.objects[o].isplotted = True if not self.objects[o].ismask: self.nbeads += self.objects[o].nbeads # PLOTobj method ----------------------- def plotobj(self,obj): """ plotobj(obj) """ if obj.alike == "circle": xmin, ymin, xmax, ymax = self.frameobj(obj) j,i = np.meshgrid(range(xmin,xmax), range(ymin,ymax)) points = np.vstack((j.flatten(),i.flatten())).T npoints = points.shape[0] inside = obj.polygon.contains_points(points) if obj.beadtype2 is None: # -- no salting -- for k in range(npoints): if inside[k] and \ points[k,0]>=0 and \ points[k,0]<self.width and \ points[k,1]>=0 and \ points[k,1]<self.height: self.imbead[points[k,1],points[k,0]] = obj.beadtype self.imobj[points[k,1],points[k,0]] = obj.index obj.nbeads += 1 else: for k in range(npoints): # -- salting -- if inside[k] and \ points[k,0]>=0 and \ points[k,0]<self.width and \ points[k,1]>=0 and \ points[k,1]<self.height: if np.random.rand()<obj.beadtype2[1]: self.imbead[points[k,1],points[k,0]] = obj.beadtype2[0] else: self.imbead[points[k,1],points[k,0]] = obj.beadtype self.imobj[points[k,1],points[k,0]] = obj.index obj.nbeads += 1 elif obj.alike == "overlay": xmin, ymin, xmax, ymax = self.frameobj(obj) j,i = np.meshgrid(range(xmin,xmax), range(ymin,ymax)) points = np.vstack((j.flatten(),i.flatten())).T npoints = points.shape[0] inside = obj.select() if obj.beadtype2 is None: # -- no salting -- for k in range(npoints): if inside[ points[k,1]-ymin, points[k,0]-xmin ] and \ points[k,0]>=0 and \ points[k,0]<self.width and \ points[k,1]>=0 and \ points[k,1]<self.height: self.imbead[points[k,1],points[k,0]] = obj.beadtype self.imobj[points[k,1],points[k,0]] = obj.index obj.nbeads += 1 else: for k in range(npoints): # -- salting -- if inside[ points[k,0]-ymin, points[k,0]-xmin ] and \ points[k,0]>=0 and \ points[k,0]<self.width and \ points[k,1]>=0 and \ points[k,1]<self.height: if np.random.rand()<obj.beadtype2[1]: self.imbead[points[k,1],points[k,0]] = obj.beadtype2[0] else: self.imbead[points[k,1],points[k,0]] = obj.beadtype self.imobj[points[k,1],points[k,0]] = obj.index obj.nbeads += 1 else: raise ValueError("This object type is notimplemented") # SHOW method ---------------------------- def show(self,extra="none",contour=True,what="beadtype"): """ show method: show(extra="label",contour=True,what="beadtype") """ self.figure() if what=="beadtype": imagesc(self.imbead) elif what == "objindex": imagesc(self.imobj) else: raise ValueError('"beadtype" and "objindex" are the only acceptable values') if extra == "label": ax = plt.gca() for o in self.names(): if not self.objects[o].ismask: self.label(o,ax=ax,contour=contour) ax.set_title("raster area: %s \n (n=%d, $n_p$=%d)" %\ (self.name,self.length(),self.nbeads) ) plt.show() # SHOW method ---------------------------- def print(self,what="beadtype"): """ print method """ txt = self.string(what=what) for i in range(len(txt)): print(txt[i],end="\n") # FIGURE method ---------------------------- def figure(self): """ set the current figure """ if self.hfig==[] or not plt.fignum_exists(self.hfig.number): self.newfigure() plt.figure(self.hfig.number) # NEWFIGURE method ---------------------------- def newfigure(self): """ create a new figure (dpi=200) """ self.hfig = plt.figure(dpi=self.dpi) # COPY OBJECT ALONG a contour ----------------- def copyalongpath(self,obj, name="path", beadtype=None, path=linear, xmin=10, ymin=10, xmax=70, ymax=90, n=7, USER=struct()): """ The method enable to copy an existing object (from the current raster, from another raster or a fake object) amp,g Parameters ---------- obj : real or fake object the object to be copied. name : string, optional the name of the object collection. The default is "path". beadtype : integet, optional type of bead (can override existing value). The default is None. path : function, optional parametric function returning x,y. The default is linear. x is between xmin and xmax, and y between ymin, ymax xmin : int64 or float, optional left x corner position. The default is 10. ymin : int64 or float, optional bottom y corner position. The default is 10. xmax : int64 or float, optional right x corner position. The default is 70. ymax : int64 or float, optional top y corner position. The default is 90. n : integet, optional number of copies. The default is 7. USER : structure to pass specific parameters Returns ------- None. """ if not isinstance(USER,struct): raise TypeError("USER should be a structure") x,y = path(xmin=xmin,ymin=ymin,xmax=xmax,ymax=ymax,n=n,USER=USER) btyp = obj.beadtype if beadtype == None else beadtype collect = {} for i in range(n): nameobj = "%s_%s_%02d" % (name,obj.name,i) x[i], y[i] = self.valid(x[i], y[i]) translate = [ x[i]-obj.xcenter, y[i]-obj.ycenter ] collect[nameobj] = obj.copy(translate=translate, name=nameobj, beadtype=btyp) self.collection(**collect,name=name) # SCATTER ------------------------------- def scatter(self, E, name="emulsion", beadtype=None, ismask = False ): """ Parameters ---------- E : scatter or emulsion object codes for x,y and r. name : string, optional name of the collection. The default is "emulsion". beadtype : integer, optional for all objects. The default is 1. ismask : logical, optional Set it to true to force a mask. The default is False. Raises ------ TypeError Return an error of the object is not a scatter type. Returns ------- None. """ if isinstance(E,scatter): collect = {} for i in range(E.n): b = E.beadtype[i] if beadtype==None else beadtype nameobj = "glob%02d" % i collect[nameobj] = self.circle(E.x[i],E.y[i],E.r[i], name=nameobj,beadtype=b,ismask=ismask,fake=True) self.collection(**collect,name=name) else: raise TypeError("the first argument must be an emulsion object")
Instance variables
var max
-
max bead type
Expand source code
@property def max(self): """ max bead type """ typlist = [] for o in self.names(): if isinstance(self.objects[o].beadtype,list): typlist += self.objects[o].beadtype else: typlist.append(self.objects[o].beadtype) return max(typlist)
Methods
def circle(self, xc, yc, radius, name=None, shaperatio=1, angle=0, beadtype=None, ismask=False, resolution=20, shiftangle=0, fake=False, beadtype2=None)
-
circle object (or any regular polygon) circle(xcenter,ycenter,radius [, beadtype=1,shaperatio=1, angle=0, ismask=False], resolution=20, shiftangle=0) use circle(…,beadtype2=(type,ratio)) to salt an object with beads from another type and with a given ratio
Expand source code
def circle(self,xc,yc,radius, name=None,shaperatio=1,angle=0,beadtype=None,ismask=False, resolution=20,shiftangle=0,fake=False,beadtype2=None): """ circle object (or any regular polygon) circle(xcenter,ycenter,radius [, beadtype=1,shaperatio=1, angle=0, ismask=False], resolution=20, shiftangle=0) use circle(...,beadtype2=(type,ratio)) to salt an object with beads from another type and with a given ratio """ # object creation self.counter["all"] += 1 if resolution==3: typ = "triangle" self.counter["triangle"] += 1 G = Triangle((self.counter["all"],self.counter["triangle"])) elif resolution==4: typ = "diamond" self.counter["diamond"] += 1 G = Diamond((self.counter["all"],self.counter["diamond"])) elif resolution==5: typ = "pentagon" self.counter["pentagon"] += 1 G = Pentagon((self.counter["all"],self.counter["pentagon"])) elif resolution==6: typ = "hexagon" self.counter["hexagon"] += 1 G = Hexagon((self.counter["all"],self.counter["hexagon"])) else: typ = "circle" self.counter["circle"] += 1 G = Circle((self.counter["all"],self.counter["circle"]),resolution=resolution) if (name != None) and (name != ""): if self.exist(name): print('RASTER:: the object "%s" is overwritten',name) self.delete(name) G.name = name else: name = G.name if beadtype is not None: G.beadtype = int(np.floor(beadtype)) if beadtype2 is not None: if not isinstance(beadtype2,tuple) or len(beadtype2)!=2: raise AttributeError("beadtype2 must be a tuple (beadtype,ratio)") G.beadtype2 = beadtype2 if ismask: G.beadtype = 0 G.ismask = G.beadtype==0 # build vertices th = np.linspace(0,2*np.pi,G.resolution+1) +shiftangle*np.pi/180 xgen = xc + radius * np.cos(th) ygen = yc + radius * shaperatio * np.sin(th) G.xcenter0, G.ycenter0, G.radius = xc, yc, radius G.vertices, G.codes = [], [] for i in range(G.resolution+1): G.vertices.append(_rotate(xgen[i],ygen[i],xc,yc,angle)) if i==0: G.codes.append(path.Path.MOVETO) elif i==G.resolution: G.codes.append(path.Path.CLOSEPOLY) else: G.codes.append(path.Path.LINETO) G.nvertices = len(G.vertices)-1 # build path object and range G.xmin0, G.ymin0, G.xmax0, G.ymax0 = G.corners() G.xmin0, G.ymin0 = self.valid(G.xmin0,G.ymin0) G.xmax0, G.ymax0 = self.valid(G.xmax0,G.ymax0) G.angle, G.shaperatio = angle, shaperatio # store the object if fake: self.counter["all"] -= 1 self.counter[typ] -= 1 return G else: self.objects[name] = G self.nobjects += 1 return None
def clear(self, what='nothing')
-
clear the plotting area, use clear("all")) to remove all objects
Expand source code
def clear(self,what="nothing"): """ clear the plotting area, use clear("all")) to remove all objects """ self.imbead = np.zeros((self.height,self.width),dtype=np.int8) self.imobj = np.zeros((self.height,self.width),dtype=np.int8) for o in self.names(): if what=="all": self.delete(o) else: self.objects[o].isplotted = False self.objects[o].islabelled = False if not self.objects[o].ismask: self.nbeads -= self.objects[o].nbeads self.objects[o].nbeads = 0 # number of beads (plotted) self.figure() plt.cla() self.show()
def collection(self, *obj, name=None, beadtype=None, ismask=None, translate=[0.0, 0.0], fake=False, **kwobj)
-
collection of objects: collection(draftraster,name="mycollect" [,beadtype=1,ismask=True] collection(name="mycollect",newobjname1 = obj1, newobjname2 = obj2…)
Expand source code
def collection(self,*obj, name=None, beadtype=None, ismask=None, translate = [0.0,0.0], fake = False, **kwobj): """ collection of objects: collection(draftraster,name="mycollect" [,beadtype=1,ismask=True] collection(name="mycollect",newobjname1 = obj1, newobjname2 = obj2...) """ self.counter["all"] += 1 self.counter["collection"] += 1 C = Collection((self.counter["all"],self.counter["collection"])) # name if name != None: if self.exist(name): print('RASTER:: the object "%s" is overwritten',name) self.delete(name) C.name = name else: name = C.name # build the collection C.collection = collection(*obj,**kwobj) xmin = ymin = +1e99 xmax = ymax = -1e99 # apply modifications (beadtype, ismask) for o in C.collection.keys(): tmp = C.collection.getattr(o) tmp.translate[0] += translate[0] tmp.translate[1] += translate[1] xmin, xmax = min(xmin,tmp.xmin), max(xmax,tmp.xmax) ymin, ymax = min(ymin,tmp.ymin), max(ymax,tmp.ymax) if beadtype != None: tmp.beadtype = beadtype if ismask != None: tmp.ismask = ismask C.collection.setattr(o,tmp) C.xmin, C.xmax, C.ymin, C.ymax = xmin, xmax, ymin, ymax C.width, C.height = xmax-xmin, ymax-ymin if fake: return C else: self.objects[name] = C self.nobjects += 1 return None
def copyalongpath(self, obj, name='path', beadtype=None, path=<function linear>, xmin=10, ymin=10, xmax=70, ymax=90, n=7, USER=structure (struct object) with 0 fields)
-
The method enable to copy an existing object (from the current raster, from another raster or a fake object) amp,g
Parameters
obj : real or fake object the object to be copied. name : string, optional the name of the object collection. The default is "path". beadtype : integet, optional type of bead (can override existing value). The default is None. path : function, optional parametric function returning x,y. The default is linear. x is between xmin and xmax, and y between ymin, ymax xmin : int64 or float, optional left x corner position. The default is 10. ymin : int64 or float, optional bottom y corner position. The default is 10. xmax : int64 or float, optional right x corner position. The default is 70. ymax : int64 or float, optional top y corner position. The default is 90. n : integet, optional number of copies. The default is 7. USER : structure to pass specific parameters
Returns
None.
Expand source code
def copyalongpath(self,obj, name="path", beadtype=None, path=linear, xmin=10, ymin=10, xmax=70, ymax=90, n=7, USER=struct()): """ The method enable to copy an existing object (from the current raster, from another raster or a fake object) amp,g Parameters ---------- obj : real or fake object the object to be copied. name : string, optional the name of the object collection. The default is "path". beadtype : integet, optional type of bead (can override existing value). The default is None. path : function, optional parametric function returning x,y. The default is linear. x is between xmin and xmax, and y between ymin, ymax xmin : int64 or float, optional left x corner position. The default is 10. ymin : int64 or float, optional bottom y corner position. The default is 10. xmax : int64 or float, optional right x corner position. The default is 70. ymax : int64 or float, optional top y corner position. The default is 90. n : integet, optional number of copies. The default is 7. USER : structure to pass specific parameters Returns ------- None. """ if not isinstance(USER,struct): raise TypeError("USER should be a structure") x,y = path(xmin=xmin,ymin=ymin,xmax=xmax,ymax=ymax,n=n,USER=USER) btyp = obj.beadtype if beadtype == None else beadtype collect = {} for i in range(n): nameobj = "%s_%s_%02d" % (name,obj.name,i) x[i], y[i] = self.valid(x[i], y[i]) translate = [ x[i]-obj.xcenter, y[i]-obj.ycenter ] collect[nameobj] = obj.copy(translate=translate, name=nameobj, beadtype=btyp) self.collection(**collect,name=name)
def count(self)
-
count objects by type
Expand source code
def count(self): """ count objects by type """ typlist = [] for o in self.names(): if isinstance(self.objects[o].beadtype,list): typlist += self.objects[o].beadtype else: typlist.append(self.objects[o].beadtype) utypes = list(set(typlist)) c = [] for t in utypes: c.append((t,typlist.count(t))) return c
def data(self, scale=(1, 1), center=(0, 0), maxtype=None, hexpacking=None)
-
return a pizza.data object data() data(scale=(scalex,scaley), center=(centerx,centery), maxtype=number, hexpacking=(0.5,0))
Expand source code
def data(self,scale=(1,1),center=(0,0),maxtype=None,hexpacking=None): """ return a pizza.data object data() data(scale=(scalex,scaley), center=(centerx,centery), maxtype=number, hexpacking=(0.5,0)) """ if not isinstance(scale,tuple) or len(scale)!=2: raise ValueError("scale must be tuple (scalex,scaley)") if not isinstance(center,tuple) or len(scale)!=2: raise ValueError("center must be tuple (centerx,centery)") scalez = np.sqrt(scale[0]*scale[1]) scalevol = scale[0]*scale[1] #*scalez maxtypeheader = self.count()[-1][0] if maxtype is None else maxtype n = self.length() i,j = self.imbead.nonzero() # x=j+0.5 y=i+0.5 x = (j+0.5-center[0])*scale[0] y = (i+0.5-center[1])*scale[1] if hexpacking is not None: if isinstance(hexpacking,tuple) and len(hexpacking)==2: for k in range(len(i)): if i[k] % 2: x[k] = (j[k]+0.5+hexpacking[1]-center[0])*scale[0] else: x[k] = (j[k]+0.5+hexpacking[0]-center[0])*scale[0] else: raise ValueError("hexpacking should be a tuple (shiftodd,shifteven)") X = data3() # empty pizza.data3.data object X.title = self.name + "(raster)" X.headers = {'atoms': n, 'atom types': maxtypeheader, 'xlo xhi': ((0.0-center[0])*scale[0], (self.width-0.0-center[0])*scale[0]), 'ylo yhi': ((0.0-center[1])*scale[1], (self.height-0.0-center[1])*scale[1]), 'zlo zhi': (0, scalez)} # [ATOMS] section X.append('Atoms',list(range(1,n+1)),True,"id") # id X.append('Atoms',self.imbead[i,j],True,"type") # Type X.append('Atoms',1,True,"mol") # mol X.append('Atoms',self.volume*scalevol,False,"c_vol") # c_vol X.append('Atoms',self.mass*scalevol,False,"mass") # mass X.append('Atoms',self.radius*scalez,False,"radius") # radius X.append('Atoms',self.contactradius*scalez,False,"c_contact_radius") # c_contact_radius X.append('Atoms',x,False,"x") # x X.append('Atoms',y,False,"y") # y X.append('Atoms',0,False,"z") # z X.append('Atoms',x,False,"x0") # x0 X.append('Atoms',y,False,"y0") # y0 X.append('Atoms',0,False,"z0") # z0 # [VELOCITIES] section X.append('Velocities',list(range(1,n+1)),True,"id") # id X.append('Velocities',self.velocities[0],False,"vx") # vx X.append('Velocities',self.velocities[1],False,"vy") # vy X.append('Velocities',self.velocities[2],False,"vz") # vz # pseudo-filename X.flist = self.filename return X
def delete(self, name)
-
delete object
Expand source code
def delete(self,name): """ delete object """ if name in self.objects: if not self.objects[name].ismask: self.nbeads -= self.objects[name].nbeads del self.objects[name] self.nobjects -= 1 else: raise ValueError("%d is not a valid name (use list()) to list valid objects" % name) self.clear() self.plot() self.show(extra="label")
def diamond(self, xc, yc, radius, name=None, shaperatio=1, angle=0, beadtype=None, ismask=False, shiftangle=0, fake=False, beadtype2=None)
-
diamond object diamond(xcenter,ycenter,radius [, beadtype=1,shaperatio=1, angle=0, ismask=False] use diamond(…,beadtype2=(type,ratio)) to salt an object with beads from another type and with a given ratio
Expand source code
def diamond(self,xc,yc,radius,name=None, shaperatio=1,angle=0,beadtype=None,ismask=False,shiftangle=0,fake=False,beadtype2=None): """ diamond object diamond(xcenter,ycenter,radius [, beadtype=1,shaperatio=1, angle=0, ismask=False] use diamond(...,beadtype2=(type,ratio)) to salt an object with beads from another type and with a given ratio """ self.circle(xc,yc,radius,name=name,shaperatio=shaperatio,resolution=4, angle=angle,beadtype=beadtype,ismask=ismask,shiftangle=0,fake=fake,beadtype2=beadtype2)
def exist(self, name)
-
exist object
Expand source code
def exist(self,name): """ exist object """ return name in self.objects
def figure(self)
-
set the current figure
Expand source code
def figure(self): """ set the current figure """ if self.hfig==[] or not plt.fignum_exists(self.hfig.number): self.newfigure() plt.figure(self.hfig.number)
def frameobj(self, obj)
-
frame coordinates by taking into account translation
Expand source code
def frameobj(self,obj): """ frame coordinates by taking into account translation """ if obj.hasclosefit: envelope = 0 else: envelope = 1 xmin, ymin = self.valid(obj.xmin-envelope, obj.ymin-envelope) xmax, ymax = self.valid(obj.xmax+envelope, obj.ymax+envelope) return xmin, ymin, xmax, ymax
def get(self, name)
-
returns the object
Expand source code
def get(self,name): """ returns the object """ if name in self.objects: return self.objects[name] else: raise ValueError('the object "%s" does not exist, use list()' % name)
def hexagon(self, xc, yc, radius, name=None, shaperatio=1, angle=0, beadtype=None, ismask=False, shiftangle=0, fake=False, beadtype2=None)
-
hexagon object hexagon(xcenter,ycenter,radius [, beadtype=1,shaperatio=1, angle=0, ismask=False] use hexagon(…,beadtype2=(type,ratio)) to salt an object with beads from another type and with a given ratio
Expand source code
def hexagon(self,xc,yc,radius,name=None, shaperatio=1,angle=0,beadtype=None,ismask=False,shiftangle=0,fake=False,beadtype2=None): """ hexagon object hexagon(xcenter,ycenter,radius [, beadtype=1,shaperatio=1, angle=0, ismask=False] use hexagon(...,beadtype2=(type,ratio)) to salt an object with beads from another type and with a given ratio """ self.circle(xc,yc,radius,name=name,shaperatio=shaperatio,resolution=6, angle=angle,beadtype=beadtype,ismask=ismask,shiftangle=0,fake=fake,beadtype2=beadtype2)
def label(self, name, **fmt)
-
label: label(name [, contour=True,edgecolor="orange",facecolor="none",linewidth=2, ax=plt.gca()])
Expand source code
def label(self,name,**fmt): """ label: label(name [, contour=True,edgecolor="orange",facecolor="none",linewidth=2, ax=plt.gca()]) """ self.figure() if name in self.objects: if not self.objects[name].islabelled: if self.objects[name].alike == "mixed": for o in self.objects[name].collection: self.labelobj(o,**fmt) else: self.labelobj(self.objects[name],**fmt) plt.show() self.objects[name].islabelled = True else: raise ValueError("%d is not a valid name (use list()) to list valid objects" % name)
def labelobj(self, obj, contour=True, edgecolor='orange', facecolor='none', linewidth=2, ax=<Axes: >)
-
labelobj: labelobj(obj [, contour=True,edgecolor="orange",facecolor="none",linewidth=2, ax=plt.gca()])
Expand source code
def labelobj(self,obj,contour=True,edgecolor="orange",facecolor="none",linewidth=2,ax=plt.gca()): """ labelobj: labelobj(obj [, contour=True,edgecolor="orange",facecolor="none",linewidth=2, ax=plt.gca()]) """ if contour: contour = obj.hascontour # e.g. overlays do not have contour if contour: patch = patches.PathPatch(obj.polygon2plot, facecolor=facecolor, edgecolor=edgecolor, lw=linewidth) obj.hlabel["contour"] = ax.add_patch(patch) else: obj.hlabel["contour"] = None obj.hlabel["text"] = \ plt.text(obj.xcenter, obj.ycenter, "%s\n(t=$%d$,$n_p$=%d)" % (obj.name, obj.beadtype,obj.nbeads), horizontalalignment = "center", verticalalignment = "center_baseline", fontsize=self.fontsize )
def length(self, t=None, what='beadtype')
-
returns the total number of beads length(type,"beadtype")
Expand source code
def length(self,t=None,what="beadtype"): """ returns the total number of beads length(type,"beadtype") """ if what == "beadtype": num = self.imbead elif what == "objindex": num = self.imobj else: raise ValueError('"beadtype" and "objindex" are the only acceptable values') if t==None: return np.count_nonzero(num>0) else: return np.count_nonzero(num==t)
def list(self)
-
list objects
Expand source code
def list(self): """ list objects """ fmt = "%%%ss:" % max(10,max([len(n) for n in self.names()])+2) print("RASTER with %d objects" % self.nobjects) for o in self.objects.keys(): print(fmt % self.objects[o].name,"%-10s" % self.objects[o].kind, "(beadtype=%d,object index=[%d,%d], n=%d)" % \ (self.objects[o].beadtype, self.objects[o].index, self.objects[o].subindex, self.objects[o].nbeads))
def names(self)
-
return the names of objects sorted as index
Expand source code
def names(self): """ return the names of objects sorted as index """ namesunsorted=namessorted=list(self.objects.keys()) nobj = len(namesunsorted) for iobj in range(nobj): namessorted[self.objects[namesunsorted[iobj]].index-1] = namesunsorted[iobj] return namessorted
def newfigure(self)
-
create a new figure (dpi=200)
Expand source code
def newfigure(self): """ create a new figure (dpi=200) """ self.hfig = plt.figure(dpi=self.dpi)
def numeric(self)
-
retrieve the image as a numpy.array
Expand source code
def numeric(self): """ retrieve the image as a numpy.array """ return self.imbead, self.imobj
def overlay(self, x0, y0, name=None, filename=None, color=1, colormax=None, ncolors=4, beadtype=None, beadtype2=None, ismask=False, fake=False, flipud=True, angle=0, scale=(1, 1))
-
overlay object: made from an image converted to nc colors the object is made from the level ranged between ic and jc (bounds included) note: if palette found, no conversion is applied
O = overlay(x0,y0,filename="/this/is/my/image.png",ncolors=nc,color=ic,colormax=jc,beadtype=b) O = overlay(…angle=0,scale=(1,1)) to induce rotation and change of scale O = overlay(....ismask=False,fake=False)
note use overlay(…flipud=False) to prevent image fliping (standard)
Outputs
O.original original image (PIL) O.raw image converted to ncolors if needed
Expand source code
def overlay(self,x0,y0, name = None, filename = None, color = 1, colormax = None, ncolors = 4, beadtype = None, beadtype2 = None, ismask = False, fake = False, flipud = True, angle = 0, scale= (1,1) ): """ overlay object: made from an image converted to nc colors the object is made from the level ranged between ic and jc (bounds included) note: if palette found, no conversion is applied O = overlay(x0,y0,filename="/this/is/my/image.png",ncolors=nc,color=ic,colormax=jc,beadtype=b) O = overlay(...angle=0,scale=(1,1)) to induce rotation and change of scale O = overlay(....ismask=False,fake=False) note use overlay(...flipud=False) to prevent image fliping (standard) Outputs: O.original original image (PIL) O.raw image converted to ncolors if needed """ if filename is None or filename=="": raise ValueError("filename is required (valid image)") O = overlay(counter=(self.counter["all"]+1,self.counter["overlay"]+1), filename = filename, xmin = x0, ymin = y0, ncolors = ncolors, flipud = flipud, angle = angle, scale = scale ) O.select(color=color, colormax=colormax) if (name is not None) and (name !=""): if self.exist(name): print('RASTER:: the object "%s" is overwritten',name) self.delete(name) O.name = name else: name = O.name if beadtype is not None: O.beadtype = int(np.floor(beadtype)) if beadtype2 is not None: if not isinstance(beadtype2,tuple) or len(beadtype2)!=2: raise AttributeError("beadtype2 must be a tuple (beadtype,ratio)") O.beadtype2 = beadtype2 if ismask: O.beadtype = 0 O.ismask = O.beadtype==0 self.counter["all"] += 1 self.counter["overlay"] += 1 if fake: self.counter["all"] -= 1 self.counter["overlay"] -= 1 return O else: self.objects[name] = O self.nobjects += 1 return None
def pentagon(self, xc, yc, radius, name=None, shaperatio=1, angle=0, beadtype=None, ismask=False, shiftangle=0, fake=False, beadtype2=None)
-
pentagon object pentagon(xcenter,ycenter,radius [, beadtype=1,shaperatio=1, angle=0, ismask=False] use pentagon(…,beadtype2=(type,ratio)) to salt an object with beads from another type and with a given ratio
Expand source code
def pentagon(self,xc,yc,radius,name=None, shaperatio=1,angle=0,beadtype=None,ismask=False,shiftangle=0,fake=False,beadtype2=None): """ pentagon object pentagon(xcenter,ycenter,radius [, beadtype=1,shaperatio=1, angle=0, ismask=False] use pentagon(...,beadtype2=(type,ratio)) to salt an object with beads from another type and with a given ratio """ self.circle(xc,yc,radius,name=name,shaperatio=shaperatio,resolution=5, angle=angle,beadtype=beadtype,ismask=ismask,shiftangle=0,fake=fake,beadtype2=beadtype2)
def plot(self)
-
plot
Expand source code
def plot(self): """ plot """ for o in self.objects: if not self.objects[o].isplotted: if self.objects[o].alike == "mixed": for o2 in self.objects[o].collection: self.plotobj(o2) else: self.plotobj(self.objects[o]) # store it as plotted self.objects[o].isplotted = True if not self.objects[o].ismask: self.nbeads += self.objects[o].nbeads
def plotobj(self, obj)
-
plotobj(obj)
Expand source code
def plotobj(self,obj): """ plotobj(obj) """ if obj.alike == "circle": xmin, ymin, xmax, ymax = self.frameobj(obj) j,i = np.meshgrid(range(xmin,xmax), range(ymin,ymax)) points = np.vstack((j.flatten(),i.flatten())).T npoints = points.shape[0] inside = obj.polygon.contains_points(points) if obj.beadtype2 is None: # -- no salting -- for k in range(npoints): if inside[k] and \ points[k,0]>=0 and \ points[k,0]<self.width and \ points[k,1]>=0 and \ points[k,1]<self.height: self.imbead[points[k,1],points[k,0]] = obj.beadtype self.imobj[points[k,1],points[k,0]] = obj.index obj.nbeads += 1 else: for k in range(npoints): # -- salting -- if inside[k] and \ points[k,0]>=0 and \ points[k,0]<self.width and \ points[k,1]>=0 and \ points[k,1]<self.height: if np.random.rand()<obj.beadtype2[1]: self.imbead[points[k,1],points[k,0]] = obj.beadtype2[0] else: self.imbead[points[k,1],points[k,0]] = obj.beadtype self.imobj[points[k,1],points[k,0]] = obj.index obj.nbeads += 1 elif obj.alike == "overlay": xmin, ymin, xmax, ymax = self.frameobj(obj) j,i = np.meshgrid(range(xmin,xmax), range(ymin,ymax)) points = np.vstack((j.flatten(),i.flatten())).T npoints = points.shape[0] inside = obj.select() if obj.beadtype2 is None: # -- no salting -- for k in range(npoints): if inside[ points[k,1]-ymin, points[k,0]-xmin ] and \ points[k,0]>=0 and \ points[k,0]<self.width and \ points[k,1]>=0 and \ points[k,1]<self.height: self.imbead[points[k,1],points[k,0]] = obj.beadtype self.imobj[points[k,1],points[k,0]] = obj.index obj.nbeads += 1 else: for k in range(npoints): # -- salting -- if inside[ points[k,0]-ymin, points[k,0]-xmin ] and \ points[k,0]>=0 and \ points[k,0]<self.width and \ points[k,1]>=0 and \ points[k,1]<self.height: if np.random.rand()<obj.beadtype2[1]: self.imbead[points[k,1],points[k,0]] = obj.beadtype2[0] else: self.imbead[points[k,1],points[k,0]] = obj.beadtype self.imobj[points[k,1],points[k,0]] = obj.index obj.nbeads += 1 else: raise ValueError("This object type is notimplemented")
def print(self, what='beadtype')
-
print method
Expand source code
def print(self,what="beadtype"): """ print method """ txt = self.string(what=what) for i in range(len(txt)): print(txt[i],end="\n")
def rectangle(self, a, b, c, d, mode='lowerleft', name=None, angle=0, beadtype=None, ismask=False, fake=False, beadtype2=None)
-
rectangle object rectangle(xleft,xright,ybottom,ytop [, beadtype=1,mode="lower", angle=0, ismask=False]) rectangle(xcenter,ycenter,width,height [, beadtype=1,mode="center", angle=0, ismask=False])
use rectangle(...,beadtype2=(type,ratio)) to salt an object with beads from another type and with a given ratio
Expand source code
def rectangle(self,a,b,c,d, mode="lowerleft",name=None,angle=0, beadtype=None,ismask=False,fake=False,beadtype2=None): """ rectangle object rectangle(xleft,xright,ybottom,ytop [, beadtype=1,mode="lower", angle=0, ismask=False]) rectangle(xcenter,ycenter,width,height [, beadtype=1,mode="center", angle=0, ismask=False]) use rectangle(...,beadtype2=(type,ratio)) to salt an object with beads from another type and with a given ratio """ # object creation self.counter["all"] += 1 self.counter["rectangle"] += 1 R = Rectangle((self.counter["all"],self.counter["rectangle"])) if (name != None) and (name != ""): if self.exist(name): print('RASTER:: the object "%s" is overwritten',name) self.delete(name) R.name = name else: name = R.name if beadtype is not None: R.beadtype = int(np.floor(beadtype)) if beadtype2 is not None: if not isinstance(beadtype2,tuple) or len(beadtype2)!=2: raise AttributeError("beadtype2 must be a tuple (beadtype,ratio)") R.beadtype2 = beadtype2 if ismask: R.beadtype = 0 R.ismask = R.beadtype==0 # build vertices if mode == "lowerleft": R.xcenter0 = (a+b)/2 R.ycenter0 = (c+d)/2 R.vertices = [ _rotate(a,c,R.xcenter0,R.ycenter0,angle), _rotate(b,c,R.xcenter0,R.ycenter0,angle), _rotate(b,d,R.xcenter0,R.ycenter0,angle), _rotate(a,d,R.xcenter0,R.ycenter0,angle), _rotate(a,c,R.xcenter0,R.ycenter0,angle) ] # anti-clockwise, closed (last point repeated) elif mode == "center": R.xcenter0 = a R.ycenter0 = b R.vertices = [ _rotate(a-c/2,b-d/2,R.xcenter0,R.ycenter0,angle), _rotate(a+c/2,b-d/2,R.xcenter0,R.ycenter0,angle), _rotate(a+c/2,b+d/2,R.xcenter0,R.ycenter0,angle), _rotate(a-c/2,b+d/2,R.xcenter0,R.ycenter0,angle), _rotate(a-c/2,b-d/2,R.xcenter0,R.ycenter0,angle) ] else: raise ValueError('"%s" is not a recognized mode, use "lowerleft" (default) and "center" instead') # build path object and range R.codes = [ path.Path.MOVETO, path.Path.LINETO, path.Path.LINETO, path.Path.LINETO, path.Path.CLOSEPOLY ] R.nvertices = len(R.vertices)-1 R.xmin0, R.ymin0, R.xmax0, R.ymax0 = R.corners() R.xmin0, R.ymin0 = self.valid(R.xmin0,R.ymin0) R.xmax0, R.ymax0 = self.valid(R.xmax0,R.ymax0) R.angle = angle # store the object (if not fake) if fake: self.counter["all"] -= 1 self.counter["rectangle"] -= 1 return R else: self.objects[name] = R self.nobjects += 1 return None
def scatter(self, E, name='emulsion', beadtype=None, ismask=False)
-
Parameters
E
:scatter
oremulsion object
- codes for x,y and r.
name
:string
, optional- name of the collection. The default is "emulsion".
beadtype
:integer
, optional- for all objects. The default is 1.
ismask
:logical
, optional- Set it to true to force a mask. The default is False.
Raises
TypeError
- Return an error of the object is not a scatter type.
Returns
None.
Expand source code
def scatter(self, E, name="emulsion", beadtype=None, ismask = False ): """ Parameters ---------- E : scatter or emulsion object codes for x,y and r. name : string, optional name of the collection. The default is "emulsion". beadtype : integer, optional for all objects. The default is 1. ismask : logical, optional Set it to true to force a mask. The default is False. Raises ------ TypeError Return an error of the object is not a scatter type. Returns ------- None. """ if isinstance(E,scatter): collect = {} for i in range(E.n): b = E.beadtype[i] if beadtype==None else beadtype nameobj = "glob%02d" % i collect[nameobj] = self.circle(E.x[i],E.y[i],E.r[i], name=nameobj,beadtype=b,ismask=ismask,fake=True) self.collection(**collect,name=name) else: raise TypeError("the first argument must be an emulsion object")
def show(self, extra='none', contour=True, what='beadtype')
-
show method: show(extra="label",contour=True,what="beadtype")
Expand source code
def show(self,extra="none",contour=True,what="beadtype"): """ show method: show(extra="label",contour=True,what="beadtype") """ self.figure() if what=="beadtype": imagesc(self.imbead) elif what == "objindex": imagesc(self.imobj) else: raise ValueError('"beadtype" and "objindex" are the only acceptable values') if extra == "label": ax = plt.gca() for o in self.names(): if not self.objects[o].ismask: self.label(o,ax=ax,contour=contour) ax.set_title("raster area: %s \n (n=%d, $n_p$=%d)" %\ (self.name,self.length(),self.nbeads) ) plt.show()
def string(self, what='beadtype')
-
convert the image as ASCII strings
Expand source code
def string(self,what="beadtype"): """ convert the image as ASCII strings """ if what == "beadtype": num = np.flipud(duplicate(self.imbead)) elif what == "objindex": num = np.flipud(duplicate(self.imobj)) else: raise ValueError('"beadtype" and "objindex" are the only acceptable values') num[num>0] = num[num>0] + 65 num[num==0] = 32 num = list(num) return ["".join(map(chr,x)) for x in num]
def torgb(self, what='beadtype', thumb=None)
-
converts bead raster to image rgb = raster.torgb(what="beadtype") thumbnail = raster.torgb(what="beadtype",(128,128)) use: rgb.save("/path/filename.png") for saving
what = "beadtype" or "objindex"
Expand source code
def torgb(self,what="beadtype",thumb=None): """ converts bead raster to image rgb = raster.torgb(what="beadtype") thumbnail = raster.torgb(what="beadtype",(128,128)) use: rgb.save("/path/filename.png") for saving what = "beadtype" or "objindex" """ if what=="beadtype": rgb = ind2rgb(self.imbead,ncolors=self.max+1) elif what == "objindex": rgb = ind2rgb(self.imobj,ncolors=len(self)+1) else: raise ValueError('"beadtype" and "objindex" are the only acceptable values') if thumb is not None: rgb.thumbnail(thumb) return rgb
def triangle(self, xc, yc, radius, name=None, shaperatio=1, angle=0, beadtype=None, ismask=False, shiftangle=0, fake=False, beadtype2=None)
-
triangle object triangle(xcenter,ycenter,radius [, beadtype=1,shaperatio=1, angle=0, ismask=False] use triangle(…,beadtype2=(type,ratio)) to salt an object with beads from another type and with a given ratio
Expand source code
def triangle(self,xc,yc,radius,name=None, shaperatio=1,angle=0,beadtype=None,ismask=False,shiftangle=0,fake=False,beadtype2=None): """ triangle object triangle(xcenter,ycenter,radius [, beadtype=1,shaperatio=1, angle=0, ismask=False] use triangle(...,beadtype2=(type,ratio)) to salt an object with beads from another type and with a given ratio """ self.circle(xc,yc,radius,name=name,shaperatio=shaperatio,resolution=3, angle=angle,beadtype=beadtype,ismask=ismask,shiftangle=0,fake=fake,beadtype2=beadtype2)
def unlabel(self, name)
-
unlabel
Expand source code
def unlabel(self,name): """ unlabel """ if name in self.objects: if self.objects[name].islabelled: self.objects[name].hlabel["contour"].remove() self.objects[name].hlabel["text"].remove() self.objects[name].hlabel = {'contour':[], 'text':[]} self.objects[name].islabelled = False else: raise ValueError("%d is not a valid name (use list()) to list valid objects" % name)
def valid(self, x, y)
-
validation of coordinates
Expand source code
def valid(self,x,y): """ validation of coordinates """ return min(self.width,max(0,round(x))),min(self.height,max(0,round(y)))
class scatter
-
generic top scatter class
The scatter class provides an easy constructor to distribute in space objects according to their positions x,y, size r (radius) and beadtype.
The class is used to derive emulsions.
Returns
None.
Expand source code
class scatter(): """ generic top scatter class """ def __init__(self): """ The scatter class provides an easy constructor to distribute in space objects according to their positions x,y, size r (radius) and beadtype. The class is used to derive emulsions. Returns ------- None. """ self.x = np.array([],dtype=int) self.y = np.array([],dtype=int) self.r = np.array([],dtype=int) self.beadtype = [] @property def n(self): return len(self.x) def pairdist(self,x,y): """ pair distance to the surface of all disks/spheres """ if self.n==0: return np.Inf else: return np.floor(np.sqrt((x-self.x)**2+(y-self.y)**2)-self.r)
Subclasses
Instance variables
var n
-
Expand source code
@property def n(self): return len(self.x)
Methods
def pairdist(self, x, y)
-
pair distance to the surface of all disks/spheres
Expand source code
def pairdist(self,x,y): """ pair distance to the surface of all disks/spheres """ if self.n==0: return np.Inf else: return np.floor(np.sqrt((x-self.x)**2+(y-self.y)**2)-self.r)
class struct (debug=False, **kwargs)
-
Class:
struct
A lightweight class that mimics Matlab-like structures, with additional features such as dynamic field creation, indexing, concatenation, and compatibility with evaluated parameters (
param
).
Features
- Dynamic creation of fields.
- Indexing and iteration support for fields.
- Concatenation and subtraction of structures.
- Conversion to and from dictionaries.
- Compatible with
param
andparamauto
for evaluation and dependency handling.
Examples
Basic Usage
s = struct(a=1, b=2, c='${a} + ${b} # evaluate me if you can') print(s.a) # 1 s.d = 11 # Append a new field delattr(s, 'd') # Delete the field
Using
param
for Evaluationp = param(a=1, b=2, c='${a} + ${b} # evaluate me if you can') p.eval() # Output: # -------- # a: 1 # b: 2 # c: ${a} + ${b} # evaluate me if you can (= 3) # --------
Concatenation and Subtraction
Fields from the right-most structure overwrite existing values.
a = struct(a=1, b=2) b = struct(c=3, d="d", e="e") c = a + b e = c - a
Practical Shorthands
Constructing a Structure from Keys
s = struct.fromkeys(["a", "b", "c", "d"]) # Output: # -------- # a: None # b: None # c: None # d: None # --------
Building a Structure from Variables in a String
s = struct.scan("${a} + ${b} * ${c} / ${d} --- ${ee}") s.a = 1 s.b = "test" s.c = [1, "a", 2] s.generator() # Output: # X = struct( # a=1, # b="test", # c=[1, 'a', 2], # d=None, # ee=None # )
Indexing and Iteration
Structures can be indexed or sliced like lists.
c = a + b c[0] # Access the first field c[-1] # Access the last field c[:2] # Slice the structure for field in c: print(field)
Dynamic Dependency Management
struct
provides control over dependencies, sorting, and evaluation.s = struct(d=3, e="${c} + {d}", c='${a} + ${b}', a=1, b=2) s.sortdefinitions() # Output: # -------- # d: 3 # a: 1 # b: 2 # c: ${a} + ${b} # e: ${c} + ${d} # --------
For dynamic evaluation, use
param
:p = param(sortdefinitions=True, d=3, e="${c} + ${d}", c='${a} + ${b}', a=1, b=2) # Output: # -------- # d: 3 # a: 1 # b: 2 # c: ${a} + ${b} (= 3) # e: ${c} + ${d} (= 6) # --------
Overloaded Methods and Operators
Supported Operators
+
: Concatenation of two structures (__add__
).-
: Subtraction of fields (__sub__
).<<
: Import values from another structure (__lshift__
)len()
: Number of fields (__len__
).in
: Check for field existence (__contains__
).
Method Overview
Method Description check(default)
Populate fields with defaults if missing. clear()
Remove all fields. dict2struct(dico)
Create a structure from a dictionary. disp()
Display the structure. eval()
Evaluate expressions within fields. fromkeys(keys)
Create a structure from a list of keys. generator()
Generate Python code representing the structure. importfrom()
Import undefined values from another struct or dict. items()
Return key-value pairs. keys()
Return all keys in the structure. read(file)
Load structure fields from a file. scan(string)
Extract variables from a string and populate fields. sortdefinitions()
Sort fields to resolve dependencies. struct2dict()
Convert the structure to a dictionary. validkeys()
Return valid keys values()
Return all field values. write(file)
Save the structure to a file.
Dynamic Properties
Property Description isempty
True
if the structure is empty.isdefined
True
if all fields are defined.
constructor, use debug=True to report eval errors
Expand source code
class struct(): """ Class: `struct` ================ A lightweight class that mimics Matlab-like structures, with additional features such as dynamic field creation, indexing, concatenation, and compatibility with evaluated parameters (`param`). --- ### Features - Dynamic creation of fields. - Indexing and iteration support for fields. - Concatenation and subtraction of structures. - Conversion to and from dictionaries. - Compatible with `param` and `paramauto` for evaluation and dependency handling. --- ### Examples #### Basic Usage ```python s = struct(a=1, b=2, c='${a} + ${b} # evaluate me if you can') print(s.a) # 1 s.d = 11 # Append a new field delattr(s, 'd') # Delete the field ``` #### Using `param` for Evaluation ```python p = param(a=1, b=2, c='${a} + ${b} # evaluate me if you can') p.eval() # Output: # -------- # a: 1 # b: 2 # c: ${a} + ${b} # evaluate me if you can (= 3) # -------- ``` --- ### Concatenation and Subtraction Fields from the right-most structure overwrite existing values. ```python a = struct(a=1, b=2) b = struct(c=3, d="d", e="e") c = a + b e = c - a ``` --- ### Practical Shorthands #### Constructing a Structure from Keys ```python s = struct.fromkeys(["a", "b", "c", "d"]) # Output: # -------- # a: None # b: None # c: None # d: None # -------- ``` #### Building a Structure from Variables in a String ```python s = struct.scan("${a} + ${b} * ${c} / ${d} --- ${ee}") s.a = 1 s.b = "test" s.c = [1, "a", 2] s.generator() # Output: # X = struct( # a=1, # b="test", # c=[1, 'a', 2], # d=None, # ee=None # ) ``` #### Indexing and Iteration Structures can be indexed or sliced like lists. ```python c = a + b c[0] # Access the first field c[-1] # Access the last field c[:2] # Slice the structure for field in c: print(field) ``` --- ### Dynamic Dependency Management `struct` provides control over dependencies, sorting, and evaluation. ```python s = struct(d=3, e="${c} + {d}", c='${a} + ${b}', a=1, b=2) s.sortdefinitions() # Output: # -------- # d: 3 # a: 1 # b: 2 # c: ${a} + ${b} # e: ${c} + ${d} # -------- ``` For dynamic evaluation, use `param`: ```python p = param(sortdefinitions=True, d=3, e="${c} + ${d}", c='${a} + ${b}', a=1, b=2) # Output: # -------- # d: 3 # a: 1 # b: 2 # c: ${a} + ${b} (= 3) # e: ${c} + ${d} (= 6) # -------- ``` --- ### Overloaded Methods and Operators #### Supported Operators - `+`: Concatenation of two structures (`__add__`). - `-`: Subtraction of fields (`__sub__`). - `<<`: Import values from another structure (`__lshift__`) - `len()`: Number of fields (`__len__`). - `in`: Check for field existence (`__contains__`). #### Method Overview | Method | Description | |-----------------------|---------------------------------------------------------| | `check(default)` | Populate fields with defaults if missing. | | `clear()` | Remove all fields. | | `dict2struct(dico)` | Create a structure from a dictionary. | | `disp()` | Display the structure. | | `eval()` | Evaluate expressions within fields. | | `fromkeys(keys)` | Create a structure from a list of keys. | | `generator()` | Generate Python code representing the structure. | | `importfrom()` | Import undefined values from another struct or dict. | | `items()` | Return key-value pairs. | | `keys()` | Return all keys in the structure. | | `read(file)` | Load structure fields from a file. | | `scan(string)` | Extract variables from a string and populate fields. | | `sortdefinitions()` | Sort fields to resolve dependencies. | | `struct2dict()` | Convert the structure to a dictionary. | | `validkeys()` | Return valid keys | | `values()` | Return all field values. | | `write(file)` | Save the structure to a file. | --- ### Dynamic Properties | Property | Description | |-------------|----------------------------------------| | `isempty` | `True` if the structure is empty. | | `isdefined` | `True` if all fields are defined. | --- """ # attributes to be overdefined _type = "struct" # object type _fulltype = "structure" # full name _ftype = "field" # field name _evalfeature = False # true if eval() is available _maxdisplay = 40 # maximum number of characters to display (should be even) _propertyasattribute = False _precision = 4 _needs_sorting = False # attributes for the iterator method # Please keep it static, duplicate the object before changing _iter_ _iter_ = 0 # excluded attributes (keep the , in the Tupple if it is singleton) _excludedattr = {'_iter_','__class__','_protection','_evaluation','_returnerror','_debug','_precision','_needs_sorting'} # used by keys() and len() # Methods def __init__(self,debug=False,**kwargs): """ constructor, use debug=True to report eval errors""" # Optionally extend _excludedattr here self._excludedattr = self._excludedattr | {'_excludedattr', '_type', '_fulltype','_ftype'} # addition 2024-10-11 self._debug = debug self.set(**kwargs) def zip(self): """ zip keys and values """ return zip(self.keys(),self.values()) @staticmethod def dict2struct(dico,makeparam=False): """ create a structure from a dictionary """ if isinstance(dico,dict): s = param() if makeparam else struct() s.set(**dico) return s raise TypeError("the argument must be a dictionary") def struct2dict(self): """ create a dictionary from the current structure """ return dict(self.zip()) def struct2param(self,protection=False,evaluation=True): """ convert an object struct() to param() """ p = param(**self.struct2dict()) for i in range(len(self)): if isinstance(self[i],pstr): p[i] = pstr(p[i]) p._protection = protection p._evaluation = evaluation return p def set(self,**kwargs): """ initialization """ self.__dict__.update(kwargs) def setattr(self,key,value): """ set field and value """ if isinstance(value,list) and len(value)==0 and key in self: delattr(self, key) else: self.__dict__[key] = value def getattr(self,key): """Get attribute override to access both instance attributes and properties if allowed.""" if key in self.__dict__: return self.__dict__[key] elif getattr(self, '_propertyasattribute', False) and \ key not in self._excludedattr and \ key in self.__class__.__dict__ and isinstance(self.__class__.__dict__[key], property): # If _propertyasattribute is True and it's a property, get its value return self.__class__.__dict__[key].fget(self) else: raise AttributeError(f'the {self._ftype} "{key}" does not exist') def hasattr(self, key): """Return true if the field exists, considering properties as regular attributes if allowed.""" return key in self.__dict__ or ( getattr(self, '_propertyasattribute', False) and key not in self._excludedattr and key in self.__class__.__dict__ and isinstance(self.__class__.__dict__[key], property) ) def __getstate__(self): """ getstate for cooperative inheritance / duplication """ return self.__dict__.copy() def __setstate__(self,state): """ setstate for cooperative inheritance / duplication """ self.__dict__.update(state) def __getattr__(self,key): """ get attribute override """ return pstr.eval(self.getattr(key)) def __setattr__(self,key,value): """ set attribute override """ self.setattr(key,value) def __contains__(self,item): """ in override """ return self.hasattr(item) def keys(self): """ return the fields """ # keys() is used by struct() and its iterator return [key for key in self.__dict__.keys() if key not in self._excludedattr] def keyssorted(self,reverse=True): """ sort keys by length() """ klist = self.keys() l = [len(k) for k in klist] return [k for _,k in sorted(zip(l,klist),reverse=reverse)] def values(self): """ return the values """ # values() is used by struct() and its iterator return [pstr.eval(value) for key,value in self.__dict__.items() if key not in self._excludedattr] @staticmethod def fromkeysvalues(keys,values,makeparam=False): """ struct.keysvalues(keys,values) creates a structure from keys and values use makeparam = True to create a param instead of struct """ if keys is None: raise AttributeError("the keys must not empty") if not isinstance(keys,_list_types): keys = [keys] if not isinstance(values,_list_types): values = [values] nk,nv = len(keys), len(values) s = param() if makeparam else struct() if nk>0 and nv>0: iv = 0 for ik in range(nk): s.setattr(keys[ik], values[iv]) iv = min(nv-1,iv+1) for ik in range(nk,nv): s.setattr(f"key{ik}", values[ik]) return s def items(self): """ return all elements as iterable key, value """ return self.zip() def __getitem__(self,idx): """ s[i] returns the ith element of the structure s[:4] returns a structure with the four first fields s[[1,3]] returns the second and fourth elements """ if isinstance(idx,int): if idx<len(self): return self.getattr(self.keys()[idx]) raise IndexError(f"the {self._ftype} index should be comprised between 0 and {len(self)-1}") elif isinstance(idx,slice): out = struct.fromkeysvalues(self.keys()[idx], self.values()[idx]) if isinstance(self,paramauto): return paramauto(**out) elif isinstance(self,param): return param(**out) else: return out elif isinstance(idx,(list,tuple)): k,v= self.keys(), self.values() nk = len(k) s = param() if isinstance(self,param) else struct() for i in idx: if isinstance(i,int): if -nk <= i < nk: # Allow standard Python negative indexing i = i % nk # Convert negative index to positive equivalent s.setattr(k[i],v[i]) else: raise IndexError(f"idx must contain integers in range [-{nk}, {nk-1}], not {i}") elif isinstance(i,str): if i in self: s.setattr(i, self.getattr(i)) else: raise KeyError((f'idx "{idx}" is not a valid key')) else: TypeError("idx must contain only integers or strings") return s elif isinstance(idx,str): return self.getattr(idx) else: raise TypeError("The index must be an integer or a slice and not a %s" % type(idx).__name__) def __setitem__(self,idx,value): """ set the ith element of the structure """ if isinstance(idx,int): if idx<len(self): self.setattr(self.keys()[idx], value) else: raise IndexError(f"the {self._ftype} index should be comprised between 0 and {len(self)-1}") elif isinstance(idx,slice): k = self.keys()[idx] if len(value)<=1: for i in range(len(k)): self.setattr(k[i], value) elif len(k) == len(value): for i in range(len(k)): self.setattr(k[i], value[i]) else: raise IndexError("the number of values (%d) does not match the number of elements in the slice (%d)" \ % (len(value),len(idx))) elif isinstance(idx,(list,tuple)): if len(value)<=1: for i in range(len(idx)): self[idx[i]]=value elif len(idx) == len(value): for i in range(len(idx)): self[idx[i]]=value[i] else: raise IndexError("the number of values (%d) does not match the number of indices (%d)" \ % (len(value),len(idx))) def __len__(self): """ return the number of fields """ # len() is used by struct() and its iterator return len(self.keys()) def __iter__(self): """ struct iterator """ # note that in the original object _iter_ is a static property not in dup dup = duplicate(self) dup._iter_ = 0 return dup def __next__(self): """ increment iterator """ self._iter_ += 1 if self._iter_<=len(self): return self[self._iter_-1] self._iter_ = 0 raise StopIteration(f"Maximum {self._ftype} iteration reached {len(self)}") def __add__(self, s, sortdefinitions=False, raiseerror=True, silentmode=True): """ Add two structure objects, with precedence as follows: paramauto > param > struct In c = a + b, if b has a higher precedence than a then c will be of b's class, otherwise it will be of a's class. The new instance is created by copying the fields from the left-hand operand (a) and then updating with the fields from the right-hand operand (b). If self or s is of class paramauto, the current state of _needs_sorting is propagated but not forced to be true. """ if not isinstance(s, struct): raise TypeError(f"the second operand must be {self._type}") # Define a helper to assign a precedence value. def get_precedence(obj): if isinstance(obj, paramauto): return 2 elif isinstance(obj, param): return 1 elif isinstance(obj, struct): return 0 else: return 0 # fallback for unknown derivations # current classes leftprecedence = get_precedence(self) rightprecedence = get_precedence(s) # Determine which class to use for the duplicate. # If s (b) has a higher precedence than self (a), use s's class; otherwise, use self's. hi_class = self.__class__ if leftprecedence >= rightprecedence else s.__class__ # Create a new instance of the chosen class by copying self's fields. dup = hi_class(**self) # Update with the fields from s. dup.update(**s) if sortdefinitions: # defer sorting by preserving the state of _needs_sorting if leftprecedence < rightprecedence == 2: # left is promoted dup._needs_sorting = s._needs_sorting elif rightprecedence < leftprecedence == 2: # right is promoted dup._needs_sorting = self._needs_sorting elif leftprecedence == rightprecedence == 2: # left and right are equivalent dup._needs_sorting = self._needs_sorting or s._needs_sorting # dup.sortdefinitions(raiseerror=raiseerror, silentmode=silentmode) return dup def __iadd__(self,s,sortdefinitions=False,raiseerror=False, silentmode=True): """ iadd a structure set sortdefintions=True to sort definitions (to maintain executability) """ if not isinstance(s,struct): raise TypeError(f"the second operand must be {self._type}") self.update(**s) if sortdefinitions: self._needs_sorting = True # self.sortdefinitions(raiseerror=raiseerror,silentmode=silentmode) return self def __sub__(self,s): """ sub a structure """ if not isinstance(s,struct): raise TypeError(f"the second operand must be {self._type}") dup = duplicate(self) listofkeys = dup.keys() for k in s.keys(): if k in listofkeys: delattr(dup,k) return dup def __isub__(self,s): """ isub a structure """ if not isinstance(s,struct): raise TypeError(f"the second operand must be {self._type}") listofkeys = self.keys() for k in s.keys(): if k in listofkeys: delattr(self,k) return self def dispmax(self,content): """ optimize display """ strcontent = str(content) if len(strcontent)>self._maxdisplay: nchar = round(self._maxdisplay/2) return strcontent[:nchar]+" [...] "+strcontent[-nchar:] else: return content def __repr__(self): """ display method """ if self.__dict__=={}: print(f"empty {self._fulltype} ({self._type} object) with no {self._type}s") return f"empty {self._fulltype}" else: numfmt = f".{self._precision}g" tmp = self.eval() if self._evalfeature else [] keylengths = [len(key) for key in self.__dict__] width = max(10,max(keylengths)+2) fmt = "%%%ss:" % width fmteval = fmt[:-1]+"=" fmtcls = fmt[:-1]+":" line = ( fmt % ('-'*(width-2)) ) + ( '-'*(min(40,width*5)) ) print(line) for key,value in self.__dict__.items(): if key not in self._excludedattr: if isinstance(value,_numeric_types): # old code (removed on 2025-01-18) # if isinstance(value,pstr): # print(fmt % key,'p"'+self.dispmax(value)+'"') # if isinstance(value,str) and value=="": # print(fmt % key,'""') # else: # print(fmt % key,self.dispmax(value)) if isinstance(value,np.ndarray): print(fmt % key, struct.format_array(value,numfmt=numfmt)) else: print(fmt % key,self.dispmax(value)) elif isinstance(value,struct): print(fmt % key,self.dispmax(value.__str__())) elif isinstance(value,(type,dict)): print(fmt % key,self.dispmax(str(value))) else: print(fmt % key,type(value)) print(fmtcls % "",self.dispmax(str(value))) if self._evalfeature: if isinstance(self,paramauto): try: if isinstance(value,pstr): print(fmteval % "",'p"'+self.dispmax(tmp.getattr(key))+'"') elif isinstance(value,str): if value == "": print(fmteval % "",self.dispmax("<empty string>")) else: print(fmteval % "",self.dispmax(tmp.getattr(key))) except Exception as err: print(fmteval % "",err.message, err.args) else: if isinstance(value,pstr): print(fmteval % "",'p"'+self.dispmax(tmp.getattr(key))+'"') elif isinstance(value,str): if value == "": print(fmteval % "",self.dispmax("<empty string>")) else: calcvalue =tmp.getattr(key) if isinstance(calcvalue, str) and "error" in calcvalue.lower(): print(fmteval % "",calcvalue) else: if isinstance(calcvalue,np.ndarray): print(fmteval % "", struct.format_array(calcvalue,numfmt=numfmt)) else: print(fmteval % "",self.dispmax(calcvalue)) elif isinstance(value,list): calcvalue =tmp.getattr(key) print(fmteval % "",self.dispmax(str(calcvalue))) print(line) return f"{self._fulltype} ({self._type} object) with {len(self)} {self._ftype}s" def disp(self): """ display method """ self.__repr__() def __str__(self): return f"{self._fulltype} ({self._type} object) with {len(self)} {self._ftype}s" @property def isempty(self): """ isempty is set to True for an empty structure """ return len(self)==0 def clear(self): """ clear() delete all fields while preserving the original class """ for k in self.keys(): delattr(self,k) def format(self, s, escape=False, raiseerror=True): """ Format a string with fields using {field} as placeholders. Handles expressions like ${variable1}. Args: s (str): The input string to format. escape (bool): If True, prevents replacing '${' with '{'. raiseerror (bool): If True, raises errors for missing fields. Note: NumPy vectors and matrices are converted into their text representation (default behavior) If expressions such ${var[1,2]} are used an error is expected, the original content will be used instead Returns: str: The formatted string. """ tmp = self.np2str() if raiseerror: try: if escape: try: # we try to evaluate with all np objects converted in to strings (default) return s.format_map(AttrErrorDict(tmp.__dict__)) except: # if an error occurs, we use the orginal content return s.format_map(AttrErrorDict(self.__dict__)) else: try: # we try to evaluate with all np objects converted in to strings (default) return s.replace("${", "{").format_map(AttrErrorDict(tmp.__dict__)) except: # if an error occurs, we use the orginal content return s.replace("${", "{").format_map(AttrErrorDict(self.__dict__)) except AttributeError as attr_err: # Handle AttributeError for expressions with operators s_ = s.replace("{", "${") if self._debug: print(f"WARNING: the {self._ftype} {attr_err} is undefined in '{s_}'") return s_ # Revert to using '${' for unresolved expressions except IndexError as idx_err: s_ = s.replace("{", "${") if self._debug: print(f"Index Error {idx_err} in '{s_}'") raise IndexError from idx_err except Exception as other_err: s_ = s.replace("{", "${") raise RuntimeError from other_err else: if escape: try: # we try to evaluate with all np objects converted in to strings (default) return s.format_map(AttrErrorDict(tmp.__dict__)) except: # if an error occurs, we use the orginal content return s.format_map(AttrErrorDict(self.__dict__)) else: try: # we try to evaluate with all np objects converted in to strings (default) return s.replace("${", "{").format_map(AttrErrorDict(tmp.__dict__)) except: # if an error occurs, we use the orginal content return s.replace("${", "{").format_map(AttrErrorDict(self.__dict__)) def format_legacy(self,s,escape=False,raiseerror=True): """ format a string with field (use {field} as placeholders) s.replace(string), s.replace(string,escape=True) where: s is a struct object string is a string with possibly ${variable1} escape is a flag to prevent ${} replaced by {} """ if raiseerror: try: if escape: return s.format(**self.__dict__) else: return s.replace("${","{").format(**self.__dict__) except KeyError as kerr: s_ = s.replace("{","${") print(f"WARNING: the {self._ftype} {kerr} is undefined in '{s_}'") return s_ # instead of s (we put back $) - OV 2023/01/27 except Exception as othererr: s_ = s.replace("{","${") raise RuntimeError from othererr else: if escape: return s.format(**self.__dict__) else: return s.replace("${","{").format(**self.__dict__) def fromkeys(self,keys): """ returns a structure from keys """ return self+struct(**dict.fromkeys(keys,None)) @staticmethod def scan(s): """ scan(string) scan a string for variables """ if not isinstance(s,str): raise TypeError("scan() requires a string") tmp = struct() #return tmp.fromkeys(set(re.findall(r"\$\{(.*?)\}",s))) found = re.findall(r"\$\{(.*?)\}",s); uniq = [] for x in found: if x not in uniq: uniq.append(x) return tmp.fromkeys(uniq) @staticmethod def isstrexpression(s): """ isstrexpression(string) returns true if s contains an expression """ if not isinstance(s,str): raise TypeError("s must a string") return re.search(r"\$\{.*?\}",s) is not None @property def isexpression(self): """ same structure with True if it is an expression """ s = param() if isinstance(self,param) else struct() for k,v in self.items(): if isinstance(v,str): s.setattr(k,struct.isstrexpression(v)) else: s.setattr(k,False) return s @staticmethod def isstrdefined(s,ref): """ isstrdefined(string,ref) returns true if it is defined in ref """ if not isinstance(s,str): raise TypeError("s must a string") if not isinstance(ref,struct): raise TypeError("ref must be a structure") if struct.isstrexpression(s): k = struct.scan(s).keys() allfound,i,nk = True,0,len(k) while (i<nk) and allfound: allfound = k[i] in ref i += 1 return allfound else: return False def isdefined(self,ref=None): """ isdefined(ref) returns true if it is defined in ref """ s = param() if isinstance(self,param) else struct() k,v,isexpr = self.keys(), self.values(), self.isexpression.values() nk = len(k) if ref is None: for i in range(nk): if isexpr[i]: s.setattr(k[i],struct.isstrdefined(v[i],self[:i])) else: s.setattr(k[i],True) else: if not isinstance(ref,struct): raise TypeError("ref must be a structure") for i in range(nk): if isexpr[i]: s.setattr(k[i],struct.isstrdefined(v[i],ref)) else: s.setattr(k[i],True) return s def sortdefinitions(self,raiseerror=True,silentmode=False): """ sortdefintions sorts all definitions so that they can be executed as param(). If any inconsistency is found, an error message is generated. Flags = default values raiseerror=True show erros of True silentmode=False no warning if True """ find = lambda xlist: [i for i, x in enumerate(xlist) if x] findnot = lambda xlist: [i for i, x in enumerate(xlist) if not x] k,v,isexpr = self.keys(), self.values(), self.isexpression.values() istatic = findnot(isexpr) idynamic = find(isexpr) static = struct.fromkeysvalues( [ k[i] for i in istatic ], [ v[i] for i in istatic ], makeparam = False) dynamic = struct.fromkeysvalues( [ k[i] for i in idynamic ], [ v[i] for i in idynamic ], makeparam=False) current = static # make static the current structure nmissing, anychange, errorfound = len(dynamic), False, False while nmissing: itst, found = 0, False while itst<nmissing and not found: teststruct = current + dynamic[[itst]] # add the test field found = all(list(teststruct.isdefined())) ifound = itst itst += 1 if found: current = teststruct # we accept the new field dynamic[ifound] = [] nmissing -= 1 anychange = True else: if raiseerror: raise KeyError('unable to interpret %d/%d expressions in "%ss"' % \ (nmissing,len(self),self._ftype)) else: if (not errorfound) and (not silentmode): print('WARNING: unable to interpret %d/%d expressions in "%ss"' % \ (nmissing,len(self),self._ftype)) current = teststruct # we accept the new field (even if it cannot be interpreted) dynamic[ifound] = [] nmissing -= 1 errorfound = True if anychange: self.clear() # reset all fields and assign them in the proper order k,v = current.keys(), current.values() for i in range(len(k)): self.setattr(k[i],v[i]) def generator(self, printout=False): """ Generate Python code of the equivalent structure. This method converts the current structure (an instance of `param`, `paramauto`, or `struct`) into Python code that, when executed, recreates an equivalent structure. The generated code is formatted with one field per line. By default (when `printout` is False), the generated code is returned as a raw string that starts directly with, for example, `param(` (or `paramauto(` or `struct(`), with no "X = " prefix or leading newline. When `printout` is True, the generated code is printed to standard output and includes a prefix "X = " to indicate the variable name. Parameters: printout (bool): If True, the generated code is printed to standard output with the "X = " prefix. If False (default), the code is returned as a raw string starting with, e.g., `param(`. Returns: str: The generated Python code representing the structure (regardless of whether it was printed). """ nk = len(self) tmp = self.np2str() # Compute the field format based on the maximum key length (with a minimum width of 10) fmt = "%%%ss =" % max(10, max([len(k) for k in self.keys()]) + 2) # Determine the appropriate class string for the current instance. if isinstance(self, param): classstr = "param" elif 'paramauto' in globals() and isinstance(self, paramauto): classstr = "paramauto" else: classstr = "struct" lines = [] if nk == 0: # For an empty structure. if printout: lines.append(f"X = {classstr}()") else: lines.append(f"{classstr}()") else: # Header: include "X = " only if printing. if printout: header = f"X = {classstr}(" else: header = f"{classstr}(" lines.append(header) # Iterate over keys to generate each field line. for i, k in enumerate(self.keys()): v = getattr(self, k) if isinstance(v, np.ndarray): vtmp = getattr(tmp, k) field = fmt % k + " " + vtmp elif isinstance(v, (int, float)) or v is None: field = fmt % k + " " + str(v) elif isinstance(v, str): field = fmt % k + " " + f'"{v}"' elif isinstance(v, (list, tuple, dict)): field = fmt % k + " " + str(v) else: field = fmt % k + " " + "/* unsupported type */" # Append a comma after each field except the last one. if i < nk - 1: field += "," lines.append(field) # Create a closing line that aligns the closing parenthesis. closing_line = fmt[:-1] % ")" lines.append(closing_line) result = "\n".join(lines) if printout: print(result) return None return result # copy and deep copy methpds for the class def __copy__(self): """ copy method """ cls = self.__class__ copie = cls.__new__(cls) copie.__dict__.update(self.__dict__) return copie def __deepcopy__(self, memo): """ deep copy method """ cls = self.__class__ copie = cls.__new__(cls) memo[id(self)] = copie for k, v in self.__dict__.items(): setattr(copie, k, duplicatedeep(v, memo)) return copie # write a file def write(self, file, overwrite=True, mkdir=False): """ write the equivalent structure (not recursive for nested struct) write(filename, overwrite=True, mkdir=False) Parameters: - file: The file path to write to. - overwrite: Whether to overwrite the file if it exists (default: True). - mkdir: Whether to create the directory if it doesn't exist (default: False). """ # Create a Path object for the file to handle cross-platform paths file_path = Path(file).resolve() # Check if the directory exists or if mkdir is set to True, create it if mkdir: file_path.parent.mkdir(parents=True, exist_ok=True) elif not file_path.parent.exists(): raise FileNotFoundError(f"The directory {file_path.parent} does not exist.") # If overwrite is False and the file already exists, raise an exception if not overwrite and file_path.exists(): raise FileExistsError(f"The file {file_path} already exists, and overwrite is set to False.") # Convert to static if needed if isinstance(p,(param,paramauto)): tmp = self.tostatic() else: tmp = self # Open and write to the file using the resolved path with file_path.open(mode="w", encoding='utf-8') as f: print(f"# {self._fulltype} with {len(self)} {self._ftype}s\n", file=f) for k, v in tmp.items(): if v is None: print(k, "=None", file=f, sep="") elif isinstance(v, (int, float)): print(k, "=", v, file=f, sep="") elif isinstance(v, str): print(k, '="', v, '"', file=f, sep="") else: print(k, "=", str(v), file=f, sep="") # read a file @staticmethod def read(file): """ read the equivalent structure read(filename) Parameters: - file: The file path to read from. """ # Create a Path object for the file to handle cross-platform paths file_path = Path(file).resolve() # Check if the parent directory exists, otherwise raise an error if not file_path.parent.exists(): raise FileNotFoundError(f"The directory {file_path.parent} does not exist.") # If the file does not exist, raise an exception if not file_path.exists(): raise FileNotFoundError(f"The file {file_path} does not exist.") # Open and read the file with file_path.open(mode="r", encoding="utf-8") as f: s = struct() # Assuming struct is defined elsewhere while True: line = f.readline() if not line: break line = line.strip() expr = line.split(sep="=") if len(line) > 0 and line[0] != "#" and len(expr) > 0: lhs = expr[0] rhs = "".join(expr[1:]).strip() if len(rhs) == 0 or rhs == "None": v = None else: v = eval(rhs) s.setattr(lhs, v) return s # argcheck def check(self,default): """ populate fields from a default structure check(defaultstruct) missing field, None and [] values are replaced by default ones Note: a.check(b) is equivalent to b+a except for [] and None values """ if not isinstance(default,struct): raise TypeError("the first argument must be a structure") for f in default.keys(): ref = default.getattr(f) if f not in self: self.setattr(f, ref) else: current = self.getattr(f) if ((current is None) or (current==[])) and \ ((ref is not None) and (ref!=[])): self.setattr(f, ref) # update values based on key:value def update(self, **kwargs): """ Update multiple fields at once, while protecting certain attributes. Parameters: ----------- **kwargs : dict The fields to update and their new values. Protected attributes defined in _excludedattr are not updated. Usage: ------ s.update(a=10, b=[1, 2, 3], new_field="new_value") """ protected_attributes = getattr(self, '_excludedattr', ()) for key, value in kwargs.items(): if key in protected_attributes: print(f"Warning: Cannot update protected attribute '{key}'") else: self.setattr(key, value) # override () for subindexing structure with key names def __call__(self, *keys): """ Extract a sub-structure based on the specified keys, keeping the same class type. Parameters: ----------- *keys : str The keys for the fields to include in the sub-structure. Returns: -------- struct A new instance of the same class as the original, containing only the specified keys. Usage: ------ sub_struct = s('key1', 'key2', ...) """ # Create a new instance of the same class sub_struct = self.__class__() # Get the full type and field type for error messages fulltype = getattr(self, '_fulltype', 'structure') ftype = getattr(self, '_ftype', 'field') # Add only the specified keys to the new sub-structure for key in keys: if key in self: sub_struct.setattr(key, self.getattr(key)) else: raise KeyError(f"{fulltype} does not contain the {ftype} '{key}'.") return sub_struct def __delattr__(self, key): """ Delete an instance attribute if it exists and is not a class or excluded attribute. """ if key in self._excludedattr: raise AttributeError(f"Cannot delete excluded attribute '{key}'") elif key in self.__class__.__dict__: # Check if it's a class attribute raise AttributeError(f"Cannot delete class attribute '{key}'") elif key in self.__dict__: # Delete only if in instance's __dict__ del self.__dict__[key] else: raise AttributeError(f"{self._type} has no attribute '{key}'") # A la Matlab display method of vectors, matrices and ND-arrays @staticmethod def format_array(value,numfmt=".4g"): """ Format NumPy array for display with distinctions for scalars, row/column vectors, and ND arrays. Recursively formats multi-dimensional arrays without introducing unwanted commas. Args: value (np.ndarray): The NumPy array to format. numfmt: numeric format to be used for the string conversion (default=".4g") Returns: str: A formatted string representation of the array. """ dtype_str = { np.float64: "double", np.float32: "single", np.int32: "int32", np.int64: "int64", np.complex64: "complex single", np.complex128: "complex double", }.get(value.dtype.type, str(value.dtype)) # Default to dtype name if not in the map max_display = 10 # Maximum number of elements to display def format_recursive(arr): """ Recursively formats the array based on its dimensions. Args: arr (np.ndarray): The array or sub-array to format. Returns: str: Formatted string of the array. """ if arr.ndim == 0: return f"{arr.item()}" if arr.ndim == 1: if len(arr) <= max_display: return "[" + " ".join(f"{v:{numfmt}}" for v in arr) + "]" else: return f"[{len(arr)} elements]" if arr.ndim == 2: if arr.shape[1] == 1: # Column vector if arr.shape[0] <= max_display: return "[" + " ".join(f"{v[0]:{numfmt}}" for v in arr) + "]T" else: return f"[{arr.shape[0]}×1 vector]" elif arr.shape[0] == 1: # Row vector if arr.shape[1] <= max_display: return "[" + " ".join(f"{v:{numfmt}}" for v in arr[0]) + "]" else: return f"[1×{arr.shape[1]} vector]" else: # General matrix return f"[{arr.shape[0]}×{arr.shape[1]} matrix]" # For higher dimensions shape_str = "×".join(map(str, arr.shape)) if arr.size <= max_display: # Show full content if arr.ndim > 2: # Represent multi-dimensional arrays with nested brackets return "[" + " ".join(format_recursive(subarr) for subarr in arr) + f"] ({shape_str} {dtype_str})" return f"[{shape_str} array ({dtype_str})]" if value.size == 0: return "[]" if value.ndim == 0 or value.size == 1: return f"{value.item()} ({dtype_str})" if value.ndim == 1 or value.ndim == 2: # Use existing logic for vectors and matrices if value.ndim == 1: if len(value) <= max_display: formatted = "[" + " ".join(f"{v:{numfmt}}" for v in value) + f"] ({dtype_str})" else: formatted = f"[{len(value)}×1 {dtype_str}]" elif value.ndim == 2: rows, cols = value.shape if cols == 1: # Column vector if rows <= max_display: formatted = "[" + " ".join(f"{v[0]:{numfmt}}" for v in value) + f"]T ({dtype_str})" else: formatted = f"[{rows}×1 {dtype_str}]" elif rows == 1: # Row vector if cols <= max_display: formatted = "[" + " ".join(f"{v:{numfmt}}" for v in value[0]) + f"] ({dtype_str})" else: formatted = f"[1×{cols} {dtype_str}]" else: # General matrix formatted = f"[{rows}×{cols} {dtype_str}]" return formatted # For higher-dimensional arrays if value.size <= max_display: formatted = format_recursive(value) else: shape_str = "×".join(map(str, value.shape)) formatted = f"[{shape_str} array ({dtype_str})]" return formatted # convert all NumPy entries to "nestable" expressions def np2str(self): """ Convert all NumPy entries of s into their string representations, handling both lists and dictionaries. """ out = struct() def format_numpy_result(value): """ Converts a NumPy array or scalar into a string representation: - Scalars and single-element arrays (any number of dimensions) are returned as scalars without brackets. - Arrays with more than one element are formatted with proper nesting and commas to make them valid `np.array()` inputs. - If the value is a list or dict, the conversion is applied recursively. - Non-ndarray inputs that are not list/dict are returned without modification. Args: value (np.ndarray, scalar, list, dict, or other): The value to format. Returns: str, list, dict, or original type: A properly formatted string for NumPy arrays/scalars, a recursively converted list/dict, or the original value. """ if isinstance(value, dict): # Recursively process each key in the dictionary. new_dict = {} for k, v in value.items(): new_dict[k] = format_numpy_result(v) return new_dict elif isinstance(value, list): # Recursively process each element in the list. return [format_numpy_result(x) for x in value] elif isinstance(value, tuple): return tuple(format_numpy_result(x) for x in value) elif isinstance(value, struct): return value.npstr() elif np.isscalar(value): # For scalars: if numeric, use str() to avoid extra quotes. if isinstance(value, (int, float, complex, str)) or value is None: return value else: return repr(value) elif isinstance(value, np.ndarray): # Check if the array has exactly one element. if value.size == 1: # Extract the scalar value. return repr(value.item()) # Convert the array to a nested list. nested_list = value.tolist() # Recursively format the nested list into a valid string. def list_to_string(lst): if isinstance(lst, list): return "[" + ",".join(list_to_string(item) for item in lst) + "]" else: return repr(lst) return list_to_string(nested_list) else: # Return the input unmodified if not a NumPy array, list, dict, or scalar. return str(value) # str() preferred over repr() for concision # Process all entries in self. for key, value in self.items(): out.setattr(key, format_numpy_result(value)) return out # minimal replacement of placeholders by numbers or their string representations def numrepl(self, text): r""" Replace all placeholders of the form ${key} in the given text by the corresponding numeric value from the instance fields, under the following conditions: 1. 'key' must be a valid field in self (i.e., if key in self). 2. The value corresponding to 'key' is either: - an int, - a float, or - a string that represents a valid number (e.g., "1" or "1.0"). Only when these conditions are met, the placeholder is substituted. The conversion preserves the original type: if the stored value is int, then the substitution will be done as an integer (e.g., 1 and not 1.0). Otherwise, if it is a float then it will be substituted as a float. Any placeholder for which the above conditions are not met remains unchanged. Placeholders are recognized by the pattern "${<key>}" where <key> is captured as all text until the next "}" (optionally allowing whitespace inside the braces). """ # Pattern: match "${", then optional whitespace, capture all characters until "}", # then optional whitespace, then "}". placeholder_pattern = re.compile(r"\$\{\s*([^}]+?)\s*\}") def replace_match(match): key = match.group(1) # Check if the key exists in self. if key in self: value = self[key] # If the value is already numeric, substitute directly. if isinstance(value, (int, float)): return str(value) # If the value is a string, try to interpret it as a numeric value. elif isinstance(value, str): s = value.strip() # Check if s is a valid integer representation. if re.fullmatch(r"[+-]?\d+", s): try: num = int(s) return str(num) except ValueError: # Should not occur because the regex already matched. return match.group(0) # Check if s is a valid float representation (including scientific notation). elif re.fullmatch(r"[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?", s): try: num = float(s) return str(num) except ValueError: return match.group(0) # If key not in self or value is not numeric (or numeric string), leave placeholder intact. return match.group(0) # Replace all placeholders in the text using the replacer function. return placeholder_pattern.sub(replace_match, text) # import method def importfrom(self, s, nonempty=True, replacedefaultvar=True): """ Import values from 's' into self according to the following rules: - Only fields that already exist in self are considered. - If s is a dictionary, it is converted to a struct via struct(**s). - If the current value of a field in self is empty (None, "", [] or ()), then that field is updated from s. - If nonempty is True (default), then only non-empty values from s are imported. - If replacedefaultvar is True (default), then if a field in self exactly equals "${key}" (with key being the field name), it is replaced by the corresponding value from s if it is empty. - Raises a TypeError if s is not a dict or struct (i.e. if it doesn’t support keys()). """ # If s is a dictionary, convert it to a struct instance. if isinstance(s, dict): s = struct(**s) elif not hasattr(s, "keys"): raise TypeError(f"s must be a struct or a dictionary not a type {type(s).__name__}") for key in self.keys(): if key in s: s_value = getattr(s, key) current_value = getattr(self, key) if is_empty(current_value) or (replacedefaultvar and current_value == "${" + key + "}"): if nonempty: if not is_empty(s_value): setattr(self, key, s_value) else: setattr(self, key, s_value) # importfrom with copy def __lshift__(self, other): """ Allows the syntax: s = s1 << s2 where a new instance is created as a copy of s1 (preserving its type, whether struct, param, or paramauto) and then updated with the values from s2 using importfrom. """ # Create a new instance preserving the type of self. new_instance = type(self)(**{k: getattr(self, k) for k in self.keys()}) # Import values from other (s2) into the new instance. new_instance.importfrom(other) return new_instance # returns only valid keys def validkeys(self, list_of_keys): """ Validate and return the subset of keys from the provided list that are valid in the instance. Parameters: ----------- list_of_keys : list A list of keys (as strings) to check against the instance’s attributes. Returns: -------- list A list of keys from list_of_keys that are valid (i.e., exist as attributes in the instance). Raises: ------- TypeError If list_of_keys is not a list or if any element in list_of_keys is not a string. Example: -------- >>> s = struct() >>> s.foo = 42 >>> s.bar = "hello" >>> valid = s.validkeys(["foo", "bar", "baz"]) >>> print(valid) # Output: ['foo', 'bar'] assuming 'baz' is not defined in s """ # Check that list_of_keys is a list if not isinstance(list_of_keys, list): raise TypeError("list_of_keys must be a list") # Check that every entry in the list is a string for key in list_of_keys: if not isinstance(key, str): raise TypeError("Each key in list_of_keys must be a string") # Assuming valid keys are those present in the instance's __dict__ return [key for key in list_of_keys if key in self]
Subclasses
- pizza.private.mstruct.param
- pizza.raster.collection
- pizza.region.regioncollection
- pizza.script.scriptobject
- pizza.script.scriptobjectgroup
- collection
Static methods
def dict2struct(dico, makeparam=False)
-
create a structure from a dictionary
Expand source code
@staticmethod def dict2struct(dico,makeparam=False): """ create a structure from a dictionary """ if isinstance(dico,dict): s = param() if makeparam else struct() s.set(**dico) return s raise TypeError("the argument must be a dictionary")
def format_array(value, numfmt='.4g')
-
Format NumPy array for display with distinctions for scalars, row/column vectors, and ND arrays. Recursively formats multi-dimensional arrays without introducing unwanted commas.
Args
value
:np.ndarray
- The NumPy array to format.
numfmt
- numeric format to be used for the string conversion (default=".4g")
Returns
str
- A formatted string representation of the array.
Expand source code
@staticmethod def format_array(value,numfmt=".4g"): """ Format NumPy array for display with distinctions for scalars, row/column vectors, and ND arrays. Recursively formats multi-dimensional arrays without introducing unwanted commas. Args: value (np.ndarray): The NumPy array to format. numfmt: numeric format to be used for the string conversion (default=".4g") Returns: str: A formatted string representation of the array. """ dtype_str = { np.float64: "double", np.float32: "single", np.int32: "int32", np.int64: "int64", np.complex64: "complex single", np.complex128: "complex double", }.get(value.dtype.type, str(value.dtype)) # Default to dtype name if not in the map max_display = 10 # Maximum number of elements to display def format_recursive(arr): """ Recursively formats the array based on its dimensions. Args: arr (np.ndarray): The array or sub-array to format. Returns: str: Formatted string of the array. """ if arr.ndim == 0: return f"{arr.item()}" if arr.ndim == 1: if len(arr) <= max_display: return "[" + " ".join(f"{v:{numfmt}}" for v in arr) + "]" else: return f"[{len(arr)} elements]" if arr.ndim == 2: if arr.shape[1] == 1: # Column vector if arr.shape[0] <= max_display: return "[" + " ".join(f"{v[0]:{numfmt}}" for v in arr) + "]T" else: return f"[{arr.shape[0]}×1 vector]" elif arr.shape[0] == 1: # Row vector if arr.shape[1] <= max_display: return "[" + " ".join(f"{v:{numfmt}}" for v in arr[0]) + "]" else: return f"[1×{arr.shape[1]} vector]" else: # General matrix return f"[{arr.shape[0]}×{arr.shape[1]} matrix]" # For higher dimensions shape_str = "×".join(map(str, arr.shape)) if arr.size <= max_display: # Show full content if arr.ndim > 2: # Represent multi-dimensional arrays with nested brackets return "[" + " ".join(format_recursive(subarr) for subarr in arr) + f"] ({shape_str} {dtype_str})" return f"[{shape_str} array ({dtype_str})]" if value.size == 0: return "[]" if value.ndim == 0 or value.size == 1: return f"{value.item()} ({dtype_str})" if value.ndim == 1 or value.ndim == 2: # Use existing logic for vectors and matrices if value.ndim == 1: if len(value) <= max_display: formatted = "[" + " ".join(f"{v:{numfmt}}" for v in value) + f"] ({dtype_str})" else: formatted = f"[{len(value)}×1 {dtype_str}]" elif value.ndim == 2: rows, cols = value.shape if cols == 1: # Column vector if rows <= max_display: formatted = "[" + " ".join(f"{v[0]:{numfmt}}" for v in value) + f"]T ({dtype_str})" else: formatted = f"[{rows}×1 {dtype_str}]" elif rows == 1: # Row vector if cols <= max_display: formatted = "[" + " ".join(f"{v:{numfmt}}" for v in value[0]) + f"] ({dtype_str})" else: formatted = f"[1×{cols} {dtype_str}]" else: # General matrix formatted = f"[{rows}×{cols} {dtype_str}]" return formatted # For higher-dimensional arrays if value.size <= max_display: formatted = format_recursive(value) else: shape_str = "×".join(map(str, value.shape)) formatted = f"[{shape_str} array ({dtype_str})]" return formatted
def fromkeysvalues(keys, values, makeparam=False)
-
struct.keysvalues(keys,values) creates a structure from keys and values use makeparam = True to create a param instead of struct
Expand source code
@staticmethod def fromkeysvalues(keys,values,makeparam=False): """ struct.keysvalues(keys,values) creates a structure from keys and values use makeparam = True to create a param instead of struct """ if keys is None: raise AttributeError("the keys must not empty") if not isinstance(keys,_list_types): keys = [keys] if not isinstance(values,_list_types): values = [values] nk,nv = len(keys), len(values) s = param() if makeparam else struct() if nk>0 and nv>0: iv = 0 for ik in range(nk): s.setattr(keys[ik], values[iv]) iv = min(nv-1,iv+1) for ik in range(nk,nv): s.setattr(f"key{ik}", values[ik]) return s
def isstrdefined(s, ref)
-
isstrdefined(string,ref) returns true if it is defined in ref
Expand source code
@staticmethod def isstrdefined(s,ref): """ isstrdefined(string,ref) returns true if it is defined in ref """ if not isinstance(s,str): raise TypeError("s must a string") if not isinstance(ref,struct): raise TypeError("ref must be a structure") if struct.isstrexpression(s): k = struct.scan(s).keys() allfound,i,nk = True,0,len(k) while (i<nk) and allfound: allfound = k[i] in ref i += 1 return allfound else: return False
def isstrexpression(s)
-
isstrexpression(string) returns true if s contains an expression
Expand source code
@staticmethod def isstrexpression(s): """ isstrexpression(string) returns true if s contains an expression """ if not isinstance(s,str): raise TypeError("s must a string") return re.search(r"\$\{.*?\}",s) is not None
def read(file)
-
read the equivalent structure read(filename)
Parameters: - file: The file path to read from.
Expand source code
@staticmethod def read(file): """ read the equivalent structure read(filename) Parameters: - file: The file path to read from. """ # Create a Path object for the file to handle cross-platform paths file_path = Path(file).resolve() # Check if the parent directory exists, otherwise raise an error if not file_path.parent.exists(): raise FileNotFoundError(f"The directory {file_path.parent} does not exist.") # If the file does not exist, raise an exception if not file_path.exists(): raise FileNotFoundError(f"The file {file_path} does not exist.") # Open and read the file with file_path.open(mode="r", encoding="utf-8") as f: s = struct() # Assuming struct is defined elsewhere while True: line = f.readline() if not line: break line = line.strip() expr = line.split(sep="=") if len(line) > 0 and line[0] != "#" and len(expr) > 0: lhs = expr[0] rhs = "".join(expr[1:]).strip() if len(rhs) == 0 or rhs == "None": v = None else: v = eval(rhs) s.setattr(lhs, v) return s
def scan(s)
-
scan(string) scan a string for variables
Expand source code
@staticmethod def scan(s): """ scan(string) scan a string for variables """ if not isinstance(s,str): raise TypeError("scan() requires a string") tmp = struct() #return tmp.fromkeys(set(re.findall(r"\$\{(.*?)\}",s))) found = re.findall(r"\$\{(.*?)\}",s); uniq = [] for x in found: if x not in uniq: uniq.append(x) return tmp.fromkeys(uniq)
Instance variables
var isempty
-
isempty is set to True for an empty structure
Expand source code
@property def isempty(self): """ isempty is set to True for an empty structure """ return len(self)==0
var isexpression
-
same structure with True if it is an expression
Expand source code
@property def isexpression(self): """ same structure with True if it is an expression """ s = param() if isinstance(self,param) else struct() for k,v in self.items(): if isinstance(v,str): s.setattr(k,struct.isstrexpression(v)) else: s.setattr(k,False) return s
Methods
def check(self, default)
-
populate fields from a default structure check(defaultstruct) missing field, None and [] values are replaced by default ones
Note: a.check(b) is equivalent to b+a except for [] and None values
Expand source code
def check(self,default): """ populate fields from a default structure check(defaultstruct) missing field, None and [] values are replaced by default ones Note: a.check(b) is equivalent to b+a except for [] and None values """ if not isinstance(default,struct): raise TypeError("the first argument must be a structure") for f in default.keys(): ref = default.getattr(f) if f not in self: self.setattr(f, ref) else: current = self.getattr(f) if ((current is None) or (current==[])) and \ ((ref is not None) and (ref!=[])): self.setattr(f, ref)
def clear(self)
-
clear() delete all fields while preserving the original class
Expand source code
def clear(self): """ clear() delete all fields while preserving the original class """ for k in self.keys(): delattr(self,k)
def disp(self)
-
display method
Expand source code
def disp(self): """ display method """ self.__repr__()
def dispmax(self, content)
-
optimize display
Expand source code
def dispmax(self,content): """ optimize display """ strcontent = str(content) if len(strcontent)>self._maxdisplay: nchar = round(self._maxdisplay/2) return strcontent[:nchar]+" [...] "+strcontent[-nchar:] else: return content
def format(self, s, escape=False, raiseerror=True)
-
Format a string with fields using {field} as placeholders. Handles expressions like ${variable1}.
Args
s
:str
- The input string to format.
escape
:bool
- If True, prevents replacing '${' with '{'.
raiseerror
:bool
- If True, raises errors for missing fields.
Note
NumPy vectors and matrices are converted into their text representation (default behavior) If expressions such ${var[1,2]} are used an error is expected, the original content will be used instead
Returns
str
- The formatted string.
Expand source code
def format(self, s, escape=False, raiseerror=True): """ Format a string with fields using {field} as placeholders. Handles expressions like ${variable1}. Args: s (str): The input string to format. escape (bool): If True, prevents replacing '${' with '{'. raiseerror (bool): If True, raises errors for missing fields. Note: NumPy vectors and matrices are converted into their text representation (default behavior) If expressions such ${var[1,2]} are used an error is expected, the original content will be used instead Returns: str: The formatted string. """ tmp = self.np2str() if raiseerror: try: if escape: try: # we try to evaluate with all np objects converted in to strings (default) return s.format_map(AttrErrorDict(tmp.__dict__)) except: # if an error occurs, we use the orginal content return s.format_map(AttrErrorDict(self.__dict__)) else: try: # we try to evaluate with all np objects converted in to strings (default) return s.replace("${", "{").format_map(AttrErrorDict(tmp.__dict__)) except: # if an error occurs, we use the orginal content return s.replace("${", "{").format_map(AttrErrorDict(self.__dict__)) except AttributeError as attr_err: # Handle AttributeError for expressions with operators s_ = s.replace("{", "${") if self._debug: print(f"WARNING: the {self._ftype} {attr_err} is undefined in '{s_}'") return s_ # Revert to using '${' for unresolved expressions except IndexError as idx_err: s_ = s.replace("{", "${") if self._debug: print(f"Index Error {idx_err} in '{s_}'") raise IndexError from idx_err except Exception as other_err: s_ = s.replace("{", "${") raise RuntimeError from other_err else: if escape: try: # we try to evaluate with all np objects converted in to strings (default) return s.format_map(AttrErrorDict(tmp.__dict__)) except: # if an error occurs, we use the orginal content return s.format_map(AttrErrorDict(self.__dict__)) else: try: # we try to evaluate with all np objects converted in to strings (default) return s.replace("${", "{").format_map(AttrErrorDict(tmp.__dict__)) except: # if an error occurs, we use the orginal content return s.replace("${", "{").format_map(AttrErrorDict(self.__dict__))
def format_legacy(self, s, escape=False, raiseerror=True)
-
format a string with field (use {field} as placeholders) s.replace(string), s.replace(string,escape=True) where: s is a struct object string is a string with possibly ${variable1} escape is a flag to prevent ${} replaced by {}
Expand source code
def format_legacy(self,s,escape=False,raiseerror=True): """ format a string with field (use {field} as placeholders) s.replace(string), s.replace(string,escape=True) where: s is a struct object string is a string with possibly ${variable1} escape is a flag to prevent ${} replaced by {} """ if raiseerror: try: if escape: return s.format(**self.__dict__) else: return s.replace("${","{").format(**self.__dict__) except KeyError as kerr: s_ = s.replace("{","${") print(f"WARNING: the {self._ftype} {kerr} is undefined in '{s_}'") return s_ # instead of s (we put back $) - OV 2023/01/27 except Exception as othererr: s_ = s.replace("{","${") raise RuntimeError from othererr else: if escape: return s.format(**self.__dict__) else: return s.replace("${","{").format(**self.__dict__)
def fromkeys(self, keys)
-
returns a structure from keys
Expand source code
def fromkeys(self,keys): """ returns a structure from keys """ return self+struct(**dict.fromkeys(keys,None))
def generator(self, printout=False)
-
Generate Python code of the equivalent structure.
This method converts the current structure (an instance of
param
,paramauto
, orstruct
) into Python code that, when executed, recreates an equivalent structure. The generated code is formatted with one field per line.By default (when
printout
is False), the generated code is returned as a raw string that starts directly with, for example,param(
(orparamauto(
orstruct(
), with no "X = " prefix or leading newline. Whenprintout
is True, the generated code is printed to standard output and includes a prefix "X = " to indicate the variable name.Parameters
printout (bool): If True, the generated code is printed to standard output with the "X = " prefix. If False (default), the code is returned as a raw string starting with, e.g.,
param(
.Returns
str
- The generated Python code representing the structure (regardless of whether it was printed).
Expand source code
def generator(self, printout=False): """ Generate Python code of the equivalent structure. This method converts the current structure (an instance of `param`, `paramauto`, or `struct`) into Python code that, when executed, recreates an equivalent structure. The generated code is formatted with one field per line. By default (when `printout` is False), the generated code is returned as a raw string that starts directly with, for example, `param(` (or `paramauto(` or `struct(`), with no "X = " prefix or leading newline. When `printout` is True, the generated code is printed to standard output and includes a prefix "X = " to indicate the variable name. Parameters: printout (bool): If True, the generated code is printed to standard output with the "X = " prefix. If False (default), the code is returned as a raw string starting with, e.g., `param(`. Returns: str: The generated Python code representing the structure (regardless of whether it was printed). """ nk = len(self) tmp = self.np2str() # Compute the field format based on the maximum key length (with a minimum width of 10) fmt = "%%%ss =" % max(10, max([len(k) for k in self.keys()]) + 2) # Determine the appropriate class string for the current instance. if isinstance(self, param): classstr = "param" elif 'paramauto' in globals() and isinstance(self, paramauto): classstr = "paramauto" else: classstr = "struct" lines = [] if nk == 0: # For an empty structure. if printout: lines.append(f"X = {classstr}()") else: lines.append(f"{classstr}()") else: # Header: include "X = " only if printing. if printout: header = f"X = {classstr}(" else: header = f"{classstr}(" lines.append(header) # Iterate over keys to generate each field line. for i, k in enumerate(self.keys()): v = getattr(self, k) if isinstance(v, np.ndarray): vtmp = getattr(tmp, k) field = fmt % k + " " + vtmp elif isinstance(v, (int, float)) or v is None: field = fmt % k + " " + str(v) elif isinstance(v, str): field = fmt % k + " " + f'"{v}"' elif isinstance(v, (list, tuple, dict)): field = fmt % k + " " + str(v) else: field = fmt % k + " " + "/* unsupported type */" # Append a comma after each field except the last one. if i < nk - 1: field += "," lines.append(field) # Create a closing line that aligns the closing parenthesis. closing_line = fmt[:-1] % ")" lines.append(closing_line) result = "\n".join(lines) if printout: print(result) return None return result
def getattr(self, key)
-
Get attribute override to access both instance attributes and properties if allowed.
Expand source code
def getattr(self,key): """Get attribute override to access both instance attributes and properties if allowed.""" if key in self.__dict__: return self.__dict__[key] elif getattr(self, '_propertyasattribute', False) and \ key not in self._excludedattr and \ key in self.__class__.__dict__ and isinstance(self.__class__.__dict__[key], property): # If _propertyasattribute is True and it's a property, get its value return self.__class__.__dict__[key].fget(self) else: raise AttributeError(f'the {self._ftype} "{key}" does not exist')
def hasattr(self, key)
-
Return true if the field exists, considering properties as regular attributes if allowed.
Expand source code
def hasattr(self, key): """Return true if the field exists, considering properties as regular attributes if allowed.""" return key in self.__dict__ or ( getattr(self, '_propertyasattribute', False) and key not in self._excludedattr and key in self.__class__.__dict__ and isinstance(self.__class__.__dict__[key], property) )
def importfrom(self, s, nonempty=True, replacedefaultvar=True)
-
Import values from 's' into self according to the following rules:
- Only fields that already exist in self are considered.
- If s is a dictionary, it is converted to a struct via struct(**s).
- If the current value of a field in self is empty (None, "", [] or ()), then that field is updated from s.
- If nonempty is True (default), then only non-empty values from s are imported.
- If replacedefaultvar is True (default), then if a field in self exactly equals "${key}" (with key being the field name), it is replaced by the corresponding value from s if it is empty.
- Raises a TypeError if s is not a dict or struct (i.e. if it doesn’t support keys()).
Expand source code
def importfrom(self, s, nonempty=True, replacedefaultvar=True): """ Import values from 's' into self according to the following rules: - Only fields that already exist in self are considered. - If s is a dictionary, it is converted to a struct via struct(**s). - If the current value of a field in self is empty (None, "", [] or ()), then that field is updated from s. - If nonempty is True (default), then only non-empty values from s are imported. - If replacedefaultvar is True (default), then if a field in self exactly equals "${key}" (with key being the field name), it is replaced by the corresponding value from s if it is empty. - Raises a TypeError if s is not a dict or struct (i.e. if it doesn’t support keys()). """ # If s is a dictionary, convert it to a struct instance. if isinstance(s, dict): s = struct(**s) elif not hasattr(s, "keys"): raise TypeError(f"s must be a struct or a dictionary not a type {type(s).__name__}") for key in self.keys(): if key in s: s_value = getattr(s, key) current_value = getattr(self, key) if is_empty(current_value) or (replacedefaultvar and current_value == "${" + key + "}"): if nonempty: if not is_empty(s_value): setattr(self, key, s_value) else: setattr(self, key, s_value)
def isdefined(self, ref=None)
-
isdefined(ref) returns true if it is defined in ref
Expand source code
def isdefined(self,ref=None): """ isdefined(ref) returns true if it is defined in ref """ s = param() if isinstance(self,param) else struct() k,v,isexpr = self.keys(), self.values(), self.isexpression.values() nk = len(k) if ref is None: for i in range(nk): if isexpr[i]: s.setattr(k[i],struct.isstrdefined(v[i],self[:i])) else: s.setattr(k[i],True) else: if not isinstance(ref,struct): raise TypeError("ref must be a structure") for i in range(nk): if isexpr[i]: s.setattr(k[i],struct.isstrdefined(v[i],ref)) else: s.setattr(k[i],True) return s
def items(self)
-
return all elements as iterable key, value
Expand source code
def items(self): """ return all elements as iterable key, value """ return self.zip()
def keys(self)
-
return the fields
Expand source code
def keys(self): """ return the fields """ # keys() is used by struct() and its iterator return [key for key in self.__dict__.keys() if key not in self._excludedattr]
def keyssorted(self, reverse=True)
-
sort keys by length()
Expand source code
def keyssorted(self,reverse=True): """ sort keys by length() """ klist = self.keys() l = [len(k) for k in klist] return [k for _,k in sorted(zip(l,klist),reverse=reverse)]
def np2str(self)
-
Convert all NumPy entries of s into their string representations, handling both lists and dictionaries.
Expand source code
def np2str(self): """ Convert all NumPy entries of s into their string representations, handling both lists and dictionaries. """ out = struct() def format_numpy_result(value): """ Converts a NumPy array or scalar into a string representation: - Scalars and single-element arrays (any number of dimensions) are returned as scalars without brackets. - Arrays with more than one element are formatted with proper nesting and commas to make them valid `np.array()` inputs. - If the value is a list or dict, the conversion is applied recursively. - Non-ndarray inputs that are not list/dict are returned without modification. Args: value (np.ndarray, scalar, list, dict, or other): The value to format. Returns: str, list, dict, or original type: A properly formatted string for NumPy arrays/scalars, a recursively converted list/dict, or the original value. """ if isinstance(value, dict): # Recursively process each key in the dictionary. new_dict = {} for k, v in value.items(): new_dict[k] = format_numpy_result(v) return new_dict elif isinstance(value, list): # Recursively process each element in the list. return [format_numpy_result(x) for x in value] elif isinstance(value, tuple): return tuple(format_numpy_result(x) for x in value) elif isinstance(value, struct): return value.npstr() elif np.isscalar(value): # For scalars: if numeric, use str() to avoid extra quotes. if isinstance(value, (int, float, complex, str)) or value is None: return value else: return repr(value) elif isinstance(value, np.ndarray): # Check if the array has exactly one element. if value.size == 1: # Extract the scalar value. return repr(value.item()) # Convert the array to a nested list. nested_list = value.tolist() # Recursively format the nested list into a valid string. def list_to_string(lst): if isinstance(lst, list): return "[" + ",".join(list_to_string(item) for item in lst) + "]" else: return repr(lst) return list_to_string(nested_list) else: # Return the input unmodified if not a NumPy array, list, dict, or scalar. return str(value) # str() preferred over repr() for concision # Process all entries in self. for key, value in self.items(): out.setattr(key, format_numpy_result(value)) return out
def numrepl(self, text)
-
Replace all placeholders of the form ${key} in the given text by the corresponding numeric value from the instance fields, under the following conditions:
- 'key' must be a valid field in self (i.e., if key in self).
- The value corresponding to 'key' is either:
- an int,
- a float, or
- a string that represents a valid number (e.g., "1" or "1.0").
Only when these conditions are met, the placeholder is substituted. The conversion preserves the original type: if the stored value is int, then the substitution will be done as an integer (e.g., 1 and not 1.0). Otherwise, if it is a float then it will be substituted as a float.
Any placeholder for which the above conditions are not met remains unchanged.
Placeholders are recognized by the pattern "${
}" where is captured as all text until the next "}" (optionally allowing whitespace inside the braces). Expand source code
def numrepl(self, text): r""" Replace all placeholders of the form ${key} in the given text by the corresponding numeric value from the instance fields, under the following conditions: 1. 'key' must be a valid field in self (i.e., if key in self). 2. The value corresponding to 'key' is either: - an int, - a float, or - a string that represents a valid number (e.g., "1" or "1.0"). Only when these conditions are met, the placeholder is substituted. The conversion preserves the original type: if the stored value is int, then the substitution will be done as an integer (e.g., 1 and not 1.0). Otherwise, if it is a float then it will be substituted as a float. Any placeholder for which the above conditions are not met remains unchanged. Placeholders are recognized by the pattern "${<key>}" where <key> is captured as all text until the next "}" (optionally allowing whitespace inside the braces). """ # Pattern: match "${", then optional whitespace, capture all characters until "}", # then optional whitespace, then "}". placeholder_pattern = re.compile(r"\$\{\s*([^}]+?)\s*\}") def replace_match(match): key = match.group(1) # Check if the key exists in self. if key in self: value = self[key] # If the value is already numeric, substitute directly. if isinstance(value, (int, float)): return str(value) # If the value is a string, try to interpret it as a numeric value. elif isinstance(value, str): s = value.strip() # Check if s is a valid integer representation. if re.fullmatch(r"[+-]?\d+", s): try: num = int(s) return str(num) except ValueError: # Should not occur because the regex already matched. return match.group(0) # Check if s is a valid float representation (including scientific notation). elif re.fullmatch(r"[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?", s): try: num = float(s) return str(num) except ValueError: return match.group(0) # If key not in self or value is not numeric (or numeric string), leave placeholder intact. return match.group(0) # Replace all placeholders in the text using the replacer function. return placeholder_pattern.sub(replace_match, text)
def set(self, **kwargs)
-
initialization
Expand source code
def set(self,**kwargs): """ initialization """ self.__dict__.update(kwargs)
def setattr(self, key, value)
-
set field and value
Expand source code
def setattr(self,key,value): """ set field and value """ if isinstance(value,list) and len(value)==0 and key in self: delattr(self, key) else: self.__dict__[key] = value
def sortdefinitions(self, raiseerror=True, silentmode=False)
-
sortdefintions sorts all definitions so that they can be executed as param(). If any inconsistency is found, an error message is generated.
Flags = default values raiseerror=True show erros of True silentmode=False no warning if True
Expand source code
def sortdefinitions(self,raiseerror=True,silentmode=False): """ sortdefintions sorts all definitions so that they can be executed as param(). If any inconsistency is found, an error message is generated. Flags = default values raiseerror=True show erros of True silentmode=False no warning if True """ find = lambda xlist: [i for i, x in enumerate(xlist) if x] findnot = lambda xlist: [i for i, x in enumerate(xlist) if not x] k,v,isexpr = self.keys(), self.values(), self.isexpression.values() istatic = findnot(isexpr) idynamic = find(isexpr) static = struct.fromkeysvalues( [ k[i] for i in istatic ], [ v[i] for i in istatic ], makeparam = False) dynamic = struct.fromkeysvalues( [ k[i] for i in idynamic ], [ v[i] for i in idynamic ], makeparam=False) current = static # make static the current structure nmissing, anychange, errorfound = len(dynamic), False, False while nmissing: itst, found = 0, False while itst<nmissing and not found: teststruct = current + dynamic[[itst]] # add the test field found = all(list(teststruct.isdefined())) ifound = itst itst += 1 if found: current = teststruct # we accept the new field dynamic[ifound] = [] nmissing -= 1 anychange = True else: if raiseerror: raise KeyError('unable to interpret %d/%d expressions in "%ss"' % \ (nmissing,len(self),self._ftype)) else: if (not errorfound) and (not silentmode): print('WARNING: unable to interpret %d/%d expressions in "%ss"' % \ (nmissing,len(self),self._ftype)) current = teststruct # we accept the new field (even if it cannot be interpreted) dynamic[ifound] = [] nmissing -= 1 errorfound = True if anychange: self.clear() # reset all fields and assign them in the proper order k,v = current.keys(), current.values() for i in range(len(k)): self.setattr(k[i],v[i])
def struct2dict(self)
-
create a dictionary from the current structure
Expand source code
def struct2dict(self): """ create a dictionary from the current structure """ return dict(self.zip())
def struct2param(self, protection=False, evaluation=True)
-
convert an object struct() to param()
Expand source code
def struct2param(self,protection=False,evaluation=True): """ convert an object struct() to param() """ p = param(**self.struct2dict()) for i in range(len(self)): if isinstance(self[i],pstr): p[i] = pstr(p[i]) p._protection = protection p._evaluation = evaluation return p
def update(self, **kwargs)
-
Update multiple fields at once, while protecting certain attributes.
Parameters:
**kwargs : dict The fields to update and their new values.
Protected attributes defined in _excludedattr are not updated.
Usage:
s.update(a=10, b=[1, 2, 3], new_field="new_value")
Expand source code
def update(self, **kwargs): """ Update multiple fields at once, while protecting certain attributes. Parameters: ----------- **kwargs : dict The fields to update and their new values. Protected attributes defined in _excludedattr are not updated. Usage: ------ s.update(a=10, b=[1, 2, 3], new_field="new_value") """ protected_attributes = getattr(self, '_excludedattr', ()) for key, value in kwargs.items(): if key in protected_attributes: print(f"Warning: Cannot update protected attribute '{key}'") else: self.setattr(key, value)
def validkeys(self, list_of_keys)
-
Validate and return the subset of keys from the provided list that are valid in the instance.
Parameters:
list_of_keys : list A list of keys (as strings) to check against the instance’s attributes.
Returns:
list A list of keys from list_of_keys that are valid (i.e., exist as attributes in the instance).
Raises:
TypeError If list_of_keys is not a list or if any element in list_of_keys is not a string.
Example:
>>> s = struct() >>> s.foo = 42 >>> s.bar = "hello" >>> valid = s.validkeys(["foo", "bar", "baz"]) >>> print(valid) # Output: ['foo', 'bar'] assuming 'baz' is not defined in s
Expand source code
def validkeys(self, list_of_keys): """ Validate and return the subset of keys from the provided list that are valid in the instance. Parameters: ----------- list_of_keys : list A list of keys (as strings) to check against the instance’s attributes. Returns: -------- list A list of keys from list_of_keys that are valid (i.e., exist as attributes in the instance). Raises: ------- TypeError If list_of_keys is not a list or if any element in list_of_keys is not a string. Example: -------- >>> s = struct() >>> s.foo = 42 >>> s.bar = "hello" >>> valid = s.validkeys(["foo", "bar", "baz"]) >>> print(valid) # Output: ['foo', 'bar'] assuming 'baz' is not defined in s """ # Check that list_of_keys is a list if not isinstance(list_of_keys, list): raise TypeError("list_of_keys must be a list") # Check that every entry in the list is a string for key in list_of_keys: if not isinstance(key, str): raise TypeError("Each key in list_of_keys must be a string") # Assuming valid keys are those present in the instance's __dict__ return [key for key in list_of_keys if key in self]
def values(self)
-
return the values
Expand source code
def values(self): """ return the values """ # values() is used by struct() and its iterator return [pstr.eval(value) for key,value in self.__dict__.items() if key not in self._excludedattr]
def write(self, file, overwrite=True, mkdir=False)
-
write the equivalent structure (not recursive for nested struct) write(filename, overwrite=True, mkdir=False)
Parameters: - file: The file path to write to. - overwrite: Whether to overwrite the file if it exists (default: True). - mkdir: Whether to create the directory if it doesn't exist (default: False).
Expand source code
def write(self, file, overwrite=True, mkdir=False): """ write the equivalent structure (not recursive for nested struct) write(filename, overwrite=True, mkdir=False) Parameters: - file: The file path to write to. - overwrite: Whether to overwrite the file if it exists (default: True). - mkdir: Whether to create the directory if it doesn't exist (default: False). """ # Create a Path object for the file to handle cross-platform paths file_path = Path(file).resolve() # Check if the directory exists or if mkdir is set to True, create it if mkdir: file_path.parent.mkdir(parents=True, exist_ok=True) elif not file_path.parent.exists(): raise FileNotFoundError(f"The directory {file_path.parent} does not exist.") # If overwrite is False and the file already exists, raise an exception if not overwrite and file_path.exists(): raise FileExistsError(f"The file {file_path} already exists, and overwrite is set to False.") # Convert to static if needed if isinstance(p,(param,paramauto)): tmp = self.tostatic() else: tmp = self # Open and write to the file using the resolved path with file_path.open(mode="w", encoding='utf-8') as f: print(f"# {self._fulltype} with {len(self)} {self._ftype}s\n", file=f) for k, v in tmp.items(): if v is None: print(k, "=None", file=f, sep="") elif isinstance(v, (int, float)): print(k, "=", v, file=f, sep="") elif isinstance(v, str): print(k, '="', v, '"', file=f, sep="") else: print(k, "=", str(v), file=f, sep="")
def zip(self)
-
zip keys and values
Expand source code
def zip(self): """ zip keys and values """ return zip(self.keys(),self.values())