Tutorial: Calculating Spectra

01/2021: updated to pyGDM v1.1+

This is an example how to calculate spectra from pyGDM simulations.

We start again by loading the pyGDM modules that we are going to use:

[1]:
## --- Load the pyGDM modules
from pyGDM2 import structures
from pyGDM2 import materials
from pyGDM2 import fields
from pyGDM2 import propagators
from pyGDM2 import core
from pyGDM2 import linear
from pyGDM2 import visu
from pyGDM2 import tools

## --- we will use numpy as well
import numpy as np

Setting up the simulation

[2]:
## --- we will use a sphere, here R=60nm and this time made of gold
## --- for demonstration purpose we will discretize rather coarse
step = 20
geometry = structures.sphere(step, R=3, mesh='hex')
material = materials.gold()
struct = structures.struct(step, geometry, material)


## --- we use again a plane wave
field_generator = fields.plane_wave
## --- this time however, we want to calculate a whole spectrum.
## --- we use numpy's *linspace* to get a list of linearly
## --- spaced wavelengths
wavelengths = np.linspace(400, 1000, 30)
## --- let's furthermore simulate three linear polarizations at normal incidence
kwargs = dict(theta=[0, 45, 90], inc_angle=180)

efield = fields.efield(field_generator,
               wavelengths=wavelengths, kwargs=kwargs)

## --- vacuum environment
n1 = n2 = 1.0
dyads = propagators.DyadsQuasistatic123(n1=n1, n2=n2)


## --- define the numerical experiment
sim = core.simulation(struct, efield, dyads)
structure initialization - automatic mesh detection: hex
structure initialization - consistency check: 195/195 dipoles valid

Run the simulation

Note: The output of scatter can be turned off by passing “verbose=False”

[3]:
## --- run the simulation
sim.scatter()
/home/hans/.local/lib/python3.8/site-packages/numba/core/dispatcher.py:238: UserWarning: Numba extension module 'numba_scipy' failed to load due to 'ValueError(No function '__pyx_fuse_0pdtr' found in __pyx_capi__ of 'scipy.special.cython_special')'.
  entrypoints.init_all()
