# -*- coding: utf-8 -*-
r'''Chemical Engineering Design Library (ChEDL). Utilities for process modeling.
Copyright (C) 2016, 2017, 2018, 2019, 2020 Caleb Bell <Caleb.Andrew.Bell@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Some of the methods implemented here are numerical while others are analytical.


The cubic EOS can be rearranged into the following polynomial form:

.. math::
    0 = Z^3 + (\delta' - B' - 1)Z^2 + [\theta' + \epsilon' - \delta(B'+1)]Z
    - [\epsilon'(B'+1) + \theta'\eta']

.. math::
    B' = \frac{bP}{RT}

.. math::
    \delta' = \frac{\delta P}{RT}

.. math::
    \theta' = \frac{a\alpha P}{(RT)^2}

.. math::
    \epsilon' = \epsilon\left(\frac{P}{RT}\right)^2

The range of pressures, temperatures, and :math:`a \alpha` values is so large
that almost all analytical solutions produce huge errors in some conditions.
Because the EOS volume cannot be under `b`, this often results in a root being
ignored where there should have been a liquid-like root detected.

A number of plots showing the relative error in volume calculation are shown
below to demonstrate how different methods work.

.. contents:: :local:

Analytical Solvers
------------------
.. autofunction:: volume_solutions_Cardano
.. autofunction:: volume_solutions_fast
.. autofunction:: volume_solutions_a1
.. autofunction:: volume_solutions_a2
.. autofunction:: volume_solutions_numpy
.. autofunction:: volume_solutions_ideal

Numerical Solvers
-----------------
.. autofunction:: volume_solutions_halley
.. autofunction:: volume_solutions_NR
.. autofunction:: volume_solutions_NR_low_P

Higher-Precision Solvers
------------------------
.. autofunction:: volume_solutions_mpmath
.. autofunction:: volume_solutions_mpmath_float
.. autofunction:: volume_solutions_sympy

'''

from __future__ import division, print_function
__all__ = ['volume_solutions_mpmath', 'volume_solutions_mpmath_float',
           'volume_solutions_NR', 'volume_solutions_NR_low_P', 'volume_solutions_halley',
           'volume_solutions_fast', 'volume_solutions_Cardano', 'volume_solutions_a1',
           'volume_solutions_a2', 'volume_solutions_numpy', 'volume_solutions_ideal',
           'volume_solutions_doubledouble_float',
           'volume_solution_polish', 'volume_solutions_sympy']


from cmath import sqrt as csqrt

from fluids.numerics import (brenth, third, sixth, roots_cubic, newton,
                             roots_cubic_a1, numpy as np, sqrt,
                             roots_cubic_a2, horner_and_der,
                             deflate_cubic_real_roots)
from fluids.numerics.doubledouble import (add_dd, add_imag_dd, cbrt_imag_dd, div_dd,
                                          div_imag_dd, mul_dd, mul_imag_dd,
                                          mul_noerrors_dd, sqrt_imag_dd, square_dd,
                                          sqrt_dd, cbrt_dd)

from fluids.constants import R, R_inv

def volume_solutions_sympy(T, P, b, delta, epsilon, a_alpha):
    r'''Solution of this form of the cubic EOS in terms of volumes, using the
    `sympy` mathematical library with real numbers. 

    This function is generally slow, and somehow still has more than desired 
    error in the real and complex result.
    
    .. math::
        V_0 = - \frac{- \frac{3 \left(- P b \delta + P \epsilon - R T \delta 
        + a \alpha\right)}{P} + \frac{\left(- P b + P \delta - R T\right)^{2}}
        {P^{2}}}{3 \sqrt[3]{\frac{\sqrt{- 4 \left(- \frac{3 \left(- P b \delta
        + P \epsilon - R T \delta + a \alpha\right)}{P} + \frac{\left(- P b 
        + P \delta - R T\right)^{2}}{P^{2}}\right)^{3} + \left(\frac{27 \left(
        - P b \epsilon - R T \epsilon - a \alpha b\right)}{P} - \frac{9 
        \left(- P b + P \delta - R T\right) \left(- P b \delta + P \epsilon 
        - R T \delta + a \alpha\right)}{P^{2}} + \frac{2 \left(- P b + P \delta
        - R T\right)^{3}}{P^{3}}\right)^{2}}}{2} + \frac{27 \left(- P b \epsilon
        - R T \epsilon - a \alpha b\right)}{2 P} - \frac{9 \left(- P b + P \delta 
        - R T\right) \left(- P b \delta + P \epsilon - R T \delta 
        + a \alpha\right)}{2 P^{2}} + \frac{\left(- P b + P \delta - R T\right)^{3}}
        {P^{3}}}} - \frac{\sqrt[3]{\frac{\sqrt{- 4 \left(- \frac{3 \left(- P b \delta 
        + P \epsilon - R T \delta + a \alpha\right)}{P} + \frac{\left(- P b 
        + P \delta - R T\right)^{2}}{P^{2}}\right)^{3} + \left(\frac{27 \left(
        - P b \epsilon - R T \epsilon - a \alpha b\right)}{P} - \frac{9 \left(
        - P b + P \delta - R T\right) \left(- P b \delta + P \epsilon - R T
        \delta + a \alpha\right)}{P^{2}} + \frac{2 \left(- P b + P \delta - R 
        T\right)^{3}}{P^{3}}\right)^{2}}}{2} + \frac{27 \left(- P b \epsilon 
        - R T \epsilon - a \alpha b\right)}{2 P} - \frac{9 \left(- P b + P 
        \delta - R T\right) \left(- P b \delta + P \epsilon - R T \delta 
        + a \alpha\right)}{2 P^{2}} + \frac{\left(- P b + P \delta - R T
        \right)^{3}}{P^{3}}}}{3} - \frac{- P b + P \delta - R T}{3 P}
                                                  
    .. math::
        V_1 = - \frac{- \frac{3 \left(- P b \delta + P \epsilon - R T \delta
        + a \alpha\right)}{P} + \frac{\left(- P b + P \delta - R T\right)^{2}}
       {P^{2}}}{3 \left(- \frac{1}{2} - \frac{\sqrt{3} i}{2}\right) \sqrt[3]
       {\frac{\sqrt{- 4 \left(- \frac{3 \left(- P b \delta + P \epsilon - R T 
        \delta + a \alpha\right)}{P} + \frac{\left(- P b + P \delta - R T
       \right)^{2}}{P^{2}}\right)^{3} + \left(\frac{27 \left(- P b \epsilon
        - R T \epsilon - a \alpha b\right)}{P} - \frac{9 \left(- P b + P \delta 
        - R T\right) \left(- P b \delta + P \epsilon - R T \delta 
        + a \alpha\right)}{P^{2}} + \frac{2 \left(- P b + P \delta - R T
        \right)^{3}}{P^{3}}\right)^{2}}}{2} + \frac{27 \left(- P b \epsilon 
        - R T \epsilon - a \alpha b\right)}{2 P} - \frac{9 \left(- P b + P
        \delta - R T\right) \left(- P b \delta + P \epsilon - R T \delta 
        + a \alpha\right)}{2 P^{2}} + \frac{\left(- P b + P \delta - R T
        \right)^{3}}{P^{3}}}} - \frac{\left(- \frac{1}{2} - \frac{\sqrt{3} i}
        {2}\right) \sqrt[3]{\frac{\sqrt{- 4 \left(- \frac{3 \left(- P b \delta
        + P \epsilon - R T \delta + a \alpha\right)}{P} + \frac{\left(- P b 
        + P \delta - R T\right)^{2}}{P^{2}}\right)^{3} + \left(\frac{27 \left(
        - P b \epsilon - R T \epsilon - a \alpha b\right)}{P} - \frac{9 \left(
        - P b + P \delta - R T\right) \left(- P b \delta + P \epsilon - R T
        \delta + a \alpha\right)}{P^{2}} + \frac{2 \left(- P b + P \delta 
        - R T\right)^{3}}{P^{3}}\right)^{2}}}{2} + \frac{27 \left(- P b \epsilon
        - R T \epsilon - a \alpha b\right)}{2 P} - \frac{9 \left(- P b
        + P \delta - R T\right) \left(- P b \delta + P \epsilon - R T \delta 
         + a \alpha\right)}{2 P^{2}} + \frac{\left(- P b + P \delta - R T
        \right)^{3}}{P^{3}}}}{3} - \frac{- P b + P \delta - R T}{3 P}
                                                   
    .. math::
        V_2 = - \frac{- \frac{3 \left(- P b \delta + P \epsilon - R T \delta 
        + a \alpha\right)}{P} + \frac{\left(- P b + P \delta - R T\right)^{2}}
        {P^{2}}}{3 \left(- \frac{1}{2} + \frac{\sqrt{3} i}{2}\right) \sqrt[3]
        {\frac{\sqrt{- 4 \left(- \frac{3 \left(- P b \delta + P \epsilon - R T 
        \delta + a \alpha\right)}{P} + \frac{\left(- P b + P \delta - R T
        \right)^{2}}{P^{2}}\right)^{3} + \left(\frac{27 \left(- P b \epsilon
        - R T \epsilon - a \alpha b\right)}{P} - \frac{9 \left(- P b + P \delta
        - R T\right) \left(- P b \delta + P \epsilon - R T \delta + a \alpha
        \right)}{P^{2}} + \frac{2 \left(- P b + P \delta - R T\right)^{3}}
        {P^{3}}\right)^{2}}}{2} + \frac{27 \left(- P b \epsilon - R T \epsilon 
        - a \alpha b\right)}{2 P} - \frac{9 \left(- P b + P \delta - R T\right)
        \left(- P b \delta + P \epsilon - R T \delta + a \alpha\right)}{2 P^{2}}
        + \frac{\left(- P b + P \delta - R T\right)^{3}}{P^{3}}}} - \frac{\left(
        - \frac{1}{2} + \frac{\sqrt{3} i}{2}\right) \sqrt[3]{\frac{\sqrt{- 4
        \left(- \frac{3 \left(- P b \delta + P \epsilon - R T \delta + a \alpha
        \right)}{P} + \frac{\left(- P b + P \delta - R T\right)^{2}}{P^{2}}
        \right)^{3} + \left(\frac{27 \left(- P b \epsilon - R T \epsilon
        - a \alpha b\right)}{P} - \frac{9 \left(- P b + P \delta - R T\right)
        \left(- P b \delta + P \epsilon - R T \delta + a \alpha\right)}{P^{2}}
        + \frac{2 \left(- P b + P \delta - R T\right)^{3}}{P^{3}}\right)^{2}}}
        {2} + \frac{27 \left(- P b \epsilon - R T \epsilon - a \alpha b\right)}
        {2 P} - \frac{9 \left(- P b + P \delta - R T\right) \left(- P b \delta 
        + P \epsilon - R T \delta + a \alpha\right)}{2 P^{2}} + \frac{\left(
        - P b + P \delta - R T\right)^{3}}{P^{3}}}}{3} - \frac{- P b + P 
        \delta - R T}{3 P}

    Parameters
    ----------
    T : float
        Temperature, [K]
    P : float
        Pressure, [Pa]
    b : float
        Coefficient calculated by EOS-specific method, [m^3/mol]
    delta : float
        Coefficient calculated by EOS-specific method, [m^3/mol]
    epsilon : float
        Coefficient calculated by EOS-specific method, [m^6/mol^2]
    a_alpha : float
        Coefficient calculated by EOS-specific method, [J^2/mol^2/Pa]

    Returns
    -------
    Vs : tuple[sympy.Rational]
        Three possible molar volumes, [m^3/mol]

    Notes
    -----
    The solution can be derived as follows:
        
    >>> from sympy import * # doctest: +SKIP
    >>> P, T, V, R, b, delta, epsilon = symbols('P, T, V, R, b, delta, epsilon') # doctest: +SKIP
    >>> a_alpha = Symbol(r'a \alpha') # doctest: +SKIP
    >>> CUBIC = R*T/(V-b) - a_alpha/(V*V + delta*V + epsilon) - P # doctest: +SKIP
    >>> V_slns = solve(CUBIC, V) # doctest: +SKIP


    Examples
    --------
    >>> Vs = volume_solutions_sympy(0.01, 1e-05, 2.5405184201558786e-05, 5.081036840311757e-05, -6.454233843151321e-10, 0.3872747173781095) # doctest: +SKIP
    >>> [complex(v) for v in Vs] # doctest: +SKIP
    [(2.540546e-05+2.402202278e-12j), (4.660380256-2.40354958e-12j), (8309.80218+1.348096981e-15j)]
    
    References
    ----------
    .. [1] Meurer, Aaron, Christopher P. Smith, Mateusz Paprocki, Ondřej 
       Čertík, Sergey B. Kirpichev, Matthew Rocklin, AMiT Kumar, Sergiu Ivanov,
       Jason K. Moore, and Sartaj Singh. "SymPy: Symbolic Computing in Python."
       PeerJ Computer Science 3 (2017): e103.
    '''
    if P == 0.0 or T == 0.0:
        raise ValueError("Bad P or T; issue is not the algorithm")
    from sympy import sqrt, Rational, I
    if isinstance(T, float):
        T = Rational(T)
    if isinstance(P, float):
        P = Rational(P)
    if isinstance(b, float):
        b = Rational(b)
    if isinstance(delta, float):
        delta = Rational(delta)
    if isinstance(epsilon, float):
        epsilon = Rational(epsilon)
    if isinstance(a_alpha, float):
        a_alpha = Rational(a_alpha)

    R_sym = Rational(R)
    x0 = 1/P
    x1 = P*b
    x2 = R_sym*T
    x3 = P*delta
    x4 = x1 + x2 - x3
    x5 = x0*x4
    x6 = a_alpha*b
    x7 = epsilon*x1
    x8 = epsilon*x2
    x9 = P**(-2)
    x10 = P*epsilon
    x11 = b*x3
    x12 = delta*x2
    x13 = 3*a_alpha
    x14 = 3*x10
    x15 = 3*x11
    x16 = 3*x12
    x17 = -x1 - x2 + x3
    x18 = x0*x17**2
    x19 = 4*x0
    x20 = (-27*x0*(x6 + x7 + x8)/2 - 9*x4*x9*(-a_alpha - x10 + x11 + x12)/2
           + sqrt(x9*(-x19*(-x13 - x14 + x15 + x16 + x18)**3
           + (-9*x0*x17*(a_alpha + x10 - x11 - x12) + 2*x17**3*x9 - 27*x6
           - 27*x7 - 27*x8)**2))/2 - x4**3/P**3)**(1/3)
    x21 = (x13 + x14 - x15 - x16 - x18)/x20
    x22 = 2*x5
    x23 = sqrt(3)*I
    x24 = x23 + 1
    x25 = x19*x21
    x26 = 1 - x23
    return (x0*x21/3 - x20/3 + x5/3, 
            x20*x24/6 + x22/6 - x25/(6*x24), 
            x20*x26/6 + x22/6 - x25/(6*x26))

