"""
pressure_loss_correlations.py

Frictional pressure-drop correlations for two-phase condensation.

Implemented
-----------
MQTPressureDrop (evaporation)
    Moreno-Quibén & Thome (2007) with Wojtan–Ursenbacher–Thome (2005) flow map.

MacdonaldPressureDrop
    Macdonald et al. correlation.

FangPressureDrop
    Fang et al. (2025), Int. J. Refrigeration 170, 214–223.
"""

import numpy as np
from math import log10, sin, cos, sqrt, exp
from abc import ABC, abstractmethod
from .base_class_two_phase import TwoPhaseBase, g
import warnings


class PressureLossBase(TwoPhaseBase, ABC):
    """Mixin that enforces a uniform calc() interface for pressure-drop classes."""

    @abstractmethod
    def calc(self):
        """Return {'dpdl': float, 'meta': dict}."""


# =========================================================================
# MQT – Moreno-Quibén & Thome (2007)
# =========================================================================

class MQTPressureDrop(PressureLossBase):
    """
    Frictional pressure drop for evaporation – Moreno-Quibén & Thome (2007),
    with Wojtan–Ursenbacher–Thome (2005) flow map.

    Parameters
    ----------
    D     : float  – inner tube diameter [m]
    G     : float  – mass flux [kg/(m² s)]
    state : FluidState
    fluid : carbatpy Fluid object
    """

    def __init__(self, D, G, state, fluid, heat_transfer_rate=None):
        super().__init__(state, fluid, D, G)
        self.heat_transfer_rate = heat_transfer_rate
        self._calc_alpha()

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def calc(self):
        """
        Frictional pressure drop Δp_f per meter.

        Parameters (set in __init__)
        ----------
        heat_transfer_rate : float, optional  – heat flux [W/m²]; required for D/M regime

        Returns
        -------
        dict with keys: dp_fric, dpdl, regime, meta
        """
        theta_strat = self._biberg_theta_strat(self.alpha_ra)
        geom        = self._wojtan_geom_from_e_theta(self.alpha_ra, theta_strat)

        xIA = self._wojtan_xIA()

        Gwavy_x   = self._wojtan_Gwavy(geom, self.state.quality)
        e_xIA         = self._ra_void_fraction(xIA)
        theta_stratIA = self._biberg_theta_strat(e_xIA)
        geom_xIA      = self._wojtan_geom_from_e_theta(e_xIA, theta_stratIA)
        Gwavy_xIA     = self._wojtan_Gwavy(geom_xIA, xIA)

        Gstrat_x = self._wojtan_Gstrat(geom)

        regime = self._classify_wojtan(xIA, Gwavy_x, Gwavy_xIA, Gstrat_x)

        # Override to Dryout (D) or Mist (M) when heat flux is provided
        xdi = xde = None
        if self.heat_transfer_rate is not None:
            xdi, xde = self._calc_dryout_boundaries()
            if self.state.quality > xde:
                regime = "M"
            elif self.state.quality > xdi:
                regime = "D"

        u_g = self._calc_uG_mqt(self.alpha_ra)
        u_l = self._calc_uL_mqt(self.alpha_ra)

        ReG = (self.G * self.state.quality * self.D
               / (self.state_v.viscosity * self.alpha_ra))
        f_G = self._f_fanning(ReG)

        if regime in ("SW", "Slug+SW"):
            theta_dry = self._theta_dry_from_map(Gwavy_x, Gstrat_x, theta_strat)
        elif regime in ("A", "Slug+I", "D"):
            theta_dry = 0.0
        elif regime == "S":
            theta_dry = self._theta_dry_from_map(Gwavy_x, Gstrat_x, theta_strat)
        elif regime == "M":
            theta_dry = 0.0

        if regime in ("A", "Slug+I", "SW", "S", "D"):
            delta = self._film_thickness_annular(geom["A_L"], theta_dry)
        elif regime == "Slug+SW":
            delta = self._film_thickness_slug_sw(geom["A_L"], theta_dry)
        elif regime == "M":
            delta = 0.0
        else:
            raise ValueError(f"Unknown regime for δ calculation: {regime}")

        f_i = self._fi_annular(delta, u_l) if regime != "M" else 0.0

        if regime in ("SW", "Slug+SW", "S"):
            theta_hat = (theta_dry / (2.0 * np.pi)
                         if regime in ("SW", "Slug+SW")
                         else theta_strat / (2.0 * np.pi))
            ftp = theta_hat * f_G + (1.0 - theta_hat) * f_i

        dpdl_L0 = self._dpdl_single_phase_fanning()

        if regime == "A":
            dpdl = ((4.0 * f_i / max(self.D, 1e-30))
                    * (1.0 / self.state_v.sp_volume * u_g ** 2 / 2.0))

        elif regime == "SW":
            dpdl = self._calc_dp_sw(ftp, u_g)

        elif regime == "Slug+I":
            dpdl_ann = ((4.0 * f_i / max(self.D, 1e-30))
                        * (1.0 / self.state_v.sp_volume * u_g ** 2 / 2.0))
            dpdl = (dpdl_L0 * (1 - self.alpha_ra / e_xIA) ** 0.25
                    + dpdl_ann * (self.alpha_ra / e_xIA) ** 0.25)

        elif regime == "Slug+SW":
            dpdl_sw = self._calc_dp_sw(ftp, u_g)
            dpdl = (dpdl_L0 * (1 - self.alpha_ra / e_xIA) ** 0.25
                    + dpdl_sw * (self.alpha_ra / e_xIA) ** 0.25)

        elif regime == "S":
            dpdl_sw = self._calc_dp_sw(ftp, u_g)
            if self.state.quality >= xIA:
                dpdl = dpdl_sw
            else:
                dpdl_S_at_xIA = dpdl_sw
                dpdl = (dpdl_L0 * (1 - self.alpha_ra / e_xIA) ** 0.25
                        + dpdl_S_at_xIA * (self.alpha_ra / e_xIA) ** 0.25)

        elif regime == "D":
            # MQT Eq. (24): linear interpolation from two-phase (at xdi) to mist (at xde)
            dpdl_tp = ((4.0 * f_i / max(self.D, 1e-30))
                       * (1.0 / self.state_v.sp_volume * u_g ** 2 / 2.0))
            dpdl_M  = self._dpdl_mist()
            w = (self.state.quality - xdi) / max(xde - xdi, 1e-12)
            dpdl = (1.0 - w) * dpdl_tp + w * dpdl_M

        elif regime == "M":
            # MQT Eq. (23): homogeneous single-phase vapour
            dpdl = self._dpdl_mist()

        else:
            raise ValueError(f"Unknown flow regime: {regime}")

        if np.iscomplex(dpdl):
            raise ValueError("dpdl is complex")
        if np.isnan(float(dpdl)):
            raise ValueError("dpdl is NaN")

        return {
            "dpdl": float(dpdl),
            "meta": {
                "regime":          regime,
                "x":               self.state.quality,
                "xIA":             float(xIA),
                "xdi":             float(xdi) if xdi is not None else None,
                "xde":             float(xde) if xde is not None else None,
                "epsilon":         float(self.alpha_ra),
                "theta_strat_rad": float(theta_strat),
                "theta_dry_rad":   float(theta_dry),
                "delta":           float(delta),
                "f_i":             float(f_i),
                "f_G":             float(f_G),
                "dpdl_L0":         float(dpdl_L0),
                "Gwavy(x)":        float(Gwavy_x),
                "Gwavy(xIA)":      float(Gwavy_xIA),
                "Gstrat(x)":       float(Gstrat_x),
            },
        }

    # ------------------------------------------------------------------
    # Wojtan flow-map helpers
    # ------------------------------------------------------------------

    def _wojtan_xIA(self):
        """I–A boundary (vertical) – Wojtan Eq. (20)."""
        return ((0.34 ** (1 / 0.875)
                 * (self.state_l.sp_volume / self.state_v.sp_volume) ** (-1 / 1.75)
                 * (self.state_l.viscosity / self.state_v.viscosity) ** (-1 / 7))
                + 1) ** -1

    def _wojtan_Gwavy(self, geom, x):
        """SW–I/A transition (adiabatic) – Wojtan Eq. (18)."""
        AVD = geom["AVD"]
        hLD = geom["hLD"]
        Fr_L = self.G ** 2 * self.state_l.sp_volume ** 2 / (g * self.D)
        We_L = self.G ** 2 * self.D * self.state_l.sp_volume / self.sigma
        geom_term = np.sqrt(1.0 - (2.0 * hLD - 1.0) ** 2)
        bracket = (np.pi ** 2 / (25.0 * hLD ** 2)
                   * (We_L / Fr_L) ** -1 + 1.0)
        core = (16.0 * AVD ** 3 * g * self.D
                / (self.state_l.sp_volume * self.state_v.sp_volume
                   * x ** 2 * np.pi ** 2 * geom_term)
                * bracket)
        return float(np.sqrt(core) + 50.0)

    def _wojtan_Gstrat(self, geom):
        """S–SW transition – Wojtan Eq. (19)."""
        ALD, AVD = geom["ALD"], geom["AVD"]
        num = (226.32 * ALD * self.D * AVD ** 2
               / self.state_v.sp_volume
               * (1 / self.state_l.sp_volume - 1 / self.state_v.sp_volume)
               * self.state_l.viscosity * g)
        den = self.state.quality ** 2 * (1.0 - self.state.quality) * np.pi ** 3
        return float((num / den) ** (1.0 / 3.0))

    def _classify_wojtan(self, xIA, Gwavy_x, Gwavy_xIA, Gstrat_x):
        if self.state.quality >= xIA:
            return "A" if self.G >= Gwavy_x else "SW"
        else:
            if self.G > Gwavy_xIA:
                return "Slug+I"
            elif self.G > Gstrat_x:
                return "Slug+SW"
            else:
                return "S"

    # ------------------------------------------------------------------
    # MQT pressure-drop helpers
    # ------------------------------------------------------------------

    def _calc_uG_mqt(self, alpha):
        return (self.G * self.state.quality
                / (self.state_v.sp_volume ** -1 * alpha))

    def _calc_uL_mqt(self, alpha):
        return (self.G * (1 - self.state.quality)
                / (self.state_l.sp_volume ** -1 * (1 - alpha)))

    def _calc_dp_sw(self, ftp, u_g):
        return ((4.0 * ftp / self.D)
                * (1.0 / self.state_v.sp_volume * u_g ** 2 / 2.0))

    @staticmethod
    def _f_fanning(Re):
        if Re < 2000.0:
            return 16.0 / Re
        return 0.079 * Re ** -0.25

    def _film_thickness_annular(self, A_L, theta_dry):
        """MQT Eq. (8): δ = A_L / [R(2π − θ_dry)]"""
        R = 0.5 * self.D
        denom = R * max(2.0 * np.pi - theta_dry, 1e-12)
        delta = A_L / denom
        if np.isnan(delta):
            raise ValueError("film thickness is NaN")
        return delta

    def _film_thickness_slug_sw(self, A_L, theta_dry):
        """MQT Eq. (18)."""
        delta = (self.D / 2
                 - np.sqrt((self.D / 2) ** 2
                            - 2 * A_L / (2 * np.pi - theta_dry)))
        if np.isnan(delta):
            delta = 0.4999 * self.D
        return float(min(max(delta, 1e-9), 0.4999 * self.D))

    def _theta_dry_from_map(self, Gwavy_x, Gstrat_x, theta_strat):
        """MQT Eq. (12): θ_dry = [(G_wavy−G)/(G_wavy−G_strat)]^0.61 · θ_strat"""
        z = max((Gwavy_x - self.G) / (Gwavy_x - Gstrat_x), 0.0)
        return z ** 0.61 * theta_strat

    def _fi_annular(self, delta, u_l):
        """Interfacial friction factor (annular) – MQT Eq. (7)."""
        R = 0.5 * self.D
        WeL = u_l ** 2 * self.D / (self.sigma * self.state_l.sp_volume)
        term1 = (delta / (2.0 * R)) ** 1.2
        term2 = ((1 / self.state_l.sp_volume
                  - 1 / self.state_v.sp_volume)
                 * g * delta ** 2 / self.sigma) ** -0.4
        term3 = (self.state_v.viscosity / self.state_l.viscosity) ** 0.08
        term4 = max(WeL, 1e-30) ** -0.034
        return 0.67 * term1 * term2 * term3 * term4

    def _dpdl_single_phase_fanning(self):
        Re = self.G * self.D / self.state_l.viscosity
        fF = self._f_fanning(Re)
        return 2.0 * fF * self.G ** 2 * self.state_l.sp_volume / self.D

    def _calc_dryout_boundaries(self):
        """
        Dryout inception (xdi) and completion (xde) qualities.
        Uses self.heat_transfer_rate [W/m²].

        Wojtan, Ursenbacher & Thome (2005) – Part I, Eqs. (13)–(15).
        """
        warnings.warn("dryout and mist flow regime not checked yet", UserWarning)
        rho_v = 1.0 / self.state_v.sp_volume
        rho_l = 1.0 / self.state_l.sp_volume

        # Eq. (15): Kutateladze critical heat flux
        q_crit = (0.131 * rho_v ** 0.5 * self.dH_GL
                  * (g * (rho_l - rho_v) * self.sigma) ** 0.25)

        # Dimensionless groups
        We_v = self.G ** 2 * self.D / (rho_v * self.sigma)
        Fr_v = self.G ** 2 / (rho_v * (rho_l - rho_v) * g * self.D)

        q_ratio   = self.heat_transfer_rate / max(q_crit, 1e-30)
        rho_ratio = rho_v / rho_l

        # Eq. (13): dryout inception quality
        xdi = float(np.clip(
            0.58 * np.exp(0.52
                          - 0.235 * We_v ** 0.17 * Fr_v ** 0.37
                                  * rho_ratio ** 0.25 * q_ratio ** 0.70),
            0.0, 1.0))

        # Eq. (14): dryout completion quality
        xde = float(np.clip(
            0.61 * np.exp(0.57
                          - 5.8e-3 * We_v ** 0.38 * Fr_v ** 0.15
                                   * rho_ratio ** -0.09 * q_ratio ** 0.27),
            0.0, 1.0))

        # Physical constraint
        xde = max(xde, xdi)

        return xdi, xde

    def _dpdl_mist(self):
        """
        Mist-flow pressure gradient – homogeneous single-phase vapour.
        MQT Eq. (23): (dp/dL)_M = 2 f_v G² v_v / D
        """
        mu_m = self.state.quality * self.state_v.viscosity * (1 - self.state.quality) * self.state_l.viscosity #eq.23
        Re_v = self.G * self.D / mu_m #eq. 22
        f_v  = self._f_fanning(Re_v)
        eps_h = 1 / (1+(1-self.state.quality)/( self.state.quality) 
                     * self.state_l.sp_volume/self.state_v.sp_volume) #eq.21
        rho_m = 1 / self.state_l.sp_volume * (1 - eps_h) + 1 / self.state_v.sp_volume * eps_h #eq. 20
        return 2.0 * f_v * self.G ** 2 / (self.D * rho_m)


