Flash_PR_EOS / app.py
nesanchezo's picture
Update app.py
e42cd6e verified
import streamlit as st
import numpy as np
# Configurar Streamlit
st.set_page_config(page_title="Thermodynamics of Hydrocarbon Fluids", layout="wide")
#st.title("THERMODYNAMICS OF HYDROCARBON FLUIDS (EME 531/PNG 520 – Fall 2024")
# Display the title
st.markdown(
"""
# **THERMODYNAMICS OF HYDROCARBON FLUIDS**
### *(EME 531/PNG 520 – Fall 2024)*
## **PROBLEM SET #9**
""",
unsafe_allow_html=True
)
# Display the author information
st.markdown(
"""
**Author:** Nestor E Sanchez Ospina
**PSU ID:** 968400074
""",
unsafe_allow_html=True
)
R = 10.73
components = {
'CO2': {'Tc': 547.91, 'Pc': 1071.0, 'omega': 0.2667, 'vol_shift': 0.0344, 'm_w': 0},
'C1': {'Tc': 343.33, 'Pc': 666.4, 'omega': 0.0080, 'vol_shift': -0.0833, 'm_w': 16.0430},
'C2': {'Tc': 549.92, 'Pc': 706.5, 'omega': 0.0979, 'vol_shift': 0.0783, 'm_w': 0},
'C3' : {'Tc': 666.06, 'Pc': 616.0, 'omega': 0.1522, 'vol_shift': -0.1000, 'm_w': 44.0970},
'iC4': {'Tc': 734.46, 'Pc': 527.9, 'omega': 0.1852, 'vol_shift': 0.0714, 'm_w': 0},
'nC4': {'Tc': 765.62, 'Pc': 550.6, 'omega': 0.1930, 'vol_shift': -0.1000, 'm_w': 58.1240},
'iC5': {'Tc': 829.10, 'Pc': 490.4, 'omega': 0.2280, 'vol_shift': 0.0679, 'm_w': 0},
'nC5': {'Tc': 845.80, 'Pc': 488.6, 'omega': 0.2514, 'vol_shift': 0.0675, 'm_w': 0},
'nC6': {'Tc': 913.60, 'Pc': 430.59, 'omega': 0.2960, 'vol_shift': 0.0200, 'm_w': 86.1780},
'nC10': {'Tc': 1111.00, 'Pc': 305.2, 'omega': 0.4898, 'vol_shift': 0.2600, 'm_w': 142.2850},
}
ki={'CO2-CO2': 0.0, 'CO2-C1': 0.0, 'C1-CO2': 0.0,
'CO2-C2': 0.0, 'C2-CO2': 0.0, 'CO2-C3': 0.0, 'C3-CO2': 0.0, 'CO2-iC4': 0.0, 'iC4-CO2': 0.0, 'CO2-nC4': 0.0,
'nC4-CO2': 0.0, 'CO2-iC5': 0.0, 'iC5-CO2': 0.0, 'CO2-nC5': 0.0, 'nC5-CO2': 0.0, 'CO2-nC6': 0.0, 'nC6-CO2': 0.0,
'CO2-nC10': 0.0, 'nC10-CO2': 0.0, 'C1-C1': 0.0, 'C1-C2': 0.0, 'C2-C1': 0.0, 'C1-C3': 0.0, 'C3-C1': 0.0,
'C1-iC4': 0.0, 'iC4-C1': 0.0, 'C1-nC4': 0.0, 'nC4-C1': 0.0, 'C1-iC5': 0.0, 'iC5-C1': 0.0, 'C1-nC5': 0.0, 'nC5-C1': 0.0,
'C1-nC6': 0.0, 'nC6-C1': 0.0, 'C1-nC10': 0.0, 'nC10-C1': 0.0, 'C2-C2': 0.0, 'C2-C3': 0.0, 'C3-C2': 0.0, 'C2-iC4': 0.0,
'iC4-C2': 0.0, 'C2-nC4': 0.0, 'nC4-C2': 0.0, 'C2-iC5': 0.0, 'iC5-C2': 0.0, 'C2-nC5': 0.0, 'nC5-C2': 0.0, 'C2-nC6': 0.0,
'nC6-C2': 0.0, 'C2-nC10': 0.0, 'nC10-C2': 0.0, 'C3-C3': 0.0, 'C3-iC4': 0.0, 'iC4-C3': 0.0, 'C3-nC4': 0.0, 'nC4-C3': 0.0,
'C3-iC5': 0.0, 'iC5-C3': 0.0, 'C3-nC5': 0.0, 'nC5-C3': 0.0, 'C3-nC6': 0.0, 'nC6-C3': 0.0, 'C3-nC10': 0.0, 'nC10-C3': 0.0, 'iC4-iC4': 0.0,
'iC4-nC4': 0.0, 'nC4-iC4': 0.0, 'iC4-iC5': 0.0, 'iC5-iC4': 0.0, 'iC4-nC5': 0.0, 'nC5-iC4': 0.0, 'iC4-nC6': 0.0, 'nC6-iC4': 0.0,
'iC4-nC10': 0.0, 'nC10-iC4': 0.0, 'nC4-nC4': 0.0, 'nC4-iC5': 0.0, 'iC5-nC4': 0.0, 'nC4-nC5': 0.0, 'nC5-nC4': 0.0, 'nC4-nC6': 0.0,
'nC6-nC4': 0.0, 'nC4-nC10': 0.0, 'nC10-nC4': 0.0, 'iC5-iC5': 0.0, 'iC5-nC5': 0.0, 'nC5-iC5': 0.0, 'iC5-nC6': 0.0, 'nC6-iC5': 0.0, 'iC5-nC10': 0.0, 'nC10-iC5': 0.0, 'nC5-nC5': 0.0, 'nC5-nC6': 0.0,
'nC6-nC5': 0.0, 'nC5-nC10': 0.0, 'nC10-nC5': 0.0, 'nC6-nC6': 0.0, 'nC6-nC10': 0.0, 'nC10-nC6': 0.0, 'nC10-nC10': 0.0}
selected_components = st.session_state.get("selected_components", [])
if "selected_components" not in st.session_state:
st.session_state["selected_components"] = []
component_to_add = st.selectbox("Select component to add:", list(components.keys()))
if st.button("Add component"):
if component_to_add not in st.session_state["selected_components"]:
st.session_state["selected_components"].append(component_to_add)
else:
st.warning(f"The component {component_to_add} is already in the list.")
# Display selected components with an option to remove them
selected_components = st.session_state["selected_components"]
if "selected_components" not in st.session_state:
st.session_state["selected_components"] = ["Component 1", "Component 2", "Component 3"]
selected_components = st.session_state["selected_components"]
st.write("### Selected Components")
if selected_components:
# Generar un identificador único para evitar conflictos
dynamic_key = str(hash(tuple(selected_components)))
# Permite seleccionar varios componentes para eliminarlos
components_to_remove = st.multiselect(
"Select components to remove:",
options=selected_components,
key=dynamic_key
)
if st.button("Remove Selected"):
for component in components_to_remove:
st.session_state["selected_components"].remove(component)
st.write(f"Remaining components: {st.session_state['selected_components']}")
else:
st.info("No components selected yet.")
concentrations = {}
if selected_components:
st.write("### Enter concentrations for the selected components:")
st.write(
"The sum of the concentrations should ideally equal 1. However, if they don't, "
"the values will be automatically normalized to ensure consistency.")
for comp in selected_components:
input_value = st.text_input(f"Concentration of {comp}:", value="0.1")
try:
concentrations[comp] = float(input_value)
except ValueError:
st.error(f"The concentration for {comp} must be a number. Using default value 0.1.")
concentrations[comp] = 0.1
# Normalizar concentraciones
ci = np.array([concentrations[comp] for comp in selected_components])
if np.sum(ci) > 0:
ci /= np.sum(ci)
else:
st.error("The sum of the concentrations must be greater than 0.")
T = st.slider("Temperature (°F):", min_value=-100.0, max_value=300.0, value=160.0) + 459.67
P = st.slider("Pressure (psia):", min_value=50.0, max_value=5000.0, value=2000.0)
# Add a dropdown menu for tolerance selection
def generate_super_script(number):
superscript_map = {
'0': '⁰',
'1': '¹',
'2': '²',
'3': '³',
'4': '⁴',
'5': '⁵',
'6': '⁶',
'7': '⁷',
'8': '⁸',
'9': '⁹',
'-': '⁻'
}
return ''.join(superscript_map.get(char, char) for char in str(number))
# tolerance from 10^-1 to 10^-14
tolerance_options = {}
for exponent in range(1, 15):
label = f"10{generate_super_script(-exponent)}"
value = 10**-exponent
tolerance_options[label] = value
selected_tolerance_label = st.selectbox("Select the desired tolerance:", list(tolerance_options.keys()))
tolerance = tolerance_options[selected_tolerance_label]
# Display the selected tolerance
st.write(f"Selected Tolerance: {selected_tolerance_label} ({tolerance})")
Pci = np.array([components[comp]['Pc'] for comp in selected_components])
Tci = np.array([components[comp]['Tc'] for comp in selected_components])
omega = np.array([components[comp]['omega'] for comp in selected_components])
m_w = np.array([components[comp]['m_w'] for comp in selected_components])
vol_shift = np.array([components[comp]['vol_shift'] for comp in selected_components])
# Constants for Gibbs energy difference function
m1 = 1 + np.sqrt(2)
m2 = 1 - np.sqrt(2)
component_names = list(selected_components)
# Crear la matriz kij
kij = np.zeros((len(component_names), len(component_names)))
for i, comp1 in enumerate(component_names):
for j, comp2 in enumerate(component_names):
kij[i, j] = ki[f"{comp1}-{comp2}"]
def calculate_phase_components(P, T, ci, Pci, Tci, omega, Ki, tol=1e-3, max_iter=100):
# Rachford-Rice function
def rachford_rice(ci, Ki, tol=1e-4, max_iter=100):
fng = 0.5 # Initial guess
for _ in range(max_iter):
g = np.sum(ci * (Ki - 1) / (1 + fng * (Ki - 1)))
gp = -np.sum(ci * (Ki - 1)**2 / (1 + fng * (Ki - 1))**2)
fng_new = fng - g / gp
if abs(fng_new - fng) < tol:
fng = fng_new
break
fng = fng_new
return fng
# Calculate vapor molar fraction
fng = rachford_rice(ci, Ki)
# Calculate phase compositions
xi = ci / (1 + fng * (Ki - 1))
yi = Ki * xi
return xi, yi, fng
def calculate_z_factor(P, T, ci, Pci, Tci, omega):
def calculate_pr_eos_params(T, P, ci, Pci, Tci, omega):
Tr = T / Tci
Pr = P / Pci
kappa = 0.37464 + 1.54226 * omega - 0.26992 * omega**2
alpha = (1 + kappa * (1 - np.sqrt(Tr)))**2
a = 0.45724 * (R * Tci)**2 / Pci * alpha
b = 0.07780 * R * Tci / Pci
a_m = np.sum(ci[:, None] * ci[None, :] * np.sqrt(a[:, None] * a[None, :]))
b_m = np.sum(ci * b)
A = a_m * P / (R**2 * T**2)
B = b_m * P / (R * T)
return A, B, a, b, a_m, b_m
# Function to solve the cubic EOS for Z
def solve_cubic_eos(A, B):
coeffs = [1, -(1 - B), A - 3 * B**2 - 2 * B, -(A * B - B**2 - B**3)]
roots = np.roots(coeffs)
roots = np.real(roots[np.isreal(roots)]) # Filter real roots
return roots
# Function to calculate Gibbs energy difference
def calculate_gibbs_energy_difference(Z_max, Z_min, A, B, m1, m2):
term1 = Z_max - Z_min
term2 = np.log((Z_min - B) / (Z_max - B))
term3 = - (A / (B * (m2 - m1))) * np.log(
((Z_min + m1 * B) * (Z_max + m2 * B)) / ((Z_min + m2 * B) * (Z_max + m1 * B))
)
dG_RT = term1 + term2 + term3
return dG_RT
# Function to select the appropriate Z root using Gibbs energy difference
def select_z_root(Z_roots, A, B, m1, m2):
# Sort roots in ascending order
Z_roots = sorted(Z_roots)
Z_min = Z_roots[0]
Z_max = Z_roots[-1]
# Ignore the middle root if there are three roots
if len(Z_roots) == 3:
Z_min, Z_max = Z_roots[0], Z_roots[2]
# Apply the selection criteria
if Z_min < B:
# Select Z_max if Z_min < B
Z = Z_max
else:
# Calculate Gibbs energy difference to choose between Z_min and Z_max
dG_RT = calculate_gibbs_energy_difference(Z_max, Z_min, A, B, m1, m2)
if dG_RT > 0:
Z = Z_min
else:
Z = Z_max
return Z
A, B, ai, bi, a_m, b_m = calculate_pr_eos_params(T, P, ci, Pci, Tci, omega)
Z_roots = solve_cubic_eos(A, B)
Z = select_z_root(Z_roots, A, B, m1, m2)
return A, B, ai, bi, a_m, b_m, Z
def calculate_mixture_density(P, Z, T, mole_fractions, MWs, b_vol_shifts):
molar_density = P / (Z * R * T) # lb-mole/ft³
#print(f"Molar Density (without correction): {molar_density:.6f} lb-mole/ft³")
# Step 2: Mixture volume shift parameter
b_mixture = sum(x * b for x, b in zip(mole_fractions, b_vol_shifts))
#print(f"Mixture Volume Shift: {b_mixture:.6f} ft³/lb-mole")
# Step 3: Corrected molar density
try:
corrected_molar_density = molar_density / (1 - b_mixture * molar_density)
#print(f"Corrected Molar Density: {corrected_molar_density:.6f} lb-mole/ft³")
except ZeroDivisionError:
raise ValueError("Volume shift correction caused division by zero.")
# Step 4: Average molecular weight
M_avg = sum(x * mw for x, mw in zip(mole_fractions, MWs))
#print(f"Average Molecular Weight (M_avg): {M_avg:.6f} lb/lb-mole")
# Step 5: Mass density
mass_density = corrected_molar_density * M_avg # lb/ft³
#print(f"Mass Density: {mass_density:.6f} lb/ft³")
return corrected_molar_density, mass_density
def calculate_fugacity_coefficients(Z, A, B, ai, bi, a_m, b_m, ci, kij):
if np.any(Z <= B):
raise ValueError("Error: Z must be greater than B in all elements to avoid undefined logarithms.")
if A <= 0 or B <= 0 or a_m <= 0 or b_m <= 0:
raise ValueError("Error: The parameters A, B, a_m, and b_m must be greater than zero.")
bi_over_bm = bi / b_m
ai_over_am = np.array([
np.sum(ci * np.sqrt(ai[i] * ai) * (1 - kij[i])) for i in range(len(ci))
]) / a_m
try:
ln_phi = (
bi_over_bm * (Z - 1)
- np.log(Z - B)
- (A / (2 * np.sqrt(2) * B)) * (2 * ai_over_am - bi_over_bm) *
np.log((Z + (1 + np.sqrt(2)) * B) / (Z + (1 - np.sqrt(2)) * B))
)
except Exception as e:
raise ValueError(f"Error in the calculation of ln(phi): {e}")
ln_phi = np.clip(ln_phi, -100, 100)
phi = np.exp(ln_phi)
if np.any(np.isnan(phi)) or np.any(phi <= 0):
raise ValueError(f"Error: Invalid fugacity coefficients: {phi}")
return phi
def ssm_method(P, T, ci, kij, components, tol=1e-3, max_iter=100):
Ki = np.exp(5.37 * (1 + omega) * (1 - Tci / T))
for iteration in range(max_iter):
xi, yi, fng = calculate_phase_components(P, T, ci, Pci, Tci, omega, Ki)
A_liq, B_liq, ai_liq, bi_liq, a_m_liq, b_m_liq, Z_liq = calculate_z_factor(P, T, xi, Pci, Tci, omega)
A_vap, B_vap, ai_vap, bi_vap, a_m_vap, b_m_vap, Z_vap = calculate_z_factor(P, T, yi, Pci, Tci, omega)
phi_liq = calculate_fugacity_coefficients(Z_liq, A_liq, B_liq, ai_liq, bi_liq, a_m_liq, b_m_liq, xi, kij)
phi_vap = calculate_fugacity_coefficients(Z_vap, A_vap, B_vap, ai_vap, bi_vap, a_m_vap, b_m_vap, yi, kij)
f_liq = phi_liq * xi * P
f_vap = phi_vap * yi * P
Ki_prev = Ki.copy()
Ki = Ki_prev*(f_liq / f_vap)
if np.all(np.abs((f_liq / f_vap) - 1)**2 < tol):
print(f"Convergence achieved in {iteration + 1} iterations.")
molar_density_liq, mass_density_liq = calculate_mixture_density(P, Z_liq, T, xi, m_w, vol_shift)
molar_density_vap, mass_density_vap = calculate_mixture_density(P, Z_vap, T, yi, m_w, vol_shift)
return Ki, Z_liq, Z_vap, f_liq, f_vap, molar_density_liq, mass_density_liq, molar_density_vap, mass_density_vap, fng, xi, yi, iteration
if iteration == max_iter - 1:
print("Warning: The method did not converge. Last values:")
print(f"Z_liq={Z_liq}, Z_vap={Z_vap}, phi_liq={phi_liq}, phi_vap={phi_vap}")
print("Warning: Convergence was not achieved.")
return Ki, None, None, None, None, max_iter
if len(selected_components)>1:
if np.sum(ci)==1:
# Call the method to calculate results
Ki, Z_liq, Z_vap, f_liq, f_vap, molar_density_liq, mass_density_liq, molar_density_vap, mass_density_vap, fng, xi, yi, iteration = ssm_method(
P, T, ci, kij, components, tol=tolerance, max_iter=100
)
# Display unique values outside the table
st.write("### Global Results")
st.write(f"**tolerance:** {tolerance}")
st.write(f"**liquid Mole frac:** {1-fng:.6f}")
st.write(f"**vapor Mole frac:** {fng:.6f}")
st.write(f"**Z_liq (liquid):** {Z_liq:.6f}")
st.write(f"**Z_vap (vapor):** {Z_vap:.6f}")
st.write(f"**Molar Density (liquid):** {molar_density_liq:.6f} lb-mole/ft³")
st.write(f"**Mass Density (liquid):** {mass_density_liq:.6f} lb/ft³")
st.write(f"**Molar Density (vapor):** {molar_density_vap:.6f} lb-mole/ft³")
st.write(f"**Mass Density (vapor):** {mass_density_vap:.6f} lb/ft³")
st.write(f"**Iterations:** {iteration}")
# Prepare per-component data for the table
st.write("### Per-Component Results")
data = {
"Component": selected_components,
"K-value": Ki,
"Liquid Fugacity": f_liq,
"Vapor Fugacity": f_vap,
"Liquid component xi": xi,
"Vapor component yi": yi,
}
# Create DataFrame and display it in Streamlit
import pandas as pd
df = pd.DataFrame(data)
st.table(df)
else:
st.info("Please ensure that the sum of the concentrations equals one.")
else:
st.info("Please select at least two component to calculate.")