Package: macromax
IntroductionThis Python 3 package enables solving the macroscopic Maxwell equations
in complex dielectric materials.The material properties are defined on a rectangular grid (1D, 2D, or
3D) for which each voxel defines an isotropic or anisotropic
permittivity. Optionally, a heterogeneous (anisotropic) permeability as
well as bi-anisotropic coupling factors may be specified (e.g. for
chiral media). The source, such as an incident laser field, is specified
as an oscillating current-density distribution.The method iteratively corrects an estimated solution for the electric
field (default: all zero). Its memory requirements are on the order of
the storage requirements for the material properties and the electric
field within the calculation volume. Full details can be found in theopen-accessmanuscript“Calculating coherent light-wave propagation in large heterogeneous
media”. Automatic leveraging
of detected GPU/Cloud is implemented using PyTorch (for further detailsfollow this link).Examples of usage can be found inthe examples/
sub-folder. TheComplete MacroMax
Documentationcan be found athttps://macromax.readthedocs.io. Allsource
codeis available onGitHubunder theMIT
License:https://opensource.org/licenses/MITInstallationPrerequisitesThis library requires Python 3 with thenumpyandscipypackages
for the main calculations. These modules will be automatically
installed. The modulesmultiprocessing,torch,pyfftw, andmkl-fft(Intel(R) CPU specific) can significantly speed up the
calculations.The examples requirematplotlibfor displaying the results. In the
creation of this package for distribution, thepypandocpackage is
used for translating this document to other formats. This is only
necessary for software development.The code has been tested on Python 3.7 and 3.8, though it is expected to
work on versions 3.6 and above.InstallingInstalling themacromaxpackage and its mandatory dependencies is as
straightforward as running the following command in a terminal:pipinstallmacromaxWhile this is sufficient to get started, optional packages are useful to
display the results and to speed-up the calculations.Optimizing execution speedThe calculation time can be reduced to a fraction by ensuring you have
the fastest libraries installed for your system. Python packages for
multi-core CPUs and the FFTW library can be installed with:pipinstallmacromaxmultiprocessingpyFFTWDo note that thepyFFTWpackage
requires theFFTW library, and
does not always install automatically. However, it is easy to install it
using Anaconda with the commands:conda install fftw, or on
Debian-based systems withsudoapt-getinstall fftw.Alternatively, themkl-fftpackage is available for Intel(R) CPUs, though it may require
compilation or relying on theAnacondaorIntel
Pythondistributions:condainstall-cintelintelpythonNVidia CUDA-enabled GPU can be leveraged to offer an even more
significant boost in efficiency. This can be as simple as installing the
appropriateCUDA
driversand
the PyTorch module following thePyTorch
Guide. Note that for PyTorch to work
correctly, Nvidia drivers need to be up to date and match the installed
CUDA version. At the time of writing, for CUDA version 11.6, PyTorch can
be installed as follows using pip:pipinstalltorch--extra-index-urlhttps://download.pytorch.org/whl/cu116Specifics for your CUDA version and operating system are listed onPyTorch Guide.When PyTorch and a GPU are detected, these will be used by default. If
not, FFTW and mkl-fft will be used if available. NumPy and SciPy will be
used otherwise. The default backend can be set in your code or by
creating a text file namedbackend_config.jsonin the current
working directory with contents:[{"type":"torch","device":"cuda"},{"type":"numpy"}]to choose PyTorch with a CUDA GPU if available, and NumPy as a back-up
option. The latter is usually faster when no GPU is available.Additional packagesThe package comes with a submodule containing example code that should
run as-is on most desktop installations of Python. Some systems may
require the installation of the ubiquitousmatplotlibgraphics
library:pipinstallmatplotlibThe output logs can be colored by installing the coloredlogs packaged:pipinstallcoloredlogsBuilding and distributing the library may require further packages as
indicated below.UsageThe basic calculation procedure consists of the following steps:define the materialdefine the coherent light sourcecallsolution =macromax.solve(...)display the solutionThemacromaxpackage must be imported to be able to use thesolvefunction. The package also contains several utility functions
that may help in defining the property and source distributions.Examples can be found inthe examples package in the examples/
folder. Ensure that the entireexamples/folder is
downloaded, including the__init__.pyfile with general definitions.
Run the examples from the parent folder using e.g.python-mexamples.air_glass_air_1D.The complete functionality is described in theLibrary Reference
Documentationathttps://macromax.readthedocs.io.Loading the Python 3 packageThemacromaxpackage can be imported using:importmacromaxOptional:If the package is installed without a package manager, it
may not be on Python’s search path. If necessary, add the library to
Python’s search path, e.g. using:importsysimportossys.path.append(os.path.dirname(os.getcwd()))Reminder: this library requires Python 3,numpy, andscipy.
Optionally,pyfftwcan be used to speed up the calculations. The
examples also requirematplotlib.Specifying the materialDefining the sampling gridThe material properties are sampled on a plaid uniform rectangular grid
of voxels. The sample points are defined by one or more linearly
increasing coordinate ranges, one range per dimensions. The coordinates
must be specified in meters, e.g.:importnumpyasnpx_range=50e-9*np.arange(1000)Ranges for multiple dimensions can be passed tosolve(...)as a
tuple of ranges:ranges = (x_range, y_range), or the convenience
objectGridin themacromax.utils.arraysub-package. The latter
can be used as follows:data_shape=(200,400)sample_pitch=50e-9# or (50e-9, 50e-9)grid=macromax.Grid(data_shape,sample_pitch)This defines a uniformly spaced plaid grid, centered around the origin,
unless specified otherwise.Defining the material property distributionsThe material properties are defined by ndarrays of 2+N dimensions, where
N can be up to 3 for three-dimensional samples. In each sample point, or
voxel, a complex 3x3 matrix defines the anisotropy at that point in the
sample volume. The first two dimensions of the ndarray are used to store
the 3x3 matrix, the following dimensions are the spatial indices x, y,
and z. Four complex ndarrays can be specified:epsilon,mu,xi, andzeta. These ndarrays represent the permittivity,
permeability, and the two coupling factors, respectively.When the first two dimensions of a property are found to be both a
singleton, i.e. 1x1, that property is assumed to be isotropic.
Similarly, singleton spatial dimensions are interpreted as homogeneity
in that property. The default permeabilitymuis 1, and the coupling
constants are zero by default.Boundary conditionsThe underlying algorithm assumesperiodic boundary
conditions.
Alternative boundary conditions can be implemented by surrounding the
calculation area with absorbing (or reflective) layers. Back reflections
can be suppressed by e.g. linearly increasing the imaginary part of the
permittivity with depth into a boundary with a thickness of a few
wavelengths.Defining the sourceThe coherent source is defined by as a spatially-variant free current
density. Although the current density may be non-zero in all of space,
it is more common to define a source at one of the edges of the volume,
to model e.g. an incident laser beam; or even as a single voxel, to
simulate a dipole emitter. The source density can be specified as a
complex number, indicating the phase and amplitude of the current at
each point. If an extended source is defined, care should be taken so
that the source currents constructively interfere in the desired
direction. I.e. the current density at neighboring voxels should have a
phase difference matching the k-vector in the background medium.
Optionally, instead of a current density, the internally-used source
distribution may be specified directly. It is related to the current
density as follows:S = i omega mu_0 Jwith units of rad s^-1 H m^-1
A m^-2 = rad V m^-3, whereomegais the angular frequency, andmu_0is the vacuum permeability, mu_0.The source distribution is stored as a complex ndarray with 1+N
dimensions. The first dimension contains the current 3D direction and
amplitude for each voxel. The complex argument indicates the relative
phase at each voxel.Calculating the electromagnetic light fieldOnce themacromaxmodule is imported, the solution satisfying the
macroscopic Maxwell’s equations is calculated by calling:solution=macromax.solve(...)The function arguments tomacromax.solve(...)can be the following:grid|x_range: A Grid object, a vector (1D), or tuple of vectors
(2D, or 3D) indicating the spatial coordinates of the sample points.
Each vector must be a uniformly increasing array of coordinates,
sufficiently dense to avoid aliasing artefacts.vacuum_wavelength|wave_number|anguler_frequency: The wavelength
in vacuum of the coherent illumination in units of meters.current_densityorsource_distribution: An ndarray of complex
values indicating the source value and direction at each sample
point. The source values define the free current density in the
sample. The first dimension contains the vector index, the following
dimensions contain the spatial dimensions. If the source distribution
is not specified, it is calculated as :math:-ic k0 mu_0 J, whereiis the imaginary constant,c,k0, andmu_0, the
light-speed, wavenumber, and permeability in vacuum. Finally,Jis the free current density (excluding the movement of bound charges
in a dielectric), specified as the input argument current_density.
These input arguments should benumpy.ndarrays with a shape as
specified by thegridinput argument, or have one extra dimension
on the left to indicate the polarization. If polarization is not
specified the solution to thescalarwave equation is calculated.
However, when polarization is specified thevectorialproblem is
solved. The returnedmacromax.Solutionobject has the propertyvectorialto indicate whether polarization is accounted for or
not.refractive_index: A complexnumpy.ndarrayof a shape as
indicated by thegridargument. Each value indicates the
refractive at the corresponding spatial grid point. Real values
indicate a loss-less material. A positive imaginary part indicates
the absorption coefficient, :math:\kappa. This input argument is
not required if the permittivity,epsilonis specified.epsilon: (optional, default: :math:n^2) A complexnumpy.ndarrayof a shape as indicated by thegridargument
forisotropicmedia, or a shape with two extra dimensions on the
left to indicateanisotropy/birefringence. The array values
indicate the relative permittivity at all sample points in space. The
optional two first (left-most) dimensions may contain a 3x3 matrix at
each spatial location to indicate the anisotropy/birefringence. By
default the 3x3 identity matrix is assumed, scaled by the scalar
value of the array without the first two dimensions. Real values
indicate loss-less permittivity. This input argument is unit-less, it
is relative to the vacuum permittivity.Optionally one can also specify magnetic and coupling factors:mu: A complex ndarray that defines the 3x3 permeability matrix at
all sample points. The first two dimensions contain the matrix
indices, the following dimensions contain the spatial dimensions.xiandzeta: Complex ndarray that define the 3x3 coupling
matrices at all sample points. This may be useful to model chiral
materials. The first two dimensions contain the matrix indices, the
following dimensions contain the spatial dimensions.It is often useful to also specify a callback function that tracks
progress. This can be done by defining thecallback-argument as a
function that takes an intermediate solution as argument. This
user-defined callback function can display the intermediate solution and
check if the convergence is adequate. The callback function should
returnTrueif more iterations are required, andFalseotherwise. E.g.:callback=lambdas:s.residue>0.01ands.iteration<1000will iterate until the residue is at most 1% or until the number of
iterations reaches 1,000.The solution object (of the Solution class) fully defines the state of
the iteration and the current solution as described below.Themacromax.solve(...)function returns a solution object. This
object contains the electric field vector distribution as well as
diagnostic information such as the number of iterations used and the
magnitude of the correction applied in the last iteration. It can also
calculate the displacement, magnetizing, and magnetic fields on demand.
These fields can be queried as follows:solution.E: Returns the electric field distribution.solution.H: Returns the magnetizing field distribution.solution.D: Returns the electric displacement field distribution.solution.B: Returns the magnetic flux density distribution.solution.S: The Poynting vector distribution in the sample.The field distributions are returned as complexnumpyndarrays in
which the first dimensions is the polarization or direction index. The
following dimensions are the spatial dimensions of the problem, e.g. x,
y, and z, for three-dimensional problems.The solution object also keeps track of the iteration itself. It has the
following diagnostic properties:solution.iteration: The number of iterations performed.solution.residue: The relative magnitude of the correction during
the previous iteration. and it can be used as a Python iterator.Further information can be found in theexamplesand
thesignatures of each function and
class.Complete ExampleThe following code loads the library, defines the material and light
source, calculates the result, and displays it. To keep this example as
simple as possible, the calculation is limited to one dimension. Higher
dimensional calculations simply require the definition of the material
and light source in 2D or 3D.The first section of the code loads themacromaxlibrary module as
well as itsutilssubmodule. Moreimportmacromaximportnumpyasnpimportmatplotlib.pyplotasplt# %matplotlib notebook  # Uncomment this line in an iPython Jupyter notebook## Define the material properties#wavelength=500e-9# [ m ] In SI units as everything else heresource_polarization=np.array([0,1,0])[:,np.newaxis]# y-polarized# Set the sampling gridnb_samples=1024sample_pitch=wavelength/10# [ m ]  # Sub-sample for displayboundary_thickness=5e-6# [ m ]x_range=sample_pitch*np.arange(nb_samples)-boundary_thickness# [ m ]# Define the medium as a spatially-variant permittivity# Don't forget absorbing boundary:dist_in_boundary=np.maximum(0,np.maximum(-x_range,x_range-(x_range[-1]-boundary_thickness))/boundary_thickness)permittivity=1.0+0.25j*dist_in_boundary# unit-less, relative to vacuum permittivity# glass has a refractive index of about 1.5permittivity[(x_range>=20e-6)&(x_range<30e-6)]+=1.5**2permittivity=permittivity[np.newaxis,np.newaxis,...]# Define an isotropic material## Define the illumination source## point source at x = 0current_density=source_polarization*(np.abs(x_range)<sample_pitch/4)## Solve Maxwell's equations## (the actual work is done in this line)solution=macromax.solve(x_range,vacuum_wavelength=wavelength,current_density=current_density,epsilon=permittivity)## Display the results#fig,ax=plt.subplots(2,1,frameon=False,figsize=(8,6))x_range=solution.grid[0]# coordinatesE=solution.E[1,:]# Electric field in yH=solution.H[2,:]# Magnetizing field in zS=solution.S_forw[0,:]# Poynting vector in xf=solution.f[0,:]# Optical force in x# Display the field for the polarization dimensionfield_to_display=Emax_val_to_display=np.amax(np.abs(field_to_display))poynting_normalization=np.amax(np.abs(S))/max_val_to_displayax[0].plot(x_range*1e6,np.abs(field_to_display)**2/max_val_to_display,color=[0,0,0])ax[0].plot(x_range*1e6,np.real(S)/poynting_normalization,color=[1,0,1])ax[0].plot(x_range*1e6,np.real(field_to_display),color=[0,0.7,0])ax[0].plot(x_range*1e6,np.imag(field_to_display),color=[1,0,0])figure_title="Iteration%d, "%solution.iterationax[0].set_title(figure_title)ax[0].set_xlabel("x  [$\mu$m]")ax[0].set_ylabel("I, E  [a.u., V/m]")ax[0].set_xlim(x_range[[0,-1]]*1e6)ax[1].plot(x_range[-1]*2e6,0,color=[0,0,0],label='I')ax[1].plot(x_range[-1]*2e6,0,color=[1,0,1],label='$S_{real}$')ax[1].plot(x_range[-1]*2e6,0,color=[0,0.7,0],label='$E_{real}$')ax[1].plot(x_range[-1]*2e6,0,color=[1,0,0],label='$E_{imag}$')ax[1].plot(x_range*1e6,permittivity[0,0].real,color=[0,0,1],label='$\epsilon_{real}$')ax[1].plot(x_range*1e6,permittivity[0,0].imag,color=[0,0.5,0.5],label='$\epsilon_{imag}$')ax[1].set_xlabel('x  [$\mu$m]')ax[1].set_ylabel('$\epsilon$')ax[1].set_xlim(x_range[[0,-1]]*1e6)ax[1].legend(loc='upper right')plt.show(block=True)# Not needed for iPython Jupyter notebookOptimization of time and memory efficiencyElectromagnetic calculations tend to test the limits of the hardware.
Two factors should be considered when optimizing the calculation:
computation and memory. Naturally, the number of operations and the
duration of each operation should be minimized. However, the latter is
often dominated by memory accesses and copying of arrays. The memory
usage therefore does not only affect the size of the problems that can
be solved, it also tends to have an important impact on the total
calculation time.A straightforward method to reduce memory usage is to switch from
128-bit precision complex numbers to 64-bit. By default, the precision
of the source_density is used, which is typicallynp.complex128or
its real equivalent. TheSolution’s defaultdtypecan be
overridden by specifying it assolve(...dtype=np.complex64).
Halving the storage requirements can eliminate additional copies between
the main memory and CPU cache. In extreme cases it can also avoid
swapping. Lower precision math also executes faster on many
architectures.While oversampling to less than 1/10th of the wavelength may aid
visualization, it is often sufficient to sample at a quarter of the
wavelength. The sample solution represents a sinc-interpolated
continuous function. The final result can be visualized with arbitrary
resolution using interpolation.The number of operations can be kept to a minimum by:using non-magnetic and non-chiral materials,using isotropic materials,limiting the largest difference in permittivity (including the
absorbing boundary), andusing a scalar approximation whenever possible.Optimization of the implementation is another route to consider.
Potentially areas of improvement are:Profiling of memory usage and elimination of redundant temporary
copiesIn-place fast-Fourier transforms. When available, theFFTWlibrary is used; however, the drop-in fft
and ifft replacements are used at the moment.Moving the calculations to a GPU or a cloud-computing environment.
Since the copying-overheads may quickly become a bottleneck, it is
important to consider the memory requirements for the problem you
want to solve.DevelopmentTheLibrary API Documentationcan
be found athttps://macromax.readthedocs.io.Source code organizationThe source code is organized as follows:/(root): Module description and distribution files.macromax/: The iterative solver.macromax/utils/: Helper functionality used
in the solver and to use the solver.examples/: Examples of how the solver can be used.tests/: Automated unit tests of the solver’s
functionality. Use this after making modifications to the solver and
extend it if new functionality is added.The library functions are contained inmacromax/:solver: Defines thesolve(...)function
and theSolutionclass.backend: Defines linear algebra
functions to work efficiently with large arrays of 3x3 matrices and
3-vectors.utils/: Defines utility functions that can be
used to prepare and interpret function arguments.The included examples in theexamples/folder are:notebook_example.ipynb: An
iPython notebook demonstrating basic usage of the library.air_glass_air_1D.py: Calculation
of the back reflection from an air-glass interface (one-dimensional
calculation)air_glass_air_2D.py: Calculation
of the refraction and reflection of light hitting a glass window at
an angle (two-dimensional calculation)birefringent_crystal.py:
Demonstration of how an anisotropic permittivity can split a
diagonally polarized Gaussian beam into ordinary and extraordinary
beams.polarizer.py: Calculation of light wave
traversing a set of two and a set of three polarizers as a
demonstration of anisotropic absorption (non-Hermitian permittivity)rutile.py: Scattering from disordered
collection of birefringent rutile (TiO2) particles.benchmark.py: Timing of a simple
two-dimensional calculation for comparison between versions.TestingUnit tests are contained inmacromax/tests. TheBackEndclass inbackend.pyis well covered and specific tests have been written for
theSolutionclass insolver.py.To run the tests, make sure that thenosepackage is installed, and
run the following commands from theMacromax/python/directory:pipinstallnosenosetests-vtestsBuilding and DistributingThesource codeconsists of
pure Python 3, hence only packaging is required for distribution. A
package is generated bysetup.py, which relies on thepypandocpackage:pipinstallpypandocPlease refer to:https://pypi.org/project/pypandoc/for instructions on
its installation for your operating system of choice.To prepare a package for distribution, increase the__version__number inmacromax/__init__.py, and run:pythonsetup.pysdistbdist_wheelpipinstall.--upgradeThe second line installs the newly-forgedmacromaxpackage for
testing.The package can then be uploaded to a test repository as follows:pipinstalltwinetwineupload--repository-urlhttps://test.pypi.org/legacy/dist/*Installing from the test repository is done as follows:pipinstall-ihttps://test.pypi.org/simple/macromax--upgradeTo facilitate importing the code, IntelliJ IDEA/PyCharm project files
can be found inMacroMax/python/:MacroMax/python/python.imland
the folderMacroMax/python/.idea.