timing for wl=400.00nm - setup: EE 2787.3ms, inv.: 171.2ms, repropa.: 802.0ms (3 field configs), tot: 3761.0ms
timing for wl=420.69nm - setup: EE 21.3ms, inv.: 136.8ms, repropa.: 31.6ms (3 field configs), tot: 190.1ms
timing for wl=441.38nm - setup: EE 20.1ms, inv.: 148.5ms, repropa.: 34.5ms (3 field configs), tot: 203.3ms
timing for wl=462.07nm - setup: EE 20.7ms, inv.: 167.5ms, repropa.: 32.1ms (3 field configs), tot: 220.5ms
timing for wl=482.76nm - setup: EE 20.3ms, inv.: 124.2ms, repropa.: 31.3ms (3 field configs), tot: 176.1ms
timing for wl=503.45nm - setup: EE 19.4ms, inv.: 82.7ms, repropa.: 32.6ms (3 field configs), tot: 135.0ms
timing for wl=524.14nm - setup: EE 20.0ms, inv.: 159.5ms, repropa.: 36.1ms (3 field configs), tot: 216.3ms
timing for wl=544.83nm - setup: EE 21.5ms, inv.: 108.5ms, repropa.: 31.4ms (3 field configs), tot: 161.7ms
timing for wl=565.52nm - setup: EE 20.9ms, inv.: 121.6ms, repropa.: 32.5ms (3 field configs), tot: 175.8ms
timing for wl=586.21nm - setup: EE 20.1ms, inv.: 173.4ms, repropa.: 30.8ms (3 field configs), tot: 224.4ms
timing for wl=606.90nm - setup: EE 21.6ms, inv.: 99.1ms, repropa.: 31.2ms (3 field configs), tot: 152.3ms
timing for wl=627.59nm - setup: EE 19.2ms, inv.: 151.3ms, repropa.: 31.8ms (3 field configs), tot: 202.7ms
timing for wl=648.28nm - setup: EE 21.3ms, inv.: 122.2ms, repropa.: 31.5ms (3 field configs), tot: 175.2ms
timing for wl=668.97nm - setup: EE 19.9ms, inv.: 147.3ms, repropa.: 34.5ms (3 field configs), tot: 202.0ms
timing for wl=689.66nm - setup: EE 21.1ms, inv.: 84.2ms, repropa.: 32.0ms (3 field configs), tot: 137.5ms
timing for wl=710.34nm - setup: EE 18.6ms, inv.: 142.0ms, repropa.: 34.9ms (3 field configs), tot: 195.6ms
timing for wl=731.03nm - setup: EE 23.1ms, inv.: 86.7ms, repropa.: 31.3ms (3 field configs), tot: 141.3ms
timing for wl=751.72nm - setup: EE 19.3ms, inv.: 124.4ms, repropa.: 32.8ms (3 field configs), tot: 177.1ms
timing for wl=772.41nm - setup: EE 20.1ms, inv.: 90.6ms, repropa.: 31.9ms (3 field configs), tot: 142.8ms
timing for wl=793.10nm - setup: EE 20.2ms, inv.: 242.2ms, repropa.: 33.0ms (3 field configs), tot: 295.8ms
timing for wl=813.79nm - setup: EE 18.2ms, inv.: 82.2ms, repropa.: 33.2ms (3 field configs), tot: 133.7ms
timing for wl=834.48nm - setup: EE 18.7ms, inv.: 136.0ms, repropa.: 32.1ms (3 field configs), tot: 187.0ms
timing for wl=855.17nm - setup: EE 19.1ms, inv.: 106.7ms, repropa.: 31.4ms (3 field configs), tot: 157.8ms
timing for wl=875.86nm - setup: EE 19.1ms, inv.: 125.2ms, repropa.: 31.2ms (3 field configs), tot: 175.7ms
timing for wl=896.55nm - setup: EE 19.7ms, inv.: 100.6ms, repropa.: 32.0ms (3 field configs), tot: 152.6ms
timing for wl=917.24nm - setup: EE 20.1ms, inv.: 111.1ms, repropa.: 32.3ms (3 field configs), tot: 164.3ms
timing for wl=937.93nm - setup: EE 21.5ms, inv.: 126.1ms, repropa.: 31.7ms (3 field configs), tot: 180.0ms
timing for wl=958.62nm - setup: EE 19.1ms, inv.: 97.5ms, repropa.: 30.7ms (3 field configs), tot: 147.5ms
timing for wl=979.31nm - setup: EE 19.5ms, inv.: 151.6ms, repropa.: 31.5ms (3 field configs), tot: 203.1ms
timing for wl=1000.00nm - setup: EE 26.8ms, inv.: 94.5ms, repropa.: 32.4ms (3 field configs), tot: 154.6ms
[3]:
1

Short comment: Structure of the simulation results

E, as well as the simulation instance (sim.E) now contain the self-consistent electric field inside the nano-particle for each incident field configuration. sim.E is a list of lists. Each element of sim.E contains as first entry the simulation configuration encoded as a dictionary with keys cooresponding to the parameters taken by the field-generator (in our case this was fields.planewave). The second entry is a list of complex 3-tuples (Ex, Ey, Ez), corresponding to the complex electric field at the according meshpoint (defined by sim.structure.geometry).

Let’s have a look:

[4]:
print("fieldindex '0' :", sim.E[0][0])
print("fieldindex '1' :", sim.E[1][0])
print("fieldindex '17':", sim.E[17][0])
print("Shape of field :", sim.E[17][1].shape)
print("     (--> (Number of meshopints, E_X;E_Y;E_Z) )")
fieldindex '0' : {'inc_angle': 180, 'theta': 0, 'wavelength': 400.0}
fieldindex '1' : {'inc_angle': 180, 'theta': 45, 'wavelength': 400.0}
fieldindex '17': {'inc_angle': 180, 'theta': 90, 'wavelength': 503.44827586206895}
Shape of field : (195, 3)
     (--> (Number of meshopints, E_X;E_Y;E_Z) )

If we search a particular field-configuration, the fieldindex notation is probably not very intuitive since it is a sequentiel number, specifying one within all possible permutations of field-parameters.

To find the closest available configuration for specific search-values, one can use:

