# ===================================================================================================
# LECTURE 27: Generalized van der Waals Equation of State Solver
# Author: Edward Maginn, CBE 20260
# Description: 
#   This notebook solves the cubic van der Waals equation of state for a variety of 
#   industrial gases. It fetches critical properties using CoolProp, converts user units,
#   solves for the molar volume roots, and plots the results on a P-V diagram.
# ===================================================================================================

# Install CoolProp if it is missing (e.g., on Google Colab)
try:
    import CoolProp
except ImportError:
    %pip install -q CoolProp

import numpy as np
import matplotlib.pyplot as plt
import ipywidgets as widgets
from IPython.display import display, clear_output
import CoolProp.CoolProp as CP

# Dictionary mapping friendly names to CoolProp string identifiers
fluid_dict = {
    'Methane': 'Methane',
    'Nitrogen': 'Nitrogen',
    'Oxygen': 'Oxygen',
    'Ethane': 'Ethane',
    'Ammonia': 'Ammonia',
    'Carbon Dioxide': 'CarbonDioxide',
    'Carbon Monoxide': 'CarbonMonoxide',
    'Argon': 'Argon',
    'Water': 'Water',
    'R-32': 'R32'
}

def solve_and_plot(fluid_name, T_val, T_unit, P_val, P_unit):
    fluid = fluid_dict[fluid_name]
    
    # --- 1. Unit Conversions to Standard Math Units (Kelvin & bar) ---
    if T_unit == '°C': 
        T = T_val + 273.15
    elif T_unit == '°F': 
        T = (T_val - 32) * 5/9 + 273.15
    else: 
        T = T_val
        
    if P_unit == 'atm': 
        P = P_val * 1.01325
    elif P_unit == 'Pa': 
        P = P_val / 1e5
    elif P_unit == 'MPa': 
        P = P_val * 10
    elif P_unit == 'psi': 
        P = P_val / 14.5038
    else: 
        P = P_val

    # Handle zero/negative T or P to prevent math errors
    if T <= 0 or P <= 0:
        print("Temperature and Pressure must be greater than absolute zero.")
        return

    # --- 2. Fetch Critical Properties ---
    Tc = CP.PropsSI('TCRIT', fluid)
    Pc_Pa = CP.PropsSI('PCRIT', fluid)
    Pc = Pc_Pa / 1e5  # Convert Pa to bar
    
    # --- 3. Calculate VDW Parameters ---
    R = 0.0831446 # L*bar/(mol*K)
    a = 27 * (R * Tc)**2 / (64 * Pc)
    b = R * Tc / (8 * Pc)
    
    # --- 4. Solve the Cubic Equation for Roots ---
    # V^3 - (b + RT/P)V^2 + (a/P)V - (ab/P) = 0
    C2 = -(b + R * T / P)
    C1 = a / P
    C0 = -a * b / P
    
    roots = np.roots([1, C2, C1, C0])
    
    # Filter out complex roots and non-physical roots (V < b)
    real_roots = roots[np.isclose(roots.imag, 0)].real
    real_roots = np.sort(real_roots[real_roots > b])

    # --- 5. Print Output Summary ---
    print("="*60)
    print(f"FLUID: {fluid_name}")
    print(f"Critical State: Tc = {Tc:.2f} K  |  Pc = {Pc:.2f} bar")
    print(f"VDW Parameters: a = {a:.4f} L^2·bar/mol^2  |  b = {b:.5f} L/mol")
    print("-" * 60)
    print(f"TARGET STATE:   T = {T:.2f} K  |  P = {P:.2f} bar")
    
    if len(real_roots) == 1:
        if T > Tc:
            state = "Supercritical Fluid"
        else:
            # Basic heuristic: if root is close to 'b', it's liquid, if close to ideal gas, it's vapor
            state = "Liquid" if real_roots[0] < 5*b else "Vapor"
        print(f"\n1 Real Root Found ({state}):")
        print(f"  ---> V = {real_roots[0]:.4f} L/mol")
    elif len(real_roots) == 3:
        print(f"\n3 Real Roots Found (Phase Split Region):")
        print(f"  ---> V_liquid   = {real_roots[0]:.4f} L/mol")
        print(f"  ---> V_unstable = {real_roots[1]:.4f} L/mol (non-physical)")
        print(f"  ---> V_vapor    = {real_roots[2]:.4f} L/mol")
    print("="*60)
    
    # --- 6. Plotting the P-V Diagram ---
    # Determine sensible bounds for the plot
    V_ig = R * T / P
    V_max = max(V_ig * 1.5, 10 * b)
    if len(real_roots) == 3:
        V_max = max(V_max, real_roots[2] * 1.3)
        
    V_arr = np.linspace(b * 1.02, V_max, 2000)
    
    # Calculate isotherms
    P_iso = (R * T) / (V_arr - b) - a / (V_arr**2)
    P_crit = (R * Tc) / (V_arr - b) - a / (V_arr**2)
    
    plt.figure(figsize=(9, 6))
    
    # Plot Critical Isotherm
    plt.plot(V_arr, P_crit, 'r--', lw=1.5, label=f'Critical Isotherm (Tc = {Tc:.1f} K)')
    plt.plot(3*b, Pc, 'rs', markersize=6, label='Critical Point')

    # Plot Selected Isotherm
    plt.plot(V_arr, P_iso, 'b-', lw=2, label=f'Target Isotherm (T = {T:.1f} K)')
    
    # Plot the Target Pressure line
    plt.axhline(P, color='k', linestyle=':', lw=1.5, label=f'Target Pressure (P = {P:.1f} bar)')
    
    # Plot roots
    for i, root in enumerate(real_roots):
        if len(real_roots) == 3 and i == 1:
            # Unstable root
            plt.plot(root, P, 'wo', markeredgecolor='k', markersize=8, label='Unstable Root' if i==1 else "")
        else:
            plt.plot(root, P, 'go', markersize=8, label='Physical Root(s)' if i==0 else "")
            
    # Formatting
    plt.ylim(0, max(P * 1.5, Pc * 1.5))
    plt.xlim(0, V_max)
    plt.xlabel('Molar Volume, V (L/mol)', fontsize=12)
    plt.ylabel('Pressure, P (bar)', fontsize=12)
    plt.title(f'van der Waals Equation of State for {fluid_name}', fontsize=14, fontweight='bold')
    plt.legend(loc='upper right')
    plt.grid(True, linestyle='--', alpha=0.6)
    
    plt.show()