def volume_solutions_mpmath(T, P, b, delta, epsilon, a_alpha, dps=50):
    r'''Solution of this form of the cubic EOS in terms of volumes, using the
    `mpmath` arbitrary precision library. The number of decimal places returned
    is controlled by the `dps` parameter.


    This function is the reference implementation which provides exactly
    correct solutions; other algorithms are compared against this one.

    Parameters
    ----------
    T : float
        Temperature, [K]
    P : float
        Pressure, [Pa]
    b : float
        Coefficient calculated by EOS-specific method, [m^3/mol]
    delta : float
        Coefficient calculated by EOS-specific method, [m^3/mol]
    epsilon : float
        Coefficient calculated by EOS-specific method, [m^6/mol^2]
    a_alpha : float
        Coefficient calculated by EOS-specific method, [J^2/mol^2/Pa]
    dps : int
        Number of decimal places in the result by `mpmath`, [-]

    Returns
    -------
    Vs : tuple[complex]
        Three possible molar volumes, [m^3/mol]

    Notes
    -----
    Although `mpmath` has a cubic solver, it has been found to fail to solve in
    some cases. Accordingly, the algorithm is as follows:

    Working precision is `dps` plus 40 digits; and if P < 1e-10 Pa, it is
    `dps` plus 400 digits. The input parameters are converted exactly to `mpf`
    objects on input.

    `polyroots` from mpmath is used with `maxsteps=2000`, and extra precision
    of 15 digits. If the solution does not converge, 20 extra digits are added
    up to 8 times. If no solution is found, mpmath's `findroot` is called on
    the pressure error function using three initial guesses from another solver.

    Needless to say, this function is quite slow.

    Examples
    --------
    Test case which presented issues for PR EOS (three roots were not being returned):

    >>> volume_solutions_mpmath(0.01, 1e-05, 2.5405184201558786e-05, 5.081036840311757e-05, -6.454233843151321e-10, 0.3872747173781095)
    (mpf('0.0000254054613415548712260258773060137'), mpf('4.66038025602155259976574392093252'), mpf('8309.80218708657190094424659859346'))

    References
    ----------
    .. [1] Johansson, Fredrik. Mpmath: A Python Library for Arbitrary-Precision
       Floating-Point Arithmetic, 2010.
    '''
    # Tried to remove some green on physical TV with more than 30, could not
    # 30 is fine, but do not dercease further!
    # No matter the precision, still cannot get better
    # Need to switch from `rindroot` to an actual cubic solution in mpmath
    # Three roots not found in some cases
    # PRMIX(T=1e-2, P=1e-5, Tcs=[126.1, 190.6], Pcs=[33.94E5, 46.04E5], omegas=[0.04, 0.011], zs=[0.5, 0.5], kijs=[[0,0],[0,0]]).volume_error()
    # Once found it possible to compute VLE down to 0.03 Tc with ~400 steps and ~500 dps.
    # need to start with a really high dps to get convergence or it is discontinuous
    if P == 0.0 or T == 0.0:
        raise ValueError("Bad P or T; issue is not the algorithm")

    import mpmath as mp
    mp.mp.dps = dps + 40#400#400
    if P < 1e-10:
        mp.mp.dps = dps + 400
    b, T, P, epsilon, delta, a_alpha = [mp.mpf(i) for i in [b, T, P, epsilon, delta, a_alpha]]
    roots = None
    if 1:
        RT_inv = 1/(mp.mpf(R)*T)
        P_RT_inv = P*RT_inv
        B = etas = b*P_RT_inv
        deltas = delta*P_RT_inv
        thetas = a_alpha*P_RT_inv*RT_inv
        epsilons = epsilon*P_RT_inv*P_RT_inv

        b = (deltas - B - 1)
        c = (thetas + epsilons - deltas*(B + 1))
        d = -(epsilons*(B + 1) + thetas*etas)

        extraprec = 15
        # extraprec alone is not enough to converge everything
        try:
            # found case 20 extrapec not enough, increased to 30
            # Found another case needing 40
            for i in range(8):
                try:
                    # Found 1 case 100 steps not enough needed 200; then found place 400 was not enough
                    roots = mp.polyroots([mp.mpf(1.0), b, c, d], extraprec=extraprec, maxsteps=2000)
                    break
                except Exception as e:
                    extraprec += 20
