# -*- coding: utf-8 -*-
# BioSTEAM: The Biorefinery Simulation and Techno-Economic Analysis Modules
# Copyright (C) 2020-2023, Yoel Cortes-Pena <yoelcortes@gmail.com>
# 
# This module is under the UIUC open-source license. See 
# github.com/BioSTEAMDevelopmentGroup/biosteam/blob/master/LICENSE.txt
# for license details.
"""
"""
from scipy.optimize import shgo
import flexsolve as flx
from numba import njit
from warnings import warn
from ..base import SparseVector
from ..exceptions import InfeasibleRegion, NoEquilibrium
from . import binary_phase_fraction as binary
from .equilibrium import Equilibrium
from .dew_point import DewPoint
from .bubble_point import BubblePoint
from .fugacity_coefficients import IdealFugacityCoefficients
from .poyinting_correction_factors import MockPoyintingCorrectionFactors
from . import activity_coefficients as ac
from .. import functional as fn
from ..utils import Cache
import thermosteam as tmo
import numpy as np

__all__ = ('VLE', 'VLECache')

@njit(cache=True)
def xy(x, Ks):
    x[x < 0] = 1e-16
    x /= x.sum()
    y = x * Ks
    y /= y.sum()
    return x, y

def xVlogK_iter_2n(xVlogK, pcf_Psat_over_P, T, P, z, f_gamma, gamma_args, f_phi, n,
                  gas_conversion, liquid_conversion):
    xVlogK = xVlogK.copy()
    x = xVlogK[:n]
    Ks = np.exp(xVlogK[n+1:])
    x, y = xy(x, Ks)
    Ks[:] = pcf_Psat_over_P * f_gamma(x, T, *gamma_args) / f_phi(y, T, P)
    if gas_conversion or liquid_conversion:
        V = xVlogK[n]
        if V < 0.: V = 0.
        elif V > 1.: V = 1.
        V = binary.compute_phase_fraction_2N(z, Ks)
        if gas_conversion: 
            z = z + gas_conversion(material=y * V, T=T, P=P, phase='g')
            z[z <= 0] = 1e-16
            z /= z.sum()
        if liquid_conversion:
            z = z + liquid_conversion(material=x * (1 - V), T=T, P=P, phase='l')
            z[z <= 0] = 1e-16
            z /= z.sum()
    Ks[Ks < 1e-16] = 1e-16
    xVlogK[n] = V = binary.compute_phase_fraction_2N(z, Ks)
    xVlogK[:n] = z/(1. + V * (Ks - 1.))
    xVlogK[n+1:] = np.log(Ks)
    return xVlogK

def xVlogK_iter(
        xVlogK, pcf_Psat_over_P, T, P,
        z, z_light, z_heavy, f_gamma, gamma_args,
        f_phi, n, gas_conversion, liquid_conversion
    ):
    xVlogK = xVlogK.copy()
    x = xVlogK[:n]
    Ks = np.exp(xVlogK[n+1:])
    x, y = xy(x, Ks)
    Ks[:] = pcf_Psat_over_P * f_gamma(x, T, *gamma_args) / f_phi(y, T, P)
    V = xVlogK[n]
    if V < 0.: V = 0.
    elif V > 1.: V = 1.
    if gas_conversion or liquid_conversion:
        if gas_conversion: 
            z = z + gas_conversion(material=y * V, T=T, P=P, phase='g')
            z[z <= 0] = 1e-16
            z /= z.sum()
        if liquid_conversion:
            z = z + liquid_conversion(material=x * (1 - V), T=T, P=P, phase='l')
            z[z <= 0] = 1e-16
            z /= z.sum()
    Ks[Ks < 1e-16] = 1e-16
    xVlogK[n] = V = binary.solve_phase_fraction_Rashford_Rice(z, Ks, V, z_light, z_heavy)
    xVlogK[:n] = z / (1. + V * (Ks - 1.))
    xVlogK[n+1:] = np.log(Ks)
    return xVlogK

def set_flows(vapor_mol, liquid_mol, index, vapor_data, total_data):
    vapor_mol[index] = vapor_data
    liquid_mol[index] = total_data - vapor_data

