Skip to content

Equations of state

The constructors on this page create native PyTorch models. Construct them after calling configure so parameter tensors inherit the intended dtype and device.

torch_flash.eos

Equation-of-state implementations.

DEFAULT_LEACHMAN_NORMAL_HYDROGEN module-attribute

DEFAULT_LEACHMAN_NORMAL_HYDROGEN = (
    "pure-helmholtz.leachman-2009-normal-hydrogen"
)

Bundled parameter identifier for the Leachman normal-hydrogen EOS.

EOSCG2021_COMPONENTS module-attribute

EOSCG2021_COMPONENTS = (
    "carbon_dioxide",
    "water",
    "nitrogen",
    "oxygen",
    "argon",
    "carbon_monoxide",
    "hydrogen",
    "methane",
    "hydrogen_sulfide",
    "sulfur_dioxide",
    "mea",
    "dea",
    "hydrogen_chloride",
    "chlorine",
    "ammonia",
    "mdea",
)

Canonical 16-component inventory supported by bundled EOS-CG-2021.

GERG2008_COMPONENTS module-attribute

GERG2008_COMPONENTS = (
    "methane",
    "nitrogen",
    "carbon_dioxide",
    "ethane",
    "propane",
    "n_butane",
    "isobutane",
    "n_pentane",
    "isopentane",
    "n_hexane",
    "n_heptane",
    "n_octane",
    "hydrogen",
    "oxygen",
    "carbon_monoxide",
    "water",
    "helium",
    "argon",
    "hydrogen_sulfide",
    "n_nonane",
    "n_decane",
)

Canonical 21-component inventory supported by bundled GERG-2008.

GERG2008_HYDROGEN_2021_COMPONENTS module-attribute

GERG2008_HYDROGEN_2021_COMPONENTS = (
    "methane",
    "nitrogen",
    "carbon_monoxide",
    "carbon_dioxide",
    "hydrogen",
)

Canonical inventory of the Beckmüller et al. H2-tailored GERG model.

CPAEOS

CPAEOS(
    parameters: tuple[CPAComponent, ...],
    *,
    kij: Tensor | None = None,
    kij_a: Tensor | None = None,
    kij_b: Tensor | None = None,
    lij: Tensor | None = None,
    cross_association_energy: Tensor | None = None,
    cross_association_volume: Tensor | None = None,
    combining_rule: CombiningRule = "ECR",
    trainable: bool = False,
    trainable_lij: bool = False,
    association_iterations: int = 10,
    association_newton_iterations: int = 8,
    dtype: dtype | None = None,
    device: device | str | None = None,
)

Bases: Module

SRK cubic-plus-association mixture model.

Parameters:

Name Type Description Default
parameters tuple[CPAComponent, ...]

Ordered pure-component CPA records. Component order defines every composition and interaction tensor axis.

required
kij Tensor | None

Constant attraction interactions or the temperature-dependent form kij(T) = kij_a + kij_b/T.

None
kij_a Tensor | None

Constant attraction interactions or the temperature-dependent form kij(T) = kij_a + kij_b/T.

None
kij_b Tensor | None

Constant attraction interactions or the temperature-dependent form kij(T) = kij_a + kij_b/T.

None
lij Tensor | None

Optional dimensionless SRK cross-covolume interactions.

None
cross_association_energy Tensor | None

Optional symmetric pair overrides in J/mol and dimensionless association volume. Both matrices must be supplied together.

None
cross_association_volume Tensor | None

Optional symmetric pair overrides in J/mol and dimensionless association volume. Both matrices must be supplied together.

None
combining_rule CombiningRule

Published "CR1" or "ECR" cross-association rule.

'ECR'
trainable bool

Register attraction/association or covolume interaction tensors as trainable PyTorch parameters.

False
trainable_lij bool

Register attraction/association or covolume interaction tensors as trainable PyTorch parameters.

False
association_iterations int

Fixed-point and Newton passes used for association site fractions.

10
association_newton_iterations int

Fixed-point and Newton passes used for association site fractions.

10
dtype dtype | None

Tensor placement, defaulting to runtime configuration.

None
device dtype | None

Tensor placement, defaulting to runtime configuration.

None
Notes

The current volume-root and fugacity path accepts one scalar TP state. Site-fraction calculations support leading batches. Association non-convergence raises :class:~torch_flash.exceptions.ConvergenceError; it is not returned as a valid thermodynamic state.

Source code in src/torch_flash/eos/cpa.py
def __init__(
    self,
    parameters: tuple[CPAComponent, ...],
    *,
    kij: Tensor | None = None,
    kij_a: Tensor | None = None,
    kij_b: Tensor | None = None,
    lij: Tensor | None = None,
    cross_association_energy: Tensor | None = None,
    cross_association_volume: Tensor | None = None,
    combining_rule: CombiningRule = "ECR",
    trainable: bool = False,
    trainable_lij: bool = False,
    association_iterations: int = 10,
    association_newton_iterations: int = 8,
    dtype: torch.dtype | None = None,
    device: torch.device | str | None = None,
) -> None:
    super().__init__()
    if not parameters:
        raise ValueError("CPA requires at least one component")
    if combining_rule not in ("CR1", "ECR"):
        raise ValueError(f"unknown CPA combining rule {combining_rule!r}")
    if association_iterations < 0 or association_newton_iterations < 0:
        raise ValueError("CPA association iteration counts must be nonnegative")
    if kij is not None and (kij_a is not None or kij_b is not None):
        raise ValueError("pass either fixed kij or temperature-dependent kij A/B, not both")
    if (kij_a is None) != (kij_b is None):
        raise ValueError("temperature-dependent CPA interactions require both kij_a and kij_b")
    if (cross_association_energy is None) != (cross_association_volume is None):
        raise ValueError("cross-association overrides require both energy and volume matrices")
    self.names = tuple(item.name for item in parameters)
    self.combining_rule = combining_rule
    self.association_iterations = association_iterations
    self.association_newton_iterations = association_newton_iterations
    supplied_tensors = tuple(
        value
        for value in (
            kij,
            kij_a,
            kij_b,
            lij,
            cross_association_energy,
            cross_association_volume,
        )
        if value is not None
    )
    runtime_dtype, runtime_device = resolve_tensor_options(dtype, device)
    dtype = supplied_tensors[0].dtype if dtype is None and supplied_tensors else runtime_dtype
    device = (
        supplied_tensors[0].device if device is None and supplied_tensors else runtime_device
    )

    def tensor(values: list[float]) -> Tensor:
        return torch.tensor(values, dtype=dtype, device=device)

    numeric_rows = (
        (
            item.critical_temperature,
            item.a0,
            item.b,
            item.c1,
            item.association_energy,
            item.association_volume,
        )
        for item in parameters
    )
    if any(
        not all(torch.isfinite(torch.tensor(row)))
        or row[0] <= 0.0
        or row[1] <= 0.0
        or row[2] <= 0.0
        or row[4] < 0.0
        or row[5] < 0.0
        for row in numeric_rows
    ):
        raise ValueError(
            "CPA temperatures, a0, and b must be positive; association parameters "
            "must be finite and nonnegative"
        )
    self.register_buffer(
        "critical_temperature",
        tensor([item.critical_temperature for item in parameters]),
    )
    fitted_parameters = {
        "a0": tensor([item.a0 for item in parameters]),
        "b": tensor([item.b for item in parameters]),
        "c1": tensor([item.c1 for item in parameters]),
        "association_energy": tensor([item.association_energy for item in parameters]),
        "association_volume": tensor([item.association_volume for item in parameters]),
    }
    for name, value in fitted_parameters.items():
        if trainable:
            setattr(self, name, nn.Parameter(value))
        else:
            self.register_buffer(name, value)
    self.register_buffer(
        "site_types",
        torch.tensor(
            [_site_types(item.scheme) for item in parameters],
            dtype=torch.int64,
            device=device,
        ),
    )

    critical_pressure: list[float] = []
    acentric_factor: list[float] = []
    molar_mass: list[float] = []
    for item in parameters:
        if (
            item.critical_pressure is None
            or item.acentric_factor is None
            or item.molar_mass is None
        ):
            try:
                shared = component(item.name)
            except KeyError as exc:
                raise ParameterDatabaseError(
                    f"custom CPA component {item.name!r} must provide critical_pressure, "
                    "acentric_factor, and molar_mass"
                ) from exc
            shared_pressure = shared.critical_pressure
            shared_acentric = shared.acentric_factor
            shared_mass = shared.molar_mass
        else:
            shared_pressure = item.critical_pressure
            shared_acentric = item.acentric_factor
            shared_mass = item.molar_mass
        resolved_pressure = (
            item.critical_pressure if item.critical_pressure is not None else shared_pressure
        )
        resolved_acentric = (
            item.acentric_factor if item.acentric_factor is not None else shared_acentric
        )
        resolved_mass = item.molar_mass if item.molar_mass is not None else shared_mass
        if resolved_acentric is None:
            raise ParameterDatabaseError(
                f"CPA component {item.name!r} requires an acentric factor"
            )
        if (
            not torch.isfinite(torch.tensor(resolved_pressure))
            or not torch.isfinite(torch.tensor(resolved_acentric))
            or not torch.isfinite(torch.tensor(resolved_mass))
            or resolved_pressure <= 0.0
            or resolved_mass <= 0.0
        ):
            raise ValueError("CPA critical pressure and molar mass must be finite and positive")
        critical_pressure.append(float(resolved_pressure))
        acentric_factor.append(float(resolved_acentric))
        molar_mass.append(float(resolved_mass))
    self.register_buffer("critical_pressure", tensor(critical_pressure))
    self.register_buffer("acentric_factor", tensor(acentric_factor))
    self.register_buffer("molar_mass", tensor(molar_mass))

    size = len(parameters)
    if kij_a is not None and kij_b is not None:
        self.mixing = TemperatureDependentQuadraticMixing(
            kij_a.to(dtype=dtype, device=device),
            kij_b.to(dtype=dtype, device=device),
            None if lij is None else lij.to(dtype=dtype, device=device),
            trainable=trainable,
            trainable_lij=trainable_lij,
        )
    else:
        fixed_kij = (
            torch.zeros((size, size), dtype=dtype, device=device)
            if kij is None
            else kij.to(dtype=dtype, device=device)
        )
        self.mixing = QuadraticMixing(
            fixed_kij,
            None if lij is None else lij.to(dtype=dtype, device=device),
            trainable=trainable,
            trainable_lij=trainable_lij,
        )

    if cross_association_energy is None or cross_association_volume is None:
        cross_mask = torch.zeros((size, size), dtype=torch.bool, device=device)
        cross_energy = torch.zeros((size, size), dtype=dtype, device=device)
        cross_volume = torch.zeros((size, size), dtype=dtype, device=device)
    else:
        supplied_energy = cross_association_energy.to(dtype=dtype, device=device)
        supplied_volume = cross_association_volume.to(dtype=dtype, device=device)
        if supplied_energy.shape != (size, size) or supplied_volume.shape != (size, size):
            raise ValueError("cross-association matrices must match the CPA component count")
        energy_mask = torch.isfinite(supplied_energy)
        volume_mask = torch.isfinite(supplied_volume)
        if not torch.equal(energy_mask, volume_mask):
            raise ValueError("cross-association energy and volume masks must match")
        if not torch.equal(energy_mask, energy_mask.mT):
            raise ValueError("cross-association overrides must be symmetric")
        if bool(torch.diagonal(energy_mask).any()):
            raise ValueError("cross-association overrides cannot replace pure-component pairs")
        cross_mask = energy_mask
        cross_energy = torch.where(
            energy_mask, supplied_energy, torch.zeros_like(supplied_energy)
        )
        cross_volume = torch.where(
            volume_mask, supplied_volume, torch.zeros_like(supplied_volume)
        )
        if (
            not torch.allclose(cross_energy, cross_energy.mT)
            or not torch.allclose(cross_volume, cross_volume.mT)
            or bool((cross_energy[cross_mask] <= 0.0).any())
            or bool((cross_volume[cross_mask] <= 0.0).any())
        ):
            raise ValueError(
                "cross-association energies and volumes must be symmetric and positive"
            )
    self.register_buffer("cross_association_mask", cross_mask)
    if trainable:
        self.cross_association_energy = nn.Parameter(cross_energy)
        self.cross_association_volume = nn.Parameter(cross_volume)
    else:
        self.register_buffer("cross_association_energy", cross_energy)
        self.register_buffer("cross_association_volume", cross_volume)

ncomponents property

ncomponents: int

Return the number of modeled components.

Returns:

Type Description
int

Length of the canonical component-name tuple.

pure_parameters

pure_parameters(
    temperature: Tensor,
) -> tuple[Tensor, Tensor]

Evaluate CPA's fitted pure SRK parameters.

Parameters:

Name Type Description Default
temperature Tensor

Temperature in K.

required

Returns:

Type Description
tuple

Attraction a_i(T) in Pa m6/mol2 and covolume b_i in m3/mol, with a final component axis.

Source code in src/torch_flash/eos/cpa.py
def pure_parameters(self, temperature: Tensor) -> tuple[Tensor, Tensor]:
    """Evaluate CPA's fitted pure SRK parameters.

    Parameters
    ----------
    temperature
        Temperature in K.

    Returns
    -------
    tuple
        Attraction ``a_i(T)`` in Pa m6/mol2 and covolume ``b_i`` in
        m3/mol, with a final component axis.
    """
    reduced = temperature[..., None] / self.critical_temperature
    a = self.a0 * (1.0 + self.c1 * (1.0 - torch.sqrt(reduced))).square()
    return a, self.b

mixture_parameters

mixture_parameters(
    temperature: Tensor, composition: Tensor
) -> tuple[Tensor, Tensor]

Mix physical-term attraction and covolume parameters.

Parameters:

Name Type Description Default
temperature Tensor

Temperature in K.

required
composition Tensor

Component mole fractions on the final axis.

required

Returns:

Type Description
tuple

Mixture a in Pa m6/mol2 and b in m3/mol.

Source code in src/torch_flash/eos/cpa.py
def mixture_parameters(self, temperature: Tensor, composition: Tensor) -> tuple[Tensor, Tensor]:
    """Mix physical-term attraction and covolume parameters.

    Parameters
    ----------
    temperature
        Temperature in K.
    composition
        Component mole fractions on the final axis.

    Returns
    -------
    tuple
        Mixture ``a`` in Pa m6/mol2 and ``b`` in m3/mol.
    """
    pure_a, pure_b = self.pure_parameters(temperature)
    result: tuple[Tensor, Tensor] = self.mixing(temperature, composition, pure_a, pure_b)
    return result

binary_interaction

binary_interaction(temperature: Tensor) -> Tensor

Evaluate the symmetric physical-term attraction interactions.

Parameters:

Name Type Description Default
temperature Tensor

Temperature in K.

required

Returns:

Type Description
Tensor

Dimensionless kij matrix, with temperature batch dimensions when temperature-dependent interactions are active.

Source code in src/torch_flash/eos/cpa.py
def binary_interaction(self, temperature: Tensor) -> Tensor:
    """Evaluate the symmetric physical-term attraction interactions.

    Parameters
    ----------
    temperature
        Temperature in K.

    Returns
    -------
    Tensor
        Dimensionless ``kij`` matrix, with temperature batch dimensions
        when temperature-dependent interactions are active.
    """
    if isinstance(self.mixing, TemperatureDependentQuadraticMixing):
        return self.mixing.kij(temperature)
    return self.mixing.kij

association_strength

association_strength(
    temperature: Tensor,
    molar_density: Tensor,
    composition: Tensor,
) -> Tensor

Evaluate component-site association strengths.

Parameters:

Name Type Description Default
temperature Tensor

Temperature in K.

required
molar_density Tensor

Homogeneous molar density in mol/m3.

required
composition Tensor

Mole fractions on the final component axis.

required

Returns:

Type Description
Tensor

Association strengths in m3/mol with trailing axes (component_i, site_A, component_j, site_B).

Source code in src/torch_flash/eos/cpa.py
def association_strength(
    self, temperature: Tensor, molar_density: Tensor, composition: Tensor
) -> Tensor:
    """Evaluate component-site association strengths.

    Parameters
    ----------
    temperature
        Temperature in K.
    molar_density
        Homogeneous molar density in mol/m3.
    composition
        Mole fractions on the final component axis.

    Returns
    -------
    Tensor
        Association strengths in m3/mol with trailing axes
        ``(component_i, site_A, component_j, site_B)``.
    """
    x = normalize_composition(composition)
    _, bm = self.mixture_parameters(temperature, x)
    packing_fraction = 0.25 * bm * molar_density
    radial_distribution = 1.0 / (1.0 - 1.9 * packing_fraction)
    bij = 0.5 * (self.b[:, None] + self.b[None, :])
    temperature_pair = temperature[..., None, None]
    if self.combining_rule == "CR1":
        epsilon = 0.5 * (self.association_energy[:, None] + self.association_energy[None, :])
        beta = torch.sqrt(self.association_volume[:, None] * self.association_volume[None, :])
        pair_strength = (
            radial_distribution[..., None, None]
            * torch.expm1(epsilon / (R * temperature_pair))
            * bij
            * beta
        )
    elif self.combining_rule == "ECR":
        pure_factor = (
            torch.expm1(self.association_energy / (R * temperature[..., None]))
            * self.b
            * self.association_volume
        )
        pair_strength = radial_distribution[..., None, None] * torch.sqrt(
            pure_factor[..., :, None] * pure_factor[..., None, :]
        )
    else:  # pragma: no cover - constructor validation protects this branch
        raise ValueError(f"unknown CPA combining rule {self.combining_rule!r}")

    # Modified CR-1 solvation overrides use the published pair energy and
    # volume directly in Delta = g [exp(epsilon/RT)-1] bij beta.
    override_strength = (
        radial_distribution[..., None, None]
        * torch.expm1(self.cross_association_energy / (R * temperature_pair))
        * bij
        * self.cross_association_volume
    )
    pair_strength = torch.where(
        self.cross_association_mask,
        override_strength,
        pair_strength,
    )
    types_i = self.site_types[:, :, None, None]
    types_j = self.site_types[None, None, :, :]
    compatible = (types_i >= 0) & (types_j >= 0) & (types_i != types_j)
    return pair_strength[..., :, None, :, None] * compatible

site_fractions

site_fractions(
    temperature: Tensor,
    molar_density: Tensor,
    composition: Tensor,
) -> Tensor

Solve the CPA mass-action equations for unbonded site fractions.

Parameters:

Name Type Description Default
temperature Tensor

Temperature in K.

required
molar_density Tensor

Homogeneous molar density in mol/m3.

required
composition Tensor

Mole fractions with a final component axis.

required

Returns:

Type Description
Tensor

Dimensionless unbonded fractions with trailing axes (ncomponents, nsites). Inactive site slots equal one.

Raises:

Type Description
ValueError

If the composition component count is inconsistent.

ConvergenceError

If the configured fixed-point/Newton iterations do not solve the mass-action residual.

Notes

Leading dimensions are broadcast as independent homogeneous states. The component axis is the final composition dimension.

Source code in src/torch_flash/eos/cpa.py
def site_fractions(
    self, temperature: Tensor, molar_density: Tensor, composition: Tensor
) -> Tensor:
    """Solve the CPA mass-action equations for unbonded site fractions.

    Parameters
    ----------
    temperature
        Temperature in K.
    molar_density
        Homogeneous molar density in mol/m3.
    composition
        Mole fractions with a final component axis.

    Returns
    -------
    Tensor
        Dimensionless unbonded fractions with trailing axes
        ``(ncomponents, nsites)``. Inactive site slots equal one.

    Raises
    ------
    ValueError
        If the composition component count is inconsistent.
    ConvergenceError
        If the configured fixed-point/Newton iterations do not solve the
        mass-action residual.

    Notes
    -----
    Leading dimensions are broadcast as independent homogeneous states.
    The component axis is the final composition dimension.
    """
    x = normalize_composition(composition)
    if x.shape[-1] != self.ncomponents:
        raise ValueError("CPA composition has the wrong number of components")
    batch_shape = torch.broadcast_shapes(
        temperature.shape,
        molar_density.shape,
        x.shape[:-1],
    )
    temperature = torch.broadcast_to(temperature, batch_shape)
    molar_density = torch.broadcast_to(molar_density, batch_shape)
    x = torch.broadcast_to(x, (*batch_shape, self.ncomponents))
    strength = self.association_strength(temperature, molar_density, x)
    active = self.site_types >= 0
    site_fraction = torch.ones(
        (*batch_shape, self.ncomponents, self.site_types.shape[-1]),
        dtype=x.dtype,
        device=x.device,
    )
    for _ in range(self.association_iterations):
        bonded_sum = torch.einsum(
            "...j,...jb,...iajb->...ia",
            x,
            site_fraction,
            strength,
        )
        update = 1.0 / (1.0 + molar_density[..., None, None] * bonded_sum)
        update = torch.where(active, update, torch.ones_like(update))
        site_fraction = 0.5 * site_fraction + 0.5 * update

    # Newton refinement uses the analytic mass-action Jacobian. A
    # positivity-limited step replaces dozens of slowly convergent Picard
    # iterations in strongly associating liquids while retaining a fixed,
    # differentiable workload suitable for torch.compile and accelerators.
    nsites = self.site_types.shape[-1]
    system_size = self.ncomponents * nsites
    flattened_strength = strength.reshape(*batch_shape, system_size, system_size)
    site_weights = (
        x[..., :, None]
        .expand(*batch_shape, self.ncomponents, nsites)
        .reshape(*batch_shape, system_size)
    )
    flattened_active = active.reshape(system_size)
    identity = torch.eye(system_size, dtype=x.dtype, device=x.device)
    for _ in range(self.association_newton_iterations):
        flattened_sites = site_fraction.reshape(*batch_shape, system_size)
        bonded_sum = torch.einsum(
            "...j,...jb,...iajb->...ia",
            x,
            site_fraction,
            strength,
        ).reshape(*batch_shape, system_size)
        residual = flattened_sites.reciprocal() - 1.0 - molar_density[..., None] * bonded_sum
        jacobian = -torch.diag_embed(flattened_sites.reciprocal().square())
        jacobian = jacobian - (
            molar_density[..., None, None] * flattened_strength * site_weights[..., None, :]
        )
        residual = torch.where(flattened_active, residual, flattened_sites - 1.0)
        jacobian = torch.where(flattened_active[:, None], jacobian, identity)
        step = torch.linalg.solve(jacobian, -residual)
        decreases_site_fraction = (step < 0.0) & flattened_active
        safe_step = torch.where(
            decreases_site_fraction,
            step,
            -torch.ones_like(step),
        )
        positivity_limits = torch.where(
            decreases_site_fraction,
            -0.9 * flattened_sites / safe_step,
            torch.full_like(step, torch.inf),
        )
        step_scale = torch.minimum(
            torch.ones_like(positivity_limits[..., 0]),
            positivity_limits.amin(dim=-1),
        )
        site_fraction = (flattened_sites + step_scale[..., None] * step).reshape(
            *batch_shape, self.ncomponents, nsites
        )
        site_fraction = torch.where(active, site_fraction, torch.ones_like(site_fraction))
    return site_fraction

residual_helmholtz_rt

residual_helmholtz_rt(
    temperature: Tensor, volume: Tensor, moles: Tensor
) -> Tensor

Return extensive reduced residual Helmholtz energy.

Parameters:

Name Type Description Default
temperature Tensor

Scalar temperature in K.

required
volume Tensor

Scalar total volume in m3.

required
moles Tensor

Component amounts in mol.

required

Returns:

Type Description
Tensor

Dimensionless extensive A^R/(RT) for physical and association contributions.

Source code in src/torch_flash/eos/cpa.py
def residual_helmholtz_rt(self, temperature: Tensor, volume: Tensor, moles: Tensor) -> Tensor:
    """Return extensive reduced residual Helmholtz energy.

    Parameters
    ----------
    temperature
        Scalar temperature in K.
    volume
        Scalar total volume in m3.
    moles
        Component amounts in mol.

    Returns
    -------
    Tensor
        Dimensionless extensive ``A^R/(RT)`` for physical and association
        contributions.
    """
    if moles.ndim != 1 or volume.ndim != 0:
        raise ValueError("CPA Helmholtz kernel currently accepts one homogeneous state")
    total = moles.sum()
    x = moles / total
    molar_volume = volume / total
    am, bm = self.mixture_parameters(temperature, x)
    if bool(molar_volume <= bm):
        raise InvalidStateError("CPA molar volume must exceed mixture covolume")
    physical = -total * torch.log1p(-bm / molar_volume)
    physical = physical - total * am / (R * temperature * bm) * torch.log(
        (molar_volume + bm) / molar_volume
    )
    site_fraction = self.site_fractions(temperature, molar_volume.reciprocal(), x)
    active = self.site_types >= 0
    association_terms = torch.where(
        active,
        torch.log(site_fraction) - 0.5 * site_fraction + 0.5,
        torch.zeros_like(site_fraction),
    )
    association = torch.sum(moles[:, None] * association_terms)
    return physical + association