#                        print(e, extraprec)
                    if i == 7:
#                            print(e, 'failed')
                        raise e

            if all(i == 0 or i == 1 for i in roots):
                return volume_solutions_mpmath(T, P, b, delta, epsilon, a_alpha, dps=dps*2)
        except:
            try:
                guesses = volume_solutions_fast(T, P, b, delta, epsilon, a_alpha)
                roots = mp.polyroots([mp.mpf(1.0), b, c, d], extraprec=40, maxsteps=100, roots_init=guesses)
            except:
                pass
#            roots = np.roots([1.0, b, c, d]).tolist()
        if roots is not None:
            RT_P = mp.mpf(R)*T/P
            hits = [V*RT_P for V in roots]

    if roots is None:
#        print('trying numerical mpmath')
        guesses = volume_solutions_fast(T, P, b, delta, epsilon, a_alpha)
        RT = T*R
        def err(V):
            return(RT/(V-b) - a_alpha/(V*(V + delta) + epsilon)) - P

        hits = []
        for Vi in guesses:
            try:
                V_calc = mp.findroot(err, Vi, solver='newton')
                hits.append(V_calc)
            except Exception as e:
                pass
        if not hits:
            raise ValueError("Could not converge any mpmath volumes")
    # Return in the specified precision
    mp.mp.dps = dps
    sort_fun = lambda x: (x.real, x.imag)
    return tuple(sorted(hits, key=sort_fun))

def volume_solutions_mpmath_float(T, P, b, delta, epsilon, a_alpha):
    r'''Simple wrapper around :obj:`volume_solutions_mpmath` which uses the
    default parameters and returns the values as floats.

    Parameters
    ----------
    T : float
        Temperature, [K]
    P : float
        Pressure, [Pa]
    b : float
        Coefficient calculated by EOS-specific method, [m^3/mol]
    delta : float
        Coefficient calculated by EOS-specific method, [m^3/mol]
    epsilon : float
        Coefficient calculated by EOS-specific method, [m^6/mol^2]
    a_alpha : float
        Coefficient calculated by EOS-specific method, [J^2/mol^2/Pa]
    dps : int
        Number of decimal places in the result by `mpmath`, [-]

    Returns
    -------
    Vs : tuple[complex]
        Three possible molar volumes, [m^3/mol]

    Notes
    -----

    Examples
    --------
    Test case which presented issues for PR EOS (three roots were not being returned):

    >>> volume_solutions_mpmath_float(0.01, 1e-05, 2.5405184201558786e-05, 5.081036840311757e-05, -6.454233843151321e-10, 0.3872747173781095)
    ((2.540546134155487e-05+0j), (4.660380256021552+0j), (8309.802187086572+0j))
    '''
    Vs = volume_solutions_mpmath(T, P, b, delta, epsilon, a_alpha)
    return tuple(float(Vi.real) + float(Vi.imag)*1.0j for Vi in Vs)

def volume_solutions_NR(T, P, b, delta, epsilon, a_alpha, tries=0):
    r'''Newton-Raphson based solver for cubic EOS volumes based on the idea
    of initializing from an analytical solver. This algorithm can only be
    described as a monstrous mess. It is fairly fast for most cases, but about
    3x slower than :obj:`volume_solutions_halley`. In the worst case this
    will fall back to `mpmath`.

    Parameters
    ----------
    T : float
        Temperature, [K]
    P : float
        Pressure, [Pa]
    b : float
        Coefficient calculated by EOS-specific method, [m^3/mol]
    delta : float
        Coefficient calculated by EOS-specific method, [m^3/mol]
    epsilon : float
        Coefficient calculated by EOS-specific method, [m^6/mol^2]
    a_alpha : float
        Coefficient calculated by EOS-specific method, [J^2/mol^2/Pa]
    tries : int, optional
        Internal parameter as this function will call itself if it needs to;
        number of previous solve attempts, [-]

    Returns
    -------
    Vs : tuple[complex]
        Three possible molar volumes, [m^3/mol]

    Notes
    -----

    Sample regions where this method works perfectly are shown below:

    .. figure:: eos/volume_error_NR_PR_methanol_high.png
       :scale: 70 %
       :alt: PR EOS methanol volume error high pressure

    .. figure:: eos/volume_error_NR_PR_methanol_low.png
       :scale: 70 %
       :alt: PR EOS methanol volume error low pressure

    '''


    '''Even if mpmath is used for greater precision in the calculated root,
    it gets rounded back to a float - and then error occurs.
    Cannot beat numerical method or numpy roots!

    The only way out is to keep volume as many decimals, to pass back in
    to initialize the TV state.
    '''
    # Initial calculation - could use any method, however this is fastest
    # 2 divisions, 2 powers in here
    # First bit is top left corner
    if a_alpha == 0.0:
        '''from sympy import *
            R, T, P, b, V = symbols('R, T, P, b, V')
            solve(Eq(P, R*T/(V-b)), V)
        '''
        # EOS has devolved into having the first term solution only
        return [b + R*T/P, -1j, -1j]
    if P < 1e-2:
    # if 0 or (0 and ((T < 1e-2 and P > 1e6) or (P < 1e-3 and T < 1e-2) or (P < 1e-1 and T < 1e-4) or P < 1)):
        # Not perfect but so much wasted dev time need to move on, try other fluids and move this tolerance up if needed
        # if P < min(GCEOS.P_discriminant_zeros_analytical(T=T, b=b, delta=delta, epsilon=epsilon, a_alpha=a_alpha, valid=True)):
            # TODO - need function that returns range two solutions are available!
            # Very important because the below strategy only works for that regime.
        if T > 1e-2 or 1:
            try:
                return volume_solutions_NR_low_P(T, P, b, delta, epsilon, a_alpha)
            except Exception as e:
                pass
#                print(e, 'was not 2 phase')

        try:
            return volume_solutions_mpmath_float(T, P, b, delta, epsilon, a_alpha)
        except:
            pass
    try:
        if tries == 0:
            Vs = list(volume_solutions_Cardano(T, P, b, delta, epsilon, a_alpha))
#                Vs = [Vi+1e-45j for Vi in volume_solutions_Cardano(T, P, b, delta, epsilon, a_alpha, quick=True)]
        elif tries == 1:
            Vs = list(volume_solutions_fast(T, P, b, delta, epsilon, a_alpha))
        elif tries == 2:
            # sometimes used successfully
            Vs = list(volume_solutions_a1(T, P, b, delta, epsilon, a_alpha))
        # elif tries == 3:
        #     # never used successfully
        #     Vs = GCEOS.volume_solutions_a2(T, P, b, delta, epsilon, a_alpha)

        # TODO fall back to tlow T
    except:
#            Vs = GCEOS.volume_solutions_Cardano(T, P, b, delta, epsilon, a_alpha)
        if tries == 0:
            Vs = list(volume_solutions_fast(T, P, b, delta, epsilon, a_alpha))
        else:
            Vs = list(volume_solutions_Cardano(T, P, b, delta, epsilon, a_alpha))
        # Zero division error is possible above

    RT = R*T
    P_inv = 1.0/P
#        maxiter = range(3)
    # The case for a fixed number of iterations has pretty much gone.
    # On 1 occasion
    failed = False
    max_err, rel_err = 0.0, 0.0
    try:
        for i in (0, 1, 2):
            V = Vi = Vs[i]
            err = 0.0
            for _ in range(11):
                # More iterations seems to create problems. No, 11 is just lucky for particular problem.
#            for _ in (0, 1, 2):
                # 3 divisions each iter = 15, triple the duration of the solve
                denom1 = 1.0/(V*(V + delta) + epsilon)
                denom0 = 1.0/(V-b)
                w0 = RT*denom0
                w1 = a_alpha*denom1
                if w0 - w1 - P == err:
                    break # No change in error
                err = w0 - w1 - P
#                print(abs(err), V, _)
                derr_dV = (V + V + delta)*w1*denom1 - w0*denom0
                V = V - err/derr_dV
                rel_err = abs(err*P_inv)
                if rel_err < 1e-14 or V == Vi:
                    # Conditional check probably not worth it
                    break
#                if _ > 5:
#                    print(_, V)
            # This check can get rid of the noise
            if rel_err > 1e-2: # originally 1e-2; 1e-5 did not change; 1e-10 to far