[5]:
search_dict = dict(theta=80, inc_angle=180, wavelength=750)
idx = tools.get_closest_field_index(sim, search_dict)

print('closest match: index', idx)
print('    --> dict:', sim.E[idx][0])
closest match: index 53
    --> dict: {'inc_angle': 180, 'theta': 90, 'wavelength': 751.7241379310344}

Evaluating the simulation

The field inside the particle might be interesting to know, but since it is hardly accessible in experiment, usually more easily accessible physical quantities are derived from the internal fields.

Let’s calculate the extinction, scattering and absorption corss-section for the above field configuration:

Note: all high-level evaluation function like provided by the linear module take (at least) the simulation object and a fieldindex as first two parameters.

[6]:
a_ext, a_scat, a_abs = linear.extinct(sim, idx)
print("cross sections:")
print("    extinction = {:.2f} nm^2".format(float(a_ext)))
print("    scattering = {:.2f} nm^2".format(float(a_scat)))
print("    absorption = {:.2f} nm^2".format(float(a_abs)))
cross sections:
    extinction = 14891.85 nm^2
    scattering = 8751.90 nm^2
    absorption = 6139.95 nm^2

Calculating a spectrum

To calculate a full spectrum of some physical quantity like the above corss sections, we now simply need to evaluate all the fields composing a spectrum. Because again, it is not evident, which fieldindices correspond to a spectrum for, let’s say, X-polarization (theta=0), there are helper-functions for spectrum-calculation, provided by pyGDM:

[7]:
## get all simulated configurations, each corresponding to a spectrum
field_kwargs = tools.get_possible_field_params_spectra(sim)
for i, conf in enumerate(field_kwargs):
    print("config", i, ":", conf)
config 0 : {'inc_angle': 180, 'theta': 0}
config 1 : {'inc_angle': 180, 'theta': 45}
config 2 : {'inc_angle': 180, 'theta': 90}

Now let’s calculate and plot cross-sections for the spectrum “config 0”:

[8]:
## --- calculate a spectrum using a specific evaluation function.
## --- We will use the spectrum with theta=0 on "linear.extinct":
## Note that instead of 'field_kwargs[config_idx]' we could also pass
## just the index of the configuration `0` (--> config_idx).
config_idx = 0
wl, spectrum = tools.calculate_spectrum(sim,
                    field_kwargs[config_idx], linear.extinct)

## --- linear.extinct returns 3-tuples, "spectrum" therefore consists
## --- of an array of 3-tuples, corresponding to extinction,
## --- scattering and absorption.

import matplotlib.pyplot as plt

plt.plot(wl, spectrum.T[0], 'g-', label='ext.')
plt.plot(wl, spectrum.T[1], 'b-', label='scat.')
plt.plot(wl, spectrum.T[2], 'r-', label='abs.')

plt.xlabel("wavelength (nm)")
plt.ylabel("cross section (nm^2)")
plt.legend(loc='best', fontsize=8)

plt.show()
../_images/tutorials_02_spectra_15_0.png

We can use any of the other evaluation functions to calculate a spectrum:

[9]:
## --- total deposited heat in particle
wl, spec_heat = tools.calculate_spectrum(sim,
          field_kwargs[config_idx], linear.heat)


## --- temperature increase at [0,0,200] (nm)
wl, spec_temp = tools.calculate_spectrum(sim,
          field_kwargs[config_idx], linear.temperature, r_probe=[0,0,200])


## --- nearfield at [0,0,200] (nm)
wl, spec_nf = tools.calculate_spectrum(sim,
          field_kwargs[config_idx], linear.nearfield, r_probe=[0,0,200])
## --- calculate the nearfield intensity (Ex**2 + Ey**2 + Ez**2)
## indices [0][1] denote:
##         - [0] first of one evaluated positions
##         - [1] total Efield (E0 + Escat)
spec_nf_intensity = (np.abs(spec_nf.T[3])**2 +
                     np.abs(spec_nf.T[4])**2 +
                     np.abs(spec_nf.T[5])**2)[0][1]
/home/hans/.local/lib/python3.8/site-packages/pyGDM2/linear.py:1320: UserWarning: `linear_py.heat` does not support tensorial permittivity yet.
  warnings.warn("`linear_py.heat` does not support tensorial permittivity yet.")