pressure

pressure(
    temperature: Tensor,
    molar_volume: Tensor,
    composition: Tensor,
) -> Tensor

Evaluate CPA pressure at a homogeneous T-v-x state.

Parameters:

Name Type Description Default
temperature Tensor

Temperature in K.

required
molar_volume Tensor

Molar volume in m3/mol.

required
composition Tensor

Mole fractions on the final axis.

required

Returns:

Type Description
Tensor

Pressure in Pa.

Source code in src/torch_flash/eos/cpa.py
def pressure(self, temperature: Tensor, molar_volume: Tensor, composition: Tensor) -> Tensor:
    """Evaluate CPA pressure at a homogeneous ``T-v-x`` state.

    Parameters
    ----------
    temperature
        Temperature in K.
    molar_volume
        Molar volume in m3/mol.
    composition
        Mole fractions on the final axis.

    Returns
    -------
    Tensor
        Pressure in Pa.
    """
    x = normalize_composition(composition)
    am, bm = self.mixture_parameters(temperature, x)
    density = molar_volume.reciprocal()
    sites = self.site_fractions(temperature, density, x)
    active = self.site_types >= 0
    unbonded = torch.where(active, 1.0 - sites, torch.zeros_like(sites))
    association_sum = torch.sum(x[..., :, None] * unbonded, dim=(-2, -1))
    packing_fraction = 0.25 * bm * density
    density_dlogg = 1.9 * packing_fraction / (1.0 - 1.9 * packing_fraction)
    physical = R * temperature / (molar_volume - bm) - am / (molar_volume * (molar_volume + bm))
    association = (
        -0.5 * R * temperature / molar_volume * (1.0 + density_dlogg) * association_sum
    )
    return physical + association

molar_volume

molar_volume(
    temperature: Tensor,
    pressure: Tensor,
    composition: Tensor,
    phase: PhaseKind = "stable",
) -> Tensor

Solve a scalar CPA molar-volume root.

Parameters:

Name Type Description Default
temperature Tensor

Scalar temperature in K and pressure in Pa.

required
pressure Tensor

Scalar temperature in K and pressure in Pa.

required
composition Tensor

One component mole-fraction vector.

required
phase PhaseKind

Smallest root ("liquid"), largest root ("vapor"), or minimum-Gibbs admissible root ("stable").

'stable'

Returns:

Type Description
Tensor

Molar volume in m3/mol.

Raises:

Type Description
ValueError

If the state is batched or phase is unknown.

ConvergenceError

If the pressure-root scan finds no admissible root.

Source code in src/torch_flash/eos/cpa.py
def molar_volume(
    self,
    temperature: Tensor,
    pressure: Tensor,
    composition: Tensor,
    phase: PhaseKind = "stable",
) -> Tensor:
    """Solve a scalar CPA molar-volume root.

    Parameters
    ----------
    temperature, pressure
        Scalar temperature in K and pressure in Pa.
    composition
        One component mole-fraction vector.
    phase
        Smallest root (``"liquid"``), largest root (``"vapor"``), or
        minimum-Gibbs admissible root (``"stable"``).

    Returns
    -------
    Tensor
        Molar volume in m3/mol.

    Raises
    ------
    ValueError
        If the state is batched or ``phase`` is unknown.
    ConvergenceError
        If the pressure-root scan finds no admissible root.
    """
    if temperature.ndim != 0 or pressure.ndim != 0 or composition.ndim != 1:
        raise ValueError("CPA volume solver currently accepts one scalar T-P state")
    x = normalize_composition(composition)
    if phase in ("liquid", "vapor"):
        fast_volume = self._phase_volume_newton(
            temperature,
            pressure,
            x,
            phase,
        )
        if fast_volume is not None:
            return fast_volume
    roots = self._volume_roots(temperature, pressure, x)
    if phase == "liquid":
        return roots[0]
    if phase == "vapor":
        return roots[-1]
    if phase != "stable":
        raise ValueError(f"unknown phase root {phase!r}")

    gibbs: list[Tensor] = []
    for volume in roots:
        z = pressure * volume / (R * temperature)
        residual_helmholtz = self.residual_helmholtz_rt(temperature, volume, x)
        gibbs.append(residual_helmholtz + z - 1.0 - torch.log(z))
    index = int(torch.argmin(torch.stack(gibbs)).detach())
    return roots[index]

select_z

select_z(
    temperature: Tensor,
    pressure: Tensor,
    composition: Tensor,
    phase: PhaseKind = "stable",
) -> Tensor

Return the CPA compressibility factor for a selected root.

Parameters:

Name Type Description Default
temperature Tensor

Scalar temperature in K and pressure in Pa.

required
pressure Tensor

Scalar temperature in K and pressure in Pa.

required
composition Tensor

One mole-fraction vector.

required
phase PhaseKind

Root-selection request.

'stable'

Returns:

Type Description
Tensor

Dimensionless Z = Pv/(RT).

Source code in src/torch_flash/eos/cpa.py
def select_z(
    self,
    temperature: Tensor,
    pressure: Tensor,
    composition: Tensor,
    phase: PhaseKind = "stable",
) -> Tensor:
    """Return the CPA compressibility factor for a selected root.

    Parameters
    ----------
    temperature, pressure
        Scalar temperature in K and pressure in Pa.
    composition
        One mole-fraction vector.
    phase
        Root-selection request.

    Returns
    -------
    Tensor
        Dimensionless ``Z = Pv/(RT)``.
    """
    volume = self.molar_volume(temperature, pressure, composition, phase)
    return pressure * volume / (R * temperature)

log_fugacity_coefficients

log_fugacity_coefficients(
    temperature: Tensor,
    pressure: Tensor,
    composition: Tensor,
    phase: PhaseKind = "stable",
) -> Tensor

Return CPA component log fugacity coefficients.

Parameters:

Name Type Description Default
temperature Tensor

Scalar temperature in K and pressure in Pa.

required
pressure Tensor

Scalar temperature in K and pressure in Pa.

required
composition Tensor

One mole-fraction vector.

required
phase PhaseKind

Root-selection request.

'stable'

Returns:

Type Description
Tensor

Dimensionless ln(phi_i) in component order.

Notes

Component residual chemical potentials are obtained by differentiating the extensive CPA residual Helmholtz energy at fixed volume.

Source code in src/torch_flash/eos/cpa.py
def log_fugacity_coefficients(
    self,
    temperature: Tensor,
    pressure: Tensor,
    composition: Tensor,
    phase: PhaseKind = "stable",
) -> Tensor:
    """Return CPA component log fugacity coefficients.

    Parameters
    ----------
    temperature, pressure
        Scalar temperature in K and pressure in Pa.
    composition
        One mole-fraction vector.
    phase
        Root-selection request.

    Returns
    -------
    Tensor
        Dimensionless ``ln(phi_i)`` in component order.

    Notes
    -----
    Component residual chemical potentials are obtained by differentiating
    the extensive CPA residual Helmholtz energy at fixed volume.
    """
    x = normalize_composition(composition)
    volume = self.molar_volume(temperature, pressure, x, phase)

    def at_fixed_volume(moles: Tensor) -> Tensor:
        return self.residual_helmholtz_rt(temperature, volume, moles)

    residual_mu_rt: Tensor = torch.func.grad(at_fixed_volume)(x)
    z = pressure * volume / (R * temperature)
    return residual_mu_rt - torch.log(z)

CPAComponent dataclass

CPAComponent(
    name: str,
    critical_temperature: float,
    a0: float,
    b: float,
    c1: float,
    association_energy: float = 0.0,
    association_volume: float = 0.0,
    scheme: AssociationScheme = "none",
    critical_pressure: float | None = None,
    acentric_factor: float | None = None,
    molar_mass: float | None = None,
)

Pure-component CPA parameters and optional petroleum pseudo-properties.

critical_pressure, acentric_factor, and molar_mass normally come from the shared component database. Petroleum pseudo-components can instead carry those three initialization properties directly.

Attributes:

Name Type Description
name str

Canonical or pseudo-component identifier.

critical_temperature float

Critical temperature in K.

a0 float

CPA/SRK attraction parameter in Pa m6/mol2.

b float

CPA/SRK covolume in m3/mol.

c1 float

Dimensionless CPA alpha-function coefficient.

association_energy float

Pure association energy in J/mol.

association_volume float

Dimensionless pure association volume.

scheme AssociationScheme

Association-site scheme.

critical_pressure float | None

Optional critical pressure in Pa for initialization.

acentric_factor float | None

Optional dimensionless acentric factor for initialization.

molar_mass float | None

Optional molar mass in kg/mol.

CPACharacterizedComponents dataclass

CPACharacterizedComponents(
    components: tuple[CPAComponent, ...],
    mole_fractions: Tensor,
    monomer_properties: tuple[CPAMonomerProperties, ...],
)

CPA pseudo-components created from analyzed heavy-end cuts.

Attributes:

Name Type Description
components tuple[CPAComponent, ...]

Non-associating CPA component records in input-cut order.

mole_fractions Tensor

Normalized internal cut fractions on the requested dtype and device.

monomer_properties tuple[CPAMonomerProperties, ...]

Full characterization diagnostics corresponding to components.

CPAHeavyEndCorrelations dataclass

CPAHeavyEndCorrelations(
    normal_boiling_pressure: float,
    srk_omega_a: float,
    srk_omega_b: float,
    srk_m: tuple[float, float, float],
    normal_alkane_temperature: tuple[float, float, float],
    log_normal_alkane_pressure_bar: tuple[
        float, float, float, float, float
    ],
    normal_alkane_acentric: tuple[float, float, float],
    normal_alkane_specific_gravity: tuple[
        float, float, float, float, float
    ],
    critical_temperature_numerator: tuple[
        float, float, float
    ],
    critical_temperature_denominator: tuple[
        float, float, float
    ],
    log_critical_pressure_ratio: tuple[
        float, float, float, float, float
    ],
)

Published coefficients used by the CPA C7+ monomer correlations.

Attributes:

Name Type Description
normal_boiling_pressure float

Normal boiling pressure in Pa.

srk_omega_a, srk_omega_b, srk_m

SRK critical constraints and acentric-factor polynomial coefficients.

normal_alkane_temperature, log_normal_alkane_pressure_bar

Coefficients for the reference normal-alkane vapor-pressure mapping.

normal_alkane_acentric, normal_alkane_specific_gravity

Reference normal-alkane property correlations.

critical_temperature_numerator, critical_temperature_denominator

Rational critical-temperature-ratio coefficients.

log_critical_pressure_ratio tuple[float, float, float, float, float]

Critical-pressure-ratio coefficients.

CPAMonomerProperties dataclass

CPAMonomerProperties(
    normal_boiling_temperature: float,
    specific_gravity: float,
    normal_alkane_specific_gravity: float,
    specific_gravity_perturbation: float,
    normal_alkane_temperature: float,
    normal_alkane_pressure: float,
    critical_temperature: float,
    critical_pressure: float,
    acentric_factor: float,
    used_boiling_point_match: bool,
    correlations: CPAHeavyEndCorrelations,
)

Intermediate and final CPA monomer characterization results.

Attributes:

Name Type Description
normal_boiling_temperature float

Cut normal boiling temperature in K.

specific_gravity float

Cut specific gravity relative to water at the source reference state.

normal_alkane_specific_gravity, specific_gravity_perturbation

Reference-alkane value and cut departure used by the correlation.

normal_alkane_temperature, normal_alkane_pressure

Mapped reference-alkane state in K and Pa.

critical_temperature, critical_pressure

Predicted critical constants in K and Pa.

acentric_factor float

Predicted dimensionless acentric factor.

used_boiling_point_match bool

Whether acentric factor was obtained by matching the normal boiling condition.

correlations CPAHeavyEndCorrelations

Exact coefficient set used.

m property

m: float

Return the SRK m coefficient corresponding to omega.

a0 property

a0: float

Return CPA/SRK energy parameter in Pa m6 mol-2.

b property

b: float

Return CPA/SRK covolume in m3 mol-1.

CubicConstants dataclass

CubicConstants(
    name: str,
    omega_a: float,
    omega_b: float,
    delta1: float,
    delta2: float,
    alpha_kind: AlphaKind,
    alpha_low: tuple[float, float, float],
    alpha_high: tuple[float, float, float, float]
    | None = None,
    alpha_switch: float | None = None,
    parameter_set: str | None = None,
)

Immutable constants defining a generalized two-parameter cubic EOS.

Attributes:

Name Type Description
name str

Human-readable model name.

omega_a, omega_b

Dimensionless critical constraints for the attraction and covolume parameters.

delta1, delta2

Dimensionless denominator-shape constants in the generalized cubic pressure equation.

alpha_kind AlphaKind

Identifier selecting the SRK, PR76, or PR78 alpha correlation.

alpha_low, alpha_high

Polynomial coefficients mapping acentric factor to the alpha-function exponent. alpha_high is used only by PR78 above alpha_switch.

alpha_switch float | None

PR78 acentric-factor switch, or None for models without a second coefficient branch.

parameter_set str | None

Versioned parameter-set identifier when loaded from the database.

CubicEOS

CubicEOS(
    components: ComponentSet,
    constants: CubicConstants,
    *,
    mixing: CubicMixing | None = None,
    volume_translation: Tensor
    | VolumeTranslation
    | None = None,
)

Bases: Module

Generalized cubic equation of state with differentiable parameters.

Parameters:

Name Type Description Default
components ComponentSet

Ordered component set supplying critical temperatures in K, critical pressures in Pa, acentric factors, and molar masses in kg/mol.

required
constants CubicConstants

Cubic family constants and alpha-function definition.

required
mixing CubicMixing | None

Mixing-rule module. The default is quadratic mixing with zero binary interactions.

None
volume_translation Tensor | VolumeTranslation | None

Additive component molar-volume shifts in m3/mol, or a temperature-dependent :class:VolumeTranslation object.

None

Attributes:

Name Type Description
names

Canonical component names in tensor component-axis order.

critical_temperature, critical_pressure, acentric_factor, molar_mass,
critical_volume Tensor | None

Per-component model buffers. critical_volume is None when the component source does not provide it.

mixing CubicMixing

Active mixing-rule module; its trainable tensors remain registered PyTorch parameters.

volume_translation Tensor

Reference additive shifts in m3/mol.

Notes

Public calculations preserve leading batch dimensions, device, dtype, and gradients. A requested "stable" root is the admissible root with the lowest residual Gibbs energy; root selection does not perform a flash.

Source code in src/torch_flash/eos/cubic.py
def __init__(
    self,
    components: ComponentSet,
    constants: CubicConstants,
    *,
    mixing: CubicMixing | None = None,
    volume_translation: Tensor | VolumeTranslation | None = None,
) -> None:
    super().__init__()
    self.names = components.names
    self.constants = constants
    self.register_buffer("critical_temperature", components.critical_temperature.clone())
    self.register_buffer("critical_pressure", components.critical_pressure.clone())
    self.register_buffer("acentric_factor", components.acentric_factor.clone())
    self.register_buffer("molar_mass", components.molar_mass.clone())
    self.register_buffer(
        "critical_volume",
        None if components.critical_volume is None else components.critical_volume.clone(),
    )
    if mixing is None:
        zeros = torch.zeros(
            (components.ncomponents, components.ncomponents),
            dtype=components.critical_temperature.dtype,
            device=components.critical_temperature.device,
        )
        mixing = QuadraticMixing(zeros)
    self.mixing = mixing
    if isinstance(volume_translation, VolumeTranslation):
        translation = volume_translation.reference_shift.to(
            dtype=components.critical_temperature.dtype,
            device=components.critical_temperature.device,
        )
        translation_slope = volume_translation.temperature_slope.to(
            dtype=components.critical_temperature.dtype,
            device=components.critical_temperature.device,
        )
        reference_temperature = volume_translation.reference_temperature
    else:
        translation = (
            torch.zeros_like(components.critical_temperature)
            if volume_translation is None
            else volume_translation
        )
        translation_slope = torch.zeros_like(translation)
        reference_temperature = 288.15
    if translation.shape != components.critical_temperature.shape:
        raise ValueError("volume_translation must have one value per component")
    self.register_buffer("volume_translation", translation.clone())
    self.register_buffer("volume_translation_slope", translation_slope.clone())
    self.register_buffer(
        "volume_translation_reference_temperature",
        components.critical_temperature.new_tensor(reference_temperature),
    )

ncomponents property

ncomponents: int

Return the number of components on the final tensor axis.

Returns:

Type Description
int

Length of :attr:names.

component_volume_translation

component_volume_translation(temperature: Tensor) -> Tensor

Evaluate additive component volume shifts.

Parameters:

Name Type Description Default
temperature Tensor

Temperature in K with arbitrary leading batch dimensions.

required

Returns:

Type Description
Tensor

Shifts in m3/mol with shape temperature.shape + (ncomponents,).

Source code in src/torch_flash/eos/cubic.py
def component_volume_translation(self, temperature: Tensor) -> Tensor:
    """Evaluate additive component volume shifts.

    Parameters
    ----------
    temperature
        Temperature in K with arbitrary leading batch dimensions.

    Returns
    -------
    Tensor
        Shifts in m3/mol with shape ``temperature.shape +
        (ncomponents,)``.
    """
    return (
        self.volume_translation
        + (temperature[..., None] - self.volume_translation_reference_temperature)
        * self.volume_translation_slope
    )

pure_parameters

pure_parameters(
    temperature: Tensor,
) -> tuple[Tensor, Tensor]

Evaluate pure-component attraction and covolume parameters.

Parameters:

Name Type Description Default
temperature Tensor

Positive temperature in K.

required

Returns:

Type Description
tuple

Attraction parameters a_i in Pa m6/mol2 and covolumes b_i in m3/mol, each with a final component axis.

Raises:

Type Description
InvalidStateError

If any temperature is nonpositive.

Source code in src/torch_flash/eos/cubic.py
def pure_parameters(self, temperature: Tensor) -> tuple[Tensor, Tensor]:
    """Evaluate pure-component attraction and covolume parameters.

    Parameters
    ----------
    temperature
        Positive temperature in K.

    Returns
    -------
    tuple
        Attraction parameters ``a_i`` in Pa m6/mol2 and covolumes ``b_i``
        in m3/mol, each with a final component axis.

    Raises
    ------
    InvalidStateError
        If any temperature is nonpositive.
    """
    if not torch.compiler.is_compiling() and bool((temperature <= 0.0).any()):
        raise InvalidStateError("temperature must be positive")
    reduced = temperature[..., None] / self.critical_temperature
    alpha = (1.0 + self._kappa() * (1.0 - torch.sqrt(reduced))).square()
    a = (
        self.constants.omega_a
        * R**2
        * self.critical_temperature.square()
        / self.critical_pressure
        * alpha
    )
    b = self.constants.omega_b * R * self.critical_temperature / self.critical_pressure
    return a, b

mixture_parameters

mixture_parameters(
    temperature: Tensor, composition: Tensor
) -> tuple[Tensor, Tensor]

Mix pure parameters at a homogeneous composition.

Parameters:

Name Type Description Default
temperature Tensor

Temperature in K.

required
composition Tensor

Mole fractions with components on the final axis; normalized internally.

required

Returns:

Type Description
tuple

Mixture attraction a in Pa m6/mol2 and covolume b in m3/mol, with the broadcast batch shape.

Source code in src/torch_flash/eos/cubic.py
def mixture_parameters(self, temperature: Tensor, composition: Tensor) -> tuple[Tensor, Tensor]:
    """Mix pure parameters at a homogeneous composition.

    Parameters
    ----------
    temperature
        Temperature in K.
    composition
        Mole fractions with components on the final axis; normalized
        internally.

    Returns
    -------
    tuple
        Mixture attraction ``a`` in Pa m6/mol2 and covolume ``b`` in
        m3/mol, with the broadcast batch shape.
    """
    x = normalize_composition(composition)
    pure_a, pure_b = self.pure_parameters(temperature)
    result: tuple[Tensor, Tensor] = self.mixing(temperature, x, pure_a, pure_b)
    return result

dimensionless_parameters

dimensionless_parameters(
    temperature: Tensor,
    pressure: Tensor,
    composition: Tensor,
) -> tuple[Tensor, Tensor]

Return conventional dimensionless cubic parameters A and B.

Parameters:

Name Type Description Default
temperature Tensor

Positive temperature in K.

required
pressure Tensor

Positive pressure in Pa.

required
composition Tensor

Mole fractions with components on the final axis.

required

Returns:

Type Description
tuple

A = aP/(RT)^2 and B = bP/(RT).

Raises:

Type Description
InvalidStateError

If pressure is nonpositive, or temperature is rejected by the pure-parameter calculation.

Source code in src/torch_flash/eos/cubic.py
def dimensionless_parameters(
    self, temperature: Tensor, pressure: Tensor, composition: Tensor
) -> tuple[Tensor, Tensor]:
    """Return conventional dimensionless cubic parameters ``A`` and ``B``.

    Parameters
    ----------
    temperature
        Positive temperature in K.
    pressure
        Positive pressure in Pa.
    composition
        Mole fractions with components on the final axis.

    Returns
    -------
    tuple
        ``A = aP/(RT)^2`` and ``B = bP/(RT)``.

    Raises
    ------
    InvalidStateError
        If pressure is nonpositive, or temperature is rejected by the
        pure-parameter calculation.
    """
    if not torch.compiler.is_compiling() and bool((pressure <= 0.0).any()):
        raise InvalidStateError("pressure must be positive")
    am, bm = self.mixture_parameters(temperature, composition)
    return self._dimensionless_from_mixture(temperature, pressure, am, bm)

z_factors

z_factors(
    temperature: Tensor,
    pressure: Tensor,
    composition: Tensor,
) -> Tensor

Return all sorted real compressibility-factor root slots.

Parameters:

Name Type Description Default
temperature Tensor

Homogeneous-state temperature in K and pressure in Pa.

required
pressure Tensor

Homogeneous-state temperature in K and pressure in Pa.

required
composition Tensor

Mole fractions with components on the final axis.

required

Returns:

Type Description
Tensor

Three sorted root slots on the final axis. For a polynomial with one real root, that root is repeated in all three slots.

Source code in src/torch_flash/eos/cubic.py
def z_factors(self, temperature: Tensor, pressure: Tensor, composition: Tensor) -> Tensor:
    """Return all sorted real compressibility-factor root slots.

    Parameters
    ----------
    temperature, pressure
        Homogeneous-state temperature in K and pressure in Pa.
    composition
        Mole fractions with components on the final axis.

    Returns
    -------
    Tensor
        Three sorted root slots on the final axis. For a polynomial with
        one real root, that root is repeated in all three slots.
    """
    a, b = self.dimensionless_parameters(temperature, pressure, composition)
    return self._z_factors_from_dimensionless(a, b)

select_z

select_z(
    temperature: Tensor,
    pressure: Tensor,
    composition: Tensor,
    phase: PhaseKind = "stable",
) -> Tensor

Select an admissible liquid, vapor, or minimum-Gibbs root.

Parameters:

Name Type Description Default
temperature Tensor

Homogeneous-state temperature in K and pressure in Pa.

required
pressure Tensor

Homogeneous-state temperature in K and pressure in Pa.

required
composition Tensor

Mole fractions with components on the final axis.

required
phase PhaseKind

"liquid" selects the smallest admissible root, "vapor" the largest, and "stable" the minimum-residual-Gibbs root.

'stable'

Returns:

Type Description
Tensor

Selected dimensionless compressibility factor for each state.

Raises:

Type Description
ValueError

If phase is unknown.

InvalidStateError

If no root exceeds the mixture covolume.

Source code in src/torch_flash/eos/cubic.py
def select_z(
    self,
    temperature: Tensor,
    pressure: Tensor,
    composition: Tensor,
    phase: PhaseKind = "stable",
) -> Tensor:
    """Select an admissible liquid, vapor, or minimum-Gibbs root.

    Parameters
    ----------
    temperature, pressure
        Homogeneous-state temperature in K and pressure in Pa.
    composition
        Mole fractions with components on the final axis.
    phase
        ``"liquid"`` selects the smallest admissible root, ``"vapor"``
        the largest, and ``"stable"`` the minimum-residual-Gibbs root.

    Returns
    -------
    Tensor
        Selected dimensionless compressibility factor for each state.

    Raises
    ------
    ValueError
        If ``phase`` is unknown.
    InvalidStateError
        If no root exceeds the mixture covolume.
    """
    roots = self.z_factors(temperature, pressure, composition)
    a, b = self.dimensionless_parameters(temperature, pressure, composition)
    return self._select_z_from_dimensionless(a, b, phase, roots=roots)

