NanoComp / meep

free finite-difference time-domain (FDTD) software for electromagnetic simulations
GNU General Public License v2.0
1.23k stars 625 forks source link

Mismatch Mie Scattering Au Sphere Submerged in Water #1530

Open 0xInfty opened 3 years ago

0xInfty commented 3 years ago

Hello!

I've been racking my brains for two weeks with a problem I hope you'll be able to help me with. Sorry to borrow some of your time and thanks for reading.

I'm simulating an Au sphere with 103 nm diameter submerged in water. To know whether I've understood Meep or not, I'm comparing the results with Mie theory. However, the difference between the model's and the simulation's maximum scattering wavelength is huge. The simulation presents an alarming redshift of 21 nm because its maximum is at 601 nm aprox. and the theory's maximum is at 582 nm aprox. Moreover, the shape of the spectrum seems to be wrong too, because it's wider than it should be when compared to the theory.

Regarding my Meep environment, I've recently updated it and it says it has Meep version 1.17.2-alpha.706753. I'm running the code in parallel in my computer using mpirun --use-hwthread-cpus -np 6 with MPI version 1.3. That way I get to use most of the Intel Core i7-9700F CPU of my group's PC, wich also has 48 GB of RAM and 48 GB of Swap Space in its Ubuntu 20.04.2 LTS.

The code is very similar to the one that appears on the tutorial. the only unknown function to you should be import_medium from my v_materials module, but all it does is rescaling the materials' properties according to from_um_factor (actually you'll see it's mostly a copy of Meep's Materials library). Since I'm calculating scattering cross section, I have to run two simulations like this:

import meep as mp
import numpy as np
from v_materials import import_medium

#%% PARAMETERS

### MEAN PARAMETERS

# Units: 10 nm as length unit
from_um_factor = 10e-3 # Conversion of 1 μm to my length unit (=10nm/1μm)
resolution = 6 # >=8 pixels per smallest wavelength, i.e. np.floor(8/wvl_min)

# Au sphere
r = 5.15  # Radius of sphere: 103 nm
medium = import_medium("Au", from_um_factor) # Medium of sphere: gold (Au)
submerged_index = 1.333 # Water refractive index

# Frequency and wavelength
wlen_range = np.array([50,65]) # 500-650 nm range from lowest to highest
nfreq = 100 # Number of frequencies to discretize range
cutoff = 3.2

# Computation time
time_factor_cell = 1.2
second_time_factor = 10

### OTHER PARAMETERS

# Frequency and wavelength
freq_range = 1/wlen_range # Hz range in Meep units from highest to lowest
freq_center = np.mean(freq_range)
freq_width = max(freq_range) - min(freq_range)

# Space configuration
pml_width = 0.38 * max(wlen_range)
air_width = r/2 # 0.5 * max(wlen_range)

#%% GENERAL GEOMETRY SETUP

air_width = air_width - air_width%(1/resolution)

pml_width = pml_width - pml_width%(1/resolution)
pml_layers = [mp.PML(thickness=pml_width)]

cell_width = 2 * (pml_width + air_width + r)
cell_width = cell_width - cell_width%(1/resolution)
cell_size = mp.Vector3(cell_width, cell_width, cell_width)

source_center = -0.5*cell_width + pml_width
sources = [mp.Source(mp.GaussianSource(freq_center,
                                       fwidth=freq_width,
                                       is_integrated=True,
                                       cutoff=cutoff),
                     center=mp.Vector3(source_center),
                     size=mp.Vector3(0, cell_width, cell_width),
                     component=mp.Ez)]
# Ez-polarized planewave pulse (its size parameter fills the entire cell in 2d)

until_after_sources = time_factor_cell * cell_width
# Enough time for the pulse to pass through all the cell
# Originally: Aprox 3 periods of lowest frequency, using T=λ/c=λ in Meep units 
# Now: Aprox 3 periods of highest frequency, using T=λ/c=λ in Meep units 