#            if abs(err*P_inv) > 1e-2 and (i.real != 0.0 and abs(i.imag/i.real) < 1E-10 ):
                failed = True
#                    break
            if not (.95 < (Vi/V).real < 1.05):
                # Cannot let a root become another root
                failed = True
                max_err = 1e100
                break
            Vs[i] = V
            max_err = max(max_err, rel_err)
    except:
        failed = True

#            def to_sln(V):
#                denom1 = 1.0/(V*(V + delta) + epsilon)
#                denom0 = 1.0/(V-b)
#                w0 = x2*denom0
#                w1 = a_alpha*denom1
#                err = w0 - w1 - P
##                print(err*P_inv, V)
#                return err#*P_inv
#            try:
#                from fluids.numerics import py_bisect as bisect, secant, linspace
##                Vs[i] = secant(to_sln, Vs[i].real, x1=Vs[i].real*1.0001, ytol=1e-12, damping=.6)
#                import matplotlib.pyplot as plt
#
#                plt.figure()
#                xs = linspace(Vs[i].real*.9999999999, Vs[i].real*1.0000000001, 2000000) + [Vs[i]]
#                ys = [abs(to_sln(V)) for V in xs]
#                plt.semilogy(xs, ys)
#                plt.show()
#
##                Vs[i] = bisect(to_sln, Vs[i].real*.999, Vs[i].real*1.001)
#            except Exception as e:
#                print(e)
    root_failed = not [i.real for i in Vs if i.real > b and (i.real == 0.0 or abs(i.imag/i.real) < 1E-12)]
    if not failed:
        failed = root_failed

    if failed and tries < 2:
        return volume_solutions_NR(T, P, b, delta, epsilon, a_alpha, tries=tries+1)
    elif root_failed:
#            print('%g, %g; ' %(T, P), end='')
        return volume_solutions_mpmath_float(T, P, b, delta, epsilon, a_alpha)
    elif failed and tries == 2:
        # Are we at least consistent? Diitch the NR and try to be OK with the answer
#            Vs0 = GCEOS.volume_solutions_Cardano(T, P, b, delta, epsilon, a_alpha, quick=True)
#            Vs1 = GCEOS.volume_solutions_a1(T, P, b, delta, epsilon, a_alpha, quick=True)
#            if sum(abs((i -j)/i) for i, j in zip(Vs0, Vs1)) < 1e-6:
#                return Vs0
        if max_err < 5e3:
        # if max_err < 1e6:
            # Try to catch floating point error
            return Vs
        return volume_solutions_NR_low_P(T, P, b, delta, epsilon, a_alpha)
#        print('%g, %g; ' %(T, P), end='')
#            print(T, P, b, delta, a_alpha)
#            if root_failed:
        return volume_solutions_mpmath_float(T, P, b, delta, epsilon, a_alpha)
        # return Vs
#        if tries == 3 or tries == 2:
#            print(tries)
    return Vs

def volume_solutions_NR_low_P(T, P, b, delta, epsilon, a_alpha):
    r'''Newton-Raphson based solver for cubic EOS volumes designed specifically
    for the low-pressure regime. Seeks only two possible solutions - an ideal
    gas like one, and one near the eos covolume `b` - as the initializations are
    `R*T/P` and `b*1.000001` .

    Parameters
    ----------
    T : float
        Temperature, [K]
    P : float
        Pressure, [Pa]
    b : float
        Coefficient calculated by EOS-specific method, [m^3/mol]
    delta : float
        Coefficient calculated by EOS-specific method, [m^3/mol]
    epsilon : float
        Coefficient calculated by EOS-specific method, [m^6/mol^2]
    a_alpha : float
        Coefficient calculated by EOS-specific method, [J^2/mol^2/Pa]
    tries : int, optional
        Internal parameter as this function will call itself if it needs to;
        number of previous solve attempts, [-]

    Returns
    -------
    Vs : tuple[complex]
        Three possible molar volumes (third one is hardcoded to 1j), [m^3/mol]

    Notes
    -----
    The algorithm is NR, with some checks that will switch the solver to
    `brenth` some of the time.
    '''

    P_inv = 1.0/P
    def err_fun(V):
        denom1 = 1.0/(V*(V + delta) + epsilon)
        denom0 = 1.0/(V-b)
        w0 = R*T*denom0
        w1 = a_alpha*denom1
        err = w0 - w1 - P
        return err

#        failed = False
    Vs = [R*T/P, b*1.000001]
    max_err, rel_err = 0.0, 0.0
    for i, damping in zip((0, 1), (1.0, 1.0)):
        V = Vi = Vs[i]
        err = 0.0
        for _ in range(31):
            denom1 = 1.0/(V*(V + delta) + epsilon)
            denom0 = 1.0/(V-b)
            w0 = R*T*denom0
            w1 = a_alpha*denom1
            if w0 - w1 - P == err:
                break # No change in error
            err = w0 - w1 - P
            derr_dV = (V + V + delta)*w1*denom1 - w0*denom0
            if derr_dV != 0.0:
                V = V - err/derr_dV*damping
            rel_err = abs(err*P_inv)
            if rel_err < 1e-14 or V == Vi:
                # Conditional check probably not worth it
                break
        if i == 1 and V > 1.5*b or V < b:
            # try:
                # try:
            try:
                try:
                    V = brenth(err_fun, b*(1.0+1e-12), b*(1.5), xtol=1e-14)
                except Exception as e:
                    if a_alpha < 1e-5:
                        V = brenth(err_fun, b*1.5, b*5.0, xtol=1e-14)
                    else:
                        raise e

                denom1 = 1.0/(V*(V + delta) + epsilon)
                denom0 = 1.0/(V-b)
                w0 = R*T*denom0
                w1 = a_alpha*denom1
                err = w0 - w1 - P
                derr_dV = (V + V + delta)*w1*denom1 - w0*denom0
                V_1NR = V - err/derr_dV*damping
                if abs((V_1NR-V)/V) < 1e-10:
                    V = V_1NR

            except:
                V = 1j
        if i == 0 and rel_err > 1e-8:
            V = 1j
#                    failed = True
                # except:
                #     V = brenth(err_fun, b*(1.0+1e-12), b*(1.5))
            # except:
            #     pass
                # print([T, P, 'fail on brenth low P root'])
        Vs[i] = V
#            max_err = max(max_err, rel_err)
    Vs.append(1j)
#        if failed:
    return Vs

def volume_solution_polish(V, T, P, b, delta, epsilon, a_alpha):
    RT = R*T
    RT_2 = RT + RT
    a_alpha_2 = a_alpha + a_alpha
    P_inv = 1.0/P

    fval_oldold = 1.0
    fval_old = 0.0
    for j in range(50):
#             print(j, V)
        x0_inv = 1.0/(V - b)
        x1_inv_den = (V*(V + delta) + epsilon)
        if x1_inv_den == 0.0:
            break
        x1_inv = 1.0/x1_inv_den
        x2 = V + V + delta
        fval = RT*x0_inv - P - a_alpha*x1_inv
        x0_inv2 = x0_inv*x0_inv # make it 1/x0^2
        x1_inv2 = x1_inv*x1_inv # make it 1/x1^2
        x3 = a_alpha*x1_inv2
        fder = x2*x3 - RT*x0_inv2
        fder2 = RT_2*x0_inv2*x0_inv - a_alpha_2*x2*x2*x1_inv2*x1_inv + x3 + x3
        if fder == 0.0:
            break
        fder_inv = 1.0/fder
        step = fval*fder_inv
        rel_err = abs(fval*P_inv)
#                print(fval, rel_err, step, j, i, V)
        step_den = 1.0 - 0.5*step*fder2*fder_inv
        if step_den == 0.0:
#                    if fval == 0.0:
#                        break # got a perfect answer
            continue
        V = V - step/step_den

        if (rel_err < 3e-15 or fval_old == fval or fval == fval_oldold
            or (j > 10 and rel_err < 1e-12)):
            # Conditional check probably not worth it
            break
        fval_oldold, fval_old = fval_old, fval
    return V