molar_volume

molar_volume(
    temperature: Tensor,
    pressure: Tensor,
    composition: Tensor,
    phase: PhaseKind = "stable",
) -> Tensor

Return translated homogeneous-phase molar volume.

Parameters:

Name Type Description Default
temperature Tensor

Temperature in K and pressure in Pa.

required
pressure Tensor

Temperature in K and pressure in Pa.

required
composition Tensor

Mole fractions with components on the final axis.

required
phase PhaseKind

Cubic root-selection request.

'stable'

Returns:

Type Description
Tensor

Physical molar volume in m3/mol.

Source code in src/torch_flash/eos/cubic.py
def molar_volume(
    self,
    temperature: Tensor,
    pressure: Tensor,
    composition: Tensor,
    phase: PhaseKind = "stable",
) -> Tensor:
    """Return translated homogeneous-phase molar volume.

    Parameters
    ----------
    temperature, pressure
        Temperature in K and pressure in Pa.
    composition
        Mole fractions with components on the final axis.
    phase
        Cubic root-selection request.

    Returns
    -------
    Tensor
        Physical molar volume in m3/mol.
    """
    x = normalize_composition(composition)
    z = self.select_z(temperature, pressure, x, phase)
    unshifted = z * R * temperature / pressure
    translation = self.component_volume_translation(temperature)
    return unshifted + torch.sum(x * translation, dim=-1)

pressure

pressure(
    temperature: Tensor,
    molar_volume: Tensor,
    composition: Tensor,
) -> Tensor

Evaluate pressure at a supplied homogeneous T-v-x state.

Parameters:

Name Type Description Default
temperature Tensor

Temperature in K.

required
molar_volume Tensor

Physical, volume-translated molar volume in m3/mol.

required
composition Tensor

Mole fractions with components on the final axis.

required

Returns:

Type Description
Tensor

Pressure in Pa.

Raises:

Type Description
InvalidStateError

If the parent-EOS volume does not exceed the mixture covolume.

Source code in src/torch_flash/eos/cubic.py
def pressure(self, temperature: Tensor, molar_volume: Tensor, composition: Tensor) -> Tensor:
    """Evaluate pressure at a supplied homogeneous ``T-v-x`` state.

    Parameters
    ----------
    temperature
        Temperature in K.
    molar_volume
        Physical, volume-translated molar volume in m3/mol.
    composition
        Mole fractions with components on the final axis.

    Returns
    -------
    Tensor
        Pressure in Pa.

    Raises
    ------
    InvalidStateError
        If the parent-EOS volume does not exceed the mixture covolume.
    """
    x = normalize_composition(composition)
    shift = torch.sum(x * self.component_volume_translation(temperature), dim=-1)
    volume = molar_volume - shift
    am, bm = self.mixture_parameters(temperature, x)
    d1 = self.constants.delta1
    d2 = self.constants.delta2
    if bool((volume <= bm).any()):
        raise InvalidStateError("molar volume must exceed the mixture covolume")
    return R * temperature / (volume - bm) - am / ((volume + d1 * bm) * (volume + d2 * bm))

residual_helmholtz_rt

residual_helmholtz_rt(
    temperature: Tensor, volume: Tensor, moles: Tensor
) -> Tensor

Evaluate the extensive reduced residual Helmholtz energy.

Parameters:

Name Type Description Default
temperature Tensor

Temperature in K.

required
volume Tensor

Physical extensive volume in m3.

required
moles Tensor

Component amounts in mol on the final axis.

required

Returns:

Type Description
Tensor

Dimensionless extensive residual Helmholtz energy A^R/(RT).

Notes

The ideal-gas reference-volume correction retains thermodynamic consistency when additive volume translation is active.

Source code in src/torch_flash/eos/cubic.py
def residual_helmholtz_rt(self, temperature: Tensor, volume: Tensor, moles: Tensor) -> Tensor:
    """Evaluate the extensive reduced residual Helmholtz energy.

    Parameters
    ----------
    temperature
        Temperature in K.
    volume
        Physical extensive volume in m3.
    moles
        Component amounts in mol on the final axis.

    Returns
    -------
    Tensor
        Dimensionless extensive residual Helmholtz energy ``A^R/(RT)``.

    Notes
    -----
    The ideal-gas reference-volume correction retains thermodynamic
    consistency when additive volume translation is active.
    """
    total = moles.sum(dim=-1)
    x = moles / total[..., None]
    translation = self.component_volume_translation(temperature)
    shift = torch.sum(x * translation, dim=-1)
    unshifted_volume = volume - total * shift
    molar_volume = unshifted_volume / total
    am, bm = self.mixture_parameters(temperature, x)
    d1 = self.constants.delta1
    d2 = self.constants.delta2
    repulsive = -total * torch.log1p(-bm / molar_volume)
    logarithm = torch.log((molar_volume + d1 * bm) / (molar_volume + d2 * bm))
    attractive = -total * am / (R * temperature * bm * (d1 - d2)) * logarithm
    # A constant Peneloux-style translation maps the physical volume V to
    # the parent-EoS volume V0 = V - sum(n_i*c_i).  The final logarithm is
    # the ideal-gas reference-volume correction required when A^R is
    # defined relative to an ideal gas at the *physical* V.  Including it
    # makes -dA/dV, fugacity, and the TP/TV routes mutually consistent.
    reference_volume_correction = total * torch.log(volume / unshifted_volume)
    return repulsive + attractive + reference_volume_correction

log_fugacity_coefficients

log_fugacity_coefficients(
    temperature: Tensor,
    pressure: Tensor,
    composition: Tensor,
    phase: PhaseKind = "stable",
) -> Tensor

Return component log fugacity coefficients.

The quadratic rule uses the standard closed form. Non-quadratic mixing rules use an autodifferentiated residual Helmholtz energy, preserving the exact composition dependence of the selected rule.

Source code in src/torch_flash/eos/cubic.py
def log_fugacity_coefficients(
    self,
    temperature: Tensor,
    pressure: Tensor,
    composition: Tensor,
    phase: PhaseKind = "stable",
) -> Tensor:
    """Return component log fugacity coefficients.

    The quadratic rule uses the standard closed form. Non-quadratic mixing
    rules use an autodifferentiated residual Helmholtz energy, preserving
    the exact composition dependence of the selected rule.
    """
    x = normalize_composition(composition)
    pure_a, pure_b = self.pure_parameters(temperature)
    quadratic_mixing = isinstance(
        self.mixing,
        QuadraticMixing | TemperatureDependentQuadraticMixing | PPR78Mixing,
    )
    if isinstance(self.mixing, PPR78Mixing):
        aij = self.mixing.cross_a(temperature, pure_a, pure_b)
        am = torch.einsum("...i,...ij,...j->...", x, aij, x)
        bm = torch.sum(x * pure_b, dim=-1)
        partial_b = pure_b + torch.zeros_like(x)
    elif isinstance(self.mixing, TemperatureDependentQuadraticMixing):
        aij = self.mixing.cross_a(temperature, pure_a)
        am = torch.einsum("...i,...ij,...j->...", x, aij, x)
        if self.mixing._linear_covolume:
            bm = torch.sum(x * pure_b, dim=-1)
            partial_b = pure_b + torch.zeros_like(x)
        else:
            bij = self.mixing.cross_b(pure_b)
            bm = torch.einsum("...i,...ij,...j->...", x, bij, x)
            partial_b = 2.0 * torch.einsum("...j,...ij->...i", x, bij) - bm[..., None]
    elif isinstance(self.mixing, QuadraticMixing):
        aij = self.mixing.cross_a(pure_a)
        am = torch.einsum("...i,...ij,...j->...", x, aij, x)
        if self.mixing._linear_covolume:
            bm = torch.sum(x * pure_b, dim=-1)
            partial_b = pure_b + torch.zeros_like(x)
        else:
            bij = self.mixing.cross_b(pure_b)
            bm = torch.einsum("...i,...ij,...j->...", x, bij, x)
            partial_b = 2.0 * torch.einsum("...j,...ij->...i", x, bij) - bm[..., None]
    else:
        am, bm = self.mixing(temperature, x, pure_a, pure_b)
    a, b = self._dimensionless_from_mixture(temperature, pressure, am, bm)
    z = self._select_z_from_dimensionless(a, b, phase)
    if quadratic_mixing:
        sum_aij = torch.einsum("...j,...ij->...i", x, aij)
        partial_b_over_b = partial_b / bm[..., None]
        composition_term = 2.0 * sum_aij / am[..., None] - partial_b_over_b
        d1 = self.constants.delta1
        d2 = self.constants.delta2
        log_ratio = torch.log((z + d1 * b) / (z + d2 * b))
        unshifted_log_phi = (
            partial_b_over_b * (z - 1.0)[..., None]
            - torch.log(z - b)[..., None]
            - (a / (b * (d1 - d2)) * log_ratio)[..., None] * composition_term
        )
        # For v = v0 + sum(x_i*c_i), thermodynamic consistency requires
        # ln(phi_i) = ln(phi_i,0) + P*c_i/(R*T).  A common sign error is
        # avoided here by defining ``volume_translation`` as the quantity
        # *added* to the parent-EoS volume in ``molar_volume``.
        return unshifted_log_phi + (pressure / (R * temperature))[
            ..., None
        ] * self.component_volume_translation(temperature)

    translation = self.component_volume_translation(temperature)
    volume = z * R * temperature / pressure + torch.sum(x * translation, dim=-1)

    def at_fixed_volume(moles: Tensor) -> Tensor:
        # Batched states are independent, so differentiating their sum
        # returns the same row-wise chemical potentials as evaluating
        # each scalar state separately. ``torch.func.grad`` requires a
        # scalar output and therefore cannot consume the unsummed batch.
        return self.residual_helmholtz_rt(temperature, volume, moles).sum()

    residual_mu_rt: Tensor = torch.func.grad(at_fixed_volume)(x)
    physical_z = pressure * volume / (R * temperature)
    return residual_mu_rt - torch.log(physical_z)[..., None]

PureFluidHelmholtzEOS

PureFluidHelmholtzEOS(
    kernel: MultiparameterEOS,
    metadata: PureFluidHelmholtzMetadata,
)

Bases: Module

One-component thermodynamic Helmholtz equation.

The internal kernel shares the differentiable Helmholtz term evaluators used by multiparameter mixture models, while this public wrapper exposes pure-fluid signatures without a redundant composition argument.

Parameters:

Name Type Description Default
kernel MultiparameterEOS

One-component native Helmholtz evaluation kernel.

required
metadata PureFluidHelmholtzMetadata

Published model identity and canonical pure-fluid name.

required
Source code in src/torch_flash/eos/helmholtz.py
def __init__(
    self,
    kernel: MultiparameterEOS,
    metadata: PureFluidHelmholtzMetadata,
) -> None:
    super().__init__()
    if len(kernel.names) != 1:
        raise ValueError("a pure-fluid Helmholtz kernel requires exactly one component")
    if kernel.names[0] != metadata.fluid:
        raise ValueError("pure-fluid metadata must match the kernel component")
    self.kernel = kernel
    self.metadata = metadata
    self.register_buffer("_composition", kernel.gas_constant.new_ones(1))

gas_constant property

gas_constant: Tensor

Equation gas constant in J/(mol K).

critical_temperature property

critical_temperature: Tensor

Critical temperature in K.

critical_pressure property

critical_pressure: Tensor

Critical pressure in Pa.

critical_density property

critical_density: Tensor

Critical molar density in mol/m3.

molar_mass property

molar_mass: Tensor

Molar mass in kg/mol.

residual_term_count property

residual_term_count: int

Number of nonzero residual Helmholtz terms.

alpha_ideal

alpha_ideal(
    temperature: Tensor, molar_density: Tensor
) -> Tensor

Return dimensionless ideal molar Helmholtz energy.

Source code in src/torch_flash/eos/helmholtz.py
def alpha_ideal(self, temperature: Tensor, molar_density: Tensor) -> Tensor:
    """Return dimensionless ideal molar Helmholtz energy."""
    return self.kernel.alpha_ideal(temperature, molar_density, self._composition)

alpha_residual

alpha_residual(
    temperature: Tensor, molar_density: Tensor
) -> Tensor

Return dimensionless residual molar Helmholtz energy.

Source code in src/torch_flash/eos/helmholtz.py
def alpha_residual(self, temperature: Tensor, molar_density: Tensor) -> Tensor:
    """Return dimensionless residual molar Helmholtz energy."""
    return self.kernel.alpha_residual(temperature, molar_density, self._composition)

alpha_total

alpha_total(
    temperature: Tensor, molar_density: Tensor
) -> Tensor

Return total dimensionless molar Helmholtz energy.

Source code in src/torch_flash/eos/helmholtz.py
def alpha_total(self, temperature: Tensor, molar_density: Tensor) -> Tensor:
    """Return total dimensionless molar Helmholtz energy."""
    return self.kernel.alpha_total(temperature, molar_density, self._composition)

pressure

pressure(
    temperature: Tensor, molar_volume: Tensor
) -> Tensor

Return pressure in Pa at the supplied homogeneous state.

Source code in src/torch_flash/eos/helmholtz.py
def pressure(self, temperature: Tensor, molar_volume: Tensor) -> Tensor:
    """Return pressure in Pa at the supplied homogeneous state."""
    return self.kernel.pressure(temperature, molar_volume, self._composition)

molar_volume

molar_volume(
    temperature: Tensor,
    pressure: Tensor,
    phase: PhaseKind = "stable",
) -> Tensor

Return the selected molar-volume root in m3/mol.

Source code in src/torch_flash/eos/helmholtz.py
def molar_volume(
    self,
    temperature: Tensor,
    pressure: Tensor,
    phase: PhaseKind = "stable",
) -> Tensor:
    """Return the selected molar-volume root in m3/mol."""
    return self.kernel.molar_volume(
        temperature,
        pressure,
        self._composition,
        phase,
    )

compressibility_factor

compressibility_factor(
    temperature: Tensor, molar_volume: Tensor
) -> Tensor

Return the homogeneous-state compressibility factor.

Source code in src/torch_flash/eos/helmholtz.py
def compressibility_factor(self, temperature: Tensor, molar_volume: Tensor) -> Tensor:
    """Return the homogeneous-state compressibility factor."""
    pressure = self.pressure(temperature, molar_volume)
    return pressure * molar_volume / (self.gas_constant * temperature)

molar_helmholtz_energy

molar_helmholtz_energy(
    temperature: Tensor, molar_density: Tensor
) -> Tensor

Return molar Helmholtz energy in J/mol.

Source code in src/torch_flash/eos/helmholtz.py
def molar_helmholtz_energy(
    self,
    temperature: Tensor,
    molar_density: Tensor,
) -> Tensor:
    """Return molar Helmholtz energy in J/mol."""
    return self.kernel.molar_helmholtz_energy(
        temperature,
        molar_density,
        self._composition,
    )

molar_entropy

molar_entropy(
    temperature: Tensor, molar_density: Tensor
) -> Tensor

Return molar entropy in J/(mol K).

Source code in src/torch_flash/eos/helmholtz.py
def molar_entropy(self, temperature: Tensor, molar_density: Tensor) -> Tensor:
    """Return molar entropy in J/(mol K)."""
    return self.kernel.molar_entropy(temperature, molar_density, self._composition)

molar_internal_energy

molar_internal_energy(
    temperature: Tensor, molar_density: Tensor
) -> Tensor

Return molar internal energy in J/mol.

Source code in src/torch_flash/eos/helmholtz.py
def molar_internal_energy(
    self,
    temperature: Tensor,
    molar_density: Tensor,
) -> Tensor:
    """Return molar internal energy in J/mol."""
    return self.kernel.molar_internal_energy(
        temperature,
        molar_density,
        self._composition,
    )

molar_enthalpy

molar_enthalpy(
    temperature: Tensor, molar_density: Tensor
) -> Tensor

Return molar enthalpy in J/mol.

Source code in src/torch_flash/eos/helmholtz.py
def molar_enthalpy(self, temperature: Tensor, molar_density: Tensor) -> Tensor:
    """Return molar enthalpy in J/mol."""
    return self.kernel.molar_enthalpy(temperature, molar_density, self._composition)

molar_gibbs_energy

molar_gibbs_energy(
    temperature: Tensor, molar_density: Tensor
) -> Tensor

Return molar Gibbs energy in J/mol.

Source code in src/torch_flash/eos/helmholtz.py
def molar_gibbs_energy(
    self,
    temperature: Tensor,
    molar_density: Tensor,
) -> Tensor:
    """Return molar Gibbs energy in J/mol."""
    return self.kernel.molar_gibbs_energy(
        temperature,
        molar_density,
        self._composition,
    )

molar_heat_capacity_cv

molar_heat_capacity_cv(
    temperature: Tensor, molar_density: Tensor
) -> Tensor

Return isochoric molar heat capacity in J/(mol K).

Source code in src/torch_flash/eos/helmholtz.py
def molar_heat_capacity_cv(
    self,
    temperature: Tensor,
    molar_density: Tensor,
) -> Tensor:
    """Return isochoric molar heat capacity in J/(mol K)."""
    return self.kernel.molar_heat_capacity_cv(
        temperature,
        molar_density,
        self._composition,
    )

molar_heat_capacity_cp

molar_heat_capacity_cp(
    temperature: Tensor, molar_density: Tensor
) -> Tensor

Return isobaric molar heat capacity in J/(mol K).

Source code in src/torch_flash/eos/helmholtz.py
def molar_heat_capacity_cp(
    self,
    temperature: Tensor,
    molar_density: Tensor,
) -> Tensor:
    """Return isobaric molar heat capacity in J/(mol K)."""
    return self.kernel.molar_heat_capacity_cp(
        temperature,
        molar_density,
        self._composition,
    )

speed_of_sound

speed_of_sound(
    temperature: Tensor, molar_density: Tensor
) -> Tensor

Return homogeneous-phase speed of sound in m/s.

Source code in src/torch_flash/eos/helmholtz.py
def speed_of_sound(self, temperature: Tensor, molar_density: Tensor) -> Tensor:
    """Return homogeneous-phase speed of sound in m/s."""
    return self.kernel.speed_of_sound(
        temperature,
        molar_density,
        self._composition,
    )

PureFluidHelmholtzMetadata dataclass

PureFluidHelmholtzMetadata(
    model: str, reference: str, version: str, fluid: str
)

Identity and scope of one pure-fluid fundamental equation.

Attributes:

Name Type Description
model str

Published equation or parameter-set identity.

reference str

Defining bibliographic reference.

version str

Version of the serialized coefficient set.

fluid str

Canonical pure-fluid name.

GaoBTerms dataclass

GaoBTerms(
    n: Tensor,
    d: Tensor,
    t: Tensor,
    eta: Tensor,
    epsilon: Tensor,
    beta: Tensor,
    gamma: Tensor,
    b: Tensor,
)

Gao-B critical-region residual Helmholtz coefficient arrays.

Attributes:

Name Type Description
n, d, t

Term amplitude and reduced-density/reduced-temperature exponents.

eta, epsilon, beta, gamma, b

Critical-region shape parameters. Every tensor must have the same shape as n.

HelmholtzTerms dataclass

HelmholtzTerms(
    n: Tensor,
    d: Tensor,
    t: Tensor,
    decay: Tensor,
    eta: Tensor | None = None,
    epsilon: Tensor | None = None,
    beta: Tensor | None = None,
    gamma: Tensor | None = None,
    linear_density: Tensor | None = None,
    linear_shift: Tensor | None = None,
)

Power, exponential, Gaussian, and GERG-special terms.

The full form is n*delta^d*tau^t*exp(-delta^l -eta*(delta-epsilon)^2-beta*(tau-gamma)^2 -linear_density*(delta-linear_shift)). Setting the optional arrays to zero recovers the power-exponential subset. The final linear-density factor is the binary departure form used by GERG-2008.

Attributes:

Name Type Description
n, d, t, decay

Term amplitudes, reduced-density exponents, inverse-reduced-temperature exponents, and exponential density powers.

eta, epsilon

Optional Gaussian density width and center arrays.

beta, gamma

Optional Gaussian temperature width and center arrays.

linear_density, linear_shift

Optional GERG linear-density exponential parameters.

Notes

Every present tensor has the same term-table shape. Pure tables typically use component rows; departure tables use component-pair rows.

IdealHelmholtzTerms dataclass

IdealHelmholtzTerms(
    lead_constant: Tensor,
    lead_tau: Tensor,
    log_tau: Tensor,
    tau_log_tau: Tensor,
    power_n: Tensor,
    power_t: Tensor,
    planck_n: Tensor,
    planck_theta: Tensor,
    gerg_n: Tensor,
    gerg_theta: Tensor,
    gerg_sign: Tensor,
    gas_scale: Tensor,
)

Canonical pure-fluid ideal Helmholtz term tables.

Every component has lead, logarithmic, power, Planck--Einstein, and optional GERG sinh/cosh terms. gas_scale is the ratio between the gas constant of the original pure-fluid equation and the common mixture gas constant. It multiplies the density-independent part only, preserving the exact ideal-gas pressure limit.

Attributes:

Name Type Description
lead_constant, lead_tau, log_tau, tau_log_tau

One leading coefficient of each analytic form per component.

power_n, power_t

Amplitudes and exponents for pure power terms.

planck_n, planck_theta

Planck--Einstein amplitudes and characteristic-temperature ratios.

gerg_n, gerg_theta, gerg_sign

GERG hyperbolic-term amplitudes, arguments, and sinh/cosh selectors.

gas_scale Tensor

Per-component ratio of the source pure-fluid gas constant to the common mixture gas constant.

MultiparameterEOS

MultiparameterEOS(
    names: tuple[str, ...],
    critical_temperature: Tensor,
    critical_density: Tensor,
    molar_mass: Tensor,
    pure_terms: HelmholtzTerms,
    departure_terms: HelmholtzTerms,
    beta_temperature: Tensor,
    gamma_temperature: Tensor,
    beta_volume: Tensor,
    gamma_volume: Tensor,
    departure_scale: Tensor,
    metadata: MultiparameterMetadata,
    *,
    trainable: bool = False,
    gas_constant: float = R,
    pure_gaob_terms: GaoBTerms | None = None,
    pure_nonanalytic_terms: NonAnalyticTerms | None = None,
    ideal_terms: IdealHelmholtzTerms | None = None,
    critical_pressure: Tensor | None = None,
    acentric_factor: Tensor | None = None,
)

Bases: Module

Autodifferentiable multiparameter Helmholtz equation of state.

Parameters:

Name Type Description Default
names tuple[str, ...]

Canonical component names defining the final tensor axis.

required
critical_temperature Tensor

Pure-component reducing constants in K, mol/m3, and kg/mol.

required
critical_density Tensor

Pure-component reducing constants in K, mol/m3, and kg/mol.

required
molar_mass Tensor

Pure-component reducing constants in K, mol/m3, and kg/mol.

required
pure_terms HelmholtzTerms

Pure residual and binary departure Helmholtz coefficient tables.

required
departure_terms HelmholtzTerms

Pure residual and binary departure Helmholtz coefficient tables.

required
beta_temperature Tensor

Symmetric binary reducing-function matrices.

required
gamma_temperature Tensor

Symmetric binary reducing-function matrices.

required
beta_volume Tensor

Symmetric binary reducing-function matrices.

required
gamma_volume Tensor

Symmetric binary reducing-function matrices.

required
departure_scale Tensor

Symmetric binary departure-function multipliers.

required
metadata MultiparameterMetadata

Exact model identity and supported-component scope.

required
trainable bool

Register coefficient arrays as trainable PyTorch parameters.

False
gas_constant float

Model gas constant in J/(mol K).

R
Notes

Property kernels preserve leading batches and gradients. Density inversion and root selection are iterative; failure raises :class:~torch_flash.exceptions.ConvergenceError. Float64 is the reference precision, especially near critical or coalescing roots.

