.. _tutorial:
Tutorial
========
This tutorial provides a hands-on introduction to the python package ``nemaktis``.
You will learn the different ways of creating permittivity field data,
how to define the sample geometry and material constants, and how to propagate and
visualise optical fields.
First of all, open your favorite text/code editor and create a new python file
(which we will call ``script.py`` in the following). The script can be tested at any
moment in a terminal on condition that the conda environment in which you installed
``nemaktis`` is activated (``conda activate [environment name]``): ::
cd [path to your script]
python script.py
Alternatively, you can work interactively with ipython (which must be run from a terminal in
which the conda environment for ``nemaktis`` is activated).
.. _nfield:
Defining the optical axes
-------------------------
Before starting using ``nemaktis``, we of course need to import the associated python package.
We will also import numpy, which will be needed to define arrays:
.. code-block:: python
import nemaktis as nm
import numpy as np
Next, we need to define the permittivity tensor inside the sample, which is always defined
on a regular cartesian mesh. As a general rule, the permittivity tensor in a non-absorbing
medium can always be represented in terms of two (resp. one) optical axes and three (resp.
two) refractive indices if the the medium is biaxial (resp. uniaxial). Currently, we only
support initialization of the optical axes from a director or Q-tensor field defining a
liquid crystal phase.
In the first case, the phase is unixial and the optical axis simply corresponds to the
director field :math:`\vec{n}`. The relation between permttivity field and unixial optical
axis is as follows (with :math:`n_e` and :math:`n_o` the extraordinay and ordinary indices):
.. math::
\epsilon_{ij} = n_o^2\delta_{ij}+(n_e^2-n_o^2)n_in_j
In the second case, the Q-tensor fully defines the orientational order of the LC phase and
therefore encompasses both biaxial and unixial media. The general definition of the Q-tensor
is as follows:
.. math::
Q_{ij} = \frac{\tilde{S}}{2}\left(n^{(1)}_in^{(1)}_j-\delta_{ij}\right)
+ \frac{\tilde{P}}{2}\left(n^{(2)}_in^{(2)}_j-n^{(3)}_in^{(3)}_j\right)
with :math:`\tilde{S}` and :math:`\tilde{P}` the renormalized scalar order parameter and
biaxiality parameter, :math:`\vec{n}^{(1)}` and :math:`\vec{n}^{(2)}` the two optical axes,
and :math:`\vec{n}^{(3)}=\vec{n}^{(1)}\times\vec{n}^{(2)}`. When :math:`\tilde{S}\neq0` and
:math:`\tilde{P}=0` (resp. :math:`\tilde{P}\neq0`), the phase is unixial (resp. biaxial). An
equilibrium (uniform) nematic liquid crystal phase is associated with :math:`\tilde{S}=1`
and :math:`\tilde{P}=0`, but both these parameters can have different values inside
topological defects. Finally, the relation between the Q-tensor and permittivity tensor is
as follows (with :math:`n_e` and :math:`n_o` the extraordinay and ordinary indices of the
uniform nematic liquid crystal phase):
.. math::
\epsilon_{ij} = \frac{2n_o^2+n_e^2}{3}\delta_{ij}+\frac{2(n_e^2-n_o^2)}{3}Q_{ij}
Note that despite the use of two refractive indices, optical biaxiality near the core of
defects is still taken into account through the associated biaxiality of the Q-tensor.
In ``nemaktis``, any tensor field (director or Q-tensor) is represented internally on a
cartesian regular mesh as a numpy array of shape ``(Nz,Ny,Nx,Nv)``, where ``Nv`` is the
dimension of the tensor data (3 for vector fields such as the director, 6 for symmetric
tensors such as the Q-tensor) and ``Nx``, ``Ny`` and ``Nz`` are the number of mesh points in
each spatial direction. In addition to these variables, one needs to specify the total
lengths of the mesh in each spatial direction, which we will call ``Lx``, ``Ly`` and ``Lz``
in the following. All lengths are in micrometer in ``nemaktis``, and the mesh for the
director field is always centerered on the origin (which means that the spatial coordinate
``u=x,y,z`` is always running from ``-Lu/2`` to ``Lu/2``).
Here, we will focus on a simple director field structure and start by defining an empty
:class:`~nemaktis.lc_material.DirectorField` object on a mesh of dimensions ``80x80x80`` and
lengths ``10x10x10`` (for q-tensor field, use instead
:class:`~nemaktis.lc_material.QTensorField`):
.. code-block:: python
nfield = nm.DirectorField(
mesh_lengths=(10,10,10), mesh_dimensions=(80,80,80))
Next, we need to specify numerical values for the director field. Two
methods are possible: either you already have a numpy array containing
the values of your director field, in which case you can directly give
this array to the :class:`~nemaktis.lc_material.DirectorField` object
(remember, you need to make sure that this array is of shape
``(Nz,Ny,Nx,3)``):
.. code-block:: python
nfield.vals = my_director_vals_numpy_array
Or you have an analytical formula for the director field, in which case you can define three
python functions and give these to the :class:`~nemaktis.lc_material.DirectorField` object.
In this tutorial, we will assume the latter option and define the director field of a double
twist cylinder:
.. code-block:: python
q = 2*np.pi/20
def nx(x,y,z):
r = np.sqrt(x**2+y**2)
return -q*y*np.sinc(q*r)
def ny(x,y,z):
r = np.sqrt(x**2+y**2)
return q*x*np.sinc(q*r)
def nz(x,y,z):
r = np.sqrt(x**2+y**2)
return np.cos(q*r)
nfield.init_from_funcs(nx,ny,nz)
If the analytical formula for the director components do not give normalized director values,
you can still normalize manually the director values after importing them:
.. code-block:: python
nfield.normalize()
Finally, we point out that both the :class:`~nemaktis.lc_material.DirectorField` class used
here and the more general :class:`~nemaktis.lc_material.QTensorField` class derive from a
common class :class:`~nemaktis.lc_material.TensorField` which includes useful geometric
transformation routines (:meth:`~nemaktis.lc_material.TensorField.rotate`,
:meth:`~nemaktis.lc_material.TensorField.rotate_90deg`,
:meth:`~nemaktis.lc_material.TensorField.rotate_180deg`,
:meth:`~nemaktis.lc_material.TensorField.rescale_mesh`,
:meth:`~nemaktis.lc_material.TensorField.extend`)
and a routine :meth:`~nemaktis.lc_material.TensorField.set_mask` allowing the specification
of non-trivial definition domain for the LC phase.
All these methods are documented in the API section of this wiki and are inherited by the
:class:`~nemaktis.lc_material.DirectorField` and :class:`~nemaktis.lc_material.QTensorField`
classes. Here, we will simply demonstrate the capabilities of the tensor field class by
applying a 90° rotation around the axis ``x``, extending the mesh in the ``xy`` plane with a
scale factor of 2, and defining a droplet mask centered on the mesh with a diameter equal to
the mesh height:
.. code-block:: python
nfield.rotate_90deg("x")
nfield.extend(2,2)
nfield.set_mask(mask_type="droplet")
Note that extending the mesh in the xy direction is essential if you define a non-trivial LC
mask, because you need to leave enough room for the optical fields to propagate around the
LC domain.
And that's it, we now have set-up the director field of a double-twist
droplet with the polar axis oriented along the axis ``y``! If you want
to save this director file to a XML VTK file (the standard format used
by the excellent visualisation software `Paraview
`_), you can add the following command to
your script:
.. code-block:: python
nfield.save_to_vti("double_twist_droplet")
You can import back the generated file in any script by directly constructing the DirectorField
object with the path to this file:
.. code-block:: python
nfield = nm.DirectorField(vti_file="double_twist_droplet.vti")
This functionality is especially useful if generating the director field values takes a lot
of time. Of course, the same type of functionality can also be found in the
:class:`~nemaktis.lc_material.QTensorField` class.
.. _lcmat:
Defining a LCMaterial
---------------------
The next step is to define possible isotropic layers above the LC layer (which can distort
the optical fields on the focal plane), as well as the refractive indices of all the
materials in the sample. Since our system here consists of a droplet embedded in another
fluid, we need to specify both extraordinay and ordinary indices for the LC droplet and the
refractive index of the host fluid. To approximate the reflection loss at the entrance of
the LC layer, the index ``nin`` of the medium just below the LC layer can also be set.
Finally, the index ``nout`` of the medium between the sample and the microscope objective
can also be set and allows to specify an objective's numerical aperture greater than one
(e.g. in the case of an oil-immersion objective) All these informations are stored in the
class :class:`~nemaktis.lc_material.LCMaterial`:
.. code-block:: python
mat = nm.LCMaterial(
lc_field=nfield, ne=1.5, no=1.7, nhost=1.55, nin=1.51, nout=1)
Note that you can also specify refractive indices with a string expression depending on the
wavelength variable "lambda" (in µm), in case you want to take into account the dispersivity
of the materials of your sample.
We also want to add a glass plate above the sample and additional space for the host fluid
between the droplet and the glass plate:
.. code-block:: python
mat.add_isotropic_layer(nlayer=1.55, thickness=5) # 5 µm space between the droplet and glass plate
mat.add_isotropic_layer(nlayer=1.51, thickness=1000) # 1mm-thick glass plate
Using all those informations, Fresnel reflections in the isotropic layers above the sample
can be calculated exactly and also give the fields transmitted through the sample.
The full details of the isotropic layers below the sample are not needed because in
``nemaktis`` the incident optical fields always correspond to a set of plane waves whose
wavectors are weakly tilted with respect to the ``z`` direction (in which case the amplitude
of the fields is uniformly affected by any isotropic layers orthogonal to ``z``). However,
anisotropic Fresnel boundary conditions at the entrance of the LC layer may affect the
optical fields in a space-dependent way. These interface conditions are determined from the
refractive indices given in the constructor of the LCMaterial, as explained above.
.. _prop:
Propagating optical fields through the sample
---------------------------------------------
Now that the sample geometry is fully caracterized, we can propagate fields through the
sample and through an objective into the visualisation plane (which we initially assume to be
conjugate to the center of the sample), as in a real microscope (see :ref:`microscopy_model` for
more details): a set of plane waves with different wavevectors and wavelengths are sent on
the LC sample, and the associated transmitted optical fields are calculated using one of the
backend.
The actual set of wavelengths for the plane waves approximate the relevant part of the
spectrum of the illumination light, whereas the set of wavevectors is determined from the
numerical aperture of the input condenser. The more open the condenser aperture is, the
smoother the micrograph will look, since an open condenser aperture is associated with a
wide range of angle for the wavectors of the mutually incoherent incident plane waves.
Conversely, an almost closed condenser aperture is associated with a single plane wave
incident normally on the sample.
With ``nemaktis``, the propagation of optical field through a LC sample is as simple as
defining an array of wavelengths defining the spectrum of the light source, creating a
:class:`~nemaktis.light_propagator.LightPropagator` object, and calling the method
:class:`~nemaktis.light_propagator.LightPropagator.propagate_fields`:
.. code-block:: python
wavelengths = np.linspace(0.4, 0.8, 11)
sim = nm.LightPropagator(
material=mat, wavelengths=wavelengths, max_NA_objective=0.4,
max_NA_condenser=0, N_radial_wavevectors=1)
output_fields = sim.propagate_fields(method="bpm")
The parameter ``max_NA_objective`` defined in this code snippet corresponds to the maximal
numerical aperture of the microscope objective. The parameters ``max_NA_condenser`` and
``N_radial_wavevectors`` respectively sets the maximal numerical aperture of the input
condenser aperture and the number ``Nr`` of incident wavevectors in the radial direction of the
condenser (the total number of wavevectors will be ``1+3*Nr*(Nr-1)``, so be carefull to not
set a value too big to avoid memory overflow or long running time). Here, we assume an
almost fully closed condenser aperture, so we set the numerical aperture to zero and the
total number of wavevectors to 1. Note that omitting the two parameters ``max_NA_objective``
and ``N_radial_wavevectors`` during the construction of the
:class:`~nemaktis.light_propagator.LightPropagator` object will default to these values,
i.e. this class will assume that there is only one single plane wave incident normally on
the sample. Finally, we mention that you will be able to dynamically set the actual values
of the numerical aperture of the objective and condenser later on when visualizing the
optical fields (with the constraints that these quantities must always be comprised between
0 and the max bounds set here).
The :class:`~nemaktis.light_propagator.LightPropagator.propagate_fields` method uses
the specified backend to propagate fields (here, ``bpm-solver``) and returns an
:class:`~nemaktis.light_propagator.OpticalFields` object containing the results of the
simulation. Periodic boundary conditions in the ``x`` and ``y`` directions are systematically
assumed, so you should always extend apropriately your director field in order to have a
uniform field near the mesh boundaries.
Note that internally two simulations are run for each wavelength and wavevector, one with an
input light source polarised along ``x`` and the other with an input light source polarised
along ``y``. This allows us to fully caracterize the transmission matrix of the sample and
reconstruct any type of micrographs (bright field, crossed polariser...), as explained in
:ref:`microscopy_model`. Similaryly to the :class:`~nemaktis.lc_material.DirectorField` object,
you can save the output fields to a XML VTK file, and reimport them in other scripts:
.. code-block:: python
# If you want to save the simulation results
output_fields.save_to_vti("optical_fields")
# If you want to reimport saved simulation results
output_fields = nm.OpticalFields(vti_file="optical_fields.vti")
.. _viz:
Visualising optical micrographs
-------------------------------
To help the user visualise optical micrographs as in a real microscope, ``nemaktis`` includes
a graphical user interface allowing to generate any type of micrograph in real-time. Once
you have generated/imported optical fields in you script, you can start using this interface
with the following lines of code:
.. code-block:: python
viewer = nm.FieldViewer(output_fields)
viewer.plot()
All parameters in this user interface should be pretty self-explanatory, with lengths
expressed in µm and optical element angles in ° with respect to ``x``. We will simply
mention here that the quarter-wavelength and half-wavelength compensators are assumed to be
achromatic, while the full-wave "tint sensitive" compensator is aproximated with a slab of
wavelength-independent refractive index with a full-wave shift at a wavelength of 540 nm.
Concerning color management, we assume a D65 light source and project the output light spectrum
first on the XYZ space, then on the sRGB color space, to finally obtain a usual RGB picture.
For more details, see ``_.
Finally, refocalisation of the optical micrographs is done by switching to Fourrier space and
using the exact propagator for the Helmholtz equation in free space. The
unit for the ``z-focus`` parameter is again micrometers.