def volume_solutions_halley(T, P, b, delta, epsilon, a_alpha):
    r'''Halley's method based solver for cubic EOS volumes based on the idea
    of initializing from a single liquid-like guess which is solved precisely,
    deflating the cubic analytically, solving the quadratic equation for the
    next two volumes, and then performing two halley steps on each of them
    to obtain the final solutions. This method does not calculate imaginary
    roots - they are set to zero on detection. This method has been rigorously
    tested over a wide range of conditions.
    
    The method uses the standard combination of bisection to provide high
    and low boundaries as well, to keep the iteration always moving forward.

    Parameters
    ----------
    T : float
        Temperature, [K]
    P : float
        Pressure, [Pa]
    b : float
        Coefficient calculated by EOS-specific method, [m^3/mol]
    delta : float
        Coefficient calculated by EOS-specific method, [m^3/mol]
    epsilon : float
        Coefficient calculated by EOS-specific method, [m^6/mol^2]
    a_alpha : float
        Coefficient calculated by EOS-specific method, [J^2/mol^2/Pa]

    Returns
    -------
    Vs : tuple[float]
        Three possible molar volumes, [m^3/mol]

    Notes
    -----
    A sample region where this method works perfectly is shown below:

    .. figure:: eos/volume_error_halley_PR_methanol_low.png
       :scale: 70 %
       :alt: PR EOS methanol volume error low pressure

    '''
    '''
    Cases known to be failing:
    '''
    # Test the case where a_alpha is so low, even with the lowest possible volume `b`,
    # the value of the second term plus P is equal to P.
    if a_alpha/(b*(b + delta) + epsilon) + P == P:
        return (b + R*T/P, 0.0, 0.0)
    # Run this first, before the low P criteria
    if a_alpha > 1e4:
        V_possible = high_alpha_one_root(T, P, b, delta, epsilon, a_alpha)
        if V_possible != 0.0:
            return (V_possible, 0.0, 0.0)
    RT = R*T
    RT_2 = RT + RT
    a_alpha_2 = a_alpha + a_alpha
    P_inv = 1.0/P

    RT_inv = R_inv/T
    P_RT_inv = P*RT_inv
    B = etas = b*P_RT_inv
    deltas = delta*P_RT_inv
    thetas = a_alpha*P_RT_inv*RT_inv
    epsilons = epsilon*P_RT_inv*P_RT_inv

    b2 = (deltas - B - 1.0)
    c2 = (thetas + epsilons - deltas*(B + 1.0))
    d2 = -(epsilons*(B + 1.0) + thetas*etas)
    RT_P = RT*P_inv
    
    low_V, high_V = b*(1.0+8e-16), -RT_P*d2/c2
    if high_V <= low_V:
        high_V = b*1.000001

    V = high_V
    for j in range(50):
        x0_inv = 1.0/(V - b)
        x1_inv = 1.0/(V*(V + delta) + epsilon)
        x2 = V + V + delta
        fval = RT*x0_inv - P - a_alpha*x1_inv
        if fval < 0.0:
            high_V = V
        else:
            low_V = V
            if j == 0:
                # If we are in the first iteration we have not decided on a upper bound yet
                high_V = RT_P*10.0
                # If the ideal gas volume is in danger of being underneath the liquid volume
                # we increase it to 10b. 10 is a guess only.
                if high_V < 10.0*b:
                    high_V = 10.0*b
        x0_inv2 = x0_inv*x0_inv # make it 1/x0^2
        x1_inv2 = x1_inv*x1_inv # make it 1/x1^2
        x3 = a_alpha*x1_inv2
        fder = x2*x3 - RT*x0_inv2
        fder2 = RT_2*x0_inv2*x0_inv - a_alpha_2*x2*x2*x1_inv2*x1_inv + x3 + x3

        fder_inv = 1.0/fder
        step = fval*fder_inv
        rel_err = abs(fval*P_inv)
        step_den = 1.0 - 0.5*step*fder2*fder_inv
        if step_den != 0.0:
            # Halley's step; if step_den == 0 we do the newton step
            step = step/step_den
        V_old = V
        V_new = V - step
        # print(V, abs(1.0 - V_new/V_old), rel_err)
        if (abs(1.0 - V_new/V_old) < 6e-16
            or (j > 25 and rel_err < 1e-12)
           ):
            # One case not taken care of is oscillating behavior within the boundaries of high_V, low_V
            V = V_new
            break
        if V_new <= low_V or V_new >= high_V:
            V_new = 0.5*(low_V + high_V)
            if V_new == low_V or V_new == high_V:
                # If the bisection has finished (interval cannot be further divided)
                # the solver is finished
                break
        V = V_new
    if j != 49:
        V0 = V

        x1, x2 = deflate_cubic_real_roots(b2, c2, d2, V*P_RT_inv)
        if x1 == 0.0:
            return (V0, 0.0, 0.0)

        # If the molar volume converged on is such that the second term can be added to the
        # first term and it is still the first term, we are *extremely* ideal
        # and we should just quit
        main0 = R*T/(V - b)
        main1 = a_alpha/(V*V + delta*V + epsilon)
        # In these checks, atetmpt to evaluate if we are highly ideal
        # and there is only one solution
        if (main0 + main1 == main0) or ((main0 - main1) != 0.0 and abs(1.0-(main0 + main1)/(main0 - main1)) < 1e-12):
            return (V0, 0.0, 0.0)


        # 8 divisions only for polishing
        V1 = x1*RT_P
        V2 = x2*RT_P
#             print(V1, V2, 'deflated Vs')

        # Fixed a lot of really bad points in the plots with these.
        # Article suggests they are not needed, but 1 is better than 11 iterations!
        # These loops do need to be converted into a tight conditional functional test
        if P < 1e-2:
            if x1 != 1.0:
                # we are so ideal, and we already have the liquid root - and the newton iteration overflows!
                # so we don't need to polish it if x1 is exatly 1.
                V1 = volume_solution_polish(V1, T, P, b, delta, epsilon, a_alpha)
            V2 = volume_solution_polish(V2, T, P, b, delta, epsilon, a_alpha)
        else:
            V = V1
            t90 = V*(V + delta) + epsilon
            if t90 != 0.0:
                x0_inv = 1.0/(V - b)
                x1_inv = 1.0/t90
                x2 = V + V + delta
                fval = -P + RT*x0_inv - a_alpha*x1_inv
                x0_inv2 = x0_inv*x0_inv # make it 1/x0^2
                x1_inv2 = x1_inv*x1_inv # make it 1/x1^2
                x3 = a_alpha*x1_inv2
                fder = x2*x3 - RT*x0_inv2
                fder2 = RT_2*x0_inv2*x0_inv - a_alpha_2*x2*x2*x1_inv2*x1_inv + x3 + x3

                if fder != 0.0:
                    fder_inv = 1.0/fder
                    step = fval*fder_inv
                    V1 = V - step/(1.0 - 0.5*step*fder2*fder_inv)

            # Take a step with V2
            V = V2
            t90 = V*(V + delta) + epsilon
            if t90 != 0.0:
                x0_inv = 1.0/(V - b)
                x1_inv = 1.0/(t90)
                x2 = V + V + delta
                fval = -P + RT*x0_inv - a_alpha*x1_inv
                x0_inv2 = x0_inv*x0_inv # make it 1/x0^2
                x1_inv2 = x1_inv*x1_inv # make it 1/x1^2
                x3 = a_alpha*x1_inv2
                fder = x2*x3 - RT*x0_inv2
                fder2 = RT_2*x0_inv2*x0_inv - a_alpha_2*x2*x2*x1_inv2*x1_inv + x3 + x3

                if fder != 0.0:
                    fder_inv = 1.0/fder
                    step = fval*fder_inv
                    V2 = V - step/(1.0 - 0.5*step*fder2*fder_inv)
        return (V0, V1, V2)
    return (0.0, 0.0, 0.0)

def volume_solutions_fast(T, P, b, delta, epsilon, a_alpha):
    r'''Solution of this form of the cubic EOS in terms of volumes. Returns
    three values, all with some complex part. This is believed to be the
    fastest analytical formula, and while it does not suffer from the same
    errors as Cardano's formula, it has plenty of its own numerical issues.

    Parameters
    ----------
    T : float
        Temperature, [K]
    P : float
        Pressure, [Pa]
    b : float
        Coefficient calculated by EOS-specific method, [m^3/mol]
    delta : float
        Coefficient calculated by EOS-specific method, [m^3/mol]
    epsilon : float
        Coefficient calculated by EOS-specific method, [m^6/mol^2]
    a_alpha : float
        Coefficient calculated by EOS-specific method, [J^2/mol^2/Pa]

    Returns
    -------
    Vs : tuple[complex]
        Three possible molar volumes, [m^3/mol]

    Notes
    -----
    Using explicit formulas, as can be derived in the following example,
    is faster than most numeric root finding techniques, and
    finds all values explicitly. It takes several seconds.

    >>> from sympy import *
    >>> P, T, V, R, b, a, delta, epsilon, alpha = symbols('P, T, V, R, b, a, delta, epsilon, alpha')
    >>> Tc, Pc, omega = symbols('Tc, Pc, omega')
    >>> CUBIC = R*T/(V-b) - a*alpha/(V*V + delta*V + epsilon) - P
    >>> #solve(CUBIC, V)

    A sample region where this method does not obtain the correct solution
    (PR EOS for methanol) is as follows:

    .. figure:: eos/volume_error_sympy_PR_methanol_high.png
       :scale: 70 %
       :alt: PR EOS methanol volume error high pressure

    References
    ----------
    .. [1] Zhi, Yun, and Huen Lee. "Fallibility of Analytic Roots of Cubic
       Equations of State in Low Temperature Region." Fluid Phase
       Equilibria 201, no. 2 (September 30, 2002): 287-94.
       https://doi.org/10.1016/S0378-3812(02)00072-9.
    '''
    x24 = 1.73205080756887729352744634151j + 1.
    x24_inv = 0.25 - 0.433012701892219323381861585376j
    x26 = -1.73205080756887729352744634151j + 1.
    x26_inv = 0.25 + 0.433012701892219323381861585376j
    # Changing over to the inverse constants changes some dew point results