Source code in src/torch_flash/eos/multiparameter.py
def __init__(
    self,
    names: tuple[str, ...],
    critical_temperature: Tensor,
    critical_density: Tensor,
    molar_mass: Tensor,
    pure_terms: HelmholtzTerms,
    departure_terms: HelmholtzTerms,
    beta_temperature: Tensor,
    gamma_temperature: Tensor,
    beta_volume: Tensor,
    gamma_volume: Tensor,
    departure_scale: Tensor,
    metadata: MultiparameterMetadata,
    *,
    trainable: bool = False,
    gas_constant: float = R,
    pure_gaob_terms: GaoBTerms | None = None,
    pure_nonanalytic_terms: NonAnalyticTerms | None = None,
    ideal_terms: IdealHelmholtzTerms | None = None,
    critical_pressure: Tensor | None = None,
    acentric_factor: Tensor | None = None,
) -> None:
    super().__init__()
    ncomponents = len(names)
    if pure_terms.n.shape[0] != ncomponents:
        raise ValueError("pure term table must have one row per component")
    expected_pair = (ncomponents, ncomponents)
    if departure_terms.n.shape[:2] != expected_pair:
        raise ValueError("departure term table must start with (component, component)")
    for matrix in (
        beta_temperature,
        gamma_temperature,
        beta_volume,
        gamma_volume,
        departure_scale,
    ):
        if matrix.shape != expected_pair:
            raise ValueError("all multiparameter interaction matrices must be square")
    self.names = names
    self.metadata = metadata
    self.register_buffer(
        "gas_constant",
        critical_temperature.new_tensor(gas_constant),
    )
    self.register_buffer("critical_temperature", critical_temperature.clone())
    self.register_buffer("critical_density", critical_density.clone())
    self.register_buffer(
        "critical_pressure",
        (
            torch.full_like(critical_temperature, torch.nan)
            if critical_pressure is None
            else critical_pressure.clone()
        ),
    )
    self.register_buffer(
        "acentric_factor",
        (
            torch.full_like(critical_temperature, torch.nan)
            if acentric_factor is None
            else acentric_factor.clone()
        ),
    )
    self.register_buffer("molar_mass", molar_mass.clone())
    self._store_terms("pure", pure_terms, trainable)
    self._store_terms("departure", departure_terms, trainable)
    self._store_gaob_terms(pure_gaob_terms, ncomponents, critical_temperature, trainable)
    self._store_nonanalytic_terms(
        pure_nonanalytic_terms, ncomponents, critical_temperature, trainable
    )
    # Padded absent critical terms are common in GERG parameter documents.
    # Keep trainable tables active because a caller may intentionally fit a
    # coefficient initialized at zero; immutable all-zero tables can bypass
    # otherwise relatively expensive critical-term algebra.
    self._has_gaob_terms = trainable or bool(torch.count_nonzero(self.pure_gaob_n))
    self._has_nonanalytic_terms = trainable or bool(
        torch.count_nonzero(self.pure_nonanalytic_n)
    )
    self._store_ideal_terms(ideal_terms, ncomponents, critical_temperature, trainable)
    self._store_parameter("beta_temperature", beta_temperature, trainable)
    self._store_parameter("gamma_temperature", gamma_temperature, trainable)
    self._store_parameter("beta_volume", beta_volume, trainable)
    self._store_parameter("gamma_volume", gamma_volume, trainable)
    self._store_parameter("departure_scale", departure_scale, trainable)
    self.register_buffer(
        "_upper_triangle",
        torch.triu(torch.ones_like(beta_temperature), diagonal=1),
    )
    self.register_buffer(
        "_critical_temperature_pair",
        torch.sqrt(critical_temperature[:, None] * critical_temperature[None, :]),
    )
    self.register_buffer(
        "_inverse_density_pair",
        0.125
        * (
            critical_density[:, None].pow(-1.0 / 3.0)
            + critical_density[None, :].pow(-1.0 / 3.0)
        ).pow(3),
    )

reducing_functions

reducing_functions(
    composition: Tensor,
) -> tuple[Tensor, Tensor]

Evaluate composition-dependent mixture reducing functions.

Parameters:

Name Type Description Default
composition Tensor

Mole fractions with components on the final axis.

required

Returns:

Type Description
tuple

Reducing temperature in K and reducing molar density in mol/m3, with the broadcast leading shape.

Source code in src/torch_flash/eos/multiparameter.py
def reducing_functions(self, composition: Tensor) -> tuple[Tensor, Tensor]:
    """Evaluate composition-dependent mixture reducing functions.

    Parameters
    ----------
    composition
        Mole fractions with components on the final axis.

    Returns
    -------
    tuple
        Reducing temperature in K and reducing molar density in mol/m3,
        with the broadcast leading shape.
    """
    return self._reducing_functions_normalized(normalize_composition(composition))

alpha_residual

alpha_residual(
    temperature: Tensor,
    molar_density: Tensor,
    composition: Tensor,
) -> Tensor

Evaluate dimensionless molar residual Helmholtz energy.

Parameters:

Name Type Description Default
temperature Tensor

Temperature in K.

required
molar_density Tensor

Molar density in mol/m3.

required
composition Tensor

Mole fractions with components on the final axis.

required

Returns:

Type Description
Tensor

Dimensionless alpha^r = a^r/(RT).

Source code in src/torch_flash/eos/multiparameter.py
def alpha_residual(
    self, temperature: Tensor, molar_density: Tensor, composition: Tensor
) -> Tensor:
    """Evaluate dimensionless molar residual Helmholtz energy.

    Parameters
    ----------
    temperature
        Temperature in K.
    molar_density
        Molar density in mol/m3.
    composition
        Mole fractions with components on the final axis.

    Returns
    -------
    Tensor
        Dimensionless ``alpha^r = a^r/(RT)``.
    """
    x = normalize_composition(composition)
    reducing_temperature, reducing_density = self._reducing_functions_normalized(x)
    tau = reducing_temperature / temperature
    delta = molar_density / reducing_density
    pure_values = self._evaluate_terms(
        self.pure_n,
        self.pure_d,
        self.pure_t,
        self.pure_decay,
        self.pure_eta,
        self.pure_epsilon,
        self.pure_beta,
        self.pure_gamma,
        self.pure_linear_density,
        self.pure_linear_shift,
        delta,
        tau,
    )
    if self._has_gaob_terms:
        pure_values = pure_values + self._evaluate_gaob(delta, tau)
    if self._has_nonanalytic_terms:
        pure_values = pure_values + self._evaluate_nonanalytic(delta, tau)
    departure_values = self._evaluate_terms(
        self.departure_n,
        self.departure_d,
        self.departure_t,
        self.departure_decay,
        self.departure_eta,
        self.departure_epsilon,
        self.departure_beta,
        self.departure_gamma,
        self.departure_linear_density,
        self.departure_linear_shift,
        delta,
        tau,
    )
    pure = torch.sum(x * pure_values, dim=-1)
    pair_weights = (
        x[..., :, None] * x[..., None, :] * self.departure_scale * self._upper_triangle
    )
    return pure + torch.sum(pair_weights * departure_values, dim=(-2, -1))

alpha_residual_delta

alpha_residual_delta(
    temperature: Tensor,
    molar_density: Tensor,
    composition: Tensor,
) -> Tensor

Return the residual Helmholtz derivative with respect to reduced density.

Source code in src/torch_flash/eos/multiparameter.py
def alpha_residual_delta(
    self, temperature: Tensor, molar_density: Tensor, composition: Tensor
) -> Tensor:
    """Return the residual Helmholtz derivative with respect to reduced density."""
    x = normalize_composition(composition)
    reducing_temperature, reducing_density = self._reducing_functions_normalized(x)
    tau = reducing_temperature / temperature
    delta = molar_density / reducing_density
    return self._alpha_residual_delta_reduced(delta, tau, x)

alpha_ideal

alpha_ideal(
    temperature: Tensor,
    molar_density: Tensor,
    composition: Tensor,
) -> Tensor

Evaluate dimensionless molar ideal-gas Helmholtz energy.

Parameters:

Name Type Description Default
temperature Tensor

Temperature in K.

required
molar_density Tensor

Molar density in mol/m3.

required
composition Tensor

Mole fractions with components on the final axis.

required

Returns:

Type Description
Tensor

Dimensionless ideal contribution alpha^0.

Raises:

Type Description
RuntimeError

If the model was constructed without ideal Helmholtz terms.

Source code in src/torch_flash/eos/multiparameter.py
def alpha_ideal(
    self, temperature: Tensor, molar_density: Tensor, composition: Tensor
) -> Tensor:
    """Evaluate dimensionless molar ideal-gas Helmholtz energy.

    Parameters
    ----------
    temperature
        Temperature in K.
    molar_density
        Molar density in mol/m3.
    composition
        Mole fractions with components on the final axis.

    Returns
    -------
    Tensor
        Dimensionless ideal contribution ``alpha^0``.

    Raises
    ------
    RuntimeError
        If the model was constructed without ideal Helmholtz terms.
    """
    if not self.has_ideal_terms:
        raise RuntimeError("this multiparameter model has no ideal Helmholtz coefficient table")
    x = normalize_composition(composition)
    tau = self.critical_temperature / temperature[..., None]
    thermal = (
        self.ideal_lead_constant
        + self.ideal_lead_tau * tau
        + self.ideal_log_tau * torch.log(tau)
        + self.ideal_tau_log_tau * tau * torch.log(tau)
        + torch.sum(
            self.ideal_power_n * tau[..., :, None].pow(self.ideal_power_t),
            dim=-1,
        )
    )
    planck_argument = self.ideal_planck_theta * tau[..., :, None]
    thermal = thermal + torch.sum(
        self.ideal_planck_n * torch.log(-torch.expm1(-planck_argument)),
        dim=-1,
    )
    gerg_argument = (self.ideal_gerg_theta * tau[..., :, None]).abs()
    safe_argument = gerg_argument.clamp_min(torch.finfo(gerg_argument.dtype).tiny)
    log_sinh = (
        safe_argument
        + torch.log1p(-torch.exp(-2.0 * safe_argument))
        - torch.log(safe_argument.new_tensor(2.0))
    )
    log_cosh = torch.logaddexp(safe_argument, -safe_argument) - torch.log(
        safe_argument.new_tensor(2.0)
    )
    gerg_value = torch.where(self.ideal_gerg_sign > 0.0, log_sinh, -log_cosh)
    thermal = thermal + torch.sum(self.ideal_gerg_n * gerg_value, dim=-1)
    pure = (
        torch.log(molar_density[..., None] / self.critical_density)
        + self.ideal_gas_scale * thermal
    )
    return torch.sum(x * pure + torch.special.xlogy(x, x), dim=-1)

alpha_total

alpha_total(
    temperature: Tensor,
    molar_density: Tensor,
    composition: Tensor,
) -> Tensor

Evaluate total dimensionless molar Helmholtz energy.

Parameters:

Name Type Description Default
temperature Tensor

Temperature in K.

required
molar_density Tensor

Molar density in mol/m3.

required
composition Tensor

Mole fractions on the final axis.

required

Returns:

Type Description
Tensor

alpha^0 + alpha^r.

Source code in src/torch_flash/eos/multiparameter.py
def alpha_total(
    self, temperature: Tensor, molar_density: Tensor, composition: Tensor
) -> Tensor:
    """Evaluate total dimensionless molar Helmholtz energy.

    Parameters
    ----------
    temperature
        Temperature in K.
    molar_density
        Molar density in mol/m3.
    composition
        Mole fractions on the final axis.

    Returns
    -------
    Tensor
        ``alpha^0 + alpha^r``.
    """
    return self.alpha_ideal(temperature, molar_density, composition) + self.alpha_residual(
        temperature, molar_density, composition
    )

residual_helmholtz_rt

residual_helmholtz_rt(
    temperature: Tensor, volume: Tensor, moles: Tensor
) -> Tensor

Evaluate extensive reduced residual Helmholtz energy.

Parameters:

Name Type Description Default
temperature Tensor

Temperature in K.

required
volume Tensor

Total volume in m3.

required
moles Tensor

Component amounts in mol on the final axis.

required

Returns:

Type Description
Tensor

Dimensionless extensive A^R/(RT).

Source code in src/torch_flash/eos/multiparameter.py
def residual_helmholtz_rt(self, temperature: Tensor, volume: Tensor, moles: Tensor) -> Tensor:
    """Evaluate extensive reduced residual Helmholtz energy.

    Parameters
    ----------
    temperature
        Temperature in K.
    volume
        Total volume in m3.
    moles
        Component amounts in mol on the final axis.

    Returns
    -------
    Tensor
        Dimensionless extensive ``A^R/(RT)``.
    """
    total = moles.sum(dim=-1)
    x = moles / total[..., None]
    density = total / volume
    return total * self.alpha_residual(temperature, density, x)

helmholtz_rt

helmholtz_rt(
    temperature: Tensor, volume: Tensor, moles: Tensor
) -> Tensor

Evaluate extensive total reduced Helmholtz energy.

Parameters:

Name Type Description Default
temperature Tensor

Temperature in K.

required
volume Tensor

Total volume in m3.

required
moles Tensor

Component amounts in mol on the final axis.

required

Returns:

Type Description
Tensor

Dimensionless extensive A/(RT).

Source code in src/torch_flash/eos/multiparameter.py
def helmholtz_rt(self, temperature: Tensor, volume: Tensor, moles: Tensor) -> Tensor:
    """Evaluate extensive total reduced Helmholtz energy.

    Parameters
    ----------
    temperature
        Temperature in K.
    volume
        Total volume in m3.
    moles
        Component amounts in mol on the final axis.

    Returns
    -------
    Tensor
        Dimensionless extensive ``A/(RT)``.
    """
    total = moles.sum(dim=-1)
    x = moles / total[..., None]
    return total * self.alpha_total(temperature, total / volume, x)

molar_helmholtz_energy

molar_helmholtz_energy(
    temperature: Tensor,
    molar_density: Tensor,
    composition: Tensor,
) -> Tensor

Return total molar Helmholtz energy.

Parameters:

Name Type Description Default
temperature Tensor

Temperature in K, density in mol/m3, and final-axis mole fractions.

required
molar_density Tensor

Temperature in K, density in mol/m3, and final-axis mole fractions.

required
composition Tensor

Temperature in K, density in mol/m3, and final-axis mole fractions.

required

Returns:

Type Description
Tensor

Molar Helmholtz energy in J/mol.

Source code in src/torch_flash/eos/multiparameter.py
def molar_helmholtz_energy(
    self, temperature: Tensor, molar_density: Tensor, composition: Tensor
) -> Tensor:
    """Return total molar Helmholtz energy.

    Parameters
    ----------
    temperature, molar_density, composition
        Temperature in K, density in mol/m3, and final-axis mole fractions.

    Returns
    -------
    Tensor
        Molar Helmholtz energy in J/mol.
    """
    return (
        self.gas_constant
        * temperature
        * self.alpha_total(temperature, molar_density, composition)
    )

molar_entropy

molar_entropy(
    temperature: Tensor,
    molar_density: Tensor,
    composition: Tensor,
) -> Tensor

Return total molar entropy by temperature differentiation.

Parameters:

Name Type Description Default
temperature Tensor

Temperature in K, fixed density in mol/m3, and mole fractions.

required
molar_density Tensor

Temperature in K, fixed density in mol/m3, and mole fractions.

required
composition Tensor

Temperature in K, fixed density in mol/m3, and mole fractions.

required

Returns:

Type Description
Tensor

Molar entropy in J/(mol K).

Source code in src/torch_flash/eos/multiparameter.py
def molar_entropy(
    self, temperature: Tensor, molar_density: Tensor, composition: Tensor
) -> Tensor:
    """Return total molar entropy by temperature differentiation.

    Parameters
    ----------
    temperature, molar_density, composition
        Temperature in K, fixed density in mol/m3, and mole fractions.

    Returns
    -------
    Tensor
        Molar entropy in J/(mol K).
    """
    derivative: Tensor = torch.func.grad(
        lambda current: self.molar_helmholtz_energy(
            current,
            molar_density,
            composition,
        ).sum()
    )(temperature)
    return -derivative

molar_internal_energy

molar_internal_energy(
    temperature: Tensor,
    molar_density: Tensor,
    composition: Tensor,
) -> Tensor

Return total molar internal energy.

Parameters:

Name Type Description Default
temperature Tensor

Temperature in K, density in mol/m3, and mole fractions.

required
molar_density Tensor

Temperature in K, density in mol/m3, and mole fractions.

required
composition Tensor

Temperature in K, density in mol/m3, and mole fractions.

required

Returns:

Type Description
Tensor

Molar internal energy in J/mol.

Source code in src/torch_flash/eos/multiparameter.py
def molar_internal_energy(
    self, temperature: Tensor, molar_density: Tensor, composition: Tensor
) -> Tensor:
    """Return total molar internal energy.

    Parameters
    ----------
    temperature, molar_density, composition
        Temperature in K, density in mol/m3, and mole fractions.

    Returns
    -------
    Tensor
        Molar internal energy in J/mol.
    """
    helmholtz = self.molar_helmholtz_energy(temperature, molar_density, composition)
    return helmholtz + temperature * self.molar_entropy(temperature, molar_density, composition)

molar_heat_capacity_cv

molar_heat_capacity_cv(
    temperature: Tensor,
    molar_density: Tensor,
    composition: Tensor,
) -> Tensor

Return isochoric molar heat capacity.

Parameters:

Name Type Description Default
temperature Tensor

Temperature in K, fixed density in mol/m3, and mole fractions.

required
molar_density Tensor

Temperature in K, fixed density in mol/m3, and mole fractions.

required
composition Tensor

Temperature in K, fixed density in mol/m3, and mole fractions.

required

Returns:

Type Description
Tensor

C_v in J/(mol K).

Source code in src/torch_flash/eos/multiparameter.py
def molar_heat_capacity_cv(
    self, temperature: Tensor, molar_density: Tensor, composition: Tensor
) -> Tensor:
    """Return isochoric molar heat capacity.

    Parameters
    ----------
    temperature, molar_density, composition
        Temperature in K, fixed density in mol/m3, and mole fractions.

    Returns
    -------
    Tensor
        ``C_v`` in J/(mol K).
    """
    derivative: Tensor = torch.func.grad(
        lambda current: self.molar_internal_energy(
            current,
            molar_density,
            composition,
        ).sum()
    )(temperature)
    return derivative

molar_heat_capacity_cp

molar_heat_capacity_cp(
    temperature: Tensor,
    molar_density: Tensor,
    composition: Tensor,
) -> Tensor

Return isobaric molar heat capacity from Helmholtz derivatives.

Parameters:

Name Type Description Default
temperature Tensor

Temperature in K, density in mol/m3, and mole fractions.

required
molar_density Tensor

Temperature in K, density in mol/m3, and mole fractions.

required
composition Tensor

Temperature in K, density in mol/m3, and mole fractions.

required

Returns:

Type Description
Tensor

C_p in J/(mol K).

Source code in src/torch_flash/eos/multiparameter.py
def molar_heat_capacity_cp(
    self, temperature: Tensor, molar_density: Tensor, composition: Tensor
) -> Tensor:
    """Return isobaric molar heat capacity from Helmholtz derivatives.

    Parameters
    ----------
    temperature, molar_density, composition
        Temperature in K, density in mol/m3, and mole fractions.

    Returns
    -------
    Tensor
        ``C_p`` in J/(mol K).
    """
    volume = molar_density.reciprocal()
    cv = self.molar_heat_capacity_cv(temperature, molar_density, composition)
    dp_dt: Tensor = torch.func.grad(
        lambda current: self.pressure(current, volume, composition).sum()
    )(temperature)
    dp_drho: Tensor = torch.func.grad(
        lambda density: self.pressure(
            temperature,
            density.reciprocal(),
            composition,
        ).sum()
    )(molar_density)
    return cv + temperature * dp_dt.square() / (molar_density.square() * dp_drho)

molar_enthalpy

molar_enthalpy(
    temperature: Tensor,
    molar_density: Tensor,
    composition: Tensor,
) -> Tensor

Return total molar enthalpy.

Parameters:

Name Type Description Default
temperature Tensor

Temperature in K, density in mol/m3, and mole fractions.

required
molar_density Tensor

Temperature in K, density in mol/m3, and mole fractions.

required
composition Tensor

Temperature in K, density in mol/m3, and mole fractions.

required

Returns:

Type Description
Tensor

Molar enthalpy in J/mol.

Source code in src/torch_flash/eos/multiparameter.py
def molar_enthalpy(
    self, temperature: Tensor, molar_density: Tensor, composition: Tensor
) -> Tensor:
    """Return total molar enthalpy.

    Parameters
    ----------
    temperature, molar_density, composition
        Temperature in K, density in mol/m3, and mole fractions.

    Returns
    -------
    Tensor
        Molar enthalpy in J/mol.
    """
    volume = molar_density.reciprocal()
    return (
        self.molar_internal_energy(temperature, molar_density, composition)
        + self.pressure(temperature, volume, composition) * volume
    )

molar_gibbs_energy

molar_gibbs_energy(
    temperature: Tensor,
    molar_density: Tensor,
    composition: Tensor,
) -> Tensor

Return total molar Gibbs energy.

Parameters:

Name Type Description Default
temperature Tensor

Temperature in K, density in mol/m3, and mole fractions.

required
molar_density Tensor

Temperature in K, density in mol/m3, and mole fractions.

required
composition Tensor

Temperature in K, density in mol/m3, and mole fractions.

required

Returns:

Type Description
Tensor

Molar Gibbs energy in J/mol.

Source code in src/torch_flash/eos/multiparameter.py
def molar_gibbs_energy(
    self, temperature: Tensor, molar_density: Tensor, composition: Tensor
) -> Tensor:
    """Return total molar Gibbs energy.

    Parameters
    ----------
    temperature, molar_density, composition
        Temperature in K, density in mol/m3, and mole fractions.

    Returns
    -------
    Tensor
        Molar Gibbs energy in J/mol.
    """
    volume = molar_density.reciprocal()
    return (
        self.molar_helmholtz_energy(temperature, molar_density, composition)
        + self.pressure(temperature, volume, composition) * volume
    )

chemical_potentials

chemical_potentials(
    temperature: Tensor,
    molar_volume: Tensor,
    composition: Tensor,
) -> Tensor

Return total component chemical potentials.

Parameters:

Name Type Description Default
temperature Tensor

Temperature in K.

required
molar_volume Tensor

Homogeneous molar volume in m3/mol.

required
composition Tensor

Mole fractions on the final axis.

required

Returns:

Type Description
Tensor

Component chemical potentials in J/mol.

Source code in src/torch_flash/eos/multiparameter.py
def chemical_potentials(
    self, temperature: Tensor, molar_volume: Tensor, composition: Tensor
) -> Tensor:
    """Return total component chemical potentials.

    Parameters
    ----------
    temperature
        Temperature in K.
    molar_volume
        Homogeneous molar volume in m3/mol.
    composition
        Mole fractions on the final axis.

    Returns
    -------
    Tensor
        Component chemical potentials in J/mol.
    """
    x = normalize_composition(composition)
    reduced: Tensor = torch.func.grad(
        lambda moles: self.helmholtz_rt(
            temperature,
            molar_volume,
            moles,
        ).sum()
    )(x)
    return self.gas_constant * temperature * reduced

speed_of_sound

speed_of_sound(
    temperature: Tensor,
    molar_density: Tensor,
    composition: Tensor,
) -> Tensor

Return homogeneous-phase speed of sound.

Parameters:

Name Type Description Default
temperature Tensor

Temperature in K, density in mol/m3, and mole fractions.

required
molar_density Tensor

Temperature in K, density in mol/m3, and mole fractions.

required
composition Tensor

Temperature in K, density in mol/m3, and mole fractions.

required

Returns:

Type Description
Tensor

Speed of sound in m/s.

Source code in src/torch_flash/eos/multiparameter.py
def speed_of_sound(
    self, temperature: Tensor, molar_density: Tensor, composition: Tensor
) -> Tensor:
    """Return homogeneous-phase speed of sound.

    Parameters
    ----------
    temperature, molar_density, composition
        Temperature in K, density in mol/m3, and mole fractions.

    Returns
    -------
    Tensor
        Speed of sound in m/s.
    """
    x = normalize_composition(composition)
    dp_drho: Tensor = torch.func.grad(
        lambda density: self.pressure(
            temperature,
            density.reciprocal(),
            x,
        ).sum()
    )(molar_density)
    cv = self.molar_heat_capacity_cv(temperature, molar_density, x)
    cp = self.molar_heat_capacity_cp(temperature, molar_density, x)
    mixture_molar_mass = torch.sum(x * self.molar_mass, dim=-1)
    return torch.sqrt((cp / cv) * dp_drho / mixture_molar_mass)

pressure

pressure(
    temperature: Tensor,
    molar_volume: Tensor,
    composition: Tensor,
) -> Tensor

Return pressure from the exact residual-Helmholtz density derivative.

Source code in src/torch_flash/eos/multiparameter.py
def pressure(self, temperature: Tensor, molar_volume: Tensor, composition: Tensor) -> Tensor:
    """Return pressure from the exact residual-Helmholtz density derivative."""
    x = normalize_composition(composition)
    molar_density = molar_volume.reciprocal()
    reducing_temperature, reducing_density = self._reducing_functions_normalized(x)
    delta = molar_density / reducing_density
    tau = reducing_temperature / temperature
    alpha_delta = self._alpha_residual_delta_reduced(delta, tau, x)
    return self.gas_constant * temperature * molar_density * (1.0 + delta * alpha_delta)

molar_volume

molar_volume(
    temperature: Tensor,
    pressure: Tensor,
    composition: Tensor,
    phase: PhaseKind = "stable",
) -> Tensor

Solve and select a homogeneous density root.

