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 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.

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 the dump 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 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.

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 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.

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 and paramauto for evaluation and dependency handling.

Examples

Basic Usage

s = struct(a=1, b=2, c='${a} + ${b} # evaluate me if you can')
print(s.a)  # 1
s.d = 11    # Append a new field
delattr(s, 'd')  # Delete the field

Using param for Evaluation

p = param(a=1, b=2, c='${a} + ${b} # evaluate me if you can')
p.eval()
# Output:
# --------
#      a: 1
#      b: 2
#      c: ${a} + ${b} # evaluate me if you can (= 3)
# --------

Concatenation and Subtraction

Fields from the right-most structure overwrite existing values.

a = struct(a=1, b=2)
b = struct(c=3, d="d", e="e")
c = a + b
e = c - a

Practical Shorthands

Constructing a Structure from Keys

s = struct.fromkeys(["a", "b", "c", "d"])
# Output:
# --------
#      a: None
#      b: None
#      c: None
#      d: None
# --------

Building a Structure from Variables in a String

s = struct.scan("${a} + ${b} * ${c} / ${d} --- ${ee}")
s.a = 1
s.b = "test"
s.c = [1, "a", 2]
s.generator()
# Output:
# X = struct(
#      a=1,
#      b="test",
#      c=[1, 'a', 2],
#      d=None,
#      ee=None
# )

Indexing and Iteration

Structures can be indexed or sliced like lists.

c = a + b
c[0]      # Access the first field
c[-1]     # Access the last field
c[:2]     # Slice the structure
for field in c:
    print(field)

Dynamic Dependency Management

struct provides control over dependencies, sorting, and evaluation.

s = struct(d=3, e="${c} + {d}", c='${a} + ${b}', a=1, b=2)
s.sortdefinitions()
# Output:
# --------
#      d: 3
#      a: 1
#      b: 2
#      c: ${a} + ${b}
#      e: ${c} + ${d}
# --------

For dynamic evaluation, use param:

p = param(sortdefinitions=True, d=3, e="${c} + ${d}", c='${a} + ${b}', a=1, b=2)
# Output:
# --------
#      d: 3
#      a: 1
#      b: 2
#      c: ${a} + ${b}  (= 3)
#      e: ${c} + ${d}  (= 6)
# --------

Overloaded Methods and Operators

Supported Operators

  • +: Concatenation of two structures (__add__).
  • -: Subtraction of fields (__sub__).
  • <<: Import values from another structure (__lshift__)
  • len(): Number of fields (__len__).
  • in: Check for field existence (__contains__).

Method Overview

Method Description
check(default) Populate fields with defaults if missing.
clear() Remove all fields.
dict2struct(dico) Create a structure from a dictionary.
disp() Display the structure.
eval() Evaluate expressions within fields.
fromkeys(keys) Create a structure from a list of keys.
generator() Generate Python code representing the structure.
importfrom() Import undefined values from another struct or dict.
items() Return key-value pairs.
keys() Return all keys in the structure.
read(file) Load structure fields from a file.
scan(string) Extract variables from a string and populate fields.
sortdefinitions() Sort fields to resolve dependencies.
struct2dict() Convert the structure to a dictionary.
validkeys() Return valid keys
values() Return all field values.
write(file) Save the structure to a file.

Dynamic Properties

Property Description
isempty True if the structure is empty.
isdefined True if all fields are defined.