# --- 7. Interactive UI Setup ---
style = {'description_width': 'initial'}

# Set default fluid to Methane
fluid_dropdown = widgets.Dropdown(options=list(fluid_dict.keys()), value='Methane', description='Fluid:')

# Set default Temperature to 300 K
T_input = widgets.FloatText(value=300, description='Temperature:', style=style, layout=widgets.Layout(width='200px'))
T_unit = widgets.Dropdown(options=['K', '°C', '°F'], value='K', layout=widgets.Layout(width='80px'))

# Set default Pressure to 100 bar
P_input = widgets.FloatText(value=100, description='Pressure:', style=style, layout=widgets.Layout(width='200px'))
P_unit = widgets.Dropdown(options=['bar', 'atm', 'Pa', 'MPa', 'psi'], value='bar', layout=widgets.Layout(width='80px'))

T_box = widgets.HBox([T_input, T_unit])
P_box = widgets.HBox([P_input, P_unit])

ui = widgets.VBox([fluid_dropdown, T_box, P_box])
out = widgets.interactive_output(solve_and_plot, {
    'fluid_name': fluid_dropdown,
    'T_val': T_input,
    'T_unit': T_unit,
    'P_val': P_input,
    'P_unit': P_unit
})

display(ui, out)
# Static preview: results for the default inputs above.
# The output is saved so the course website can show it.
# To try other inputs, use the controls above, or open
# this notebook in Colab (badge on the course website).
solve_and_plot(fluid_name=fluid_dropdown.value, T_val=T_input.value, T_unit=T_unit.value,
               P_val=P_input.value, P_unit=P_unit.value)
============================================================
FLUID: Methane
Critical State: Tc = 190.56 K  |  Pc = 45.99 bar
VDW Parameters: a = 2.3028 L^2·bar/mol^2  |  b = 0.04306 L/mol
------------------------------------------------------------
TARGET STATE:   T = 300.00 K  |  P = 100.00 bar

1 Real Root Found (Supercritical Fluid):
  ---> V = 0.2032 L/mol
============================================================