# =========================================================================
# Macdonald pressure drop
# =========================================================================

class MacdonaldPressureDrop(PressureLossBase):
    """
    Macdonald et al. two-phase frictional pressure-drop correlation.

    Parameters
    ----------
    D     : float  – inner tube diameter [m]
    G     : float  – mass flux [kg/(m² s)]
    state : FluidState
    fluid : carbatpy Fluid object
    """

    def __init__(self, D, G, state, fluid):
        super().__init__(state, fluid, D, G)
        self._calc_alpha()

    def calc(self):
        """Returns frictional pressure gradient [Pa/m]."""
        a, b, c, d = 20, -0.15, 1.15, -0.2

        u_l = self.G * (1 - self.state.quality) * self.state_l.sp_volume
        u_v = self.G * self.state.quality * self.state_v.sp_volume
        Bo = ((1 / self.state_l.sp_volume - 1 / self.state_v.sp_volume)
              * g * self.D ** 2 / self.sigma)
        S_r = (self.state_v.sp_volume / self.state_l.sp_volume
               * self.state.quality / (1 - self.state.quality)
               * (1 - self.alpha) / self.alpha)
        Re_l = self.G * (1 - self.state.quality) * self.D / self.state_l.viscosity
        Re_v = self.G * self.state.quality * self.D / self.state_v.viscosity
        C = a * Re_l ** b * S_r ** c * Bo ** d
        f_l = self._friction_factor_churchill(Re_l)
        f_v = self._friction_factor_churchill(Re_v)
        dpdl_l = 0.5 * f_l * u_l ** 2 / (self.state_l.sp_volume * self.D)
        dpdl_v = 0.5 * f_v * u_v ** 2 / (self.state_v.sp_volume * self.D)
        dpdl = dpdl_v + C * (dpdl_l * dpdl_v) ** 0.5 + dpdl_l
        return {
            "dpdl": float(dpdl),
            "meta": {
                "dpdl_v": float(dpdl_v),
                "dpdl_l": float(dpdl_l),
                "C": float(C),
            },
        }

    def _friction_factor_churchill(self, Re, e=0.000015):
        A = (2.457 * np.log(1 / ((7 / Re) ** 0.9
                                  + 0.27 * e / self.D))) ** 16
        B = (37530 / Re) ** 16
        f = ((8 / Re) ** 12 + 1 / (A + B) ** 1.5) ** (1 / 12)
        return 8 * f