Phase-specific states first use a differentiable logarithmic-density Newton solve. If it fails, a logarithmic scan brackets every mechanically admissible positive root before bisection and Newton polishing. This conservative fallback matters because Helmholtz mixture models can have three density roots below the mixture critical locus, and an initial guess can cross a spinodal during phase-envelope calculations.

Source code in src/torch_flash/eos/multiparameter.py
def molar_volume(
    self,
    temperature: Tensor,
    pressure: Tensor,
    composition: Tensor,
    phase: PhaseKind = "stable",
) -> Tensor:
    """Solve and select a homogeneous density root.

    Phase-specific states first use a differentiable logarithmic-density
    Newton solve.  If it fails, a logarithmic scan brackets every
    mechanically admissible positive root before bisection and Newton
    polishing.  This conservative fallback matters because Helmholtz
    mixture models can have three density roots below the mixture critical
    locus, and an initial guess can cross a spinodal during phase-envelope
    calculations.
    """
    x = normalize_composition(composition)
    if x.shape[-1] != len(self.names):
        raise ValueError("multiparameter composition has the wrong number of components")
    if phase not in ("vapor", "liquid", "stable"):
        raise ValueError(f"unknown phase root {phase!r}")
    batch_shape = torch.broadcast_shapes(
        temperature.shape,
        pressure.shape,
        x.shape[:-1],
    )
    if batch_shape:
        temperature = torch.broadcast_to(temperature, batch_shape)
        pressure = torch.broadcast_to(pressure, batch_shape)
        x = torch.broadcast_to(x, (*batch_shape, len(self.names)))
        if phase in ("vapor", "liquid"):
            return self._batched_phase_volume(temperature, pressure, x, phase)
        if phase == "stable":
            return self._batched_stable_volume(temperature, pressure, x)

    ideal_density = pressure / (self.gas_constant * temperature)
    density_scale = torch.sum(x * self.critical_density)
    reducing_temperature, _ = self.reducing_functions(x)

    def residual(current: Tensor) -> Tensor:
        volume = torch.exp(-current)
        return (self.pressure(temperature, volume, x) - pressure) / pressure

    needs_implicit_gradient = (
        temperature.requires_grad
        or pressure.requires_grad
        or x.requires_grad
        or any(parameter.requires_grad for parameter in self.parameters())
    )

    def volume_with_implicit_gradient(current: Tensor) -> Tensor:
        """Restore root derivatives after detached numerical iteration."""
        if needs_implicit_gradient:
            value = residual(current)
            derivative: Tensor = torch.func.grad(residual)(current)
            current = current - value / derivative
        return torch.exp(-current)

    # Locate a phase-specific root with detached, safeguarded secant steps.
    # One centered slope initializes the iteration; subsequent slopes reuse
    # adjacent residual values instead of evaluating pressure twice more at
    # every step. One exact correction at the converged root restores the
    # implicit state and parameter gradients. The scan below remains the
    # conservative fallback near spinodals or when the conventional seed
    # lies in the basin of another root.
    if phase != "stable":
        density = (
            ideal_density
            if phase == "vapor"
            else torch.maximum(ideal_density, 3.0 * density_scale)
        )
        log_density = torch.log(density)
        slope_offset = log_density.new_tensor(1.0e-4)
        value = residual(log_density)
        derivative = (
            residual(log_density + slope_offset) - residual(log_density - slope_offset)
        ) / (2.0 * slope_offset)
        for _ in range(20):
            if float(value.detach().abs()) <= 1.0e-10:
                solved_density = torch.exp(log_density)
                phase_consistent = (
                    solved_density <= density_scale
                    if phase == "vapor"
                    else solved_density >= density_scale
                )
                if bool((temperature >= reducing_temperature) | phase_consistent):
                    return volume_with_implicit_gradient(log_density)
                break
            step = torch.clamp(-value / derivative, -0.5, 0.5)
            if not bool(torch.isfinite(step)):
                break
            next_log_density = (log_density + step).detach()
            next_value = residual(next_log_density)
            log_density_step = next_log_density - log_density
            next_derivative = (next_value - value) / log_density_step
            if not bool(
                torch.isfinite(next_derivative)
                & (next_derivative.abs() > torch.finfo(next_derivative.dtype).tiny)
            ):
                next_derivative = (
                    residual(next_log_density + slope_offset)
                    - residual(next_log_density - slope_offset)
                ) / (2.0 * slope_offset)
            log_density = next_log_density
            value = next_value
            derivative = next_derivative

    minimum = torch.minimum(ideal_density * 1.0e-4, density_scale * 1.0e-8)
    maximum = torch.maximum(
        ideal_density * 10.0,
        100.0 * torch.max(self.critical_density),
    )
    grid = torch.logspace(
        float(torch.log10(minimum.detach())),
        float(torch.log10(maximum.detach())),
        96,
        dtype=temperature.dtype,
        device=temperature.device,
    )

    brackets: list[tuple[Tensor, Tensor]] = []
    left = torch.log(grid[0])
    left_value = residual(left)
    for density in grid[1:]:
        right = torch.log(density)
        right_value = residual(right)
        finite = bool(torch.isfinite(left_value) & torch.isfinite(right_value))
        changes_sign = bool(torch.signbit(left_value) != torch.signbit(right_value))
        if finite and (
            float(left_value.detach()) == 0.0
            or float(right_value.detach()) == 0.0
            or changes_sign
        ):
            brackets.append((left, right))
        left, left_value = right, right_value
    if not brackets:
        raise ConvergenceError("multiparameter density solve did not converge")

    roots: list[Tensor] = []
    for left, right in brackets:
        left_value = residual(left)
        midpoint = 0.5 * (left + right)
        for _ in range(80):
            midpoint = 0.5 * (left + right)
            midpoint_value = residual(midpoint)
            if float(midpoint_value.detach().abs()) <= 1.0e-12:
                break
            if bool(torch.signbit(left_value) != torch.signbit(midpoint_value)):
                right = midpoint
            else:
                left = midpoint
                left_value = midpoint_value

        root = torch.exp(midpoint)
        root_residual = residual(midpoint)
        if not bool(torch.isfinite(root_residual) & (root_residual.abs() <= 1.0e-8)):
            # A sign change across a discontinuity or overflow is not a
            # density root. This occurs in very cold, dense Helmholtz
            # states and must not enter the Gibbs-root comparison.
            continue
        if not roots or abs(float((root / roots[-1] - 1.0).detach())) > 1.0e-8:
            roots.append(root)

    if not roots:
        raise ConvergenceError("multiparameter density solve did not find an admissible root")
    if phase == "vapor":
        density = roots[0]
    elif phase == "liquid":
        density = roots[-1]
    elif phase == "stable":
        gibbs: list[Tensor] = []
        for density_root in roots:
            volume = density_root.reciprocal()
            z = pressure * volume / (self.gas_constant * temperature)
            residual_helmholtz = self.residual_helmholtz_rt(temperature, volume, x)
            gibbs.append(residual_helmholtz + z - 1.0 - torch.log(z))
        density = roots[int(torch.argmin(torch.stack(gibbs)).detach())]
    return volume_with_implicit_gradient(torch.log(density))

select_z

select_z(
    temperature: Tensor,
    pressure: Tensor,
    composition: Tensor,
    phase: PhaseKind = "stable",
) -> Tensor

Return the compressibility factor for a selected density root.

Parameters:

Name Type Description Default
temperature Tensor

Temperature in K and pressure in Pa.

required
pressure Tensor

Temperature in K and pressure in Pa.

required
composition Tensor

Mole fractions on the final axis.

required
phase PhaseKind

Liquid, vapor, or stable-root request.

'stable'

Returns:

Type Description
Tensor

Dimensionless Z = Pv/(RT).

Source code in src/torch_flash/eos/multiparameter.py
def select_z(
    self,
    temperature: Tensor,
    pressure: Tensor,
    composition: Tensor,
    phase: PhaseKind = "stable",
) -> Tensor:
    """Return the compressibility factor for a selected density root.

    Parameters
    ----------
    temperature, pressure
        Temperature in K and pressure in Pa.
    composition
        Mole fractions on the final axis.
    phase
        Liquid, vapor, or stable-root request.

    Returns
    -------
    Tensor
        Dimensionless ``Z = Pv/(RT)``.
    """
    volume = self.molar_volume(temperature, pressure, composition, phase)
    return pressure * volume / (self.gas_constant * temperature)

log_fugacity_coefficients

log_fugacity_coefficients(
    temperature: Tensor,
    pressure: Tensor,
    composition: Tensor,
    phase: PhaseKind = "stable",
) -> Tensor

Return component log fugacity coefficients.

Parameters:

Name Type Description Default
temperature Tensor

Temperature in K and pressure in Pa.

required
pressure Tensor

Temperature in K and pressure in Pa.

required
composition Tensor

Mole fractions on the final axis.

required
phase PhaseKind

Liquid, vapor, or stable-root request.

'stable'

Returns:

Type Description
Tensor

Dimensionless ln(phi_i) with a final component axis.

Notes

Residual chemical potentials are obtained by differentiating the extensive residual Helmholtz energy at fixed volume.

Source code in src/torch_flash/eos/multiparameter.py
def log_fugacity_coefficients(
    self,
    temperature: Tensor,
    pressure: Tensor,
    composition: Tensor,
    phase: PhaseKind = "stable",
) -> Tensor:
    """Return component log fugacity coefficients.

    Parameters
    ----------
    temperature, pressure
        Temperature in K and pressure in Pa.
    composition
        Mole fractions on the final axis.
    phase
        Liquid, vapor, or stable-root request.

    Returns
    -------
    Tensor
        Dimensionless ``ln(phi_i)`` with a final component axis.

    Notes
    -----
    Residual chemical potentials are obtained by differentiating the
    extensive residual Helmholtz energy at fixed volume.
    """
    x = normalize_composition(composition)
    volume = self.molar_volume(temperature, pressure, x, phase)
    residual_mu: Tensor = torch.func.grad(
        lambda moles: self.residual_helmholtz_rt(
            temperature,
            volume,
            moles,
        ).sum()
    )(x)
    z = pressure * volume / (self.gas_constant * temperature)
    return residual_mu - torch.log(z)[..., None]

MultiparameterMetadata dataclass

MultiparameterMetadata(
    model: str,
    reference: str,
    version: str,
    validated_components: tuple[str, ...],
)

Identity and validation scope of one multiparameter coefficient set.

Attributes:

Name Type Description
model str

Exact published model or fitted-parameter identity.

reference str

Defining bibliographic reference.

version str

Version of the serialized coefficient set.

validated_components tuple[str, ...]

Component inventory for which the parameterization is defined.

NonAnalyticTerms dataclass

NonAnalyticTerms(
    n: Tensor,
    capital_a: Tensor,
    capital_b: Tensor,
    capital_c: Tensor,
    capital_d: Tensor,
    a: Tensor,
    b: Tensor,
    beta: Tensor,
)

Span--Wagner non-analytic critical-region coefficient arrays.

Attributes:

Name Type Description
n Tensor

Term amplitudes.

capital_a, capital_b, capital_c, capital_d

Non-analytic shape coefficients.

a, b, beta

Critical exponents and crossover parameters. Every tensor must have the same shape as n.

VolumeTranslation dataclass

VolumeTranslation(
    reference_shift: Tensor,
    temperature_slope: Tensor,
    reference_temperature: float = 288.15,
    source: str = "custom",
)

Component translations in the additive torch-flash convention.

reference_shift is in m3/mol, temperature_slope in m3/(mol K), and reference_temperature in K. A published positive Péneloux c therefore appears as a negative reference_shift.

Attributes:

Name Type Description
reference_shift Tensor

Per-component additive shift in m3/mol at reference_temperature.

temperature_slope Tensor

Per-component linear slope in m3/(mol K).

reference_temperature float

Positive reference temperature in K.

source str

Parameter or fitting provenance identifier.

constant classmethod

constant(
    shift: Tensor, *, source: str = "custom"
) -> VolumeTranslation

Construct a temperature-independent additive translation.

Parameters:

Name Type Description Default
shift Tensor

Per-component additive shifts in m3/mol.

required
source str

Human-readable provenance identifier.

'custom'

Returns:

Type Description
VolumeTranslation

Translation with zero temperature slope.

Source code in src/torch_flash/eos/volume_translation.py
@classmethod
def constant(cls, shift: Tensor, *, source: str = "custom") -> VolumeTranslation:
    """Construct a temperature-independent additive translation.

    Parameters
    ----------
    shift
        Per-component additive shifts in m3/mol.
    source
        Human-readable provenance identifier.

    Returns
    -------
    VolumeTranslation
        Translation with zero temperature slope.
    """
    return cls(shift, torch.zeros_like(shift), source=source)

at_temperature

at_temperature(temperature: Tensor) -> Tensor

Evaluate component additive shifts at temperature.

Parameters:

Name Type Description Default
temperature Tensor

Temperature in K with arbitrary leading batch dimensions.

required

Returns:

Type Description
Tensor

Shifts in m3/mol with a final component axis.

Source code in src/torch_flash/eos/volume_translation.py
def at_temperature(self, temperature: Tensor) -> Tensor:
    """Evaluate component additive shifts at temperature.

    Parameters
    ----------
    temperature
        Temperature in K with arbitrary leading batch dimensions.

    Returns
    -------
    Tensor
        Shifts in m3/mol with a final component axis.
    """
    return (
        self.reference_shift
        + (temperature[..., None] - self.reference_temperature) * self.temperature_slope
    )

to

to(
    *,
    dtype: dtype | None = None,
    device: device | str | None = None,
) -> VolumeTranslation

Copy translation coefficients to a dtype and device.

Parameters:

Name Type Description Default
dtype dtype | None

Optional PyTorch destination options.

None
device dtype | None

Optional PyTorch destination options.

None

Returns:

Type Description
VolumeTranslation

New immutable translation preserving metadata.

Source code in src/torch_flash/eos/volume_translation.py
def to(
    self,
    *,
    dtype: torch.dtype | None = None,
    device: torch.device | str | None = None,
) -> VolumeTranslation:
    """Copy translation coefficients to a dtype and device.

    Parameters
    ----------
    dtype, device
        Optional PyTorch destination options.

    Returns
    -------
    VolumeTranslation
        New immutable translation preserving metadata.
    """
    return VolumeTranslation(
        self.reference_shift.to(dtype=dtype, device=device),
        self.temperature_slope.to(dtype=dtype, device=device),
        self.reference_temperature,
        self.source,
    )

cpa_eos

cpa_eos(
    names: tuple[str, ...],
    parameter_set: ParameterSource = "cpa.folas-2005",
    *,
    kij: Tensor | None = None,
    kij_a: Tensor | None = None,
    kij_b: Tensor | None = None,
    lij: Tensor | None = None,
    cross_association_energy: Tensor | None = None,
    cross_association_volume: Tensor | None = None,
    combining_rule: CombiningRule | None = None,
    trainable: bool = False,
    trainable_lij: bool = False,
    association_iterations: int = 10,
    association_newton_iterations: int = 8,
    dtype: dtype | None = None,
    device: device | str | None = None,
) -> CPAEOS

Construct CPA from a bundled/custom YAML set or use :class:CPAEOS.

Parameters:

Name Type Description Default
names tuple[str, ...]

Component names in model tensor order.

required
parameter_set ParameterSource

Bundled identifier, custom YAML path, mapping, or loaded CPA set.

'cpa.folas-2005'
kij Tensor | None

Optional physical-term interaction overrides.

None
kij_a Tensor | None

Optional physical-term interaction overrides.

None
kij_b Tensor | None

Optional physical-term interaction overrides.

None
lij Tensor | None

Optional physical-term interaction overrides.

None
cross_association_energy Tensor | None

Optional cross-association pair overrides.

None
cross_association_volume Tensor | None

Optional cross-association pair overrides.

None
combining_rule CombiningRule | None

Override the parameter set's CR1/ECR default.

None
trainable bool

Enable fitting of attraction/association or covolume interactions.

False
trainable_lij bool

Enable fitting of attraction/association or covolume interactions.

False
association_iterations int

Site-fraction solver iteration budgets.

10
association_newton_iterations int

Site-fraction solver iteration budgets.

10
dtype dtype | None

Tensor placement.

None
device dtype | None

Tensor placement.

None

Returns:

Type Description
CPAEOS

Configured CPA model.

Raises:

Type Description
KeyError

If a requested component has no pure parameters.

ParameterDatabaseError

If the source model, parameter records, or combining rule is invalid.

Notes

Direct construction with a tuple of :class:CPAComponent remains the explicit in-memory API for fitting new species or parameterizations. lij optionally modifies the physical SRK co-volume rule; trainable_lij controls it independently of the attraction parameters.

Source code in src/torch_flash/eos/cpa.py
def cpa_eos(
    names: tuple[str, ...],
    parameter_set: ParameterSource = "cpa.folas-2005",
    *,
    kij: Tensor | None = None,
    kij_a: Tensor | None = None,
    kij_b: Tensor | None = None,
    lij: Tensor | None = None,
    cross_association_energy: Tensor | None = None,
    cross_association_volume: Tensor | None = None,
    combining_rule: CombiningRule | None = None,
    trainable: bool = False,
    trainable_lij: bool = False,
    association_iterations: int = 10,
    association_newton_iterations: int = 8,
    dtype: torch.dtype | None = None,
    device: torch.device | str | None = None,
) -> CPAEOS:
    """Construct CPA from a bundled/custom YAML set or use :class:`CPAEOS`.

    Parameters
    ----------
    names
        Component names in model tensor order.
    parameter_set
        Bundled identifier, custom YAML path, mapping, or loaded CPA set.
    kij, kij_a, kij_b, lij
        Optional physical-term interaction overrides.
    cross_association_energy, cross_association_volume
        Optional cross-association pair overrides.
    combining_rule
        Override the parameter set's CR1/ECR default.
    trainable, trainable_lij
        Enable fitting of attraction/association or covolume interactions.
    association_iterations, association_newton_iterations
        Site-fraction solver iteration budgets.
    dtype, device
        Tensor placement.

    Returns
    -------
    CPAEOS
        Configured CPA model.

    Raises
    ------
    KeyError
        If a requested component has no pure parameters.
    ParameterDatabaseError
        If the source model, parameter records, or combining rule is invalid.

    Notes
    -----
    Direct construction with a tuple of :class:`CPAComponent` remains the
    explicit in-memory API for fitting new species or parameterizations.
    ``lij`` optionally modifies the physical SRK co-volume rule;
    ``trainable_lij`` controls it independently of the attraction parameters.
    """
    supplied_tensors = tuple(
        value
        for value in (
            kij,
            kij_a,
            kij_b,
            lij,
            cross_association_energy,
            cross_association_volume,
        )
        if value is not None
    )
    runtime_dtype, runtime_device = resolve_tensor_options(dtype, device)
    dtype = supplied_tensors[0].dtype if dtype is None and supplied_tensors else runtime_dtype
    device = supplied_tensors[0].device if device is None and supplied_tensors else runtime_device
    selected = tuple(canonical_component_name(name) for name in names)
    loaded = load_model_parameters(parameter_set)
    if loaded.model_kind != "cpa":
        raise ParameterDatabaseError(f"{loaded.identifier!r} is {loaded.model_kind!r}, not 'cpa'")
    records = loaded.parameters.get("components")
    if not isinstance(records, Mapping):
        raise ParameterDatabaseError(f"{loaded.identifier!r} requires a components mapping")
    parameters: list[CPAComponent] = []
    for name in selected:
        record = records.get(name)
        if not isinstance(record, Mapping):
            raise KeyError(f"{loaded.identifier} CPA parameters are unavailable for {name!r}")
        parameters.append(_cpa_component(name, record))
    default_rule = loaded.parameters.get("default_combining_rule", "ECR")
    resolved_rule = default_rule if combining_rule is None else combining_rule
    if resolved_rule not in ("CR1", "ECR"):
        raise ParameterDatabaseError(f"unsupported CPA combining rule {resolved_rule!r}")
    if kij is None and kij_a is None and kij_b is None:
        kij, kij_a, kij_b = _database_binary_interactions(
            loaded.parameters.get("binary_interactions"),
            selected,
            loaded.identifier,
            dtype=dtype,
            device=device,
        )
    if cross_association_energy is None and cross_association_volume is None:
        cross_association_energy, cross_association_volume = _database_cross_association(
            loaded.parameters.get("cross_association"),
            selected,
            loaded.identifier,
            dtype=dtype,
            device=device,
        )
    return CPAEOS(
        tuple(parameters),
        kij=kij,
        kij_a=kij_a,
        kij_b=kij_b,
        lij=lij,
        cross_association_energy=cross_association_energy,
        cross_association_volume=cross_association_volume,
        combining_rule=resolved_rule,
        trainable=trainable,
        trainable_lij=trainable_lij,
        association_iterations=association_iterations,
        association_newton_iterations=association_newton_iterations,
        dtype=dtype,
        device=device,
    )

cpa_folas_2005

cpa_folas_2005(
    names: tuple[str, ...],
    *,
    kij: Tensor | None = None,
    lij: Tensor | None = None,
    combining_rule: CombiningRule = "ECR",
    trainable: bool = False,
    trainable_lij: bool = False,
    dtype: dtype | None = None,
    device: device | str | None = None,
) -> CPAEOS

Construct CPA with the Folas et al. (2005), Table 1, pure parameters.

Parameters:

Name Type Description Default
names tuple[str, ...]

Component names in model order.

required
kij Tensor | None

Optional physical-term attraction and covolume interactions.

None
lij Tensor | None

Optional physical-term attraction and covolume interactions.

None
combining_rule CombiningRule

"CR1" or "ECR" association combining rule.

'ECR'
trainable bool

Enable interaction-parameter fitting.

False
trainable_lij bool

Enable interaction-parameter fitting.

False
dtype dtype | None

Tensor placement.

None
device dtype | None

Tensor placement.

None

Returns:

Type Description
CPAEOS

Configured Folas-2005 parameterization defined by doi:10.1021/ie048832j.

Source code in src/torch_flash/eos/cpa.py
def cpa_folas_2005(
    names: tuple[str, ...],
    *,
    kij: Tensor | None = None,
    lij: Tensor | None = None,
    combining_rule: CombiningRule = "ECR",
    trainable: bool = False,
    trainable_lij: bool = False,
    dtype: torch.dtype | None = None,
    device: torch.device | str | None = None,
) -> CPAEOS:
    """Construct CPA with the Folas et al. (2005), Table 1, pure parameters.

    Parameters
    ----------
    names
        Component names in model order.
    kij, lij
        Optional physical-term attraction and covolume interactions.
    combining_rule
        ``"CR1"`` or ``"ECR"`` association combining rule.
    trainable, trainable_lij
        Enable interaction-parameter fitting.
    dtype, device
        Tensor placement.

    Returns
    -------
    CPAEOS
        Configured Folas-2005 parameterization defined by
        doi:10.1021/ie048832j.
    """
    return cpa_eos(
        names,
        "cpa.folas-2005",
        kij=kij,
        lij=lij,
        combining_rule=combining_rule,
        trainable=trainable,
        trainable_lij=trainable_lij,
        dtype=dtype,
        device=device,
    )

cpa_oliveira_2007

cpa_oliveira_2007(
    names: tuple[str, ...],
    *,
    kij: Tensor | None = None,
    lij: Tensor | None = None,
    cross_association_energy: Tensor | None = None,
    cross_association_volume: Tensor | None = None,
    trainable: bool = False,
    trainable_lij: bool = False,
    dtype: dtype | None = None,
    device: device | str | None = None,
) -> CPAEOS

Construct the hydrocarbon-water CPA parameterization of Oliveira et al.

Parameters:

Name Type Description Default
names tuple[str, ...]

Component names in model order.

required
kij Tensor | None

Optional physical-term interaction overrides.

None
lij Tensor | None

Optional physical-term interaction overrides.

None
cross_association_energy Tensor | None

Optional modified-CR1 solvation overrides in J/mol and dimensionless association volume.

None
cross_association_volume Tensor | None

Optional modified-CR1 solvation overrides in J/mol and dimensionless association volume.

None
trainable bool

Enable interaction-parameter fitting.

False
trainable_lij bool

Enable interaction-parameter fitting.

False
dtype dtype | None

Tensor placement.

None
device dtype | None

Tensor placement.

None

Returns:

Type Description
CPAEOS

Configured Oliveira-2007 hydrocarbon-water parameterization.

Notes

Pure parameters and constant physical-term interactions are from Tables 1-3 of Oliveira, Coutinho, and Queimada, Fluid Phase Equilibria 258 (2007) 58-66, doi:10.1016/j.fluid.2007.05.023. Aromatic-water solvation uses the paper's modified CR-1 cross-association parameters.

