Spaces:
Sleeping
Sleeping
| 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.") |