Skip to content

Commit adc607c

Browse files
authored
Fix Bruggeman EMA (#173)
* Fix documentation typos * Correct faulty comparison * Implement Rouseel-Vanhellemont-Meas algorithm * Fix RVM algorithm * Disable Jansson code for now * Include new tests for mixture materials * Remove commented-out code
1 parent 7180529 commit adc607c

File tree

3 files changed

+70
-122
lines changed

3 files changed

+70
-122
lines changed

src/elli/materials.py

Lines changed: 18 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131

3232
import numpy as np
3333
import numpy.typing as npt
34-
from numpy.lib.scimath import sqrt
34+
from numpy.lib.scimath import sqrt, power
3535

3636
from .dispersions.base_dispersion import BaseDispersion
3737

@@ -313,7 +313,7 @@ class VCAMaterial(MixtureMaterial):
313313
* :math:`\varepsilon_\text{eff}` is the effective permittivity of host/mixture material,
314314
* :math:`\varepsilon_h` is the permittivity of the host mixture material,
315315
* :math:`\varepsilon_g` is the permittivity of the guest mixture material and
316-
* :math:`f` is the volume fraction of material a in the guest material.
316+
* :math:`f` is the volume fraction of the guest in the host material.
317317
"""
318318

319319
def get_tensor_fraction(self, lbda: npt.ArrayLike, fraction: float) -> npt.NDArray:
@@ -346,7 +346,7 @@ class LooyengaEMA(MixtureMaterial):
346346
* :math:`\varepsilon_\text{eff}` is the effective permittivity of host/mixture material,
347347
* :math:`\varepsilon_h` is the permittivity of the host mixture material,
348348
* :math:`\varepsilon_g` is the permittivity of the guest mixture material and
349-
* :math:`f` is the volume fraction of material a in the guest material.
349+
* :math:`f` is the volume fraction of the guest in the host material.
350350
351351
References:
352352
Looyenga, H. (1965). Physica, 31(3), 401–406.
@@ -384,7 +384,7 @@ class MaxwellGarnettEMA(MixtureMaterial):
384384
* :math:`\varepsilon_\text{eff}` is the effective permittivity of host/mixture material,
385385
* :math:`\varepsilon_h` is the permittivity of the host mixture material,
386386
* :math:`\varepsilon_g` is the permittivity of the guest mixture material and
387-
* :math:`f` is the volume fraction of material a in the guest material.
387+
* :math:`f` is the volume fraction of the guest in the host material.
388388
"""
389389

390390
def get_tensor_fraction(self, lbda: npt.ArrayLike, fraction: float) -> npt.NDArray:
@@ -420,22 +420,22 @@ def get_tensor_fraction(self, lbda: npt.ArrayLike, fraction: float) -> npt.NDArr
420420

421421
class BruggemanEMA(MixtureMaterial):
422422
r"""Mixture Material approximated with the Bruggeman formula
423-
for spherical inclusions.
423+
for isotropic spherical inclusions.
424424
425425
Returns one of the two analytical solutions to this quadratic equation:
426426
427427
.. math::
428428
2 \varepsilon_\text{eff}^2 +
429-
\varepsilon_\text{eff} [(3f - 2) \varepsilon_a
430-
+ (1 - 3f)\varepsilon_b] - \varepsilon_a \cdot \varepsilon_b = 0
429+
[(3f - 2) \varepsilon_a + (1 - 3f)\varepsilon_b] \varepsilon_\text{eff}
430+
- \varepsilon_a \cdot \varepsilon_b = 0
431431
432432
where :math:`\varepsilon_\text{eff}` is the effective permittivity of host/mixture material,
433433
:math:`\varepsilon_a` is the permittivity of the first mixture material,
434434
:math:`\varepsilon_b` is the permittivity of the second mixture material
435435
and :math:`f` is the volume fraction of material a in the material b.
436436
437437
References:
438-
* Josef Humlicek in Ellipsometry at the Nanoscale, Springer-Verlag Berlin Heidelberg, 2013
438+
* Ph.J. Rouseel; J. Vanhellemont; H.E. Maes. (1993) Thin Solid Films, 234, 423-427
439439
"""
440440

441441
def get_tensor_fraction(self, lbda: npt.ArrayLike, fraction: float) -> npt.NDArray:
@@ -453,106 +453,17 @@ def get_tensor_fraction(self, lbda: npt.ArrayLike, fraction: float) -> npt.NDArr
453453
e_g = self.guest_material.get_tensor(lbda)
454454
f = fraction
455455

456-
# fmt: off
457-
root1 = 3*e_g*f/4 - e_g/4 - 3*e_h*f/4 + e_h/2 - sqrt(
458-
9*e_g**2*f**2 - 6*e_g**2*f + e_g**2 - 18*e_g*e_h*f**2 +
459-
18*e_g*e_h*f + 4*e_g*e_h + 9*e_h**2*f**2 - 12*e_h**2*f + 4*e_h**2)/4
460-
root2 = 3*e_g*f/4 - e_g/4 - 3*e_h*f/4 + e_h/2 + sqrt(
461-
9*e_g**2*f**2 - 6*e_g**2*f + e_g**2 - 18*e_g*e_h*f**2 +
462-
18*e_g*e_h*f + 4*e_g*e_h + 9*e_h**2*f**2 - 12*e_h**2*f + 4*e_h**2)/4
463-
# fmt: on
464-
465-
return self.jansson_algorithm(e_h, e_g, root1, root2)
466-
467-
@staticmethod
468-
def jansson_algorithm(
469-
e_h: npt.ArrayLike,
470-
e_g: npt.ArrayLike,
471-
root1: npt.ArrayLike,
472-
root2: npt.ArrayLike,
473-
):
474-
"""Use the algorithm proposed by Jansson and Arwin to find the correct root of
475-
the solution to the Bruggeman formula.
476-
477-
References:
478-
* Jansson R. , Arwin H. (1994) Optics Communications, 106, 4-6, 133-138,
479-
https://doi.org/10.1016/0030-4018(94)90309-3.
456+
mask_equal = np.nonzero(np.equal(e_h, e_g))
457+
mask_different = np.nonzero(np.not_equal(e_h, e_g))
480458

481-
Args:
482-
e_h (npt.ArrayLike): Dielectric tensor of host material.
483-
e_g (npt.ArrayLike): Dielectric tensor of host material.
484-
root1 (npt.ArrayLike): Solution 1 for dielectric tensor of mixture.
485-
root2 (npt.ArrayLike): Solution 2 for dielectric tensor of mixture.
486-
487-
Returns:
488-
npt.NDArray: Physically correct permittivity tensor for the mixture.
489-
"""
490-
491-
# Catch calculation warnings
492-
old_settings = np.geterr()
493-
np.seterr(invalid="ignore", divide="ignore")
494-
495-
z0 = (
496-
e_h
497-
* e_g
498-
* (np.conj(e_h) - np.conj(e_g))
499-
/ (np.conj(e_h) * e_g - e_h * np.conj(e_g))
500-
)
501-
scaling_factor = np.conj(e_g - e_h) / np.abs(e_g - e_h)
502-
503-
# Reset numpy settings
504-
np.seterr(**old_settings)
459+
p = sqrt(e_h[mask_different]) / sqrt(e_g[mask_different])
460+
b = 0.25 * ((3 * f - 1) * (1 / p - p) + p)
461+
z = b + sqrt(power(b, 2) + 0.5)
505462

506-
# Find indices for the three cases
507-
mask_equal = np.nonzero(np.isnan(z0.real))
508-
mask_straight = np.nonzero(np.isinf(z0.real))
509-
mask_general = np.nonzero(
510-
np.logical_not(np.logical_or(np.isnan(z0.real), np.isinf(z0.real)))
463+
e_mix = np.full_like(e_h, np.nan)
464+
e_mix[mask_equal] = e_h[mask_equal]
465+
e_mix[mask_different] = (
466+
z * sqrt(e_h[mask_different]) * sqrt(e_g[mask_different])
511467
)
512468

513-
def check_straight_line():
514-
def w(z):
515-
return np.where(
516-
z0[mask_straight].real == np.inf,
517-
z[mask_straight] * scaling_factor[mask_straight],
518-
-1 * z[mask_straight] * scaling_factor[mask_straight],
519-
)
520-
521-
return np.where(
522-
np.logical_and(
523-
w(e_h).real < w(root1).real, w(root1).real < w(e_g).real
524-
),
525-
root1[mask_straight],
526-
root2[mask_straight],
527-
)
528-
529-
def check_general_case():
530-
def zeta(z):
531-
return (
532-
(z[mask_general] - z0[mask_general])
533-
/ np.abs(z0[mask_general])
534-
* scaling_factor[mask_general]
535-
)
536-
537-
zeta_1, zeta_2, zeta_root1 = np.where(
538-
np.logical_and(zeta(e_h).imag > 0, zeta(e_g).imag > 0),
539-
(zeta(e_h), zeta(e_g), zeta(root1)),
540-
(-zeta(e_h), -zeta(e_g), -zeta(root1)),
541-
)
542-
543-
return np.where(
544-
np.logical_and(
545-
np.abs(zeta_root1 <= 1),
546-
zeta_root1.imag >= np.imag((zeta_1 + zeta_2) / 2),
547-
),
548-
root1[mask_general],
549-
root2[mask_general],
550-
)
551-
552-
# Create new array and write correct values into it
553-
correct_root = np.full_like(e_h, np.nan)
554-
correct_root[mask_equal] = e_h[mask_equal]
555-
correct_root[mask_straight] = check_straight_line()
556-
correct_root[mask_general] = check_general_case()
557-
558-
return correct_root
469+
return e_mix

tests/test_materials.py

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
"""Tests for the result class"""
1+
"""Tests for the materials classes"""
22

3-
import numpy as np
4-
from pytest import raises
53
import elli
4+
from pytest import raises
65

76

87
class TestMaterials:
@@ -25,8 +24,6 @@ def test_materials_typeguard(self):
2524
with raises(TypeError):
2625
elli.BiaxialMaterial(self.disp, self.disp, 23)
2726

28-
def test_mixture_materials(self):
29-
"""Basic mixture material tests"""
3027
with raises(TypeError):
3128
elli.VCAMaterial(self.mat, self.disp, 0.5)
3229

@@ -35,13 +32,3 @@ def test_mixture_materials(self):
3532

3633
with raises(ValueError):
3734
elli.VCAMaterial(self.mat, self.mat, 10)
38-
39-
vca = elli.VCAMaterial(self.mat2, self.mat, 0.1)
40-
looyenga = elli.LooyengaEMA(self.mat2, self.mat, 0.1)
41-
mg = elli.MaxwellGarnettEMA(self.mat2, self.mat, 0.1)
42-
brug = elli.BruggemanEMA(self.mat2, self.mat, 0.1)
43-
44-
for mixture in [looyenga, mg, brug]:
45-
np.testing.assert_array_almost_equal(
46-
mixture.get_tensor(500), vca.get_tensor(500), decimal=-1
47-
)

tests/test_mixture_models.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""Tests for the different mixture models"""
2+
3+
import pytest
4+
import elli
5+
import numpy as np
6+
7+
8+
material_list = [
9+
("Si", "Aspnes"),
10+
("GaAs", "Aspnes"),
11+
("SiO2", "Malitson"),
12+
("CaF2", "Li"),
13+
("Rh", "Weaver"),
14+
("MoS2", "Song-1L"),
15+
]
16+
17+
18+
@pytest.fixture
19+
def RII():
20+
RII = elli.db.RII()
21+
return RII
22+
23+
24+
@pytest.mark.parametrize(
25+
"host_book, host_page",
26+
material_list,
27+
)
28+
@pytest.mark.parametrize(
29+
"guest_book, guest_page",
30+
material_list,
31+
)
32+
@pytest.mark.parametrize(
33+
"EMA",
34+
[
35+
elli.LooyengaEMA,
36+
elli.MaxwellGarnettEMA,
37+
],
38+
)
39+
def test_mixture_models(host_book, host_page, guest_book, guest_page, EMA, RII):
40+
host_mat = RII.get_mat(host_book, host_page)
41+
guest_mat = RII.get_mat(guest_book, guest_page)
42+
43+
lbda = np.arange(250, 800, 25)
44+
45+
np.testing.assert_allclose(
46+
elli.BruggemanEMA(host_mat, guest_mat, 0.1).get_tensor(lbda),
47+
EMA(host_mat, guest_mat, 0.1).get_tensor(lbda),
48+
rtol=0.5,
49+
atol=0.5 + 0.5j,
50+
)

0 commit comments

Comments
 (0)