#        if quick:
    x0 = 1./P
    x1 = P*b
    x2 = R*T
    x3 = P*delta
    x4 = x1 + x2 - x3
    x5 = x0*x4
    x6 = a_alpha*b
    x7 = epsilon*x1
    x8 = epsilon*x2
    x9 = x0*x0
    x10 = P*epsilon
    x11 = delta*x1
    x12 = delta*x2
#            x13 = 3.*a_alpha
#            x14 = 3.*x10
#            x15 = 3.*x11
#            x16 = 3.*x12
    x17 = -x4
    x17_2 = x17*x17
    x18 = x0*x17_2
    tm1 = x12 - a_alpha + (x11  - x10)
#            print(x11, x12, a_alpha, x10)
    t0 = x6 + x7 + x8
    t1 = (3.0*tm1  + x18) # custom vars
#            t1 = (-x13 - x14 + x15 + x16 + x18) # custom vars
    t2 = (9.*x0*x17*tm1 + 2.0*x17_2*x17*x9
             - 27.*t0)

    x4x9  = x4*x9
    x19 = ((-13.5*x0*t0 - 4.5*x4x9*tm1
           - x4*x4x9*x5
            + 0.5*csqrt((x9*(-4.*x0*t1*t1*t1 + t2*t2))+0.0j)
            )+0.0j)**third

    x20 = -t1/x19#
    x22 = x5 + x5
    x25 = 4.*x0*x20
    return ((x0*x20 - x19 + x5)*third,
            (x19*x24 + x22 - x25*x24_inv)*sixth,
            (x19*x26 + x22 - x25*x26_inv)*sixth)


def volume_solutions_Cardano(T, P, b, delta, epsilon, a_alpha):
    r'''Calculate the molar volume solutions to a cubic equation of state using
    Cardano's formula, and a few tweaks to improve numerical precision.
    This solution is quite fast in general although it involves powers or
    trigonometric functions. However, it has numerical issues at many
    seemingly random areas in the low pressure region.

    Parameters
    ----------
    T : float
        Temperature, [K]
    P : float
        Pressure, [Pa]
    b : float
        Coefficient calculated by EOS-specific method, [m^3/mol]
    delta : float
        Coefficient calculated by EOS-specific method, [m^3/mol]
    epsilon : float
        Coefficient calculated by EOS-specific method, [m^6/mol^2]
    a_alpha : float
        Coefficient calculated by EOS-specific method, [J^2/mol^2/Pa]

    Returns
    -------
    Vs : list[float]
        Three possible molar volumes, [m^3/mol]

    Notes
    -----

    Two sample regions where this method does not obtain the correct solution
    (PR EOS for hydrogen) are as follows:

    .. figure:: eos/volume_error_cardano_PR_hydrogen_high.png
       :scale: 100 %
       :alt: PR EOS hydrogen volume error high pressure

    .. figure:: eos/volume_error_cardano_PR_hydrogen_low.png
       :scale: 100 %
       :alt: PR EOS hydrogen volume error low pressure

    References
    ----------
    .. [1] Reid, Robert C.; Prausnitz, John M.; Poling, Bruce E.
       Properties of Gases and Liquids. McGraw-Hill Companies, 1987.
    '''
    RT_inv = R_inv/T
    P_RT_inv = P*RT_inv
    B = etas = b*P_RT_inv
    deltas = delta*P_RT_inv
    thetas = a_alpha*P_RT_inv*RT_inv
    epsilons = epsilon*P_RT_inv*P_RT_inv

    b = (deltas - B - 1.0)
    c = (thetas + epsilons - deltas*(B + 1.0))
    d = -(epsilons*(B + 1.0) + thetas*etas)
    roots = list(roots_cubic(1.0, b, c, d))
    RT_P = R*T/P
    return [V*RT_P for V in roots]

def volume_solutions_a1(T, P, b, delta, epsilon, a_alpha):
    r'''Solution of this form of the cubic EOS in terms of volumes. Returns
    three values, all with some complex part. This uses an analytical solution
    for the cubic equation with the leading coefficient set to 1 as in the EOS
    case; and the analytical solution is the one recommended by Mathematica.

    Parameters
    ----------
    T : float
        Temperature, [K]
    P : float
        Pressure, [Pa]
    b : float
        Coefficient calculated by EOS-specific method, [m^3/mol]
    delta : float
        Coefficient calculated by EOS-specific method, [m^3/mol]
    epsilon : float
        Coefficient calculated by EOS-specific method, [m^6/mol^2]
    a_alpha : float
        Coefficient calculated by EOS-specific method, [J^2/mol^2/Pa]

    Returns
    -------
    Vs : tuple[complex]
        Three possible molar volumes, [m^3/mol]

    Notes
    -----
    A sample region where this method does not obtain the correct solution
    (PR EOS for methanol) is as follows:

    .. figure:: eos/volume_error_mathematica_PR_methanol_high.png
       :scale: 70 %
       :alt: PR EOS methanol volume error high pressure

    Examples
    --------
    Numerical precision is always challenging and has edge cases. The following
    results all havev imaginary components, but depending on the math 
    library used by the compiler even the first complex digit may not match!
    
    
    >>> volume_solutions_a1(8837.07874361444, 216556124.0631852, 0.0003990176625589891, 0.0010590390565805598, -1.5069972655436541e-07, 7.20417995032918e-15) # doctest:+SKIP
    ((0.000738308-7.5337e-20j), (-0.001186094-6.52444e-20j), (0.000127055+6.52444e-20j))
    '''
    RT_inv = R_inv/T
    P_RT_inv = P*RT_inv
    B = etas = b*P_RT_inv
    deltas = delta*P_RT_inv
    thetas = a_alpha*P_RT_inv*RT_inv
    epsilons = epsilon*P_RT_inv*P_RT_inv

    b = (deltas - B - 1.0)
    c = (thetas + epsilons - deltas*(B + 1.0))
    d = -(epsilons*(B + 1.0) + thetas*etas)
#        roots_cubic_a1, roots_cubic_a2
    RT_P = R*T/P
    return tuple(V*RT_P for V in roots_cubic_a1(b, c, d))

def volume_solutions_a2(T, P, b, delta, epsilon, a_alpha):
    r'''Solution of this form of the cubic EOS in terms of volumes. Returns
    three values, all with some complex part. This uses an analytical solution
    for the cubic equation with the leading coefficient set to 1 as in the EOS
    case; and the analytical solution is the one recommended by Maple.

    Parameters
    ----------
    T : float
        Temperature, [K]
    P : float
        Pressure, [Pa]
    b : float
        Coefficient calculated by EOS-specific method, [m^3/mol]
    delta : float
        Coefficient calculated by EOS-specific method, [m^3/mol]
    epsilon : float
        Coefficient calculated by EOS-specific method, [m^6/mol^2]
    a_alpha : float
        Coefficient calculated by EOS-specific method, [J^2/mol^2/Pa]

    Returns
    -------
    Vs : tuple[complex]
        Three possible molar volumes, [m^3/mol]

    Notes
    -----
    A sample region where this method does not obtain the correct solution
    (SRK EOS for decane) is as follows:

    .. figure:: eos/volume_error_maple_SRK_decane_high.png
       :scale: 70 %
       :alt: SRK EOS decane volume error high pressure
    '''
    #
    RT_inv = R_inv/T
    P_RT_inv = P*RT_inv
    B = etas = b*P_RT_inv
    deltas = delta*P_RT_inv
    thetas = a_alpha*P_RT_inv*RT_inv
    epsilons = epsilon*P_RT_inv*P_RT_inv

    b = (deltas - B - 1.0)
    c = (thetas + epsilons - deltas*(B + 1.0))
    d = -(epsilons*(B + 1.0) + thetas*etas)
#        roots_cubic_a1, roots_cubic_a2
    roots = list(roots_cubic_a2(1.0, b, c, d))

    RT_P = R*T/P
    return [V*RT_P for V in roots]



def volume_solutions_numpy(T, P, b, delta, epsilon, a_alpha):
    r'''Calculate the molar volume solutions to a cubic equation of state using
    NumPy's `roots` function, which is a power series iterative matrix solution
    that is very stable but does not have full precision in some cases.

    Parameters
    ----------
    T : float
        Temperature, [K]
    P : float
        Pressure, [Pa]
    b : float
        Coefficient calculated by EOS-specific method, [m^3/mol]
    delta : float
        Coefficient calculated by EOS-specific method, [m^3/mol]
    epsilon : float
        Coefficient calculated by EOS-specific method, [m^6/mol^2]
    a_alpha : float
        Coefficient calculated by EOS-specific method, [J^2/mol^2/Pa]

    Returns
    -------
    Vs : list[float]
        Three possible molar volumes, [m^3/mol]

    Notes
    -----

    A sample region where this method does not obtain the correct solution
    (SRK EOS for ethane) is as follows:

    .. figure:: eos/volume_error_numpy_SRK_ethane.png
       :scale: 100 %
       :alt: numpy.roots error for SRK eos using ethane_


    References
    ----------
    .. [1] Reid, Robert C.; Prausnitz, John M.; Poling, Bruce E.
       Properties of Gases and Liquids. McGraw-Hill Companies, 1987.
    '''
    RT_inv = R_inv/T
    P_RT_inv = P*RT_inv
    B = etas = b*P_RT_inv
    deltas = delta*P_RT_inv
    thetas = a_alpha*P_RT_inv*RT_inv
    epsilons = epsilon*P_RT_inv*P_RT_inv

    b = (deltas - B - 1.0)
    c = (thetas + epsilons - deltas*(B + 1.0))
    d = -(epsilons*(B + 1.0) + thetas*etas)

    roots = np.roots([1.0, b, c, d]).tolist()
    RT_P = R*T/P
    return [V*RT_P for V in roots]