[10]:
## --- plot the spectra
plt.figure(figsize=(7,10))
plt.subplot(311)
plt.plot(wl, spec_heat/1E3)
plt.ylabel("Q (micro Watt)")

plt.subplot(312)
plt.plot(wl, spec_temp)
plt.ylabel("Delta T (Kelvin)")

plt.subplot(313)
plt.plot(wl, spec_nf_intensity)
plt.xlabel("wavelength (nm)")
plt.ylabel("|E|**2 / |E0|**2")

plt.show()
../_images/tutorials_02_spectra_18_0.png

Finally, let’s compare the three different polarizations we simulated. Since we calculated a sphere in vaccum, they should result in an identical response.

Let’s check the scattering spectra:

[11]:
wl, spec0 = tools.calculate_spectrum(sim,
                    field_kwargs[0], linear.extinct)
wl, spec45 = tools.calculate_spectrum(sim,
                    field_kwargs[1], linear.extinct)
wl, spec90 = tools.calculate_spectrum(sim,
                    field_kwargs[2], linear.extinct)


import matplotlib.pyplot as plt

plt.plot(wl, spec0.T[1], 'r-', label='0 deg')
plt.plot(wl, spec45.T[1], 'b--', label='45 deg')
plt.plot(wl, spec45.T[1], 'g', dashes=[2,2], label='90 deg')

plt.xlabel("wavelength (nm)")
plt.ylabel("scattering cross section (nm^2)")
plt.legend(loc='best', fontsize=8)

plt.show()
../_images/tutorials_02_spectra_20_0.png

Looks as if the curves were overlapping. Good!

Simulation with polarization dependent response

Finally, let’s try what happens if we use a non-symmetric structure which should have an optical response that varies with the incident polarization angle.

[12]:
## --- rectangular wire with otherwise same configuration as above
geometry = structures.rect_wire(step, L=20, W=4, H=3)
struct = structures.struct(step, geometry, material)

## --- same spectrum as above but with a lot more polarization angles
kwargs = dict(theta=np.linspace(0, 90, 31), inc_angle=180)
efield = fields.efield(field_generator,
               wavelengths=wavelengths, kwargs=kwargs)

## --- define the numerical experiment
sim_polarizations = core.simulation(struct, efield, dyads)