Source code in src/torch_flash/eos/cpa.py
def cpa_oliveira_2007(
    names: tuple[str, ...],
    *,
    kij: Tensor | None = None,
    lij: Tensor | None = None,
    cross_association_energy: Tensor | None = None,
    cross_association_volume: Tensor | None = None,
    trainable: bool = False,
    trainable_lij: bool = False,
    dtype: torch.dtype | None = None,
    device: torch.device | str | None = None,
) -> CPAEOS:
    """Construct the hydrocarbon-water CPA parameterization of Oliveira et al.

    Parameters
    ----------
    names
        Component names in model order.
    kij, lij
        Optional physical-term interaction overrides.
    cross_association_energy, cross_association_volume
        Optional modified-CR1 solvation overrides in J/mol and dimensionless
        association volume.
    trainable, trainable_lij
        Enable interaction-parameter fitting.
    dtype, device
        Tensor placement.

    Returns
    -------
    CPAEOS
        Configured Oliveira-2007 hydrocarbon-water parameterization.

    Notes
    -----
    Pure parameters and constant physical-term interactions are from Tables
    1-3 of Oliveira, Coutinho, and Queimada, Fluid Phase Equilibria 258
    (2007) 58-66, doi:10.1016/j.fluid.2007.05.023. Aromatic-water solvation
    uses the paper's modified CR-1 cross-association parameters.
    """
    return cpa_eos(
        names,
        "cpa.oliveira-2007-hydrocarbon-water",
        kij=kij,
        lij=lij,
        cross_association_energy=cross_association_energy,
        cross_association_volume=cross_association_volume,
        combining_rule="CR1",
        trainable=trainable,
        trainable_lij=trainable_lij,
        dtype=dtype,
        device=device,
    )

cpa_yan_2009

cpa_yan_2009(
    names: tuple[str, ...],
    *,
    kij: Tensor | None = None,
    kij_a: Tensor | None = None,
    kij_b: Tensor | None = None,
    lij: Tensor | None = None,
    trainable: bool = False,
    trainable_lij: bool = False,
    dtype: dtype | None = None,
    device: device | str | None = None,
) -> CPAEOS

Construct reservoir-fluid CPA with Yan et al.'s A + B/T water BIPs.

Parameters:

Name Type Description Default
names tuple[str, ...]

Component names in model order.

required
kij Tensor | None

Optional constant or A + B/T attraction interactions and optional covolume interactions.

None
kij_a Tensor | None

Optional constant or A + B/T attraction interactions and optional covolume interactions.

None
kij_b Tensor | None

Optional constant or A + B/T attraction interactions and optional covolume interactions.

None
lij Tensor | None

Optional constant or A + B/T attraction interactions and optional covolume interactions.

None
trainable bool

Enable interaction-parameter fitting.

False
trainable_lij bool

Enable interaction-parameter fitting.

False
dtype dtype | None

Tensor placement.

None
device dtype | None

Tensor placement.

None

Returns:

Type Description
CPAEOS

Yan-2009 reservoir-fluid parameterization from Fluid Phase Equilibria 276 (2009) 75-85, doi:10.1016/j.fluid.2008.10.007.

Source code in src/torch_flash/eos/cpa.py
def cpa_yan_2009(
    names: tuple[str, ...],
    *,
    kij: Tensor | None = None,
    kij_a: Tensor | None = None,
    kij_b: Tensor | None = None,
    lij: Tensor | None = None,
    trainable: bool = False,
    trainable_lij: bool = False,
    dtype: torch.dtype | None = None,
    device: torch.device | str | None = None,
) -> CPAEOS:
    """Construct reservoir-fluid CPA with Yan et al.'s ``A + B/T`` water BIPs.

    Parameters
    ----------
    names
        Component names in model order.
    kij, kij_a, kij_b, lij
        Optional constant or ``A + B/T`` attraction interactions and optional
        covolume interactions.
    trainable, trainable_lij
        Enable interaction-parameter fitting.
    dtype, device
        Tensor placement.

    Returns
    -------
    CPAEOS
        Yan-2009 reservoir-fluid parameterization from *Fluid Phase
        Equilibria* 276 (2009) 75-85,
        doi:10.1016/j.fluid.2008.10.007.
    """
    return cpa_eos(
        names,
        "cpa.yan-2009-reservoir-fluids",
        kij=kij,
        kij_a=kij_a,
        kij_b=kij_b,
        lij=lij,
        trainable=trainable,
        trainable_lij=trainable_lij,
        dtype=dtype,
        device=device,
    )

cpa_components_from_cuts

cpa_components_from_cuts(
    cuts: tuple[PseudoComponentCut, ...],
    *,
    parameter_set: ParameterSource = "cpa.yan-2009-reservoir-fluids",
    dtype: dtype | None = None,
    device: device | str | None = None,
) -> CPACharacterizedComponents

Characterize analyzed C7+ cuts and normalize their internal fractions.

Parameters:

Name Type Description Default
cuts tuple[PseudoComponentCut, ...]

Measured/estimated heavy-end cuts in desired component order.

required
parameter_set ParameterSource

CPA parameter source containing Yan-style heavy-end correlations.

'cpa.yan-2009-reservoir-fluids'
dtype dtype | None

Placement of the normalized fraction tensor.

None
device dtype | None

Placement of the normalized fraction tensor.

None

Returns:

Type Description
CPACharacterizedComponents

CPA records, normalized internal cut fractions, and complete monomer characterization diagnostics.

Raises:

Type Description
ValueError

If no cuts are supplied or their fractions do not define a positive finite distribution.

Notes

This function covers the EoS-parameter step of a plus-fraction workflow. It deliberately does not invent a carbon-number distribution when only a bulk C7+ molecular weight is known; splitting or lumping must be performed upstream with measured cuts or an explicitly selected Whitson/Pedersen distribution model.

Source code in src/torch_flash/eos/cpa_heavy_end.py
def cpa_components_from_cuts(
    cuts: tuple[PseudoComponentCut, ...],
    *,
    parameter_set: ParameterSource = "cpa.yan-2009-reservoir-fluids",
    dtype: torch.dtype | None = None,
    device: torch.device | str | None = None,
) -> CPACharacterizedComponents:
    """Characterize analyzed C7+ cuts and normalize their internal fractions.

    Parameters
    ----------
    cuts
        Measured/estimated heavy-end cuts in desired component order.
    parameter_set
        CPA parameter source containing Yan-style heavy-end correlations.
    dtype, device
        Placement of the normalized fraction tensor.

    Returns
    -------
    CPACharacterizedComponents
        CPA records, normalized internal cut fractions, and complete monomer
        characterization diagnostics.

    Raises
    ------
    ValueError
        If no cuts are supplied or their fractions do not define a positive
        finite distribution.

    Notes
    -----
    This function covers the EoS-parameter step of a plus-fraction workflow.
    It deliberately does not invent a carbon-number distribution when only a
    bulk C7+ molecular weight is known; splitting or lumping must be performed
    upstream with measured cuts or an explicitly selected Whitson/Pedersen
    distribution model.
    """
    dtype, device = resolve_tensor_options(dtype, device)
    if not cuts:
        raise ValueError("at least one pseudo-component cut is required")
    if any(not isfinite(cut.mole_fraction) or cut.mole_fraction < 0.0 for cut in cuts):
        raise ValueError("pseudo-component cut mole fractions must be finite and nonnegative")
    fractions = torch.tensor(
        [cut.mole_fraction for cut in cuts],
        dtype=dtype,
        device=device,
    )
    if not bool(fractions.sum() > 0.0):
        raise ValueError("pseudo-component cut mole fractions must have a positive sum")
    properties = tuple(
        cpa_monomer_properties(
            cut.normal_boiling_temperature,
            cut.specific_gravity,
            parameter_set,
        )
        for cut in cuts
    )
    components = tuple(
        CPAComponent(
            name=cut.name,
            critical_temperature=item.critical_temperature,
            a0=item.a0,
            b=item.b,
            c1=item.m,
            critical_pressure=item.critical_pressure,
            acentric_factor=item.acentric_factor,
            molar_mass=cut.molar_mass,
        )
        for cut, item in zip(cuts, properties, strict=True)
    )
    return CPACharacterizedComponents(
        components,
        fractions / fractions.sum(),
        properties,
    )

cpa_heavy_end_correlations

cpa_heavy_end_correlations(
    parameter_set: ParameterSource = "cpa.yan-2009-reservoir-fluids",
) -> CPAHeavyEndCorrelations

Load and validate CPA heavy-end correlation coefficients.

Parameters:

Name Type Description Default
parameter_set ParameterSource

CPA parameter source containing a heavy_end_correlations block.

'cpa.yan-2009-reservoir-fluids'

Returns:

Type Description
CPAHeavyEndCorrelations

Typed coefficient inventory in the SI conventions declared by the parameter document.

Raises:

Type Description
ParameterDatabaseError

If the model kind or any required coefficient is missing, nonnumeric, nonfinite, or outside its required positive domain.

Source code in src/torch_flash/eos/cpa_heavy_end.py
def cpa_heavy_end_correlations(
    parameter_set: ParameterSource = "cpa.yan-2009-reservoir-fluids",
) -> CPAHeavyEndCorrelations:
    """Load and validate CPA heavy-end correlation coefficients.

    Parameters
    ----------
    parameter_set
        CPA parameter source containing a ``heavy_end_correlations`` block.

    Returns
    -------
    CPAHeavyEndCorrelations
        Typed coefficient inventory in the SI conventions declared by the
        parameter document.

    Raises
    ------
    ParameterDatabaseError
        If the model kind or any required coefficient is missing, nonnumeric,
        nonfinite, or outside its required positive domain.
    """
    loaded = load_model_parameters(parameter_set)
    if loaded.model_kind != "cpa":
        raise ParameterDatabaseError(f"{loaded.identifier!r} is not a CPA parameter set")
    record = loaded.parameters.get("heavy_end_correlations")
    if not isinstance(record, Mapping):
        raise ParameterDatabaseError(
            f"{loaded.identifier!r} requires a heavy_end_correlations mapping"
        )

    def positive(key: str) -> float:
        value = record.get(key)
        if not isinstance(value, int | float) or not isfinite(value) or value <= 0.0:
            raise ParameterDatabaseError(
                f"{loaded.identifier!r} heavy_end_correlations {key!r} must be finite and positive"
            )
        return float(value)

    temperature_ratio = record.get("critical_temperature_ratio")
    if not isinstance(temperature_ratio, Mapping):
        raise ParameterDatabaseError(
            f"{loaded.identifier!r} requires critical_temperature_ratio coefficients"
        )
    return CPAHeavyEndCorrelations(
        positive("normal_boiling_pressure"),
        positive("srk_omega_a"),
        positive("srk_omega_b"),
        _numeric_tuple(record.get("srk_m"), 3, "srk_m", loaded.identifier),  # type: ignore[arg-type]
        _numeric_tuple(
            record.get("normal_alkane_temperature"),
            3,
            "normal_alkane_temperature",
            loaded.identifier,
        ),  # type: ignore[arg-type]
        _numeric_tuple(
            record.get("log_normal_alkane_pressure_bar"),
            5,
            "log_normal_alkane_pressure_bar",
            loaded.identifier,
        ),  # type: ignore[arg-type]
        _numeric_tuple(
            record.get("normal_alkane_acentric"),
            3,
            "normal_alkane_acentric",
            loaded.identifier,
        ),  # type: ignore[arg-type]
        _numeric_tuple(
            record.get("normal_alkane_specific_gravity"),
            5,
            "normal_alkane_specific_gravity",
            loaded.identifier,
        ),  # type: ignore[arg-type]
        _numeric_tuple(
            temperature_ratio.get("numerator"),
            3,
            "critical_temperature_ratio.numerator",
            loaded.identifier,
        ),  # type: ignore[arg-type]
        _numeric_tuple(
            temperature_ratio.get("denominator"),
            3,
            "critical_temperature_ratio.denominator",
            loaded.identifier,
        ),  # type: ignore[arg-type]
        _numeric_tuple(
            record.get("log_critical_pressure_ratio"),
            5,
            "log_critical_pressure_ratio",
            loaded.identifier,
        ),  # type: ignore[arg-type]
    )

cpa_monomer_properties

cpa_monomer_properties(
    normal_boiling_temperature: float,
    specific_gravity: float,
    parameter_set: ParameterSource = "cpa.yan-2009-reservoir-fluids",
) -> CPAMonomerProperties

Calculate CPA monomer properties for one narrow heavy-end cut.

Parameters:

Name Type Description Default
normal_boiling_temperature float

Atmospheric normal boiling temperature in K.

required
specific_gravity float

Dimensionless liquid specific gravity used by the source characterization.

required
parameter_set ParameterSource

CPA parameter source containing heavy-end correlation coefficients.

'cpa.yan-2009-reservoir-fluids'

Returns:

Type Description
CPAMonomerProperties

Intermediate reference-alkane quantities and characterized critical, acentric, attraction, and covolume properties.

Raises:

Type Description
ValueError

If inputs are nonpositive/nonfinite or the correlation leaves its physical domain.

ConvergenceError

If the normal-boiling-point acentric-factor match cannot be solved.

Source code in src/torch_flash/eos/cpa_heavy_end.py
def cpa_monomer_properties(
    normal_boiling_temperature: float,
    specific_gravity: float,
    parameter_set: ParameterSource = "cpa.yan-2009-reservoir-fluids",
) -> CPAMonomerProperties:
    """Calculate CPA monomer properties for one narrow heavy-end cut.

    Parameters
    ----------
    normal_boiling_temperature
        Atmospheric normal boiling temperature in K.
    specific_gravity
        Dimensionless liquid specific gravity used by the source
        characterization.
    parameter_set
        CPA parameter source containing heavy-end correlation coefficients.

    Returns
    -------
    CPAMonomerProperties
        Intermediate reference-alkane quantities and characterized critical,
        acentric, attraction, and covolume properties.

    Raises
    ------
    ValueError
        If inputs are nonpositive/nonfinite or the correlation leaves its
        physical domain.
    ConvergenceError
        If the normal-boiling-point acentric-factor match cannot be solved.
    """
    tb = float(normal_boiling_temperature)
    sg = float(specific_gravity)
    if not isfinite(tb) or not isfinite(sg) or tb <= 0.0 or sg <= 0.0:
        raise ValueError("boiling temperature and specific gravity must be finite and positive")

    correlations = cpa_heavy_end_correlations(parameter_set)
    temperature_constant, temperature_slope, temperature_denominator = (
        correlations.normal_alkane_temperature
    )
    normal_temperature = (
        (temperature_constant + temperature_slope * tb) * tb / (temperature_denominator + tb)
    )
    p4, p3, p2, p1, p0 = correlations.log_normal_alkane_pressure_bar
    log_normal_pressure_bar = p4 * tb**4 + p3 * tb**3 + p2 * tb**2 + p1 * tb + p0
    normal_pressure = 1.0e5 * exp(log_normal_pressure_bar)
    sg_scale, sg_constant, sg_linear, sg_inverse, sg_inverse_square = (
        correlations.normal_alkane_specific_gravity
    )
    normal_specific_gravity = (sg_scale * tb) ** (1.0 / 3.0) / (
        sg_constant + sg_linear * tb + sg_inverse / tb + sg_inverse_square / tb**2
    )
    perturbation = sg - normal_specific_gravity
    tn1, tn2, tn3 = correlations.critical_temperature_numerator
    td1, td2, td3 = correlations.critical_temperature_denominator
    temperature_ratio = (
        1.0 + tn1 * perturbation + tn2 * perturbation**2 + tn3 * perturbation**3
    ) / (1.0 + td1 * perturbation + td2 * perturbation**2 + td3 * perturbation**3)
    critical_temperature = normal_temperature * temperature_ratio
    pr0, pr1, pr2, pr3, pr4 = correlations.log_critical_pressure_ratio
    log_pressure_ratio = (
        perturbation
        * (pr0 + (pr1 + pr2 / sg) * perturbation)
        / (1.0 + pr3 * perturbation + pr4 * perturbation**2)
    )
    critical_pressure = normal_pressure * exp(log_pressure_ratio)
    if (
        not isfinite(critical_temperature)
        or not isfinite(critical_pressure)
        or critical_temperature <= 0.0
        or critical_pressure <= 0.0
    ):
        raise ValueError("Yan CPA characterization is outside its physical correlation domain")

    if tb < critical_temperature:
        acentric_factor = _match_normal_boiling_point(
            tb,
            critical_temperature,
            critical_pressure,
            correlations,
        )
        used_match = True
    else:
        omega_constant, omega_slope, omega_denominator = correlations.normal_alkane_acentric
        acentric_factor = exp((omega_constant + omega_slope * tb) / (omega_denominator + tb))
        used_match = False
    return CPAMonomerProperties(
        tb,
        sg,
        normal_specific_gravity,
        perturbation,
        normal_temperature,
        normal_pressure,
        critical_temperature,
        critical_pressure,
        acentric_factor,
        used_match,
        correlations,
    )

cpa_pseudocomponent

cpa_pseudocomponent(
    name: str,
    normal_boiling_temperature: float,
    specific_gravity: float,
    molar_mass: float,
    parameter_set: ParameterSource = "cpa.yan-2009-reservoir-fluids",
) -> CPAComponent

Create a non-associating CPA pseudo-component for one heavy-end cut.

Parameters:

Name Type Description Default
name str

Nonempty pseudo-component identifier.

required
normal_boiling_temperature float

Normal boiling temperature in K.

required
specific_gravity float

Dimensionless specific gravity used by the selected correlation.

required
molar_mass float

Molar mass in kg/mol.

required
parameter_set ParameterSource

CPA parameter source containing the heavy-end correlations.

'cpa.yan-2009-reservoir-fluids'

Returns:

Type Description
CPAComponent

Characterized monomer with zero association sites.

Raises:

Type Description
ValueError

If the name or molar mass is invalid, or the cut lies outside the correlation's physical domain.

Source code in src/torch_flash/eos/cpa_heavy_end.py
def cpa_pseudocomponent(
    name: str,
    normal_boiling_temperature: float,
    specific_gravity: float,
    molar_mass: float,
    parameter_set: ParameterSource = "cpa.yan-2009-reservoir-fluids",
) -> CPAComponent:
    """Create a non-associating CPA pseudo-component for one heavy-end cut.

    Parameters
    ----------
    name
        Nonempty pseudo-component identifier.
    normal_boiling_temperature
        Normal boiling temperature in K.
    specific_gravity
        Dimensionless specific gravity used by the selected correlation.
    molar_mass
        Molar mass in kg/mol.
    parameter_set
        CPA parameter source containing the heavy-end correlations.

    Returns
    -------
    CPAComponent
        Characterized monomer with zero association sites.

    Raises
    ------
    ValueError
        If the name or molar mass is invalid, or the cut lies outside the
        correlation's physical domain.
    """
    if not isinstance(name, str) or not name.strip():
        raise ValueError("pseudo-component name must be a non-empty string")
    mass = float(molar_mass)
    if not isfinite(mass) or mass <= 0.0:
        raise ValueError("pseudo-component molar mass must be finite and positive")
    properties = cpa_monomer_properties(
        normal_boiling_temperature,
        specific_gravity,
        parameter_set,
    )
    return CPAComponent(
        name=name,
        critical_temperature=properties.critical_temperature,
        a0=properties.a0,
        b=properties.b,
        c1=properties.m,
        critical_pressure=properties.critical_pressure,
        acentric_factor=properties.acentric_factor,
        molar_mass=mass,
    )

cubic_constants

cubic_constants(source: ParameterSource) -> CubicConstants

Load and validate generalized cubic EOS constants.

Parameters:

Name Type Description Default
source ParameterSource

Bundled parameter-set identifier, YAML path, mapping, or an already loaded model parameter set.

required

Returns:

Type Description
CubicConstants

Typed, immutable constants retaining the source identifier.

Raises:

Type Description
ParameterDatabaseError

If the source is not a cubic parameter set or required alpha and critical-constraint entries are missing or malformed.

Source code in src/torch_flash/eos/cubic.py
def cubic_constants(source: ParameterSource) -> CubicConstants:
    """Load and validate generalized cubic EOS constants.

    Parameters
    ----------
    source
        Bundled parameter-set identifier, YAML path, mapping, or an already
        loaded model parameter set.

    Returns
    -------
    CubicConstants
        Typed, immutable constants retaining the source identifier.

    Raises
    ------
    ParameterDatabaseError
        If the source is not a cubic parameter set or required alpha and
        critical-constraint entries are missing or malformed.
    """
    parameter_set = load_model_parameters(source)
    if parameter_set.model_kind != "cubic":
        raise ParameterDatabaseError(
            f"{parameter_set.identifier!r} is {parameter_set.model_kind!r}, not 'cubic'"
        )
    parameters = parameter_set.parameters
    alpha = parameters.get("alpha")
    if not isinstance(alpha, Mapping):
        raise ParameterDatabaseError(f"{parameter_set.identifier!r} requires an alpha mapping")
    alpha_kind = alpha.get("kind")
    if alpha_kind not in ("srk", "pr76", "pr78"):
        raise ParameterDatabaseError(
            f"{parameter_set.identifier!r} has unsupported alpha kind {alpha_kind!r}"
        )

    def coefficients(key: str, size: int) -> tuple[float, ...]:
        block = alpha.get(key)
        if not isinstance(block, Mapping):
            raise ParameterDatabaseError(f"{parameter_set.identifier!r} requires alpha.{key}")
        values = block.get("coefficients")
        if not isinstance(values, tuple | list) or len(values) != size:
            raise ParameterDatabaseError(
                f"{parameter_set.identifier!r} alpha.{key}.coefficients must have length {size}"
            )
        if any(not isinstance(value, int | float) for value in values):
            raise ParameterDatabaseError("cubic alpha coefficients must be numeric")
        return tuple(float(value) for value in values)

    low = coefficients("low", 3)
    high = coefficients("high", 4) if alpha_kind == "pr78" else None
    switch = alpha.get("switch_acentric_factor")
    if alpha_kind == "pr78" and not isinstance(switch, int | float):
        raise ParameterDatabaseError(
            f"{parameter_set.identifier!r} requires alpha.switch_acentric_factor"
        )
    required = ("omega_a", "omega_b", "delta1", "delta2")
    if any(not isinstance(parameters.get(key), int | float) for key in required):
        raise ParameterDatabaseError(
            f"{parameter_set.identifier!r} cubic constants must be numeric"
        )
    return CubicConstants(
        parameter_set.model,
        float(parameters["omega_a"]),
        float(parameters["omega_b"]),
        float(parameters["delta1"]),
        float(parameters["delta2"]),
        alpha_kind,
        (low[0], low[1], low[2]),
        None if high is None else (high[0], high[1], high[2], high[3]),
        None if switch is None else float(switch),
        parameter_set.identifier,
    )

cubic_eos

cubic_eos(
    components: ComponentSet,
    parameter_set: ParameterSource | CubicConstants,
    *,
    kij: Tensor | None = None,
    kij_a: Tensor | None = None,
    kij_b: Tensor | None = None,
    lij: Tensor | None = None,
    trainable: bool = False,
    trainable_lij: bool = False,
    mixing: CubicMixing | None = None,
    volume_translation: Tensor
    | VolumeTranslation
    | None = None,
) -> CubicEOS

Construct a cubic EoS from bundled YAML or explicit typed constants.

Parameters:

Name Type Description Default
components ComponentSet

Ordered component set.

required
parameter_set ParameterSource | CubicConstants

Cubic parameter source or prevalidated :class:CubicConstants.

required
kij Tensor | None

Optional constant dimensionless attraction interactions.

None
kij_a Tensor | None

Optional temperature-dependent form kij(T) = kij_a + kij_b/T; both matrices are required together.

None
kij_b Tensor | None

Optional temperature-dependent form kij(T) = kij_a + kij_b/T; both matrices are required together.

None
lij Tensor | None

Optional dimensionless cross-covolume interactions.

None
trainable bool

Register attraction or covolume interactions as trainable parameters.

False
trainable_lij bool

Register attraction or covolume interactions as trainable parameters.

False
mixing CubicMixing | None

Explicit mixing module, mutually exclusive with interaction controls.

None
volume_translation Tensor | VolumeTranslation | None

Additive shifts in m3/mol or a :class:VolumeTranslation.

None

Returns:

Type Description
CubicEOS

Configured model on the component set's dtype and device.

Raises:

Type Description
ValueError

If mutually exclusive interaction/mixing options are combined or matrix shapes are inconsistent.

Notes

Supply kij for a constant quadratic interaction matrix, or both kij_a and kij_b for kij(T) = kij_a + kij_b/T. The latter coefficients are dimensionless and kelvin, respectively. Supply lij for bij = 0.5*(bi+bj)*(1-lij); omitting it recovers the conventional linear covolume rule. trainable controls attraction interactions; trainable_lij independently enables co-volume fitting from the supplied matrix or from zero.