class VLE(Equilibrium, phases='lg'):
    """
    Create a VLE object that performs vapor-liquid equilibrium when called.
        
    Parameters
    ----------
    imol=None : :class:`~thermosteam.indexer.MaterialIndexer`, optional
        Molar chemical phase data is stored here.
    thermal_condition=None : :class:`~thermosteam.ThermalCondition`, optional
        Temperature and pressure results are stored here.
    thermo=None : :class:`~thermosteam.Thermo`, optional
        Themodynamic property package for equilibrium calculations.
        Defaults to `thermosteam.settings.get_thermo()`.
    
    Examples
    --------
    First create a VLE object:
    
    >>> from thermosteam import indexer, equilibrium, settings
    >>> settings.set_thermo(['Water', 'Ethanol', 'Methanol', 'Propanol'], cache=True)
    >>> imol = indexer.MolarFlowIndexer(
    ...             l=[('Water', 304), ('Ethanol', 30)],
    ...             g=[('Methanol', 40), ('Propanol', 1)])
    >>> vle = equilibrium.VLE(imol)
    >>> vle
    VLE(imol=MolarFlowIndexer(
            g=[('Methanol', 40), ('Propanol', 1)],
            l=[('Water', 304), ('Ethanol', 30)]),
        thermal_condition=ThermalCondition(T=298.15, P=101325))
    
    Equilibrium given vapor fraction and pressure:
    
    >>> vle(V=0.5, P=101325)
    >>> vle
    VLE(imol=MolarFlowIndexer(
            g=[('Water', 126.7), ('Ethanol', 26.38), ('Methanol', 33.49), ('Propanol', 0.8958)],
            l=[('Water', 177.3), ('Ethanol', 3.622), ('Methanol', 6.509), ('Propanol', 0.1042)]),
        thermal_condition=ThermalCondition(T=363.85, P=101325))
    
    Equilibrium given temperature and pressure:
    
    >>> vle(T=363.88, P=101325)
    >>> vle
    VLE(imol=MolarFlowIndexer(
            g=[('Water', 127.4), ('Ethanol', 26.41), ('Methanol', 33.54), ('Propanol', 0.8968)],
            l=[('Water', 176.6), ('Ethanol', 3.59), ('Methanol', 6.456), ('Propanol', 0.1032)]),
        thermal_condition=ThermalCondition(T=363.88, P=101325))
    
    Equilibrium given enthalpy and pressure:
    
    >>> H = vle.thermo.mixture.xH(vle.imol, T=363.88, P=101325)
    >>> vle(H=H, P=101325)
    >>> vle
    VLE(imol=MolarFlowIndexer(
            g=[('Water', 127.4), ('Ethanol', 26.41), ('Methanol', 33.54), ('Propanol', 0.8968)],
            l=[('Water', 176.6), ('Ethanol', 3.59), ('Methanol', 6.456), ('Propanol', 0.1032)]),
        thermal_condition=ThermalCondition(T=363.88, P=101325))
    
    Equilibrium given entropy and pressure:
    
    >>> S = vle.thermo.mixture.xS(vle.imol, T=363.88, P=101325)
    >>> vle(S=S, P=101325)
    >>> vle
    VLE(imol=MolarFlowIndexer(
            g=[('Water', 127.4), ('Ethanol', 26.41), ('Methanol', 33.54), ('Propanol', 0.8968)],
            l=[('Water', 176.6), ('Ethanol', 3.59), ('Methanol', 6.456), ('Propanol', 0.1032)]),
        thermal_condition=ThermalCondition(T=363.88, P=101325))
    
    Equilibrium given vapor fraction and temperature:
    
    >>> vle(V=0.5, T=363.88)
    >>> vle
    VLE(imol=MolarFlowIndexer(
            g=[('Water', 126.7), ('Ethanol', 26.38), ('Methanol', 33.49), ('Propanol', 0.8958)],
            l=[('Water', 177.3), ('Ethanol', 3.622), ('Methanol', 6.509), ('Propanol', 0.1042)]),
        thermal_condition=ThermalCondition(T=363.88, P=101431))
    
    Equilibrium given enthalpy and temperature:
    
    >>> vle(H=H, T=363.88)
    >>> vle
    VLE(imol=MolarFlowIndexer(
            g=[('Water', 127.4), ('Ethanol', 26.41), ('Methanol', 33.54), ('Propanol', 0.8968)],
            l=[('Water', 176.6), ('Ethanol', 3.59), ('Methanol', 6.456), ('Propanol', 0.1032)]),
        thermal_condition=ThermalCondition(T=363.88, P=101325))
    
    Non-partitioning heavy and gaseous chemicals also affect VLE. Calculation 
    are repeated with non-partitioning chemicals:
        
    >>> from thermosteam import indexer, equilibrium, settings, Chemical
    >>> O2 = Chemical('O2', phase='g')
    >>> Glucose = Chemical('Glucose', phase='l', default=True)
    >>> settings.set_thermo(['Water', 'Ethanol', 'Methanol', 'Propanol', O2, Glucose], cache=True)
    >>> imol = indexer.MolarFlowIndexer(
    ...             l=[('Water', 304), ('Ethanol', 30), ('Glucose', 5)],
    ...             g=[('Methanol', 40), ('Propanol', 1), ('O2', 10)])
    >>> vle = equilibrium.VLE(imol)
    >>> vle(T=363.88, P=101325)
    >>> vle
    VLE(imol=MolarFlowIndexer(
            g=[('Water', 159.5), ('Ethanol', 27.65), ('Methanol', 35.63), ('Propanol', 0.9337), ('O2', 10)],
            l=[('Water', 144.5), ('Ethanol', 2.352), ('Methanol', 4.369), ('Propanol', 0.0663), ('Glucose', 5)]),
        thermal_condition=ThermalCondition(T=363.88, P=101325))
    
    >>> vle(V=0.5, P=101325)
    >>> vle
    VLE(imol=MolarFlowIndexer(
            g=[('Water', 126.7), ('Ethanol', 26.38), ('Methanol', 33.52), ('Propanol', 0.8957), ('O2', 10)],
            l=[('Water', 177.3), ('Ethanol', 3.618), ('Methanol', 6.478), ('Propanol', 0.1043), ('Glucose', 5)]),
        thermal_condition=ThermalCondition(T=362.47, P=101325))
    
    >>> H = vle.thermo.mixture.xH(vle.imol, T=363.88, P=101325)
    >>> vle(H=H, P=101325)
    >>> vle
    VLE(imol=MolarFlowIndexer(
            g=[('Water', 127.4), ('Ethanol', 26.42), ('Methanol', 33.58), ('Propanol', 0.8968), ('O2', 10)],
            l=[('Water', 176.6), ('Ethanol', 3.583), ('Methanol', 6.421), ('Propanol', 0.1032), ('Glucose', 5)]),
        thermal_condition=ThermalCondition(T=362.51, P=101325))
    
    >>> S = vle.thermo.mixture.xS(vle.imol, T=363.88, P=101325)
    >>> vle(S=S, P=101325)
    >>> vle
    VLE(imol=MolarFlowIndexer(
            g=[('Water', 128.2), ('Ethanol', 26.45), ('Methanol', 33.63), ('Propanol', 0.8979), ('O2', 10)],
            l=[('Water', 175.8), ('Ethanol', 3.548), ('Methanol', 6.365), ('Propanol', 0.1021), ('Glucose', 5)]),
        thermal_condition=ThermalCondition(T=362.54, P=101325))
    
    >>> vle(V=0.5, T=363.88)
    >>> vle
    VLE(imol=MolarFlowIndexer(
            g=[('Water', 126.7), ('Ethanol', 26.38), ('Methanol', 33.49), ('Propanol', 0.8958), ('O2', 10)],
            l=[('Water', 177.3), ('Ethanol', 3.622), ('Methanol', 6.509), ('Propanol', 0.1042), ('Glucose', 5)]),
        thermal_condition=ThermalCondition(T=363.88, P=106841))
    
    >>> vle(H=H, T=363.88)
    >>> vle 
    VLE(imol=MolarFlowIndexer(
            g=[('Water', 126.7), ('Ethanol', 26.38), ('Methanol', 33.49), ('Propanol', 0.8958), ('O2', 10)],
            l=[('Water', 177.3), ('Ethanol', 3.622), ('Methanol', 6.51), ('Propanol', 0.1042), ('Glucose', 5)]),
        thermal_condition=ThermalCondition(T=363.88, P=106842))

    >>> vle(S=S, T=363.88)
    >>> vle 
    VLE(imol=MolarFlowIndexer(
            g=[('Water', 128.1), ('Ethanol', 26.45), ('Methanol', 33.6), ('Propanol', 0.8978), ('O2', 10)],
            l=[('Water', 175.9), ('Ethanol', 3.555), ('Methanol', 6.399), ('Propanol', 0.1022), ('Glucose', 5)]),
        thermal_condition=ThermalCondition(T=363.88, P=106562))

    The presence of a non-partitioning gaseous chemical will result in some
    evaporation, even if the tempeture is below the saturated bubble point:
        
    >>> from thermosteam import indexer, equilibrium, settings, Chemical
    >>> O2 = Chemical('O2', phase='g')
    >>> settings.set_thermo(['Water', O2], cache=True)
    >>> imol = indexer.MolarFlowIndexer(
    ...             l=[('Water', 30)],
    ...             g=[('O2', 10)])
    >>> vle = equilibrium.VLE(imol)
    >>> vle(T=300., P=101325)
    >>> vle
    VLE(imol=MolarFlowIndexer(
            g=[('Water', 0.3617), ('O2', 10)],
            l=[('Water', 29.64)]),
        thermal_condition=ThermalCondition(T=300.00, P=101325))

    """
    __slots__ = (
        'method', # [str] Method for solving equilibrium.
        '_T', # [float] Temperature [K].
        '_P', # [float] Pressure [Pa].
        '_H_hat', # [float] Specific enthalpy [kJ/kg].
        '_S_hat', # [float] Specific entropy [kJ/K/kg].
        '_V', # [float] Molar vapor fraction.
        '_dew_point', # [DewPoint] Solves for dew point.
        '_bubble_point', # [BubblePoint] Solves for bubble point.
        '_y', # [1d array] Vapor composition.
        '_z_last', # tuple[1d array] Last bulk composition.
        '_phi', # [FugacityCoefficients] Estimates fugacity coefficients of gas.
        '_pcf', # [PoyintingCorrectionFactors] Estimates the PCF of a liquid.
        '_gamma', # [ActivityCoefficients] Estimates activity coefficients of a liquid.
        '_liquid_mol', # [1d array] Liquid molar data.
        '_vapor_mol', # [1d array] Vapor molar data.
        '_phase_data', # tuple[str, 1d array] Phase-data pairs.
        '_K', # [1d array] Partition coefficients.
        '_v', # [1d array] Molar vapor data in equilibrium.
        '_index', # [1d array] Index of chemicals in equilibrium.
        '_F_mass', # [float] Total mass data.
        '_chemical', # [Chemical] Single chemical in equilibrium.
        '_mol_vle', # [1d array] Moles of chemicals in VLE calculations.
        '_N', # [int] Number of chemicals in equilibrium.
        '_z', # [1d array] Molar composition of chemicals in equilibrium
        '_z_norm', # [1d array] Normalized molar composition of chemicals in equilibrium
        '_z_light', # [1d array] Molar composition of light chemicals not included in equilibrium calculation.
        '_z_heavy', # [1d array] Molar composition of heavy chemicals not included in equilibrium calculation.
        '_nonzero', # [1d array(bool)] Chemicals present in the mixture
        '_F_mol', # [float] Total moles of chemicals (accounting for dissociation).
        '_F_mol_vle', # [float] Total moles in equilibrium.
        '_F_mol_light', # [float] Total moles of gas chemicals not included in equilibrium calculation.
        '_F_mol_heavy', # [float] Total moles of heavy chemicals not included in equilibrium calculation.
        '_dmol_vle',
        '_dF_mol',
    )
    maxiter = 20
    T_tol = 5e-8
    P_tol = 1.
    H_hat_tol = 1e-6
    S_hat_tol = 1e-6
    V_tol = 1e-6
    K_tol = 1e-6
    y_tol = 1e-8
    default_method = 'fixed-point'
    
    def __init__(self, imol=None, thermal_condition=None,
                 thermo=None):
        self.method = self.default_method
        self._T = self._P = self._H_hat = self._V = 0
        super().__init__(imol, thermal_condition, thermo)
        self._z_last = None
        self._nonzero = None
        self._index = ()
    
    def __call__(self, *, T=None, P=None, V=None, H=None, S=None, x=None, y=None,
                 gas_conversion=None, liquid_conversion=None):
        """
        Perform vapor-liquid equilibrium.

        Parameters
        ----------
        T : float, optional
            Operating temperature [K].
        P : float, optional
            Operating pressure [Pa].
        V : float, optional
            Molar vapor fraction.
        H : float, optional
            Enthalpy [kJ/hr].
        S : float, optional
            Entropy [kJ/hr/K]
        x : float, optional
            Molar composition of liquid (for binary mixtures).
        y : float, optional
            Molar composition of vapor (for binary mixtures).
        
        Notes
        -----
        You may only specify two of the following parameters: P, H, T, V, x, and y.
        Additionally, If x or y is specified, the other parameter must be either
        P or T (e.g., x and V is invalid).
        
        """
        if gas_conversion or liquid_conversion:
            if gas_conversion:
                if isinstance(gas_conversion, tmo.KineticReaction):
                    gas_conversion = gas_conversion.conversion_handle(tmo.Stream(thermo=self.thermo))
                elif isinstance(gas_conversion, (tmo.Reaction, tmo.ReactionItem)):
                    gas_conversion = gas_conversion.conversion_handle(None)
            if liquid_conversion:
                if isinstance(liquid_conversion, tmo.KineticReaction):
                    liquid_conversion = liquid_conversion.conversion_handle(tmo.Stream(thermo=self.thermo))
                elif isinstance(liquid_conversion, (tmo.Reaction, tmo.ReactionItem)):
                    liquid_conversion = liquid_conversion.conversion_handle(None)
            if x is not None or y is not None:
                raise ValueError(
                    "can not pass either 'x' or 'y' arguments with reactions preset"
                )
        
        ### Decide what kind of equilibrium to run ###
        T_spec = T is not None
        P_spec = P is not None
        V_spec = V is not None
        H_spec = H is not None
        S_spec = S is not None
        x_spec = x is not None
        y_spec = y is not None
        N_specs = (T_spec + P_spec + V_spec + H_spec + S_spec + x_spec + y_spec)
        assert N_specs == 2, ("must pass two and only two of the following "
                              "specifications: T, P, V, H, S, x, y")
        
        # Run equilibrium
        if T_spec:
            if P_spec:
                try:
                    self.set_thermal_condition(T, P, gas_conversion, liquid_conversion)
                except NoEquilibrium:
                    thermal_condition = self._thermal_condition
                    thermal_condition.T = T
                    thermal_condition.P = P
            elif V_spec:
                try:
                    self.set_TV(T, V, gas_conversion, liquid_conversion)
                except NoEquilibrium:
                    thermal_condition = self._thermal_condition
                    thermal_condition.T = T
            elif H_spec:
                self.set_TH(T, H, gas_conversion, liquid_conversion)
            elif S_spec:
                self.set_TS(T, S, gas_conversion, liquid_conversion)
            elif x_spec:
                self.set_Tx(T, np.asarray(x))
            else: # y_spec
                self.set_Ty(T, np.asarray(y))
        elif P_spec:
            if V_spec:
                try:
                    self.set_PV(P, V, gas_conversion, liquid_conversion)
                except NoEquilibrium:
                    thermal_condition = self._thermal_condition
                    thermal_condition.P = P
            elif H_spec:
                try:
                    self.set_PH(P, H, gas_conversion, liquid_conversion)
                except NoEquilibrium:
                    thermal_condition = self._thermal_condition
                    thermal_condition.P = P
            elif S_spec:
                try:
                    self.set_PS(P, S, gas_conversion, liquid_conversion)
                except:
                    try:
                        self.set_PS(P, S, gas_conversion, liquid_conversion)
                    except NoEquilibrium:
                        thermal_condition = self._thermal_condition
                        thermal_condition.P = P
            elif x_spec:
                self.set_Px(P, np.asarray(x))
            else: # y_spec
                self.set_Py(P, np.asarray(y))
        elif S_spec: # pragma: no cover
            if y_spec:
                raise NotImplementedError('specification S and y is invalid')
            elif x_spec:
                raise NotImplementedError('specification S and x is invalid')
            elif H_spec:
                raise NotImplementedError('specification H and S is invalid')
            else: # V_spec
                raise NotImplementedError('specification V and S not implemented')
        elif H_spec: # pragma: no cover
            if y_spec:
                raise NotImplementedError('specification H and y is invalid')
            elif x_spec:
                raise NotImplementedError('specification H and x is invalid')
            else: # V_spec
                raise NotImplementedError('specification V and H not implemented')
        elif V_spec: # pragma: no cover
            if y_spec:
                raise ValueError("specification V and y is invalid")
            else: # x_spec
                raise ValueError("specification V and x is invalid")
        else: # pragma: no cover
            raise ValueError("can only pass either 'x' or 'y' arguments, not both")
    
    ### Single component equilibrium case ###
        
    def _set_thermal_condition_chemical(self, T, P):
        chemical = self._chemical
        if T >= chemical.Tc: 
            self._liquid_mol[self._index] = 0
            self._vapor_mol[self._index] = self._mol_vle
        else:
            # Either liquid or gas
            Psat = chemical.Psat(T)
            tol = 1e-3
            if P < Psat - tol:
                self._liquid_mol[self._index] = 0
                self._vapor_mol[self._index] = self._mol_vle
            elif P > Psat + tol:
                self._liquid_mol[self._index] = self._mol_vle
                self._vapor_mol[self._index] = 0
    
    def _set_TV_chemical(self, T, V):
        # Set vapor fraction
        self._T = self._thermal_condition.T = self._chemical.Psat(T)
        self._vapor_mol[self._index] = V * self._mol_vle
        self._liquid_mol[self._index] = self._mol_vle - self._vapor_mol[self._index]
        
    def _set_PV_chemical(self, P, V):
        # Set vapor fraction
        self._T = self._thermal_condition.T = self._chemical.Tsat(P, check_validity=False)
        self._vapor_mol[self._index] = self._mol_vle * V
        self._liquid_mol[self._index] = self._mol_vle - self._vapor_mol[self._index]
        
    def _set_PH_chemical(self, P, H): 
        mol = self._mol_vle
        index = self._index
        vapor_mol = self._vapor_mol
        liquid_mol = self._liquid_mol
        thermo = self._thermo
        phase_data = self._phase_data
        mixture = thermo.mixture
        chemical = self._chemical
        
        # Set temperature in equilibrium
        self._T = self._thermal_condition.T = T = chemical.Tsat(P, check_validity=False) 
        
        # Check if super heated vapor
        vapor_mol[index] = mol
        liquid_mol[index] = 0
        H_dew = mixture.xH(phase_data, T, P)
        if H >= H_dew:
            self._thermal_condition.T = mixture.xsolve_T_at_HP(phase_data, H, T, P)
            return

        # Check if subcooled liquid
        vapor_mol[index] = 0
        liquid_mol[index] = mol
        H_bubble = mixture.xH(phase_data, T, P)
        if H <= H_bubble:
            self._thermal_condition.T = mixture.xsolve_T_at_HP(phase_data, H, T, P)
            return
        
        # Adjust vapor fraction accordingly
        V = (H - H_bubble)/(H_dew - H_bubble)
        vapor_mol[index] = mol*V
        liquid_mol[index] = mol - vapor_mol[index]
        
    def _set_TH_chemical(self, T, H):
        index = self._index
        mol = self._mol_vle
        vapor_mol = self._vapor_mol
        liquid_mol = self._liquid_mol
        mixture = self._thermo.mixture
        phase_data = self._phase_data
        
        # Set Pressure in equilibrium
        self._thermal_condition.P = P = self._chemical.Psat(T)
        
        # Check if super heated vapor
        vapor_mol[index] = mol
        liquid_mol[index] = 0
        H_dew = mixture.xH(phase_data, T, P)
        if H >= H_dew:
            raise NotImplementedError('cannot solve for pressure yet')
            self._thermal_condition.P = mixture.xsolve_P_at_HT(phase_data, H, T, P)
            return

        # Check if subcooled liquid
        vapor_mol[index] = 0
        liquid_mol[index] = mol
        H_bubble = mixture.xH(phase_data, T, P)
        if H <= H_bubble:
            raise NotImplementedError('cannot solve for pressure yet')
            self._thermal_condition.P = mixture.xsolve_P_at_HT(phase_data, H, T, P)
            return
        
        # Adjust vapor fraction accordingly
        V = (H - H_bubble)/(H_dew - H_bubble)
        vapor_mol[index] = mol*V
        liquid_mol[index] = mol - vapor_mol[index]
        
    def _set_PS_chemical(self, P, S): 
        mol = self._mol_vle
        index = self._index
        vapor_mol = self._vapor_mol
        liquid_mol = self._liquid_mol
        thermo = self._thermo
        phase_data = self._phase_data
        mixture = thermo.mixture
        chemical = self._chemical
        
        # Set temperature in equilibrium
        self._T = self._thermal_condition.T = T = chemical.Tsat(P, check_validity=False)
        
        # Check if super heated vapor
        vapor_mol[index] = mol
        liquid_mol[index] = 0
        S_dew = mixture.xS(phase_data, T, P)
        if S >= S_dew:
            self._thermal_condition.T = mixture.xsolve_T_at_SP(phase_data, S, T, P)
            return

        # Check if subcooled liquid
        vapor_mol[index] = 0
        liquid_mol[index] = mol
        S_bubble = mixture.xS(phase_data, T, P)
        if S <= S_bubble:
            self._thermal_condition.T = mixture.xsolve_T_at_SP(phase_data, S, T, P)
            return
        
        # Adjust vapor fraction accordingly
        V = (S - S_bubble)/(S_dew - S_bubble)
        vapor_mol[index] = mol*V
        liquid_mol[index] = mol - vapor_mol[index]
        
    def _set_TS_chemical(self, T, S):
        index = self._index
        mol = self._mol_vle
        vapor_mol = self._vapor_mol
        liquid_mol = self._liquid_mol
        mixture = self._thermo.mixture
        phase_data = self._phase_data
        
        # Set Pressure in equilibrium
        self._thermal_condition.P = P = self._chemical.Psat(T)
        
        # Check if super heated vapor
        vapor_mol[index] = mol
        liquid_mol[index] = 0
        S_dew = mixture.xS(phase_data, T, P)
        if S >= S_dew:
            raise NotImplementedError('cannot solve for pressure yet')
            self._thermal_condition.P = mixture.xsolve_P_at_ST(phase_data, S, T, P)
            return

        # Check if subcooled liquid
        vapor_mol[index] = 0
        liquid_mol[index] = mol
        S_bubble = mixture.xS(phase_data, T, P)
        if S <= S_bubble:
            raise NotImplementedError('cannot solve for pressure yet')
            self._thermal_condition.P = mixture.xsolve_P_at_ST(phase_data, S, T, P)
            return
        
        # Adjust vapor fraction accordingly
        V = (S - S_bubble)/(S_dew - S_bubble)
        vapor_mol[index] = mol*V
        liquid_mol[index] = mol - vapor_mol[index]
        
    def _lever_rule(self, x, y):
        split_frac = (self._z[0]-x[0])/(y[0]-x[0])
        if not -0.00001 < split_frac < 1.00001:
            raise InfeasibleRegion('phase composition')
        if split_frac > 1:
            split_frac = 1
        elif split_frac < 0:
            split_frac = 0
        self._vapor_mol[self._index] = v = self._F_mol * split_frac * y
        self._liquid_mol[self._index] = self._mol_vle - v
    
    def set_Tx(self, T, x):
        self._setup()
        assert self._N == 2, 'number of species in equilibrium must be 2 to specify x'
        self._thermal_condition.P, y = self._bubble_point.solve_Py(x, T)
        self._lever_rule(x, y)
    
    def set_Px(self, P, x):
        self._setup()
        assert self._N == 2, 'number of species in equilibrium must be 2 to specify x'
        self._thermal_condition.T, y = self._bubble_point.solve_Ty(x, P) 
        self._lever_rule(x, y)
        
    def set_Ty(self, T, y):
        self._setup()
        assert self._N == 2, 'number of species in equilibrium must be 2 to specify y'
        self._thermal_condition.P, x = self._dew_point.solve_Px(y, T)
        self._lever_rule(x, y)
    
    def set_Py(self, P, y):
        self._setup()
        assert self._N == 2, 'number of species in equilibrium must be 2 to specify y'
        self._thermal_condition.T, x = self._dew_point.solve_Tx(y, P) 
        self._lever_rule(x, y)
        
    def set_thermal_condition(self, T, P, gas_conversion=None, liquid_conversion=None):
        self._setup(gas_conversion, liquid_conversion)
        thermal_condition = self._thermal_condition
        self._T = thermal_condition.T = T
        self._P = thermal_condition.P = P
        if self._N == 0: return
        if self._N == 1: return self._set_thermal_condition_chemical(T, P)
        # Check if there is equilibrium
        if gas_conversion:
            P_dew, dz_dew, x_dew, _ = self._dew_point.solve_Px(self._z, T, gas_conversion)
            if P <= P_dew and not self._F_mol_heavy:
                self._vapor_mol[self._index] = self._mol_vle + dz_dew * self._F_mol_vle
                self._liquid_mol[self._index] = 0
                return
        else:
            P_dew, x_dew = self._dew_point.solve_Px(self._z, T)
            if P <= P_dew and not self._F_mol_heavy:
                self._vapor_mol[self._index] = self._mol_vle
                self._liquid_mol[self._index] = 0
                return
            dz_dew = None
        if liquid_conversion:
            P_bubble, dz_bubble, y_bubble, _ = self._bubble_point.solve_Py(self._z, T, liquid_conversion)
            if P >= P_bubble and not self._F_mol_light:
                self._vapor_mol[self._index] = 0
                self._liquid_mol[self._index] = self._mol_vle + dz_bubble * self._F_mol_vle
                return
        else:
            P_bubble, y_bubble = self._bubble_point.solve_Py(self._z, T)
            if P >= P_bubble and not self._F_mol_light:
                self._vapor_mol[self._index] = 0
                self._liquid_mol[self._index] = self._mol_vle
                return
            dz_bubble = None
        # Guess composition in the vapor is a
        # weighted average of bubble/dew points
        dP = (P_bubble - P_dew)
        V = (P - P_dew) / dP if dP > 1. else 0.5
        self._refresh_K(V, y_bubble, x_dew, dz_bubble, dz_dew)
        v = self._solve_v(T, P, gas_conversion, liquid_conversion)
        mol_vle = self._mol_vle + self._dmol_vle if (gas_conversion or liquid_conversion) else self._mol_vle 
        set_flows(self._vapor_mol, self._liquid_mol, self._index, v, mol_vle)
        
    def set_TV(self, T, V, gas_conversion=None, liquid_conversion=None):
        self._setup(gas_conversion, liquid_conversion)
        thermal_condition = self._thermal_condition
        thermal_condition.T = self._T = T
        if self._N == 0: raise RuntimeError('no chemicals present to perform VLE')
        if self._N == 1: return self._set_TV_chemical(T, V)
        if self._F_mol_heavy and V == 1.: V = 1. - 1e-3
        if self._F_mol_light and V == 0.: V = 1e-3
        if V == 1:
            if gas_conversion:
                P_dew, dz, y, x_dew = self._dew_point.solve_Px(self._z, T, gas_conversion)
                self._vapor_mol[self._index] = self._mol_vle + dz * self._F_mol_vle
            else:
                P_dew, x_dew = self._dew_point.solve_Px(self._z, T)
                self._vapor_mol[self._index] = self._mol_vle
            self._liquid_mol[self._index] = 0
            thermal_condition.P = P_dew
        elif V == 0:
            if liquid_conversion:
                P_bubble, dz, y_bubble, x = self._bubble_point.solve_Py(self._z, T)
                self._liquid_mol[self._index] = self._mol_vle + dz * self._F_mol_vle
            else:
                P_bubble, y_bubble = self._bubble_point.solve_Py(self._z, T)
                self._liquid_mol[self._index] = self._mol_vle
            self._vapor_mol[self._index] = 0
            thermal_condition.P = P_bubble
        else:
            if liquid_conversion:
                P_bubble, dz_bubble, y_bubble, _ = self._bubble_point.solve_Py(self._z, T, liquid_conversion)
            else:
                P_bubble, y_bubble = self._bubble_point.solve_Py(self._z, T)
                dz_bubble = None
            if gas_conversion:
                P_dew, dz_dew, y, x_dew = self._dew_point.solve_Px(self._z, T, gas_conversion)
            else:
                P_dew, x_dew = self._dew_point.solve_Px(self._z, T, gas_conversion)
                dz_dew = None
            self._refresh_K(V, y_bubble, x_dew, dz_bubble, dz_dew)
            if self._F_mol_light: P_bubble = 0.1 * self._bubble_point.Pmax + 0.9 * P_bubble
            if self._F_mol_heavy: P_dew = 0.1 * self._bubble_point.Pmin + 0.9 * P_dew
            
            V_bubble = self._V_err_at_P(P_bubble, 0., gas_conversion, liquid_conversion)
            if V_bubble > V:
                F_mol = self._F_mol
                mol = self._mol_vle
                if liquid_conversion:
                    mol = mol + self._dmol_vle
                    F_mol = F_mol + self._dF_mol
                F_mol_vapor = F_mol * V
                v = y_bubble * F_mol_vapor
                mask = v > mol
                v[mask] = mol[mask]
                P = P_bubble
            else:
                V_dew = self._V_err_at_P(P_dew, 0., gas_conversion, liquid_conversion)
                if V_dew < V:
                    F_mol = self._F_mol
                    mol = self._mol_vle
                    if gas_conversion:
                        mol = mol + self._dmol_vle
                        F_mol = F_mol + self._dF_mol
                    l = x_dew * F_mol * (1. - V)
                    mask = l > mol 
                    l[mask] = mol[mask]
                    v = mol - l
                    P = P_dew
                else:
                    P = flx.IQ_interpolation(
                        self._V_err_at_P,
                        P_bubble, P_dew, V_bubble - V, V_dew - V,
                        self._P, self.P_tol, self.V_tol,
                        (V, gas_conversion, liquid_conversion),
                        checkiter=False, checkbounds=False,
                        maxiter=self.maxiter,
                    )
                    v = self._v
                    mol = self._mol_vle
                    if gas_conversion or liquid_conversion:
                        mol = mol + self._dmol_vle
            
            self._P = thermal_condition.P = P
            set_flows(self._vapor_mol, self._liquid_mol, self._index, v, mol)
            if liquid_conversion or gas_conversion:
                try: self._H_hat = (
                        self.mixture.xH(self._phase_data, T, P) + (self.chemicals.Hf * mol).sum()
                    ) / self._F_mass
                except: pass
            else:
                try: self._H_hat = self.mixture.xH(self._phase_data, T, P) / self._F_mass
                except: pass

    def set_TH(self, T, H, gas_conversion=None, liquid_conversion=None):
        self._setup(gas_conversion, liquid_conversion)
        if self._N == 0: raise RuntimeError('no chemicals present to perform VLE')
        if self._N == 1: return self._set_TH_chemical(T, H)
        self._T = T
        index = self._index
        mol = self._mol_vle
        vapor_mol = self._vapor_mol
        liquid_mol = self._liquid_mol
        phase_data = self._phase_data
        
        # Check if super heated vapor
        P_dew, x_dew = self._dew_point.solve_Px(self._z, T)
        if self._F_mol_heavy: P_dew = 0.5 * P_dew + 0.5 * self._bubble_point.Pmin
        vapor_mol[index] = mol
        liquid_mol[index] = 0
        H_dew = self.mixture.xH(phase_data, T, P_dew)
        dH_dew = (H - H_dew)
        if dH_dew >= 0:
            raise NotImplementedError('cannot solve for pressure yet')

        # Check if subcooled liquid
        P_bubble, y_bubble = self._bubble_point.solve_Py(self._z, T)
        if self._F_mol_light: P_bubble = 2 * P_bubble
        vapor_mol[index] = 0
        liquid_mol[index] = mol
        H_bubble = self.mixture.xH(phase_data, T, P_bubble)
        dH_bubble = (H - H_bubble)
        if dH_bubble <= 0:
            raise NotImplementedError('cannot solve for pressure yet')

        # Guess overall vapor fraction, and vapor flow rates
        V = dH_bubble/(H_dew - H_bubble)
        
        # Guess composition in the vapor is a weighted average of boiling points
        self._refresh_K(V, y_bubble, x_dew)
        F_mass = self._F_mass
        H_hat = H/F_mass
        P = flx.IQ_interpolation(
            self._H_hat_err_at_P,
            P_bubble, P_dew,
            H_bubble/F_mass - H_hat, H_dew/F_mass - H_hat,
            self._P, self.P_tol, self.H_hat_tol,
            (H_hat,), checkiter=False, checkbounds=False,
            maxiter=self.maxiter,
        )
        self._P = self._thermal_condition.P = P   
        self._thermal_condition.T = T
    
    def set_TS(self, T, S, gas_conversion=None, liquid_conversion=None):
        self._setup(gas_conversion, liquid_conversion)
        if self._N == 0: raise RuntimeError('no chemicals present to perform VLE')
        if self._N == 1: return self._set_TS_chemical(T, S)
        self._T = T
        index = self._index
        mol = self._mol_vle
        vapor_mol = self._vapor_mol
        liquid_mol = self._liquid_mol
        phase_data = self._phase_data
        
        # Check if super heated vapor
        P_dew, x_dew = self._dew_point.solve_Px(self._z, T)
        if self._F_mol_heavy: P_dew = 0.5 * P_dew + 0.5 * self._bubble_point.Pmin
        vapor_mol[index] = mol
        liquid_mol[index] = 0
        S_dew = self.mixture.xS(phase_data, T, P_dew)
        dS_dew = (S - S_dew)
        if dS_dew >= 0:
            raise NotImplementedError('cannot solve for pressure yet')

        # Check if subcooled liquid
        P_bubble, y_bubble = self._bubble_point.solve_Py(self._z, T)
        if self._F_mol_light: P_bubble = 2 * P_bubble
        vapor_mol[index] = 0
        liquid_mol[index] = mol
        S_bubble = self.mixture.xS(phase_data, T, P_bubble)
        dS_bubble = (S - S_bubble)
        if dS_bubble <= 0:
            raise NotImplementedError('cannot solve for pressure yet')

        # Guess overall vapor fraction, and vapor flow rates
        V = dS_bubble/(S_dew - S_bubble)
        
        # Guess composition in the vapor is a weighted average of boiling points
        self._refresh_K(V, y_bubble, x_dew)
        F_mass = self._F_mass
        S_hat = S/F_mass
        P = flx.IQ_interpolation(
            self._S_hat_err_at_P,
            P_bubble, P_dew,
            S_bubble/F_mass - S_hat, S_dew/F_mass - S_hat,
            self._P, self.P_tol, self.S_hat_tol,
            (S_hat,), checkiter=False, checkbounds=False,
            maxiter=self.maxiter,
        )
        self._P = self._thermal_condition.P = P   
        self._thermal_condition.T = T
    
    def set_PV(self, P, V, gas_conversion=None, liquid_conversion=None):
        self._setup(gas_conversion, liquid_conversion)
        self._thermal_condition.P = self._P = P
        if self._N == 0: raise RuntimeError('no chemicals present to perform VLE')
        if self._N == 1: return self._set_PV_chemical(P, V)
        
        # Setup bounderies
        thermal_condition = self._thermal_condition
        index = self._index
        vapor_mol = self._vapor_mol
        liquid_mol = self._liquid_mol
        if self._F_mol_heavy and V == 1.: V = 1. - 1e-3
        if self._F_mol_light and V == 0.: V = 1e-3
        if V == 1 and not self._F_mol_heavy:
            if gas_conversion:
                T_dew, dz, y, x_dew = self._dew_point.solve_Tx(self._z, P, gas_conversion)
                self._vapor_mol[self._index] = self._mol_vle + dz * self._F_mol_vle
            else:
                T_dew, x_dew = self._dew_point.solve_Tx(self._z, P)
                self._vapor_mol[self._index] = self._mol_vle
            self._liquid_mol[self._index] = 0
            thermal_condition.T = T_dew
        elif V == 0 and not self._F_mol_light:
            if liquid_conversion:
                T_bubble, dz, y_bubble, x = self._bubble_point.solve_Ty(self._z, P, liquid_conversion)
                self._liquid_mol[self._index] = self._mol_vle + dz * self._F_mol_vle
            else:
                T_bubble, y_bubble = self._bubble_point.solve_Ty(self._z, P)
                self._liquid_mol[self._index] = self._mol_vle
            self._vapor_mol[self._index] = 0
            thermal_condition.T = T_bubble
        else:
            if liquid_conversion:
                T_bubble, dz_bubble, y_bubble, _ = self._bubble_point.solve_Ty(self._z, P, liquid_conversion)
            else:
                T_bubble, y_bubble = self._bubble_point.solve_Ty(self._z, P)
                dz_bubble = None
            if gas_conversion:
                T_dew, dz_dew, y, x_dew = self._dew_point.solve_Tx(self._z, P, gas_conversion)
            else:
                T_dew, x_dew = self._dew_point.solve_Tx(self._z, P)
                dz_dew = None
            self._refresh_K(V, y_bubble, x_dew, dz_bubble, dz_dew)
            if self._F_mol_light: T_bubble = 0.1 * self._bubble_point.Tmin + 0.9 * T_bubble
            if self._F_mol_heavy: T_dew = 0.1 * self._bubble_point.Tmax + 0.9 * T_dew
            V_bubble = self._V_err_at_T(T_bubble, 0., gas_conversion, liquid_conversion)
            if V_bubble > V:
                F_mol = self._F_mol
                mol = self._mol_vle
                if liquid_conversion:
                    mol = mol + self._dmol_vle
                    F_mol = F_mol + self._dF_mol
                F_mol_vapor = F_mol * V
                v = y_bubble * F_mol_vapor
                mask = v > mol
                v[mask] = mol[mask]
                T = T_bubble
            else:
                V_dew = self._V_err_at_T(T_dew, 0., gas_conversion, liquid_conversion)
                if V_dew < V:
                    F_mol = self._F_mol
                    mol = self._mol_vle
                    if gas_conversion:
                        mol = mol + self._dmol_vle
                        F_mol = F_mol + self._dF_mol
                    l = x_dew * F_mol * (1. - V)
                    mask = l > mol 
                    l[mask] = mol[mask]
                    v = mol - l
                    T = T_dew
                else:
                    T = flx.IQ_interpolation(
                        self._V_err_at_T,
                        T_bubble, T_dew, V_bubble - V, V_dew - V,
                        self._T, self.T_tol, self.V_tol,
                        (V, gas_conversion, liquid_conversion),
                        checkiter=False, checkbounds=False,
                        maxiter=self.maxiter,
                    )
                    v = self._v
                    mol = self._mol_vle
                    if gas_conversion or liquid_conversion:
                        mol = mol + self._dmol_vle
            self._T = thermal_condition.T = T
            set_flows(vapor_mol, liquid_mol, index, v, mol)
            if liquid_conversion or gas_conversion:
                try: self._H_hat = (
                        self.mixture.xH(self._phase_data, T, P) + (self.chemicals.Hf * mol).sum(0)
                    ) / self._F_mass
                except: pass
            else:
                try: self._H_hat = self.mixture.xH(self._phase_data, T, P) / self._F_mass
                except: pass
    
    def set_PS(self, P, S, gas_conversion=None, liquid_conversion=None):
        self._setup(gas_conversion, liquid_conversion)
        thermal_condition = self._thermal_condition
        thermal_condition.P = self._P = P
        if self._N == 0: 
            thermal_condition.T = self.mixture.xsolve_T_at_SP(
                self._phase_data, S, thermal_condition.T, P
            )
            return
        if self._N == 1: return self._set_PS_chemical(P, S)
        
        # Setup bounderies
        index = self._index
        mol = self._mol_vle
        vapor_mol = self._vapor_mol
        liquid_mol = self._liquid_mol
        
        # Check if subcooled liquid
        T_bubble, y_bubble = self._bubble_point.solve_Ty(self._z, P)
        if self._F_mol_light: T_bubble = 0.9 * T_bubble + 0.1 * self._bubble_point.Tmin
        vapor_mol[index] = 0
        liquid_mol[index] = mol
        S_bubble = self.mixture.xS(self._phase_data, T_bubble, P)
        dS_bubble = S - S_bubble
        if dS_bubble <= 0:
            thermal_condition.T = self.mixture.xsolve_T_at_SP(self._phase_data, S, T_bubble, P)
            return
        
        # Check if super heated vapor
        T_dew, x_dew = self._dew_point.solve_Tx(self._z, P)
        if T_dew <= T_bubble: 
            T_dew, T_bubble = T_bubble, T_dew
            T_dew += 0.5
            T_bubble -= 0.5
        if self._F_mol_heavy: T_dew = 0.9 * T_dew + 0.1 * self._dew_point.Tmax
        vapor_mol[index] = mol
        liquid_mol[index] = 0
        S_dew = self.mixture.xS(self._phase_data, T_dew, P)
        dS_dew = S - S_dew
        if dS_dew >= 0:
            thermal_condition.T = self.mixture.xsolve_T_at_SP(self._phase_data, S, T_dew, P)
            return
        
        # Guess T, overall vapor fraction, and vapor flow rates
        V = dS_bubble/(S_dew - S_bubble)
        self._refresh_K(V, y_bubble, x_dew)
        
        F_mass = self._F_mass
        S_hat = S/F_mass
        S_hat_bubble = self._S_hat_err_at_T(T_bubble, 0.)
        if (S_hat_bubble:=self._S_hat_err_at_T(T_bubble, 0.)) > S_hat:
            T = T_bubble
        elif (S_hat_dew:=self._S_hat_err_at_T(T_dew, 0.)) < S_hat:
            T = T_dew
        else:
            T = flx.IQ_interpolation(
                self._S_hat_err_at_T, T_bubble, T_dew, 
                S_hat_bubble - S_hat, S_hat_dew - S_hat,
                self._T, self.T_tol, self.S_hat_tol,
                (S_hat,), checkiter=False, checkbounds=False,
                maxiter=self.maxiter
            )
        # Make sure energy balance is correct by vaporizing a fraction
        # of the liquid or condensing a fraction of the vapor
        self._T = thermal_condition.T = T
        mol_liq = liquid_mol.copy()
        mol_liq[:] = 0.
        mol_liq[index] = liquid_mol[index]
        mol_gas= vapor_mol.copy()
        mol_gas[:] = 0.
        mol_gas[index] = vapor_mol[index]
        S_gas = self.mixture.S('g', mol_gas, T, P)
        S_liq = self.mixture.S('l', mol_liq, T, P)
        S_current = self.mixture.xS(self._phase_data, T, P)
        if S_current > S: # Condense a fraction of the vapor
            # Energy balance: S = f * S_condense + S_current
            S_condense = self.mixture.S('l', mol_gas, T, P) - S_gas
            try:
                f = (S - S_current) / S_condense
            except: # Floating point errors
                f = 0
            else:
                if f < 0.:
                    f = 0.
                elif f > 0.:
                    if f > 1.: f = 1.
                    condensed = f * mol_gas[index]
                    liquid_mol[index] += condensed
                    vapor_mol[index] -= condensed 
                else:
                    f = 0.
        else: # Vaporize a fraction of the liquid
            # Energy balance: S = f * S_vaporise + S_current
            S_vaporise = self.mixture.S('g', mol_liq, T, P) - S_liq
            try:
                f = (S - S_current) / S_vaporise
            except: # Floating point errors
                f = 0
            else:
                if f < 0.:
                    f = 0.
                elif f > 0.:
                    if f > 1.: f = 1.
                    vaporised = f * mol_liq[index]
                    vapor_mol[index] += vaporised
                    liquid_mol[index] -= vaporised
                else:
                    f = 0.
        if f == 0. or f == 1.:
            self._T = thermal_condition.T = self.mixture.xsolve_T_at_SP(
                self._phase_data, S, T, P
            )
        self._S_hat = S_hat
    
    def set_PH(self, P, H, gas_conversion=None, liquid_conversion=None):
        self._setup(gas_conversion, liquid_conversion)
        thermal_condition = self._thermal_condition
        thermal_condition.P = self._P = P
        if self._N == 0: 
            thermal_condition.T = self.mixture.xsolve_T_at_HP(
                self._phase_data, H, thermal_condition.T, P
            )
            return
        if self._N == 1: return self._set_PH_chemical(P, H)
        
        # Setup bounderies
        index = self._index
        vapor_mol = self._vapor_mol
        liquid_mol = self._liquid_mol
        
        # Check if subcooled liquid
        if liquid_conversion:
            T_bubble, dz_bubble, y_bubble, x = self._bubble_point.solve_Ty(self._z, P, liquid_conversion)
            mol = self._mol_vle + dz_bubble * self._F_mol_vle
        else:
            T_bubble, y_bubble = self._bubble_point.solve_Ty(self._z, P)
            mol = self._mol_vle
            dz_bubble = None
        if self._F_mol_light: T_bubble = 0.9 * T_bubble + 0.1 * self._bubble_point.Tmin
        vapor_mol[index] = 0
        liquid_mol[index] = mol
        H_bubble = self.mixture.xH(self._phase_data, T_bubble, P)
        if liquid_conversion: 
            Hf = (self.chemicals.Hf * mol).sum()
            H_bubble += Hf
        dH_bubble = H - H_bubble
        if dH_bubble <= 0:
            if liquid_conversion: H -= Hf
            thermal_condition.T = self.mixture.xsolve_T_at_HP(self._phase_data, H, T_bubble, P)
            return
        
        # Check if super heated vapor
        if gas_conversion:
            T_dew, dz_dew, y, x_dew = self.dew_point.solve_Tx(self._z, P, gas_conversion)
            mol = self._mol_vle + dz_dew
        else:
            T_dew, x_dew = self._dew_point.solve_Tx(self._z, P)
            mol = self._mol_vle
            dz_dew = None
        if T_dew <= T_bubble: 
            T_dew, T_bubble = T_bubble, T_dew
            T_dew += 0.5
            T_bubble -= 0.5
        if self._F_mol_heavy: T_dew = 0.9 * T_dew + 0.1 * self._dew_point.Tmax
        vapor_mol[index] = mol
        liquid_mol[index] = 0
        H_dew = self.mixture.xH(self._phase_data, T_dew, P)
        if gas_conversion: 
            Hf = (self.chemicals.Hf * mol).sum()
            H_dew += Hf
        dH_dew = H - H_dew
        if dH_dew >= 0:
            if gas_conversion: H -= Hf
            thermal_condition.T = self.mixture.xsolve_T_at_HP(self._phase_data, H, T_dew, P)
            return
        
        # Guess T, overall vapor fraction, and vapor flow rates
        V = abs(dH_bubble / (H_dew - H_bubble))
        self._refresh_K(V, y_bubble, x_dew, dz_bubble, dz_dew)
        
        F_mass = self._F_mass
        H_hat = H/F_mass
        
        if (H_hat_bubble:=self._H_hat_err_at_T(T_bubble, 0., gas_conversion, liquid_conversion)) > H_hat:
            T = T_bubble
        elif (H_hat_dew:=self._H_hat_err_at_T(T_dew, 0., gas_conversion, liquid_conversion)) < H_hat:
            T = T_dew
        else:
            T = flx.IQ_interpolation(
                self._H_hat_err_at_T, T_bubble, T_dew, 
                H_hat_bubble - H_hat, H_hat_dew - H_hat,
                self._T, self.T_tol, self.H_hat_tol,
                (H_hat, gas_conversion, liquid_conversion),
                checkiter=False, checkbounds=False,
                maxiter=self.maxiter
            )
         
        if gas_conversion or liquid_conversion: 
            H -= (self.chemicals.Hf * (self._mol_vle + self._dmol_vle)).sum()
            
        # Make sure energy balance is correct by vaporizing a fraction
        # of the liquid or condensing a fraction of the vapor
        self._T = thermal_condition.T = T
        mol_liq = liquid_mol.copy()
        mol_liq[:] = 0.
        mol_liq[index] = liquid_mol[index]
        mol_gas= vapor_mol.copy()
        mol_gas[:] = 0.
        mol_gas[index] = vapor_mol[index]
        H_gas = self.mixture.H('g', mol_gas, T, P)
        H_liq = self.mixture.H('l', mol_liq, T, P)
        H_current = self.mixture.xH(self._phase_data, T, P)
        if H_current > H: # Condense a fraction of the vapor
            # Energy balance: H = f * H_condense + H_current
            H_condense = self.mixture.H('l', mol_gas, T, P) - H_gas
            try:
                f = (H - H_current) / H_condense
            except: # Floating point errors
                f = 0
            else:
                if f < 0.:
                    f = 0.
                elif f > 0.:
                    if f > 1.: f = 1.
                    condensed = f * mol_gas[index]
                    liquid_mol[index] += condensed
                    vapor_mol[index] -= condensed 
                else:
                    f = 0
        else: # Vaporize a fraction of the liquid
            # Energy balance: H = f * H_vaporise + H_current
            H_vaporise = self.mixture.H('g', mol_liq, T, P) - H_liq
            try:
                f = (H - H_current) / H_vaporise
            except: # Floating point errors
                f = 0
            else:
                if f < 0.:
                    f = 0.
                elif f > 0.:
                    if f > 1.: f = 1.
                    vaporised = f * mol_liq[index]
                    vapor_mol[index] += vaporised
                    liquid_mol[index] -= vaporised
                else:
                    f = 0
        if f == 0. or f == 1.:
            self._T = thermal_condition.T = self.mixture.xsolve_T_at_HP(
                self._phase_data, H, T, P
            )
        self._H_hat = H_hat
    
    def _refresh_K(self, V, y_bubble, x_dew, dz_bubble=None, dz_dew=None): # TODO: use reaction data here for better estimate
        F_mol_vle = self._F_mol_vle
        z = self._z_norm
        L = (1 - V)
        if dz_bubble is not None:
            F_mol_vle = F_mol_vle - dz_bubble * L * F_mol_vle
        if dz_dew is not None:
            F_mol_vle = F_mol_vle - dz_dew * V * F_mol_vle
        K_bubble = y_bubble / z
        x_min = z * 1e-9
        index = x_dew < x_min
        x_dew[index] = x_min[index]
        K_dew = z / x_dew
        self._K = K_dew * V + K_bubble * L
        self._V = V
    
    def _H_hat_err_at_T(self, T, H_hat, gas_conversion, liquid_conversion):
        v = self._solve_v(T, self._P, gas_conversion, liquid_conversion)
        if gas_conversion or liquid_conversion: 
            mol_vle = self._mol_vle + self._dmol_vle
            set_flows(self._vapor_mol, self._liquid_mol, self._index, v, mol_vle)
            H = self.mixture.xH(self._phase_data, T, self._P) + (self.chemicals.Hf * mol_vle).sum()
        else:
            mol_vle = self._mol_vle 
            set_flows(self._vapor_mol, self._liquid_mol, self._index, v, mol_vle)
            H = self.mixture.xH(self._phase_data, T, self._P)
        self._H_hat = H / self._F_mass
        return self._H_hat - H_hat
    
    def _H_hat_err_at_P(self, P, H_hat, gas_conversion=None, liquid_conversion=None):
        v = self._solve_v(self._T, P, gas_conversion, liquid_conversion)
        if gas_conversion or liquid_conversion: 
            mol_vle = self._mol_vle + self._dmol_vle
            set_flows(self._vapor_mol, self._liquid_mol, self._index, v, mol_vle)
            H = self.mixture.xH(self._phase_data, self._T, P) + (self.chemicals.Hf * mol_vle).sum()
        else:
            mol_vle = self._mol_vle 
            set_flows(self._vapor_mol, self._liquid_mol, self._index, v, mol_vle)
            H = self.mixture.xH(self._phase_data, self._T, P)
        self._H_hat = H / self._F_mass
        return self._H_hat - H_hat
    
    def _S_hat_err_at_T(self, T, S_hat):
        set_flows(self._vapor_mol, self._liquid_mol, self._index,
                  self._solve_v(T, self._P), self._mol_vle)
        self._S_hat = self.mixture.xS(self._phase_data, T, self._P)/self._F_mass
        return self._S_hat - S_hat
    
    def _S_hat_err_at_P(self, P, S_hat):
        set_flows(self._vapor_mol, self._liquid_mol, self._index,
                  self._solve_v(self._T, P), self._mol_vle)
        self._S_hat = self.mixture.xS(self._phase_data, self._T, P)/self._F_mass
        return self._S_hat - S_hat
    
    def _V_err_at_P(self, P, V, gas_conversion, liquid_conversion):
        v = self._solve_v(self._T , P, gas_conversion, liquid_conversion).sum()
        if gas_conversion or liquid_conversion:
            F_mol_vle = self._F_mol_vle + self._dF_mol
        else:
            F_mol_vle = self._F_mol_vle
        return v / F_mol_vle - V
        
    def _V_err_at_T(self, T, V, gas_conversion, liquid_conversion):
        v = self._solve_v(T, self._P, gas_conversion, liquid_conversion).sum()
        if gas_conversion or liquid_conversion:
            F_mol_vle = self._F_mol_vle + self._dF_mol
        else:
            F_mol_vle = self._F_mol_vle
        return v / F_mol_vle - V
    
    def _solve_v(self, T, P, gas_conversion=None, liquid_conversion=None):
        """Solve for vapor mol"""
        method = self.method
        if method == 'shgo':
            if gas_conversion or liquid_conversion:
                raise RuntimeError('shgo is not a valid method (yet) when reactions are present')
            gamma = self._gamma
            phi = self._phi
            Psats = np.array([i(T) for i in self._bubble_point.Psats]) 
            pcf = self._pcf(T, P, Psats)
            F_mol_vle = self._F_mol_vle
            mol_vle = self._mol_vle
            z = mol_vle / F_mol_vle
            self._v = v = F_mol_vle * solve_vle_vapor_mol_shgo(
                z, T, gamma.f, gamma.args, P, pcf * Psats, phi.f, phi.args, 
                dict(f_tol=self.y_tol, minimizer_kwargs=dict(f_tol=self.y_tol)),
            )
            self._z_last = z
        elif method == 'fixed-point':
            Psats = np.array([i(T) for i in
                              self._bubble_point.Psats])
            pcf_Psats_over_P = self._pcf(T, P, Psats) * Psats / P
            self._T = T
            self._v = v = self._solve_v_fixed_point(pcf_Psats_over_P, T, P, gas_conversion, liquid_conversion)
            if gas_conversion or liquid_conversion:
                mol_vle = self._mol_vle + self._dmol_vle
            else:
                mol_vle = self._mol_vle
            mask = v > mol_vle
            v[mask] = mol_vle[mask]
            v[v < 0.] = 0.
        else:
            raise RuntimeError(f"invalid method '{method}'")
        return v
    
    def _solve_v_fixed_point(self, pcf_Psat_over_P, T, P, gas_conversion, liquid_conversion):
        gamma = self._gamma
        z = self._z
        K = self._K # pcf_Psat_over_P
        n = z.size
        if n > 2 or self._z_light or self._z_heavy:
            f = xVlogK_iter
            args = (pcf_Psat_over_P, T, P, z, 
                    self._z_light, self._z_heavy, 
                    gamma.f, gamma.args, self._phi, n,
                    gas_conversion, liquid_conversion)
        else:
            f = xVlogK_iter_2n
            args = (pcf_Psat_over_P, T, P, z, 
                    gamma.f, gamma.args, self._phi, n,
                    gas_conversion, liquid_conversion)
        xVlogK = np.zeros(2 * n + 1)
        xVlogK[n] = V = self._V
        K[K < 1e-16] = 1e-16
        xVlogK[:n] = z/(1. + V * (K - 1.))
        xVlogK[n+1:] = np.log(K)
        xVlogK = flx.aitken(
            f, xVlogK, self.K_tol, args, checkiter=False, 
            checkconvergence=False, convergenceiter=5,
            maxiter=self.maxiter
        )
        self._V = V = xVlogK[n]
        self._K = K = np.exp(xVlogK[n+1:])
        x = xVlogK[:n]
        x, y = xy(x, K)
        self._z_last = z
        if gas_conversion or liquid_conversion:
            dmol_vle = 0
            if gas_conversion: 
                dmol_vle += gas_conversion(material=y * V, T=T, P=P, phase='g')
            if liquid_conversion:
                dmol_vle += liquid_conversion(material=x * (1 - V), T=T, P=P, phase='l')
            dmol_vle *= self._F_mol
            self._dmol_vle = dmol_vle
            self._dF_mol = dmol_vle.sum()
            v = (self._F_mol - self._dF_mol) * V * x * K
        else:
            v = self._F_mol * V * x * K
        return v
    
    def _setup(self, gas_conversion=None, liquid_conversion=None):
        imol = self._imol
        self._phase_data = tuple(imol)
        self._liquid_mol = liquid_mol = imol['l']
        self._vapor_mol = vapor_mol = imol['g']
        mol = liquid_mol + vapor_mol
        if not mol.any(): raise NoEquilibrium('no chemicals to perform equilibrium')
        nonzero = set(mol.nonzero_keys())
        if gas_conversion:
            nonzero.update(
                gas_conversion.reaction.stoichiometry.nonzero_keys()
            )
        if liquid_conversion:
            nonzero.update(
                liquid_conversion.reaction.stoichiometry.nonzero_keys()
            )
        chemicals = self.chemicals
        if self._nonzero == nonzero:
            index = self._index
            eq_chems = chemicals.tuple
            eq_chems = [eq_chems[i] for i in index]
            reset = False
        else:
            # Set up indices for both equilibrium and non-equilibrium species
            index = chemicals.get_vle_indices(nonzero)
            eq_chems = chemicals.tuple
            eq_chems = [eq_chems[i] for i in index]
            reset = True     
            self._nonzero = nonzero
            self._index = index
        if gas_conversion and [i.ID for i in eq_chems] != [i.ID for i in gas_conversion.reaction.chemicals]:
            gas_conversion.set_chemicals(eq_chems)
        if liquid_conversion and [i.ID for i in eq_chems] != [i.ID for i in liquid_conversion.reaction.chemicals]:
            liquid_conversion.set_chemicals(eq_chems)
        
        # Get overall composition
        self._F_mass = (chemicals.MW * mol).sum()
        self._mol_vle = mol_vle = mol[index]

        # Set light and heavy keys
        LNK_index = chemicals._light_indices
        HNK_index = chemicals._heavy_indices
        vapor_mol[HNK_index] = 0
        vapor_mol[LNK_index] = light_mol = mol[LNK_index]
        liquid_mol[LNK_index] = 0
        liquid_mol[HNK_index] = heavy_mol = mol[HNK_index]
        if not mol_vle.any(): raise NoEquilibrium('no chemicals to perform equilibrium')
        self._F_mol_light = F_mol_light = light_mol.sum()
        self._F_mol_heavy = F_mol_heavy = (heavy_mol * chemicals._heavy_solutes).sum()
        self._F_mol_vle = F_mol_vle = mol_vle.sum()
        self._F_mol = F_mol = F_mol_vle + F_mol_light + F_mol_heavy
        self._z = mol_vle / F_mol
        self._z_light = z_light = F_mol_light / F_mol
        self._z_heavy = z_heavy = F_mol_heavy / F_mol
        self._z_norm = mol_vle / F_mol_vle
        N = len(index)
        if N:
            N += z_light > 0.
            N += z_heavy > 0.
        self._N = N
        if reset:
            if N == 0:
                self._phi = self._gamma = self._pcf = self._dew_point = self._bubble_point = None
            elif N == 1:
                self._chemical, = eq_chems
            else:
                # Set equilibrium objects
                thermo = self._thermo
                self._bubble_point = bp = BubblePoint(eq_chems, thermo)
                self._dew_point = DewPoint(eq_chems, thermo)
                self._pcf = bp.pcf
                self._gamma = bp.gamma
                self._phi = bp.phi