geometry = [mp.Sphere(material=medium,
                      center=mp.Vector3(),
                      radius=r)]
# Au sphere with frequency-dependant characteristics imported from Meep.
# Water sourrounding it will be added by using default_material.

#%% FIRST RUN: SET UP

sim = mp.Simulation(resolution=resolution,
                    cell_size=cell_size,
                    boundary_layers=pml_layers,
                    sources=sources,
                    k_point=mp.Vector3(),
                    default_material=mp.Medium(index=submerged_index))

# Scattered power --> Computed by surrounding it with closed DFT flux box 
box_x1 = sim.add_flux(freq_center, freq_width, nfreq, 
                      mp.FluxRegion(center=mp.Vector3(x=-r),
                                    size=mp.Vector3(0,2*r,2*r)))
box_x2 = sim.add_flux(freq_center, freq_width, nfreq, 
                      mp.FluxRegion(center=mp.Vector3(x=+r),
                                    size=mp.Vector3(0,2*r,2*r)))
box_y1 = sim.add_flux(freq_center, freq_width, nfreq,
                      mp.FluxRegion(center=mp.Vector3(y=-r),
                                    size=mp.Vector3(2*r,0,2*r)))
box_y2 = sim.add_flux(freq_center, freq_width, nfreq,
                      mp.FluxRegion(center=mp.Vector3(y=+r),
                                    size=mp.Vector3(2*r,0,2*r)))
box_z1 = sim.add_flux(freq_center, freq_width, nfreq,
                      mp.FluxRegion(center=mp.Vector3(z=-r),
                                    size=mp.Vector3(2*r,2*r,0)))
box_z2 = sim.add_flux(freq_center, freq_width, nfreq,
                      mp.FluxRegion(center=mp.Vector3(z=+r),
                                    size=mp.Vector3(2*r,2*r,0)))
# Funny you can encase the sphere (r radius) so closely (2r-sided box)

#%% FIRST RUN: INITIALIZE

sim.init_sim()

#%% FIRST RUN: SIMULATION

temp = time()
sim.run(until_after_sources=until_after_sources)

freqs = np.asarray(mp.get_flux_freqs(box_x1))
box_x1_data = sim.get_flux_data(box_x1)
box_x2_data = sim.get_flux_data(box_x2)
box_y1_data = sim.get_flux_data(box_y1)
box_y2_data = sim.get_flux_data(box_y2)
box_z1_data = sim.get_flux_data(box_z1)
box_z2_data = sim.get_flux_data(box_z2)

box_x1_flux0 = np.asarray(mp.get_fluxes(box_x1))

sim.reset_meep()

#%% SECOND RUN: SETUP

sim = mp.Simulation(resolution=resolution,
                    cell_size=cell_size,
                    boundary_layers=pml_layers,
                    sources=sources,
                    k_point=mp.Vector3(),
                    default_material=mp.Medium(index=submerged_index),
                    geometry=geometry)

box_x1 = sim.add_flux(freq_center, freq_width, nfreq, 
                      mp.FluxRegion(center=mp.Vector3(x=-r),
                                    size=mp.Vector3(0,2*r,2*r)))
box_x2 = sim.add_flux(freq_center, freq_width, nfreq, 
                      mp.FluxRegion(center=mp.Vector3(x=+r),
                                    size=mp.Vector3(0,2*r,2*r)))
box_y1 = sim.add_flux(freq_center, freq_width, nfreq, 
                      mp.FluxRegion(center=mp.Vector3(y=-r),
                                    size=mp.Vector3(2*r,0,2*r)))
box_y2 = sim.add_flux(freq_center, freq_width, nfreq, 
                      mp.FluxRegion(center=mp.Vector3(y=+r),
                                    size=mp.Vector3(2*r,0,2*r)))
box_z1 = sim.add_flux(freq_center, freq_width, nfreq, 
                      mp.FluxRegion(center=mp.Vector3(z=-r),
                                    size=mp.Vector3(2*r,2*r,0)))
