The Shapes

The scatman.Shapes submodule contains a set of objects that aim to describe different kinds of shapes.

All shapes derive from an abstract base class scatman.Shapes.Shape. Being an abstract class, it cannot be used directly, i.e. it is not possible to instantiate an object of that type:

>>> import scatman
>>> MyShape = scatman.Shapes.Shape()
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
TypeError: scatman.Shapes.Shape: No constructor defined!

Shape orientation

A set of parameters that is common among all the shapes is the one that defines the orientation in space. The three parameters are:

rotation:

rotation of the shape along its main axis (dashed red line in the figure), in degrees.

latitude, longitude:
 

coordinates at which the main axis of the shape points. These coordinates are, in particular, defined with respect to the beam propagation axis, depicted in blue in the figure below.

latitude:angle, in degrees, between the shape main axis and the equator defined by the beam propagation axis.
longitude:rotation, in degrees, along the beam propagation axis.
_images/orientation.png

Optical properties

The scattering pattern is affected also by the optical properties of the sample material, defined by the complex refractive index n=n' + \mathbf{i} n''. However, it is often more convenient, and well spread in literature, to identify the optical properties via two quantities:

  • \delta: refractive index decrease
  • \beta: absorpion coefficient

Their relationship with the complex refractive index n is defined by the following relations:

\begin{eqnarray*}
n' &=& 1 - \delta \\
n''&=& \beta\\
n &=& 1 - \delta + \mathbf{i} \beta
\end{eqnarray*}

Thus, for each shape, the following parameters define the optical properties of the sample:

delta:decrease of the real part of the refractive index \delta
beta:absorption coefficient beta

Position within the beam focus

If the simulation includes also the photons statistics (i.e. if a detector that is not the MSFT is used) the photon density is relevant for the correct simulation of the diffraction pattern. In a real experiment, even if the photon density is constant, the amount of scattered radiation for similar samples can vary a lot. This derives from the fact that the beam focus doesn’t have a perfect flat intensity spatial distribution, but can be modelled as a Gaussian 2D profile, with the peak intensity in the center. The scattered intensity thus depends also on how far the shape is from the peak intensity, as the following figure depicts:

_images/beam_position.png

To simulate this behavior, especially when the simulation involves a dataset, the parameter beam_position can be provided to the shape constructor. This value represents the distance from the peak intensity in units of \sigma, where \sigma is the standard deviation of the 2D Gaussian beam profile in the interaction zone.

Pre-defined Shapes

class scatman.Shapes.Shape
get(self: scatman.Shapes.Shape, resolution: int = 80, os: int = 3) → Tuple[array, float]

Get a voxel rendering of the shape.

Get a 3D rendering of the shape.

Parameters:
  • resolution – number of voxels in one dimension. The current implementation draws the shape inside a 3D cube with a total number of voxels equal to \text{resolution}^3.
  • oversampling – oversampling factor for each axis. When a shape is rendered on the 3D grid, the value for each voxel is assigned by sampling the shape in multiple points. For example, setting the value to 3 implies that a total of 3 \times 3 \times 3 = 27 sample values are taken, within the voxel size, to assign its final value. Higher values mitigate the discretization artifacts, but increase the rendering computational cost.
Returns:

A tuple containing a 3D numpy array containing the rendered shape, and float that provides the linear dimension of the voxels.

get_name(self: scatman.Shapes.Shape) → str
get_size(self: scatman.Shapes.Shape) → float
class scatman.Shapes.Ellipsoid(self: scatman.Shapes.Ellipsoid, a: float, b: float, c: float, delta: float = 0, beta: float = 0.001, latitude: float = 90, longitude: float = 0, rotation: float = 0, beam_position: float = 0) → None

Bases: scatman.Shapes.Shape

Define an Ellipsoid via the values of its three axes.

Parameters:
  • a – Main axis of the ellipsoid.
  • b,c – the other two axes
  • delta, beta – see here
  • latitude, longitude, rotation – see here.
  • beam_position – see here.
class scatman.Shapes.Dumbbell(self: scatman.Shapes.Dumbbell, l: float, a: float, b: float, c: float, z: float, delta: float = 0, beta: float = 0.001, latitude: float = 90, longitude: float = 0, rotation: float = 0, beam_position: float = 0) → None

Bases: scatman.Shapes.Shape

Define a dumbbell shape via the parameters described in the picture.

_images/dumbbell.png
Parameters:
  • l – Main axis of the dumbbell
  • a,b,c,z – the other relevant lengths
  • delta, beta – see here
  • latitude, longitude, rotation – see here.
  • beam_position – see here.
class scatman.Shapes.ShellSphere(self: scatman.Shapes.ShellSphere, r: float, s: float, t: float = 0, delta: float = 0, beta: float = 0.01, delta_shell: float = 0.01, beta_shell: float = 0.001, beam_position: float = 0) → None