Source code in src/torch_flash/eos/cubic.py
def cubic_eos(
    components: ComponentSet,
    parameter_set: ParameterSource | CubicConstants,
    *,
    kij: Tensor | None = None,
    kij_a: Tensor | None = None,
    kij_b: Tensor | None = None,
    lij: Tensor | None = None,
    trainable: bool = False,
    trainable_lij: bool = False,
    mixing: CubicMixing | None = None,
    volume_translation: Tensor | VolumeTranslation | None = None,
) -> CubicEOS:
    """Construct a cubic EoS from bundled YAML or explicit typed constants.

    Parameters
    ----------
    components
        Ordered component set.
    parameter_set
        Cubic parameter source or prevalidated :class:`CubicConstants`.
    kij
        Optional constant dimensionless attraction interactions.
    kij_a, kij_b
        Optional temperature-dependent form ``kij(T) = kij_a + kij_b/T``;
        both matrices are required together.
    lij
        Optional dimensionless cross-covolume interactions.
    trainable, trainable_lij
        Register attraction or covolume interactions as trainable parameters.
    mixing
        Explicit mixing module, mutually exclusive with interaction controls.
    volume_translation
        Additive shifts in m3/mol or a :class:`VolumeTranslation`.

    Returns
    -------
    CubicEOS
        Configured model on the component set's dtype and device.

    Raises
    ------
    ValueError
        If mutually exclusive interaction/mixing options are combined or
        matrix shapes are inconsistent.

    Notes
    -----
    Supply ``kij`` for a constant quadratic interaction matrix, or both
    ``kij_a`` and ``kij_b`` for ``kij(T) = kij_a + kij_b/T``. The latter
    coefficients are dimensionless and kelvin, respectively. Supply ``lij``
    for ``bij = 0.5*(bi+bj)*(1-lij)``; omitting it recovers the conventional
    linear covolume rule. ``trainable`` controls attraction interactions;
    ``trainable_lij`` independently enables co-volume fitting from the
    supplied matrix or from zero.
    """
    constants = (
        parameter_set
        if isinstance(parameter_set, CubicConstants)
        else cubic_constants(parameter_set)
    )
    return CubicEOS(
        components,
        constants,
        mixing=_resolve_mixing(
            components,
            kij,
            kij_a,
            kij_b,
            lij,
            trainable,
            trainable_lij,
            mixing,
        ),
        volume_translation=volume_translation,
    )

enhanced_predictive_peng_robinson_1978

enhanced_predictive_peng_robinson_1978(
    components: ComponentSet,
    *,
    parameter_set: ParameterSource = DEFAULT_EPPR78_GROUP_CONTRIBUTION,
    group_counts: Mapping[str, Mapping[str, float]]
    | Tensor
    | None = None,
    trainable: bool = False,
    volume_translation: Tensor
    | VolumeTranslation
    | None = None,
) -> CubicEOS

Construct E-PPR78 with the global 40-group kij(T) correlation.

Parameters:

Name Type Description Default
components ComponentSet

Ordered component set. The bundled parameter document supplies decompositions for the package's unambiguous hydrocarbon, permanent gas, water, ammonia, and sulfur-dioxide components.

required
parameter_set ParameterSource

E-PPR78 group-contribution parameter source. The default is the global 40-group 2022 revision. A custom source must use the same pressure-valued A/B convention and explicitly classify unavailable group pairs.

DEFAULT_EPPR78_GROUP_CONTRIBUTION
group_counts Mapping[str, Mapping[str, float]] | Tensor | None

Optional explicit component group counts overriding stored decompositions. Counts are normalized to molecular group fractions.

None
trainable bool

Register the active, unique off-diagonal A/B group interactions as PyTorch parameters.

False
volume_translation Tensor | VolumeTranslation | None

Optional additive volume translation in m3/mol.

None

Returns:

Type Description
CubicEOS

PR78 pure-component equation with :class:~torch_flash.mixing.PPR78Mixing configured from E-PPR78.

Raises:

Type Description
KeyError

If a requested component lacks a bundled decomposition.

ParameterDatabaseError

If the parameter document is malformed or an interaction between two active groups is explicitly unavailable.

Notes

E-PPR78 retains the predictive Peng--Robinson group-contribution equation but uses the refitted group interactions of Jaubert, Qian, Lasala, and Privat, Fluid Phase Equilibria 560 (2022) 113456, doi:10.1016/j.fluid.2022.113456, Eq. (5) and Table S4. The bundled database contains all 40 named groups, 356 available group pairs, and 424 explicitly unavailable pairs from that source. The CCS use case follows Xu et al., International Journal of Greenhouse Gas Control 56 (2017) 126--154, doi:10.1016/j.ijggc.2016.11.015; the default numerical values are the later 2022 global revision.

Only groups active in components are materialized for an incomplete interaction inventory. This avoids turning an unavailable pair into a numerical zero and keeps fitting tensors proportional to the selected problem. E-PPR78 uses the linear covolume rule defined by the published model.

Source code in src/torch_flash/eos/cubic.py
def enhanced_predictive_peng_robinson_1978(
    components: ComponentSet,
    *,
    parameter_set: ParameterSource = DEFAULT_EPPR78_GROUP_CONTRIBUTION,
    group_counts: Mapping[str, Mapping[str, float]] | Tensor | None = None,
    trainable: bool = False,
    volume_translation: Tensor | VolumeTranslation | None = None,
) -> CubicEOS:
    """Construct E-PPR78 with the global 40-group ``kij(T)`` correlation.

    Parameters
    ----------
    components
        Ordered component set. The bundled parameter document supplies
        decompositions for the package's unambiguous hydrocarbon, permanent
        gas, water, ammonia, and sulfur-dioxide components.
    parameter_set
        E-PPR78 group-contribution parameter source. The default is the
        global 40-group 2022 revision. A custom source must use the same
        pressure-valued A/B convention and explicitly classify unavailable
        group pairs.
    group_counts
        Optional explicit component group counts overriding stored
        decompositions. Counts are normalized to molecular group fractions.
    trainable
        Register the active, unique off-diagonal A/B group interactions as
        PyTorch parameters.
    volume_translation
        Optional additive volume translation in m3/mol.

    Returns
    -------
    CubicEOS
        PR78 pure-component equation with
        :class:`~torch_flash.mixing.PPR78Mixing` configured from E-PPR78.

    Raises
    ------
    KeyError
        If a requested component lacks a bundled decomposition.
    ParameterDatabaseError
        If the parameter document is malformed or an interaction between two
        active groups is explicitly unavailable.

    Notes
    -----
    E-PPR78 retains the predictive Peng--Robinson group-contribution equation
    but uses the refitted group interactions of Jaubert, Qian, Lasala, and
    Privat, *Fluid Phase Equilibria* 560 (2022) 113456,
    doi:10.1016/j.fluid.2022.113456, Eq. (5) and Table S4. The bundled
    database contains all 40 named groups, 356 available group pairs, and 424
    explicitly unavailable pairs from that source. The CCS use case follows
    Xu et al., *International Journal of Greenhouse Gas Control* 56 (2017)
    126--154, doi:10.1016/j.ijggc.2016.11.015; the default numerical values
    are the later 2022 global revision.

    Only groups active in ``components`` are materialized for an incomplete
    interaction inventory. This avoids turning an unavailable pair into a
    numerical zero and keeps fitting tensors proportional to the selected
    problem. E-PPR78 uses the linear covolume rule defined by the published
    model.
    """
    return predictive_peng_robinson_1978(
        components,
        parameter_set=parameter_set,
        group_counts=group_counts,
        trainable=trainable,
        volume_translation=volume_translation,
    )

peng_robinson_1976

peng_robinson_1976(
    components: ComponentSet,
    *,
    kij: Tensor | None = None,
    kij_a: Tensor | None = None,
    kij_b: Tensor | None = None,
    lij: Tensor | None = None,
    trainable: bool = False,
    trainable_lij: bool = False,
    mixing: CubicMixing | None = None,
    volume_translation: Tensor
    | VolumeTranslation
    | None = None,
) -> CubicEOS

Construct the original 1976 Peng--Robinson equation of state.

Parameters:

Name Type Description Default
components ComponentSet

Ordered component set.

required
kij Tensor | None

Optional constant or a + b/T attraction interactions and optional covolume interactions.

None
kij_a Tensor | None

Optional constant or a + b/T attraction interactions and optional covolume interactions.

None
kij_b Tensor | None

Optional constant or a + b/T attraction interactions and optional covolume interactions.

None
lij Tensor | None

Optional constant or a + b/T attraction interactions and optional covolume interactions.

None
trainable bool

Make attraction or covolume interaction tensors trainable.

False
trainable_lij bool

Make attraction or covolume interaction tensors trainable.

False
mixing CubicMixing | None

Explicit mixing rule, mutually exclusive with interaction arguments.

None
volume_translation Tensor | VolumeTranslation | None

Optional additive shifts in m3/mol.

None

Returns:

Type Description
CubicEOS

Configured PR76 model.

Source code in src/torch_flash/eos/cubic.py
def peng_robinson_1976(
    components: ComponentSet,
    *,
    kij: Tensor | None = None,
    kij_a: Tensor | None = None,
    kij_b: Tensor | None = None,
    lij: Tensor | None = None,
    trainable: bool = False,
    trainable_lij: bool = False,
    mixing: CubicMixing | None = None,
    volume_translation: Tensor | VolumeTranslation | None = None,
) -> CubicEOS:
    """Construct the original 1976 Peng--Robinson equation of state.

    Parameters
    ----------
    components
        Ordered component set.
    kij, kij_a, kij_b, lij
        Optional constant or ``a + b/T`` attraction interactions and optional
        covolume interactions.
    trainable, trainable_lij
        Make attraction or covolume interaction tensors trainable.
    mixing
        Explicit mixing rule, mutually exclusive with interaction arguments.
    volume_translation
        Optional additive shifts in m3/mol.

    Returns
    -------
    CubicEOS
        Configured PR76 model.
    """
    return cubic_eos(
        components,
        PR76,
        kij=kij,
        kij_a=kij_a,
        kij_b=kij_b,
        lij=lij,
        trainable=trainable,
        trainable_lij=trainable_lij,
        mixing=mixing,
        volume_translation=volume_translation,
    )

peng_robinson_1978

peng_robinson_1978(
    components: ComponentSet,
    *,
    kij: Tensor | None = None,
    kij_a: Tensor | None = None,
    kij_b: Tensor | None = None,
    lij: Tensor | None = None,
    trainable: bool = False,
    trainable_lij: bool = False,
    mixing: CubicMixing | None = None,
    volume_translation: Tensor
    | VolumeTranslation
    | None = None,
) -> CubicEOS

Construct the 1978 Peng--Robinson acentric-factor variant.

Parameters:

Name Type Description Default
components ComponentSet

Ordered component set.

required
kij Tensor | None

Optional constant or a + b/T attraction interactions and optional covolume interactions.

None
kij_a Tensor | None

Optional constant or a + b/T attraction interactions and optional covolume interactions.

None
kij_b Tensor | None

Optional constant or a + b/T attraction interactions and optional covolume interactions.

None
lij Tensor | None

Optional constant or a + b/T attraction interactions and optional covolume interactions.

None
trainable bool

Make attraction or covolume interaction tensors trainable.

False
trainable_lij bool

Make attraction or covolume interaction tensors trainable.

False
mixing CubicMixing | None

Explicit mixing rule, mutually exclusive with interaction arguments.

None
volume_translation Tensor | VolumeTranslation | None

Optional additive shifts in m3/mol.

None

Returns:

Type Description
CubicEOS

Configured PR78 model.

Source code in src/torch_flash/eos/cubic.py
def peng_robinson_1978(
    components: ComponentSet,
    *,
    kij: Tensor | None = None,
    kij_a: Tensor | None = None,
    kij_b: Tensor | None = None,
    lij: Tensor | None = None,
    trainable: bool = False,
    trainable_lij: bool = False,
    mixing: CubicMixing | None = None,
    volume_translation: Tensor | VolumeTranslation | None = None,
) -> CubicEOS:
    """Construct the 1978 Peng--Robinson acentric-factor variant.

    Parameters
    ----------
    components
        Ordered component set.
    kij, kij_a, kij_b, lij
        Optional constant or ``a + b/T`` attraction interactions and optional
        covolume interactions.
    trainable, trainable_lij
        Make attraction or covolume interaction tensors trainable.
    mixing
        Explicit mixing rule, mutually exclusive with interaction arguments.
    volume_translation
        Optional additive shifts in m3/mol.

    Returns
    -------
    CubicEOS
        Configured PR78 model.
    """
    return cubic_eos(
        components,
        PR78,
        kij=kij,
        kij_a=kij_a,
        kij_b=kij_b,
        lij=lij,
        trainable=trainable,
        trainable_lij=trainable_lij,
        mixing=mixing,
        volume_translation=volume_translation,
    )

predictive_peng_robinson_1978

predictive_peng_robinson_1978(
    components: ComponentSet,
    *,
    parameter_set: ParameterSource = DEFAULT_PPR78_GROUP_CONTRIBUTION,
    group_counts: Mapping[str, Mapping[str, float]]
    | Tensor
    | None = None,
    trainable: bool = False,
    volume_translation: Tensor
    | VolumeTranslation
    | None = None,
) -> CubicEOS

Construct PPR78 with group-contribution kij(T).

Parameters:

Name Type Description Default
components ComponentSet

Ordered component set.

required
parameter_set ParameterSource

PPR78 group-contribution parameter source. Use PPR78_HYDROGEN_WATER_GROUP_CONTRIBUTION for the published H2/N2/H2O submatrix.

DEFAULT_PPR78_GROUP_CONTRIBUTION
group_counts Mapping[str, Mapping[str, float]] | Tensor | None

Optional explicit component group decompositions.

None
trainable bool

Register universal off-diagonal A/B group interactions as trainable parameters.

False
volume_translation Tensor | VolumeTranslation | None

Optional additive volume translation.

None

Returns:

Type Description
CubicEOS

PR78 model with :class:~torch_flash.mixing.PPR78Mixing.

Notes

The default parameter set is the original six-group saturated-hydrocarbon fit of Jaubert and Mutelet (2004), doi:10.1016/j.fluid.2004.06.059. The separate H2/N2/H2O parameter set combines the hydrogen-group extension of Qian et al. (2013), doi:10.1016/j.supflu.2012.12.014, and the water-group extension of Qian et al. (2013), doi:10.1021/ie402541h. Custom YAML parameter sets and explicit component group counts use the same path. trainable=True exposes the unique universal A/B group interactions as PyTorch parameters.

PPR78 was derived with the linear covolume rule, so co-volume interactions are intentionally not accepted by this named constructor.

Source code in src/torch_flash/eos/cubic.py
def predictive_peng_robinson_1978(
    components: ComponentSet,
    *,
    parameter_set: ParameterSource = DEFAULT_PPR78_GROUP_CONTRIBUTION,
    group_counts: Mapping[str, Mapping[str, float]] | Tensor | None = None,
    trainable: bool = False,
    volume_translation: Tensor | VolumeTranslation | None = None,
) -> CubicEOS:
    """Construct PPR78 with group-contribution ``kij(T)``.

    Parameters
    ----------
    components
        Ordered component set.
    parameter_set
        PPR78 group-contribution parameter source. Use
        ``PPR78_HYDROGEN_WATER_GROUP_CONTRIBUTION`` for the published
        H2/N2/H2O submatrix.
    group_counts
        Optional explicit component group decompositions.
    trainable
        Register universal off-diagonal A/B group interactions as trainable
        parameters.
    volume_translation
        Optional additive volume translation.

    Returns
    -------
    CubicEOS
        PR78 model with :class:`~torch_flash.mixing.PPR78Mixing`.

    Notes
    -----
    The default parameter set is the original six-group saturated-hydrocarbon
    fit of Jaubert and Mutelet (2004), doi:10.1016/j.fluid.2004.06.059.
    The separate H2/N2/H2O parameter set combines the hydrogen-group
    extension of Qian et al. (2013), doi:10.1016/j.supflu.2012.12.014, and
    the water-group extension of Qian et al. (2013),
    doi:10.1021/ie402541h. Custom YAML parameter sets and explicit component
    group counts use the same path. ``trainable=True`` exposes the unique
    universal A/B group interactions as PyTorch parameters.

    PPR78 was derived with the linear covolume rule, so co-volume interactions
    are intentionally not accepted by this named constructor.
    """
    return CubicEOS(
        components,
        PR78,
        mixing=ppr78_mixing(
            components,
            parameter_set,
            group_counts=group_counts,
            trainable=trainable,
        ),
        volume_translation=volume_translation,
    )

soave_redlich_kwong

soave_redlich_kwong(
    components: ComponentSet,
    *,
    kij: Tensor | None = None,
    kij_a: Tensor | None = None,
    kij_b: Tensor | None = None,
    lij: Tensor | None = None,
    trainable: bool = False,
    trainable_lij: bool = False,
    mixing: CubicMixing | None = None,
    volume_translation: Tensor
    | VolumeTranslation
    | None = None,
) -> CubicEOS

Construct the 1972 Soave--Redlich--Kwong equation of state.

Parameters:

Name Type Description Default
components ComponentSet

Ordered component set.

required
kij Tensor | None

Optional constant or a + b/T attraction interactions and optional covolume interactions. Matrix axes follow components.names.

None
kij_a Tensor | None

Optional constant or a + b/T attraction interactions and optional covolume interactions. Matrix axes follow components.names.

None
kij_b Tensor | None

Optional constant or a + b/T attraction interactions and optional covolume interactions. Matrix axes follow components.names.

None
lij Tensor | None

Optional constant or a + b/T attraction interactions and optional covolume interactions. Matrix axes follow components.names.

None
trainable bool

Register supplied/default attraction or covolume interactions as trainable PyTorch parameters.

False
trainable_lij bool

Register supplied/default attraction or covolume interactions as trainable PyTorch parameters.

False
mixing CubicMixing | None

Explicit mixing rule, mutually exclusive with interaction arguments.

None
volume_translation Tensor | VolumeTranslation | None

Optional additive shifts in m3/mol.

None

Returns:

Type Description
CubicEOS

Configured SRK model on the component set's dtype and device.

Source code in src/torch_flash/eos/cubic.py
def soave_redlich_kwong(
    components: ComponentSet,
    *,
    kij: Tensor | None = None,
    kij_a: Tensor | None = None,
    kij_b: Tensor | None = None,
    lij: Tensor | None = None,
    trainable: bool = False,
    trainable_lij: bool = False,
    mixing: CubicMixing | None = None,
    volume_translation: Tensor | VolumeTranslation | None = None,
) -> CubicEOS:
    """Construct the 1972 Soave--Redlich--Kwong equation of state.

    Parameters
    ----------
    components
        Ordered component set.
    kij, kij_a, kij_b, lij
        Optional constant or ``a + b/T`` attraction interactions and optional
        covolume interactions. Matrix axes follow ``components.names``.
    trainable, trainable_lij
        Register supplied/default attraction or covolume interactions as
        trainable PyTorch parameters.
    mixing
        Explicit mixing rule, mutually exclusive with interaction arguments.
    volume_translation
        Optional additive shifts in m3/mol.

    Returns
    -------
    CubicEOS
        Configured SRK model on the component set's dtype and device.
    """
    return cubic_eos(
        components,
        SRK,
        kij=kij,
        kij_a=kij_a,
        kij_b=kij_b,
        lij=lij,
        trainable=trainable,
        trainable_lij=trainable_lij,
        mixing=mixing,
        volume_translation=volume_translation,
    )

leachman_normal_hydrogen

leachman_normal_hydrogen(
    *,
    dtype: dtype | None = None,
    device: device | str | None = None,
    trainable: bool = False,
    parameter_set: ParameterSource = DEFAULT_LEACHMAN_NORMAL_HYDROGEN,
) -> PureFluidHelmholtzEOS

Construct the Leachman et al. normal-hydrogen Helmholtz EOS.

Parameters:

Name Type Description Default
dtype dtype | None

Tensor placement, defaulting to the configured runtime policy.

None
device dtype | None

Tensor placement, defaulting to the configured runtime policy.

None
trainable bool

Register supported Helmholtz coefficients as trainable parameters.

False
parameter_set ParameterSource

Compatible standalone normal-hydrogen parameter source.

DEFAULT_LEACHMAN_NORMAL_HYDROGEN

Returns:

Type Description
PureFluidHelmholtzEOS

Native PyTorch pure-fluid Helmholtz model for normal hydrogen.

Raises:

Type Description
ParameterDatabaseError

If parameter_set does not identify the Leachman normal-hydrogen thermodynamic equation.

Notes

This constructor covers the thermodynamic Helmholtz equation. It does not add saturation ancillaries, melting or sublimation curves, or transport correlations.

References

J. W. Leachman et al., J. Phys. Chem. Ref. Data 38 (2009) 721--748, doi:10.1063/1.3160306.

Source code in src/torch_flash/eos/leachman.py
def leachman_normal_hydrogen(
    *,
    dtype: torch.dtype | None = None,
    device: torch.device | str | None = None,
    trainable: bool = False,
    parameter_set: ParameterSource = DEFAULT_LEACHMAN_NORMAL_HYDROGEN,
) -> PureFluidHelmholtzEOS:
    """Construct the Leachman et al. normal-hydrogen Helmholtz EOS.

    Parameters
    ----------
    dtype, device
        Tensor placement, defaulting to the configured runtime policy.
    trainable
        Register supported Helmholtz coefficients as trainable parameters.
    parameter_set
        Compatible standalone normal-hydrogen parameter source.

    Returns
    -------
    PureFluidHelmholtzEOS
        Native PyTorch pure-fluid Helmholtz model for normal hydrogen.

    Raises
    ------
    ParameterDatabaseError
        If ``parameter_set`` does not identify the Leachman normal-hydrogen
        thermodynamic equation.

    Notes
    -----
    This constructor covers the thermodynamic Helmholtz equation. It does not
    add saturation ancillaries, melting or sublimation curves, or transport
    correlations.

    References
    ----------
    J. W. Leachman et al., *J. Phys. Chem. Ref. Data* 38 (2009) 721--748,
    doi:10.1063/1.3160306.
    """
    dtype, device = resolve_tensor_options(dtype, device)
    loaded = load_model_parameters(parameter_set)
    normalized = loaded.model.strip().lower().replace("_", "-")
    if loaded.model_kind != "pure_helmholtz" or not normalized.startswith(
        "leachman normal hydrogen"
    ):
        raise ParameterDatabaseError(
            "leachman_normal_hydrogen requires the Leachman normal-hydrogen "
            f"parameter set, got {loaded.model!r}"
        )
    kernel = _eoscg_eos(
        loaded,
        ("hydrogen",),
        dtype=dtype,
        device=device,
        trainable=trainable,
    )
    return PureFluidHelmholtzEOS(
        kernel,
        PureFluidHelmholtzMetadata(
            model=loaded.model,
            reference="doi:10.1063/1.3160306",
            version=loaded.version,
            fluid="hydrogen",
        ),
    )

eoscg2021

eoscg2021(
    names: tuple[str, ...] | None = None,
    *,
    dtype: dtype | None = None,
    device: device | str | None = None,
    trainable: bool = False,
    parameter_set: ParameterSource = "multiparameter.eos-cg-2021",
) -> MultiparameterEOS

Construct the complete native 16-component EOS-CG-2021 Helmholtz model.

Parameters:

Name Type Description Default
names tuple[str, ...] | None

Optional ordered subset of the 16 supported components.

None
dtype dtype | None

Tensor placement.

None
device dtype | None

Tensor placement.

None
trainable bool

Register supported coefficient tensors as trainable parameters.

False
parameter_set ParameterSource

EOS-CG-2021 parameter source.

'multiparameter.eos-cg-2021'

Returns:

Type Description
MultiparameterEOS

Native EOS-CG-2021 model defined by Neumann et al. (2023), doi:10.1007/s10765-023-03263-6, including its supplementary tables.

Source code in src/torch_flash/eos/named.py
def eoscg2021(
    names: tuple[str, ...] | None = None,
    *,
    dtype: torch.dtype | None = None,
    device: torch.device | str | None = None,
    trainable: bool = False,
    parameter_set: ParameterSource = "multiparameter.eos-cg-2021",
) -> MultiparameterEOS:
    """Construct the complete native 16-component EOS-CG-2021 Helmholtz model.

    Parameters
    ----------
    names
        Optional ordered subset of the 16 supported components.
    dtype, device
        Tensor placement.
    trainable
        Register supported coefficient tensors as trainable parameters.
    parameter_set
        EOS-CG-2021 parameter source.

    Returns
    -------
    MultiparameterEOS
        Native EOS-CG-2021 model defined by Neumann et al. (2023),
        doi:10.1007/s10765-023-03263-6, including its supplementary tables.
    """
    dtype, device = resolve_tensor_options(dtype, device)
    loaded = load_model_parameters(parameter_set)
    if not loaded.model.upper().startswith("EOS-CG-2021"):
        raise ParameterDatabaseError(
            f"eoscg2021 requires an EOS-CG-2021 parameter set, got {loaded.model!r}"
        )
    return _eoscg_eos(
        loaded,
        names,
        dtype=dtype,
        device=device,
        trainable=trainable,
    )

