Spaces:
Running
Running
| """Self-consistent scheme (SCS) for effective composite properties. | |
| Three implementations span increasing generality: | |
| 1. **self_consistent** — 2-phase SCS with isotropic inclusions. | |
| Algebraically identical to the symmetric Bruggeman equation; provided | |
| as a named alias so callers can choose the physically descriptive name. | |
| 2. **self_consistent_ellipsoid** — 2-phase SCS for *randomly oriented* | |
| ellipsoidal inclusions. Both phases are treated symmetrically (neither | |
| is designated the host), each represented by the same depolarization | |
| L-tuple. For spheres this reduces to **self_consistent**; for needles | |
| or platelets it yields a different P_eff than the sphere formula. | |
| 3. **self_consistent_multiphase** — N-phase symmetric SCS for isotropic | |
| (spherical) inclusions. Generalises Bruggeman's two-phase equation to | |
| an arbitrary number of phases. | |
| Mathematical background | |
| ----------------------- | |
| The Budiansky (1965) / Hill (1965) self-consistent equation for N phases, | |
| each with property P_i and volume fraction φ_i, using depolarization L: | |
| .. math:: | |
| \\sum_{i} \\phi_i \\, | |
| \\frac{P_i - P^*}{P^* + L(P_i - P^*)} = 0 | |
| For randomly oriented ellipsoids with depolarization factors | |
| (L_a, L_b, L_c) summing to 1, the orientation-averaged polarizability | |
| replaces the scalar factor: | |
| .. math:: | |
| \\sum_{i} \\phi_i \\,(P_i - P^*)\\, | |
| \\frac{1}{3}\\sum_{j\\in\\{a,b,c\\}} | |
| \\frac{1}{P^* + L_j(P_i - P^*)} = 0 | |
| For spheres all three L_j equal 1/3 and the inner sum collapses to | |
| 3/(P* + 1/3 (P_i - P*)), recovering the scalar equation. | |
| In both forms the physical root P* always lies in | |
| [min(P_i), max(P_i)] and is found by Brent's method. | |
| References | |
| ---------- | |
| Budiansky, B. (1965). *J. Mech. Phys. Solids*, 13, 223–227. | |
| Hill, R. (1965). *J. Mech. Phys. Solids*, 13, 213–222. | |
| Bruggeman, D.A.G. (1935). *Ann. Phys.*, 416, 636–664. | |
| """ | |
| from __future__ import annotations | |
| import numpy as np | |
| from scipy.optimize import brentq | |
| # 2-phase, single-L SCS is identical to symmetric Bruggeman | |
| from compositecalc.core.bruggeman import bruggeman as self_consistent | |
| __all__ = [ | |
| "self_consistent", | |
| "self_consistent_ellipsoid", | |
| "self_consistent_multiphase", | |
| ] | |
| # --------------------------------------------------------------------------- | |
| # Internal helpers | |
| # --------------------------------------------------------------------------- | |
| def _orient_avg_factor( | |
| P_eff: float, | |
| P_i: float, | |
| L_a: float, | |
| L_b: float, | |
| L_c: float, | |
| ) -> float: | |
| """Orientation-averaged polarizability for phase i at P_eff. | |
| Computes (1/3)·Σⱼ 1/(P_eff + Lⱼ(Pᵢ − P_eff)). | |
| This is the key building block of the ellipsoidal self-consistent | |
| equation. For spheres (L_a = L_b = L_c = 1/3) it equals | |
| 1/(P_eff + 1/3 (Pᵢ − P_eff)), the standard scalar factor. | |
| """ | |
| delta = P_i - P_eff | |
| # Each denominator P_eff + L_j·delta must be positive (guaranteed when | |
| # P_eff ∈ [min(P_m, P_f), max(P_m, P_f)] and L_j ∈ [0, 1]). | |
| s = ( | |
| 1.0 / (P_eff + L_a * delta) | |
| + 1.0 / (P_eff + L_b * delta) | |
| + 1.0 / (P_eff + L_c * delta) | |
| ) | |
| return s / 3.0 | |
| def _sc_ellipsoid_residual( | |
| P_eff: float, | |
| P_m: float, | |
| P_f: float, | |
| phi: float, | |
| L_a: float, | |
| L_b: float, | |
| L_c: float, | |
| ) -> float: | |
| """Residual of the 2-phase ellipsoidal self-consistent equation.""" | |
| term_m = (1.0 - phi) * (P_m - P_eff) * _orient_avg_factor( | |
| P_eff, P_m, L_a, L_b, L_c | |
| ) | |
| term_f = phi * (P_f - P_eff) * _orient_avg_factor( | |
| P_eff, P_f, L_a, L_b, L_c | |
| ) | |
| return term_m + term_f | |
| def _sc_multiphase_residual( | |
| P_eff: float, | |
| components: list[tuple[float, float]], | |
| L: float, | |
| ) -> float: | |
| """Residual of the N-phase isotropic self-consistent equation.""" | |
| return sum( | |
| phi_i * (P_i - P_eff) / (P_eff + L * (P_i - P_eff)) | |
| for P_i, phi_i in components | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Public API | |
| # --------------------------------------------------------------------------- | |
| def self_consistent_ellipsoid( | |
| P_m: float | np.ndarray, | |
| P_f: float | np.ndarray, | |
| phi: float | np.ndarray, | |
| L_tuple: tuple[float, float, float], | |
| ) -> float | np.ndarray: | |
| """2-phase self-consistent scheme for randomly oriented ellipsoidal inclusions. | |
| Solves the implicit equation | |
| .. math:: | |
| (1-\\phi)\\,(P_m - P^*)\\,\\frac{1}{3} | |
| \\sum_{j} \\frac{1}{P^* + L_j(P_m - P^*)} | |
| \\;+\\; | |
| \\phi\\,(P_f - P^*)\\,\\frac{1}{3} | |
| \\sum_{j} \\frac{1}{P^* + L_j(P_f - P^*)} | |
| = 0 | |
| for P* using Brent's method. Both phases are treated symmetrically | |
| with the same depolarization L-tuple. | |
| 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]. | |
| L_tuple : tuple of three floats | |
| Depolarization factors (L_a, L_b, L_c) ∈ [0, 1] each; must sum | |
| to 1 ± 1 × 10⁻⁸. Examples: sphere = (1/3, 1/3, 1/3); | |
| needle = (0, 1/2, 1/2); platelet = (1/2, 1/2, 0). | |
| Returns | |
| ------- | |
| P_eff : float or np.ndarray | |
| Effective composite property. | |
| Raises | |
| ------ | |
| ValueError | |
| If phi ∉ [0, 1]; P_m or P_f ≤ 0; any Lⱼ ∉ [0, 1]; | |
| or ΣLⱼ ≠ 1. | |
| Notes | |
| ----- | |
| * For spheres (L_a = L_b = L_c = 1/3) this is algebraically identical | |
| to :func:`self_consistent` / :func:`~compositecalc.core.bruggeman.bruggeman`. | |
| * Both phases are assigned the *same* L-tuple — appropriate for a | |
| symmetric polycrystalline model where all grains have the same shape. | |
| For a matrix-inclusion composite (matrix = spherical, filler = | |
| ellipsoidal), use :func:`~compositecalc.core.maxwell_garnett.maxwell_garnett` | |
| with an orientation-averaged effective L. | |
| * The result satisfies the phase-swap symmetry: | |
| ``self_consistent_ellipsoid(P_m, P_f, φ, L) == | |
| self_consistent_ellipsoid(P_f, P_m, 1−φ, L)``. | |
| * P* always lies in [min(P_m, P_f), max(P_m, P_f)]. | |
| References | |
| ---------- | |
| Budiansky, B. (1965). *J. Mech. Phys. Solids*, 13, 223–227. | |
| Hill, R. (1965). *J. Mech. Phys. Solids*, 13, 213–222. | |
| Examples | |
| -------- | |
| >>> # Spheres — recovers bruggeman(1, 10, 0.3) ≈ 2.261 | |
| >>> self_consistent_ellipsoid(1.0, 10.0, 0.3, (1/3, 1/3, 1/3)) | |
| 2.261... | |
| >>> # Needles — symmetric polycrystalline model with needle grains | |
| >>> self_consistent_ellipsoid(1.0, 10.0, 0.3, (0.0, 0.5, 0.5)) | |
| 1.5... | |
| """ | |
| L_a, L_b, L_c = (float(l) for l in L_tuple) | |
| if not (0.0 <= L_a <= 1.0 and 0.0 <= L_b <= 1.0 and 0.0 <= L_c <= 1.0): | |
| raise ValueError("All depolarization factors must be in [0, 1].") | |
| if abs(L_a + L_b + L_c - 1.0) > 1e-8: | |
| raise ValueError( | |
| f"L_a + L_b + L_c must equal 1 (got {L_a + L_b + L_c:.10f})." | |
| ) | |
| 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.") | |
| def _scalar(p_m: float, p_f: float, ph: float) -> float: | |
| if ph == 0.0: | |
| return p_m | |
| if ph == 1.0: | |
| return p_f | |
| if abs(p_m - p_f) < 1e-14 * max(p_m, p_f): | |
| return p_m | |
| lo, hi = min(p_m, p_f), max(p_m, p_f) | |
| return brentq( | |
| _sc_ellipsoid_residual, | |
| lo, | |
| hi, | |
| args=(p_m, p_f, ph, L_a, L_b, L_c), | |
| xtol=1e-12, | |
| ) | |
| vec = np.vectorize(_scalar) | |
| result = vec(P_m, P_f, phi) | |
| return float(result) if result.ndim == 0 else result | |
| def self_consistent_multiphase( | |
| components: list[tuple[float, float]], | |
| L: float = 1.0 / 3.0, | |
| ) -> float: | |
| """N-phase symmetric self-consistent scheme for isotropic inclusions. | |
| Solves the implicit equation | |
| .. math:: | |
| \\sum_{i=1}^{N} \\phi_i \\, | |
| \\frac{P_i - P^*}{P^* + L(P_i - P^*)} = 0 | |
| for P*, where the sum runs over all N phases. | |
| Parameters | |
| ---------- | |
| components : list of (P_i, phi_i) pairs | |
| Each element is a ``(property, volume_fraction)`` tuple. All | |
| P_i must be positive and the phi_i must sum to 1 ± 1 × 10⁻⁸. | |
| L : float, optional | |
| Depolarization factor for all phases ∈ [0, 1]. Defaults to | |
| 1/3 (spheres). | |
| Returns | |
| ------- | |
| P_eff : float | |
| Effective composite property. | |
| Raises | |
| ------ | |
| ValueError | |
| If any P_i ≤ 0; Σφᵢ ≠ 1; L ∉ [0, 1]; or fewer than 2 phases | |
| are provided. | |
| Notes | |
| ----- | |
| * For N = 2 phases this reduces to | |
| :func:`self_consistent` / :func:`~compositecalc.core.bruggeman.bruggeman`. | |
| * The root bracket [min(P_i), max(P_i)] is guaranteed to bracket the | |
| physical root: at P* = min(P_i) the sum is ≥ 0; at P* = max(P_i) | |
| the sum is ≤ 0. | |
| * If all P_i are equal the function returns that common value. | |
| References | |
| ---------- | |
| Budiansky, B. (1965). *J. Mech. Phys. Solids*, 13, 223–227. | |
| Hill, R. (1965). *J. Mech. Phys. Solids*, 13, 213–222. | |
| Examples | |
| -------- | |
| >>> # 2-phase: recovers bruggeman(1, 10, 0.3) ≈ 2.261 | |
| >>> self_consistent_multiphase([(1.0, 0.7), (10.0, 0.3)]) | |
| 2.261... | |
| >>> # 3-phase example | |
| >>> self_consistent_multiphase([(1.0, 0.5), (5.0, 0.3), (10.0, 0.2)]) | |
| 3.0... | |
| """ | |
| if len(components) < 2: | |
| raise ValueError("At least 2 phases are required.") | |
| if not (0.0 <= L <= 1.0): | |
| raise ValueError("L must be in [0, 1].") | |
| props = [] | |
| phis = [] | |
| for P_i, phi_i in components: | |
| if P_i <= 0.0: | |
| raise ValueError(f"All properties must be positive (got P_i={P_i}).") | |
| if phi_i < 0.0: | |
| raise ValueError( | |
| f"All volume fractions must be non-negative (got phi_i={phi_i})." | |
| ) | |
| props.append(float(P_i)) | |
| phis.append(float(phi_i)) | |
| phi_sum = sum(phis) | |
| if abs(phi_sum - 1.0) > 1e-8: | |
| raise ValueError( | |
| f"Volume fractions must sum to 1 (got {phi_sum:.10f})." | |
| ) | |
| # Check if all properties are equal | |
| lo, hi = min(props), max(props) | |
| if abs(hi - lo) < 1e-14 * max(hi, 1.0): | |
| return lo | |
| comps = list(zip(props, phis)) | |
| return brentq( | |
| _sc_multiphase_residual, | |
| lo, | |
| hi, | |
| args=(comps, L), | |
| xtol=1e-12, | |
| ) | |