CompositePropCalc / compositecalc /core /lichtenecker.py
tengfeiluo's picture
Upload folder using huggingface_hub
01a0b26 verified
"""Lichtenecker power-law mixing rules and Looyenga equation.
Three related closed-form mixing rules express the effective composite
property as a power-mean combination of constituent properties:
1. **Geometric mean (Lichtenecker, 1926):** The limiting case of the
power-law as the exponent n → 0. No free parameters; lies strictly
between the Reuss and Voigt bounds for P_m ≠ P_f.
2. **General power-law (Lichtenecker–Rother, 1931):** The exponent
n ∈ [−1, 1] interpolates between the Reuss lower bound (n = −1),
the geometric mean (n → 0), the Looyenga value (n = 1/3), and the
Voigt upper bound (n = 1). The effective property is *monotone
increasing* in n whenever P_f ≠ P_m.
3. **Looyenga (1965):** The special case n = 1/3, independently derived
from a differential effective medium argument. Performs well for
moderate-contrast mixtures and is the dielectric Bruggeman analogue in
the optical limit.
Mathematical conventions
------------------------
Power-law (n ≠ 0):
P_eff^n = (1 − φ)·P_m^n + φ·P_f^n
Geometric mean (n → 0, limiting case):
P_eff = P_m^(1−φ) · P_f^φ = exp[(1−φ)·ln P_m + φ·ln P_f]
Looyenga (n = 1/3):
P_eff^(1/3) = (1−φ)·P_m^(1/3) + φ·P_f^(1/3)
Special values of n
n = −1 → Reuss series bound (exact lower bound)
n → 0 → Geometric mean (Lichtenecker 1926)
n = 1/3 → Looyenga (1965)
n = 1 → Voigt parallel bound (exact upper bound)
References
----------
Lichtenecker, K. (1926). Die Dielektrizitätskonstante natürlicher und
künstlicher Mischkörper. *Phys. Z.*, 27, 115–158.
Lichtenecker, K. and Rother, K. (1931). Die Herleitung des
logarithmischen Mischungsgesetzes. *Phys. Z.*, 32, 255–260.
Looyenga, H. (1965). Dielectric constants of heterogeneous mixtures.
*Physica*, 31(3), 401–406.
"""
from __future__ import annotations
import numpy as np
__all__ = [
"geometric_mean",
"power_law",
"looyenga",
]
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def geometric_mean(
P_m: float | np.ndarray,
P_f: float | np.ndarray,
phi: float | np.ndarray,
) -> float | np.ndarray:
"""Geometric mean (Lichtenecker, 1926) effective composite property.
The limiting case of the power-law mixing rule as the exponent n → 0:
.. math::
P_{\\text{eff}} = P_m^{1-\\phi} \\cdot P_f^{\\phi}
Equivalent to ``exp((1−φ)·ln P_m + φ·ln P_f)``. Lies strictly between
the Reuss and Voigt bounds for P_m ≠ P_f.
Parameters
----------
P_m : float or np.ndarray
Matrix property (positive).
P_f : float or np.ndarray
Filler property (positive, same units as P_m).
phi : float or np.ndarray
Filler volume fraction ∈ [0, 1].
Returns
-------
P_eff : float or np.ndarray
Effective composite property.
Raises
------
ValueError
If *phi* is outside [0, 1] or *P_m* / *P_f* is non-positive.
Notes
-----
* For P_m = P_f the result equals P_m regardless of φ.
* Symmetric under phase swap with complementary fraction:
``geometric_mean(P_m, P_f, φ) == geometric_mean(P_f, P_m, 1−φ)``.
* The result lies strictly between Reuss and Voigt whenever P_m ≠ P_f
and 0 < φ < 1.
References
----------
Lichtenecker, K. (1926). *Phys. Z.*, 27, 115.
Examples
--------
>>> geometric_mean(1.0, 10.0, 0.3) # 10^0.3 ≈ 1.9953
1.9952623149688797
>>> geometric_mean(1.0, 10.0, 0.0) # phi=0 → P_m
1.0
>>> geometric_mean(1.0, 10.0, 1.0) # phi=1 → P_f
10.0
"""
P_m = np.asarray(P_m, dtype=float)
P_f = np.asarray(P_f, dtype=float)
phi = np.asarray(phi, dtype=float)
if np.any((phi < 0.0) | (phi > 1.0)):
raise ValueError("phi must be in [0, 1].")
if np.any(P_m <= 0.0):
raise ValueError("P_m must be positive.")
if np.any(P_f <= 0.0):
raise ValueError("P_f must be positive.")
result = np.exp((1.0 - phi) * np.log(P_m) + phi * np.log(P_f))
return float(result) if result.ndim == 0 else result
def power_law(
P_m: float | np.ndarray,
P_f: float | np.ndarray,
phi: float | np.ndarray,
n: float = 1.0 / 3.0,
) -> float | np.ndarray:
"""General power-law (Lichtenecker–Rother, 1931) effective composite property.
Computes
.. math::
P_{\\text{eff}} =
\\bigl[(1-\\phi)\\,P_m^n + \\phi\\,P_f^n\\bigr]^{1/n}
for n ≠ 0, with the limit n → 0 giving the geometric mean
``P_m^(1−φ)·P_f^φ``.
Parameters
----------
P_m : float or np.ndarray
Matrix property (positive).
P_f : float or np.ndarray
Filler property (positive, same units as P_m).
phi : float or np.ndarray
Filler volume fraction ∈ [0, 1].
n : float, optional
Power-law exponent. Must lie in [−1, 1]. Defaults to ``1/3``
(Looyenga). Special values:
* ``n = −1`` → Reuss series lower bound (exact)
* ``n → 0`` → Geometric mean (Lichtenecker 1926)
* ``n = 1/3`` → Looyenga (1965)
* ``n = 1`` → Voigt parallel upper bound (exact)
Returns
-------
P_eff : float or np.ndarray
Effective composite property (same units as P_m).
Raises
------
ValueError
If *phi* is outside [0, 1], *P_m* / *P_f* is non-positive, or *n*
is outside [−1, 1].
Notes
-----
* The mapping n ↦ P_eff(n) is strictly monotone increasing for P_f ≠ P_m
and 0 < φ < 1. Thus Reuss ≤ geometric mean ≤ Looyenga ≤ Voigt.
* For |n| < 1×10⁻⁹ the geometric-mean limit is used directly to
avoid numerical overflow in ``base^(1/n)``.
References
----------
Lichtenecker, K. and Rother, K. (1931). *Phys. Z.*, 32, 255.
Examples
--------
>>> power_law(1.0, 10.0, 0.3, n=1) # Voigt upper bound
3.7
>>> power_law(1.0, 10.0, 0.3, n=-1) # Reuss lower bound
1.3698630136986301
>>> power_law(1.0, 10.0, 0.3, n=0) # geometric mean
1.9952623149688797
>>> power_law(1.0, 8.0, 0.3, n=1/3) # Looyenga: (0.7 + 0.6)^3 = 2.197
2.197
"""
P_m = np.asarray(P_m, dtype=float)
P_f = np.asarray(P_f, dtype=float)
phi = np.asarray(phi, dtype=float)
n = float(n)
if np.any((phi < 0.0) | (phi > 1.0)):
raise ValueError("phi must be in [0, 1].")
if np.any(P_m <= 0.0):
raise ValueError("P_m must be positive.")
if np.any(P_f <= 0.0):
raise ValueError("P_f must be positive.")
if not (-1.0 <= n <= 1.0):
raise ValueError("n must be in [-1, 1].")
if abs(n) < 1e-9:
# Geometric mean: the n→0 limit
result = np.exp((1.0 - phi) * np.log(P_m) + phi * np.log(P_f))
else:
result = ((1.0 - phi) * P_m**n + phi * P_f**n) ** (1.0 / n)
return float(result) if result.ndim == 0 else result
def looyenga(
P_m: float | np.ndarray,
P_f: float | np.ndarray,
phi: float | np.ndarray,
) -> float | np.ndarray:
"""Looyenga (1965) effective composite property.
The special case n = 1/3 of the general power-law mixing rule:
.. math::
P_{\\text{eff}}^{1/3} =
(1-\\phi)\\,P_m^{1/3} + \\phi\\,P_f^{1/3}
Independently derived by Looyenga from a differential effective medium
argument. Symmetric in the phase roles; performs well for moderate-
contrast mixtures and is widely used in dielectric mixing.
Parameters
----------
P_m : float or np.ndarray
Matrix property (positive).
P_f : float or np.ndarray
Filler property (positive, same units as P_m).
phi : float or np.ndarray
Filler volume fraction ∈ [0, 1].
Returns
-------
P_eff : float or np.ndarray
Effective composite property (same units as P_m).
Raises
------
ValueError
If *phi* is outside [0, 1] or *P_m* / *P_f* is non-positive.
Notes
-----
* Algebraically identical to ``power_law(P_m, P_f, phi, n=1/3)``.
* Symmetric under phase swap and complementary fraction:
``looyenga(P_m, P_f, φ) == looyenga(P_f, P_m, 1−φ)``.
* Lies strictly between the Reuss lower bound and Voigt upper bound,
and strictly above the geometric mean, for P_m ≠ P_f and 0 < φ < 1.
References
----------
Looyenga, H. (1965). Dielectric constants of heterogeneous mixtures.
*Physica*, 31(3), 401–406.
Examples
--------
>>> looyenga(1.0, 8.0, 0.3) # (0.7·1 + 0.3·2)^3 = 1.3^3 = 2.197
2.197
>>> looyenga(1.0, 10.0, 0.0) # phi=0 → P_m
1.0
>>> looyenga(1.0, 10.0, 1.0) # phi=1 → P_f
10.0
"""
P_m = np.asarray(P_m, dtype=float)
P_f = np.asarray(P_f, dtype=float)
phi = np.asarray(phi, dtype=float)
if np.any((phi < 0.0) | (phi > 1.0)):
raise ValueError("phi must be in [0, 1].")
if np.any(P_m <= 0.0):
raise ValueError("P_m must be positive.")
if np.any(P_f <= 0.0):
raise ValueError("P_f must be positive.")
result = (
(1.0 - phi) * P_m ** (1.0 / 3.0) + phi * P_f ** (1.0 / 3.0)
) ** 3.0
return float(result) if result.ndim == 0 else result