Bases: scatman.Shapes.Shape

Define a sphere with optical properties distributed in a core-shell manner. Due to the simmetry of the sphere, the orientation parameters have no effect and are thus not available for this shape.

_images/shell_sphere.png
Parameters:
  • r – sphere radius
  • s – thickness of the shell
  • t – width of the transition between the shell and the core
  • delta, beta, delta_shell, beta_shell – see here
  • beam_position – see here.
class scatman.Shapes.MoonSphere(self: scatman.Shapes.MoonSphere, r: float, s: float, t: float = 0, delta: float = 0, beta: float = 0.01, delta_shell: float = 0.01, beta_shell: float = 0.001, beam_position: float = 0) → None

Bases: scatman.Shapes.Shape

Define a sphere with optical properties distributed in a core-shell manner, where the shell has a falling moon shape oriented in the direction of the incoming beam. Due to the simmetry of the sphere, the orientation parameters have no effect and are thus not available for this shape.

_images/moon_sphere.png
Parameters:
  • r – sphere radius
  • s – thickness of the shell
  • t – width of the transition between the shell and the core
  • delta, beta, delta_shell, beta_shell – see here
  • beam_position – see here.

Custom Shapes

Up to now, each presented shape can only describe a small subset of all the possible sample shapes. Here two additional kinds of shapes are presented, that allow much more flexibility in the shape defition, but on the other side are not so straightforward to define, as it was for the other ones.

Custom Radial Function

An useful way to identify a 3D shape is to restrict to the (still wide) subset of shapes whose surface can be identified by a function R(\theta, \phi), where R is the sample radius and \theta, \phi are the angles in spherical coordinates.

The shape scatman.Shapes.RadialMap() is provided exactly for this purpose. At construction time, it requires a 2D numpy array that defines the radius as function of \theta and \phi.

The following code provides a minimal example of its functionalities.

import scatman
import numpy as np

radii_values = np.ones([100,200]) # set the same radius at any angle theta and phi

scatman.set_experiment(wavelength=30, angle=30, resolution=512)        # set the experimental parameters

detector = scatman.Detectors.Ideal(mask_radius=50)                     # set the detector, with a central hole of 50 pixels

shape = scatman.Shapes.RadialMap(a = 400, radii=radii_values)  # a is a scaling factor (see class documentation)
                                                                        # radii is the radial map
pattern=detector.acquire(shape)

import render_sh                        # you can find it in the "tests" folder. Requires the Vtk python module

render_sh.Scene([pattern], [shape])     # render the shape and the simulated diffraction pattern together, in a fancy way
_images/sphere_radii.png

As expected, the result is just a sphere, because the radius is a flat function.

A second, fancier, example is to load an image and use it as a radial function. The following image will be used:

_images/NUX_radii.png

The following code should do the job. First, we load the image (you can found it in the tests folder) and convert it to a 2D numpy array

from PIL import Image
import numpy as np

image = Image.open('NUX.png')               # load the grayscale image and
nux = np.copy(np.asarray(image)[:,:,0])     # convert it to a 2D numpy array
nux=nux/np.max(nux)

Then, we do the same steps as the previous example, where the radii passed to the shape constructor are now provided by the nux 2D numpy array.

import scatman

scatman.set_experiment(wavelength=30, angle=30, resolution=512)        # set the experimental parameters

detector = scatman.Detectors.Ideal(mask_radius=50)                     # set the detector, with a central hole of 50 pixels

shape = scatman.Shapes.RadialMap(a=700, radii=nux, \                   # a is a scaling factor (see class documentation)
                    latitude=0, longitude=0, rotation=58)              # the nux image is used as radial map

pattern=detector.acquire(shape)

import render_sh                        # you can find it in the "tests" folder. Requires the Vtk python module

render_sh.Scene([pattern], [shape])     # render the shape and the simulated diffraction pattern together, in a fancy way
_images/NUX_scatman.png
class scatman.Shapes.RadialMap(self: scatman.Shapes.RadialMap, radii: numpy.ndarray[float32], a: float = -1, delta: float = 0, beta: float = 0.001, latitude: float = 90, longitude: float = 0, rotation: float = 0, beam_position: float = 0) → None

Bases: scatman.Shapes.Shape

Define a RadialMap.

Parameters:
  • radii – 2D array providing the radii as function of theta and phi.
  • a – scaling factor: if a>0 the shape will be scaled such that the maximum radius has a value equal to a.
  • delta, beta – see here
  • latitude, longitude, rotation – see here.
  • beam_position – see here.

Cube Mapping

The main drawback of the RadialMap object described above regards the strongly uneven way of sampling a sphere surface. In particular, the amount of sampling points per unit area is much higher at the poles than at the equator. However, a perfectly uniform sampling of a sphere surface is a challanging task, and approximate methods (that is, giving just fairly uniform samplings) are often used. Among them, the Cube Mapping is one of the easiest and most efficient ways to accomplish a fairly uniform sampling of a function (the radius in this case) defined on a sphere surface.

