Cluster of spheres¶
Four high-index spheres with radii from 75 to 110 nm sit at the corners of a tetrahedron. Multiple scattering couples them: the T-matrix of the cluster includes every interaction and keeps one expansion center per sphere. Expanding it about the origin up to l = 6 gives one global T-matrix, valid outside the sphere of radius 260 nm that encloses the cluster. Rotating the cluster by 90 degrees about y and turning the incident wave with it leaves the cross sections unchanged and rotates the intensity map.
"""Four spheres coupled by multiple scattering: cross sections and near fields."""
import numpy as np
import treams_rs as tr
# Enlarge for a full map: treams uses a 101 x 101 grid.
GRID_POINTS = 7
k0 = 2 * np.pi / 1000
material = tr.Material(16 + 0.5j)
lmax = 3
radii = [110, 90, 80, 75]
positions = (220 / np.sqrt(24)) * np.array(
[
[np.sqrt(8), 0, -1],
[-np.sqrt(2), np.sqrt(6), -1],
[-np.sqrt(2), -np.sqrt(6), -1],
[0, 0, 3],
]
)
spheres = [
tr.sphere_tmatrix(k0=k0, lmax=lmax, radius=r, material=material) for r in radii
]
# Cluster.solve couples the spheres and returns one T-matrix with an
# expansion center at every sphere.
tm = tr.Cluster(spheres, positions=positions).solve()
inc = tr.plane_wave(direction=[1, 0, 0], pol="negative_helicity", k0=k0)
xs = tm.cross_sections(inc)
x = np.linspace(-300, 300, GRID_POINTS)
z = np.linspace(-300, 300, GRID_POINTS)
xx, zz = np.meshgrid(x, z, indexing="ij")
grid = np.stack((xx, np.zeros_like(xx), zz), axis=-1)
def intensity_map(tm, inc, radii):
"""Total-field intensity on the grid; NaN where the expansion does not hold."""
sca = tm.scatter(inc)
intensity = np.full_like(xx, np.nan)
valid = tm.valid_points(grid, radii)
total = inc.efield(grid[valid]) + sca.efield(grid[valid])
intensity[valid] = 0.5 * np.sum(np.abs(total) ** 2, axis=-1)
return intensity
intensity = intensity_map(tm, inc, radii)
# One expansion about the origin up to l = 6. Its fields hold only outside
# the sphere of radius 260 that encloses the cluster.
tm_global = tm.in_basis(tr.SphericalBasis.default(6))
xs_global = tm_global.cross_sections(inc)
intensity_global = intensity_map(tm_global, inc, [260])
# Rotating the cluster by 90 degrees about y and illuminating it along -z
# reproduces the first illumination in the rotated frame.
inc_rotate = tr.plane_wave(direction=[0, 0, -1], pol="negative_helicity", k0=k0)
tm_rotate = tm_global.rotate(0, np.pi / 2)
xs_rotate = tm_rotate.cross_sections(inc_rotate)
intensity_rotate = intensity_map(tm_rotate, inc_rotate, [260])
if __name__ == "__main__":
print("Cross sections (nm^2)")
headers = ("expansion", "scattering", "extinction", "absorption")
print("".join(f"{h:>13}" for h in headers))
for label, result in [("local", xs), ("global", xs_global), ("rotated", xs_rotate)]:
print(
f"{label:>13}"
+ "".join(f"{v:>13.6g}" for v in (*result, result.absorption))
)
maps = [
("local", intensity),
("global", intensity_global),
("rotated", intensity_rotate),
]
for label, values in maps:
print(f"\nIntensity at y = 0, {label} expansion (rows: x, columns: z)")
for row in values:
print("".join(f"{v:>11.6g}" for v in row))
# Source: https://github.com/tfp-photonics/treams/blob/1f5d0d6ebb007288f28bc9e16f6d266e8b55dc39/docs/examples/cluster.py
# Adapted: reduced sizes, plotting removed.
# MIT license, copyright Dominik Beutel; see LICENSE.treams.
import numpy as np
import treams
k0 = 2 * np.pi / 1000
materials = [treams.Material(16 + 0.5j), treams.Material()]
lmax = 3
radii = [110, 90, 80, 75]
positions = (220 / np.sqrt(24)) * np.array(
[
[np.sqrt(8), 0, -1],
[-np.sqrt(2), np.sqrt(6), -1],
[-np.sqrt(2), -np.sqrt(6), -1],
[0, 0, 3],
]
)
spheres = [treams.TMatrix.sphere(lmax, k0, r, materials) for r in radii]
tm = treams.TMatrix.cluster(spheres, positions).interaction.solve()
inc = treams.plane_wave([k0, 0, 0], 0, k0=tm.k0, material=tm.material)
sca = tm @ inc.expand(tm.basis)
xs = tm.xs(inc)
x = np.linspace(-300, 300, 7)
y = 0
z = np.linspace(-300, 300, 7)
xx, zz = np.meshgrid(x, z, indexing="ij")
yy = np.full_like(xx, y)
grid = np.stack((xx, yy, zz), axis=-1)
intensity = np.zeros_like(xx)
valid = tm.valid_points(grid, radii)
intensity[~valid] = np.nan
intensity[valid] = 0.5 * np.sum(
np.abs(inc.efield(grid[valid]) + sca.efield(grid[valid])) ** 2, -1
)
tm_global = tm.expand(treams.SphericalWaveBasis.default(6))
sca = tm_global @ inc.expand(tm_global.basis)
xs = tm_global.xs(inc)
intensity_global = np.zeros_like(xx)
valid = tm_global.valid_points(grid, [260])
intensity_global[~valid] = np.nan
intensity_global[valid] = 0.5 * np.sum(
np.abs(inc.efield(grid[valid]) + sca.efield(grid[valid])) ** 2, -1
)
inc = treams.plane_wave([0, 0, -k0], 0, k0=tm.k0, material=tm.material)
tm_rotate = tm_global.rotate(0, np.pi / 2)
sca = tm_rotate @ inc.expand(tm_rotate.basis)
xs = tm_rotate.xs(inc)
intensity_global = np.zeros_like(xx)
valid = tm_rotate.valid_points(grid, [260])
intensity_global[~valid] = np.nan
intensity_global[valid] = 0.5 * np.sum(
np.abs(inc.efield(grid[valid]) + sca.efield(grid[valid])) ** 2, -1
)
Output¶
Cross sections (nm^2)
expansion scattering extinction absorption
local 191950 227233 35283.7
global 191950 227234 35283.8
rotated 191950 227234 35283.8
Intensity at y = 0, local expansion (rows: x, columns: z)
0.492519 0.461788 0.451871 0.451982 0.460042 0.485888 0.523742
0.386665 0.37545 0.396188 0.389244 0.352223 0.34476 0.384914
0.398542 0.567635 1.36139 1.65666 0.548466 0.356186 0.363744
0.498961 0.886037 1.57047 2.29615 nan nan 0.556188
0.611584 1.5044 nan nan 1.88694 0.986848 0.575312
0.652524 1.30309 nan nan 1.47173 0.779578 0.539411
0.64797 0.909768 1.09706 1.09561 0.981412 0.735045 0.547142
Intensity at y = 0, global expansion (rows: x, columns: z)
0.492517 0.461037 0.451883 0.449963 0.455679 0.486385 0.524034
0.387159 0.372718 nan nan nan 0.350069 0.38486
0.398657 nan nan nan nan nan 0.365329
0.492758 nan nan nan nan nan 0.542634
0.610437 nan nan nan nan nan 0.578162
0.655731 1.30454 nan nan nan 0.764384 0.539154
0.64818 0.905497 1.09644 1.09049 0.982361 0.735165 0.546885
Intensity at y = 0, rotated expansion (rows: x, columns: z)
0.64818 0.655731 0.610437 0.492758 0.398657 0.387159 0.492517
0.905497 1.30454 nan nan nan 0.372718 0.461037
1.09644 nan nan nan nan nan 0.451883
1.09049 nan nan nan nan nan 0.449963
0.982361 nan nan nan nan nan 0.455679
0.735165 0.764384 nan nan nan 0.350069 0.486385
0.546885 0.539154 0.578162 0.542634 0.365329 0.38486 0.524034
Differences from the treams version¶
Cluster(spheres, positions=...)holds the uncoupled spheres andsolve()couples them; treams writesTMatrix.cluster(...).interaction.solve().cross_sectionsreturns namedscattering,extinctionandabsorptionvalues; treamsxsreturns the tuple(scattering, extinction).in_basisexpands the T-matrix about new centers; treams calls itexpand.- Each result has its own name (
xs_global,intensity_rotate, ...); treams reusesxs,scaandintensity_global. - One function,
intensity_map, computes all three maps.