```python

```

## **Broadband scattering suppression using 3D nano-assembled plasmonic meta-structure. [Plasmonic Cloaking](https://www.osapublishing.org/oe/fulltext.cfm?uri=oe-28-15-22732&id=433552)**

# Md Imran Khan
---
Graduate Researcher .
Department of Physics. University of California. Merced

---

```python
import  time
print( 'Last updated: %s' %time.strftime('%d/%m/%Y') )
```

    Last updated: 04/03/2021

```python
# defaults for the codes below

import numpy as np
import matplotlib.pyplot as plt

```

# Introduction:
The concept of “cloaking” an object is a very attractive one, especially in the visible (VIS) and near infra-red (NIR) regions of the electromagnetic spectrum, as that would reduce the visibility of an object to the eye. One possible route to achieving this goal is by leveraging the plasmonic property of metallic nanoparticles (NPs). We model and simulate light in the VIS and NIR scattered by a core of a homogeneous medium, covered by plasmonic cloak that is a spherical shell composed of gold nanoparticles (AuNPs). To consider realistic, scalable, and robust plasmonic cloaks that are comparable, or larger, in size to the wavelength, we introduce a multiscale simulation platform. This model uses the multiple scattering theory of Foldy and Lax to model interactions of light with AuNPs combined with the method of fundamental solutions to model interactions with the core. Numerical results of our simulations for the scattering cross-sections of core-shell composite indicate significant scattering suppression of up to 50% over a substantial portion of the desired spectral range (400 - 600 nm) for cores as large as 900 nm in diameter by a suitable combination of AuNP sizes and filling fractions of AuNPs in the shell.

# Computational methods:
This section briefly states the computational procedure for modeling scattering cross-section of a plasmonic meta-structures .

* Interaction of the monochromatic light with the dielectric core was computed using Methods of Fundamentals of Solutions(MFS).
* Multiple scattering and absorption by the nanoparticles in the shell and the core was computed using generalized Foldy-Lax scattering theory following a modification.
* Total scattered power by the meta-strucutre was computed using the Optical Theorem $\sigma_{t}=\frac{4\pi}{k_{0}}Im[f(\hat{o},\hat{i})]$

#Modeling Assumptions:

* Metallic nanopparticles in the shell are assumed as point scatterer.
* A point scatterer has isotropic scattering pattern.
* Extinction properties of a point scatterer was attributed to a parameter, $\alpha$, known as strength parameter as well. $\alpha = [\frac{\sigma_{s}}{4\pi}-(\frac{k_{0}\sigma_{t}}{4\pi})]^{1/2} +\frac{ik_{0}\sigma_{t}}{4\pi}$
*$\sigma_{t} = \sigma_{s}+\sigma_{a}$, total cross-section of a point scatterer.
* The scattering ($\sigma_{s}$) and the absorption ($\sigma_{a}$) cross-sections were computed using Rayleigh Scatterig theory.

```python

```

## A. Problem statement and solution method

## 1 Problem description

A plane wave with wavelength $\lambda$ propagates in the $+\hat{z}$ direction and is incident on a
structure consisting of a dielectric sphere with radius $a_{0}$ surrounded
by $N$ small, metal nanoparticles. The metal nanoparticles may be
situated at subwavelength distances away from the surface of the
dielectric sphere. Hence, the field scattered by this structure is the
result of strong multiple scattering interactions between each and all
of the components of the structure. In what follows, we describe a
method to compute the field scattered by this structure. For
simplicity, we describe the problem for scalar waves. In particular, we use the method of fundamental solutions
to compute scattering by the dielectric sphere and the Foldy-Lax equations to model the multiple scattering by
the point particles that surround the sphere.

Suppose that the origin of a coordinate system lies at the center of
the dielectric sphere. We denote the interior domain by
$D = \{ r < a \}$ and the exterior domain by $E = \{ r > a \}$. The
spherical surface $B = \{ r = a \}$ is interface between the interior
and exterior. The wave field in the interior is denoted by
$\psi^{\text{int}}$ and satisfies the homogeneous reduced wave or
Helmholtz equation,
\begin{equation}
  \left( \nabla^{2} + k_{1}^{2} \right) \psi^{\text{int}}
  = 0, \quad \text{in $D$},
  \label{eq:interiorDE}
\end{equation}
with $k_{1}$ denoting the wavenumber for the dielectric sphere. The
wave field in the exterior is denoted by $\psi^{\text{ext}}$ and
satisfies
\begin{equation}
  \left( \nabla^{2} + k_{0}^{2} \right) \psi^{\text{ext}}
  = -k_{0}^{2} \sum_{n = 1}^{N} V_{n} \psi^{\text{ext}}, \quad
  \text{in $E$}.
  \label{eq:exteriorDE}
\end{equation}
Here, $k_{0}$ is the wavenumber for the exterior and $V_{n}$
corresponds to the scattering potential for the $n$th metal
nanoparticle. We write $\psi^{\text{ext}}$ as the sum
\begin{equation}
  \psi^{\text{ext}} = \psi^{\text{inc}} + \psi^{s}
\end{equation}
with $\psi^{\text{inc}}$ denoting the incident field satisfying
\begin{equation}
  \left( \nabla^{2} + k_{0}^{2} \right) \psi^{\text{inc}}
  = 0, \quad \text{in $R^{3}$},
  \label{eq:incidentDE}
\end{equation}
and $\psi^{\text{s}}$ denoting the scattered field satisfying
\begin{equation}
  \left( \nabla^{2} + k_{0}^{2} \right) \psi^{\text{s}}
  = -k_{0}^{2} \sum_{n = 1}^{N} V_{n} \psi^{\text{inc}} - k_{0}^{2}
  \sum_{n = 1}^{N} V_{n} \psi^{\text{s}}, \quad \text{in $E$}.
  \label{eq:scatteredDE}
\end{equation}

We must supplement the equations above with conditions on $B$ as well
as radiation conditions. We prescribe that
\begin{gather}
  \psi^{\text{int}} = \psi^{\text{inc}} + \psi^{\text{s}} \quad
  \text{on $B$},
  \label{eq:BC1}
\end{gather}
and
\begin{gather}
  \partial_{\nu} \psi^{\text{int}} = \partial_{\nu} \psi^{\text{inc}}
  + \partial_{\nu} \psi^{\text{s}} \quad \text{on $B$},
  \label{eq:BC2}
\end{gather}
with $\partial_{\nu}$ denoting the derivative along the normal on $B$
pointing into $E$. Additionally, we require that $\psi^{s}$ satisfies
the Sommerfeld radiation condition. The differential equations supplemented by the interface conditions,
and the requirement that $\psi^{\text{s}}$ satisfies the Sommerfeld radiation condition
constitute a complete mathematical description of the problem.

## 2 Interior field