def volume_solutions_ideal(T, P, b=0.0, delta=0.0, epsilon=0.0, a_alpha=0.0):
    r'''Calculate the ideal-gas molar volume in a format compatible with the
    other cubic EOS solvers. The ideal gas volume is the first element; and the
    secodn and third elements are zero. This is implemented to allow the
    ideal-gas model to be compatible with the cubic models, whose equations
    do not work with parameters of zero.

    Parameters
    ----------
    T : float
        Temperature, [K]
    P : float
        Pressure, [Pa]
    b : float, optional
        Coefficient calculated by EOS-specific method, [m^3/mol]
    delta : float, optional
        Coefficient calculated by EOS-specific method, [m^3/mol]
    epsilon : float, optional
        Coefficient calculated by EOS-specific method, [m^6/mol^2]
    a_alpha : float, optional
        Coefficient calculated by EOS-specific method, [J^2/mol^2/Pa]

    Returns
    -------
    Vs : list[float]
        Three possible molar volumes, [m^3/mol]

    Examples
    --------
    >>> volume_solutions_ideal(T=300, P=1e7)
    (0.0002494338785445972, 0.0, 0.0)
    '''
    return (R*T/P, 0.0, 0.0)



def volume_solutions_doubledouble_inline(T, P, b, delta, epsilon, a_alpha):
#     P, b, delta, epsilon, a_alpha = 1.0, 0.00025873301932518694, 0.0005174660386503739, -6.694277528912756e-08, 5.665358984369413
#     P *= T*2e-4
#     b *= T*1e-2
#     delta *= T*1e-3
#     epsilon *= T*1e-4
#     a_alpha *= T*1e-5
    x0r, x0e = div_dd(1.0, 0.0, P, 0.0)
#     print([x0r, x0e], 'x0r, x0e') # good
    x1r, x1e = mul_noerrors_dd(P, b)
    x2r, x2e = mul_noerrors_dd(R, T)
    x3r, x3e = mul_noerrors_dd(P, delta)

    w0r, w0e = add_dd(x1r, x1e, x2r, x2e)
    x4r, x4e = add_dd(w0r, w0e, -x3r, x3e)

    x5r, x5e = mul_dd(x0r, x0e, x4r, x4e)
    x22r, x22e = x5r + x5r, x5e + x5e # double
    x6r, x6e = mul_noerrors_dd(a_alpha, b)
    x7r, x7e = mul_dd(epsilon, 0.0, x1r, x1e)
    x8r, x8e = mul_dd(epsilon, 0.0, x2r, x2e)
    x9r, x9e = square_dd(x0r, x0e)
#     print([x9r, x9e], 'x9r, x9e') # good
    x10r, x10e = mul_noerrors_dd(P, epsilon)
    x11r, x11e = mul_dd(delta, 0.0, x1r, x1e)
    x12r, x12e = mul_dd(delta, 0.0, x2r, x2e)
    x17r, x17e = -x4r, -x4e
#     print([x17r, x17e], 'x17r, x17e') # good
    x17_2r, x17_2e = square_dd(x17r, x17e)
#     print([x17_2r, x17_2e], 'x17_2r, x17_2e') # good
    x18r, x18e = mul_dd(x0r, x0e, x17_2r, x17_2e)
    w0r, w0e = add_dd(x12r, x12e, -a_alpha, 0.0)
    w1r, w1e = add_dd(x11r, x11e, -x10r, -x10e)
    tm1r, tm1e = add_dd(w0r, w0e, w1r, w1e)
    w0r, w0e = add_dd(x6r, x6e, x7r, x7e)
    t0r, t0c = add_dd(w0r, w0e, x8r, x8e)
#     print([t0r, t0c], 't0r, t0c') # good
    t1r, t1c = add_dd(3.0*tm1r, 3.0*tm1e, x18r, x18e)
#     print([t1r, t1c], 't1r, t1c') # good
#     print(x17r, x17e, 'x17r, x17e') # good
    w0r, w0e = mul_dd(9.0*x0r, 9.0*x0e, x17r, x17e)
    w0r, w0e = mul_dd(w0r, w0e, tm1r, tm1e)
    w1r, w1e = mul_dd(2.0*x17_2r, 2.0*x17_2e, x17r, x17e)
    w1r, w1e = mul_dd(w1r, w1e, x9r, x9e)
    w0r, w0e = add_dd(w0r, w0e, w1r, w1e)
    w1r, w1e = mul_dd(t0r, t0c, -27.0, 0.0)
    t2r, t2c = add_dd(w0r, w0e, w1r, w1e) # has different rounding when not done
#     print([t2r, t2c], 't2r, t2c') # error term has a different value # fixed

    x4x9r, x4x9e = mul_dd(x4r, x4e, x9r, x9e)
#     print([x4x9r, x4x9e], 'x4x9r, x4x9e')  # good

    w0r, w0e = square_dd(t1r, t1c)
    w0r, w0e = mul_dd(-4.0*t1r, -4.0*t1c, w0r, w0e)
    w0r, w0e = mul_dd(w0r, w0e, x0r, x0e)
    w1r, w1e = square_dd(t2r, t2c)
    w0r, w0e = add_dd(w0r, w0e, w1r, w1e)
    to_sqrtr, to_sqrtrc = mul_dd(x9r, x9e, w0r, w0e)
#     print([to_sqrtr, to_sqrtrc], 'to_sqrtr, to_sqrtrc')

    w0r, w0e = mul_dd(t0r, t0c, -13.5, 0.0)
    w0r, w0e = mul_dd(x0r, x0e, w0r, w0e)
    w1r, w1e = mul_dd(x4x9r, x4x9e, -4.5*tm1r, -4.5*tm1e)
    w0r, w0e = add_dd(w0r, w0e, w1r, w1e)
    w1r, w1e = mul_dd(x4r, x4e, x4x9r, x4x9e)
    w1r, w1e = mul_dd(w1r, w1e, x5r, x5e)
    easy_addsr, easy_addse = add_dd(w0r, w0e, -w1r, -w1e)

    sqrtrr, sqrtre, sqrtcr, sqrtce = sqrt_imag_dd(to_sqrtr, to_sqrtrc, 0.0, 0.0)

    v0rr, v0re, v0cr, v0ce = add_imag_dd(easy_addsr, easy_addse, 0.0, 0.0,
                                         0.5*sqrtrr, 0.5*sqrtre, 0.5*sqrtcr, 0.5*sqrtce)
#     print([sqrtrr, sqrtre, sqrtcr, sqrtce], 'sqrtrr, sqrtre, sqrtcr, sqrtce') # good
#     print([v0rr, v0re, v0cr, v0ce], 'v0rr, v0re, v0cr, v0ce')

    x19rr, x19re, x19cr, x19ce = cbrt_imag_dd(v0rr, v0re, v0cr, v0ce)
#     print([x19rr, x19re, x19cr, x19ce], 'x19rr, x19re, x19cr, x19ce') # good
    x20rr, x20re, x20cr, x20ce = div_imag_dd(-t1r, -t1c, 0.0, 0.0, x19rr, x19re, x19cr, x19ce)
#     print([x20rr, x20re, x20cr, x20ce], 'x20rr, x20re, x20cr, x20ce')
#     print(x19rr, x19cr)

    f0rr, f0re, f0cr, f0ce = mul_imag_dd(x20rr, x20re, x20cr, x20ce, x0r, x0e, 0.0, 0.0)
    x25rr, x25re, x25cr, x25ce = 4.0*f0rr, 4.0*f0re, 4.0*f0cr, 4.0*f0ce
#     print([x25rr, x25re, x25cr, x25ce], 'x25rr, x25re, x25cr, x25ce') # perfect

    w0r, w0e = add_dd(f0rr, f0re, -x19rr, -x19re)
    g0, _ = add_dd(w0r, w0e, x5r, x5e)
#     print([f0cr, f0ce, -x19cr, -x19ce], 'f0cr, f0ce, -x19cr, -x19ce')
    g1, temp = add_dd(f0cr, f0ce, -x19cr, -x19ce)
#     print([g0, g1])

    f1rr, f1re, f1cr, f1ce = mul_imag_dd(x19rr, x19re, x19cr, x19ce,
                                          1.0, 0.0, 1.7320508075688772, 1.0035084221806902e-16)
#     print([f1rr, f1re, f1cr, f1ce], 'f1rr, f1re, f1cr, f1ce') # same
    f2rr, f2re, f2cr, f2ce = mul_imag_dd(x25rr, x25re, x25cr, x25ce,
                                          0.25, 0.0, -0.4330127018922193, -2.5087710554517254e-17)