box_z2 = sim.add_flux(freq_center, freq_width, nfreq, 
                      mp.FluxRegion(center=mp.Vector3(z=+r),
                                    size=mp.Vector3(2*r,2*r,0)))

#%% SECOND RUN: INITIALIZE

sim.init_sim()

sim.load_minus_flux_data(box_x1, box_x1_data)
sim.load_minus_flux_data(box_x2, box_x2_data)
sim.load_minus_flux_data(box_y1, box_y1_data)
sim.load_minus_flux_data(box_y2, box_y2_data)
sim.load_minus_flux_data(box_z1, box_z1_data)
sim.load_minus_flux_data(box_z2, box_z2_data)

#%% SECOND RUN: SIMULATION

sim.run(until_after_sources=second_time_factor*until_after_sources) 
# Aprox 30 periods of lowest frequency, using T=λ/c=λ in Meep units 

box_x1_flux = np.asarray(mp.get_fluxes(box_x1))
box_x2_flux = np.asarray(mp.get_fluxes(box_x2))
box_y1_flux = np.asarray(mp.get_fluxes(box_y1))
box_y2_flux = np.asarray(mp.get_fluxes(box_y2))
box_z1_flux = np.asarray(mp.get_fluxes(box_z1))
box_z2_flux = np.asarray(mp.get_fluxes(box_z2))

#%% ANALYSIS

scatt_flux = box_x1_flux - box_x2_flux
scatt_flux = scatt_flux + box_y1_flux - box_y2_flux
scatt_flux = scatt_flux + box_z1_flux - box_z2_flux

intensity = box_x1_flux0/(2*r)**2
# Flux of one of the six monitor planes / Área
# (the closest one, facing the planewave source) 

scatt_cross_section = np.divide(scatt_flux, intensity)
# Scattering cross section σ = scattered power in all directions / incident intensity.

scatt_eff_meep = -1 * scatt_cross_section / (np.pi*r**2)
# Scattering efficiency = scattering cross section / cross sectional area of the sphere

What could the problem be here? Is it OK to use water as default_material in both simulations?

One more time, thank you for taking the time to read this! I hope you can help me.

stevengj commented 3 years ago

Are you sure you're using the same material parameters in your Mie calculation? Units and dispersive media are tricky…

Have you checked that the results are converged with resolution? You might need a high resolution to resolve the skin depth of gold. (Metals in FDTD are often a challenge for this reason.)

oskooi commented 2 years ago

Sorry for the long overdue reply. There are several challenging aspects in setting up the Meep simulation to produce results which are accurate enough to agree with Mie scattering theory when lossy materials (such as Au) are involved. This is mainly due to the surface plasmon polaritons on the metal-dielectric interface which is a unique feature not found in the tutorial example which involved only lossless materials. To give just one example, aside from cranking up the grid resolution to resolve the skin depth of Au and increasing the runtime until the surface plasmons have fully decayed away, the size of the closed box of flux monitors must also be enlarged (compared to the original example in the tutorial) such that the monitors are not touching the Au sphere. Generally, the flux monitors need to be positioned sufficiently far from the sphere such that the evanescent tails of the surface plasmons have decayed away. Otherwise, discretization artifacts will be exacerbated which could affect the accuracy of the flux measurements. To demonstrate this effect, here are results for two different runs: with and without "padding" of the flux box around the sphere. A slight padding of the flux box has a large impact on the results particularly at the longer wavelengths (more padding leads to better agreement with the theory). More generally though, the uniform Yee grid of FDTD is not well suited for problems involving a large relative mismatch in the dimensions of the geometry and field oscillations.

mie_scattering_Au_in_H2O

rom meep.materials import Au
import meep as mp
import numpy as np
import matplotlib
matplotlib.use('agg')
import matplotlib.pyplot as plt
import PyMieScatt as ps

n_H2O = 1.333 # refractive index of water                                                                                                                      

r = 1.0  # radius of sphere                                                                                                                                    