# =========================================================================
# Fang et al. (2025) pressure drop
# =========================================================================

# Coefficients from Table 3 of Fang et al. (2025)
_FANG_A = {
    "a1": -0.915157847,
    "a2":  0.366828489,
    "a3": -1.176154454,
    "a4": -0.664842127,
    "a5":  5.523865127,
    "a6": -11.33590599,
    "a7":  0.588517005,
    "a8":  0.132181669,
    "a9": -0.192279121,
    "a10": 0.630825535,
    "a11": 0.04718358,
}


def _dp_single_phase_per_length(G, D, rho, mu, eps):
    """Darcy–Weisbach: Δp/L = f · G²/(2ρD)  with Haaland friction factor."""
    Re = G * D / mu
    if Re <= 0.0:
        raise ValueError("Re must be > 0")
    if Re < 2000.0:
        f = 64.0 / Re
    else:
        rel_rough = 0.0 if D == 0.0 else eps / D
        term = (rel_rough / 3.7) ** 1.11 + 6.9 / Re
        f = (-1.8 * log10(term)) ** -2
    return f * G ** 2 / (2.0 * rho * D)


class FangPressureDrop(PressureLossBase):
    """
    DISCLAIMER: ERROR IN PAPER? RESULTS IN INPLAUSIBLE PRESSURE LOSSES. CONTACTED AUTHORS.
    Frictional pressure drop for condensation – Fang et al. (2025),
    Int. J. Refrigeration 170, 214–223.

    Parameters
    ----------
    state  : FluidState
    fluid  : carbatpy Fluid object
    mdot   : float  – mass flow rate [kg/s]
    D      : float  – inner tube diameter [m]
    """

    def __init__(self, state, fluid, mdot, D, eps=0.0):
        warnings.warn("DISCLAIMER: ERROR IN Fang-PAPER? RESULTS IN INPLAUSIBLE PRESSURE LOSSES. CONTACTED AUTHORS.", UserWarning)
        self.mdot   = mdot
        self.eps    = eps
        self._g     = 9.81   # original PressureLoss uses self.g = 9.81
        A_cs = 0.25 * np.pi * D ** 2
        G    = mdot / A_cs
        super().__init__(state, fluid, D, G)

    def _calc_sigma(self):
        """
        Override: original PressureLoss does NOT apply [0] indexing for
        non-TREND fluids, so sigma stays as the raw return value (numpy array).
        This preserves the exact numpy computation path of the original.
        """
        import carbatpy as cb
        if self.fluid.props == 'TREND':
            thermo = cb.CB_DEFAULTS["Fluid_Defaults"]['THERMO_TREND']
            sigma = self.fluid.set_state([self.state.temperature, 0], "TQ",
                                         wanted=thermo + ['ST'])[-1]
            if sigma <= 0:
                print("Warning: surface tension not correctly calculated.")
        else:
            # Original intentionally omits [0] – keeps sigma as numpy array
            sigma = self.fluid.set_state([self.state.temperature, 0], "TQ",
                                         wanted='STN')
        self.sigma = sigma

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def calc(self):
        """
        Total frictional pressure drop over the pipe.
        Uses self.eps (roughness [m]) set in __init__.

        Returns
        -------
        dict with keys: dp_fric, meta
        """
        rho_l = 1.0 / self.state_l.sp_volume
        dpdl_lo = _dp_single_phase_per_length(
            self.G, self.D, rho_l, self.state_l.viscosity, self.eps)

        phi2 = float(self._calc_phi2())
        dpdl_tp_fric = phi2 * dpdl_lo

        return {
            "dpdl": float(dpdl_tp_fric),
            "meta": {
                "x":      self.state.quality,
                "G":      self.G,
                "phi2":   phi2,
                "dpdl_lo": dpdl_lo,
            },
        }

    # ------------------------------------------------------------------
    # Fang correlation internals
    # ------------------------------------------------------------------

    def _calc_phi2(self):
        eps_v     = self._void_fraction_fang()
        fi        = self._fi_interfacial(eps_v)
        u_v       = (self.G * self.state.quality
                     * self.state_v.sp_volume) / eps_v

        theta_strat = self._biberg_theta_strat(eps_v)
        geo = self._areas_from_theta(theta_strat)
        A_l, A_v, h_Ld = geo["A_l"], geo["A_v"], geo["h_Ld"]

        m_strat = self._m_strat(A_l, A_v)
        m_wavy  = self._m_wavy(A_v, h_Ld)

        if self.G >= m_wavy:
            dpdl_tp = fi * u_v ** 2 / (2.0 * self.D * self.state_v.sp_volume)
        else:
            denom = m_wavy - m_strat
            factor = (m_wavy - self.G) / denom
            theta_wavy = theta_strat * factor ** 0.5
            f_v = self._fv_darcy(eps_v)
            f_tp_wavy = ((theta_wavy / (2.0 * np.pi)) * f_v
                         + (1.0 - theta_wavy / (2.0 * np.pi)) * fi)
            dpdl_tp = f_tp_wavy * u_v ** 2 / (2.0 * self.D * self.state_v.sp_volume)

        rho_l = 1.0 / self.state_l.sp_volume
        dpdl_lo = _dp_single_phase_per_length(
            self.G, self.D, rho_l, self.state_l.viscosity, self.eps)

        return float(max(dpdl_tp / max(dpdl_lo, 1e-30), 1e-12))

    def _void_fraction_fang(self):
        """
        Eq. (3) – exact copy of original _void_fraction_wojtan to preserve
        the original expression structure and floating-point evaluation order.
        """
        _g = self._g  # original uses self.g = 9.81 (instance variable)
        denom = (
            (1.0 + 0.12 * (1.0 - self.state.quality)) *
            (self.state.quality * self.state_v.sp_volume
             + (1.0 - self.state.quality) * self.state_l.sp_volume)
            + 1.18 * (1.0 - self.state.quality)
            * ((_g * self.sigma
                * (1 / self.state_l.sp_volume
                   - 1 / self.state_v.sp_volume)) ** 0.25)
            / (self.G * ((1 / self.state_l.sp_volume) ** 0.5))
        )
        return self.state.quality * self.state_v.sp_volume / denom

    def _fi_interfacial(self, eps_v):
        """Eq. (4)+(5) – interfacial friction factor."""
        We_l = (self.G ** 2 * self.D
                * self.state_l.sp_volume / self.sigma)
        Rrho = self.state_v.sp_volume / self.state_l.sp_volume
        Rmu  = self.state_v.viscosity / self.state_l.viscosity
        a = _FANG_A
        # Computed term-by-term to match original floating-point evaluation order
        term1 = a["a1"] * Rrho ** a["a2"]
        term2 = We_l ** a["a3"]
        term3 = Rmu ** a["a4"]
        term4 = self.state.quality ** a["a5"]
        term5 = eps_v ** a["a6"]
        term6 = a["a7"] * Rrho ** a["a8"]
        term7 = We_l ** a["a9"]
        term8 = Rmu ** a["a10"]
        term9 = self.state.quality ** a["a11"]
        return term1 * term2 * term3 * term4 * term5 + term6 * term7 * term8 * term9

    def _areas_from_theta(self, theta_g):
        """Circular-segment areas for a given gas angle θ_g [rad]."""
        R = 0.5 * self.D
        A_total = np.pi * R ** 2
        A_gas   = 0.5 * R ** 2 * (theta_g - sin(theta_g))
        A_liq   = A_total - A_gas
        h_Ld    = (1.0 + cos(0.5 * theta_g)) / 2.0
        return {"A_total": A_total, "A_v": A_gas, "A_l": A_liq, "h_Ld": h_Ld}

    def _m_strat(self, A_l, A_v):
        """Eq. (9)."""
        num = (226.3 ** 2 * A_l * A_v ** 2
               / self.state_v.sp_volume
               * (1 / self.state_l.sp_volume - 1 / self.state_v.sp_volume)
               * self.state_l.viscosity * self._g)
        den = self.state.quality ** 2 * (1.0 - self.state.quality) * np.pi ** 3
        return max((num / den) ** (1.0 / 3.0) + 20.0 * self.state.quality, 0.0)

    def _m_wavy(self, A_v, h_Ld):
        """Eq. (10)."""
        We_l = (self.G ** 2 * self.D
                * self.state_l.sp_volume / self.sigma)
        Fr_l = (self.G ** 2 * self.state_l.sp_volume ** 2
                / (self._g * self.D))
        geom_term = sqrt(1.0 - (2.0 * h_Ld - 1.0) ** 2)
        bracket = (np.pi ** 2 / (25.0 * h_Ld ** 2)
                   * (We_l / Fr_l) ** -1.023 + 1.0)
        core = ((16.0 * A_v ** 3 * self._g * self.D)
                / (self.state.quality ** 2 * np.pi ** 2 * geom_term
                   * self.state_v.sp_volume * self.state_l.sp_volume)
                * bracket)
        corr = (50.0 - 75.0
                * exp(-((self.state.quality ** 2 - 0.97) ** 2)
                      / (self.state.quality * (1.0 - self.state.quality))))
        return sqrt(core) + corr

    def _fv_darcy(self, eps_v):
        """Eq. (7) – Darcy friction factor for vapour phase."""
        Re_v = (self.G * self.state.quality * self.D
                / (self.state_v.viscosity * eps_v))
        return 4.0 * 0.079 * Re_v ** -0.25