Cube Mapping usess the six faces of a cube as the map shape. The spherical coordinates are projected onto the sides of a cube. Each face is a square matrix, that gives the values of the sample radius in the respective direction.

To define a CubeMap object, a 3D array of size [6, dim, dim] has to be defined. Each index in the first dimension identifies a face of the cube, which is a 2D square matrix of size [dim, dim].

_images/cube_map.png

In the left side of upper image, the organization of the different faces is shown. In the upper left of the square faces, the index of the face is shown. The letter at the center, instead, indicates the axis normal to the face. Finally, the color indicates if the face is directed in the positive direction (blue) or negative direction (red). For example, the face with index 4 is normal to the z axis, oriented towards its positive side. Face 2, instead, is normal to the x axis, and looks towards its negative versus.

The right side of the figure represents how the 6 faces are organized and oriented into the 3D space, relative to the beam propagation direction.

The following code is a trivial example on how to define the CubeMap shape object. First, as usual, the Scatman module must be imported, the relevant experimental parameters should be set, and the detector defined.

import scatman
import random
import numpy as np

scatman.set_experiment(wavelength=40, angle=30, resolution=512)
detector = scatman.Detectors.MSFT()

Then, the 6 faces of the cube has to be defined:

radii=np.ones([6, 7, 7])*400
radii[:,::2,::2]=600
radii[1,2,2]=800

The following figure is a representation of the 6 faces defined in the above code

_images/cube_map_exradii.png

Then, the shape object can be constructed, passing the radii array defined above, and the simulation carried on in the usual way:

shape = scatman.Shapes.CubeMap(radii=radii, delta=0.02, beta=0.01)
pattern = detector.acquire(shape)

The following picture is a rendering of the shape and the simulated diffraction pattern:

_images/cube_map_expattern.png

Note

Despite the “low” resolution of the cube faces (6 by 6 pixels in the example), the rendering of the shape looks smooth. This is because the radius value is obtained by cubic interpolation during the simulation.

class scatman.Shapes.CubeMap(self: scatman.Shapes.CubeMap, radii: numpy.ndarray[float32], a: float = -1, delta: float = 0, beta: float = 0.001, latitude: float = 90, longitude: float = 0, rotation: float = 0, beam_position: float = 0) → None

Bases: scatman.Shapes.Shape

Define a sample’s shape using a Cube Mapping of its radius.

Parameters:
  • radii – 3D array providing the radii. The first dimension of the array must be equal to 6, and represents the 6 faces of the cube. Second and third dimesions should be equal, which means that the faces of the cube must be square matrices.
  • a – scaling factor: if a>0 the shape will be scaled such that the maximum radius has a value equal to a.
  • delta, beta – see here
  • latitude, longitude, rotation – see here.
  • beam_position – see here.
get_radii(self: scatman.Shapes.CubeMap) → array

Spherical Harmonics Expansion

Following what was discussed in the previous section about the scatman.Shapes.RadialMap(), it is clear that manually providing the radial map is very ductile, but not straightforward.

However, there exists a convenient way to express a radial map with fewer parameters, exploiting the expansion in Spherical Harmonics . Spherical harmonics are an orthonormal base for any function defined on a sphere, i.e. f(\theta,\phi). Being this the case of R(\theta, \phi), spherical harmonics may represent a convenient and more compact way to define 3D shapes.

The class scatman.Shapes.SphericalHarmonics(), which inherits from scatman.Shapes.RadialMap(), enables the user to define the radial map based on spherical harmonics. Spherical harmonics may have different definitions, based on different conventions about the normalization factors. Here we use the following definition:

Y_{\ell }^{m}(\theta ,\varphi )={\sqrt {{(2\ell +1)}{(\ell -m)! \over (\ell +m)!}}}\,P_{\ell }^{m}(\cos {\theta })\,e^{im\varphi }

where |m| \leq \ell and P_{\ell }^{m}(x) are the associated Legendre polynomials , that can be defined in the following way:

P_{\ell }^{m}(x)={\frac {(-1)^{m}}{2^{\ell }\ell !}}(1-x^{2})^{m/2}\ {\frac {d^{\ell +m}}{dx^{\ell +m}}}(x^{2}-1)^{\ell }

These spherical harmonics are actually complex-valued. However, the radius R(\theta, \phi) is a real function. For this reason, real spherical harmonics will be used as a basis, following this definition:

Y_{\ell, m}=
\begin{cases}
\displaystyle {\sqrt {2}}\,\operatorname {Im} [{Y_{\ell }^{|m|}}] & {\text{if}}\ m<0\\
\displaystyle Y_{\ell }^{0} & {\text{if}}\ m=0\\
\displaystyle {\sqrt {2}}\,\operatorname {Re} [{Y_{\ell }^{m}}]  & {\text{if}}\ m>0.
\end{cases}