wvl_min = 2*np.pi*r/10
wvl_max = 2*np.pi*r/2

frq_min = 1/wvl_max
frq_max = 1/wvl_min
frq_cen = 0.5*(frq_min+frq_max)
dfrq = frq_max-frq_min
nfrq = 100

## needs to be large enough to resolve skin depth of Au sphere                                                                                         
resolution = 80

dpml = 0.5*wvl_max
dair = 0.5*wvl_max

pml_layers = [mp.PML(thickness=dpml)]

s = 2*(dpml+dair+r)
cell_size = mp.Vector3(s,s,s)

# is_integrated=True necessary for any planewave source extending into PML                                                                                     
sources = [mp.Source(mp.GaussianSource(frq_cen,fwidth=dfrq,is_integrated=True),
                     center=mp.Vector3(-0.5*s+dpml),
                     size=mp.Vector3(0,s,s),
                     component=mp.Ez)]

## symmetries sometimes cause field instabilities at certain resolutions                                                                                       
symmetries = [mp.Mirror(mp.Y),
              mp.Mirror(mp.Z,phase=-1)]

sim = mp.Simulation(resolution=resolution,
                    cell_size=cell_size,
                    boundary_layers=pml_layers,
                    sources=sources,
                    k_point=mp.Vector3(),
                    symmetries=symmetries,
                    default_material=mp.Medium(index=n_H2O))

# padding of closed flux box around Au sphere
dpad = 0.5*dair
box_x1 = sim.add_flux(frq_cen, dfrq, nfrq, mp.FluxRegion(center=mp.Vector3(x=-r-dpad),size=mp.Vector3(0,2*(r+dpad),2*(r+dpad))))
box_x2 = sim.add_flux(frq_cen, dfrq, nfrq, mp.FluxRegion(center=mp.Vector3(x=+r+dpad),size=mp.Vector3(0,2*(r+dpad),2*(r+dpad))))
box_y1 = sim.add_flux(frq_cen, dfrq, nfrq, mp.FluxRegion(center=mp.Vector3(y=-r-dpad),size=mp.Vector3(2*(r+dpad),0,2*(r+dpad))))
box_y2 = sim.add_flux(frq_cen, dfrq, nfrq, mp.FluxRegion(center=mp.Vector3(y=+r+dpad),size=mp.Vector3(2*(r+dpad),0,2*(r+dpad))))
box_z1 = sim.add_flux(frq_cen, dfrq, nfrq, mp.FluxRegion(center=mp.Vector3(z=-r-dpad),size=mp.Vector3(2*(r+dpad),2*(r+dpad),0)))
box_z2 = sim.add_flux(frq_cen, dfrq, nfrq, mp.FluxRegion(center=mp.Vector3(z=+r+dpad),size=mp.Vector3(2*(r+dpad),2*(r+dpad),0)))

sim.run(until_after_sources=10)

freqs = mp.get_flux_freqs(box_x1)
box_x1_data = sim.get_flux_data(box_x1)
box_x2_data = sim.get_flux_data(box_x2)
box_y1_data = sim.get_flux_data(box_y1)
box_y2_data = sim.get_flux_data(box_y2)
box_z1_data = sim.get_flux_data(box_z1)
box_z2_data = sim.get_flux_data(box_z2)

box_x1_flux0 = mp.get_fluxes(box_x1)

sim.reset_meep()

geometry = [mp.Sphere(material=Au,
                      center=mp.Vector3(),
                      radius=r)]

sim = mp.Simulation(resolution=resolution,
                    cell_size=cell_size,
                    boundary_layers=pml_layers,
                    sources=sources,
                    k_point=mp.Vector3(),
                    symmetries=symmetries,
                    default_material=mp.Medium(index=n_H2O),
                    eps_averaging=False,
                    geometry=geometry)