constructor, use debug=True to report eval errors

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

    A lightweight class that mimics Matlab-like structures, with additional features
    such as dynamic field creation, indexing, concatenation, and compatibility with
    evaluated parameters (`param`).

    ---

    ### Features
    - Dynamic creation of fields.
    - Indexing and iteration support for fields.
    - Concatenation and subtraction of structures.
    - Conversion to and from dictionaries.
    - Compatible with `param` and `paramauto` for evaluation and dependency handling.

    ---

    ### Examples

    #### Basic Usage
    ```python
    s = struct(a=1, b=2, c='${a} + ${b} # evaluate me if you can')
    print(s.a)  # 1
    s.d = 11    # Append a new field
    delattr(s, 'd')  # Delete the field
    ```

    #### Using `param` for Evaluation
    ```python
    p = param(a=1, b=2, c='${a} + ${b} # evaluate me if you can')
    p.eval()
    # Output:
    # --------
    #      a: 1
    #      b: 2
    #      c: ${a} + ${b} # evaluate me if you can (= 3)
    # --------
    ```

    ---

    ### Concatenation and Subtraction
    Fields from the right-most structure overwrite existing values.
    ```python
    a = struct(a=1, b=2)
    b = struct(c=3, d="d", e="e")
    c = a + b
    e = c - a
    ```

    ---

    ### Practical Shorthands

    #### Constructing a Structure from Keys
    ```python
    s = struct.fromkeys(["a", "b", "c", "d"])
    # Output:
    # --------
    #      a: None
    #      b: None
    #      c: None
    #      d: None
    # --------
    ```

    #### Building a Structure from Variables in a String
    ```python
    s = struct.scan("${a} + ${b} * ${c} / ${d} --- ${ee}")
    s.a = 1
    s.b = "test"
    s.c = [1, "a", 2]
    s.generator()
    # Output:
    # X = struct(
    #      a=1,
    #      b="test",
    #      c=[1, 'a', 2],
    #      d=None,
    #      ee=None
    # )
    ```

    #### Indexing and Iteration
    Structures can be indexed or sliced like lists.
    ```python
    c = a + b
    c[0]      # Access the first field
    c[-1]     # Access the last field
    c[:2]     # Slice the structure
    for field in c:
        print(field)
    ```

    ---

    ### Dynamic Dependency Management
    `struct` provides control over dependencies, sorting, and evaluation.

    ```python
    s = struct(d=3, e="${c} + {d}", c='${a} + ${b}', a=1, b=2)
    s.sortdefinitions()
    # Output:
    # --------
    #      d: 3
    #      a: 1
    #      b: 2
    #      c: ${a} + ${b}
    #      e: ${c} + ${d}
    # --------
    ```

    For dynamic evaluation, use `param`:
    ```python
    p = param(sortdefinitions=True, d=3, e="${c} + ${d}", c='${a} + ${b}', a=1, b=2)
    # Output:
    # --------
    #      d: 3
    #      a: 1
    #      b: 2
    #      c: ${a} + ${b}  (= 3)
    #      e: ${c} + ${d}  (= 6)
    # --------
    ```

    ---

    ### Overloaded Methods and Operators
    #### Supported Operators
    - `+`: Concatenation of two structures (`__add__`).
    - `-`: Subtraction of fields (`__sub__`).
    - `<<`: Import values from another structure (`__lshift__`)
    - `len()`: Number of fields (`__len__`).
    - `in`: Check for field existence (`__contains__`).

    #### Method Overview
    | Method                | Description                                             |
    |-----------------------|---------------------------------------------------------|
    | `check(default)`      | Populate fields with defaults if missing.               |
    | `clear()`             | Remove all fields.                                      |
    | `dict2struct(dico)`   | Create a structure from a dictionary.                   |
    | `disp()`              | Display the structure.                                  |
    | `eval()`              | Evaluate expressions within fields.                     |
    | `fromkeys(keys)`      | Create a structure from a list of keys.                 |
    | `generator()`         | Generate Python code representing the structure.        |
    | `importfrom()`        | Import undefined values from another struct or dict.    |
    | `items()`             | Return key-value pairs.                                 |
    | `keys()`              | Return all keys in the structure.                       |
    | `read(file)`          | Load structure fields from a file.                      |
    | `scan(string)`        | Extract variables from a string and populate fields.    |
    | `sortdefinitions()`   | Sort fields to resolve dependencies.                    |
    | `struct2dict()`       | Convert the structure to a dictionary.                  |
    | `validkeys()`         | Return valid keys                                       |
    | `values()`            | Return all field values.                                |
    | `write(file)`         | Save the structure to a file.                           |

    ---

    ### Dynamic Properties
    | Property    | Description                            |
    |-------------|----------------------------------------|
    | `isempty`   | `True` if the structure is empty.      |
    | `isdefined` | `True` if all fields are defined.      |

    ---
    """

    # attributes to be overdefined
    _type = "struct"        # object type
    _fulltype = "structure" # full name
    _ftype = "field"        # field name
    _evalfeature = False    # true if eval() is available
    _maxdisplay = 40        # maximum number of characters to display (should be even)
    _propertyasattribute = False
    _precision = 4
    _needs_sorting = False

    # attributes for the iterator method
    # Please keep it static, duplicate the object before changing _iter_
    _iter_ = 0

    # excluded attributes (keep the , in the Tupple if it is singleton)
    _excludedattr = {'_iter_','__class__','_protection','_evaluation','_returnerror','_debug','_precision','_needs_sorting'} # used by keys() and len()


    # Methods
    def __init__(self,debug=False,**kwargs):
        """ constructor, use debug=True to report eval errors"""
        # Optionally extend _excludedattr here
        self._excludedattr = self._excludedattr | {'_excludedattr', '_type', '_fulltype','_ftype'} # addition 2024-10-11
        self._debug = debug
        self.set(**kwargs)

    def zip(self):
        """ zip keys and values """
        return zip(self.keys(),self.values())

    @staticmethod
    def dict2struct(dico,makeparam=False):
        """ create a structure from a dictionary """
        if isinstance(dico,dict):
            s = param() if makeparam else struct()
            s.set(**dico)
            return s
        raise TypeError("the argument must be a dictionary")

    def struct2dict(self):
        """ create a dictionary from the current structure """
        return dict(self.zip())

    def struct2param(self,protection=False,evaluation=True):
        """ convert an object struct() to param() """
        p = param(**self.struct2dict())
        for i in range(len(self)):
            if isinstance(self[i],pstr): p[i] = pstr(p[i])
        p._protection = protection
        p._evaluation = evaluation
        return p

    def set(self,**kwargs):
        """ initialization """
        self.__dict__.update(kwargs)

    def setattr(self,key,value):
        """ set field and value """
        if isinstance(value,list) and len(value)==0 and key in self:
            delattr(self, key)
        else:
            self.__dict__[key] = value

    def getattr(self,key):
        """Get attribute override to access both instance attributes and properties if allowed."""
        if key in self.__dict__:
            return self.__dict__[key]
        elif getattr(self, '_propertyasattribute', False) and \
             key not in self._excludedattr and \
             key in self.__class__.__dict__ and isinstance(self.__class__.__dict__[key], property):
            # If _propertyasattribute is True and it's a property, get its value
            return self.__class__.__dict__[key].fget(self)
        else:
            raise AttributeError(f'the {self._ftype} "{key}" does not exist')

    def hasattr(self, key):
        """Return true if the field exists, considering properties as regular attributes if allowed."""
        return key in self.__dict__ or (
            getattr(self, '_propertyasattribute', False) and
            key not in self._excludedattr and
            key in self.__class__.__dict__ and isinstance(self.__class__.__dict__[key], property)
        )

    def __getstate__(self):
        """ getstate for cooperative inheritance / duplication """
        return self.__dict__.copy()

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

    def __getattr__(self,key):
        """ get attribute override """
        return pstr.eval(self.getattr(key))

    def __setattr__(self,key,value):
        """ set attribute override """
        self.setattr(key,value)

    def __contains__(self,item):
        """ in override """
        return self.hasattr(item)

    def keys(self):
        """ return the fields """
        # keys() is used by struct() and its iterator
        return [key for key in self.__dict__.keys() if key not in self._excludedattr]

    def keyssorted(self,reverse=True):
        """ sort keys by length() """
        klist = self.keys()
        l = [len(k) for k in klist]
        return [k for _,k in sorted(zip(l,klist),reverse=reverse)]

    def values(self):
        """ return the values """
        # values() is used by struct() and its iterator
        return [pstr.eval(value) for key,value in self.__dict__.items() if key not in self._excludedattr]

    @staticmethod
    def fromkeysvalues(keys,values,makeparam=False):
        """ struct.keysvalues(keys,values) creates a structure from keys and values
            use makeparam = True to create a param instead of struct
        """
        if keys is None: raise AttributeError("the keys must not empty")
        if not isinstance(keys,_list_types): keys = [keys]
        if not isinstance(values,_list_types): values = [values]
        nk,nv = len(keys), len(values)
        s = param() if makeparam else struct()
        if nk>0 and nv>0:
            iv = 0
            for ik in range(nk):
                s.setattr(keys[ik], values[iv])
                iv = min(nv-1,iv+1)
            for ik in range(nk,nv):
                s.setattr(f"key{ik}", values[ik])
        return s

    def items(self):
        """ return all elements as iterable key, value """
        return self.zip()

    def __getitem__(self,idx):
        """
            s[i] returns the ith element of the structure
            s[:4] returns a structure with the four first fields
            s[[1,3]] returns the second and fourth elements
        """
        if isinstance(idx,int):
            if idx<len(self):
                return self.getattr(self.keys()[idx])
            raise IndexError(f"the {self._ftype} index should be comprised between 0 and {len(self)-1}")
        elif isinstance(idx,slice):
            out = struct.fromkeysvalues(self.keys()[idx], self.values()[idx])
            if isinstance(self,paramauto):
                return paramauto(**out)
            elif isinstance(self,param):
                return param(**out)
            else:
                return out
        elif isinstance(idx,(list,tuple)):
            k,v= self.keys(), self.values()
            nk = len(k)
            s = param() if isinstance(self,param) else struct()
            for i in idx:
                if isinstance(i,int):
                    if -nk <= i < nk:  # Allow standard Python negative indexing
                        i = i % nk  # Convert negative index to positive equivalent
                        s.setattr(k[i],v[i])
                    else:
                        raise IndexError(f"idx must contain integers in range [-{nk}, {nk-1}], not {i}")
                elif isinstance(i,str):
                    if i in self:
                        s.setattr(i, self.getattr(i))
                    else:
                        raise KeyError((f'idx "{idx}" is not a valid key'))
                else:
                    TypeError("idx must contain only integers or strings")
            return s
        elif isinstance(idx,str):
            return self.getattr(idx)
        else:
            raise TypeError("The index must be an integer or a slice and not a %s" % type(idx).__name__)

    def __setitem__(self,idx,value):
        """ set the ith element of the structure  """
        if isinstance(idx,int):
            if idx<len(self):
                self.setattr(self.keys()[idx], value)
            else:
                raise IndexError(f"the {self._ftype} index should be comprised between 0 and {len(self)-1}")
        elif isinstance(idx,slice):
            k = self.keys()[idx]
            if len(value)<=1:
                for i in range(len(k)): self.setattr(k[i], value)
            elif len(k) == len(value):
                for i in range(len(k)): self.setattr(k[i], value[i])
            else:
                raise IndexError("the number of values (%d) does not match the number of elements in the slice (%d)" \
                       % (len(value),len(idx)))
        elif isinstance(idx,(list,tuple)):
            if len(value)<=1:
                for i in range(len(idx)): self[idx[i]]=value
            elif len(idx) == len(value):
                for i in range(len(idx)): self[idx[i]]=value[i]
            else:
                raise IndexError("the number of values (%d) does not match the number of indices (%d)" \
                                 % (len(value),len(idx)))

    def __len__(self):
        """ return the number of fields """
        # len() is used by struct() and its iterator
        return len(self.keys())

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

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

    def __add__(self, s, sortdefinitions=False, raiseerror=True, silentmode=True):
        """
        Add two structure objects, with precedence as follows:

          paramauto > param > struct

        In c = a + b, if b has a higher precedence than a then c will be of b's class,
        otherwise it will be of a's class.

        The new instance is created by copying the fields from the left-hand operand (a)
        and then updating with the fields from the right-hand operand (b).

        If self or s is of class paramauto, the current state of _needs_sorting is propagated
        but not forced to be true.

        """
        if not isinstance(s, struct):
            raise TypeError(f"the second operand must be {self._type}")

        # Define a helper to assign a precedence value.
        def get_precedence(obj):
            if isinstance(obj, paramauto):
                return 2
            elif isinstance(obj, param):
                return 1
            elif isinstance(obj, struct):
                return 0
            else:
                return 0  # fallback for unknown derivations

        # current classes
        leftprecedence = get_precedence(self)
        rightprecedence = get_precedence(s)
        # Determine which class to use for the duplicate.
        # If s (b) has a higher precedence than self (a), use s's class; otherwise, use self's.
        hi_class = self.__class__ if leftprecedence >= rightprecedence else s.__class__
        # Create a new instance of the chosen class by copying self's fields.
        dup = hi_class(**self)
        # Update with the fields from s.
        dup.update(**s)
        if sortdefinitions: # defer sorting by preserving the state of _needs_sorting
            if leftprecedence < rightprecedence == 2: # left is promoted
                dup._needs_sorting = s._needs_sorting
            elif rightprecedence < leftprecedence == 2: # right is promoted
                dup._needs_sorting = self._needs_sorting
            elif leftprecedence == rightprecedence == 2: # left and right are equivalent
                dup._needs_sorting = self._needs_sorting or s._needs_sorting
            # dup.sortdefinitions(raiseerror=raiseerror, silentmode=silentmode)
        return dup

    def __iadd__(self,s,sortdefinitions=False,raiseerror=False, silentmode=True):
        """ iadd a structure
            set sortdefintions=True to sort definitions (to maintain executability)
        """
        if not isinstance(s,struct):
            raise TypeError(f"the second operand must be {self._type}")
        self.update(**s)
        if sortdefinitions:
            self._needs_sorting = True
            # self.sortdefinitions(raiseerror=raiseerror,silentmode=silentmode)
        return self

    def __sub__(self,s):
        """ sub a structure """
        if not isinstance(s,struct):
            raise TypeError(f"the second operand must be {self._type}")
        dup = duplicate(self)
        listofkeys = dup.keys()
        for k in s.keys():
            if k in listofkeys:
                delattr(dup,k)
        return dup

    def __isub__(self,s):
        """ isub a structure """
        if not isinstance(s,struct):
            raise TypeError(f"the second operand must be {self._type}")
        listofkeys = self.keys()
        for k in s.keys():
            if k in listofkeys:
                delattr(self,k)
        return self

    def dispmax(self,content):
        """ optimize display """
        strcontent = str(content)
        if len(strcontent)>self._maxdisplay:
            nchar = round(self._maxdisplay/2)
            return strcontent[:nchar]+" [...] "+strcontent[-nchar:]
        else:
            return content

    def __repr__(self):
        """ display method """
        if self.__dict__=={}:
            print(f"empty {self._fulltype} ({self._type} object) with no {self._type}s")
            return f"empty {self._fulltype}"
        else:
            numfmt = f".{self._precision}g"
            tmp = self.eval() if self._evalfeature else []
            keylengths = [len(key) for key in self.__dict__]
            width = max(10,max(keylengths)+2)
            fmt = "%%%ss:" % width
            fmteval = fmt[:-1]+"="
            fmtcls =  fmt[:-1]+":"
            line = ( fmt % ('-'*(width-2)) ) + ( '-'*(min(40,width*5)) )
            print(line)
            for key,value in self.__dict__.items():
                if key not in self._excludedattr:
                    if isinstance(value,_numeric_types):
                        # old code (removed on 2025-01-18)
                        # if isinstance(value,pstr):
                        #     print(fmt % key,'p"'+self.dispmax(value)+'"')
                        # if isinstance(value,str) and value=="":
                        #     print(fmt % key,'""')
                        # else:
                        #     print(fmt % key,self.dispmax(value))
                        if isinstance(value,np.ndarray):
                            print(fmt % key, struct.format_array(value,numfmt=numfmt))
                        else:
                            print(fmt % key,self.dispmax(value))
                    elif isinstance(value,struct):
                        print(fmt % key,self.dispmax(value.__str__()))
                    elif isinstance(value,(type,dict)):
                        print(fmt % key,self.dispmax(str(value)))
                    else:
                        print(fmt % key,type(value))
                        print(fmtcls % "",self.dispmax(str(value)))
                    if self._evalfeature:
                        if isinstance(self,paramauto):
                            try:
                                if isinstance(value,pstr):
                                    print(fmteval % "",'p"'+self.dispmax(tmp.getattr(key))+'"')
                                elif isinstance(value,str):
                                    if value == "":
                                        print(fmteval % "",self.dispmax("<empty string>"))
                                    else:
                                        print(fmteval % "",self.dispmax(tmp.getattr(key)))
                            except Exception as err:
                                print(fmteval % "",err.message, err.args)
                        else:
                            if isinstance(value,pstr):
                                print(fmteval % "",'p"'+self.dispmax(tmp.getattr(key))+'"')
                            elif isinstance(value,str):
                                if value == "":
                                    print(fmteval % "",self.dispmax("<empty string>"))
                                else:
                                    calcvalue =tmp.getattr(key)
                                    if isinstance(calcvalue, str) and "error" in calcvalue.lower():
                                        print(fmteval % "",calcvalue)
                                    else:
                                        if isinstance(calcvalue,np.ndarray):
                                            print(fmteval % "", struct.format_array(calcvalue,numfmt=numfmt))
                                        else:
                                            print(fmteval % "",self.dispmax(calcvalue))
                            elif isinstance(value,list):
                                calcvalue =tmp.getattr(key)
                                print(fmteval % "",self.dispmax(str(calcvalue)))
            print(line)
            return f"{self._fulltype} ({self._type} object) with {len(self)} {self._ftype}s"

    def disp(self):
        """ display method """
        self.__repr__()

    def __str__(self):
        return f"{self._fulltype} ({self._type} object) with {len(self)} {self._ftype}s"

    @property
    def isempty(self):
        """ isempty is set to True for an empty structure """
        return len(self)==0

    def clear(self):
        """ clear() delete all fields while preserving the original class """
        for k in self.keys(): delattr(self,k)

    def format(self, s, escape=False, raiseerror=True):
        """
            Format a string with fields using {field} as placeholders.
            Handles expressions like ${variable1}.

            Args:
                s (str): The input string to format.
                escape (bool): If True, prevents replacing '${' with '{'.
                raiseerror (bool): If True, raises errors for missing fields.

            Note:
                NumPy vectors and matrices are converted into their text representation (default behavior)
                If expressions such ${var[1,2]} are used an error is expected, the original content will be used instead

            Returns:
                str: The formatted string.
        """
        tmp = self.np2str()
        if raiseerror:
            try:
                if escape:
                    try: # we try to evaluate with all np objects converted in to strings (default)
                        return s.format_map(AttrErrorDict(tmp.__dict__))
                    except: # if an error occurs, we use the orginal content
                        return s.format_map(AttrErrorDict(self.__dict__))
                else:
                    try: # we try to evaluate with all np objects converted in to strings (default)
                        return s.replace("${", "{").format_map(AttrErrorDict(tmp.__dict__))
                    except: # if an error occurs, we use the orginal content
                        return s.replace("${", "{").format_map(AttrErrorDict(self.__dict__))
            except AttributeError as attr_err:
                # Handle AttributeError for expressions with operators
                s_ = s.replace("{", "${")
                if self._debug:
                    print(f"WARNING: the {self._ftype} {attr_err} is undefined in '{s_}'")
                return s_  # Revert to using '${' for unresolved expressions
            except IndexError as idx_err:
                s_ = s.replace("{", "${")
                if self._debug:
                    print(f"Index Error {idx_err} in '{s_}'")
                raise IndexError from idx_err
            except Exception as other_err:
                s_ = s.replace("{", "${")
                raise RuntimeError from other_err
        else:
            if escape:
                try: # we try to evaluate with all np objects converted in to strings (default)
                    return s.format_map(AttrErrorDict(tmp.__dict__))
                except: # if an error occurs, we use the orginal content
                    return s.format_map(AttrErrorDict(self.__dict__))
            else:
                try: # we try to evaluate with all np objects converted in to strings (default)
                    return s.replace("${", "{").format_map(AttrErrorDict(tmp.__dict__))
                except:  # if an error occurs, we use the orginal content
                    return s.replace("${", "{").format_map(AttrErrorDict(self.__dict__))

    def format_legacy(self,s,escape=False,raiseerror=True):
        """
            format a string with field (use {field} as placeholders)
                s.replace(string), s.replace(string,escape=True)
                where:
                    s is a struct object
                    string is a string with possibly ${variable1}
                    escape is a flag to prevent ${} replaced by {}
        """
        if raiseerror:
            try:
                if escape:
                    return s.format(**self.__dict__)
                else:
                    return s.replace("${","{").format(**self.__dict__)
            except KeyError as kerr:
                s_ = s.replace("{","${")
                print(f"WARNING: the {self._ftype} {kerr} is undefined in '{s_}'")
                return s_ # instead of s (we put back $) - OV 2023/01/27
            except Exception as othererr:
                s_ = s.replace("{","${")
                raise RuntimeError from othererr
        else:
            if escape:
                return s.format(**self.__dict__)
            else:
                return s.replace("${","{").format(**self.__dict__)

    def fromkeys(self,keys):
        """ returns a structure from keys """
        return self+struct(**dict.fromkeys(keys,None))

    @staticmethod
    def scan(s):
        """ scan(string) scan a string for variables """
        if not isinstance(s,str): raise TypeError("scan() requires a string")
        tmp = struct()
        #return tmp.fromkeys(set(re.findall(r"\$\{(.*?)\}",s)))
        found = re.findall(r"\$\{(.*?)\}",s);
        uniq = []
        for x in found:
            if x not in uniq: uniq.append(x)
        return tmp.fromkeys(uniq)

    @staticmethod
    def isstrexpression(s):
        """ isstrexpression(string) returns true if s contains an expression  """
        if not isinstance(s,str): raise TypeError("s must a string")
        return re.search(r"\$\{.*?\}",s) is not None

    @property
    def isexpression(self):
        """ same structure with True if it is an expression """
        s = param() if isinstance(self,param) else struct()
        for k,v in self.items():
            if isinstance(v,str):
                s.setattr(k,struct.isstrexpression(v))
            else:
                s.setattr(k,False)
        return s

    @staticmethod
    def isstrdefined(s,ref):
        """ isstrdefined(string,ref) returns true if it is defined in ref  """
        if not isinstance(s,str): raise TypeError("s must a string")
        if not isinstance(ref,struct): raise TypeError("ref must be a structure")
        if struct.isstrexpression(s):
            k = struct.scan(s).keys()
            allfound,i,nk = True,0,len(k)
            while (i<nk) and allfound:
                allfound = k[i] in ref
                i += 1
            return allfound
        else:
            return False


    def isdefined(self,ref=None):
        """ isdefined(ref) returns true if it is defined in ref """
        s = param() if isinstance(self,param) else struct()
        k,v,isexpr = self.keys(), self.values(), self.isexpression.values()
        nk = len(k)
        if ref is None:
            for i in range(nk):
                if isexpr[i]:
                    s.setattr(k[i],struct.isstrdefined(v[i],self[:i]))
                else:
                    s.setattr(k[i],True)
        else:
            if not isinstance(ref,struct): raise TypeError("ref must be a structure")
            for i in range(nk):
                if isexpr[i]:
                    s.setattr(k[i],struct.isstrdefined(v[i],ref))
                else:
                    s.setattr(k[i],True)
        return s


    def sortdefinitions(self,raiseerror=True,silentmode=False):
        """ sortdefintions sorts all definitions
            so that they can be executed as param().
            If any inconsistency is found, an error message is generated.

            Flags = default values
                raiseerror=True show erros of True
                silentmode=False no warning if True
        """
        find = lambda xlist: [i for i, x in enumerate(xlist) if x]
        findnot = lambda xlist: [i for i, x in enumerate(xlist) if not x]
        k,v,isexpr =  self.keys(), self.values(), self.isexpression.values()
        istatic = findnot(isexpr)
        idynamic = find(isexpr)
        static = struct.fromkeysvalues(
            [ k[i] for i in istatic ],
            [ v[i] for i in istatic ],
            makeparam = False)
        dynamic = struct.fromkeysvalues(
            [ k[i] for i in idynamic ],
            [ v[i] for i in idynamic ],
            makeparam=False)
        current = static # make static the current structure
        nmissing, anychange, errorfound = len(dynamic), False, False
        while nmissing:
            itst, found = 0, False
            while itst<nmissing and not found:
                teststruct = current + dynamic[[itst]] # add the test field
                found = all(list(teststruct.isdefined()))
                ifound = itst
                itst += 1
            if found:
                current = teststruct # we accept the new field
                dynamic[ifound] = []
                nmissing -= 1
                anychange = True
            else:
                if raiseerror:
                    raise KeyError('unable to interpret %d/%d expressions in "%ss"' % \
                                   (nmissing,len(self),self._ftype))
                else:
                    if (not errorfound) and (not silentmode):
                        print('WARNING: unable to interpret %d/%d expressions in "%ss"' % \
                              (nmissing,len(self),self._ftype))
                    current = teststruct # we accept the new field (even if it cannot be interpreted)
                    dynamic[ifound] = []
                    nmissing -= 1
                    errorfound = True
        if anychange:
            self.clear() # reset all fields and assign them in the proper order
            k,v = current.keys(), current.values()
            for i in range(len(k)):
                self.setattr(k[i],v[i])


    def generator(self, printout=False):
        """
        Generate Python code of the equivalent structure.

        This method converts the current structure (an instance of `param`, `paramauto`, or `struct`)
        into Python code that, when executed, recreates an equivalent structure. The generated code is
        formatted with one field per line.

        By default (when `printout` is False), the generated code is returned as a raw string that starts
        directly with, for example, `param(` (or `paramauto(` or `struct(`), with no "X = " prefix or leading
        newline. When `printout` is True, the generated code is printed to standard output and includes a prefix
        "X = " to indicate the variable name.

        Parameters:
            printout (bool): If True, the generated code is printed to standard output with the "X = " prefix.
                             If False (default), the code is returned as a raw string starting with, e.g.,
                             `param(`.

        Returns:
            str: The generated Python code representing the structure (regardless of whether it was printed).
        """
        nk = len(self)
        tmp = self.np2str()
        # Compute the field format based on the maximum key length (with a minimum width of 10)
        fmt = "%%%ss =" % max(10, max([len(k) for k in self.keys()]) + 2)
        # Determine the appropriate class string for the current instance.
        if isinstance(self, param):
            classstr = "param"
        elif 'paramauto' in globals() and isinstance(self, paramauto):
            classstr = "paramauto"
        else:
            classstr = "struct"

        lines = []
        if nk == 0:
            # For an empty structure.
            if printout:
                lines.append(f"X = {classstr}()")
            else:
                lines.append(f"{classstr}()")
        else:
            # Header: include "X = " only if printing.
            if printout:
                header = f"X = {classstr}("
            else:
                header = f"{classstr}("
            lines.append(header)
            # Iterate over keys to generate each field line.
            for i, k in enumerate(self.keys()):
                v = getattr(self, k)
                if isinstance(v, np.ndarray):
                    vtmp = getattr(tmp, k)
                    field = fmt % k + " " + vtmp
                elif isinstance(v, (int, float)) or v is None:
                    field = fmt % k + " " + str(v)
                elif isinstance(v, str):
                    field = fmt % k + " " + f'"{v}"'
                elif isinstance(v, (list, tuple, dict)):
                    field = fmt % k + " " + str(v)
                else:
                    field = fmt % k + " " + "/* unsupported type */"
                # Append a comma after each field except the last one.
                if i < nk - 1:
                    field += ","
                lines.append(field)
            # Create a closing line that aligns the closing parenthesis.
            closing_line = fmt[:-1] % ")"
            lines.append(closing_line)
        result = "\n".join(lines)
        if printout:
            print(result)
            return None
        return result


    # copy and deep copy methpds for the class
    def __copy__(self):
        """ copy method """
        cls = self.__class__
        copie = cls.__new__(cls)
        copie.__dict__.update(self.__dict__)
        return copie

    def __deepcopy__(self, memo):
        """ deep copy method """
        cls = self.__class__
        copie = cls.__new__(cls)
        memo[id(self)] = copie
        for k, v in self.__dict__.items():
            setattr(copie, k, duplicatedeep(v, memo))
        return copie


    # write a file
    def write(self, file, overwrite=True, mkdir=False):
        """
            write the equivalent structure (not recursive for nested struct)
                write(filename, overwrite=True, mkdir=False)

            Parameters:
            - file: The file path to write to.
            - overwrite: Whether to overwrite the file if it exists (default: True).
            - mkdir: Whether to create the directory if it doesn't exist (default: False).
        """
        # Create a Path object for the file to handle cross-platform paths
        file_path = Path(file).resolve()

        # Check if the directory exists or if mkdir is set to True, create it
        if mkdir:
            file_path.parent.mkdir(parents=True, exist_ok=True)
        elif not file_path.parent.exists():
            raise FileNotFoundError(f"The directory {file_path.parent} does not exist.")
        # If overwrite is False and the file already exists, raise an exception
        if not overwrite and file_path.exists():
            raise FileExistsError(f"The file {file_path} already exists, and overwrite is set to False.")
        # Convert to static if needed
        if isinstance(p,(param,paramauto)):
            tmp = self.tostatic()
        else:
            tmp = self
        # Open and write to the file using the resolved path
        with file_path.open(mode="w", encoding='utf-8') as f:
            print(f"# {self._fulltype} with {len(self)} {self._ftype}s\n", file=f)
            for k, v in tmp.items():
                if v is None:
                    print(k, "=None", file=f, sep="")
                elif isinstance(v, (int, float)):
                    print(k, "=", v, file=f, sep="")
                elif isinstance(v, str):
                    print(k, '="', v, '"', file=f, sep="")
                else:
                    print(k, "=", str(v), file=f, sep="")


    # read a file
    @staticmethod
    def read(file):
        """
            read the equivalent structure
                read(filename)

            Parameters:
            - file: The file path to read from.
        """
        # Create a Path object for the file to handle cross-platform paths
        file_path = Path(file).resolve()
        # Check if the parent directory exists, otherwise raise an error
        if not file_path.parent.exists():
            raise FileNotFoundError(f"The directory {file_path.parent} does not exist.")
        # If the file does not exist, raise an exception
        if not file_path.exists():
            raise FileNotFoundError(f"The file {file_path} does not exist.")
        # Open and read the file
        with file_path.open(mode="r", encoding="utf-8") as f:
            s = struct()  # Assuming struct is defined elsewhere
            while True:
                line = f.readline()
                if not line:
                    break
                line = line.strip()
                expr = line.split(sep="=")
                if len(line) > 0 and line[0] != "#" and len(expr) > 0:
                    lhs = expr[0]
                    rhs = "".join(expr[1:]).strip()
                    if len(rhs) == 0 or rhs == "None":
                        v = None
                    else:
                        v = eval(rhs)
                    s.setattr(lhs, v)
        return s

    # argcheck
    def check(self,default):
        """
        populate fields from a default structure
            check(defaultstruct)
            missing field, None and [] values are replaced by default ones

            Note: a.check(b) is equivalent to b+a except for [] and None values
        """
        if not isinstance(default,struct):
            raise TypeError("the first argument must be a structure")
        for f in default.keys():
            ref = default.getattr(f)
            if f not in self:
                self.setattr(f, ref)
            else:
                current = self.getattr(f)
                if ((current is None)  or (current==[])) and \
                    ((ref is not None) and (ref!=[])):
                        self.setattr(f, ref)


    # update values based on key:value
    def update(self, **kwargs):
        """
        Update multiple fields at once, while protecting certain attributes.

        Parameters:
        -----------
        **kwargs : dict
            The fields to update and their new values.

        Protected attributes defined in _excludedattr are not updated.

        Usage:
        ------
        s.update(a=10, b=[1, 2, 3], new_field="new_value")
        """
        protected_attributes = getattr(self, '_excludedattr', ())
        for key, value in kwargs.items():
            if key in protected_attributes:
                print(f"Warning: Cannot update protected attribute '{key}'")
            else:
                self.setattr(key, value)


    # override () for subindexing structure with key names
    def __call__(self, *keys):
        """
        Extract a sub-structure based on the specified keys,
        keeping the same class type.

        Parameters:
        -----------
        *keys : str
            The keys for the fields to include in the sub-structure.

        Returns:
        --------
        struct
            A new instance of the same class as the original, containing
            only the specified keys.

        Usage:
        ------
        sub_struct = s('key1', 'key2', ...)
        """
        # Create a new instance of the same class
        sub_struct = self.__class__()

        # Get the full type and field type for error messages
        fulltype = getattr(self, '_fulltype', 'structure')
        ftype = getattr(self, '_ftype', 'field')

        # Add only the specified keys to the new sub-structure
        for key in keys:
            if key in self:
                sub_struct.setattr(key, self.getattr(key))
            else:
                raise KeyError(f"{fulltype} does not contain the {ftype} '{key}'.")

        return sub_struct


    def __delattr__(self, key):
        """ Delete an instance attribute if it exists and is not a class or excluded attribute. """
        if key in self._excludedattr:
            raise AttributeError(f"Cannot delete excluded attribute '{key}'")
        elif key in self.__class__.__dict__:  # Check if it's a class attribute
            raise AttributeError(f"Cannot delete class attribute '{key}'")
        elif key in self.__dict__:  # Delete only if in instance's __dict__
            del self.__dict__[key]
        else:
            raise AttributeError(f"{self._type} has no attribute '{key}'")


    # A la Matlab display method of vectors, matrices and ND-arrays
    @staticmethod
    def format_array(value,numfmt=".4g"):
        """
        Format NumPy array for display with distinctions for scalars, row/column vectors, and ND arrays.
        Recursively formats multi-dimensional arrays without introducing unwanted commas.

        Args:
            value (np.ndarray): The NumPy array to format.
            numfmt: numeric format to be used for the string conversion (default=".4g")

        Returns:
            str: A formatted string representation of the array.
        """
        dtype_str = {
            np.float64: "double",
            np.float32: "single",
            np.int32: "int32",
            np.int64: "int64",
            np.complex64: "complex single",
            np.complex128: "complex double",
        }.get(value.dtype.type, str(value.dtype))  # Default to dtype name if not in the map

        max_display = 10  # Maximum number of elements to display

        def format_recursive(arr):
            """
            Recursively formats the array based on its dimensions.

            Args:
                arr (np.ndarray): The array or sub-array to format.

            Returns:
                str: Formatted string of the array.
            """
            if arr.ndim == 0:
                return f"{arr.item()}"

            if arr.ndim == 1:
                if len(arr) <= max_display:
                    return "[" + " ".join(f"{v:{numfmt}}" for v in arr) + "]"
                else:
                    return f"[{len(arr)} elements]"

            if arr.ndim == 2:
                if arr.shape[1] == 1:
                    # Column vector
                    if arr.shape[0] <= max_display:
                        return "[" + " ".join(f"{v[0]:{numfmt}}" for v in arr) + "]T"
                    else:
                        return f"[{arr.shape[0]}×1 vector]"
                elif arr.shape[0] == 1:
                    # Row vector
                    if arr.shape[1] <= max_display:
                        return "[" + " ".join(f"{v:{numfmt}}" for v in arr[0]) + "]"
                    else:
                        return f"[1×{arr.shape[1]} vector]"
                else:
                    # General matrix
                    return f"[{arr.shape[0]}×{arr.shape[1]} matrix]"

            # For higher dimensions
            shape_str = "×".join(map(str, arr.shape))
            if arr.size <= max_display:
                # Show full content
                if arr.ndim > 2:
                    # Represent multi-dimensional arrays with nested brackets
                    return "[" + " ".join(format_recursive(subarr) for subarr in arr) + f"] ({shape_str} {dtype_str})"
            return f"[{shape_str} array ({dtype_str})]"

        if value.size == 0:
            return "[]"

        if value.ndim == 0 or value.size == 1:
            return f"{value.item()} ({dtype_str})"

        if value.ndim == 1 or value.ndim == 2:
            # Use existing logic for vectors and matrices
            if value.ndim == 1:
                if len(value) <= max_display:
                    formatted = "[" + " ".join(f"{v:{numfmt}}" for v in value) + f"] ({dtype_str})"
                else:
                    formatted = f"[{len(value)}×1 {dtype_str}]"
            elif value.ndim == 2:
                rows, cols = value.shape
                if cols == 1:  # Column vector
                    if rows <= max_display:
                        formatted = "[" + " ".join(f"{v[0]:{numfmt}}" for v in value) + f"]T ({dtype_str})"
                    else:
                        formatted = f"[{rows}×1 {dtype_str}]"
                elif rows == 1:  # Row vector
                    if cols <= max_display:
                        formatted = "[" + " ".join(f"{v:{numfmt}}" for v in value[0]) + f"] ({dtype_str})"
                    else:
                        formatted = f"[1×{cols} {dtype_str}]"
                else:  # General matrix
                    formatted = f"[{rows}×{cols} {dtype_str}]"
            return formatted

        # For higher-dimensional arrays
        if value.size <= max_display:
            formatted = format_recursive(value)
        else:
            shape_str = "×".join(map(str, value.shape))
            formatted = f"[{shape_str} array ({dtype_str})]"

        return formatted


    # convert all NumPy entries to "nestable" expressions
    def np2str(self):
        """ Convert all NumPy entries of s into their string representations, handling both lists and dictionaries. """
        out = struct()
        def format_numpy_result(value):
            """
            Converts a NumPy array or scalar into a string representation:
            - Scalars and single-element arrays (any number of dimensions) are returned as scalars without brackets.
            - Arrays with more than one element are formatted with proper nesting and commas to make them valid `np.array()` inputs.
            - If the value is a list or dict, the conversion is applied recursively.
            - Non-ndarray inputs that are not list/dict are returned without modification.

            Args:
                value (np.ndarray, scalar, list, dict, or other): The value to format.

            Returns:
                str, list, dict, or original type: A properly formatted string for NumPy arrays/scalars,
                a recursively converted list/dict, or the original value.
            """
            if isinstance(value, dict):
                # Recursively process each key in the dictionary.
                new_dict = {}
                for k, v in value.items():
                    new_dict[k] = format_numpy_result(v)
                return new_dict
            elif isinstance(value, list):
                # Recursively process each element in the list.
                return [format_numpy_result(x) for x in value]
            elif isinstance(value, tuple):
                return tuple(format_numpy_result(x) for x in value)
            elif isinstance(value, struct):
                return value.npstr()
            elif np.isscalar(value):
                # For scalars: if numeric, use str() to avoid extra quotes.
                if isinstance(value, (int, float, complex, str)) or value is None:
                    return value
                else:
                    return repr(value)
            elif isinstance(value, np.ndarray):
                # Check if the array has exactly one element.
                if value.size == 1:
                    # Extract the scalar value.
                    return repr(value.item())
                # Convert the array to a nested list.
                nested_list = value.tolist()
                # Recursively format the nested list into a valid string.
                def list_to_string(lst):
                    if isinstance(lst, list):
                        return "[" + ",".join(list_to_string(item) for item in lst) + "]"
                    else:
                        return repr(lst)
                return list_to_string(nested_list)
            else:
                # Return the input unmodified if not a NumPy array, list, dict, or scalar.
                return str(value) # str() preferred over repr() for concision
        # Process all entries in self.
        for key, value in self.items():
            out.setattr(key, format_numpy_result(value))
        return out

    # minimal replacement of placeholders by numbers or their string representations
    def numrepl(self, text):
        r"""
        Replace all placeholders of the form ${key} in the given text by the corresponding
        numeric value from the instance fields, under the following conditions:

        1. 'key' must be a valid field in self (i.e., if key in self).
        2. The value corresponding to 'key' is either:
             - an int,
             - a float, or
             - a string that represents a valid number (e.g., "1" or "1.0").

        Only when these conditions are met, the placeholder is substituted.
        The conversion preserves the original type: if the stored value is int, then the
        substitution will be done as an integer (e.g., 1 and not 1.0). Otherwise, if it is
        a float then it will be substituted as a float.

        Any placeholder for which the above conditions are not met remains unchanged.

        Placeholders are recognized by the pattern "${<key>}" where <key> is captured as all
        text until the next "}" (optionally allowing whitespace inside the braces).
        """
        # Pattern: match "${", then optional whitespace, capture all characters until "}",
        # then optional whitespace, then "}".
        placeholder_pattern = re.compile(r"\$\{\s*([^}]+?)\s*\}")

        def replace_match(match):
            key = match.group(1)
            # Check if the key exists in self.
            if key in self:
                value = self[key]
                # If the value is already numeric, substitute directly.
                if isinstance(value, (int, float)):
                    return str(value)
                # If the value is a string, try to interpret it as a numeric value.
                elif isinstance(value, str):
                    s = value.strip()
                    # Check if s is a valid integer representation.
                    if re.fullmatch(r"[+-]?\d+", s):
                        try:
                            num = int(s)
                            return str(num)
                        except ValueError:
                            # Should not occur because the regex already matched.
                            return match.group(0)
                    # Check if s is a valid float representation (including scientific notation).
                    elif re.fullmatch(r"[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?", s):
                        try:
                            num = float(s)
                            return str(num)
                        except ValueError:
                            return match.group(0)
            # If key not in self or value is not numeric (or numeric string), leave placeholder intact.
            return match.group(0)

        # Replace all placeholders in the text using the replacer function.
        return placeholder_pattern.sub(replace_match, text)

    # import method
    def importfrom(self, s, nonempty=True, replacedefaultvar=True):
        """
        Import values from 's' into self according to the following rules:

        - Only fields that already exist in self are considered.
        - If s is a dictionary, it is converted to a struct via struct(**s).
        - If the current value of a field in self is empty (None, "", [] or ()),
          then that field is updated from s.
        - If nonempty is True (default), then only non-empty values from s are imported.
        - If replacedefaultvar is True (default), then if a field in self exactly equals
          "${key}" (with key being the field name), it is replaced by the corresponding
          value from s if it is empty.
        - Raises a TypeError if s is not a dict or struct (i.e. if it doesn’t support keys()).
        """
        # If s is a dictionary, convert it to a struct instance.
        if isinstance(s, dict):
            s = struct(**s)
        elif not hasattr(s, "keys"):
            raise TypeError(f"s must be a struct or a dictionary not a type {type(s).__name__}")

        for key in self.keys():
            if key in s:
                s_value = getattr(s, key)
                current_value = getattr(self, key)
                if is_empty(current_value) or (replacedefaultvar and current_value == "${" + key + "}"):
                    if nonempty:
                        if not is_empty(s_value):
                            setattr(self, key, s_value)
                    else:
                        setattr(self, key, s_value)

    # importfrom with copy
    def __lshift__(self, other):
        """
        Allows the syntax:

            s = s1 << s2

        where a new instance is created as a copy of s1 (preserving its type, whether
        struct, param, or paramauto) and then updated with the values from s2 using
        importfrom.
        """
        # Create a new instance preserving the type of self.
        new_instance = type(self)(**{k: getattr(self, k) for k in self.keys()})
        # Import values from other (s2) into the new instance.
        new_instance.importfrom(other)
        return new_instance

    # returns only valid keys
    def validkeys(self, list_of_keys):
        """
        Validate and return the subset of keys from the provided list that are valid in the instance.

        Parameters:
        -----------
        list_of_keys : list
            A list of keys (as strings) to check against the instance’s attributes.

        Returns:
        --------
        list
            A list of keys from list_of_keys that are valid (i.e., exist as attributes in the instance).

        Raises:
        -------
        TypeError
            If list_of_keys is not a list or if any element in list_of_keys is not a string.

        Example:
        --------
        >>> s = struct()
        >>> s.foo = 42
        >>> s.bar = "hello"
        >>> valid = s.validkeys(["foo", "bar", "baz"])
        >>> print(valid)   # Output: ['foo', 'bar'] assuming 'baz' is not defined in s
        """
        # Check that list_of_keys is a list
        if not isinstance(list_of_keys, list):
            raise TypeError("list_of_keys must be a list")

        # Check that every entry in the list is a string
        for key in list_of_keys:
            if not isinstance(key, str):
                raise TypeError("Each key in list_of_keys must be a string")

        # Assuming valid keys are those present in the instance's __dict__
        return [key for key in list_of_keys if key in self]

Subclasses

  • pizza.private.mstruct.param
  • pizza.raster.collection
  • pizza.region.regioncollection
  • pizza.script.scriptobject
  • pizza.script.scriptobjectgroup
  • 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, or struct) into Python code that, when executed, recreates an equivalent structure. The generated code is formatted with one field per line.

By default (when printout is False), the generated code is returned as a raw string that starts directly with, for example, param( (or paramauto( or struct(), with no "X = " prefix or leading newline. When printout is True, the generated code is printed to standard output and includes a prefix "X = " to indicate the variable name.

Parameters

printout (bool): If True, the generated code is printed to standard output with the "X = " prefix. If False (default), the code is returned as a raw string starting with, e.g., param(.

Returns

str
The generated Python code representing the structure (regardless of whether it was printed).
Expand source code
def generator(self, printout=False):
    """
    Generate Python code of the equivalent structure.

    This method converts the current structure (an instance of `param`, `paramauto`, or `struct`)
    into Python code that, when executed, recreates an equivalent structure. The generated code is
    formatted with one field per line.

    By default (when `printout` is False), the generated code is returned as a raw string that starts
    directly with, for example, `param(` (or `paramauto(` or `struct(`), with no "X = " prefix or leading
    newline. When `printout` is True, the generated code is printed to standard output and includes a prefix
    "X = " to indicate the variable name.

    Parameters:
        printout (bool): If True, the generated code is printed to standard output with the "X = " prefix.
                         If False (default), the code is returned as a raw string starting with, e.g.,
                         `param(`.

    Returns:
        str: The generated Python code representing the structure (regardless of whether it was printed).
    """
    nk = len(self)
    tmp = self.np2str()
    # Compute the field format based on the maximum key length (with a minimum width of 10)
    fmt = "%%%ss =" % max(10, max([len(k) for k in self.keys()]) + 2)
    # Determine the appropriate class string for the current instance.
    if isinstance(self, param):
        classstr = "param"
    elif 'paramauto' in globals() and isinstance(self, paramauto):
        classstr = "paramauto"
    else:
        classstr = "struct"

    lines = []
    if nk == 0:
        # For an empty structure.
        if printout:
            lines.append(f"X = {classstr}()")
        else:
            lines.append(f"{classstr}()")
    else:
        # Header: include "X = " only if printing.
        if printout:
            header = f"X = {classstr}("
        else:
            header = f"{classstr}("
        lines.append(header)
        # Iterate over keys to generate each field line.
        for i, k in enumerate(self.keys()):
            v = getattr(self, k)
            if isinstance(v, np.ndarray):
                vtmp = getattr(tmp, k)
                field = fmt % k + " " + vtmp
            elif isinstance(v, (int, float)) or v is None:
                field = fmt % k + " " + str(v)
            elif isinstance(v, str):
                field = fmt % k + " " + f'"{v}"'
            elif isinstance(v, (list, tuple, dict)):
                field = fmt % k + " " + str(v)
            else:
                field = fmt % k + " " + "/* unsupported type */"
            # Append a comma after each field except the last one.
            if i < nk - 1:
                field += ","
            lines.append(field)
        # Create a closing line that aligns the closing parenthesis.
        closing_line = fmt[:-1] % ")"
        lines.append(closing_line)
    result = "\n".join(lines)
    if printout:
        print(result)
        return None
    return result
def getattr(self, key)

Get attribute override to access both instance attributes and properties if allowed.

Expand source code
def getattr(self,key):
    """Get attribute override to access both instance attributes and properties if allowed."""
    if key in self.__dict__:
        return self.__dict__[key]
    elif getattr(self, '_propertyasattribute', False) and \
         key not in self._excludedattr and \
         key in self.__class__.__dict__ and isinstance(self.__class__.__dict__[key], property):
        # If _propertyasattribute is True and it's a property, get its value
        return self.__class__.__dict__[key].fget(self)
    else:
        raise AttributeError(f'the {self._ftype} "{key}" does not exist')
def hasattr(self, key)

Return true if the field exists, considering properties as regular attributes if allowed.

Expand source code
def hasattr(self, key):
    """Return true if the field exists, considering properties as regular attributes if allowed."""
    return key in self.__dict__ or (
        getattr(self, '_propertyasattribute', False) and
        key not in self._excludedattr and
        key in self.__class__.__dict__ and isinstance(self.__class__.__dict__[key], property)
    )
def importfrom(self, s, nonempty=True, replacedefaultvar=True)

Import values from 's' into self according to the following rules:

  • Only fields that already exist in self are considered.
  • If s is a dictionary, it is converted to a struct via struct(**s).
  • If the current value of a field in self is empty (None, "", [] or ()), then that field is updated from s.
  • If nonempty is True (default), then only non-empty values from s are imported.
  • If replacedefaultvar is True (default), then if a field in self exactly equals "${key}" (with key being the field name), it is replaced by the corresponding value from s if it is empty.
  • Raises a TypeError if s is not a dict or struct (i.e. if it doesn’t support keys()).
Expand source code
def importfrom(self, s, nonempty=True, replacedefaultvar=True):
    """
    Import values from 's' into self according to the following rules:

    - Only fields that already exist in self are considered.
    - If s is a dictionary, it is converted to a struct via struct(**s).
    - If the current value of a field in self is empty (None, "", [] or ()),
      then that field is updated from s.
    - If nonempty is True (default), then only non-empty values from s are imported.
    - If replacedefaultvar is True (default), then if a field in self exactly equals
      "${key}" (with key being the field name), it is replaced by the corresponding
      value from s if it is empty.
    - Raises a TypeError if s is not a dict or struct (i.e. if it doesn’t support keys()).
    """
    # If s is a dictionary, convert it to a struct instance.
    if isinstance(s, dict):
        s = struct(**s)
    elif not hasattr(s, "keys"):
        raise TypeError(f"s must be a struct or a dictionary not a type {type(s).__name__}")

    for key in self.keys():
        if key in s:
            s_value = getattr(s, key)
            current_value = getattr(self, key)
            if is_empty(current_value) or (replacedefaultvar and current_value == "${" + key + "}"):
                if nonempty:
                    if not is_empty(s_value):
                        setattr(self, key, s_value)
                else:
                    setattr(self, key, s_value)
def isdefined(self, ref=None)

isdefined(ref) returns true if it is defined in ref

Expand source code
def isdefined(self,ref=None):
    """ isdefined(ref) returns true if it is defined in ref """
    s = param() if isinstance(self,param) else struct()
    k,v,isexpr = self.keys(), self.values(), self.isexpression.values()
    nk = len(k)
    if ref is None:
        for i in range(nk):
            if isexpr[i]:
                s.setattr(k[i],struct.isstrdefined(v[i],self[:i]))
            else:
                s.setattr(k[i],True)
    else:
        if not isinstance(ref,struct): raise TypeError("ref must be a structure")
        for i in range(nk):
            if isexpr[i]:
                s.setattr(k[i],struct.isstrdefined(v[i],ref))
            else:
                s.setattr(k[i],True)
    return s
def items(self)

return all elements as iterable key, value

Expand source code
def items(self):
    """ return all elements as iterable key, value """
    return self.zip()
def keys(self)

return the fields

Expand source code
def keys(self):
    """ return the fields """
    # keys() is used by struct() and its iterator
    return [key for key in self.__dict__.keys() if key not in self._excludedattr]
def keyssorted(self, reverse=True)

sort keys by length()

Expand source code
def keyssorted(self,reverse=True):
    """ sort keys by length() """
    klist = self.keys()
    l = [len(k) for k in klist]
    return [k for _,k in sorted(zip(l,klist),reverse=reverse)]
def np2str(self)

Convert all NumPy entries of s into their string representations, handling both lists and dictionaries.

Expand source code
def np2str(self):
    """ Convert all NumPy entries of s into their string representations, handling both lists and dictionaries. """
    out = struct()
    def format_numpy_result(value):
        """
        Converts a NumPy array or scalar into a string representation:
        - Scalars and single-element arrays (any number of dimensions) are returned as scalars without brackets.
        - Arrays with more than one element are formatted with proper nesting and commas to make them valid `np.array()` inputs.
        - If the value is a list or dict, the conversion is applied recursively.
        - Non-ndarray inputs that are not list/dict are returned without modification.

        Args:
            value (np.ndarray, scalar, list, dict, or other): The value to format.

        Returns:
            str, list, dict, or original type: A properly formatted string for NumPy arrays/scalars,
            a recursively converted list/dict, or the original value.
        """
        if isinstance(value, dict):
            # Recursively process each key in the dictionary.
            new_dict = {}
            for k, v in value.items():
                new_dict[k] = format_numpy_result(v)
            return new_dict
        elif isinstance(value, list):
            # Recursively process each element in the list.
            return [format_numpy_result(x) for x in value]
        elif isinstance(value, tuple):
            return tuple(format_numpy_result(x) for x in value)
        elif isinstance(value, struct):
            return value.npstr()
        elif np.isscalar(value):
            # For scalars: if numeric, use str() to avoid extra quotes.
            if isinstance(value, (int, float, complex, str)) or value is None:
                return value
            else:
                return repr(value)
        elif isinstance(value, np.ndarray):
            # Check if the array has exactly one element.
            if value.size == 1:
                # Extract the scalar value.
                return repr(value.item())
            # Convert the array to a nested list.
            nested_list = value.tolist()
            # Recursively format the nested list into a valid string.
            def list_to_string(lst):
                if isinstance(lst, list):
                    return "[" + ",".join(list_to_string(item) for item in lst) + "]"
                else:
                    return repr(lst)
            return list_to_string(nested_list)
        else:
            # Return the input unmodified if not a NumPy array, list, dict, or scalar.
            return str(value) # str() preferred over repr() for concision
    # Process all entries in self.
    for key, value in self.items():
        out.setattr(key, format_numpy_result(value))
    return out
def numrepl(self, text)

Replace all placeholders of the form ${key} in the given text by the corresponding numeric value from the instance fields, under the following conditions:

  1. 'key' must be a valid field in self (i.e., if key in self).
  2. The value corresponding to 'key' is either:
    • an int,
    • a float, or
    • a string that represents a valid number (e.g., "1" or "1.0").

Only when these conditions are met, the placeholder is substituted. The conversion preserves the original type: if the stored value is int, then the substitution will be done as an integer (e.g., 1 and not 1.0). Otherwise, if it is a float then it will be substituted as a float.

Any placeholder for which the above conditions are not met remains unchanged.

Placeholders are recognized by the pattern "${}" where is captured as all text until the next "}" (optionally allowing whitespace inside the braces).

Expand source code
def numrepl(self, text):
    r"""
    Replace all placeholders of the form ${key} in the given text by the corresponding
    numeric value from the instance fields, under the following conditions:

    1. 'key' must be a valid field in self (i.e., if key in self).
    2. The value corresponding to 'key' is either:
         - an int,
         - a float, or
         - a string that represents a valid number (e.g., "1" or "1.0").

    Only when these conditions are met, the placeholder is substituted.
    The conversion preserves the original type: if the stored value is int, then the
    substitution will be done as an integer (e.g., 1 and not 1.0). Otherwise, if it is
    a float then it will be substituted as a float.

    Any placeholder for which the above conditions are not met remains unchanged.

    Placeholders are recognized by the pattern "${<key>}" where <key> is captured as all
    text until the next "}" (optionally allowing whitespace inside the braces).
    """
    # Pattern: match "${", then optional whitespace, capture all characters until "}",
    # then optional whitespace, then "}".
    placeholder_pattern = re.compile(r"\$\{\s*([^}]+?)\s*\}")

    def replace_match(match):
        key = match.group(1)
        # Check if the key exists in self.
        if key in self:
            value = self[key]
            # If the value is already numeric, substitute directly.
            if isinstance(value, (int, float)):
                return str(value)
            # If the value is a string, try to interpret it as a numeric value.
            elif isinstance(value, str):
                s = value.strip()
                # Check if s is a valid integer representation.
                if re.fullmatch(r"[+-]?\d+", s):
                    try:
                        num = int(s)
                        return str(num)
                    except ValueError:
                        # Should not occur because the regex already matched.
                        return match.group(0)
                # Check if s is a valid float representation (including scientific notation).
                elif re.fullmatch(r"[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?", s):
                    try:
                        num = float(s)
                        return str(num)
                    except ValueError:
                        return match.group(0)
        # If key not in self or value is not numeric (or numeric string), leave placeholder intact.
        return match.group(0)

    # Replace all placeholders in the text using the replacer function.
    return placeholder_pattern.sub(replace_match, text)
def set(self, **kwargs)

initialization

Expand source code
def set(self,**kwargs):
    """ initialization """
    self.__dict__.update(kwargs)
def setattr(self, key, value)

set field and value

Expand source code
def setattr(self,key,value):
    """ set field and value """
    if isinstance(value,list) and len(value)==0 and key in self:
        delattr(self, key)
    else:
        self.__dict__[key] = value
def sortdefinitions(self, raiseerror=True, silentmode=False)

sortdefintions sorts all definitions so that they can be executed as param(). If any inconsistency is found, an error message is generated.

Flags = default values raiseerror=True show erros of True silentmode=False no warning if True

Expand source code
def sortdefinitions(self,raiseerror=True,silentmode=False):
    """ sortdefintions sorts all definitions
        so that they can be executed as param().
        If any inconsistency is found, an error message is generated.

        Flags = default values
            raiseerror=True show erros of True
            silentmode=False no warning if True
    """
    find = lambda xlist: [i for i, x in enumerate(xlist) if x]
    findnot = lambda xlist: [i for i, x in enumerate(xlist) if not x]
    k,v,isexpr =  self.keys(), self.values(), self.isexpression.values()
    istatic = findnot(isexpr)
    idynamic = find(isexpr)
    static = struct.fromkeysvalues(
        [ k[i] for i in istatic ],
        [ v[i] for i in istatic ],
        makeparam = False)
    dynamic = struct.fromkeysvalues(
        [ k[i] for i in idynamic ],
        [ v[i] for i in idynamic ],
        makeparam=False)
    current = static # make static the current structure
    nmissing, anychange, errorfound = len(dynamic), False, False
    while nmissing:
        itst, found = 0, False
        while itst<nmissing and not found:
            teststruct = current + dynamic[[itst]] # add the test field
            found = all(list(teststruct.isdefined()))
            ifound = itst
            itst += 1
        if found:
            current = teststruct # we accept the new field
            dynamic[ifound] = []
            nmissing -= 1
            anychange = True
        else:
            if raiseerror:
                raise KeyError('unable to interpret %d/%d expressions in "%ss"' % \
                               (nmissing,len(self),self._ftype))
            else:
                if (not errorfound) and (not silentmode):
                    print('WARNING: unable to interpret %d/%d expressions in "%ss"' % \
                          (nmissing,len(self),self._ftype))
                current = teststruct # we accept the new field (even if it cannot be interpreted)
                dynamic[ifound] = []
                nmissing -= 1
                errorfound = True
    if anychange:
        self.clear() # reset all fields and assign them in the proper order
        k,v = current.keys(), current.values()
        for i in range(len(k)):
            self.setattr(k[i],v[i])
def struct2dict(self)

create a dictionary from the current structure

Expand source code
def struct2dict(self):
    """ create a dictionary from the current structure """
    return dict(self.zip())
def struct2param(self, protection=False, evaluation=True)

convert an object struct() to param()

Expand source code
def struct2param(self,protection=False,evaluation=True):
    """ convert an object struct() to param() """
    p = param(**self.struct2dict())
    for i in range(len(self)):
        if isinstance(self[i],pstr): p[i] = pstr(p[i])
    p._protection = protection
    p._evaluation = evaluation
    return p
def update(self, **kwargs)

Update multiple fields at once, while protecting certain attributes.

Parameters:

**kwargs : dict The fields to update and their new values.

Protected attributes defined in _excludedattr are not updated.

Usage:

s.update(a=10, b=[1, 2, 3], new_field="new_value")

Expand source code
def update(self, **kwargs):
    """
    Update multiple fields at once, while protecting certain attributes.

    Parameters:
    -----------
    **kwargs : dict
        The fields to update and their new values.

    Protected attributes defined in _excludedattr are not updated.

    Usage:
    ------
    s.update(a=10, b=[1, 2, 3], new_field="new_value")
    """
    protected_attributes = getattr(self, '_excludedattr', ())
    for key, value in kwargs.items():
        if key in protected_attributes:
            print(f"Warning: Cannot update protected attribute '{key}'")
        else:
            self.setattr(key, value)
def validkeys(self, list_of_keys)

Validate and return the subset of keys from the provided list that are valid in the instance.

Parameters:

list_of_keys : list A list of keys (as strings) to check against the instance’s attributes.

Returns:

list A list of keys from list_of_keys that are valid (i.e., exist as attributes in the instance).

Raises:

TypeError If list_of_keys is not a list or if any element in list_of_keys is not a string.

Example:

>>> s = struct()
>>> s.foo = 42
>>> s.bar = "hello"
>>> valid = s.validkeys(["foo", "bar", "baz"])
>>> print(valid)   # Output: ['foo', 'bar'] assuming 'baz' is not defined in s
Expand source code
def validkeys(self, list_of_keys):
    """
    Validate and return the subset of keys from the provided list that are valid in the instance.

    Parameters:
    -----------
    list_of_keys : list
        A list of keys (as strings) to check against the instance’s attributes.

    Returns:
    --------
    list
        A list of keys from list_of_keys that are valid (i.e., exist as attributes in the instance).

    Raises:
    -------
    TypeError
        If list_of_keys is not a list or if any element in list_of_keys is not a string.

    Example:
    --------
    >>> s = struct()
    >>> s.foo = 42
    >>> s.bar = "hello"
    >>> valid = s.validkeys(["foo", "bar", "baz"])
    >>> print(valid)   # Output: ['foo', 'bar'] assuming 'baz' is not defined in s
    """
    # Check that list_of_keys is a list
    if not isinstance(list_of_keys, list):
        raise TypeError("list_of_keys must be a list")

    # Check that every entry in the list is a string
    for key in list_of_keys:
        if not isinstance(key, str):
            raise TypeError("Each key in list_of_keys must be a string")

    # Assuming valid keys are those present in the instance's __dict__
    return [key for key in list_of_keys if key in self]
def values(self)

return the values

Expand source code
def values(self):
    """ return the values """
    # values() is used by struct() and its iterator
    return [pstr.eval(value) for key,value in self.__dict__.items() if key not in self._excludedattr]
def write(self, file, overwrite=True, mkdir=False)

write the equivalent structure (not recursive for nested struct) write(filename, overwrite=True, mkdir=False)

Parameters: - file: The file path to write to. - overwrite: Whether to overwrite the file if it exists (default: True). - mkdir: Whether to create the directory if it doesn't exist (default: False).

Expand source code
def write(self, file, overwrite=True, mkdir=False):
    """
        write the equivalent structure (not recursive for nested struct)
            write(filename, overwrite=True, mkdir=False)

        Parameters:
        - file: The file path to write to.
        - overwrite: Whether to overwrite the file if it exists (default: True).
        - mkdir: Whether to create the directory if it doesn't exist (default: False).
    """
    # Create a Path object for the file to handle cross-platform paths
    file_path = Path(file).resolve()

    # Check if the directory exists or if mkdir is set to True, create it
    if mkdir:
        file_path.parent.mkdir(parents=True, exist_ok=True)
    elif not file_path.parent.exists():
        raise FileNotFoundError(f"The directory {file_path.parent} does not exist.")
    # If overwrite is False and the file already exists, raise an exception
    if not overwrite and file_path.exists():
        raise FileExistsError(f"The file {file_path} already exists, and overwrite is set to False.")
    # Convert to static if needed
    if isinstance(p,(param,paramauto)):
        tmp = self.tostatic()
    else:
        tmp = self
    # Open and write to the file using the resolved path
    with file_path.open(mode="w", encoding='utf-8') as f:
        print(f"# {self._fulltype} with {len(self)} {self._ftype}s\n", file=f)
        for k, v in tmp.items():
            if v is None:
                print(k, "=None", file=f, sep="")
            elif isinstance(v, (int, float)):
                print(k, "=", v, file=f, sep="")
            elif isinstance(v, str):
                print(k, '="', v, '"', file=f, sep="")
            else:
                print(k, "=", str(v), file=f, sep="")
def zip(self)

zip keys and values

Expand source code
def zip(self):
    """ zip keys and values """
    return zip(self.keys(),self.values())