The following example provides a simple overview over the scatman.Shapes.SphericalHarmonics() functionalities. Its use is very similar to scatman.Shapes.RadialMap(), where, instead of passing the image of the radii, the indices and the coefficients of the spherical harmonics are provided.

First, as usual, the scatman module is imported, and the relevant parameters and objects are defined

import scatman

scatman.set_experiment(wavelength=30, angle=30, resolution=512)        # set the experimental parameters

detector = scatman.Detectors.Ideal(mask_radius=50)                     # set the detector, with a central hole of 50 pixels radius

shapes=[]   # empty list of shapes, that will be filled then with the different shape objects

Now, we start to define a shape with spherical harmonics. The first shape we define is the easiest one, that is just a sphere. A sphere in spherical harmonics is defined by just the spherical harmonic Y_{\ell=0, m=0}:

harmonics = [[0,0,1]]

shapes.append(scatman.Shapes.SphericalHarmonics(a=400, coefficients=harmonics))

where the array [0,0,1] is a triplet that indicates \ell=0, m=0, c, where the latter value is the coefficient. The parameter a is a scaling factor, that defines the maximum radius of the shape.

A second shape can be defined with an additional coefficient, for example.

harmonics = [[0,0,1],
            [2,-1,0.25]]

shapes.append(scatman.Shapes.SphericalHarmonics(a=400, coefficients=harmonics, latitude=43, longitude=190, rotation=58))

This shape recreates, more or less, the scatman.Shapes.Dumbbell().

The amount of coefficients has no upper limit, and bizarre shapes can be easily created by setting the coefficients for higher spherical harmonics. For example:

harmonics = [ [0,0,1],
            [1,1,0.2],
            [2,-1,0.06],
            [7,6,0.08]]

shapes.append(scatman.Shapes.SphericalHarmonics(a=700, coefficients=harmonics, latitude=-13, longitude=40, rotation=76))

Then, it is possible to perform the experiment and visualize the results:

patterns=detector.acquire_dataset(shapes)

import render_sh                      # you can find it in the "tests" folder. Requires the Vtk python module

render_sh.Scene(patterns, shapes)     # render the shape and the simulated diffraction pattern together, in a fancy way
_images/sh_1.png
_images/sh_2.png
_images/sh_3.png
class scatman.Shapes.SphericalHarmonics(self: scatman.Shapes.SphericalHarmonics, coefficients: numpy.ndarray[float32], a: float = -1, delta: float = 0, beta: float = 0.001, latitude: float = 90, longitude: float = 0, rotation: float = 0, beam_position: float = 0) → None

Bases: scatman.Shapes.RadialMap

Define a shape basing on the spherical harmonics coefficients. See the documentation for a deeper description.

Parameters:
  • coefficients – coefficients of the spherical harmonics, organized in a list of triplets [l,m,val]
  • a – scaling factor: if a>0 the shape will be scaled such that the maximum radius has a value equal to a.
  • delta, beta – see here
  • latitude, longitude, rotation – see here.
  • beam_position – see here.

3D Volume

class scatman.Shapes.VolumeMap(self: scatman.Shapes.VolumeMap, dx: float, data: numpy.ndarray[bool], delta: float = 0, beta: float = 0.001, latitude: float = 90, longitude: float = 0, rotation: float = 0, beam_position: float = 0) → None

Bases: scatman.Shapes.Shape

Define a custom sample, by directly providing its 3D rendering. Here, the rendering is provided as a 3D boolean array, where True indicates the presence of the material, while False is vacuum. The 3D array provided as input can be of any size, and it is automatically rescaled to match the experimental conditions via interpolation.

Parameters:
  • dx – linear size of the voxels
  • data – 3D bool array that contains the 3D distribution of the sample’s optical properties.
  • delta, beta – see here
  • latitude, longitude, rotation – see here.
  • beam_position – see here.
class scatman.Shapes.VolumeDistribution(self: scatman.Shapes.VolumeDistribution, dx: float, data: numpy.ndarray[complex64], latitude: float = 90, longitude: float = 0, rotation: float = 0, beam_position: float = 0) → None

Bases: scatman.Shapes.Shape

Define a custom sample, by directly providing its 3D complex refractive index distribution by means of a 3D complex-valued array. The 3D array provided as input can be of any size, and it is automatically rescaled to match the experimental conditions via interpolation. Values equal to 1 correspond to the optical properties of vacuum.

Parameters:
  • dx – linear size of the voxels
  • data – 3D complex array that contains the 3D distribution of the sample’s optical properties.
  • latitude, longitude, rotation – see here.
  • beam_position – see here.