box_x1 = sim.add_flux(frq_cen, dfrq, nfrq, mp.FluxRegion(center=mp.Vector3(x=-r-dpad),size=mp.Vector3(0,2*(r+dpad),2*(r+dpad))))
box_x2 = sim.add_flux(frq_cen, dfrq, nfrq, mp.FluxRegion(center=mp.Vector3(x=+r+dpad),size=mp.Vector3(0,2*(r+dpad),2*(r+dpad))))
box_y1 = sim.add_flux(frq_cen, dfrq, nfrq, mp.FluxRegion(center=mp.Vector3(y=-r-dpad),size=mp.Vector3(2*(r+dpad),0,2*(r+dpad))))
box_y2 = sim.add_flux(frq_cen, dfrq, nfrq, mp.FluxRegion(center=mp.Vector3(y=+r+dpad),size=mp.Vector3(2*(r+dpad),0,2*(r+dpad))))
box_z1 = sim.add_flux(frq_cen, dfrq, nfrq, mp.FluxRegion(center=mp.Vector3(z=-r-dpad),size=mp.Vector3(2*(r+dpad),2*(r+dpad),0)))
box_z2 = sim.add_flux(frq_cen, dfrq, nfrq, mp.FluxRegion(center=mp.Vector3(z=+r+dpad),size=mp.Vector3(2*(r+dpad),2*(r+dpad),0)))

sim.load_minus_flux_data(box_x1, box_x1_data)
sim.load_minus_flux_data(box_x2, box_x2_data)
sim.load_minus_flux_data(box_y1, box_y1_data)
sim.load_minus_flux_data(box_y2, box_y2_data)
sim.load_minus_flux_data(box_z1, box_z1_data)
sim.load_minus_flux_data(box_z2, box_z2_data)

sim.run(until_after_sources=100)

box_x1_flux = mp.get_fluxes(box_x1)
box_x2_flux = mp.get_fluxes(box_x2)
box_y1_flux = mp.get_fluxes(box_y1)
box_y2_flux = mp.get_fluxes(box_y2)
box_z1_flux = mp.get_fluxes(box_z1)
box_z2_flux = mp.get_fluxes(box_z2)

scatt_flux = np.asarray(box_x1_flux)-np.asarray(box_x2_flux)+np.asarray(box_y1_flux)-np.asarray(box_y2_flux)+np.asarray(box_z1_flux)-np.asarray(box_z2_flux)
intensity = np.asarray(box_x1_flux0)/(2*r)**2
scatt_cross_section = np.divide(scatt_flux,intensity)
scatt_eff_meep = scatt_cross_section*-1/(np.pi*r**2)

trace = lambda t: (t[0][0]+t[1][1]+t[2][2])/3
scatt_eff_theory = [ps.MieQ(np.sqrt(trace(Au.epsilon(f))),1000/f,2*r*1000,nMedium=n_H2O,asDict=True)['Qsca'] for f in freqs]

if mp.am_master():
    plt.figure(dpi=150)
    plt.loglog(2*np.pi*r*np.asarray(freqs),scatt_eff_meep,'bo-',label='Meep')
    plt.loglog(2*np.pi*r*np.asarray(freqs),scatt_eff_theory,'ro-',label='theory')
    plt.grid(True,which="both",ls="-")
    plt.xlabel('(sphere circumference)/wavelength, 2πr/λ')
    plt.ylabel('scattering efficiency, σ/πr$^{2}$')
    plt.legend(loc='upper left')
    plt.title('Mie Scattering of a Au Sphere in H$_2$O')
    plt.tight_layout()
    plt.savefig("mie_scattering_Au_in_H2O.png")
    np.savez('scatt_eff_res{}.npz'.format(resolution),scatt_eff_meep=scatt_eff_meep)
WeiKuoLi commented 7 months ago

Hello Sir thank you for your nice and detailed explanation. However I want to point out that I think in your script: intensity = np.asarray(box_x1_flux0)/(2*r)**2

should be intensity = np.asarray(box_x1_flux0)/(2*(r+dpad))**2

Since the box is now bigger, so is the area of box_x1.

Cheers