class VLECache(Cache): load = VLE
del Cache, Equilibrium

@njit(cache=True)
def liquid_fugacity(mol_L, T, pcf_Psats, f_gamma, gamma_args):
    total_mol_L = mol_L.sum()
    if total_mol_L:
        x = mol_L / total_mol_L
        fugacity = x * f_gamma(x, T, *gamma_args) * pcf_Psats
    else:
        fugacity = np.ones_like(mol_L)
    return fugacity 

@njit(cache=True)
def vapor_fugacity(mol_v, T, P, f_phi, phi_args):
    total_mol_v = mol_v.sum()
    if total_mol_v:
        y = mol_v / total_mol_v
        fugacity = y * P * f_phi(y, T, P, *phi_args)
    else:
        fugacity = np.ones_like(mol_v)
    return fugacity

@njit(cache=True)
def gibbs_free_energy(mol, fugacity):
    fugacity[fugacity <= 0] = 1
    g_mix = (mol * np.log(fugacity)).sum()
    return g_mix

@njit(cache=True)
def vle_objective_function(mol_v, mol, T, f_gamma, gamma_args, P, pcf_Psats, f_phi, phi_args):
    mol_l = mol - mol_v
    g_mix_l = gibbs_free_energy(mol_l, liquid_fugacity(mol_l, T, pcf_Psats, f_gamma, gamma_args))
    g_mix_g = gibbs_free_energy(mol_v, vapor_fugacity(mol_v, T, P, f_phi, phi_args))
    g_mix = g_mix_l + g_mix_g
    return g_mix

def solve_vle_vapor_mol_shgo(
        mol, T, f_gamma, gamma_args, P, pcf_Psats, f_phi, phi_args, shgo_options,
    ):
    args = (mol, T, f_gamma, gamma_args, P, pcf_Psats, f_phi, phi_args)
    bounds = np.zeros([mol.size, 2])
    bounds[:, 1] = mol
    result = shgo(vle_objective_function, bounds, args, options=shgo_options)
    return result.x
