benvial/gyptis

View on GitHub
tutorials/basic/plot_basic.py

Summary

Maintainability
A
0 mins
Test Coverage
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Author: Benjamin Vial
# This file is part of gyptis
# Version: 1.0.2
# License: MIT
# See the documentation at gyptis.gitlab.io


# sphinx_gallery_thumbnail_number = -1

"""
A scattering simulation in 2D
-----------------------------

In this example we will study the diffraction of a plane wave by a nanorod
"""


############################################################################
# We first need to import the Python packages

import matplotlib.pyplot as plt
import numpy as np

import gyptis as gy

plt.ion()
plt.close("all")

############################################################################
# Define the wavelength of operation. This can be choosen arbitrarily, but
# it is best for numerical stability not to use too big/small numbers
# (e.g. if we work in optics, it is better to assume the
# units are in microns amd use ``wavelength = 0.8`` rather than considering
# meters and ``wavelength = 800e-9``).

wavelength = 0.8

############################################################################
# Silicon permittivity at this wavelength:

epsilon_rod = 13.646 - 1j * 0.048


############################################################################
# We now define the geometry using the class :class:`~gyptis.BoxPML` a
# subclass of :class:`~gyptis.Geometry`.
# This is a rectangular box in 2D (with the argument `dim=2`) centered at the
# origin by default and surrounded by Cartesian Perfectly Matched Layers.
# The important arguments are its size `box_size` and  the witdh of the
# PMLs along :math:`x` and :math:`y`.
# And optinal argument ``Rcalc`` can be used
# to build a circular boundary containing the scatterer(s) in order to compute
# cross sections.


geom = gy.BoxPML(
    dim=2,
    box_size=(5 * wavelength, 5 * wavelength),
    pml_width=(wavelength, wavelength),
    Rcalc=2.4 * wavelength,
)


############################################################################
# Now we add a circular rod.
radius = wavelength * 0.5
rod = geom.add_circle(0, 0, 0, radius)

############################################################################
# We use the boolean operation :meth:`~gyptis.BoxPML.fragment` to substract
# the rod from the box and get the remaining entities:

rod, *box = geom.fragment(rod, geom.box)


############################################################################
# Add physical domains:

geom.add_physical(box, "box")
geom.add_physical(rod, "rod")

############################################################################
# And set the mesh sizes. A good practice is to have a mesh size that
# is smaller than the wavelength in the media to resolve the field
# so ``size = wavelength / (n*pmesh)``, with ``n`` the refractive index.

pmesh = 10
geom.set_pml_mesh_size(wavelength / pmesh)
geom.set_size("box", wavelength / pmesh)
geom.set_size("rod", wavelength / (pmesh * np.real(epsilon_rod) ** 0.5))

############################################################################
# We can now build and mesh the geometry. The method
# :meth:`~gyptis.BoxPML.build` takes an ``interactive`` boolean argument
# to open and wisualize the geometry in ``gmsh`` (usefull for debugging).

geom.build()

############################################################################
# .. attention::
#       A geometry object cannot be modified after the method
#       :meth:`~gyptis.BoxPML.build` has been called: you should create a new object,
#       define the geometry and set mesh parameters before building.


############################################################################
# Visualize the mesh:

fig, ax = plt.subplots(figsize=(2, 2))
geom.plot_subdomains(ax=ax)
geom.plot_mesh(ax=ax, color="red", lw=0.2)
plt.axis("equal")
plt.xlabel("$x$ (μm)")
plt.ylabel("$y$ (μm)")
plt.tight_layout()

############################################################################
# Visualize the subdomains:

fig, ax = plt.subplots(figsize=(3, 2.3))
out = geom.plot_subdomains(markers=True, ax=ax)
plt.axis("scaled")
plt.xlabel("$x$ (μm)")
plt.ylabel("$y$ (μm)")
plt.tight_layout()


############################################################################
# We define the incident plane wave and plot it. The angle is in radian and
# ``theta=0`` corresponds to a wave travelling from the bottom.


pw = gy.PlaneWave(wavelength=wavelength, angle=0, dim=2, domain=geom.mesh, degree=2)

fig, ax = plt.subplots(1, 2, figsize=(4, 1.8))
fig, ax, maps, cb = pw.plot(ax=ax)
plt.axis("scaled")
for a in ax[:2]:
    geom.plot_subdomains(ax=a)
    a.set_xlabel("$x$ (μm)")
    a.set_ylabel("$y$ (μm)")
plt.tight_layout()


############################################################################
# Initialize the simulation. By default, materials properties (``epsilon`` and
# ``mu`` arguments are ``None``, and they will be initialized to unity in all
# subdomains). The values for the PMLs are constructed automatically.

epsilon = dict(box=1, rod=epsilon_rod)

simulation = gy.Scattering(
    geom,
    epsilon=epsilon,
    source=pw,
    degree=2,
    polarization="TE",
)

############################################################################
# We are now ready to solve the problem with the FEM:

simulation.solve()

############################################################################
# The attribute ``simulation.solution`` is a dictionary with keys
# ``diffracted`` and ``total`` containing the diffracted and total fields.

print(simulation.solution)

############################################################################
# Lets plot the results. By default, we visualize the real part of the total field:

fig, ax = plt.subplots(1, figsize=(2.5, 2))
simulation.plot_field(ax=ax)
geom_lines = geom.plot_subdomains()
plt.xlabel(r"$x$ (μm)")
plt.ylabel(r"$y$ (μm)")
plt.tight_layout()


############################################################################
# But we can switch to plot the module of the diffracted field:

fig, ax = plt.subplots(1, figsize=(2.5, 2))
simulation.plot_field(ax=ax, field="diffracted", type="module", cmap="inferno")
geom_lines = geom.plot_subdomains(color="white")
plt.xlabel(r"$x$ (μm)")
plt.ylabel(r"$y$ (μm)")
plt.tight_layout()


##############################################################################
# Compute cross sections (in 2D those are rather scattering width *etc.*,
# or cross sections per unit length) and check energy conservation (optical theorem).


cs = simulation.get_cross_sections()
print(f'scattering width = {cs["scattering"]:0.4f}μm')
print(f'absorption width = {cs["absorption"]:0.4f}μm')
print(f'extinction width = {cs["extinction"]:0.4f}μm')
balance = (cs["scattering"] + cs["absorption"]) / cs["extinction"]
print(f"    balance      = {balance}")
assert np.allclose(cs["extinction"], cs["scattering"] + cs["absorption"], rtol=1e-3)


##############################################################################
# Lets recalculate for TM polarization

simulationTM = gy.Scattering(
    geom,
    epsilon=epsilon,
    source=pw,
    degree=2,
    polarization="TM",
)

simulationTM.solve()
fig, ax = plt.subplots(1, figsize=(2.5, 2))
simulationTM.plot_field(ax=ax, field="diffracted", type="module", cmap="inferno")
geom_lines = geom.plot_subdomains(color="white")
plt.xlabel(r"$x$ (μm)")
plt.ylabel(r"$y$ (μm)")
plt.tight_layout()


cs = simulationTM.get_cross_sections()
print(f'scattering width = {cs["scattering"]:0.4f}μm')
print(f'absorption width = {cs["absorption"]:0.4f}μm')
print(f'extinction width = {cs["extinction"]:0.4f}μm')
balance = (cs["scattering"] + cs["absorption"]) / cs["extinction"]
print(f"    balance      = {balance}")
assert np.allclose(cs["extinction"], cs["scattering"] + cs["absorption"], rtol=1e-3)