To compute the interior field, we make use of the Method of
Fundamental Solutions (MFS) which is also known as the Discrete Source
Method. This method relies on knowing Green's function, denoted by
$G_{1}$, which satisfies
\begin{equation}
  \left( \nabla^{2} + k_{1}^{2} \right) G_{1} = -\delta(r - r'),
\end{equation}
and the Sommerfeld radiation condition. It is given explicitly by
\begin{equation}
  G_{1}(r - r') = \frac{e^{\mathrm{i} k_{1} | r - r '|}}{4 \pi | r -
    r' |},
\end{equation}
which is a spherical wave centered at $r'$ propagating in the whole
space with wavenumber $k_{1}$.

The key observation is that when $r' \not\in D$, $G_{1}$ exactly
satisfies the differential equation for $\psi^{\text{int}}$ since $\delta(r - r') = 0$ for
$r \in D$ and $r' \not\in D$. Because of this, we consider a set of
$M$ points $r_{j}^{\text{int}} \in E$ for $j = 1, \cdots, M$ and form
the following approximation,
\begin{equation}
  \psi^{\text{int}}(r) \cong \sum_{j = 1}^{M} c^{\text{int}}_{j} G_{1}(r -
  \rho^{\text{int}}_{j}), \quad r \in D.
\end{equation}
This approximation is an *exact solution* of
the differential equation for $\psi^{\text{int}}$ for any set of points $\rho_{j}^{\text{int}} \in E$
for $j = 1, \cdots, M$. It is an approximation of the solution of this scattering problem because it does
not exactly satisfy the boundary conditions. Rather, we will require that this approximation satisfies the
boundary conditions only at a set of $M$ points, which is called a collocation method.

In practice, we would like to have these
points uniformly sample the interface $B$ to make the approximation of
the interface conditions as accurate as possible. Suppose the set of
$M$ points $\rho^{\text{bdy}}_{j} \in B$ for $j = 1, \cdots, B$ uniformly sample (or
close to uniformly sample) the sphere of radius $a_{0}$. Then, we set
\begin{equation}
  \rho^{\text{int}}_{j} = \rho^{\text{bdy}}_{j} + \ell \nu_{j}, \quad j
  = 1, \cdots, M,
\end{equation}
with $\nu_{j}$ denoting the unit outward normal at $r_{j}$, and
$\ell > 0$ denoting a user-defined length parameter which we typically
set to be smaller than a wavelength.

## 3 Exterior field

To approximate $\psi^{\text{ext}}$ we make use of Green's identity
which essentially states that the exterior field is given by a
superposition of surface and volume sources. Here, surface sources
correspond to scattering by the dielectric sphere and volume sources
correspond to the $N$ metal nanoparticles. In what follows, we
approximate the surface sources using the Method of Fundamental
Solutions and the volume sources using the self-consistent Foldy-Lax
scattering theory, which is a scalar wave theory closely related to
what is used in the Discrete Dipole Approximation.

In what follows, we represent $\psi^{\text{ext}}$ as the following
sum,
\begin{equation}
  \psi^{\text{ext}} = \psi^{\text{inc}} + \underbrace{\psi^{B} + \sum_{n =
      1}^{N} \Psi_{n}}_{\psi^{\text{s}}},
\end{equation}
with $\psi^{B}$ denoting the "surface source" field scattered by the
dielectric sphere and $\Psi_{n}$ denoting the "volume source" field
scattered by the $n$th metal nanoparticle. We describe our
approximations for each of these scattered fields below.

### 3.1 Field scattered by the dielectric sphere

We use the Method of Fundamental solutions to compute the field
scattered by the dielectric sphere. Here, we use the Green's function
$G_{0}$ satisfying
\begin{equation}
  \left( \nabla^{2} + k_{0}^{2} \right) G_{0} = -\delta(r - r'),
\end{equation}
and the Sommerfeld radiation condition, which is given explicitly by
\begin{equation}
  G_{0}(r - r') = \frac{e^{\mathrm{i} k_{0} | r - r' |}}{4 \pi | r -
    r' |}.
  \label{eq:exteriorGreens}
\end{equation}
Just as we have done for the interior field, we introduce the set of
$M$ points,
\begin{equation}
  \rho_{j}^{\text{ext}} = \rho^{\text{bdy}}_{j} - \ell \nu_{j}, \quad j = 1, \cdots, M.
\end{equation}
Note that because $\ell > 0$, it follows that
$\rho_{j}^{\text{ext}} \not\in E$. Consequently, the approximation given
by
\begin{equation}
  \psi^{B}(r) \cong \sum_{j = 1}^{M} c_{j}^{\text{ext}} G_{0}(r -
  \rho_{j}^{\text{ext}})
  \label{eq:exteriorMFS}
\end{equation}
is an exact solution of
\begin{equation}
  \left( \nabla^{2} + k_{0}^{2} \right) \psi^{B} = 0, \quad r \in E.
\end{equation}
We use the approximation given above to approximate the field scattered by
the dielectric sphere. Because this approximation is a
superposition of spherical waves, each of which satisfies the
Sommerfeld radiation condition, this approximation satisfies the
Sommerfeld radiation condition.  Because this approximation
exactly satisfies the equation above and the Sommerfeld radiation
condition, it is only the interface conditions that make
it an approximation rather than an exact
solution.

### 3.2 Field scattered by the metal nanoparticles

Let us denote the center of the $n$th metal nanoparticle by $R_{n}$.
In what follows, we assume that each of the $N$ metal nanoparticles is
small enough that we can account for its scattering properties by a
scattering coefficient, $\alpha_{n}$, which may depend on
$\lambda$. It follows that the field scattered by the $n$th metal
nanoparticle is given by
\begin{equation}
  \Psi_{n}(r) = \alpha_{n} G_{0}(r - R_{n}) \Psi_{E}(R_{n}),
\end{equation}
with $G_{0}$ denoting the Green's function defined above and $\Psi_{E}(R_{n})$ denoting the exciting
field evaluated at $R_{n}$.  Supposing that $R_{n}$ and $\alpha_{n}$
for $n = 1, \cdots, N$ are known, all that remains to be determined is
$\Psi_{E}(R_{n})$ for $n = 1, \cdots, N$.

To determine this exciting field, we use Foldy-Lax theory which we
describe below.  Foldy-Lax theory is a self-consistent multiple
scattering theory. It is not an exact theory, but it does account for
infinitely many interactions between the metal nanoparticles, so it
provides a useful multiple scattering model. This theory is limited to
settings in which the metal nanoparticles are not too densely
distributed over the dielectric sphere so that these scattering
elements are statistically independent.

We model the field exciting the $n$th metal nanoparticle as the
following sum,
\begin{equation}
  \Psi_{E}(R_{n}) = \psi^{\text{inc}}(R_{n}) + \psi^{B}(R_{n}) +
  \sum_{\substack{n' = 1\\ n' \neq n}}^{N} \sigma_{n'} G_{0}(R_{n} - R_{n'})
  \Psi_{E}(R_{n'}).
  \label{eq:exciting-fields}
\end{equation}
This equation gives the exciting field at
$r_{n}$ as the sum of the incident field $\psi^{\text{inc}}$, the
field scattered by the dielectric sphere $\psi^{B}$, and the fields
scattered by all of the other $N-1$ metal nanoparticles evaluated at
$r_{n}$. By rearranging this equation and considering it
for each and all $N$ metal nanoparticles, we obtain the $N \times N$
linear system,
\begin{equation}
  \Psi_{E}(R_{n}) - \sum_{\substack{n' = 1\\ n' \neq n}}^{N}
  \sigma_{n'} G_{0}(R_{n} - R_{n'}) \Psi_{E}(R_{n'}) =
  \psi^{\text{inc}}(R_{n}) + \psi^{B}(R_{n}), \quad n = 1, \cdots, N.
\end{equation}
Upon solution of this linear system, we obtain $\Psi_{E}(r_{n})$ for
$n = 1, \cdots, N$. This result can then be used to evaluate
\begin{equation}
  \Psi(r) = \sum_{n = 1}^{N} \alpha_{n} G_{0}(r - R_{n})
  \Psi_{E}(R_{n}), \quad r \in E.
\end{equation}
This field gives the field scattered by the entire set of $N$ metal
nanoparticles. Because this field is given as a superposition of
Green's functions, each of which satisfies the Sommerfeld radiation
condition, it follows that $\Psi$ satisfies the Sommerfeld radiation
condition.

## 4 Interface conditions

At this point we have introduced approximations for the interior and
exterior fields. Both are exact solutions of the associated
differential equations and the exterior field satisfies the Sommerfeld
radiation condition. All that remains is to require that these fields
satisfy interface conditions given in Section 1.

Let us review what we have so far. The approximation of the interior
field is given by
\begin{equation}
  \psi^{\text{int}}(r) \cong \sum_{j = 0}^{M} c_{j}^{\text{int}} G_{1}(r -
  \rho_{j}^{\text{int}}) \quad r \in D.
\end{equation}
The approximation of the exterior field is given by
\begin{equation}
  \psi^{\text{ext}}(r) \cong \psi^{\text{inc}}(r) +
  \underbrace{\sum_{j = 1}^{M} c_{j}^{\text{ext}} G_{0}(r -
    \rho_{j}^{\text{ext}})}_{\psi^{B}} + \sum_{n = 1}^{N} \alpha_{n}
  G_{0}(r - R_{n}) \Psi_{E}(R_{n}), \quad r \in E,
\end{equation}
with $\Psi_{E}(R_{n})$ satisfying
\begin{equation}
  \Psi_{E}(R_{n}) - \sum_{\substack{n' = 1\\ n' \neq n}}^{N}
  \sigma_{n'} G_{0}(R_{n} - R_{n'}) \Psi_{E}(R_{n'}) =
  \psi^{\text{inc}}(R_{n}) + \sum_{j = 1}^{M} c_{j}^{\text{ext}}
  G_{0}(R_{n} - \rho_{j}^{\text{ext}}), \quad n = 1, \cdots, N.
\end{equation}
The quantities $c_{j}^{\text{int}}$ for $j = 1, \cdots, N$,
$c_{j}^{\text{ext}}$ for $j = 1, \cdots, N$, and $\Psi_{E}(R_{n})$ for
$n = 1, \cdots, N$ are to be determined.

By requiring that the interface condition for the fields is satified
exactly on the $M$ boundary points, $\rho^{\text{bdy}}_{i} \in B$ for
$i = 1, \cdots, M$, we find that
\begin{equation}
  \sum_{j = 0}^{M} c_{j}^{\text{int}} G_{1}(\rho^{\text{bdy}}_{i} - \rho_{j}^{\text{int}}) -
  \sum_{j = 1}^{M} c_{j}^{\text{ext}} G_{0}(\rho^{\text{bdy}}_{i} -
  \rho_{j}^{\text{ext}}) - \sum_{n = 1}^{N} \alpha_{n} G_{0}(\rho^{\text{bdy}}_{i} -
  R_{n}) \Psi_{E}(R_{n}) = \psi^{\text{inc}}(\rho^{\text{bdy}}_{i}), \quad i = 1,
  \cdots, M.
\end{equation}
Similarly, by requiring that the interface condition for the normal derivatives of the fields is
satisfied exactly on the $M$ boundary points, we find that
\begin{equation}
  \sum_{j = 0}^{M} c_{j}^{\text{int}} \partial_{\nu} G_{1}(\rho^{\text{bdy}}_{i} -
  \rho_{j}^{\text{int}}) - \sum_{j = 1}^{M} c_{j}^{\text{ext}}
  \partial_{\nu} G_{0}(\rho^{\text{bdy}}_{i} - \rho_{j}^{\text{ext}}) - \sum_{n = 1}^{N}
  \alpha_{n} \partial_{\nu} G_{0}(\rho^{\text{bdy}}_{i} - R_{n}) \Psi_{E}(R_{n}) =
  \partial_{\nu} \psi^{\text{inc}}(\rho^{\text{bdy}}_{i}), \quad i = 1, \cdots, M.
\end{equation}
Finally, by rearranging terms in this equation, we find that
\begin{equation}
  \Psi_{E}(R_{n}) - \sum_{\substack{n' = 1\\ n' \neq n}}^{N}
  \alpha_{n'} G_{0}(R_{n} - R_{n'}) \Psi_{E}(R_{n'}) - \sum_{j = 1}^{M}
  c_{j}^{\text{ext}} G_{0}(R_{n} - \rho_{j}^{\text{ext}})=
  \psi^{\text{inc}}(R_{n}) , \quad n = 1, \cdots, N.
\end{equation}
These three equations give $2MN$ equations for the $2MN$ unknowns,
$c_{j}^{\text{int}}$ for $j = 1, \cdots, M$, $c_{j}^{\text{ext}}$ for
$j = 1, \cdots, M$, and $\Psi_{E}(r_{n})$ for $n = 1, \cdots,
N$. These equations can be combined to form a $2MN \times 2MN$
linear system that can be solves using standard numerical linear
algebra methods.

We now present the codes that computes the solution of this linear system and uses that result to compute
the field scattered by this cloaking structure.

## A. Compute the points needed for the method of fundamental solutions
Here, we compute the points on  $ρ^{bdy}_{i}∈B$  and the corresponding points  $ρ^{ext}_{i}∈ D$  and  $ρ^{int}_{i}∈E$  for  $i=1,⋯,M $ that will be used in the method of fundamental solutions. In particular, we compute the  $M$  Fibonnaci lattice on the unit sphere, which gives our unit normals,  $ν_{i}$  for  $i=1,⋯,M$ . We set  $ρ^{bdy}_{i}= a_{0}ν_{i}$  for  $i=1,⋯,M$  to set the points on the sphere of radius  $a_{0}$ . To compute  $ρ^{ext}_{i}\in D$  for  $i=1,⋯,M$ , we set  $ρ^{ext}_{i}=(a_{0}−ℓ)ν_{i}$ . Similarly, to compute  $ρ^{int}_{i}\in E$  for  $i=1,⋯,M$ , we set  $ρinti=(a0+ℓ)νi$ . The distance into  $D$  and  $E$ is set by the parameter  $ℓ$  in the code below. This function requires the sphere radius,  $a_{0}$ , and the number of Fibonnaci lattice points,  $M$ .

```python
def ComputeMFSPoints( a0, M ):
    """
    This function computes the set of points needed for the method of fundamental solutions (MFS).
    In particular, given the radius of a sphere, a0, and the number of points M, this function computes
    the Fibonnaci lattice on the unit sphere and stores them as the unit normal vectors ν. Using ν, we
    then compute ρ_bdy = a0 * ν, ρ_int = ( a0 + ℓ ) * ν, and ρ_ext = ( a0 - ℓ ) * ν.

    This function outputs four vectors: ν, ρ_bdy, ρ_int, and ρ_ext.
    """

    # allocate memory for the Fibonacci lattice points on the unit sphere

    ν = np.full( ( M, 3 ), float( 'nan' ) )

    # compute the "golden angle"

    golden_angle = np.pi * ( 3 - np.sqrt( 5 ) )

    # compute the points on the unit sphere

    ν[:,2] = ( 1 - 1 / M ) * ( 1 - 2 * np.arange( 0, M ) / ( M - 1 ) )

    ρ = np.sqrt( 1 - ν[:,2] ** 2 )
    θ = golden_angle * np.arange( 0, M )

    ν[:,0] = ρ * np.cos( θ )
    ν[:,1] = ρ * np.sin( θ )

    # compute the boundary points, interior points, and exterior points

    ℓ = 0.25 * a0

    ρ_bdy = a0 * ν
    ρ_int = ( a0 + ℓ ) * ν
    ρ_ext = ( a0 - ℓ ) * ν

    return ν, ρ_bdy, ρ_int, ρ_ext;
```

## B. Compute the positions of the point scatterers(plasmonic nanoparticles)

*   Compute the scattering and absorption cross-section for an individual meta nanoparticle(NP) using the experimentally measure optical properties for gold or silver. With those data, we compute the cross-section for the metal NPs using Rayleigh scattering. Those cross-section are then used to compute $\alpha$ through evaluation of $\alpha = [\frac{\sigma_{s}}{4\pi}-(\frac{k_{0}\sigma_{t}}{4\pi})]^{1/2} +\frac{ik_{0}\sigma_{t}}{4\pi}$

*  Specify the filling fraction $f$ which gives the volume of all $N$ metal NPs divided by the volume of the shell. Using that, we compute:
$$N=\lfloor f \frac{\text{volume of shell}}{\text{volume of metal NP}}\rfloor$$
with $\lfloor x \rfloor$ denoting the floor function giving the largest integre that is less than or equal to $x$.

*   Compute the position of the metal $NPs$, $r_{n}^{NP}$ for $n = 1,....,N$. For each metal NP. we compute three, pseudo-random numbers: $r$, $\theta$, and $\phi$. The radial length $r$ is sampled uniformly over $R_{c}\leq r \leq R_{s}$ with $R_{C} = d/2$ and $R_{s}$ denoting the outer radius of the shell set to be three times over $0 \leq \theta \leq \pi$, and the azimuthal angle $\phi$ is uniformly sampled over $0\leq \phi \leq 2\pi$. The coordinates for the center of the metal NP are $x = r \sin\theta \cos\phi$, $y = r\sin\theta \sin\phi$, and $z=r\cos\theta$. Thet algorithm checks position of subsequent NPs for overlap and vacancy with other metal NPs. If the algoright detects and overlap, that computed coordinate is descarded and another coordinate is computed. This process of generating non-overlapping coordiunate is continued until it finishes generating N positions.

```python
def Coordinates(CoreDia, NPDia, Vff):

  #this part of the code takes user input to initiate computation
  #print('Insert Silica core diameter')
  #CoreDia = np.double(input())
  #print('Insert nanoparticle diameter')
  #NPDia = np.double(input())
  #print('Inser ligand length')
    LigLen = 0.9800
  #print('Insert volume filling fraction')
  #Vff = np.double(input())
    ShellThickness = NPDia * 3
  # Physical space taken by a satelite sphere
  # Physical space taken by a satelite sphere

    R = (NPDia / 2 + LigLen)

    SmallSphereRadius = R
    ShellOuterRad = CoreDia /2  + ShellThickness
    ShellInnerRadius = CoreDia/2

    #Vs = (4/3)* np.pi * (R ** 3)
  #Floor returns rounding to nearest highest value of the floating number
    ND = np.floor((ShellOuterRad ** 3 - ShellInnerRadius**3) / (R**3) * Vff)
    N = np.int(ND) # Number of nanoparticles in integre
  #This part of the code calcuates random distributions of the coordinates of the
  #Allocating memory for coordinates
    R_N = np.full((N,3), float('nan'))
  # Allocating memory for specific nanoparticles coordinates
    Xcoord = np.full((N,1), float('nan'))
    Ycoord = np.full((N,1), float('nan'))
    Zcoord = np.full((N,1), float('nan'))
    for i in range(N):
        t = 2 * np.pi * np.random.rand() #Elevation angle
        p = np.arccos(2* np.random.rand()-1) # Azimuthal Angle
        dr = ShellInnerRadius +(np.random.rand())**(1/3) * (ShellOuterRad - ShellInnerRadius) # Distance along z
        Xcoord[i] = dr * np.cos(t) * np.sin(p)
        Ycoord[i] = dr * np.sin(t) * np.sin(p)
        Zcoord[i] = dr * np.cos(p)
        I = []
        # finding vacancies and checking for overlap
        NoLap = 0 # Seting overlap flag
        #m = 0;
        #Compute distances between the naoparticles
        if i > 1 :

            dist = np.sqrt((Xcoord - Xcoord[i])**2 + (Ycoord - Ycoord[i])**2 + (Zcoord - Zcoord[i])**2)
           #looking for the overlap
            idx = [idx for idx in range(i)]
            I = np.where(dist[idx] < SmallSphereRadius)
            I = np.asanyarray(I)
            if I.size == 0:
                NoLap = 0 #No overlap
            else:
                NoLap = 1 # Overlap Present
        if NoLap == 1:
            while NoLap == 1:

                t = 2 * np.pi * np.random.rand() #Elevation angle
                p = np.arccos(2* np.random.rand()-1) # Azimuthal Angle
                dr = ShellInnerRadius +(np.random.rand())**(1/3) * (ShellOuterRad - ShellInnerRadius )
                Xcoord[i] = dr * np.cos(t) * np.sin(p)
                Ycoord[i] = dr * np.sin(t) * np.sin(p)
                Zcoord[i] = dr * np.cos(p)
                dist = np.sqrt((Xcoord - Xcoord[i])**2 + (Ycoord - Ycoord[i])**2 + (Zcoord - Zcoord[i])**2)
                idx = [idx for idx in range(i)]
                I = np.where(dist[idx] < SmallSphereRadius)
                I = np.asanyarray(I)
                if I.size == 0:
                    NoLap = 0
                else:
                    NoLap = 1

        X_coords = Xcoord
        Y_coords = Ycoord
        Z_coords = Zcoord

    R_N[:,0:1] = X_coords
    R_N[:,1:2] = Y_coords
    R_N[:,2:3] = Z_coords

    return R_N;

def RayleighAlpha(R, WL, n, K):
      #refractive_index_data = ('gold_11nm_silica_film.csv') # loads .csv data
      #RawData = open(refractive_index_data,'rt') # Data filec processed
      #Data = np.loadtxt(RawData, delimiter=",") # Data now in CSV format
      #WL = Data[:,0] # Wavelength over which the computation takes place
      #n  = Data[:,1] # Refractive index of plasmonic gold nanoparticles
      #K  = Data[:,2] # Extinction of the plasmonic gold nanoparticles
      #nb = 1.0 # Refractive index of the Background medium
      N = np.zeros(1, dtype=complex)
      m = np.zeros(1,dtype=complex)
      K0 = np.zeros(1,dtype=complex)
      sigma_s = np.zeros(1,dtype=complex)
      sigma_abs = np.zeros(1,dtype=complex)
      sigma_t = np.zeros(1,dtype=complex)
      alpha_imag = np.zeros(1,dtype=complex)
      alpha_r = np.zeros(1,dtype=complex)
      alphaN = np.zeros(1,dtype=complex)
      # R Radius of the nanoparticle

      N = np.complex(n,K)
      m = N / nb
      K0 = (2 * np.pi / WL)
      sigma_s = (((2*np.pi**5/3)*((2*R)**6)*(np.absolute((m**2-1)/(m**2+2)))**2) /(WL**4))
      sigma_abs = ((((8*np.pi**2)*(1/WL)*R**3*np.imag((m**2-1)/(m**2+2)))))
      sigma_t = sigma_s+ sigma_abs
      alpha_imag = (K0)*sigma_t
      alpha_r = np.absolute(np.sqrt((4*np.pi*sigma_s) - K0*alpha_imag**2))
      alphaN = (4*np.pi)*(alpha_r + 1j*alpha_imag)

      return alphaN;

```

```python

```

## C. Computing Green's function and its normal derivative
In what follows, we make use of Green's function for wavenumber  $k$  given as
$$G(r,r′)= \frac{e^{ik}|r−r^{′}|}{4π|r−r^{′}|},$$
and its normal derivative defined as
$$∂_{ν}G(r,r^{′})=ν⋅\nabla G(r,r^{′})=\frac{ν⋅(r−r′)}{|r−r′|}(ik−\frac{1}{|r−r^{′}|})\frac{e^{ik}|r−r^{′}|}{4π|r−r′|}.$$
Thus, we first define a function that computes  $G$  and a function that computes  $∂νG$  given the wavenumber,  $k$ , a matrix of  $Rd=|r−r^{′}|$  values, and a matrix of $\cos\theta=ν⋅(r−r^{′})/|r−r^{′}|$ values.

```python
# function to compute Green's function

def ComputeG( k, Rd ):
    """
    This function computes the whole space Green's function given a wavenumber k and a distance Rd.

    The output of this function is the evaluation of the whole space Green's function.
    """

    # compute Green's function

    G = np.exp( 1j * k * Rd ) / ( 4 * np.pi * Rd )

    return G;

# function to compute the normal derivative of Green's function

def ComputeDνG( k, Rd, CosTheta ):
    """
    This function computes the normal derivative of the whole space Green's function
    given a wavenumber k, a distance Rd, and the cosine of the angle made between the
    difference vector and the unit normal.

    The output of this function is the evaluation of the normal derivative of the whole
    space Green's function.
    """

    # compute Green's function

    G = ComputeG( k, Rd )

    # compute the normal derivative of Green's function

    DνG = CosTheta * ( 1j * k - 1 / Rd ) * G

    return DνG;
```

## D. Compute the MFS expansion coefficients and Foldy-Lax exciting fields.
From Section 4, we found that the following equations form the $2MN \times 2MN$ system of equations for the MFS expansion coefficients and Foldy-Lax exciting fields.
\begin{equation}
  \sum_{j = 0}^{M} c_{j}^{\text{int}} G_{1}(\rho^{\text{bdy}}_{i} - \rho_{j}^{\text{int}}) -
  \sum_{j = 1}^{M} c_{j}^{\text{ext}} G_{0}(\rho^{\text{bdy}}_{i} -
  \rho_{j}^{\text{ext}}) - \sum_{n = 1}^{N} \alpha_{n} G_{0}(\rho^{\text{bdy}}_{i} -
  R_{n}) \Psi_{E}(R_{n}) = \psi^{\text{inc}}(\rho^{\text{bdy}}_{i}), \quad i = 1,
  \cdots, M.
\end{equation}
\begin{equation}
  \sum_{j = 0}^{M} c_{j}^{\text{int}} \partial_{\nu} G_{1}(\rho^{\text{bdy}}_{i} -
  \rho_{j}^{\text{int}}) - \sum_{j = 1}^{M} c_{j}^{\text{ext}}
  \partial_{\nu} G_{0}(\rho^{\text{bdy}}_{i} - \rho_{j}^{\text{ext}}) - \sum_{n = 1}^{N}
  \alpha_{n} \partial_{\nu} G_{0}(\rho^{\text{bdy}}_{i} - R_{n}) \Psi_{E}(R_{n}) =
  \partial_{\nu} \psi^{\text{inc}}(\rho^{\text{bdy}}_{i}), \quad i = 1, \cdots, M.
\end{equation}
and
\begin{equation}
  \Psi_{E}(R_{n}) - \sum_{\substack{n' = 1\\ n' \neq n}}^{N}
  \alpha_{n'} G_{0}(R_{n} - R_{n'}) \Psi_{E}(R_{n'}) - \sum_{j = 1}^{M}
  c_{j}^{\text{ext}} G_{0}(R_{n} - \rho_{j}^{\text{ext}})=
  \psi^{\text{inc}}(R_{n}) , \quad n = 1, \cdots, N.
\end{equation}
The following function computes the solution of this linear system.

```python
def ComputeExpansionCoefficientsExcitingFields( k0, k1, a0, ν, ρ_bdy, ρ_int, ρ_ext, α, R_N, M, N ):

    # interior points

    indx, jndx = np.meshgrid( np.arange(0,M), np.arange(0,M) )

    R_int = np.sqrt( ( ( ρ_bdy[indx] - ρ_int[jndx] ) ** 2 ).sum( axis = 2 ) )
    μ_int = np.divide(( ν[indx] * ( ρ_bdy[indx] - ρ_int[jndx] ) ).sum( axis = 2 ), R_int)

    # exterior points

    R_ext = np.sqrt( ( ( ρ_bdy[indx] - ρ_ext[jndx] ) ** 2 ).sum( axis = 2 ) )
    μ_ext = np.divide(( ν[indx] * ( ρ_bdy[indx] - ρ_ext[jndx] ) ).sum( axis = 2 ) , R_ext)

    # Foldy-Lax points

    indx, jndx = np.meshgrid( np.arange(0,M), np.arange(0,N) )

    ρ_bdy2RN = np.sqrt( ( ( ρ_bdy[indx] - R_N[jndx] ) ** 2 ).sum( axis = 2 ) )
    μ_RN     = np.divide(( ν[indx] * ( ρ_bdy[indx] - R_N[jndx] ) ).sum( axis = 2 ), ρ_bdy2RN)
    RN2ρ_ext = np.sqrt( ( ( ρ_ext[indx] - R_N[jndx] ) ** 2 ).sum( axis = 2 ) )

    indx, jndx = np.meshgrid( np.arange(0,N), np.arange(0,N) )

    RN2RN    = np.sqrt( ( ( R_N[indx] - R_N[jndx] ) ** 2 ).sum( axis = 2 ) )

    # compute the incident field and its normal derivative on the M boundary points

    Ψ_inc   = np.exp( 1j * k0 * ρ_bdy[:,2] )
    DνΨ_inc = 1j * k0 * np.multiply( ν[:,2], Ψ_inc )

    # compute the incident field on the N point scatterers

    Ψ_inc4FL = np.exp( 1j * k0 * R_N[:,2] )

    # compute the matrix blocks

    A11 =  ComputeG( k1, R_int )
    A12 = - ComputeG( k0, R_ext )
    A13 = -α * ComputeG( k0, ρ_bdy2RN ).T

    A21 =  ComputeDνG( k1, R_int, μ_int )
    A22 = - ComputeDνG( k0, R_ext, μ_ext )
    A23 = - α * ComputeDνG( k0, ρ_bdy2RN, μ_RN ).T

    A31 = np.zeros( ( N, M ), dtype ='complex' )
    A32 = - ComputeG( k0, RN2ρ_ext )

    A33 = np.zeros( ( N, N ), dtype = 'complex')
    offdiags = np.where( RN2RN != 0 )
    A33[offdiags] = -α * ( np.exp( 1j * k0 * RN2RN[offdiags]  ) / ( 4 * np.pi * RN2RN[offdiags] ) )
    #A33 = -α * np.divide( np.exp( 1j * k0 * RN2RN ), ( 4 * np.pi * RN2RN ) )
    A33 += np.eye( N, dtype = 'complex' )

    # form the linear system of equations

    A = np.block( [ [ A11, A12, A13 ], [ A21, A22, A23 ], [ A31, A32, A33 ] ] )
    b = np.block( [ Ψ_inc, DνΨ_inc, Ψ_inc4FL ] )

    # solve the linear system

    c = np.linalg.solve( A, b )

    # parse the solution

    c_int = c[0:M]
    c_ext = c[M:2*M]
    Ψ_E   = c[2*M:2*M*N]

    return c_int, c_ext, Ψ_E;

```

```python
def ComputeMFSExpansionCoefficients( k0, k1, a0, ν, ρ_bdy, ρ_int, ρ_ext, M ):
    """
    This function solves the 2M x 2M system of equations for the MFS expansion coefficients.

    This code requires the results from ComputeMFSPoints, namely ν, ρ_bdy, ρ_int, and ρ_ext, in
    addition to the two wavenumbers k0 and k1, the sphere radius, a0, and the number of MFS points,
    M.

    The output from this code are the 2 M-vectors, c_int and c_sca, corresponding to the MFS
    expansions for the interior and scattered fields, respectively.
    """

    # interior points

    indx, jndx = np.meshgrid( np.arange(0,M), np.arange(0,M) )

    R_int = np.sqrt( ( ( ρ_bdy[indx] - ρ_int[jndx] ) ** 2 ).sum( axis = 2 ) )
    μ_int = ( ν[indx] * ( ρ_bdy[indx] - ρ_int[jndx] ) ).sum( axis = 2 ) / R_int

    # exterior points

    R_ext = np.sqrt( ( ( ρ_bdy[indx] - ρ_ext[jndx] ) ** 2 ).sum( axis = 2 ) )
    μ_ext = ( ν[indx] * ( ρ_bdy[indx] - ρ_ext[jndx] ) ).sum( axis = 2 ) / R_ext

    # compute the incident field and its normal derivative on the M boundary points

    Ψ_inc   = np.exp( 1j * k0 * ρ_bdy[:,2] )
    DνΨ_inc = 1j * k0 * np.multiply( ν[:,2], Ψ_inc )

    # compute the matrix blocks

    A11 =  ComputeG( k1, R_int )
    A12 = -ComputeG( k0, R_ext )

    A21 =  ComputeDνG( k1, R_int, μ_int )
    A22 = -ComputeDνG( k0, R_ext, μ_ext )

    # form the linear system of equations

    A = np.block( [ [ A11, A12 ], [ A21, A22 ] ] )
    b = np.block( [ Ψ_inc, DνΨ_inc ] )

    # solve the linear system

    c = np.linalg.solve( A, b )

    # parse the solution

    c_int0 = c[0:M]
    c_ext0 = c[M:2*M]

    return c_int0, c_ext0;
```

### E.  Computing the total cross section

The scattered field evaluated at distances far from the scattering
structure behaves like a spherical wave and is given by
\begin{equation}
  \psi^{\text{s}}(r) = f(\hat{o},\hat{\imath}) \frac{e^{\mathrm{i} k_{0}
      R}}{R}, \quad R > a^{2}/\lambda.
\end{equation}
Here, $f(\hat{o},\hat{\imath})$ is the scattering amplitude for the
scattered field in the far field in direction $\hat{o}$ when the
particle is illuminated by a plane wave propagating in direction
$\hat{\imath}$ with unit amplitude. Suppose we evaluate
$\psi^{\text{s}}$ at the point $R \hat{o}$ when $\psi^{\text{inc}}$ is
a plane wave of unit amplitude propagating in direction
$\hat{\imath}$. We compute this scattering amplitude through
evaluation of
\begin{equation}
  f(\hat{o},\hat{\imath}) = R e^{-\mathrm{i} k_{0} R}
  \psi^{\text{s}}(R \hat{o}).
\end{equation}
Substituting our approximation for the scattered field given in Section 5 into this expression, we
obtain
\begin{equation}
  f(\hat{o},\hat{\imath}) \cong \sum_{j = 1}^{M}
  c_{j}^{\text{ext}} R e^{-\mathrm{i} k_{0} R} G_{0}(R \hat{o} -
  \rho_{j}^{\text{ext}}) + \sum_{n = 1}^{N} \alpha_{n} R e^{-\mathrm{i}
    k_{0} R} G_{0}(R \hat{o} - R_{n}) \Psi_{E}(R_{n}).
\end{equation}
Next, we make use of
\begin{equation}
  | R \hat{o} - r' | = \left( R^{2} + |r'|^{2} - 2 R \hat{o} \cdot r'
  \right)^{1/2} \sim R - \hat{o} \cdot r', \quad R \to +\infty,
\end{equation}
to write the far-field Green's function,
\begin{equation}
  G_{0}( R \hat{o} - r' ) \sim e^{-\mathrm{i}
    k_{0} \hat{o} \cdot r'} \frac{e^{\mathrm{i} k_{0} R}}{4 \pi R},
  \quad R \to +\infty.
  \label{eq:farfieldGreens}
\end{equation}
Replacing $G_{0}$ in the scattering amplitude equation by this far-field Green's function, we obtain
\begin{equation}
  f(\hat{o},\hat{\imath}) \cong \frac{1}{4\pi} \sum_{j = 1}^{M}
  c_{j}^{\text{ext}} e^{-\mathrm{i} \hat{o}
    \cdot \rho_{j}^{\text{ext}}} + \frac{1}{4\pi} \sum_{n = 1}^{N}
  \alpha_{n} e^{-\mathrm{i} \hat{o} \cdot R_{n}} \Psi_{E}(R_{n}).
\end{equation}

According to the Optical Theorem or forward scattering theorem, we can
compute the total cross section $\sigma_{t}$ of the scattering
structure through evaluation of
\begin{equation}
  \sigma_{t} = \frac{4\pi}{k_{0}}
  \text{Im}[f(\hat{\imath},\hat{\imath})].
\end{equation}
Hence, we can use our approximation for the scattering amplitude in the Optical Theorem to compute the total scattering cross section.

```python
def ComputeTotalCrossSection( k0, k1, a0, ν, ρ_bdy, ρ_int, ρ_ext, α, R_N, M, N ):
    """
    This function computes the total cross-section by evaluating the Optical Theorem using
    the results from the MFS approximation for the scattered field.
    """

    # compute the MFS expansion coefficients

    c_int, c_ext, Ψ_E = ComputeExpansionCoefficientsExcitingFields( k0, k1, a0, ν, ρ_bdy, ρ_int, ρ_ext, α, R_N, M, N )

    # compute the scattering amplitude

    f1 = (0.25 / np.pi)  * np.exp( -1j * k0 * ρ_ext[:,2] ).T @ c_ext
    f2 = (0.25 / np.pi ) * α * np.exp( -1j * k0 * R_N[:,2] ).T @ Ψ_E

    # compute the scattering cross-section

    σ_t = 4 * np.pi * np.imag( f1 + f2 ) / k0

    return σ_t;

def ComputeTotalCrossSectionMFS( k0, k1, a0, ν, ρ_bdy, ρ_int, ρ_ext, M ):
    """
    This function computes the total cross-section by evaluating the Optical Theorem using
    the results from the MFS approximation for the scattered field.
    """

    # compute the MFS expansion coefficients

    c_int, c_sca = ComputeMFSExpansionCoefficients( k0, k1, a0, ν, ρ_bdy, ρ_int, ρ_ext, M )

    # compute the scattering amplitude

    f = 0.25 / np.pi * np.exp( -1j * k0 * ρ_ext[:,2] ).T @ c_sca

    # compute the scattering cross-section

    σ_t = 4 * np.pi * np.imag( f ) / k0

    return σ_t;
```

```python

```

## 6 Compute and plot the total cross-section over the visible spectrum

We compute the total cross section over the visible spectrum, $380\, nm \le \lambda \le 750\, nm$ for a fixed sphere radius, $a_{0}$, and plot the result.

```python
#Here we upload data file containng refractive index and
#extinction values of the plasmoninc gold nanoparticles and Silica.
#The wavelength dependent optical parameters of plasmoninc gold nano particles.
#The included file (11nm_gold_film_silica.csv)in this project containes those data .

# **--------***-------****

# Activate the commented line below to upload optical parameter files
#gold_Refractive_index = files.upload() # upload refractive index, extinction data for gold NPs and Silica

# **--------***-------****

Refractive_index_data = ('11nm_gold_film_silica.csv') # Data file is avaialble for use
RawData = open(Refractive_index_data,'rt')
Data = np.loadtxt(RawData, delimiter=",") # Processed data file
Wavelength = np.asarray(Data[:,0]) # Wavelength over which the computation takes place
NPRefndx  = np.asarray(Data[:,1]) # Refractive index of plasmonic gold nanoparticles
NPExtinction  = np.asarray(Data[:,2]) # Extinction of the plasmonic gold nanoparticles
Silica_refndx = Data[:,3] # Refractive index of interior or dielectric core
nb = 1.0 # Refractive index of the Background medium

#Core (dielectric) diameter

CoreDia = 690

#Dielectric core radius

a0 = CoreDia/2

#Plasmonic nanoparticles Dia

NPDia = 20

R = NPDia / 2 #radius of plasmonic NP

#Set the filling fraction of NPs in the shell

Vff = 0.10

#Compute the geometric cross-section

σ_g = np.pi * a0 ** 2 # bare core
σ_g_cs = np.pi * (a0+NPDia*3) ** 2 # core-shell

# Set refractive index

k0 = nb # Refractive index exterior or the background

#Set the number of MFS points

M = 512

#Compute the MFS points

ν, ρ_bdy, ρ_int, ρ_ext = ComputeMFSPoints( a0, M )

# Generate the point scatteres position

R_N = Coordinates(CoreDia, NPDia, Vff)

# allocate memory for the total cross-sections

σ_t0 = np.full( ( len(Wavelength), 1 ), 'nan', dtype = 'float' )
σ_s  = np.full( ( len(Wavelength), 1 ), 'nan', dtype = 'float' )

N = len(R_N) # number of nanoparticles in the shell volume

for i in range(len(Wavelength)):
    WL = Wavelength[i]
    n = NPRefndx[i]
    K = NPExtinction[i]
    α = RayleighAlpha(R, WL, n, K)
    k0 = (2*np.pi/WL)*nb
    k1 = (2*np.pi/WL)*Silica_refndx[i]

  #Compute the MFS results for a Bare core and a core-shell plasmonic meta structure

    σ_t0[i] = ComputeTotalCrossSectionMFS( k0, k1, a0, ν, ρ_bdy, ρ_int, ρ_ext, M )
    σ_s[i]  = ComputeTotalCrossSection( k0, k1, a0, ν, ρ_bdy, ρ_int, ρ_ext, α, R_N, M, N )

```

```python
# plot the results

plt.rcParams['figure.figsize'] = [12,8]

plt.plot( Wavelength, σ_t0 / σ_g, Wavelength, σ_s / σ_g , '--'  )
plt.xlabel( r'$\lambda$ (nm)', fontsize = 24 )
plt.ylabel( r'$\sigma_{s}/\sigma_{g}$', fontsize = 24 )
plt.legend( ( 'Bare Core', 'Core-shell Meta structure' ),  fontsize = 24 )
plt.grid()

plt.show()
```

## Result :
The plot above shows scattering efficiency ($Q_{s} = \sigma_{s}/\sigma_{g}$) of a plasmonic core-shell meta-structure (orange, dashed line) and a 690nm silica sphere (blue, solid line). The scattering efficiencies of the silica core show the generic Mie scattering peaks. On the contrary, the core-shell meta-structure showing flattened scattering efficiencies (scattering suppression) in the visible spectrum ($380 nm \leq \lambda \leq 530 nm$). This particular core-shell meta-structure has a 690 nm dielectric core surrounding 20 nm plasmonic gold nanoparticles distributed in the shell volume. The shell thickness set to 60 nm following a general trend observed in the nano-assembly process, three times a constituent nanoparticle diameter.

## Scattered E-filed computation and plot
In this section we are going to compute the scattered E-field by the core-shell meta-structure and plot that filed. This computation process is also going to varify the accuracy of the Methods of the Fundamentals of Solutions (MFS). We plot the scattered field, $\psi^{s}$ on the $xy-$ , $xz-,$ and $yz-$ planes, and over the visible wavelengths. We evaluate the following expression to compute the scattered filed
\begin{equation}
\psi^{s}(r)\cong \sum_{j=1}^{M}c^{s}_{j}G_{0}(r-r^{\text{ext}}_{j}).
\end{equation}

```python
# compute the scattered field on the xy-, xz-, and yz-planes

def ComputeScatteredField( k0, ρ_ext, c_sca, X1, X2 ):

    # compute distances

    Rdiff_xy = np.sqrt( ( X1 - ρ_ext[:,0] ) ** 2 + ( X2 - ρ_ext[:,1] ) ** 2 + ρ_ext[:,2] ** 2 )
    Rdiff_xz = np.sqrt( ( X1 - ρ_ext[:,0] ) ** 2 + ρ_ext[:,1] ** 2 + ( X2 - ρ_ext[:,2] ) ** 2 )
    Rdiff_yz = np.sqrt( ρ_ext[:,0] ** 2 + ( X1 - ρ_ext[:,1] ) ** 2 + ( X2 - ρ_ext[:,2] ) ** 2 )

    # compute the scattered field on the xy-, xz-, and yz-planes

    Uxy = np.exp( 1j * k0 * Rdiff_xy ) / ( 4 * np.pi * Rdiff_xy ) @ c_sca
    Uxz = np.exp( 1j * k0 * Rdiff_xz ) / ( 4 * np.pi * Rdiff_xz ) @ c_sca
    Uyz = np.exp( 1j * k0 * Rdiff_yz ) / ( 4 * np.pi * Rdiff_yz ) @ c_sca

    return Uxy, Uxz, Uyz;

# compute the interior field on the xy-, xz-, and yz-planes

def ComputeInteriorField( k1, ρ_int, c_int, X1, X2 ):

    # compute distances

    Rdiff_xy = np.sqrt( ( X1 - ρ_int[:,0] ) ** 2 + ( X2 - ρ_int[:,1] ) ** 2 + ρ_int[:,2] ** 2 )
    Rdiff_xz = np.sqrt( ( X1 - ρ_int[:,0] ) ** 2 + ρ_int[:,1] ** 2 + ( X2 - ρ_int[:,2] ) ** 2 )
    Rdiff_yz = np.sqrt( ρ_int[:,0] ** 2 + ( X1 - ρ_int[:,1] ) ** 2 + ( X2 - ρ_int[:,2] ) ** 2 )

    # compute the scattered field on the xy-, xz-, and yz-planes

    Uxy = np.exp( 1j * k1 * Rdiff_xy ) / ( 4 * np.pi * Rdiff_xy ) @ c_int
    Uxz = np.exp( 1j * k1 * Rdiff_xz ) / ( 4 * np.pi * Rdiff_xz ) @ c_int
    Uyz = np.exp( 1j * k1 * Rdiff_yz ) / ( 4 * np.pi * Rdiff_yz ) @ c_int

    return Uxy, Uxz, Uyz;
```

```python
from ipywidgets import interact

# set the size of the plots

plt.rcParams['figure.figsize'] = [20,6]

# set the values of λ to display

dict_comp = {"{} nm".format(Wavelength[wavelength]):wavelength for wavelength in range(len(Wavelength)) }

# compute the evaluation mesh

NN = 256

X1plot, X2plot = np.meshgrid( a0 * np.linspace( -10, 10, NN ), a0 * np.linspace( -10, 10, NN ) )

# plot the fields for each wavelength

@interact( wavelength = dict_comp )
def fun( wavelength = 0 ):

    # compute the wavenumbers

    k0 = 2 * np.pi / Wavelength[wavelength] * nb
    k1 = 2 * np.pi / Wavelength[wavelength] * Silica_refndx[wavelength]

    # compute the MFS expansion coefficients

    c_int, c_sca = ComputeMFSExpansionCoefficients( k0, k1, a0, ν, ρ_bdy, ρ_int, ρ_ext, M )

    # allocate memory for Uxy, Uxz, and Uyz

    Uxy = np.full( ( NN, NN ), 'nan', dtype = 'complex' )
    Uxz = np.full( ( NN, NN ), 'nan', dtype = 'complex' )
    Uyz = np.full( ( NN, NN ), 'nan', dtype = 'complex' )

    for i in range( NN ):

        for j in range( NN ):

            # compute the radial distance of the evaluation point

            R = np.sqrt( X1plot[i,j] ** 2 + X2plot[i,j] ** 2 )

            # compute the fields

            if R < a0:

                Uxy[i,j], Uxz[i,j], Uyz[i,j] = ComputeInteriorField( k1, ρ_int, c_int, X1plot[i,j], X2plot[i,j] )

            else:

                Uxy[i,j], Uxz[i,j], Uyz[i,j] = ComputeScatteredField( k0, ρ_ext, c_sca, X1plot[i,j], X2plot[i,j] )

    # plot the fields

    plt.subplot(1,3,1)
    plt.pcolormesh( X1plot / a0, X2plot / a0, np.real( Uxy ), vmin=-1, vmax=1, cmap='cividis'  )
    plt.colorbar()
    plt.gca().set_aspect('equal', adjustable='box')
    plt.draw()
    plt.tight_layout()
    plt.xlabel( 'x', fontsize=18 )
    plt.ylabel( 'y', fontsize=18 )
    plt.title( 'field on the xy-plane', fontsize=20 )

    plt.subplot(1,3,2)
    plt.pcolormesh( X1plot / a0, X2plot / a0, np.real( Uxz ), vmin=-1, vmax=1, cmap='cividis'  )
    plt.colorbar()
    plt.gca().set_aspect('equal', adjustable='box')
    plt.draw()
    plt.tight_layout()
    plt.xlabel( 'x', fontsize=18 )
    plt.ylabel( 'z', fontsize=18 )
    plt.title( 'field on the xz-plane', fontsize=20 )

    plt.subplot(1,3,3)
    plt.pcolormesh( X1plot / a0, X2plot / a0, np.real( Uyz ), vmin=-1, vmax=1, cmap='cividis'  )
    plt.colorbar()
    plt.gca().set_aspect('equal', adjustable='box')
    plt.draw()
    plt.tight_layout()
    plt.xlabel( 'y', fontsize=18 )
    plt.ylabel( 'z', fontsize=18 )
    plt.title( 'field on the yz-plane', fontsize=20 )

    plt.show()
```

    interactive(children=(Dropdown(description='wavelength', options={'380.0 nm': 0, '390.0 nm': 1, '400.0 nm': 2,…

## Conclusion:
We developed a unique and robust model to compute the scattering cross-section of a core-shell plasmonic meta-structure. This model is scalable and fully three-dimensional that incorporates multiple scattering by a spherical dielectric core and plasmonic nanoparticles surrounding it explicitly. This model can account for practically realizable structure sizes larger than previous models were able to consider. This code computes scattering cross-section for a fixed core and nanoparticle size and filling fraction, but it is not limited to applied parameters we showed here. The physics of the multiple scattering and plasmonic interactions with the incident light is discussed in the publication.

```python

```


<p align="center">

</p>

## Data Analytics

### Optimum Decision Making in Python

#### Michael Pyrcz, Associate Professor, University of Texas at Austin

##### [Twitter](https://twitter.com/geostatsguy) | [GitHub](https://github.com/GeostatsGuy) | [Website](http://michaelpyrcz.com) | [GoogleScholar](https://scholar.google.com/citations?user=QVZ20eQAAAAJ&hl=en&oi=ao) | [Book](https://www.amazon.com/Geostatistical-Reservoir-Modeling-Michael-Pyrcz/dp/0199731446) | [YouTube](https://www.youtube.com/channel/UCLqEr-xV-ceHdXXXrTId5ig)  | [LinkedIn](https://www.linkedin.com/in/michael-pyrcz-61a648a1)

Optimum Decision Making in Python

Here's a simple interactive demonstration of optimum decision making in the precense of uncertainty. This should help you get started with using your uncertainty models to make decisions.

#### Optimum Decision Making in the Precense of Uncertainty

Definition: the decision that minimizes the expected loss or maximizes the expected profit

Procedure:

1. Calculate M realizations / reservoir scenarios (the uncertainty model)
2. Establish S, development scenarios (includes all development decisions, could be many)
3. Establish profit metric (accounts for all relevant factors)
4. Calculate profit over all reservoir realization / scenario (m) for each S development scenario (s).

\begin{equation}
P_{s,m}, s=1,\ldots,s, m=1,\ldots,M
\end{equation}

calculate the expected profit for each development scenario

\begin{equation}
E{𝑃_𝑠} =  \frac{1}{\sum_{m=1}^M \lambda_m} \sum_{\ell=1}^L \lambda_m \cdot P_{s,m}
\end{equation}

define the optimal development scenario as the one with highest expected profit

* If all models are equiprobable then, $𝜆_𝑚 = \frac{1}{m}, m=1,…,M$

#### What Uncertainty Distribution is Best?

It is natural to assume that a narrower uncertainty distribution is better.

* lower dispersion or variance in the probability density function

Yet, higher variance has more downside and more upside.

* depending on the loss function and over- and under-estimation loss or profit the optimum estimate may shift up, shift down or stay the same

To make an optimum estimate / decision from a distribution we need a function that describes the:

* loss due to under and overestimation.

#### Limitations

The optimum decision making in the precense of uncertainty assumes:
1. **representativity** - the uncertainty distribution is representative, a good uncertainty model
2. **independence** - between the variables / features impacting the uncertainty distribuiton and those impacting the lose function
3. **stationarity** - the uncertainty model and the loss function are invariant under translation over included spatial samples and for location of implementation

#### Getting Started

Here's the steps to get setup in Python with the GeostatsPy package:

1. Install Anaconda 3 on your machine (https://www.anaconda.com/download/).
2. From Anaconda Navigator (within Anaconda3 group), go to the environment tab, click on base (root) green arrow and open a terminal.
3. In the terminal type: pip install geostatspy.
4. Open Jupyter and in the top block get started by copy and pasting the code block below from this Jupyter Notebook to start using the geostatspy functionality.

There are examples below with these functions. You can go here to see a list of the available functions, https://git.io/fh4eX, other example workflows and source code.

```python
import geostatspy.GSLIB as GSLIB          # GSLIB utilies, visualization and wrapper
import geostatspy.geostats as geostats    # GSLIB methods convert to Python
```

We will also need some standard packages. These should have been installed with Anaconda 3.

```python
import numpy as np                        # ndarrys for gridded data
import pandas as pd                       # DataFrames for tabular data
import os                                 # set working directory, run executables
import matplotlib.pyplot as plt           # for plotting
from scipy import stats                   # summary statistics
import math                               # trig etc.
import random
from ipywidgets import interactive        # widgets and interactivity
from ipywidgets import widgets
from ipywidgets import Layout
from ipywidgets import Label
from ipywidgets import VBox, HBox
```

#### Set Up the Interactive Dashboard

The following code includes the:

* dashboard
* numerical methods
* graphical displays

```python
# interactive calculation of the random sample set (control of source parametric distribution and number of samples)
l = widgets.Text(value='                                      Optimum Decision Making Demonstration, Michael Pyrcz, Associate Professor, The University of Texas at Austin',layout=Layout(width='950px', height='30px'))

mean = widgets.FloatSlider(min=0.0, max = 1.0, value = 0.2,step=0.01,description = '$\mu$',orientation='horizontal',layout=Layout(width='230px', height='50px'),continuous_update=False)
mean.style.handle_color = 'red'

stdev = widgets.FloatSlider(min=0.001, max = 0.25, value = 0.03,step=0.01,description = '$\sigma$',orientation='horizontal',layout=Layout(width='230px', height='50px'),continuous_update=False)
stdev.style.handle_color = 'red'

ui1 = widgets.VBox([mean,stdev],kwargs = {'justify_content':'center'})

slope_under = widgets.FloatSlider(min=0.01, max = 1.0,value = 0.1,step=0.01,description = '$m_{under}$',orientation='horizontal',layout=Layout(width='230px', height='50px'),continuous_update=False)
slope_under.style.handle_color = 'blue'

power_under = widgets.FloatSlider(min=1.0, max = 5.0, value = 0.0, description = '$p_{under}$',orientation='horizontal',layout=Layout(width='230px', height='50px'),continuous_update=False)
power_under.style.handle_color = 'blue'

ui2 = widgets.VBox([slope_under,power_under],kwargs = {'justify_content':'center'})

slope_over = widgets.FloatSlider(min=0.01, max = 1.0,value = 0.1,step=0.1,description = '$m_{over}$',orientation='horizontal',layout=Layout(width='230px', height='50px'),continuous_update=False)
slope_over.style.handle_color = 'blue'

power_over = widgets.FloatSlider(min=1.0, max = 5.0, value = 0.0, description = '$p_{over}$',orientation='horizontal',layout=Layout(width='230px', height='50px'),continuous_update=False)
power_over.style.handle_color = 'blue'

ui3 = widgets.VBox([slope_over,power_over],kwargs = {'justify_content':'center'})

step = widgets.FloatLogSlider(min=-4, max = -1, value = 0.01, description = 'Step',orientation='horizontal',layout=Layout(width='230px', height='50px'),continuous_update=False)
step.style.handle_color = 'gray'

ui4 = widgets.VBox([step],kwargs = {'justify_content':'none'})

ui = widgets.HBox([ui1,ui2,ui3,ui4])
ui_title = widgets.VBox([l,ui],)

def make_plot(mean,stdev,slope_under,power_under,slope_over,power_over,step):
    xmin = mean - 5.0 * stdev
    xmax = mean + 5.0 * stdev
    X = np.arange(xmin,xmax,step)              # build uncertainty distribution
    pdf = stats.norm.pdf(X, loc = mean, scale = stdev)
    y = stats.norm.rvs(loc=mean,scale=stdev,size=10000)

    delta = np.arange(-1*(xmax-xmin)*4,(xmax-xmin)*4,step) # build loss function
    loss = np.zeros(len(delta))
    rloss = np.zeros(len(delta))               # flip for convolve operator

    for id, d in enumerate(delta):
        if d < 0.0:
            loss[id] = pow(abs(d),power_under)*slope_under
            rloss[len(delta)-id-1] = pow(abs(d),power_under)*slope_under
        else:
            loss[id] = pow(d,power_over)*slope_over
            rloss[len(delta)-id-1] = pow(d,power_over)*slope_over

    pdf_norm = pdf / pdf.sum()                 # calculate expected loss
    exp_loss = np.convolve(pdf_norm,loss,mode='valid')
    x_exp_loss = np.arange(-0.5*step*(len(exp_loss))+mean,0.5*step*(len(exp_loss))+mean,step)
    x_exp_loss = x_exp_loss[0:len(exp_loss)]   # avoid rounding error
    inside = np.logical_and(x_exp_loss >= xmin, x_exp_loss <= xmax)
    max_loss_plot = np.max(exp_loss,where = inside,initial=0.0)
    best_estimate = x_exp_loss[np.argmin(exp_loss)]
    min_loss = np.min(exp_loss)

    plt.subplot(131)                           # plot uncertainty distribution
    plt.plot(X,pdf,'--',color='black',alpha=0.4,zorder=1)
    hist = plt.hist(y,bins=X,edgecolor='black',color='red',alpha=0.1,density=True,stacked=True)
    plt.plot([best_estimate,best_estimate],[0,np.max(hist[0])*1.2],color='black',linestyle='--')
    plt.annotate('Optimum Estimate',[best_estimate,np.max(hist[0])*0.6], rotation=270)
    plt.xlim(0.0,1.0); plt.xlabel("Feature, $y_1$"); plt.ylim([0,np.max(hist[0])*1.2])
    plt.title('Uncertainty Distribution'); plt.ylabel('Density')
    plt.grid(color='grey', ls = '-.', lw = 0.1)

    plt.subplot(132)                           # plot loss function, loss vs. estimate error
    plt.plot(delta,loss,color='blue',alpha=0.2)
#    plt.plot([0,0],[0,np.max(loss)],color='black',linestyle='--',alpha=0.3)
    plt.plot([0,0],[0,1.0],color='black',linestyle='--',alpha=0.3)
#    plt.annotate('Underestimation',[(xmax-xmin)*-0.1,np.max(loss)*0.8], rotation=0,horizontalalignment='right')
#    plt.annotate('Overestimation',[(xmax-xmin)*0.1,np.max(loss)*0.8], rotation=0,horizontalalignment='left')
    plt.annotate('Underestimation',[(xmax-xmin)*-0.1,0.7], rotation=0,horizontalalignment='right')
    plt.annotate('Overestimation',[(xmax-xmin)*0.1,0.7], rotation=0,horizontalalignment='left')
#    plt.xlim([-1*(xmax-xmin)*3,(xmax-xmin)*3])
#   plt.ylim([0,np.max(loss)])
    plt.xlim([-1,1]); plt.ylim([0,1.0])
    plt.xlabel('Error In Estimate'); plt.title('Loss Function'); plt.ylabel('Loss')
    plt.grid(color='grey', ls = '-.', lw = 0.1)

    plt.subplot(133)                           # plot expected loss vs. estimate
    plt.plot(x_exp_loss,exp_loss,color='black',alpha=0.2)
    plt.plot([best_estimate,best_estimate],[0,max_loss_plot],color='black',linestyle='--')
    plt.xlim([0.0,1.0]); # plt.ylim([0,1.0])
    plt.annotate('Optimum Estimate',[best_estimate,max_loss_plot*0.6], rotation=270)
#    plt.plot([xmin,xmax],[min_loss,min_loss],color='black',linestyle='--',alpha=0.3)
    plt.plot([0,1],[min_loss,min_loss],color='black',linestyle='--',alpha=0.3)
    plt.annotate('Minimum Expected Loss',[xmin+(xmax-xmin)*0.2,min_loss*1.1], rotation=0,horizontalalignment='left')
    plt.ylim([0,max_loss_plot])
    plt.xlabel('Estimate, $y^*_1$'); plt.title('Expected Loss vs. Estimate'); plt.ylabel('Expected Loss')
    plt.grid(color='grey', ls = '-.', lw = 0.1)

    plt.subplots_adjust(left=0.0, bottom=0.0, right=3.0, top=1.2, wspace=0.2, hspace=0.2)
    plt.show()

interactive_plot = widgets.interactive_output(make_plot, {'mean':mean,'stdev':stdev,'slope_under':slope_under,'power_under':power_under,'slope_over':slope_over,'power_over':power_over,'step':step})
interactive_plot.clear_output(wait = True)                # reduce flickering by delaying plot updating

```

### Optimum Decision Making Demonstration

* specify the uncertainty distribuiton $\mu$, $\sigma$ and loss function $m$ and $p$ and observe the loss function, expected loss and optimum estimate

#### Michael Pyrcz, Associate Professor, University of Texas at Austin

##### [Twitter](https://twitter.com/geostatsguy) | [GitHub](https://github.com/GeostatsGuy) | [Website](http://michaelpyrcz.com) | [GoogleScholar](https://scholar.google.com/citations?user=QVZ20eQAAAAJ&hl=en&oi=ao) | [Book](https://www.amazon.com/Geostatistical-Reservoir-Modeling-Michael-Pyrcz/dp/0199731446) | [YouTube](https://www.youtube.com/channel/UCLqEr-xV-ceHdXXXrTId5ig)  | [LinkedIn](https://www.linkedin.com/in/michael-pyrcz-61a648a1) | [GeostatsPy](https://github.com/GeostatsGuy/GeostatsPy)

```python
display(ui_title, interactive_plot)                            # display the interactive plot
```

    VBox(children=(Text(value='                                      Optimum Decision Making Demonstration, Michae…

    Output(outputs=({'output_type': 'display_data', 'data': {'text/plain': '<Figure size 432x288 with 3 Axes>', 'i…

#### Comments

This was a basic demonstration of optimum decision making in the precense of uncertainty.

* we build uncertainty models all the time to support decision making. Here's how it gets done.

I have other demonstrations on the basics of working with DataFrames, ndarrays, univariate statistics, plotting data, declustering, data transformations, trend modeling, multivariate analysis and many other workflows available at https://github.com/GeostatsGuy/PythonNumericalDemos and https://github.com/GeostatsGuy/GeostatsPy.

I hope this was helpful,

*Michael*

#### The Author:

### Michael Pyrcz, Associate Professor, University of Texas at Austin
*Novel Data Analytics, Geostatistics and Machine Learning Subsurface Solutions*

With over 17 years of experience in subsurface consulting, research and development, Michael has returned to academia driven by his passion for teaching and enthusiasm for enhancing engineers' and geoscientists' impact in subsurface resource development.

For more about Michael check out these links:

#### [Twitter](https://twitter.com/geostatsguy) | [GitHub](https://github.com/GeostatsGuy) | [Website](http://michaelpyrcz.com) | [GoogleScholar](https://scholar.google.com/citations?user=QVZ20eQAAAAJ&hl=en&oi=ao) | [Book](https://www.amazon.com/Geostatistical-Reservoir-Modeling-Michael-Pyrcz/dp/0199731446) | [YouTube](https://www.youtube.com/channel/UCLqEr-xV-ceHdXXXrTId5ig)  | [LinkedIn](https://www.linkedin.com/in/michael-pyrcz-61a648a1)

#### Want to Work Together?

I hope this content is helpful to those that want to learn more about subsurface modeling, data analytics and machine learning. Students and working professionals are welcome to participate.

* Want to invite me to visit your company for training, mentoring, project review, workflow design and / or consulting? I'd be happy to drop by and work with you!

* Interested in partnering, supporting my graduate student research or my Subsurface Data Analytics and Machine Learning consortium (co-PIs including Profs. Foster, Torres-Verdin and van Oort)? My research combines data analytics, stochastic modeling and machine learning theory with practice to develop novel methods and workflows to add value. We are solving challenging subsurface problems!

* I can be reached at mpyrcz@austin.utexas.edu.

I'm always happy to discuss,

*Michael*

Michael Pyrcz, Ph.D., P.Eng. Associate Professor The Hildebrand Department of Petroleum and Geosystems Engineering, Bureau of Economic Geology, The Jackson School of Geosciences, The University of Texas at Austin

#### More Resources Available at: [Twitter](https://twitter.com/geostatsguy) | [GitHub](https://github.com/GeostatsGuy) | [Website](http://michaelpyrcz.com) | [GoogleScholar](https://scholar.google.com/citations?user=QVZ20eQAAAAJ&hl=en&oi=ao) | [Book](https://www.amazon.com/Geostatistical-Reservoir-Modeling-Michael-Pyrcz/dp/0199731446) | [YouTube](https://www.youtube.com/channel/UCLqEr-xV-ceHdXXXrTId5ig)  | [LinkedIn](https://www.linkedin.com/in/michael-pyrcz-61a648a1)

```python

```