## --- run the simulation
sim_polarizations.scatter()
structure initialization - automatic mesh detection: cube
structure initialization - consistency check: 240/240 dipoles valid
timing for wl=400.00nm - setup: EE 24.6ms, inv.: 151.3ms, repropa.: 449.8ms (31 field configs), tot: 625.9ms
timing for wl=420.69nm - setup: EE 28.4ms, inv.: 173.8ms, repropa.: 406.5ms (31 field configs), tot: 609.6ms
timing for wl=441.38nm - setup: EE 26.3ms, inv.: 128.2ms, repropa.: 412.3ms (31 field configs), tot: 567.3ms
timing for wl=462.07nm - setup: EE 25.5ms, inv.: 93.7ms, repropa.: 405.0ms (31 field configs), tot: 524.4ms
timing for wl=482.76nm - setup: EE 27.0ms, inv.: 103.6ms, repropa.: 402.6ms (31 field configs), tot: 533.4ms
timing for wl=503.45nm - setup: EE 24.1ms, inv.: 95.6ms, repropa.: 404.2ms (31 field configs), tot: 524.1ms
timing for wl=524.14nm - setup: EE 26.8ms, inv.: 93.9ms, repropa.: 393.8ms (31 field configs), tot: 514.9ms
timing for wl=544.83nm - setup: EE 24.7ms, inv.: 93.6ms, repropa.: 403.1ms (31 field configs), tot: 521.6ms
timing for wl=565.52nm - setup: EE 26.3ms, inv.: 98.4ms, repropa.: 411.9ms (31 field configs), tot: 536.8ms
timing for wl=586.21nm - setup: EE 33.4ms, inv.: 111.5ms, repropa.: 417.2ms (31 field configs), tot: 562.4ms
timing for wl=606.90nm - setup: EE 24.0ms, inv.: 92.4ms, repropa.: 400.9ms (31 field configs), tot: 517.5ms
timing for wl=627.59nm - setup: EE 24.3ms, inv.: 110.4ms, repropa.: 398.6ms (31 field configs), tot: 533.6ms
timing for wl=648.28nm - setup: EE 29.0ms, inv.: 99.2ms, repropa.: 402.4ms (31 field configs), tot: 530.8ms
timing for wl=668.97nm - setup: EE 25.5ms, inv.: 121.5ms, repropa.: 396.3ms (31 field configs), tot: 543.6ms
timing for wl=689.66nm - setup: EE 24.0ms, inv.: 112.9ms, repropa.: 412.1ms (31 field configs), tot: 549.1ms
timing for wl=710.34nm - setup: EE 26.2ms, inv.: 117.2ms, repropa.: 401.0ms (31 field configs), tot: 545.1ms
timing for wl=731.03nm - setup: EE 23.7ms, inv.: 100.9ms, repropa.: 390.9ms (31 field configs), tot: 516.0ms
timing for wl=751.72nm - setup: EE 23.8ms, inv.: 100.2ms, repropa.: 410.6ms (31 field configs), tot: 534.9ms
timing for wl=772.41nm - setup: EE 24.8ms, inv.: 135.5ms, repropa.: 402.5ms (31 field configs), tot: 562.9ms
timing for wl=793.10nm - setup: EE 26.2ms, inv.: 150.2ms, repropa.: 396.7ms (31 field configs), tot: 573.3ms
timing for wl=813.79nm - setup: EE 24.9ms, inv.: 98.6ms, repropa.: 395.6ms (31 field configs), tot: 519.3ms
timing for wl=834.48nm - setup: EE 25.1ms, inv.: 122.8ms, repropa.: 395.6ms (31 field configs), tot: 543.7ms
timing for wl=855.17nm - setup: EE 26.3ms, inv.: 98.8ms, repropa.: 397.0ms (31 field configs), tot: 522.3ms
timing for wl=875.86nm - setup: EE 25.1ms, inv.: 122.1ms, repropa.: 397.1ms (31 field configs), tot: 544.5ms
timing for wl=896.55nm - setup: EE 33.8ms, inv.: 94.2ms, repropa.: 399.2ms (31 field configs), tot: 527.3ms
timing for wl=917.24nm - setup: EE 25.8ms, inv.: 128.5ms, repropa.: 394.9ms (31 field configs), tot: 549.5ms
timing for wl=937.93nm - setup: EE 24.2ms, inv.: 98.6ms, repropa.: 395.1ms (31 field configs), tot: 518.4ms
timing for wl=958.62nm - setup: EE 27.0ms, inv.: 131.3ms, repropa.: 392.6ms (31 field configs), tot: 551.1ms
timing for wl=979.31nm - setup: EE 23.4ms, inv.: 94.1ms, repropa.: 401.9ms (31 field configs), tot: 519.7ms
timing for wl=1000.00nm - setup: EE 24.6ms, inv.: 114.2ms, repropa.: 385.7ms (31 field configs), tot: 524.8ms
[12]:
1
[13]:
## --- get the spectra-configurations
spectra_kwargs = tools.get_possible_field_params_spectra(sim_polarizations)

## --- plot scattering for all configs (--> diff. polarizations)
colors = plt.cm.Blues(np.linspace(0.3, 1.0, len(spectra_kwargs)))
for i, field_kwargs in enumerate(spectra_kwargs):
    ## here we call `calculate_spectrum` just using the spectrum's index (--> `i`)
    wl, spec0 = tools.calculate_spectrum(sim_polarizations, i, linear.extinct)

    lab = ''
    if i in [0, len(spectra_kwargs)/2, len(spectra_kwargs)-1]:
        lab = field_kwargs['theta']
    plt.plot(wl, spec0.T[1], color=colors[i], label=lab)

plt.legend(loc='best')
plt.xlabel("wavelength (nm)")
plt.ylabel("scattering cross section (nm^2)")

plt.show()
../_images/tutorials_02_spectra_24_0.png

The different shades of blue indicate the different incident linear polarizations from light blue (zero degrees, parallel to the rod) to dark blue (90 degrees, perpendicular to the rod). We actually found two isosbestic points, where the response is independent of the polarization (around 500nm and around 600nm).