gerg2008

gerg2008(
    names: tuple[str, ...] | None = None,
    *,
    dtype: dtype | None = None,
    device: device | str | None = None,
    trainable: bool = False,
    parameter_set: ParameterSource = "multiparameter.gerg-2008",
) -> MultiparameterEOS

Construct the complete native 21-component GERG-2008 Helmholtz model.

Parameters:

Name Type Description Default
names tuple[str, ...] | None

Optional ordered subset of the 21 supported components.

None
dtype dtype | None

Tensor placement.

None
device dtype | None

Tensor placement.

None
trainable bool

Register supported coefficient tensors as trainable parameters.

False
parameter_set ParameterSource

GERG-2008 parameter source, overridable for an explicitly versioned compatible document.

'multiparameter.gerg-2008'

Returns:

Type Description
MultiparameterEOS

Native GERG-2008 model. The defining reference is Kunz and Wagner (2012), doi:10.1021/je300655b.

Source code in src/torch_flash/eos/named.py
def gerg2008(
    names: tuple[str, ...] | None = None,
    *,
    dtype: torch.dtype | None = None,
    device: torch.device | str | None = None,
    trainable: bool = False,
    parameter_set: ParameterSource = "multiparameter.gerg-2008",
) -> MultiparameterEOS:
    """Construct the complete native 21-component GERG-2008 Helmholtz model.

    Parameters
    ----------
    names
        Optional ordered subset of the 21 supported components.
    dtype, device
        Tensor placement.
    trainable
        Register supported coefficient tensors as trainable parameters.
    parameter_set
        GERG-2008 parameter source, overridable for an explicitly versioned
        compatible document.

    Returns
    -------
    MultiparameterEOS
        Native GERG-2008 model. The defining reference is Kunz and Wagner
        (2012), doi:10.1021/je300655b.
    """
    dtype, device = resolve_tensor_options(dtype, device)
    loaded = load_model_parameters(parameter_set)
    if not loaded.model.upper().startswith("GERG-2008"):
        raise ParameterDatabaseError(
            f"gerg2008 requires a GERG-2008 parameter set, got {loaded.model!r}"
        )
    return _gerg_eos(
        loaded,
        names,
        dtype=dtype,
        device=device,
        trainable=trainable,
    )

gerg2008_hydrogen_2021

gerg2008_hydrogen_2021(
    names: tuple[str, ...] | None = None,
    *,
    dtype: dtype | None = None,
    device: device | str | None = None,
    trainable: bool = False,
    parameter_set: ParameterSource = "multiparameter.gerg-2008-hydrogen-2021",
) -> MultiparameterEOS

Construct the Beckmüller et al. H2-tailored GERG mixture model.

Parameters:

Name Type Description Default
names tuple[str, ...] | None

Optional ordered subset of methane, nitrogen, carbon monoxide, carbon dioxide, and hydrogen.

None
dtype dtype | None

Tensor placement.

None
device dtype | None

Tensor placement.

None
trainable bool

Register supported coefficient tensors as trainable parameters.

False
parameter_set ParameterSource

Compatible H2-tailored GERG parameter source.

'multiparameter.gerg-2008-hydrogen-2021'

Returns:

Type Description
MultiparameterEOS

Five-component H2-tailored GERG model using GERG-2008 pure fluids for CH4, N2, CO, and CO2 and the Leachman normal-hydrogen equation.

References

Beckmüller et al., J. Phys. Chem. Ref. Data 50, 013102 (2021), doi:10.1063/5.0040533.

Source code in src/torch_flash/eos/named.py
def gerg2008_hydrogen_2021(
    names: tuple[str, ...] | None = None,
    *,
    dtype: torch.dtype | None = None,
    device: torch.device | str | None = None,
    trainable: bool = False,
    parameter_set: ParameterSource = "multiparameter.gerg-2008-hydrogen-2021",
) -> MultiparameterEOS:
    """Construct the Beckmüller et al. H2-tailored GERG mixture model.

    Parameters
    ----------
    names
        Optional ordered subset of methane, nitrogen, carbon monoxide, carbon
        dioxide, and hydrogen.
    dtype, device
        Tensor placement.
    trainable
        Register supported coefficient tensors as trainable parameters.
    parameter_set
        Compatible H2-tailored GERG parameter source.

    Returns
    -------
    MultiparameterEOS
        Five-component H2-tailored GERG model using GERG-2008 pure fluids for
        CH4, N2, CO, and CO2 and the Leachman normal-hydrogen equation.

    References
    ----------
    Beckmüller et al., *J. Phys. Chem. Ref. Data* 50, 013102 (2021),
    doi:10.1063/5.0040533.
    """
    dtype, device = resolve_tensor_options(dtype, device)
    loaded = load_model_parameters(parameter_set)
    if not loaded.model.upper().startswith("GERG-2008 H2-TAILORED"):
        raise ParameterDatabaseError(
            "gerg2008_hydrogen_2021 requires the Beckmüller et al. "
            f"H2-tailored GERG parameter set, got {loaded.model!r}"
        )
    return _gerg_eos(
        loaded,
        names,
        dtype=dtype,
        device=device,
        trainable=trainable,
    )

multiparameter_eos

multiparameter_eos(
    parameter_set: ParameterSource,
    names: tuple[str, ...] | None = None,
    *,
    dtype: dtype | None = None,
    device: device | str | None = None,
    trainable: bool = False,
) -> MultiparameterEOS

Construct a native multiparameter EOS from versioned parameters.

Parameters:

Name Type Description Default
parameter_set ParameterSource

Bundled identifier, custom YAML path, mapping, or loaded multiparameter parameter set.

required
names tuple[str, ...] | None

Optional ordered component subset. Omitting it selects the complete inventory defined by the parameter set.

None
dtype dtype | None

Tensor placement, defaulting to runtime configuration.

None
device dtype | None

Tensor placement, defaulting to runtime configuration.

None
trainable bool

Register supported coefficient tensors as trainable parameters.

False

Returns:

Type Description
MultiparameterEOS

GERG-family or EOS-CG model selected from the source identity.

Raises:

Type Description
ParameterDatabaseError

If the source is not a supported multiparameter model or its coefficient inventory is malformed.

Source code in src/torch_flash/eos/named.py
def multiparameter_eos(
    parameter_set: ParameterSource,
    names: tuple[str, ...] | None = None,
    *,
    dtype: torch.dtype | None = None,
    device: torch.device | str | None = None,
    trainable: bool = False,
) -> MultiparameterEOS:
    """Construct a native multiparameter EOS from versioned parameters.

    Parameters
    ----------
    parameter_set
        Bundled identifier, custom YAML path, mapping, or loaded multiparameter
        parameter set.
    names
        Optional ordered component subset. Omitting it selects the complete
        inventory defined by the parameter set.
    dtype, device
        Tensor placement, defaulting to runtime configuration.
    trainable
        Register supported coefficient tensors as trainable parameters.

    Returns
    -------
    MultiparameterEOS
        GERG-family or EOS-CG model selected from the source identity.

    Raises
    ------
    ParameterDatabaseError
        If the source is not a supported multiparameter model or its coefficient
        inventory is malformed.
    """
    dtype, device = resolve_tensor_options(dtype, device)
    loaded = load_model_parameters(parameter_set)
    if loaded.model_kind not in ("multiparameter", "multifluid"):
        raise ParameterDatabaseError(
            f"{loaded.identifier!r} is {loaded.model_kind!r}, not 'multiparameter'"
        )
    if loaded.model_kind == "multifluid":
        warnings.warn(
            "model_kind='multifluid' is deprecated; use 'multiparameter'",
            DeprecationWarning,
            stacklevel=2,
        )
    normalized = loaded.model.strip().lower().replace("_", "-")
    if normalized.startswith("gerg"):
        return _gerg_eos(
            loaded,
            names,
            dtype=dtype,
            device=device,
            trainable=trainable,
        )
    if normalized.startswith("eos-cg") or normalized.startswith("eoscg"):
        return _eoscg_eos(
            loaded,
            names,
            dtype=dtype,
            device=device,
            trainable=trainable,
        )
    raise ParameterDatabaseError(
        f"{loaded.identifier!r} has unsupported multiparameter model {loaded.model!r}"
    )

density_matched_translation

density_matched_translation(
    eos_molar_volume: Tensor,
    molar_mass: Tensor,
    mass_density: Tensor,
    *,
    source: str = "density-match",
) -> VolumeTranslation

Match a reference density with an additive component translation.

Parameters:

Name Type Description Default
eos_molar_volume Tensor

Parent-EOS pure-component molar volumes in m3/mol.

required
molar_mass Tensor

Component molar masses in kg/mol.

required
mass_density Tensor

Target pure-component mass densities in kg/m3.

required
source str

Provenance label stored on the returned translation.

'density-match'

Returns:

Type Description
VolumeTranslation

Constant shifts M/rho - v_eos in m3/mol.

Raises:

Type Description
ValueError

If input vectors differ in shape or contain nonfinite/nonpositive values.

Notes

All inputs use SI units. The returned shift is M/rho - v_eos, while the equivalent published Péneloux coefficient is its negative.

Source code in src/torch_flash/eos/volume_translation.py
def density_matched_translation(
    eos_molar_volume: Tensor,
    molar_mass: Tensor,
    mass_density: Tensor,
    *,
    source: str = "density-match",
) -> VolumeTranslation:
    """Match a reference density with an additive component translation.

    Parameters
    ----------
    eos_molar_volume
        Parent-EOS pure-component molar volumes in m3/mol.
    molar_mass
        Component molar masses in kg/mol.
    mass_density
        Target pure-component mass densities in kg/m3.
    source
        Provenance label stored on the returned translation.

    Returns
    -------
    VolumeTranslation
        Constant shifts ``M/rho - v_eos`` in m3/mol.

    Raises
    ------
    ValueError
        If input vectors differ in shape or contain nonfinite/nonpositive
        values.

    Notes
    -----
    All inputs use SI units.  The returned shift is
    ``M/rho - v_eos``, while the equivalent published Péneloux coefficient is
    its negative.
    """
    if eos_molar_volume.shape != molar_mass.shape or mass_density.shape != molar_mass.shape:
        raise ValueError("volume, molar mass, and density vectors must have the same shape")
    if not bool(
        torch.isfinite(eos_molar_volume).all()
        & torch.isfinite(molar_mass).all()
        & torch.isfinite(mass_density).all()
    ):
        raise ValueError("density-matching inputs must be finite")
    if bool(
        (eos_molar_volume <= 0.0).any() | (molar_mass <= 0.0).any() | (mass_density <= 0.0).any()
    ):
        raise ValueError("density-matching inputs must be positive")
    target_volume = molar_mass / mass_density
    return VolumeTranslation.constant(target_volume - eos_molar_volume, source=source)

pedersen_peneloux_translation

pedersen_peneloux_translation(
    components: ComponentSet,
    eos: VolumeTranslationEOS,
    *,
    rackett_factor: Tensor | None = None,
    source: ParameterSource = _PEDERSEN_SOURCE,
) -> VolumeTranslation

Return the Pedersen light-component SRK/PR Péneloux translation.

Parameters:

Name Type Description Default
components ComponentSet

Ordered critical constants and acentric factors.

required
eos VolumeTranslationEOS

Cubic family, "srk" or "pr".

required
rackett_factor Tensor | None

Optional per-component dimensionless Rackett factors.

None
source ParameterSource

Pedersen volume-translation parameter source.

_PEDERSEN_SOURCE

Returns:

Type Description
VolumeTranslation

Temperature-independent additive shifts in m3/mol.

Notes

SRK uses Pedersen Eq. 4.46 and PR uses the Jhaveri-Youngren Eq. 4.49. These correlations were fitted for nonhydrocarbons and hydrocarbons lighter than C7; heavier fractions should use density matching or a characterized parameter set.

Source code in src/torch_flash/eos/volume_translation.py
def pedersen_peneloux_translation(
    components: ComponentSet,
    eos: VolumeTranslationEOS,
    *,
    rackett_factor: Tensor | None = None,
    source: ParameterSource = _PEDERSEN_SOURCE,
) -> VolumeTranslation:
    """Return the Pedersen light-component SRK/PR Péneloux translation.

    Parameters
    ----------
    components
        Ordered critical constants and acentric factors.
    eos
        Cubic family, ``"srk"`` or ``"pr"``.
    rackett_factor
        Optional per-component dimensionless Rackett factors.
    source
        Pedersen volume-translation parameter source.

    Returns
    -------
    VolumeTranslation
        Temperature-independent additive shifts in m3/mol.

    Notes
    -----
    SRK uses Pedersen Eq. 4.46 and PR uses the Jhaveri-Youngren Eq. 4.49.
    These correlations were fitted for nonhydrocarbons and hydrocarbons
    lighter than C7; heavier fractions should use density matching or a
    characterized parameter set.
    """
    parameters = _parameter_mapping(source, "Pedersen-Peneloux")
    correlations = _mapping(parameters, "correlations", str(source))
    correlation = _mapping(correlations, eos, str(source))
    z_ra = (
        rackett_compressibility_factor(components.acentric_factor, source=source)
        if rackett_factor is None
        else rackett_factor.to(
            dtype=components.critical_temperature.dtype,
            device=components.critical_temperature.device,
        )
    )
    if z_ra.shape != components.critical_temperature.shape:
        raise ValueError("rackett_factor must have one value per component")
    if not bool(torch.isfinite(z_ra).all()):
        raise ValueError("rackett_factor must be finite")
    scale = _number(correlation, "scale", str(source))
    target = _number(correlation, "target", str(source))
    published_c = (
        scale * R * components.critical_temperature / components.critical_pressure * (target - z_ra)
    )
    return VolumeTranslation.constant(
        -published_c,
        source=f"{load_model_parameters(source).identifier}:{eos}",
    )

pedersen_temperature_dependent_translation

pedersen_temperature_dependent_translation(
    model: CubicEOS,
    reference_density: Tensor,
    *,
    pressure: float = 101325.0,
    source: ParameterSource = _PEDERSEN_SOURCE,
) -> VolumeTranslation

Fit Pedersen's linear C7+ translation using Eqs. 5.7-5.9.

Parameters:

Name Type Description Default
model CubicEOS

Untranslated parent cubic EOS.

required
reference_density Tensor

One pure-component reference density per component in kg/m3.

required
pressure float

Positive matching pressure in Pa.

101325.0
source ParameterSource

Pedersen temperature-dependent parameter source.

_PEDERSEN_SOURCE

Returns:

Type Description
VolumeTranslation

Reference shifts in m3/mol and slopes in m3/(mol K).

Raises:

Type Description
ValueError

If density/pressure inputs are invalid or model already has a translation.

InvalidStateError

If the correlation target temperature is not subcritical for every component.

Notes

reference_density is in kg/m3 at 288.15 K by default. The ASTM correlation supplies the target density at 353.15 K, and the parent cubic EoS supplies the unshifted liquid volumes at both temperatures.

Source code in src/torch_flash/eos/volume_translation.py
def pedersen_temperature_dependent_translation(
    model: CubicEOS,
    reference_density: Tensor,
    *,
    pressure: float = 101_325.0,
    source: ParameterSource = _PEDERSEN_SOURCE,
) -> VolumeTranslation:
    """Fit Pedersen's linear C7+ translation using Eqs. 5.7-5.9.

    Parameters
    ----------
    model
        Untranslated parent cubic EOS.
    reference_density
        One pure-component reference density per component in kg/m3.
    pressure
        Positive matching pressure in Pa.
    source
        Pedersen temperature-dependent parameter source.

    Returns
    -------
    VolumeTranslation
        Reference shifts in m3/mol and slopes in m3/(mol K).

    Raises
    ------
    ValueError
        If density/pressure inputs are invalid or ``model`` already has a
        translation.
    InvalidStateError
        If the correlation target temperature is not subcritical for every
        component.

    Notes
    -----
    ``reference_density`` is in kg/m3 at 288.15 K by default.  The ASTM
    correlation supplies the target density at 353.15 K, and the parent cubic
    EoS supplies the unshifted liquid volumes at both temperatures.
    """
    parameters = _parameter_mapping(source, "Pedersen-Peneloux")
    temperature_parameters = _mapping(
        parameters,
        "temperature_dependent",
        str(source),
    )
    reference_temperature = _number(
        temperature_parameters,
        "reference_temperature",
        str(source),
    )
    target_temperature = _number(
        temperature_parameters,
        "target_temperature",
        str(source),
    )
    astm_constant = _number(
        temperature_parameters,
        "astm_density_constant",
        str(source),
    )
    nonlinear_factor = _number(
        temperature_parameters,
        "nonlinear_factor",
        str(source),
    )
    if not 0.0 < reference_temperature < target_temperature:
        raise ParameterDatabaseError(
            "temperature-dependent translation requires 0 < "
            "reference_temperature < target_temperature"
        )
    if astm_constant <= 0.0 or nonlinear_factor < 0.0:
        raise ParameterDatabaseError(
            "temperature-dependent translation requires a positive ASTM "
            "constant and non-negative nonlinear factor"
        )
    density = reference_density.to(
        dtype=model.critical_temperature.dtype,
        device=model.critical_temperature.device,
    )
    if density.shape != model.critical_temperature.shape:
        raise ValueError("reference_density must have one value per component")
    if not bool(torch.isfinite(density).all() & (density > 0.0).all()):
        raise ValueError("reference_density must be finite and positive")
    if not math.isfinite(pressure) or pressure <= 0.0:
        raise ValueError("pressure must be finite and positive")
    if bool(
        (model.volume_translation != 0.0).any() | (model.volume_translation_slope != 0.0).any()
    ):
        raise ValueError(
            "Pedersen temperature-dependent matching requires an untranslated parent EoS"
        )
    if bool((model.critical_temperature <= target_temperature).any()):
        raise InvalidStateError(
            "Pedersen temperature-dependent density matching requires "
            "target_temperature below every component critical temperature"
        )

    ncomponents = model.ncomponents
    identity = torch.eye(
        ncomponents,
        dtype=model.critical_temperature.dtype,
        device=model.critical_temperature.device,
    )
    pressure_vector = model.critical_temperature.new_full((ncomponents,), pressure)

    def parent_volume(temperature_value: float) -> Tensor:
        temperature_vector = model.critical_temperature.new_full(
            (ncomponents,),
            temperature_value,
        )
        z = model.select_z(
            temperature_vector,
            pressure_vector,
            identity,
            "liquid",
        )
        return z * R * temperature_vector / pressure_vector

    reference_parent_volume = parent_volume(reference_temperature)
    target_parent_volume = parent_volume(target_temperature)
    reference_shift = model.molar_mass / density - reference_parent_volume
    delta_temperature = target_temperature - reference_temperature
    expansion = astm_constant / density.square()
    target_density = density * torch.exp(
        -expansion * delta_temperature * (1.0 + nonlinear_factor * expansion * delta_temperature)
    )
    target_shift = model.molar_mass / target_density - target_parent_volume
    slope = (target_shift - reference_shift) / delta_temperature
    return VolumeTranslation(
        reference_shift,
        slope,
        reference_temperature,
        f"{load_model_parameters(source).identifier}:temperature-dependent",
    )

rackett_compressibility_factor

rackett_compressibility_factor(
    acentric_factor: Tensor,
    *,
    source: ParameterSource = _PEDERSEN_SOURCE,
) -> Tensor

Evaluate the Rackett compressibility-factor correlation.

Parameters:

Name Type Description Default
acentric_factor Tensor

Dimensionless component acentric factors.

required
source ParameterSource

Volume-translation parameter source providing intercept and slope.

_PEDERSEN_SOURCE

Returns:

Type Description
Tensor

Dimensionless Rackett factors with the same shape, dtype, and device as acentric_factor.

Notes

The bundled Pedersen set evaluates Z_RA = 0.29056 - 0.08775 omega (Pedersen et al., Eq. 4.47).

Source code in src/torch_flash/eos/volume_translation.py
def rackett_compressibility_factor(
    acentric_factor: Tensor,
    *,
    source: ParameterSource = _PEDERSEN_SOURCE,
) -> Tensor:
    """Evaluate the Rackett compressibility-factor correlation.

    Parameters
    ----------
    acentric_factor
        Dimensionless component acentric factors.
    source
        Volume-translation parameter source providing intercept and slope.

    Returns
    -------
    Tensor
        Dimensionless Rackett factors with the same shape, dtype, and device
        as ``acentric_factor``.

    Notes
    -----
    The bundled Pedersen set evaluates ``Z_RA = 0.29056 - 0.08775 omega``
    (Pedersen et al., Eq. 4.47).
    """
    parameters = _parameter_mapping(source, "Pedersen-Peneloux")
    rackett = _mapping(parameters, "rackett", str(source))
    intercept = _number(rackett, "intercept", str(source))
    slope = _number(rackett, "acentric_slope", str(source))
    return intercept + slope * acentric_factor

whitson_volume_translation

whitson_volume_translation(
    components: ComponentSet,
    eos: VolumeTranslationEOS,
    *,
    heavy_families: Mapping[str, HydrocarbonFamily]
    | None = None,
    source: ParameterSource = _WHITSON_SOURCE,
) -> VolumeTranslation

Return Whitson Tables 4.2-4.3 translations.

Parameters:

Name Type Description Default
components ComponentSet

Ordered component set.

required
eos VolumeTranslationEOS

Cubic family, "srk" or "pr".

required
heavy_families Mapping[str, HydrocarbonFamily] | None

Family assignments for components without tabulated shift factors.

None
source ParameterSource

Whitson volume-translation parameter source.

_WHITSON_SOURCE

Returns:

Type Description
VolumeTranslation

Temperature-independent additive shifts in m3/mol.

Raises:

Type Description
ValueError

If an untabulated component lacks a heavy-family assignment.

Notes

Named light components and normal paraffins through n-decane use the tabulated s_i = c_i/b_i values. Any other component requires a family entry and uses s_i = 1 - A0/M_i**A1 with M in g/mol.

Source code in src/torch_flash/eos/volume_translation.py
def whitson_volume_translation(
    components: ComponentSet,
    eos: VolumeTranslationEOS,
    *,
    heavy_families: Mapping[str, HydrocarbonFamily] | None = None,
    source: ParameterSource = _WHITSON_SOURCE,
) -> VolumeTranslation:
    """Return Whitson Tables 4.2-4.3 translations.

    Parameters
    ----------
    components
        Ordered component set.
    eos
        Cubic family, ``"srk"`` or ``"pr"``.
    heavy_families
        Family assignments for components without tabulated shift factors.
    source
        Whitson volume-translation parameter source.

    Returns
    -------
    VolumeTranslation
        Temperature-independent additive shifts in m3/mol.

    Raises
    ------
    ValueError
        If an untabulated component lacks a heavy-family assignment.

    Notes
    -----
    Named light components and normal paraffins through n-decane use the
    tabulated ``s_i = c_i/b_i`` values.  Any other component requires a family
    entry and uses ``s_i = 1 - A0/M_i**A1`` with ``M`` in g/mol.
    """
    parameters = _parameter_mapping(source, "Whitson-Peneloux")
    eos_parameters = _mapping(_mapping(parameters, "eos", str(source)), eos, str(source))
    factors = _mapping(eos_parameters, "pure_shift_factors", str(source))
    family_parameters = _mapping(parameters, "heavy_families", str(source))
    supplied_families = {} if heavy_families is None else dict(heavy_families)
    shift_factors: list[Tensor] = []
    for index, name in enumerate(components.names):
        tabulated = factors.get(name)
        if isinstance(tabulated, int | float):
            shift_factors.append(components.molar_mass.new_tensor(float(tabulated)))
            continue
        family = supplied_families.get(name)
        if family is None:
            raise ValueError(
                f"Whitson has no tabulated shift factor for {name!r}; provide heavy_families[name]"
            )
        family_values = _mapping(family_parameters, family, str(source))
        a0 = _number(family_values, "A0", str(source))
        a1 = _number(family_values, "A1", str(source))
        molar_mass_g = 1000.0 * components.molar_mass[index]
        shift_factors.append(1.0 - a0 / molar_mass_g.pow(a1))
    factor = torch.stack(shift_factors)
    omega_b = _number(eos_parameters, "covolume_factor", str(source))
    covolume = omega_b * R * components.critical_temperature / components.critical_pressure
    published_c = factor * covolume
    return VolumeTranslation.constant(
        -published_c,
        source=f"{load_model_parameters(source).identifier}:{eos}",
    )