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.")