#     print([f2rr, f2re, f2cr, f2ce], 'f2rr, f2re, f2cr, f2ce')
    w0r, w0e = add_dd(f1rr, f1re, x22r, x22e)

    g2, _ = add_dd(w0r, w0e, -f2rr, -f2re)
    g3, _ = add_dd(f1cr, f1ce, -f2cr, -f2ce)

    f3rr, f3re, f3cr, f3ce = mul_imag_dd(x19rr, x19re, x19cr, x19ce,
                                         1.0, 0.0, -1.7320508075688772, -1.0035084221806902e-16)

    f4rr, f4re, f4cr, f4ce = mul_imag_dd(x25rr, x25re, x25cr, x25ce,
                                          0.25, 0.0, 0.4330127018922193, 2.5087710554517254e-17)

    w0r, w0e = add_dd(f3rr, f3re, x22r, x22e)
    g4, _ = add_dd(w0r, w0e, -f4rr, -f4re)
    g5, _ = add_dd(f3cr, f3ce, -f4cr, -f4ce)
#     ans[0] = (g0 + g1*1j)*0.3333333333333333
#     ans[1] = (g2 + g3*1j)*0.16666666666666666
#     ans[2] = (g4 + g5*1j)*0.16666666666666666
#     return ans
    return ((g0 + g1*1j)*0.3333333333333333,
            (g2 + g3*1j)*0.16666666666666666,
            (g4 + g5*1j)*0.16666666666666666)


def volume_solutions_doubledouble_float(T, P, b, delta, epsilon, a_alpha):
#     P, b, delta, epsilon, a_alpha = 1.0, 0.00025873301932518694, 0.0005174660386503739, -6.694277528912756e-08, 5.665358984369413
    third = 0.3333333333333333
    RT_invr, RT_inve = div_dd(0.12027235504272604, 6.2277131030532505e-18, T, 0.0)
    P_RT_invr, P_RT_inve = mul_dd(P, 0.0, RT_invr, RT_inve)
    Br, Be = mul_dd(b, 0.0, P_RT_invr, P_RT_inve)
    Bp1r, Bp1e = add_dd(Br, Be, 1.0, 0.0)
    deltasr, deltase = mul_dd(delta, 0.0, P_RT_invr, P_RT_inve)
    w0r, w0e = mul_dd(a_alpha, 0.0, P_RT_invr, P_RT_inve)
    thetasr, thetase = mul_dd(w0r, w0e, RT_invr, RT_inve)
    w0r, w0e = mul_dd(epsilon, 0.0, P_RT_invr, P_RT_inve) # Could just multiply epsilon by P here
    epsilonsr, epsilonse = mul_dd(w0r, w0e, P_RT_invr, P_RT_inve)

    w0r, w0e = add_dd(deltasr, deltase, -Br, -Be)
    br, be = add_dd(w0r, w0e, -1.0, 0.0)

    w0r, w0e = add_dd(thetasr, thetase, epsilonsr, epsilonse)
    w1r, w1e = mul_dd(deltasr, deltase, Bp1r, Bp1e)
    cr, ce = add_dd(w0r, w0e, -w1r, -w1e)

    w0r, w0e = mul_dd(epsilonsr, epsilonse, Bp1r, Bp1e)
    w1r, w1e = mul_dd(thetasr, thetase, Br, Be)
    dr, de = add_dd(w0r, w0e, w1r, w1e)
    dr = -dr
    de = -de

    a_invr, a_inve = div_dd(1.0, 0.0, 1.0, 0)
    a_inv2r, a_inv2c = square_dd(a_invr, a_inve)
#     print([a_inv2r, a_inv2c])
    bbr, bbe = square_dd(br, be)
    b_ar, b_ae = br, be#mul_dd(br, be, a_invr, a_inve)
    b_a2r, b_a2e = square_dd(b_ar, b_ae)

#     w0r, w0e = mul_dd(cr, ce, a_invr, a_inve)
    fr, fe = add_dd(cr, ce, -third*b_a2r, -third*b_a2e)

    w0r, w0e = mul_dd(bbr+bbr, bbe+bbe, br, be)
#     w0r, w0e = mul_dd(w0r, w0e, a_inv2r, a_inv2c)
#     w0r, w0e = mul_dd(w0r, w0e, a_invr, a_invc)
    w1r, w1e = mul_dd(-3.0*br, -3.0*be, -3.0*cr, -3.0*ce)
#     w1r, w1e = mul_dd(w1r, w1e, a_inv2r, a_inv2c)
    w2r, w2e = mul_dd(27.0, 0.0, dr, de)
#     w2r, w2e = mul_dd(w2r, w2e, a_invr, a_inve)
    w0r, w0e = add_dd(w0r, w0e, w1r, w1e)
    w0r, w0e = add_dd(w0r, w0e, w2r, w2e)
    gr, ge = div_dd(w0r, w0e, 27.0, 0.0)

    w0r, w0e = square_dd(gr, ge)
    w1r, w1e = square_dd(fr, fe)
    w1r, w1e = mul_dd(fr, fe, w1r, w1e)
    w1r, w1e = div_dd(w1r, w1e, 27.0, 0.0)

    hr, he = add_dd(0.25*w0r, 0.25*w0e, w1r, w1e)

    if hr > 0.0:
        root_hr, root_he = sqrt_dd(hr, he)
        Rr, Re = add_dd(-0.5*gr, -0.5*ge, root_hr, root_he)

        if Rr >= 0.0:
            Sr, Se = cbrt_dd(Rr, Re)
        else:
            Sr, Se = cbrt_dd(-Rr, -Re)
            Sr, Se = -Sr, -Se
        Tr, Te = add_dd(-0.5*gr, -0.5*ge, -root_hr, -root_he)
        if T >= 0.0:
            Ur, Ue = cbrt_dd(Tr, Te)
        else:
            Ur, Ue = cbrt_dd(-Tr, -Te)
            Ur, Ue = -Ur, -Ue

        SUr, SUe = add_dd(Sr, Se, Ur, Ue)
        x1r, x1e = add_dd(SUr, SUe, -third*br, -third*be)

        # Broken somewhere.
        argr, arge = div_dd(1.0, 0.0, P_RT_invr, P_RT_inve)
        V1, _ = mul_dd(x1r, x1e, argr, arge)
        return (V1, 0.0, 0.0)
    else:
         return volume_solutions_doubledouble_inline(T, P, b, delta, epsilon, a_alpha)



third = 1/3.
one_27 = 1.0/27.0
complex_factor = 0.8660254037844386j # (sqrt(3)*0.5j)

def horner_and_der_as_error(x, coeffs):
    # Coefficients in same order as for horner
    f = 0.0
    der = 0.0
    for a in coeffs:
        der = x*der + f
        f = x*f + a
    return (f, der)

def high_alpha_one_root(T, P, b, delta, epsilon, a_alpha):
    '''It is not really possible to provide solutions that resolve the equation
    for P correctly for extremely high alpha values. P can change from 1e-2
    to 1e8 and change by 1 or 2 bits only.

    This solver handles those cases, always finding only one volume root.

    The best strategy for a continuous solution that matches mpmath really well
    is to use Cardano's method to obtain the correct single volume
    (use the cubic criteria h > 0 but set it to
    h > 200 to ensure we are well into that region),
    and then use Newton's method to polish it (normally converges in 1
    iteration).

    If the criteria is not met, 0 is returned and another solver must be used.
    '''
    b_eos = b
    RT_inv = R_inv/T
    RT_P = R*T/P

    P_RT_inv = P*RT_inv
    B = etas = b*P_RT_inv
    deltas = delta*P_RT_inv
    thetas = a_alpha*P_RT_inv*RT_inv
    epsilons = epsilon*P_RT_inv*P_RT_inv

    b = (deltas - B - 1.0)
    c = (thetas + epsilons - deltas*(B + 1.0))
    d = -(epsilons*(B + 1.0) + thetas*etas)

    a, b, c, d = 1.0, b, c, d
    coeffs = (1.0, b, c, d)

    a_inv = 1.0/a
    a_inv2 = a_inv*a_inv
    bb = b*b
    '''Herbie modifications for f:
    c*a_inv - b_a*b_a*third
    '''
    b_a = b*a_inv
    b_a2 = b_a*b_a
    f = c*a_inv - b_a2*third
    g = ((2.0*(bb*b) * a_inv2*a_inv) - (9.0*b*c)*(a_inv2) + (27.0*d*a_inv))*one_27
    h = (0.25*(g*g) + (f*f*f)*one_27)

    if h < 200.0 or abs(g) > 1e152:
        return 0.0

    root_h = sqrt(h)
    R_poly = -0.5*g + root_h

    # It is possible to save one of the power of thirds!
    if R_poly >= 0.0:
        S = R_poly**third
    else:
        S = -((-R_poly)**third)
    T = -(0.5*g) - root_h
    if T >= 0.0:
        U = (T**(third))
    else:
        U = -(((-T)**(third)))

    SU = S + U
    b_3a = b*(third*a_inv)
    x1 = SU - b_3a

    # Must be polished
    x1 = newton(horner_and_der_as_error, x1, bisection=True, fprime=True, low=b_eos/RT_P, xtol=1e-16, args=(coeffs,))

    V = x1*RT_P
    if V == b_eos:
        V = b_eos*(1.0 + 3e-16)
    return V
