From a5922fcf698fb43749e0abc8ab1b0f238bc261b2 Mon Sep 17 00:00:00 2001 From: Vinicius Santana Date: Mon, 28 Oct 2024 12:05:39 +0100 Subject: [PATCH 01/13] Removing old files --- src/HeatExchange/Jacket.jl | 99 -------- .../ReactionManager/KineticReaction.jl | 24 -- src/Separation/Flash.jl | 138 ---------- src/Sources/MaterialSource.jl | 236 ------------------ src/Sources/Sourceutils.jl | 45 ---- src/Valve/Valves.jl | 46 ---- src/database/benzene.json | 40 --- src/database/carbon monoxide.json | 40 --- src/database/methane.json | 40 --- src/database/methanol.json | 40 --- src/database/methyloxirane.json | 41 --- src/database/nitrogen.json | 40 --- src/database/propyleneglycol.json | 40 --- src/database/toluene.json | 40 --- src/database/water.json | 40 --- src/utils.jl | 46 ---- test/Flash_test/FlashDrum.jl | 98 -------- test/Reactor_tests/CSTR_test.jl | 59 ----- test/Reactor_tests/Jacket_test.jl | 48 ---- test/Sources_tests/source_tests.jl | 35 --- 20 files changed, 1235 deletions(-) delete mode 100644 src/HeatExchange/Jacket.jl delete mode 100644 src/Reactors/ReactionManager/KineticReaction.jl delete mode 100644 src/Separation/Flash.jl delete mode 100644 src/Sources/MaterialSource.jl delete mode 100644 src/Sources/Sourceutils.jl delete mode 100644 src/Valve/Valves.jl delete mode 100644 src/database/benzene.json delete mode 100644 src/database/carbon monoxide.json delete mode 100644 src/database/methane.json delete mode 100644 src/database/methanol.json delete mode 100644 src/database/methyloxirane.json delete mode 100644 src/database/nitrogen.json delete mode 100644 src/database/propyleneglycol.json delete mode 100644 src/database/toluene.json delete mode 100644 src/database/water.json delete mode 100644 src/utils.jl delete mode 100644 test/Flash_test/FlashDrum.jl delete mode 100644 test/Reactor_tests/CSTR_test.jl delete mode 100644 test/Reactor_tests/Jacket_test.jl delete mode 100644 test/Sources_tests/source_tests.jl diff --git a/src/HeatExchange/Jacket.jl b/src/HeatExchange/Jacket.jl deleted file mode 100644 index 25df848..0000000 --- a/src/HeatExchange/Jacket.jl +++ /dev/null @@ -1,99 +0,0 @@ -@component function Jacket(;substances_user, - Nc = length(substances_user), - phase, - thermal_fluid_model, - heat_transfer_coef, - name, - ) - - - -properties = Dict(subs => load_component_properties(subs) for subs in substances_user) -MWs = [properties[subs]["MW"] for subs in substances_user] - -pars = @parameters begin - U = heat_transfer_coef -end - -systems = @named begin - Out = matcon(; Nc = Nc) - In = matcon(; Nc = Nc) - EnergyCon = thermal_energy_connector() -end - -vars = @variables begin - LMTD(t), [description = "log mean temperature difference (-)"] - Tⱼ(t), [description = "Thermal fluid exiting temperature (K)"] - H(t), [description = "Thermal fluid exiting enthalpy (J/mol)"] - S(t), [description = "Thermal fluid exiting entropy (J/K/mol)"] - ρ(t), [description = "Thermal fluid exiting molar density (mol/m³)"] - ρʷ(t), [description = "Thermal fluid exiting mass density (kg/m³)"] - P_out(t), [description = "Thermal fluid exiting pressure (Pa)"] - F_out(t), [description = "Thermal fluid exiting molar flow rate (mol/s)"] - Fʷ_out(t), [description = "Thermal fluid exiting mass flow rate (mol/s)"] - Q_out(t), [description = "Thermal fluid exiting volumetric flow rate (m³/s)"] - Q̇(t), [description = "Heat transfer rate from/to the jacket"] -end - - -enthalpy_eq = [H ~ enthalpy(thermal_fluid_model, P_out, Tⱼ, Out.z₁, phase = "unknown")] - -entropy_eq = [S ~ 0.0] - -densities_eq = [ρ ~ molar_density(thermal_fluid_model, P_out, Tⱼ, Out.z₁, phase = "unknown") - ρʷ ~ mass_density(thermal_fluid_model, P_out, Tⱼ, Out.z₁, phase = "unknown")] - -pressure_drop = [P_out ~ In.P] - -mass_balance = [In.Fʷ - Fʷ_out ~ 0.0 - Q_out ~ Fʷ_out/ρʷ - Q_out ~ F_out/ρ] - -lmtd = [LMTD ~ log((EnergyCon.T - In.T)/(EnergyCon.T - Tⱼ))] - -heat_flux = [Q̇ ~ U*EnergyCon.A*(In.T - Tⱼ)/LMTD] - -energy_balance = [In.H*In.F - H*F_out - Q̇ ~ 0.0] # Quasi steady state assumption (Temperature change is much faster than reactor change) - - - -#Outlet connector equations: -out_conn = [Out.P ~ P_out - Out.T ~ Tⱼ - Out.F ~ F_out - Out.Fʷ ~ Fʷ_out - Out.H ~ H - Out.S ~ S - Out.ρʷ ~ ρʷ - Out.ρ ~ ρ - scalarize(Out.z₁ .~ In.z₁)... - Out.MW[1] ~ MWs - EnergyCon.ϕᴱ ~ Q̇ -] - - -if phase == :liquid - out_conn_phases = [ - scalarize(Out.z₂ .~ 0.0)... - scalarize(Out.z₃ .~ In.z₁)... - Out.MW[2] ~ 0.0 - Out.MW[3] ~ MWs - Out.α_g ~ 0.0] - - elseif phase == :vapor - out_conn_phases = [ - scalarize(Out.z₂ .~ In.z₁)... - scalarize(Out.z₃ .~ 0.0)... - Out.MW[2] ~ MWs - Out.MW[3] ~ 0.0 - Out.α_g ~ 1.0] -end - -eqs = [enthalpy_eq...; entropy_eq...; densities_eq...; pressure_drop...; mass_balance...; lmtd...; heat_flux...; energy_balance...; out_conn...; out_conn_phases...] - -ODESystem([eqs...;], t, collect(Iterators.flatten(vars)), collect(Iterators.flatten(pars)); name, systems = [Out, In, EnergyCon]) - -end - - -export Jacket \ No newline at end of file diff --git a/src/Reactors/ReactionManager/KineticReaction.jl b/src/Reactors/ReactionManager/KineticReaction.jl deleted file mode 100644 index 5e96f7e..0000000 --- a/src/Reactors/ReactionManager/KineticReaction.jl +++ /dev/null @@ -1,24 +0,0 @@ -# Definindo a estrutura -struct KineticReactionNetwork{T} - Coef_Cr::Union{Vector{T}, Matrix{T}} # Stoichiometric coefficients of each component in each reaction (-) - Do_r::Union{Vector{T}, Matrix{T}} # Forward order of the components () - substances_user::Vector{String} # Substances in the reaction network - Nc::Int # Number of components in the reaction network - Nri::Int # Number of reactions in the reaction network (r) - Af_r::T # Arrhenius constant of each reaction (s⁻¹) - Ef_r::T # Activation energy of each reaction (J.mol⁻¹) - name::String -end - -# Construtor para aceitar palavras-chave -function KineticReactionNetwork(; Coef_Cr::Union{Vector{T}, Matrix{T}}, Do_r::Union{Vector{T}, Matrix{T}}, - substances_user::Vector{String}, Af_r::T, Ef_r::T, name::String) where {T} - - Nc = length(substances_user) - Nri = Coef_Cr isa Matrix ? size(Coef_Cr, 1) : length(Coef_Cr) - - - return KineticReactionNetwork{T}(Coef_Cr, Do_r, substances_user, Nc, Nri, Af_r, Ef_r, name) -end - -export KineticReactionNetwork \ No newline at end of file diff --git a/src/Separation/Flash.jl b/src/Separation/Flash.jl deleted file mode 100644 index 9c18e06..0000000 --- a/src/Separation/Flash.jl +++ /dev/null @@ -1,138 +0,0 @@ -@component function ThreePortDrum(; substances, - non_volatiles::Vector{Bool} = falses(length(substances)), - non_condensables::Vector{Bool} = falses(length(substances)), - Vtot_::Real, - Ac::Real, - Nc::Int = length(substances), - model, - name, - ) - - - -#Constants -g = 9.81 # m/s² - - -pars = @parameters begin - Vtot = Vtot_, [description = "Total volume of the drum (m³)"] -end - - -Ports = @named begin - In = matcon(; Nc = Nc) - Outᴸ = matcon(; Nc = Nc) - Outᵍ = matcon(; Nc = Nc) - #EnergyCon = thermal_energy_connector() -end - -vars = @variables begin - (Nᵢᴸ(t))[1:Nc], [description = "Molar holdup of each component in liquid phase (mol)"] - (Nᵢᵍ(t))[1:Nc], [description = "Molar holdup of each component in gas phase (mol)", guess = [0.0, 0.0, 10.0]] - (xᵢ(t))[1:Nc], [description = "Molar fraction in liquid phase (-)", guess = [0.0, 0.0, 1.0]] - (yᵢ(t))[1:Nc], [description = "Molar fraction in gas phase (-)", guess = [0.0, 0.0, 1.0]] - (Nᵢ(t))[1:Nc], [description = "Molar holdup of each component (mol)"] - (ϕᵢᴸ(t))[1:Nc], [description = "Fugacity coefficient in liquid phase"] - (ϕᵢᵍ(t))[1:Nc], [description = "Fugacity coefficient in liquid phase"] - Vᴸ(t), [description = "Volume of the liquid phase (m³)", guess = 0.5*Vtot_, irreducible = true] - Vᵍ(t), [description = "Volume of the gas phase (m³)", guess = 0.5*Vtot_, irreducible = true] - ρᴸ(t), [description = "Molar Density of gas phase (mol/m³)", guess = molar_density(model, 1.5*101325.0, 377.0, [0.0, 0.0, 0.001], phase = :liquid)] - ρᵍ(t), [description = "Molar Density of liquid phase (mol/m³)"] - Nᴸ(t), [description = "Total molar holdup in the liquid phase (mol)", guess = 0.0001] - Nᵍ(t), [description = "Total molar holdup in the gas phase (mol)", guess = 10.0] - - T(t), [description = "Drum temperature (K)"] - P(t), [description = "Drum pressue (Pa)", guess = 1.2*101325.0] - U(t), [description = "Total internal energy holdup in the tank (L + G) (J)"] - V(t), [description = "Total volume in the tank (L + G) (m³)"] - - hᴸ_outflow(t), [description = "Outflow Enthalpy of liquid phase (J/mol)"] - hᵍ_outflow(t), [description = "Outflow Enthalpy of liquid phase (J/mol)"] - Fᴸ_outflow(t), [description = "Outlet molar flow rate of liquid phase (mol/s)"] - Fᵍ_outflow(t), [description = "Outlet molar flow rate of gas phase (mol/s)"] - Q̇(t), [description = "Heat transfer rate (J/s)"] - - - _0_Nᴸ(t), [description = "Condition for all gas phase"] - _0_Nᵍ(t), [description = "Condition for all liquid phase"] - -end - - -#Conservation equations - -#fractions_cons = [0.0 ~ sum(collect(xᵢ)) - sum(collect(yᵢ))] - -liquid_gas_holdups = [sum(collect(Nᵢ)) ~ Nᵍ + Nᴸ] - -xy_fractions = [scalarize(xᵢ.*Nᴸ .~ Nᵢᴸ)...; scalarize(yᵢ.*Nᵍ .~ Nᵢᵍ)...] - -component_holdups = [Nᵢ[i] ~ Nᵢᴸ[i] + Nᵢᵍ[i] for i in 1:Nc] - -component_balance = [D(Nᵢ[i]) ~ In.F*In.z₁[i] - Fᴸ_outflow*xᵢ[i] - Fᵍ_outflow*yᵢ[i] for i in 1:Nc] - -energy_balance = [D(U) ~ In.F*In.H - Fᴸ_outflow*hᴸ_outflow - Fᵍ_outflow*hᵍ_outflow + Q̇] - -heat_source_equation = [Q̇ ~ 9000.0] - -#Thermodynamic constraints - -internal_energy = [U + P*V ~ Nᴸ*hᴸ_outflow + Nᵍ*hᵍ_outflow] - -total_volume = [V ~ Vᴸ + Vᵍ, V ~ Vtot] - -liquid_gas_enthalpy = [hᴸ_outflow ~ enthalpy(model, P, T, xᵢ, phase = "liquid") - hᵍ_outflow ~ enthalpy(model, P, T, yᵢ, phase = "vapor")] - -liquid_gas_densities = [ρᴸ ~ molar_density(model, P, T, Nᵢᴸ, phase = "liquid") - ρᵍ ~ molar_density(model, P, T, Nᵢᵍ, phase = "vapor")] - -liquid_volume = [ρᴸ*Vᴸ ~ Nᴸ] - -gas_volume = [ρᵍ*Vᵍ ~ Nᵍ] - -fugacities_g = [ϕᵢᵍ[i] ~ fugacity_coefficient(model, P, T, yᵢ, phase = "vapor")[i] for i in 1:Nc] - -fugacities_l = [ϕᵢᴸ[i] ~ fugacity_coefficient(model, P, T, xᵢ, phase = "liquid")[i] for i in 1:Nc] - -non_smoothness = [_0_Nᵍ ~ Nᵍ/(Nᵍ + Nᴸ) - _0_Nᴸ ~ Nᵍ/(Nᵍ + Nᴸ) - 1.0 - 0.0 ~ _0_Nᵍ + _0_Nᴸ + (sum(collect(xᵢ)) - sum(collect(yᵢ))) - min(_0_Nᵍ, _0_Nᴸ, sum(collect(xᵢ)) - sum(collect(yᵢ))) - max(_0_Nᵍ, _0_Nᴸ, sum(collect(xᵢ)) - sum(collect(yᵢ))) - ] #Non-smooth formulation - - equilibrium = [ϕᵢᵍ[i].*yᵢ[i] .~ ϕᵢᴸ[i].*xᵢ[i] for i in 1:Nc] - - # Out connectors - - out_conn_L = [Outᴸ.P ~ P + g*Vᴸ/Ac - Outᴸ.T ~ T - Outᴸ.F ~ Fᴸ_outflow - Outᴸ.H ~ hᴸ_outflow - scalarize(Outᴸ.z₁ .~ xᵢ)... - scalarize(Outᴸ.z₂ .~ 0.0)... - scalarize(Outᴸ.z₃ .~ xᵢ)... - Outᴸ.α_g ~ 0.0 -] - -out_conn_g = [Outᵍ.P ~ P -Outᵍ.T ~ T -Outᵍ.F ~ Fᵍ_outflow -Outᵍ.H ~ hᵍ_outflow -scalarize(Outᵍ.z₁ .~ yᵢ)... -scalarize(Outᵍ.z₂ .~ yᵢ)... -scalarize(Outᵍ.z₃ .~ 0.0)... -Outᵍ.α_g ~ 1.0 -] - - - -eqs = [liquid_gas_holdups...; xy_fractions...; component_holdups...; component_balance...; energy_balance...; -heat_source_equation...; internal_energy...; total_volume...; liquid_gas_enthalpy...; liquid_gas_densities...; liquid_volume...; -gas_volume...; fugacities_g...; fugacities_l...; non_smoothness...; equilibrium...; out_conn_L...; out_conn_g...] - - -ODESystem([eqs...;], t, collect(Iterators.flatten(vars)), collect(Iterators.flatten(pars)); name, systems = [Ports...]) - -end - -export ThreePortDrum \ No newline at end of file diff --git a/src/Sources/MaterialSource.jl b/src/Sources/MaterialSource.jl deleted file mode 100644 index 660c4ff..0000000 --- a/src/Sources/MaterialSource.jl +++ /dev/null @@ -1,236 +0,0 @@ -@component function MaterialSource(;substances_user , - Nc = size(substances_user, 1), - model, - properties = Dict(subs => load_component_properties(subs) for subs in substances_user), - P_user, T_user, - Fₜ_user, zₜ_user, name, guesses) - #phase 1 is total, 2 is vapor, 3 is liquid - - @assert sum(zₜ_user) ≈ 1.0 "Sum of fractions is not close to one" - - Tcs = [properties[subs]["Tc"] for subs in substances_user] #Critical temperature (K) - MWs = [properties[subs]["MW"] for subs in substances_user] #Molecular weight (g/mol) - ΔH₀f = [properties[subs]["IGHF"]/10^3 for subs in substances_user] # (IG formation enthalpy) J/mol - gramsToKilograms = 10^(-3) - - vars = @variables begin - P(t), [description = "Pressure (Pa)"] - T(t), [description = "Temperature (K)"] - (zₜ(t))[1:Nc], [description = "Components molar fraction (-)"] - Fₜ(t), [description = "Total molar flow rate (mol/s)"] - Tc(t), [description = "Critical temperature (K)"] - Pc(t), [description = "Critical pressure (Pa)"] - P_buble(t), [description = "Bubble point pressure (Pa)"] - P_dew(t), [description = "Dew point pressure (Pa)"] - α_g(t), [description = "Vapor phase molar fraction"] - α_l(t), [description = "Liquid phase molar fraction"] - αᵂ_g(t), [description = "Vapor phase mass fraction"] - αᵂ_l(t), [description = "Liquid phase mass fraction"] - (Fⱼ(t))[1:3], [description = "Molar flow rate in each phase j (mol/s)"] - (Fᵂⱼ(t))[1:3], [description = "Mass flow rate in each phase j (kg/s)"] - (zⱼᵢ(t))[1:3, 1:Nc], [description = "Component molar fraction in each phase j component i (-)"] - (zᵂⱼᵢ(t))[1:3, 1:Nc], [description = "Component mass fraction in each phase j component i (-)", guess = guesses[:zᵂⱼᵢ]] - (Fⱼᵢ(t))[1:3, 1:Nc], [description = "Molar flow rate in each phase j and component i (mol/s)"] - (Fᵂⱼᵢ(t))[1:3, 1:Nc], [description = "Mass flow rate in each phase j and component i (mol/s)"] - (Hⱼ(t))[1:3], [description = "Enthalpy in each phase j at T and P (J/mol)"] - (Sⱼ(t))[1:3], [description = "Entropy in each phase j at T and P (J/mol.K)"] - (ρ(t))[1:3], [description = "Molar density in each phase j (mol/m³)"] - (ρʷ(t))[1:3], [description = "Mass density in each phase j(mol/m³)"] - (MWⱼ(t))[1:3], [description = "Molar mass of each phase j (kg/mol)"] - end - - systems = @named begin - Out = matcon(; Nc = Nc) - end - - #Connector equations - eqs_conn = [ - P ~ P_user - T ~ T_user - scalarize(zₜ .~ zₜ_user)... - Fₜ ~ Fₜ_user - - #Out stuff - Out.P ~ P - Out.T ~ T - Out.F ~ Fₜ # F is negative as it is leaving the pbject - Out.H ~ Hⱼ[1] - scalarize(Out.z₁ .~ zⱼᵢ[1, :])... - scalarize(Out.z₂ .~ zⱼᵢ[2, :])... - scalarize(Out.z₃ .~ zⱼᵢ[3, :])... - Out.α_g ~ α_g - ] - - #Global Mass and Molar balances - global_mol_balance = [ - Fⱼ[1] ~ Fₜ - Fⱼ[1] - Fⱼ[2] - Fⱼ[3] ~ 0.0 - #Fᵂⱼ[1] - Fᵂⱼ[2] - Fᵂⱼ[3] ~ 0.0 - α_l + α_g ~ 1.0 - Fⱼ[2] ~ α_g*Fⱼ[1] - - αᵂ_g + αᵂ_l ~ 1.0 - αᵂ_g ~ Fᵂⱼ[2]/Fᵂⱼ[1] - - ] - - # Mass based Equations - - molar_to_mass = [ - scalarize(Fᵂⱼ[:] .~ sum(Fᵂⱼᵢ[:, :], dims = 2))... - ] - - molar_to_mass_2 = [scalarize(Fᵂⱼᵢ[:, i] .~ (MWs[i]*gramsToKilograms)*Fⱼᵢ[:, i] ) for i in 1:Nc] - - molar_to_mass = [molar_to_mass...; molar_to_mass_2...] - - #Component Molar and Mass Balance - component_balance = [scalarize(Fⱼᵢ[:, i] .~ Fⱼ[:].*zⱼᵢ[:, i]) for i in 1:Nc] - - - #Phase check - #Tci, Pci, Vc = crit_mix(model, zₜ_user) #Critical point of the mixture (Removed as too time consuming to calculate) - Tci = sum(Tcs.*zₜ_user) - Pci = 0.0 - Pdew, Vᵍdew, Vˡdew, xdew = dew_pressure(model, T_user, zₜ_user) - Pbuble, Vᵍbuble, Vˡbuble, xbuble = bubble_pressure(model, T_user, zₜ_user) - - if T_user ≥ Tci - pc = [α_g ~ 1.0 - Tc ~ Tci - Pc ~ Pci - P_buble ~ Pbuble - P_dew ~ Pdew - Hⱼ[1] ~ enthalpy(model, P_user, T_user, zₜ_user, phase = :vapor) + sum(zₜ_user.*ΔH₀f) - Hⱼ[2] ~ enthalpy(model, P_user, T_user, zₜ_user, phase = :vapor) + sum(zₜ_user.*ΔH₀f) - Hⱼ[3] ~ 0.0 - Sⱼ[1] ~ Sⱼ[2] - Sⱼ[3] ~ 0.0 - Sⱼ[2] ~ entropy(model, P_user, T_user, zₜ_user, phase = :vapor) - scalarize(zⱼᵢ[1, :] .~ zₜ)... - scalarize(zⱼᵢ[2, :] .~ zₜ)... - scalarize(zⱼᵢ[3, :] .~ 0.0)... - scalarize(zᵂⱼᵢ[3, :] .~ 0.0)... # Mass base - scalarize(zᵂⱼᵢ[1, :] .~ zᵂⱼᵢ[2, :])... # Mass base - scalarize(Fᵂⱼᵢ[2, :] .~ Fᵂⱼ[2].*zᵂⱼᵢ[2, :])... # Mass base - ρ[1] ~ molar_density(model, P_user, T_user, zₜ_user, phase = :vapor) - ρ[2] ~ molar_density(model, P_user, T_user, zₜ_user, phase = :vapor) - ρ[3] ~ 0.0 - ρʷ[1] ~ mass_density(model, P_user, T_user, zₜ_user, phase = :vapor) - ρʷ[2] ~ mass_density(model, P_user, T_user, zₜ_user, phase = :vapor) - ρʷ[3] ~ 0.0 - MWⱼ[1] ~ sum(MWs.*zⱼᵢ[2, :]*gramsToKilograms) - MWⱼ[2] ~ sum(MWs.*zⱼᵢ[2, :]*gramsToKilograms) - MWⱼ[3] ~ 0.0 - ] - else - - if P_user < Pdew - pc = [α_g ~ 1.0 - Tc ~ Tci - Pc ~ Pci - P_buble ~ Pbuble - P_dew ~ Pdew - Hⱼ[1] ~ enthalpy(model, P_user, T_user, zₜ_user, phase = :vapor) + sum(zₜ_user.*ΔH₀f) - Hⱼ[2] ~ enthalpy(model, P_user, T_user, zₜ_user, phase = :vapor) + sum(zₜ_user.*ΔH₀f) - Hⱼ[3] ~ 0.0 - Sⱼ[1] ~ Sⱼ[2] - Sⱼ[2] ~ entropy(model, P_user, T_user, zₜ_user, phase = :vapor) - Sⱼ[3] ~ 0.0 - scalarize(zⱼᵢ[1, :] .~ zₜ)... - scalarize(zⱼᵢ[2, :] .~ zₜ)... - scalarize(zⱼᵢ[3, :] .~ 0.0)... - scalarize(zᵂⱼᵢ[3, :] .~ 0.0)... # Mass base - scalarize(zᵂⱼᵢ[1, :] .~ zᵂⱼᵢ[2, :])... # Mass base - scalarize(Fᵂⱼᵢ[2, :] .~ Fᵂⱼ[2].*zᵂⱼᵢ[2, :])... # Mass base - ρ[1] ~ molar_density(model, P_user, T_user, zₜ_user, phase = :vapor) - ρ[2] ~ molar_density(model, P_user, T_user, zₜ_user, phase = :vapor) - ρ[3] ~ 0.0 - ρʷ[1] ~ mass_density(model, P_user, T_user, zₜ_user, phase = :vapor) - ρʷ[2] ~ mass_density(model, P_user, T_user, zₜ_user, phase = :vapor) - ρʷ[3] ~ 0.0 - MWⱼ[1] ~ sum(MWs.*zⱼᵢ[2, :]*gramsToKilograms) - MWⱼ[2] ~ sum(MWs.*zⱼᵢ[2, :]*gramsToKilograms) - MWⱼ[3] ~ 0.0 - - ] - - elseif P_user > Pbuble - pc = [α_g ~ 0.0 - Tc ~ Tci - Pc ~ Pci - P_buble ~ Pbuble - P_dew ~ Pdew - Hⱼ[1] ~ enthalpy(model, P_user, T_user, zₜ_user, phase = :liquid) + sum(zₜ_user.*ΔH₀f) - Hⱼ[3] ~ enthalpy(model, P_user, T_user, zₜ_user, phase = :liquid) + sum(zₜ_user.*ΔH₀f) - Hⱼ[2] ~ 0.0 - Sⱼ[1] ~ entropy(model, P_user, T_user, zₜ_user, phase = :liquid) - Sⱼ[3] ~ entropy(model, P_user, T_user, zₜ_user, phase = :liquid) - Sⱼ[2] ~ 0.0 - scalarize(zⱼᵢ[1, :] .~ zₜ)... - scalarize(zⱼᵢ[3, :] .~ zₜ)... - scalarize(zⱼᵢ[2, :] .~ 0.0)... - scalarize(zᵂⱼᵢ[2, :] .~ 0.0)... # Mass base - scalarize(zᵂⱼᵢ[1, :] .- zᵂⱼᵢ[3, :] .~ 0.0)... # Mass base - scalarize(Fᵂⱼᵢ[3, :] .~ Fᵂⱼ[3].*zᵂⱼᵢ[3, :])... # Mass base - ρ[1] ~ molar_density(model, P_user, T_user, zₜ_user, phase = :liquid) - ρ[3] ~ molar_density(model, P_user, T_user, zₜ_user, phase = :liquid) - ρ[2] ~ 0.0 - ρʷ[1] ~ mass_density(model, P_user, T_user, zₜ_user, phase = :liquid) - ρʷ[3] ~ mass_density(model, P_user, T_user, zₜ_user, phase = :liquid) - ρʷ[2] ~ 0.0 - MWⱼ[1] ~ sum(scalarize(MWs.*zⱼᵢ[3, :])*gramsToKilograms) - MWⱼ[3] ~ sum(scalarize(MWs.*zⱼᵢ[3, :])*gramsToKilograms) - MWⱼ[2] ~ 0.0 - - ] - else - #flash calculation of the two phases (assuming liquid and gas only) - xᵢⱼ, nᵢⱼ, G = tp_flash(model, P_user, T_user, zₜ_user, DETPFlash(; equilibrium = :vle)) #Phase i, component j - H_l = enthalpy(model, P_user, T_user, xᵢⱼ[1, :], phase = :liquid) - H_g = enthalpy(model, P_user, T_user, xᵢⱼ[2, :], phase = :vapor) - S_l = entropy(model, P_user, T_user, xᵢⱼ[1, :], phase = :liquid) - S_g = entropy(model, P_user, T_user, xᵢⱼ[2, :], phase = :vapor) - V_l = volume(model, P_user, T_user, nᵢⱼ[1, :], phase = :liquid) - V_g = volume(model, P_user, T_user, nᵢⱼ[2, :], phase = :vapor) - - pc = [α_g ~ sum(nᵢⱼ[2, :])/(sum(nᵢⱼ[1, :]) + sum(nᵢⱼ[2, :])) # Vapor phase is the second entry - Tc ~ Tci - Pc ~ Pci - P_buble ~ Pbuble - P_dew ~ Pdew - Hⱼ[2] ~ H_g + sum(xᵢⱼ[2, :].*ΔH₀f) - Hⱼ[3] ~ H_l + sum(xᵢⱼ[1, :].*ΔH₀f) - Hⱼ[1] ~ sum(nᵢⱼ[2, :])*Hⱼ[2] + sum(nᵢⱼ[1, :])*Hⱼ[3] - Sⱼ[2] ~ S_g - Sⱼ[3] ~ S_l - Sⱼ[1] ~ sum(nᵢⱼ[2, :])*Sⱼ[2] + sum(nᵢⱼ[1, :])*Sⱼ[3] - scalarize(zⱼᵢ[1, :] .~ zₜ)... #Global phase - scalarize(zⱼᵢ[3, :] .~ xᵢⱼ[1, :])... #Liquid phase - scalarize(zⱼᵢ[2, :] .~ xᵢⱼ[2, :])... #Vapor phase - scalarize(Fᵂⱼᵢ[1, :] .~ Fᵂⱼ[1].*zᵂⱼᵢ[1, :])... # Mass base - scalarize(Fᵂⱼᵢ[2, :] .~ Fᵂⱼ[2].*zᵂⱼᵢ[2, :])... # Mass base - scalarize(Fᵂⱼᵢ[3, :] .~ Fᵂⱼ[3].*zᵂⱼᵢ[3, :])... # Mass base - ρ[1] ~ (sum(nᵢⱼ[2, :]) + sum(nᵢⱼ[1, :]))/(V_l + V_g) - ρ[2] ~ sum(nᵢⱼ[2, :])/V_g - ρ[3] ~ sum(nᵢⱼ[1, :])/V_l - ρʷ[1] ~ (sum(nᵢⱼ[2, :].*MWs)*gramsToKilograms + sum(nᵢⱼ[1, :].*MWs)*gramsToKilograms)/(V_l + V_g) - ρʷ[2] ~ sum(nᵢⱼ[2, :].*MWs)*gramsToKilograms/(V_g) - ρʷ[3] ~ sum(nᵢⱼ[1, :].*MWs)*gramsToKilograms/(V_l) - MWⱼ[1] ~ sum(MWs.*zⱼᵢ[1, :]*gramsToKilograms) - MWⱼ[2] ~ sum(MWs.*zⱼᵢ[2, :]*gramsToKilograms) - MWⱼ[3] ~ sum(MWs.*zⱼᵢ[3, :]*gramsToKilograms) - - ] - end - - end - - #phase 1 is total, 2 is vapor, 3 is liquid - eqs = [eqs_conn..., global_mol_balance..., molar_to_mass..., component_balance..., pc...] - - - ODESystem([eqs...;], t, collect(Iterators.flatten(vars)), []; name, systems) -end - -export MaterialSource \ No newline at end of file diff --git a/src/Sources/Sourceutils.jl b/src/Sources/Sourceutils.jl deleted file mode 100644 index 2638444..0000000 --- a/src/Sources/Sourceutils.jl +++ /dev/null @@ -1,45 +0,0 @@ -@connector function matcon(; Nc, name) - - vars = @variables begin - P(t), [description = "Pressure (Pa)", output = true] - T(t), [description = "Temperature (K)", output = true] - F(t), [description = "Molar Flow rate (mol/s)", output = true] - H(t), [description = "Enthalpy (J/mol)", output = true] - (z₁(t))[1:Nc], [description = "component molar fraction global (mol/mol)", output = true] - (z₂(t))[1:Nc], [description = "component molar fraction in vapor phase (mol/mol)", output = true] # - (z₃(t))[1:Nc], [description = "component molar fraction in liquid phase (mol/mol)", output = true] # - α_g(t), [description = "gas phase fraction (mol/mol)", output = true] # - end - - ODESystem(Equation[], t, collect(Iterators.flatten(vars)), []; name) - -end - - - -@connector function thermal_energy_connector(; name) - - vars = @variables begin - ϕᴱ(t), [description = "Energy flux at the interface (W/m²)", output = true] - T(t), [description = "Interface temperature (T)", output = true] - A(t), [description = "Actual heat transfer area", output = true] - end - - ODESystem(Equation[], t, collect(Iterators.flatten(vars)), []; name) - -end - -#= @connector con_ begin - - @structural_parameters begin - Nc - end - - @variables begin - z[1:Nc] - end - -end =# - - -export matcon \ No newline at end of file diff --git a/src/Valve/Valves.jl b/src/Valve/Valves.jl deleted file mode 100644 index 77f5739..0000000 --- a/src/Valve/Valves.jl +++ /dev/null @@ -1,46 +0,0 @@ -@component function LinearValve(; Nc, CV, model, name) - - pars = @parameters begin - Cᵥ = CV, [description = "Valve coefficient"] - end - - vars = @variables begin - θ(t), [description = "Valve opening (-)", guess = 1.0, irreducible = true] - T(t), [description = "Exit temperature (K)"] - P(t), [description = "Exit pressure (Pa)", guess = 101325.0] - F_outflow(t), [description = "Outlet molar flow rate of liquid phase (mol/s)"] - Q̇(t), [description = "Heat transfer rate (J/s)"] - end - - - Ports = @named begin - In = matcon(; Nc = Nc) - Out = matcon(; Nc = Nc) - #EnergyCon = thermal_energy_connector() - end - - connector_eqs = [Out.P ~ P - Out.T ~ T - scalarize(Out.z₁ .~ In.z₁)... - scalarize(Out.z₂ .~ In.z₂)... - scalarize(Out.z₃ .~ In.z₃)... - Out.α_g ~ In.α_g - P ~ 101325.0] - - heat = [Q̇ ~ 0.0] - - - SS_mass_balance = [F_outflow ~ Cᵥ*θ*(In.P - P)/√(abs(In.P - P) + eps(10.)) - D(θ) ~ 0.0 - In.T ~ Out.T - F_outflow ~ In.F - Out.F ~ F_outflow - Out.H ~ In.H] - - eqs = [SS_mass_balance...; connector_eqs...; heat...] - - ODESystem([eqs...;], t, collect(Iterators.flatten(vars)), collect(Iterators.flatten(pars)); name, systems = [Ports...]) - -end - -export LinearValve \ No newline at end of file diff --git a/src/database/benzene.json b/src/database/benzene.json deleted file mode 100644 index 1b6b995..0000000 --- a/src/database/benzene.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "SN": 125, - "name": "Benzene", - "CAS": "71-43-2", - "Tc": 562.05, - "Pc": 4895000, - "Vc": 0.256, - "Cc": 0.268, - "Tb": 353.24, - "Tm": 278.68, - "TT": 278.68, - "TP": 4764.22, - "MW": 78.114, - "LVB": 0.08941, - "AF": 0.209, - "SP": 18700, - "DM": 0, - "SH": -168070000.0, - "IGHF": 8.288E+07, - "GEF": 1.296E+08, - "AS": 269300, - "HFMP": 9866000, - "HOC": -3.136E+09, - "LiqDen": [105, 0.99938, 0.26348, 562.05, 0.27856, 0], - "VP": [101, 88.368, -6712.9, -10.022, 0.000007694, 2], - "LiqCp": [16, 111460, -1854.3, 22.399, -0.028936, 0.000028991], - "HOV": [106, 4.881E+07, 0.61066, -0.25882, 0.032238, 0.022475], - "VapCp": [16, 34010.24, -588.0978, 12.81777, -0.000197306, 5.142899E-08], - "ReidCp": [27.096833672561402,0.011274411310215792,0.00012488322852466165,-1.9738534255495787e-7,8.780072524769821e-11], - "LiqVis": [101, -24.61, 1576.5, 2.1698, -0.0000051366, 2], - "VapVis": [102, 3.1366E-08, 0.9675, 8.0285, -35.629, 0], - "LiqK": [16, 0.049539, -177.97, 0.19475, -0.0073805, 0.0000027938], - "VapK": [102, 0.0000049549, 1.4519, 154.14, 26202, 0], - "Racketparam": 0.2696, - "UniquacR": 3.1878, - "UniquacQ": 2.4, - "ChaoSeadAF": 0.213, - "ChaoSeadSP": 18736.78, - "ChaoSeadLV": 0.0894 -} \ No newline at end of file diff --git a/src/database/carbon monoxide.json b/src/database/carbon monoxide.json deleted file mode 100644 index 0d8a6de..0000000 --- a/src/database/carbon monoxide.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "SN": 5, - "name": "carbonmonoxide", - "CAS": "630-08-0", - "Tc": 132.85, - "Pc": 3494000, - "Vc": 0.0931, - "Cc": 0.292, - "Tb": 81.66, - "Tm": 68.15, - "TT": 68.15, - "TP": 15400, - "MW": 28.01, - "LVB": 0.03488, - "AF": 0.045, - "SP": 6402, - "DM": 3.74E-31, - "SH": -110530000.0, - "IGHF": -1.1053E+08, - "GEF": -1.3715E+08, - "AS": 197556, - "HFMP": 840984, - "HOC": -2.83E+08, - "LiqDen": [105, 2.2423, 0.2437, 132.93, 0.24196, 0], - "VP": [101, 42.283, -1035.1, -4.2012, 0.000062546, 2], - "LiqCp": [16, 63364, -10524, 359.6, -3.9494, 0.014624], - "HOV": [106, 8585000, 0.4921, -0.326, 0.2231, 0], - "VapCp": [16, 29100, -1979.753, 10.58274, -0.0000790406, -1.99685E-07], - "ReidCp": [32.52617776221547,-0.03253449222483362,9.827694814657128e-5,-1.080880140359921e-7,4.2819482483489174e-11], - "LiqVis": [101, -82.158, 1037.8, 14.229, -0.00028204, 2], - "VapVis": [102, 0.0000012713, 0.51494, 105.97, -231.11, 0], - "LiqK": [16, -0.23621, -3.5251, -0.55788, -0.0039362, -0.0000082725], - "VapK": [102, 0.00061581, 0.6828, 61.287, 221.32, 0], - "Racketparam": 0.2918, - "UniquacR": 1.0679, - "UniquacQ": 1.112, - "ChaoSeadAF": 0.093, - "ChaoSeadSP": 6402.36, - "ChaoSeadLV": 0.0354426 -} \ No newline at end of file diff --git a/src/database/methane.json b/src/database/methane.json deleted file mode 100644 index f4d6fff..0000000 --- a/src/database/methane.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "SN": 31, - "name": "methane", - "CAS": "74-82-8", - "Tc": 190.56, - "Pc": 4599000, - "Vc": 0.0986, - "Cc": 0.286, - "Tb": 111.66, - "Tm": 90.694, - "TT": 90.694, - "TP": 11696, - "MW": 16.043, - "LVB": 0.03554, - "AF": 0.011, - "SP": 11600, - "DM": 0, - "SH": -74520000.0, - "IGHF": -74520000.0, - "GEF": -50490000.0, - "AS": 186270, - "HFMP": 941400, - "HOC": -802620000.0, - "LiqDen": [105, 1.894, 0.23603, 191.05, 0.21974, 0], - "VP": [101, 39.98844, -1337.308, -3.580049, 0.0000320698, 2], - "LiqCp": [16, 61157, 5034.1, -48.913, -0.22998, 0.0022243], - "HOV": [106, 14418000.0, 2.3055, -5.4199, 5.658, -2.1286], - "VapCp": [16, 33151.9, -1220.001, 12.0907, -0.000384791, 9.896403E-08], - "ReidCp": [37.98046523972399,-0.07462230199792531, 0.00030189813766514414, -2.832737414004808e-7, 9.071078716405182e-11], - "LiqVis": [101, -45.328, 724.39, 6.5917, -0.00010373, 2], - "VapVis": [102, 5.3432E-07, 0.58831, 114.58, -1338.5, 0], - "LiqK": [16, 0.011567, -46.041, 0.10435, -0.012133, -0.0000051716], - "VapK": [102, 0.0000074705, 1.4432, -57.569, 587.82, 0], - "Racketparam": 0.2876, - "UniquacR": 1.1239, - "UniquacQ": 1.152, - "ChaoSeadAF": 0, - "ChaoSeadSP": 11618.44, - "ChaoSeadLV": 0.0378392 -} diff --git a/src/database/methanol.json b/src/database/methanol.json deleted file mode 100644 index cbe66d0..0000000 --- a/src/database/methanol.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "SN": 32, - "name": "methanol", - "CAS": "67-56-1", - "Tc": 512.64, - "Pc": 8097000, - "Vc": 0.118, - "Cc": 0.224, - "Tb": 337.69, - "Tm": 175.47, - "TT": 175.47, - "TP": 0.111264, - "MW": 32.042, - "LVB": 0.04073, - "AF": 0.565, - "SP": 29440, - "DM": 5.67E-30, - "SH": -74520000.0, - "IGHF": -2.0094E+08, - "GEF": -1.6232E+08, - "AS": 239880, - "HFMP": 3215000, - "HOC": -6.382E+08, - "LiqDen": [105, 1.7918, 0.23929, 512.64, 0.21078, 0], - "VP": [101, 73.40342, -6548.076, -7.409987, 5.72492E-06, 2], - "LiqCp": [16, 62799, 1254.2, -5.9906, 0.052937, -0.00004711], - "HOV": [106, 5.8058E+07, 0.87168, -0.81501, 0.1695, 0.17846], - "VapCp": [16, 36313.16, -680.4577, 11.10203, 0.000756766, -2.902645E-07], - "ReidCp": [39.19437678197436,-0.05808483585041852,0.0003501220208504329,-3.6941157412454843e-7,1.276270011886522e-10], - "LiqVis": [101, -32.996, 1981.4, 3.3666, -0.0000039246, 2], - "VapVis": [102, 3.0654E-07, 0.69658, 204.87, 24.304, 0], - "LiqK": [16, -0.056817, 13.156, -1.2214, -0.00028282, -0.0000010129], - "VapK": [102, 7.8368E-07, 1.7569, 108.12, -21101, 0], - "Racketparam": 0, - "UniquacR": 1.43, - "UniquacQ": 1.43, - "ChaoSeadAF": 0.5589, - "ChaoSeadSP": 29546.4, - "ChaoSeadLV": 0.0407027 -} diff --git a/src/database/methyloxirane.json b/src/database/methyloxirane.json deleted file mode 100644 index 9d9a7ed..0000000 --- a/src/database/methyloxirane.json +++ /dev/null @@ -1,41 +0,0 @@ - -{ - "SN": 409, - "name": "methyloxirane", - "CAS": "75-56-9", - "Tc": 482.25, - "Pc": 4920000, - "Vc": 0.186, - "Cc": 0.228, - "Tb": 307.05, - "Tm": 161.22, - "TT": 161.22, - "TP": 0.965624, - "MW": 58.0791, - "LVB": 0.0705481, - "AF": 0.268304, - "SP": 19050, - "DM": 6.7E-30, - "SH": -61100000.0, - "IGHF": -93720000, - "GEF": -25800000, - "AS": 286700, - "HFMP": 6531000, - "HOC": -1790000000, - "LiqDen": [105, 1.5769, 0.28598, 482.25, 0.29139, 0], - "VP": [101, 83.693, -5715.8, -9.522, 0.00001033, 2], - "LiqCp": [16, 78704, 274.26, 7.2963, 0.0088641, -0.0000023407], - "HOV": [106, 52413050, 1.339985, -1.496096, 0.72766, -0.151947], - "VapCp": [16, 42195, -578.73, 12.252, 0.00010777, -4.7082E-08], - "ReidCp": [34.91747774761048, -1.4935581577635826e-02, 7.56101594841365e-04, -1.0894144551347726e-06, 4.896983427747592e-10], - "LiqVis": [101, 20.905, 283.5, -5.5156, 0.000016261, 2], - "VapVis": [102, 1.1059E-07, 0.81831, 109.91, -5863.4, 0], - "LiqK": [16, 0.10066, 294.75, -5.9561, 0.019433, -0.000039547], - "VapK": [102, 0.00022671, 0.95467, 579.31, 32798, 0], - "Racketparam": 0.228, - "UniquacR": 2.2663, - "UniquacQ": 1.856, - "ChaoSeadAF": 0.268304, - "ChaoSeadSP": 19050, - "ChaoSeadLV": 0.0705481 -} diff --git a/src/database/nitrogen.json b/src/database/nitrogen.json deleted file mode 100644 index 5945719..0000000 --- a/src/database/nitrogen.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "SN": 21, - "name": "Nitrogen", - "CAS": "7727-37-9", - "Tc": 126.2, - "Pc": 3398000, - "Vc": 0.0901, - "Cc": 0.289, - "Tb": 77.35, - "Tm": 63.149, - "TT": 63.149, - "TP": 12520, - "MW": 28.014, - "LVB": 0.03484, - "AF": 0.037, - "SP": 9082, - "DM": 0, - "SH": 0.0, - "IGHF": 0, - "GEF": 0, - "AS": 191500, - "HFMP": 720000, - "HOC": 0, - "LiqDen": [105, 2.435, 0.25137, 126.27, 0.249, 0], - "VP": [101, 42.32946, -965.9771, -4.321774, 0.0000797271, 2], - "LiqCp": [16, 55135, 217.45, -0.9071, 0.05327, 0.00024166], - "HOV": [106, 2.7284E+07, 7.8021, -19.125, 19.518, -7.5428], - "VapCp": [16, 29103.63, -2305.946, 11.31935, -0.00100557, 1.706099E-07], - "ReidCp":[29.424883205644313,-0.002170074743337995, 5.820123832707267e-7,1.3053706310500584e-8,-8.231317991971705e-12], - "LiqVis": [101, 3.4358, -24.706, -2.6748, -0.000041603, 2], - "VapVis": [102, 4.6051E-07, 0.65049, 5.8019, 2822.7, 0], - "LiqK": [16, -0.21743, 10.383, -1.0631, 0.00036245, -0.000023265], - "VapK": [102, 0.0003395, 0.76921, 19.592, 293.93, 0], - "Racketparam": 0.2906, - "UniquacR": 1.0415, - "UniquacQ": 1.088, - "ChaoSeadAF": 0.045, - "ChaoSeadSP": 9081.94, - "ChaoSeadLV": 0.0346723 -} diff --git a/src/database/propyleneglycol.json b/src/database/propyleneglycol.json deleted file mode 100644 index 6315807..0000000 --- a/src/database/propyleneglycol.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "SN": 32, - "name": "propylene glycol", - "CAS": "57-55-6", - "Tc": 625.00, - "Pc": 6070.00e3, - "Vc": 0, - "Cc": 0, - "Tb": 0, - "Tm": 0, - "TT": 0, - "TP": 0, - "MW": 76.09, - "LVB": 0, - "AF": 0, - "SP": 0, - "DM": 0, - "SH": 0, - "IGHF": -4.3530E+08, - "GEF": 0, - "AS": 0, - "HFMP": 0, - "HOC": 0, - "LiqDen": 0, - "VP": 0, - "LiqCp": 0, - "HOV": 0, - "VapCp":0, - "ReidCp": [25.7415, 0.2355,0.0001578, -4.0939e-7, 2.1166e-10], - "LiqVis": 0, - "VapVis": 0, - "LiqK": 0, - "VapK": 0, - "Racketparam": 0, - "UniquacR": 0, - "UniquacQ": 0, - "ChaoSeadAF": 0, - "ChaoSeadSP": 0, - "ChaoSeadLV": 0 -} diff --git a/src/database/toluene.json b/src/database/toluene.json deleted file mode 100644 index abb89f0..0000000 --- a/src/database/toluene.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "SN": 140, - "name": "Toluene", - "CAS": "108-88-3", - "Tc": 591.75, - "Pc": 4108000, - "Vc": 0.316, - "Cc": 0.264, - "Tb": 383.79, - "Tm": 178.18, - "TT": 178.18, - "TP": 0.0475285, - "MW": 92.141, - "LVB": 0.10687, - "AF": 0.264, - "SP": 18250, - "DM": 1.2E-30, - "SH": -168070000.0, - "IGHF": 5.017E+07, - "GEF": 1.222E+08, - "AS": 320990, - "HFMP": 6636000, - "HOC": -3.734E+09, - "LiqDen": [105, 0.89799, 0.27359, 591.75, 0.30006, 0], - "VP": [101, 32.89891, -5013.81, -1.348918, -1.869928E-06, 2], - "LiqCp": [16, 28291, 48.171, 10.912, 0.0020542, 8.7875E-07], - "HOV": [106, 5.3752E+07, 0.50341, 0.24755, -0.72898, 0.37794], - "VapCp": [16, 47225, -565.85, 12.856, 0.000005535, -1.998E-08], - "ReidCp":[32.14371248178042, 0.029582857995389223, 0.0011104796272805465, -1.5513955799212127e-6, 6.393821753359841e-10], - "LiqVis": [101, -152.84, 5644.6, 22.826, -0.000040987, 2], - "VapVis": [102, 8.5581E-07, 0.49514, 307.82, 1891.6, 0], - "LiqK": [16, -0.072922, -23.153, -1.0277, -0.0017074, 3.6787E-07], - "VapK": [102, 0.000006541, 1.4227, 190.97, 21890, 0], - "Racketparam": 0.2646, - "UniquacR": 3.9228, - "UniquacQ": 2.968, - "ChaoSeadAF": 0.2591, - "ChaoSeadSP": 18245.86, - "ChaoSeadLV": 0.1068 -} \ No newline at end of file diff --git a/src/database/water.json b/src/database/water.json deleted file mode 100644 index ec25719..0000000 --- a/src/database/water.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "SN": 14, - "name": "water", - "CAS": "7732-18-5", - "Tc": 647.14, - "Pc": 22064000, - "Vc": 0.05595, - "Cc": 0.229, - "Tb": 373.15, - "Tm": 273.15, - "TT": 273.16, - "TP": 611.73, - "MW": 18.015, - "LVB": 0.01807, - "AF": 0.344, - "SP": 47860, - "DM": 6.17E-30, - "SH": 0.0, - "IGHF": -2.41814E+08, - "GEF": -2.2859E+08, - "AS": 188724, - "HFMP": 6001740, - "HOC": 0, - "LiqDen": [106, 32.51621, -3.213004, 7.92411, -7.359898, 2.703522], - "VP": [101, 74.55502, -7295.586, -7.442448, 0.0000042881, 2], - "LiqCp": [16, 75539, -22297, 136.02, -0.25622, 0.00018273], - "HOV": [106, 5.964E+07, 0.86515, -1.1134, 0.67764, -0.026925], - "VapCp": [16, 33200, -878.9001, 8.436956, 0.00207627, -6.467085E-07], - "ReidCp": [36.54206320678348,-0.03480434051958945,0.000116818199785053,-1.3003819534791665e-7,5.2547403746728466e-11], - "LiqVis": [101, -133.7, 6785.7, 18.47, -0.000014736, 2], - "VapVis": [102, 7.002327E-08, 0.934576, 195.6338, -13045.99, 0], - "LiqK": [16, -1.5697, -55.141, 0.7832, 0.0011484, -0.0000018151], - "VapK": [102, 0.0000065986, 1.3947, 59.478, -15484, 0], - "Racketparam": 0.2338, - "UniquacR": 0.92, - "UniquacQ": 1.4, - "ChaoSeadAF": 0.328, - "ChaoSeadSP": 47812.7, - "ChaoSeadLV": 0.0180674 -} diff --git a/src/utils.jl b/src/utils.jl deleted file mode 100644 index d71898d..0000000 --- a/src/utils.jl +++ /dev/null @@ -1,46 +0,0 @@ -""" - load_component_properties(filepath::String) - -Load component properties from a file. - -# Arguments -- `filepath::String`: Path to the file containing the component properties. - -# Returns -- `properties::Dict`: A dictionary containing the component properties. -""" -function load_component_properties(component_name::String) - file_path = abspath(joinpath(@__DIR__, "database/$(component_name).json")) - if isfile(file_path) - return JSON.parsefile(file_path) - else - error("Component file does not exist: $file_path") - end -end - - -function read_reidcp(substances::Vector{String}) - - _size = length(substances) - a = Vector{Float64}(undef, _size) - b = Vector{Float64}(undef, _size) - c = Vector{Float64}(undef, _size) - d = Vector{Float64}(undef, _size) - e = Vector{Float64}(undef, _size) - - data = Dict(subs => load_component_properties(subs) for subs in substances) - - for i in eachindex(substances) - reidcp = data[substances[i]]["ReidCp"] - a[i] = reidcp[1] - b[i] = reidcp[2] - c[i] = reidcp[3] - d[i] = reidcp[4] - e[i] = reidcp[5] - end - - return (a = a, b = b, c = c, d = d, e = e) -end - - -export load_component_properties, read_reidcp \ No newline at end of file diff --git a/test/Flash_test/FlashDrum.jl b/test/Flash_test/FlashDrum.jl deleted file mode 100644 index 8cc93b8..0000000 --- a/test/Flash_test/FlashDrum.jl +++ /dev/null @@ -1,98 +0,0 @@ -## Dynamic Flash Drum Operation - Ali M. Sahlodin, Harry A. J. Watson, and Paul I. Barton (2016) DOI 10.1002/aic.15378 - -using Revise -using ProcessSimulator -using ModelingToolkit, DifferentialEquations, Clapeyron -#import ModelingToolkit: get_unknowns, get_observed, get_defaults, get_eqs, scalarize -using ModelingToolkit: t_nounits as t, D_nounits as D -using ProcessSimulator: matcon -using NonlinearSolve - -substances = ["benzene", "toluene", "nitrogen"] - -idealmodel = ReidIdeal(substances) - -PR_model = PR(substances, idealmodel = idealmodel, translation = RackettTranslation) - -molar_density(PR_model, 1.5*101325.0, 400.0, 1.0*[0.5, 0.5, 0.5], phase = :vapor) - -x, n, G = Clapeyron.tp_flash(PR_model, 1.5*101325.0, 50.0, [0.0, 1.0, 1.0], DETPFlash()) - -bubble_temperature(PR_model, 1.5*101325.0, [0.0, 1.0, 1.0]) - -@named Drum_101 = ThreePortDrum(; substances = substances, - Vtot_ = 0.2, - non_condensables = [false, false, false], - non_volatiles = [false, false, false], - Ac = 0.1, - model = PR_model) - - -for eq in equations(Drum_101) - println(eq) -end - -@named source = MaterialSource(;substances_user = substances, -model = PR_model, -P_user = 1.5*101325.0, -T_user = 377.0, #Kelvin -Fₜ_user = 2.0, #mol/s -zₜ_user = [0.5, 0.5, 0.0], -guesses = Dict((:zᵂⱼᵢ) => ones(3, 3)*0.33)) - - -for eq in equations(source) - println(eq) -end - - -@named Liquid_Valve = LinearValve(; - Nc = 3, - CV = 0.141, - model = PR_model) - -@named Vapor_Valve = LinearValve(; - Nc = 3, - CV = 0.01, - model = PR_model) - - - -cons = [connect(source.Out, Drum_101.In), - -connect(Drum_101.Outᴸ, Liquid_Valve.In), - -connect(Drum_101.Outᵍ, Vapor_Valve.In)] - -check_and_block = [(Drum_101.Vᴸ <= 0.01*0.2) => [Liquid_Valve.θ ~ 0.0] - (Drum_101.Vᵍ <= 0.02*0.2) => [Vapor_Valve.θ ~ 0.0] - (Drum_101.P - Liquid_Valve.P <= 0.0) => [Liquid_Valve.θ ~ 0.0] - (Drum_101.P - Vapor_Valve.P <= 0.0) => [Vapor_Valve.θ ~ 0.0]] - - -flowsheet = ODESystem(cons, t; name = :myflowsheet, systems = [Drum_101, source, Liquid_Valve, Vapor_Valve], discrete_events = check_and_block) - -sistema = structural_simplify(flowsheet) - -u0 = [sistema.Drum_101.Nᵢ[1] => 0.0, sistema.Drum_101.Nᵢ[2] => 0.0, - sistema.Drum_101.Nᵢ[3] => 10.0, - sistema.Drum_101.T => 377.0, sistema.Liquid_Valve.θ => 0.5, sistema.Vapor_Valve.θ => 0.5] - -unknowns(sistema) -alg_equations(sistema) - -prob = ODEProblem(sistema, u0, (0.0, 0.01)) - -dt = 1e-7 -sol = solve(prob, ImplicitEuler(nlsolve = NLNewton(check_div=false, always_new=true, relax=4/10, max_iter=100)); dt, adaptive=false) -sol = solve(prob, FBDF()) - -sol[sistema.Drum_101.yᵢ] -a = sum.(sol[sistema.Drum_101.xᵢ]) - sum.(sol[sistema.Drum_101.yᵢ]) -b = sol[sistema.Drum_101._0_Nᴸ] -c = sol[sistema.Drum_101._0_Nᵍ] - -my_med(a,b,c) = a + b + c - min(a,b,c) - max(a,b,c) - -my_med(a, b, c) - diff --git a/test/Reactor_tests/CSTR_test.jl b/test/Reactor_tests/CSTR_test.jl deleted file mode 100644 index 7139fca..0000000 --- a/test/Reactor_tests/CSTR_test.jl +++ /dev/null @@ -1,59 +0,0 @@ -using ProcessSimulator -using ModelingToolkit, OrdinaryDiffEq, Clapeyron -using ModelingToolkit: t_nounits as t, D_nounits as D, scalarize -using ProcessSimulator: matcon - - -# Test for CSTR - -#---------- Source objct -substances = ["water", "methanol", "propyleneglycol","methyloxirane"] -idealmodel = ReidIdeal(substances; userlocations = read_reidcp(substances)) -PCSAFT_model = PCPSAFT(substances, idealmodel = idealmodel) - -fugacity_coefficient(PCSAFT_model, 101325.0, 340.15, [0.0, 0.33, 0.33, 0.33]) - -@named source = MaterialSource(;substances_user = substances, -model = PCSAFT_model, -P_user = 101325.0*2, T_user = 297.0, -Fₜ_user = (36.3 + 453.6 + 45.4)*1e3/3600, -zₜ_user = [0.8473, 1.0 - (0.0678 + 0.8473), 0.0, 0.0678], -guesses = Dict((:zᵂⱼᵢ) => ones(3, 4)*0.25)) - - -#----------------- CSTR object -Reaction = KineticReactionNetwork(substances_user = substances, -Af_r = 4.71e9, Ef_r = 32400*1055.6/453.6, Coef_Cr = [-1.0 0.0 1.0 -1.0], -Do_r = [0.0 0.0 0.0 1.0], name = "Propyleneglycol synthesis") - -@named R_101 = CSTR(; substances_user = substances, - phase = :liquid, - model = PCSAFT_model, - Reaction = Reaction, - Ac = 1.93, #m² - height_out_port = 0.0, #m - guesses = Dict(:Cᵢ => [57252.65, 0.0, 0.0, 0.0], :V => 1.9, - :F_out => (36.3 + 453.6 + 45.4)*1e3/3600, :Fʷ_out => (36.3 + 453.6 + 45.4)*18/3600) - ) - - -cons = [connect(source.Out, R_101.In)] - -flowsheet = ODESystem(cons, t; name = :mycon, systems = [R_101, source]) -sistema = structural_simplify(flowsheet) - -u0 = [sistema.R_101.Nᵢ[1] => 1.9*57252.65, sistema.R_101.Nᵢ[2] => 1e-10, - sistema.R_101.Nᵢ[3] => 1e-10, sistema.R_101.Nᵢ[4] => 1e-10, - sistema.R_101.T => 297.15] - -prob = ODEProblem(sistema, u0, (0.0, 2*3600.0)) -@time sol = solve(prob, QNDF(autodiff = true)) -using Plots - -plot(sol.t, sol[sistema.R_101.R[3]], label = "Rate of reaction") -plot(sol.t, sol[sistema.R_101.fᵢ], label = "fugacity") -plot(sol.t, sol[sistema.R_101.T], label = "Temperature") -plot(sol.t, sol[sistema.R_101.Cᵢ[1]], label = "water") -plot(sol.t, sol[sistema.R_101.V], label = "Volume") -plot(sol.t, sol[sistema.R_101.Cᵢ[3]], label = "propyleneglycol") -plot(sol.t, sol[sistema.R_101.Cᵢ[4]], label = "methyloxirane") diff --git a/test/Reactor_tests/Jacket_test.jl b/test/Reactor_tests/Jacket_test.jl deleted file mode 100644 index 42c0300..0000000 --- a/test/Reactor_tests/Jacket_test.jl +++ /dev/null @@ -1,48 +0,0 @@ -using ProcessSimulator -using ModelingToolkit, DifferentialEquations, Clapeyron -using NonlinearSolve -import ModelingToolkit: get_unknowns, get_observed, get_defaults, get_eqs, scalarize -using ModelingToolkit: t_nounits as t, D_nounits as D -using Test - -thermal_fluid_model = IAPWS95() -substances = ["water"] - -@named source = MaterialSource(;substances_user = substances, -model = thermal_fluid_model, -P_user = 2*101325.0, T_user = 288.7, -Fₜ_user = 126., -zₜ_user = [1.], -guesses = Dict((:zᵂⱼᵢ) => ones(3, 1)*0.25)) - -@named J_101 = Jacket(; substances_user = substances, - phase = :liquid, - thermal_fluid_model = thermal_fluid_model, - heat_transfer_coef = 914. - ) - - -@named energy_con = thermal_energy_connector() - -energy_source = [energy_con.T ~ 350., energy_con.A ~ 9.23] -energy_source_to_J_101 = [connect(energy_con, J_101.EnergyCon)] -source_to_J_101 = [connect(source.Out, J_101.In)] - -flowsheet = ODESystem([energy_source...; energy_source_to_J_101...;source_to_J_101...], t; name = :mycon, systems = [source, J_101, energy_con]) -flowsheet_ = structural_simplify(flowsheet) - - -variables = get_unknowns(flowsheet_) -u0 = [variables[1] => .5, variables[2] => 300., variables[3] => .5, variables[4] => 25.] -prob = SteadyStateProblem(flowsheet_, u0) - -sol = solve(prob, SSRootfind()) - - -for eq in unknowns(J_101) - println(eq) -end - -for eq in equations(flowsheet_) - println(eq) -end \ No newline at end of file diff --git a/test/Sources_tests/source_tests.jl b/test/Sources_tests/source_tests.jl deleted file mode 100644 index ecd9385..0000000 --- a/test/Sources_tests/source_tests.jl +++ /dev/null @@ -1,35 +0,0 @@ -using ProcessSimulator -using ModelingToolkit, DifferentialEquations, Clapeyron -import ModelingToolkit: get_unknowns, get_observed, get_defaults, get_eqs -using ModelingToolkit: t_nounits as t, D_nounits as D -using JSON -using NonlinearSolve -using Test - -substances = ["water", "methanol", "propyleneglycol","methyloxirane"] -properties = Dict(subs => load_component_properties(subs) for subs in substances) -idealmodel = ReidIdeal(substances; userlocations = read_reidcp(properties, substances)) -PCSAFT_model = PCPSAFT(substances, idealmodel = idealmodel) - -@named source = MaterialSource(; substances_user = substances, -model = PCSAFT_model, -P_user = 101325, T_user = 297.0, -Fₜ_user = (36.3 + 453.6 + 45.4)*1e3/3600, -zₜ_user = [0.8473, 1.0 - (0.0678 + 0.8473), 0.0, 0.0678], -guesses = Dict((:zᵂⱼᵢ) => ones(3, 4)/0.25)) - -#@named myDisplay = Display(; Nc = 4) - -#connections = ODESystem([connect(source.Out, myDisplay.InPort)], t, [], [] ; name = :connection) - -#system = compose(connections, [source, myDisplay]) - - -sys = structural_simplify(source) -variables = get_unknowns(sys) -u0 = [x => 0.5 for x in variables] -prob = SteadyStateProblem(sys, u0) -sol = solve(prob, SSRootfind()) -sol[source.Hⱼ] - - From c457e3b36ac78cb386df1c77b3146012b5250759 Mon Sep 17 00:00:00 2001 From: Vinicius Santana Date: Tue, 5 Nov 2024 11:36:43 +0100 Subject: [PATCH 02/13] =?UTF-8?q?Corrigido=20conflito=20de=20caminho=20dev?= =?UTF-8?q?ido=20a=20diferen=C3=A7as=20de=20mai=C3=BAsculas=20e=20min?= =?UTF-8?q?=C3=BAsculas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Reactors/CSTR.jl | 172 +++++++++---------------------------------- src/reactors/CSTR.jl | 34 --------- 2 files changed, 33 insertions(+), 173 deletions(-) delete mode 100644 src/reactors/CSTR.jl diff --git a/src/Reactors/CSTR.jl b/src/Reactors/CSTR.jl index 244cf8f..3efd85d 100644 --- a/src/Reactors/CSTR.jl +++ b/src/Reactors/CSTR.jl @@ -1,141 +1,35 @@ -@component function CSTR(; substances_user, - Nc = length(substances_user), - phase, - model, - Reaction, - Ac, - height_out_port, - name, - guesses - ) - - - -#Constants -gramsToKilograms = 10^(-3) -Rᵍ = 8.314 # J/(mol K) -Nri = Reaction.Nri - -#Properties of individual substances -properties = Dict(subs => load_component_properties(subs) for subs in substances_user) -MWs = [properties[subs]["MW"] for subs in substances_user] -ΔH₀f = [properties[subs]["IGHF"]/10^3 for subs in substances_user] # (IG formation enthalpy) J/mol - - -pars = @parameters begin -Af_r[1:Nri] = Reaction.Af_r, [description = "Arrhenius constant of each reaction at given temperature ()"] -Coef_Cr[1:Nri, 1:Nc] = Reaction.Coef_Cr, [description = "Stoichiometric coefficients of each component in each reaction (-)"] -Do_r[1:Nri, 1:Nc] = Reaction.Do_r, [description = "Forward order of the components (-) "] -Ef_r[1:Nri] = Reaction.Ef_r, [description = "Activation energy of each reaction at given temperature ()"] -A = Ac, [description = "Cross sectional area of the tank (m²)"] +@component function CSTR(ms::MaterialSource;name, i_reacts=1:length(ms.reaction), flowtype="") + # Subsystems + @named cv = TPControlVolume(ms;N_mcons=2,N_heats=1,N_works=1,phases=["liquid"],reactive=true) + + # Variables + vars = @variables begin + Q(t), [description="heat flux"] #, unit=u"J s^-1"] + end + + # Parameters + pars = @parameters begin + W=0.0, [description="work"] #, unit=u"J s^1"] + end + + # Equations + eqs = Equation[ + cv.w1.W ~ W, + cv.q1.Q ~ Q, + [cv.c2.xᵢ[i] ~ cv.xᵢ[1,i] for i in 1:ms.N_c-1]..., + cv.c2.p ~ cv.p, + cv.c2.T ~ cv.T, + # Change of moles by reaction + [cv.ΔnR[i] ~ reac.r(cv.p,cv.T,collect(cv.xᵢ[1,:]))*reac.ν[i]*cv.n for i in 1:ms.N_c, reac in ms.reaction[i_reacts]]..., + # Enthalpy of reaction + [cv.ΔHᵣ ~ reac.r(cv.p,cv.T,collect(cv.xᵢ[1,:]))*reac.Δhᵣ(cv.T)*cv.n for reac in ms.reaction[i_reacts]]..., + ] + if flowtype == "const. mass" + push!(eqs,0.0 ~ cv.c1.n * sum(ms.Mw[i]*cv.c1.xᵢ[i] for i in 1:ms.N_c) + cv.c2.n * sum(ms.Mw[i]*cv.c2.xᵢ[i] for i in 1:ms.N_c)) + elseif flowtype == "const. volume" + push!(eqs,D(cv.V) ~ 0.0) + end + + return ODESystem(eqs, t, vars, pars; name, systems=[cv]) end - -Ports = @named begin - In = matcon(; Nc = Nc) - Out = matcon(; Nc = Nc) - #EnergyCon = thermal_energy_connector() -end - -vars = @variables begin - M(t), [description = "Mass holdup in the tank (kg)"] - N(t), [description = "Total molar holdup in the tank (mol)"] - V(t), [description = "Volume holdup in the tank (m³)", guess = guesses[:V]] - (Nᵢ(t))[1:Nc], [description = "Molar holdup of each component in the tank (mol)"] - (Cᵢ(t))[1:Nc], [description = "Concentration of each component in the tank (mol/m³)", guess = guesses[:Cᵢ]] - - ρ(t), [description = "Molar Density of the fluid in the tank (mol/m³)"] - ρʷ(t), [description = "Mass Density of the fluid in the tank (kg/m³)"] - MW(t), [description = "Molecular weight of fluid in the tank (kg/kmol)"] - T(t), [description = "Temperature of vessel contents (K)"] - P_atm(t), [description = "Tank pressure (Pa)"] # Equal to inlet pressures. - H(t), [description = "Enthalpy holdup of the fluid in the tank (J)"] - - F_out(t), [description = "Outlet molar flow rate (mol/s)", guess = guesses[:F_out]] - Fʷ_out(t), [description = "Outlet molar flow rate (mol/s)", guess = guesses[:Fʷ_out]] - Q_out(t), [description = "Outlet volumetric flow rate (m³/s)"] # DoF - - - Q̇(t), [description = "Heat transfer rate (J/s)"] # Potential DoF - height(t), [description = "Liquid level in vessel measured from bottom of the tank (m)"] - P_out(t), [description = "Pressure at the outlet stream level (Pa)", guess = 101325.] - - (r(t))[1:Nri], [description = "Rate of each reaction for each component (mol/s/m³)"] - (R(t))[1:Nc], [description = "Overall reaction rate (mol/s/m³)"] - - (F_in(t)), [description = "Inlet molar flow rate (mol/s)"] # DoF through inlet stream - (T_in(t)), [description = "Inlet temperature (K)"] # DoF through inlet stream - (h_in(t)), [description = "Inlet specific enthalpy (J/mol)"] - - fᵢ(t), [description = "fugacity"] - -end - - - -#Reaction equations -reaction_rate = [r[i] ~ Af_r[i]*exp(-Ef_r[i]/(Rᵍ*T))*prod(scalarize((Cᵢ[:].^Do_r[i, :]))) for i in 1:Nri] # If there's an inert, the order is just zero, but it has to be written -overall_reaction_rate = [R[i] ~ sum(scalarize(r[:].*Coef_Cr[:, i])) for i in 1:Nc] # If there's an inert, the coefficient is just zero, but it has to be written - - -#Inlet connector variables's equations -atm_pressure = [P_atm ~ In.P] -inletenthalpy = [h_in ~ In.H] -inlettemperature_eqs = [T_in ~ In.T] -inletmolarflow_eqs = [F_in ~ In.F] - - -#Outlet connector equations: -out_conn = [Out.P ~ P_out - Out.T ~ T - Out.F ~ F_out - Out.H ~ H/N - scalarize(Out.z₁ .~ Nᵢ/N)... -] - -if phase == :liquid -out_conn_phases = [ - scalarize(Out.z₂ .~ 0.0)... - scalarize(Out.z₃ .~ Nᵢ/N)... - Out.α_g ~ 0.0] - -elseif phase == :vapor - out_conn_phases = [ - scalarize(Out.z₂ .~ Nᵢ/N)... - scalarize(Out.z₃ .~ 0.0)... - Out.α_g ~ 1.0] -end - - -#balances - -component_balance = [D(Nᵢ[i]) ~ F_in*In.z₁[i] - F_out*Nᵢ[i]/N + R[i]*V for i in 1:Nc] #Neglectable loss to vapor phase head space -energy_balance = [D(H) ~ F_in*h_in - F_out*H/N + Q̇] -jacket_energy_balance = [Q̇ ~ -2.27*4184.0*(T - 288.7)*(1.0 - exp(-8440.26/(2.27*4184)))] -mass_volume_eq = [ρʷ*V ~ M, ρ*V ~ N] -mol_holdup = [N ~ sum(collect(Nᵢ))] -mol_to_concentration = [scalarize(Nᵢ .~ Cᵢ*V)...] -height_to_volume = [height*A ~ V] -volumetricflow_to_molarflow = [Q_out ~ F_out/ρ, F_out ~ F_in] -volumetricflow_to_massflow = [Q_out ~ Fʷ_out/ρʷ] - -#Thermodynamic properties (outlet) -pressure_out = [phase == :liquid ? P_out ~ P_atm : P_out ~ P_atm] # Estimation considering static pressure (May be off as tank is agitated and not static) -density_eqs = [ρ ~ molar_density(model, P_out, T, Nᵢ, phase = "unknown")] -mass_density = [ρʷ ~ ρ*MW] -globalEnthalpy_eq = [H ~ enthalpy(model, P_out, T, Nᵢ, phase = "unknown") + sum(scalarize(ΔH₀f.*Nᵢ)), fᵢ ~ fugacity_coefficient(model, P_out, T, Nᵢ, phase = "liquid")[1]] #fᵢ ~ fugacity_coefficient(model, P_out, T, Nᵢ, "liquid")[1] -molar_mass = [MW ~ sum(scalarize(MWs.*Nᵢ))/N*gramsToKilograms] - - -eqs = [reaction_rate...; overall_reaction_rate...; atm_pressure...; inletenthalpy...; inlettemperature_eqs...; inletmolarflow_eqs...; out_conn...; -out_conn_phases...; component_balance...; energy_balance...; jacket_energy_balance...; mass_volume_eq...; mol_holdup...; mol_to_concentration...; -height_to_volume...; volumetricflow_to_molarflow...; volumetricflow_to_massflow...; -pressure_out...; density_eqs...; mass_density...; globalEnthalpy_eq...; molar_mass...] - - -ODESystem([eqs...;], t, collect(Iterators.flatten(vars)), collect(Iterators.flatten(pars)); name, systems = [Ports...]) - -end - -export CSTR - diff --git a/src/reactors/CSTR.jl b/src/reactors/CSTR.jl deleted file mode 100644 index dba50af..0000000 --- a/src/reactors/CSTR.jl +++ /dev/null @@ -1,34 +0,0 @@ -@component function CSTR(ms::MaterialSource;name, i_reacts=1:length(ms.reaction), flowtype="") - # Subsystems - @named cv = TPControlVolume(ms;N_mcons=2,N_heats=1,N_works=1,phases=["liquid"],reactive=true) - - # Variables - vars = @variables begin - Q(t), [description="heat flux"] #, unit=u"J s^-1"] - end - - # Parameters - pars = @parameters begin - W=0.0, [description="work"] #, unit=u"J s^1"] - end - - # Equations - eqs = Equation[ - cv.w1.W ~ W, - cv.q1.Q ~ Q, - [cv.c2.xᵢ[i] ~ cv.xᵢ[1,i] for i in 1:ms.N_c-1]..., - cv.c2.p ~ cv.p, - cv.c2.T ~ cv.T, - # Change of moles by reaction - [cv.ΔnR[i] ~ reac.r(cv.p,cv.T,collect(cv.xᵢ[1,:]))*reac.ν[i]*cv.n for i in 1:ms.N_c, reac in ms.reaction[i_reacts]]..., - # Enthalpy of reaction - [cv.ΔHᵣ ~ reac.r(cv.p,cv.T,collect(cv.xᵢ[1,:]))*reac.Δhᵣ(cv.T)*cv.n for reac in ms.reaction[i_reacts]]..., - ] - if flowtype == "const. mass" - push!(eqs,0.0 ~ cv.c1.n * sum(ms.Mw[i]*cv.c1.xᵢ[i] for i in 1:ms.N_c) + cv.c2.n * sum(ms.Mw[i]*cv.c2.xᵢ[i] for i in 1:ms.N_c)) - elseif flowtype == "const. volume" - push!(eqs,D(cv.V) ~ 0.0) - end - - return ODESystem(eqs, t, vars, pars; name, systems=[cv]) -end \ No newline at end of file From 453595a345132635278e729f86ff5ff36883b235 Mon Sep 17 00:00:00 2001 From: Vinicius Santana Date: Tue, 5 Nov 2024 11:37:02 +0100 Subject: [PATCH 03/13] starting dynamic flash drum --- src/separation/flashdrum.jl | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/separation/flashdrum.jl diff --git a/src/separation/flashdrum.jl b/src/separation/flashdrum.jl new file mode 100644 index 0000000..e69de29 From 921e44c1be4243e3de80ae1f98d001909eb1e465 Mon Sep 17 00:00:00 2001 From: Vinicius Santana Date: Mon, 7 Apr 2025 13:23:12 +0200 Subject: [PATCH 04/13] thermophysical properties, clapeyron extension, two phase control volume working --- ext/ClapeyronExt.jl | 187 +++++++++++++++++++++++ src/Reactors/CSTR.jl | 49 +++--- src/base/base_components.jl | 253 +++++++++++++++++++------------ src/base/materials.jl | 170 +++++++++++++++------ src/base/utils.jl | 5 +- src/separation/flashdrum.jl | 0 test/base/fixed_pTzN_boundary.jl | 176 +++++++++++++++++++++ 7 files changed, 665 insertions(+), 175 deletions(-) create mode 100644 ext/ClapeyronExt.jl delete mode 100644 src/separation/flashdrum.jl create mode 100644 test/base/fixed_pTzN_boundary.jl diff --git a/ext/ClapeyronExt.jl b/ext/ClapeyronExt.jl new file mode 100644 index 0000000..fb69eb4 --- /dev/null +++ b/ext/ClapeyronExt.jl @@ -0,0 +1,187 @@ +module ClapeyronExt + +import ProcessSimulator +import Clapeyron +import Symbolics + + +function EosBasedGuesses(EoSModel::M, p::V, T::V, z::D) where {M <: Clapeyron.ActivityModel, V <: Real, D <: AbstractArray{ <: Real}} + + ## Bubble and dew pressure + pᵇ = bubble_pressure(EoSModel, T, z)[1] + pᵈ = dew_pressure(EoSModel, T, z)[1] + pᵇᵈ = [pᵇ, pᵈ] + + Nc = length(z) + nᵢⱼ = zeros(V, Nc, 2) + x = zeros(V, Nc, 3) + ρ = zeros(V, 3) + h = zeros(V, 3) + + if p ≤ pᵇ && p ≥ pᵈ + sol = TP_flash(EoSModel, p, T, z) + nᵢⱼ .= sol[1] + x .= sol[2] + + ## Enthalpy + hₗ = enthalpy(EoSModel, p, T, x[:, 2], phase = "liquid") + hᵥ = enthalpy(EoSModel, p, T, x[:, 3], phase = "vapor") + ϕ = sum(nᵢⱼ, dims = 1)/sum(nᵢⱼ) + hₒᵥ = hₗ*ϕ[1] + hᵥ*ϕ[2] + h .= [hₒᵥ, hₗ, hᵥ] + + elseif p > pᵇ + nᵢⱼ[:, 1] .= z + x[:, 1:2] .= z + ϕ = sum(nᵢⱼ, dims = 1)/sum(nᵢⱼ) + ρ[2] = PT_molar_density(EoSModel, p, T, z, phase = "liquid") + ρ[3] = PT_molar_density(EoSModel, p, T, z, phase = "vapor") + ρ[1] = 1.0/(ϕ[1]/ρ[2] + ϕ[2]/ρ[3]) + hₗ = enthalpy(EoSModel, p, T, z, phase = :liquid) + hᵥ = enthalpy(EoSModel, p, T, z, phase = :vapor) + hₒᵥ = hₗ*ϕ[1] + hᵥ*ϕ[2] + h .= [hₒᵥ, hₗ, hᵥ] + + elseif p < pᵈ + nᵢⱼ[:, 2] .= z + x[:, [1, 3]] .= z + ϕ = sum(nᵢⱼ, dims = 1)/sum(nᵢⱼ) + ρ[2] = PT_molar_density(EoSModel, p, T, z, phase = "liquid") + ρ[3] = PT_molar_density(EoSModel, p, T, z, phase = "vapor") + ρ[1] = 1.0/(ϕ[1]/ρ[2] + ϕ[2]/ρ[3]) + hₗ = enthalpy(EoSModel, p, T, z, phase = :liquid) + hᵥ = enthalpy(EoSModel, p, T, z, phase = :vapor) + hₒᵥ = hₗ*ϕ[1] + hᵥ*ϕ[2] + h .= [hₒᵥ, hₗ, hᵥ] + end + + + return EosBasedGuesses(EoSModel, p, T, ρ, x, h, pᵇᵈ) +end + +function PT_molar_density(EoSModel::M, p, T, x; phase = "unknown") where M <: Clapeyron.EoSModel + Clapeyron.molar_density(EoSModel, p, T, x, phase = phase) +end + +#Assumes only two phases +function TP_flash(EoSModel::M, p, T, x) where M <: Clapeyron.EoSModel + + if two_phase_check(EoSModel, p, T, x) + + sol = Clapeyron.tp_flash(EoSModel, p, T, x, RRTPFlash()) + xᵢⱼ = transpose(sol[1]) |> Array + nᵢⱼ = transpose(sol[2]) |> Array + ϕ = sum(nᵢⱼ, dims = 1)/sum(nᵢⱼ) + + elseif vapor_check(EoSModel, p, T, x) + + xᵢⱼ = [ones(length(x))/length(x) x] + ϕ = [0.0, 1.0 - 1e-7] + + else + + xᵢⱼ = [x ones(length(x))/length(x)] + ϕ = [1.0 - 1e-7, 0.0] + + end + + return (ϕ, [x xᵢⱼ]) +end + +function two_phase_check(EoSModel::M, p, T, x) where M <: Clapeyron.EoSModel + istwophase = (p ≥ dewP(EoSModel, T, x) + 1e-3) && (p ≤ bubbleP(EoSModel, T, x) - 1e-3) ? true : false + return istwophase +end + +function vapor_check(EoSModel::M, p, T, x) where M <: Clapeyron.EoSModel + isvapor = (p < dewP(EoSModel, T, x)) ? true : false + return isvapor +end + +function flash_mol_fractions(EoSModel::M, p, T, x) where M <: Clapeyron.EoSModel + z = TP_flash(EoSModel, p, T, x)[2] + return z +end + +function flash_mol_fractions_liquid(EoSModel::M, p, T, x) where M <: Clapeyron.EoSModel + z = flash_mol_fractions(EoSModel, p, T, x)[:, 2] + return z +end + +function flash_mol_fractions_vapor(EoSModel::M, p, T, x) where M <: Clapeyron.EoSModel + z = flash_mol_fractions(EoSModel, p, T, x)[:, 3] + return z +end + +function flash_vaporized_fraction(EoSModel::M, p, T, x) where M <: Clapeyron.EoSModel + ϕ = TP_flash(EoSModel, p, T, x)[1] + return ϕ +end + +function bubbleP(EoSModel::M, T, x) where M <: Clapeyron.EoSModel + + if length(EoSModel.components) ≥ 2 + + Clapeyron.bubble_pressure(EoSModel, T, x)[1] + else + + Clapeyron.saturation_pressure(EoSModel, T)[1] + end +end + +function dewP(EoSModel::M, T, x) where M <: Clapeyron.EoSModel + + if length(EoSModel.components) ≥ 2 + + Clapeyron.dew_pressure(EoSModel, T, x)[1] + else + + Clapeyron.saturation_pressure(EoSModel, T)[1] + end +end + +function ρT_enthalpy(EoSModel::M, ρ, T, x) where M <: Clapeyron.EoSModel + return Clapeyron.VT_enthalpy(EoSModel, 1.0/ρ, T, x) +end + +function ρT_internal_energy(EoSModel::M, ρ, T, x) where M <: Clapeyron.EoSModel + return Clapeyron.VT_internal_energy(EoSModel, 1.0/ρ, T, x) +end + +function sat_temperature(EoSModel::M, p) where M <: Clapeyron.EoSModel + return Clapeyron.saturation_temperature(EoSModel, p) +end + +Symbolics.@register_array_symbolic TP_flash(model::Clapeyron.EoSModel, p, T, arr::AbstractVector) begin + size = (2,) + eltype = eltype(arr) +end + +Symbolics.@register_array_symbolic flash_mol_fractions_liquid(model::Clapeyron.EoSModel, p, T, arr::AbstractVector) begin + size = (length(arr), ) + eltype = eltype(arr) +end + +Symbolics.@register_array_symbolic flash_mol_fractions_vapor(model::Clapeyron.EoSModel, p, T, arr::AbstractVector) begin + size = (length(arr), ) + eltype = eltype(arr) +end + +Symbolics.@register_array_symbolic flash_vaporized_fraction(model::Clapeyron.EoSModel, p, T, arr::AbstractVector) begin + size = (2,) + eltype = eltype(arr) +end + +@register_symbolic sat_temperature(model::Clapeyron.EoSModel, p) + +@register_symbolic ρT_enthalpy(model::Clapeyron.EoSModel, ρ, T, arr::AbstractVector) + +@register_symbolic ρT_internal_energy(model::Clapeyron.EoSModel, ρ, T, arr::AbstractVector) + +@register_symbolic bubbleP(model::Clapeyron.EoSModel, T, arr::AbstractVector) + +@register_symbolic dewP(model::Clapeyron.EoSModel, T, arr::AbstractVector) + +end + + diff --git a/src/Reactors/CSTR.jl b/src/Reactors/CSTR.jl index 3efd85d..611472c 100644 --- a/src/Reactors/CSTR.jl +++ b/src/Reactors/CSTR.jl @@ -1,35 +1,38 @@ -@component function CSTR(ms::MaterialSource;name, i_reacts=1:length(ms.reaction), flowtype="") - # Subsystems - @named cv = TPControlVolume(ms;N_mcons=2,N_heats=1,N_works=1,phases=["liquid"],reactive=true) +@component function CSTR(ms::FluidMedium, Reactions::Union{AbstractReaction, Vector{AbstractReaction}}; name, N_heats::Int, N_works::Int, flowtype = "const. volume") + + # Subsystems - I think that CSTR would be an extension of the TPControlVolume instead of a composition - # Variables - vars = @variables begin - Q(t), [description="heat flux"] #, unit=u"J s^-1"] - end + N_Reactions = length(Reactions) - # Parameters - pars = @parameters begin - W=0.0, [description="work"] #, unit=u"J s^1"] - end + @named cv = TwoPortControlVolume(ms; + N_heats = N_heats, + N_works = N_works, + phases = ["liquid"], + N_SinkSource = N_Reactions) # Equations eqs = Equation[ - cv.w1.W ~ W, - cv.q1.Q ~ Q, - [cv.c2.xᵢ[i] ~ cv.xᵢ[1,i] for i in 1:ms.N_c-1]..., - cv.c2.p ~ cv.p, - cv.c2.T ~ cv.T, - # Change of moles by reaction - [cv.ΔnR[i] ~ reac.r(cv.p,cv.T,collect(cv.xᵢ[1,:]))*reac.ν[i]*cv.n for i in 1:ms.N_c, reac in ms.reaction[i_reacts]]..., - # Enthalpy of reaction - [cv.ΔHᵣ ~ reac.r(cv.p,cv.T,collect(cv.xᵢ[1,:]))*reac.Δhᵣ(cv.T)*cv.n for reac in ms.reaction[i_reacts]]..., + ## Connector equations + [c2.xᵢ[i] ~ xᵢ[1, i] for i in 1:ms.N_c - 1]..., + c2.p ~ p, + c2.T ~ T, + + # Change of moles by each reaction + [ifelse(isa(Reactions, Vector), [rᵥ[j, :] .~ Rate(Reactions[i], V*xᵢ[1, :]..., T, V) for j in 1:N_reaction]..., rᵥ[1, :] .~ Rate(Reactions, V*xᵢ[1, :]..., T, V))]..., + + ] + if flowtype == "const. mass" - push!(eqs,0.0 ~ cv.c1.n * sum(ms.Mw[i]*cv.c1.xᵢ[i] for i in 1:ms.N_c) + cv.c2.n * sum(ms.Mw[i]*cv.c2.xᵢ[i] for i in 1:ms.N_c)) + + push!(eqs, 0.0 ~ cv.c1.n * sum(ms.Mw[i]*cv.c1.xᵢ[i] for i in 1:ms.N_c) + cv.c2.n * sum(ms.Mw[i]*cv.c2.xᵢ[i] for i in 1:ms.N_c)) + elseif flowtype == "const. volume" - push!(eqs,D(cv.V) ~ 0.0) + + push!(eqs, D(V) ~ 0.0) + end - return ODESystem(eqs, t, vars, pars; name, systems=[cv]) + return extend(ODESystem(eqs, t, vars, pars; name), cv) end diff --git a/src/base/base_components.jl b/src/base/base_components.jl index 095509c..925c836 100644 --- a/src/base/base_components.jl +++ b/src/base/base_components.jl @@ -1,58 +1,52 @@ +@component function ρTz_ThermodynamicState_(;medium, name) + + pars = [] -@component function MaterialStream(ms::MaterialSource;phase="unknown",name) - @named c1 = MaterialConnector(ms) - @named c2 = MaterialConnector(ms) - mcons = [c1,c2] - vars = @variables begin - T(t), [description="temperature"] #, unit=u"K"] - ϱ(t), [description="density"] #, unit=u"mol m^-3"] - p(t), [description="pressure"] #, unit=u"Pa"] - h(t), [description="molar enthalpy"] #, unit=u"J mol^-1"] - n(t), [description="molar flow "] #, unit=u"mol s^-1"] - xᵢ(t)[1:ms.N_c], [description="mole fractions"] #, unit=u"mol mol^-1"] + ϕ(t)[1:medium.FluidConstants.nphases - 1], [description = "phase fraction"] + ρ(t)[1:medium.FluidConstants.nphases], [description = "molar density"] + z(t)[1:medium.FluidConstants.Nc, 1:medium.FluidConstants.nphases], [description = "mole fraction", irreducible = true] + p(t), [description = "pressure"] + T(t), [description = "Temperature"] end - eqs = [ - # EOS - ϱ ~ ms.molar_density(p,T,xᵢ;phase=phase), - h ~ ms.VT_enthalpy(ϱ,T,xᵢ), - 1.0 ~ sum(collect(xᵢ)), - # Connector "1" - T ~ c1.T, - ϱ ~ c1.ϱ, - p ~ c1.p, - h ~ c1.h, - n ~ c1.n, - scalarize(xᵢ .~ c1.xᵢ)..., - # Connector "2" - T ~ c2.T, - ϱ ~ c2.ϱ, - p ~ c2.p, - h ~ c2.h, - n ~ -c2.n, - scalarize(xᵢ .~ c2.xᵢ)..., + eq = [ + [ρ[j] ~ PT_molar_density(medium.EoSModel, p, T, collect(z[:, j]), phase = medium.FluidConstants.phaseNames[j]) for j in 2:medium.FluidConstants.nphases]... + ρ[1] .~ 1.0/(sum([ϕ[j - 1]/ρ[j] for j in 2:medium.FluidConstants.nphases]))... ] - return ODESystem(eqs, t, collect(Iterators.flatten(vars)), []; name, systems=mcons) + guesses = [z[i, j] => medium.Guesses.x[i, j] for i in 1:medium.FluidConstants.Nc, j in 1:medium.FluidConstants.nphases] + + ODESystem(eq, t, collect(Iterators.flatten(vars)), pars; guesses = guesses, name) + end -@connector function MaterialConnector(ms::MaterialSource;name) + +@connector function PhZConnector_(;medium, name) + vars = @variables begin - T(t), [description="temperature", output=true] #, unit=u"K"] - ϱ(t), [description="density", output=true] #, unit=u"mol m^-3"] - p(t), [description="pressure"] #, unit=u"Pa"] - h(t), [description="molar enthalpy", output=true] #, unit=u"J mol^-1"] - xᵢ(t)[1:ms.N_c], [description="mole fractions", output=true] #, unit=u"mol mol^-1"] - n(t), [description="total molar flow", connect=Flow] #, unit=u"mol s^-1"] + p(t), [description = "pressure"] + h(t)[1:medium.FluidConstants.nphases], [description = "molar enthalpy"] + z₁(t)[1:medium.FluidConstants.Nc], [description = "overall mole fraction"] + z₂(t)[1:medium.FluidConstants.Nc], [description = "dense state mole fraction"] + z₃(t)[1:medium.FluidConstants.Nc], [description = "dense mole fraction"] + ṅ(t)[1:medium.FluidConstants.nphases], [description = "molar flow"] end - return ODESystem(Equation[], t, collect(Iterators.flatten(vars)), []; name) + pars = [] + + eqs = [] # This avoids "BoundsError: attempt to access 0-element Vector{Vector{Any}} at index [0]" + eq = eqs==[] ? Equation[] : eqs + + ODESystem(eq, t, collect(Iterators.flatten(vars)), pars; name) + end +#### ------ ControlVolumes ------ + @component function HeatConnector(;name) vars = @variables begin - Q(t), [description="heat flux"] #, unit=u"J s^-1"] + Q(t), [description = "heat flux"] #, unit=u"J s^-1"] end return ODESystem(Equation[], t, vars, []; name) @@ -60,84 +54,141 @@ end @component function WorkConnector(;name) vars = @variables begin - W(t), [description="power"] #, unit=u"J s^1"] + W(t), [description = "power"] #, unit=u"J s^1"] end return ODESystem(Equation[], t, vars, []; name) end -@component function SimpleControlVolume(ms::MaterialSource; - N_mcons, - N_heats=0, - N_works=0, - name) - # Init - mcons = [MaterialConnector(ms;name=Symbol("c$i")) for i in 1:N_mcons] - works = [WorkConnector(name=Symbol("w$i")) for i in 1:N_works] - heats = [HeatConnector(name=Symbol("q$i")) for i in 1:N_heats] + +@component function TwoPortControlVolume_(;medium, name) + + + systems = @named begin + InPort = PhZConnector_(medium = medium) + OutPort = PhZConnector_(medium = medium) + ControlVolumeState = ρTz_ThermodynamicState_(medium = medium) + end vars = @variables begin - ΔH(t), [description="Enthalpy difference inlets/outlets"] #, unit=u"J/s"] - ΔE(t), [description="Added/removed heat or work"] #, unit=u"J/s"] + rᵥ(t)[1:medium.FluidConstants.Nc, 1:medium.FluidConstants.nphases], [description = "mass source or sink - volumetric basis"] + rₐ(t)[1:medium.FluidConstants.Nc, 1:medium.FluidConstants.nphases], [description = "molar source or sink - through surface in contact with other phases"] + Nᵢ(t)[1:medium.FluidConstants.Nc], [description = "molar holdup"] + nᴸⱽ(t)[1:medium.FluidConstants.nphases - 1], [description = "molar holdup"] + U(t), [description = "internal energy holdup"] + p(t), [description = "pressure"] + V(t)[1:medium.FluidConstants.nphases], [description = "volume"] + Q(t), [description = "heat flux"] + Wₛ(t), [description = "shaft work"] end - eqs = Equation[ - ΔH ~ sum([c.h*c.n for c in mcons]) - ΔE ~ (isempty(heats) ? 0.0 : sum([q.Q for q in heats])) + (isempty(works) ? 0.0 : sum([w.W for w in works])) + pars = [] + + + eqs = [ + # Energy balance - 0.0 ~ ΔH + ΔE + + D(U) ~ InPort.h[1]*InPort.ṅ[1] + OutPort.h[1]*(OutPort.ṅ[1]) + Q + Wₛ + # Mole balance - [0.0 ~ sum([c.xᵢ[j]*c.n for c in mcons]) for j in 1:ms.N_c][:] - ] - return ODESystem(eqs, t, vars, []; name, systems=[mcons...,works...,heats...]) + scalarize(D(Nᵢ) .~ InPort.ṅ[1].*InPort.z₁ + (OutPort.ṅ[2].*OutPort.z₂ + OutPort.ṅ[3].*OutPort.z₃) .+ collect(rᵥ[:, 2:end]*V[2:end]))... + + scalarize(rᵥ[:, 1] .~ sum(collect(rᵥ[:, 2:end]), dims = 2))... + + scalarize(rₐ[:, 1] .~ sum(collect(rₐ[:, 2:end]), dims = 2))... + + scalarize(Nᵢ .~ nᴸⱽ[1]*ControlVolumeState.z[:, 2] + nᴸⱽ[2]*ControlVolumeState.z[:, 3])... + + scalarize(sum(Nᵢ) ~ sum(nᴸⱽ)) + + scalarize(sum(collect(ControlVolumeState.ϕ)) ~ 1.0) + + + # Thermodynamic state equations + + scalarize(ControlVolumeState.z[:, 1] .~ Nᵢ ./ sum(collect(Nᵢ)))... + + ControlVolumeState.p ~ p + + #[ControlVolumeState.ϕ[1] ~ sum(collect(nᵢ[:, 2]), dims = 1)./sum(collect(nᵢ[:, 1]), dims = 1)]... + + + # Control Volume properties + + U ~ (OutPort.h[1] - ControlVolumeState.p/ControlVolumeState.ρ[1])*sum(collect(Nᵢ)) + + V[1] ~ sum(collect(V[2:end])) + + V[2]*ControlVolumeState.ρ[2] ~ nᴸⱽ[1] + + V[3]*ControlVolumeState.ρ[3] ~ nᴸⱽ[2] + + + # Outlet port properties + + [OutPort.h[j] ~ ρT_enthalpy(medium.EoSModel, ControlVolumeState.ρ[j], ControlVolumeState.T, collect(ControlVolumeState.z[:, j])) for j in 2:medium.FluidConstants.nphases]... + + OutPort.h[1] ~ dot(collect(OutPort.h[2:end]), collect(ControlVolumeState.ϕ)) + + OutPort.p ~ ControlVolumeState.p + + scalarize(OutPort.z₁ .~ ControlVolumeState.z[:, 1])... + scalarize(OutPort.z₂ .~ ControlVolumeState.z[:, 2])... + scalarize(OutPort.z₃ .~ ControlVolumeState.z[:, 3])... + + scalarize(ControlVolumeState.z[:, 3] ~ flash_mol_fractions_vapor(medium.EoSModel, ControlVolumeState.p, ControlVolumeState.T, collect(ControlVolumeState.z[:, 1])))... + scalarize(nᴸⱽ[1]/sum(nᴸⱽ) ~ flash_vaporized_fraction(medium.EoSModel, ControlVolumeState.p, ControlVolumeState.T, collect(ControlVolumeState.z[:, 1]))[1]) + + ControlVolumeState.ϕ[1] ~ nᴸⱽ[1]/sum(nᴸⱽ) + + ] + + guesses = [nᴸⱽ[2] => 5.0, V[2] => 5.0/medium.Guesses.ρ[2], V[3] => 5.0/medium.Guesses.ρ[3], + ] + #guesses = [nᵢ[:, 2:end] => medium.Guesses.x[:, 2:end]] + + return ODESystem(eqs, t, collect(Iterators.flatten(vars)), pars; name, guesses = guesses, systems = [systems...]) + end -@component function TPControlVolume(ms::MaterialSource; - N_mcons, - N_heats=0, - N_works=0, - phases=["unknown"], - reactive=false, - name) - # Init - N_ph = length(phases) - mcons = [MaterialConnector(ms;name=Symbol("c$i")) for i in 1:N_mcons] - works = [WorkConnector(name=Symbol("w$i")) for i in 1:N_works] - heats = [HeatConnector(name=Symbol("q$i")) for i in 1:N_heats] +@component function FixedBoundary_pTzn_(;medium, p, T, z, ṅ, name) - vars = @variables begin - T(t), [description="temperature", bounds=(0,Inf)] #, unit=u"K"] - p(t), [description="pressure", bounds=(0,Inf)] #, unit=u"Pa"] - ϱ(t)[1:N_ph], [description="density", bounds=(0,Inf)] #, unit=u"mol m^-3"] - (nᵢ(t))[1:N_ph,1:ms.N_c], [description="molar holdup", bounds=(0,Inf)] #, unit=u"mol"] - (xᵢ(t))[1:N_ph,1:ms.N_c], [description="mole fractions", bounds=(0,1)] #, unit=u"mol mol^-1"] - n(t), [description="total molar holdup", bounds=(0,Inf)] #, unit=u"mol"] - U(t), [description="internal energy"] #, unit=u"J"] - ΔH(t), [description="enthalpy difference inlets/outlets"] #, unit=u"J/s"] - ΔE(t), [description="added/removed heat or work"] #, unit=u"J/s"] - V(t), [description="volume", bounds=(0,Inf)] #, unit=u"m^3"] + systems = @named begin + OutPort = PhZConnector_(medium = medium) + ControlVolumeState = ρTz_ThermodynamicState_(medium = medium) end - reactive ? append!(vars, @variables begin - ΔnR(t)[1:ms.N_c], [description="molar holdup change by reaction"] #, unit=u"mol"]) - ΔHᵣ(t), [description="enthalpy of reaction"] #, unit=u"J/s"] - end) : nothing + + vars = [] + + pars = [] eqs = [ - ΔH ~ sum([c.h*c.n for c in mcons]), - ΔE ~ (isempty(heats) ? 0.0 : sum([q.Q for q in heats])) + (isempty(works) ? 0.0 : sum([w.W for w in works])), - # Energy balance - D(U) ~ ΔH + ΔE + (reactive ? ΔHᵣ : 0.0), - # Mole balance - [D(sum(collect(nᵢ[:,i]))) ~ sum([c.xᵢ[i]*c.n for c in mcons]) + - (reactive ? ΔnR[i] : 0.0) for i in 1:ms.N_c]..., - n ~ sum(collect([nᵢ...])), - [xᵢ[j,i] ~ nᵢ[j,i]/sum(collect(nᵢ[j,:])) for i in 1:ms.N_c, j in 1:N_ph]..., - # Thermodynamic system properties - [ϱ[j] ~ ms.molar_density(p,T,collect(xᵢ[j,:]);phase=phases[j]) for j in 1:N_ph]..., - U ~ sum([ms.VT_internal_energy(ϱ[j],T,collect(xᵢ[j,:]))*sum(collect(nᵢ[j,:])) for j in 1:N_ph]), - V ~ n / sum(collect(ϱ)), + # Port equations + OutPort.p ~ p + scalarize(OutPort.z₁ .~ z)... + scalarize(OutPort.z₂ .~ ControlVolumeState.z[:, 2])... + scalarize(OutPort.z₃ .~ ControlVolumeState.z[:, 3])... + OutPort.ṅ[1] ~ ṅ + OutPort.ṅ[2] ~ ControlVolumeState.ϕ[1]*ṅ + OutPort.ṅ[3] ~ ControlVolumeState.ϕ[2]*ṅ + [OutPort.h[i] ~ ρT_enthalpy(medium.EoSModel, ControlVolumeState.ρ[i], ControlVolumeState.T, ControlVolumeState.z[:, i]) for i in 2:medium.FluidConstants.nphases]... + OutPort.h[1] ~ dot(collect(OutPort.h[2:end]), collect(ControlVolumeState.ϕ)) + + # State equations + scalarize(ControlVolumeState.z[:, 1] .~ z)... + scalarize(ControlVolumeState.z[:, 2] .~ flash_mol_fractions_liquid(medium.EoSModel, p, T, z))... + scalarize(ControlVolumeState.z[:, 3] .~ flash_mol_fractions_vapor(medium.EoSModel, p, T, z))... + ControlVolumeState.T ~ T + OutPort.ṅ[2] + OutPort.ṅ[3] ~ OutPort.ṅ[1] + ControlVolumeState.p ~ p + scalarize(ControlVolumeState.ϕ[1] ~ flash_vaporized_fraction(medium.EoSModel, p, T, z)[1]) ] - return ODESystem(eqs, t, collect(Iterators.flatten(vars)), []; name, systems=[mcons...,works...,heats...]) -end \ No newline at end of file + return ODESystem(eqs, t, vars, collect(Iterators.flatten(pars)); name, systems = [systems...]) + + +end + + diff --git a/src/base/materials.jl b/src/base/materials.jl index 9e0b603..1d13f90 100644 --- a/src/base/materials.jl +++ b/src/base/materials.jl @@ -1,51 +1,123 @@ -abstract type AbstractMaterialSource end - -struct Reaction - ν::Vector{Float64} # Stoichiometry - r::Function # Reaction rate (defined as f(p,T,xᵢ)) in mol/s - Δhᵣ::Function # Reaction enthalpy (defined as f(T)) in J/mol - Reaction(;ν, r, Δhᵣ) = new(ν, r, Δhᵣ) -end - -struct MaterialSource <: AbstractMaterialSource - name::String # Name of the material source - components::Vector{String} # Component names - N_c::Int # Number of components - Mw::Vector{Float64} # Molar weight in kg/mol - pressure::Function # Pressure function (defined as f(ϱ,T,xᵢ;kwargs...)) in Pa - molar_density::Function # Molar density function (defined as f(p,T,xᵢ;kwargs...)) in mol/m³ - VT_internal_energy::Function # Internal energy function (defined as f(ϱ,T,xᵢ;kwargs...)) in J/mol - VT_enthalpy::Function # Enthalpy function (defined as f(ϱ,T,xᵢ;kwargs...)) in J/mol - VT_entropy::Function # Entropy function (defined as f(ϱ,T,xᵢ;kwargs...)) in J/(mol K) - tp_flash::Function # Flash function (defined as f(p,T,xᵢ;kwargs...)) - reaction::Vector{Reaction} # Reaction struct -end - -function MaterialSource(components::Union{String,Vector{String}}; kwargs...) - components = components isa String ? [components] : components - - # Check for mandatory keyword arguments - mandatory = [:Mw, :molar_density, :VT_enthalpy] - [haskey(kwargs, k) || throw(ArgumentError("Missing keyword argument $k")) for k in mandatory] - - N_c = length(components) - length(kwargs[:Mw]) == N_c || throw(ArgumentError("Length of Mw must be equal to the number of components")) - name = haskey(kwargs, :name) ? kwargs[:name] : join(components, "_") - - f_NA(field) = error("Function $field not defined in MaterialSource") - - MaterialSource( - name, - components, - N_c, - kwargs[:Mw] isa Number ? [kwargs[:Mw]] : kwargs[:Mw], - get(kwargs, :pressure, (a,T,n;kws...) -> f_NA(:pressure)), - kwargs[:molar_density], - get(kwargs, :VT_internal_energy, (a,T,n;kws...) -> f_NA(:VT_internal_energy)), - kwargs[:VT_enthalpy], - get(kwargs, :VT_entropy, (a,T,n;kws...) -> f_NA(:VT_entropy)), - get(kwargs, :tp_flash, (a,T,n;kws...) -> f_NA(:tp_flash)), - get(kwargs, :reactions, Reaction[]) - ) +abstract type AbstractFluidMedium end + +abstract type AbstractEoSBased <: AbstractFluidMedium end + +abstract type AbstractSinkSource end + +abstract type AbstractReaction <: AbstractSinkSource end + +const R = 8.31446261815324 # J/(mol K) + +struct PowerLawReaction{T <: Real, Arr <: AbstractArray{T}} <: AbstractReaction + ν::Arr # Stoichiometry + n::Arr # Reaction order + A::T # Arrhenius constant + Eₐ::T # Activation energy end + +function Rate(SinkSource::PowerLawReaction, cᵢ, T, V) + A, Eₐ, n, ν = SinkSource.A, SinkSource.Eₐ, SinkSource.n, SinkSource.ν + return A * exp(-Eₐ / (R * T)) * prod(ν[i]*cᵢ[i]^n[i] for i in eachindex(cᵢ))*V +end + +struct LDFAdsorption{K <: AbstractArray} <: AbstractSinkSource + k::K # Mass transfer coefficient in 1/s. +end + +struct BasicFluidConstants{S <: Union{AbstractString, Nothing}, M <: Union{Real, AbstractVector{<: Real}}, N <: Int, V <: Union{Nothing, AbstractVector{<: AbstractString}}} + iupacName::S # "Complete IUPAC name (or common name, if non-existent)"; + casRegistryNumber::S # "Chemical abstracts sequencing number (if it exists)"; + chemicalFormula::S # "Chemical formula"; + structureFormula::S # "Chemical structure formula"; + molarMass::M # "Molar mass"; + Nc::N # "Number of components"; + nphases::N # "Maximum number of phases"; + phaseNames::V # "Label of the phases" +end + +##That should be part of the PropertyModels package + + +function BasicFluidConstants(molarMass::M) where M <: AbstractVector{<: Real} + Nc = length(molarMass) + return BasicFluidConstants(nothing, nothing, nothing, nothing, molarMass, Nc, 3, ["overall", "liquid", "vapor"]) +end + +struct EosBasedGuesses{M <: Any, V <: Real, D <: AbstractArray{V}, F <: AbstractArray{V}} + EoSModel::M + p::V #Pressure + T::V #Temperature + ρ::D #Molar density per phase + x::F #Mole fraction per phase + h::D #Molar enthalpy per phase + pᵇᵈ::D #Bubble and dew pressure +end + +function EosBasedGuesses(EoSModel::M, p::V, T::V, z::D) where {M <: Any, V <: Real, D <: AbstractArray{ <: Real}} + + ## Bubble and dew pressure + pᵇ = bubble_pressure(EoSModel, T, z)[1] + pᵈ = dew_pressure(EoSModel, T, z)[1] + pᵇᵈ = [pᵇ, pᵈ] + + Nc = length(z) + nᵢⱼ = zeros(V, Nc, 2) + x = zeros(V, Nc, 3) + ρ = zeros(V, 3) + h = zeros(V, 3) + + if p ≤ pᵇ && p ≥ pᵈ + sol = TP_flash(EoSModel, p, T, z) + ϕ = sol[1] + x .= sol[2] + ρₗ = PT_molar_density(EoSModel, p, T, xᵢⱼ[:, 1], phase = "liquid") #Assumes only two phases + ρᵥ = PT_molar_density(EoSModel, p, T, xᵢⱼ[:, 2], phase = "vapor") + ρₒᵥ = 1.0/(ϕ[1]/ρₗ + ϕ[2]/ρᵥ) + ρ .= [ρₒᵥ, ρₗ, ρᵥ] + + ## Enthalpy + hₗ = ρT_enthalpy(EoSModel, ρ[2], T, x[:, 2]) + hᵥ = ρT_enthalpy(EoSModel, ρ[3], T, x[:, 3]) + hₒᵥ = hₗ*ϕ[1] + hᵥ*ϕ[2] + h .= [hₒᵥ, hₗ, hᵥ] + + elseif p > pᵇ + nᵢⱼ[:, 1] .= z + x[:, 1:2] .= z + ϕ = sum(nᵢⱼ, dims = 1)/sum(nᵢⱼ) + println(ϕ) + ρ[2] = PT_molar_density(EoSModel, p, T, z, phase = "liquid") + ρ[3] = PT_molar_density(EoSModel, p, T, z, phase = "vapor") + ρ[1] = 1.0/(ϕ[1]/ρ[2] + ϕ[2]/ρ[3]) + hₗ = ρT_enthalpy(EoSModel, ρ[2], T, z) + hᵥ = ρT_enthalpy(EoSModel, ρ[3], T, z) + hₒᵥ = hₗ*ϕ[1] + hᵥ*ϕ[2] + h .= [hₒᵥ, hₗ, hᵥ] + + elseif p < pᵈ + nᵢⱼ[:, 2] .= z + x[:, [1, 3]] .= z + ϕ = sum(nᵢⱼ, dims = 1)/sum(nᵢⱼ) + ρ[2] = PT_molar_density(EoSModel, p, T, z, phase = "liquid") + ρ[3] = PT_molar_density(EoSModel, p, T, z, phase = "vapor") + ρ[1] = 1.0/(ϕ[1]/ρ[2] + ϕ[2]/ρ[3]) + hₗ = ρT_enthalpy(EoSModel, ρ[2], T, z) + hᵥ = ρT_enthalpy(EoSModel, ρ[3], T, z) + hₒᵥ = hₗ*ϕ[1] + hᵥ*ϕ[2] + h .= [hₒᵥ, hₗ, hᵥ] + end + + + return EosBasedGuesses(EoSModel, p, T, ρ, x, h, pᵇᵈ) +end + +struct EoSBased{F <: BasicFluidConstants, E <: Any, G <: EosBasedGuesses} <: AbstractEoSBased + FluidConstants::F + EoSModel::E + Guesses::G +end + + + + diff --git a/src/base/utils.jl b/src/base/utils.jl index 676335d..1b0dd80 100644 --- a/src/base/utils.jl +++ b/src/base/utils.jl @@ -1,5 +1,5 @@ -@component function Port(ms;phase="unknown", name) - @named c = MaterialConnector(ms) +#= @component function Port(ms;phase="unknown", name) + @named c = PhXConnector(ms) vars = @variables begin T(t), [description="inlet temperature"] #, unit=u"K"] @@ -26,3 +26,4 @@ return ODESystem(eqs, t, collect(Iterators.flatten(vars)), []; name, systems=[c]) end + =# \ No newline at end of file diff --git a/src/separation/flashdrum.jl b/src/separation/flashdrum.jl deleted file mode 100644 index e69de29..0000000 diff --git a/test/base/fixed_pTzN_boundary.jl b/test/base/fixed_pTzN_boundary.jl new file mode 100644 index 0000000..2f29b62 --- /dev/null +++ b/test/base/fixed_pTzN_boundary.jl @@ -0,0 +1,176 @@ +using ModelingToolkit +using Clapeyron +using DynamicQuantities +using LinearAlgebra, DifferentialEquations +using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkit: scalarize, equations, get_unknowns +using NonlinearSolve + +#Building media +#model = Clapeyron.NRTL(["water", "methanol", "ethanol"], puremodel = PR(["water", "methanol", "ethanol"], idealmodel = ReidIdeal)) +#model = Clapeyron.PR(["water", "methanol"]) + +model = Clapeyron.PR(["water", "methanol"], idealmodel = ReidIdeal) +bubble_pressure(model, 298.15, [0.5, 0.5]) +dew_pressure(model, 298.15, [0.55, 0.5]) +#flash_cl = Clapeyron.tp_flash(model, 1.01325e5, 350.15, [0.33, 0.33, 0.34]) +#enthalpy(model, 1.01325e5, 350.15, [0.8, 0.2], phase = "liquid") + +h(p) = TP_flash(model, p, 298.15, [0.5, 0.5]) +sol = TP_flash(model, 101325.0, 350.15, [10.0, 5])[2] +rho = molar_density(model, 101325.0, 350.15, sol[:, 3]) +ρT_enthalpy(model, rho, 350.15, sol[:, 3]) +enthalpy(model, 101325.0, 350.15, sol[:, 3]) + +FiniteDifferences.central_fdm(3, 1)(h, 150_000.0) + +guess = EosBasedGuesses(model, 1.01325e5, 298.15, [0.8, 0.2]) +medium = EoSBased(BasicFluidConstants([0.01801528, 0.1801528]), model, guess) +medium.Guesses + + +#= @named InPort = PhZConnector_(medium = medium) +@named OutPort = PhZConnector_(medium = medium) +@named ControlVolumeState = ρTz_ThermodynamicState_(medium = medium) =# + +### ------ Reservoir test +@named stream = FixedBoundary_pTzn_(medium = medium, p = 1.01325e5, T = 350.15, z = [.8, .2], ṅ = 10.0) + +keys(guesses(stream)) |> collect + +simple_stream = structural_simplify(stream) +#= u0 = [simple_stream.ControlVolumeState.z[1, 1] => 0.8, simple_stream.ControlVolumeState.z[2, 1] => 0.2 +,simple_stream.ControlVolumeState.z[1, 3] => 0.5, simple_stream.ControlVolumeState.z[2, 3] => 0.5] =# + +prob = SteadyStateProblem(simple_stream, []) +@time sol = solve(prob, SSRootfind()) +sol[stream.ControlVolumeState.ϕ] + +### ------ ControlVolume + + +@component function HeatedTank_(;medium, Q̇, pressure, ṅ_out, name) + + @named CV = TwoPortControlVolume_(medium = medium) + @unpack OutPort, rₐ, rᵥ, Q, p, Wₛ = CV + + eqs = [ + Wₛ ~ 0.0 + scalarize(rᵥ[:, 2:end] .~ 0.0)... + scalarize(rₐ[:, 2:end] .~ 0.0)... + Q ~ Q̇ + p ~ pressure + OutPort.ṅ[1] ~ -ṅ_out + OutPort.ṅ[2] ~ -ṅ_out + OutPort.ṅ[3] ~ -1e-8 + + ] + + pars = [] + + vars = [] + + + return extend(ODESystem(eqs, t, vars, pars; name), CV) +end + +@named tank = HeatedTank_(medium = medium, Q̇ = 100.0, pressure = 1.01325e5, ṅ_out = 10.0) + +@component function PerfectFlowHeatedTank_(; medium, ṅ_in, ṅ_out, p, Q, name) + + systems = @named begin + pT_Boundary = FixedBoundary_pTzn_(medium = medium, p = p, T = 300.15, z = [0.8, 0.2], ṅ = ṅ_in) + tank = HeatedTank_(medium = medium, Q̇ = Q, pressure = p, ṅ_out = ṅ_out) + end + + vars = [] + + pars = [] + + connections = [ + connect(pT_Boundary.OutPort, tank.InPort) + ] + + return ODESystem(connections, t, vars, pars; name = name, systems = [systems...]) + +end + + +@named WaterTank = PerfectFlowHeatedTank_(medium = medium, ṅ_in = 10.0, ṅ_out = 10.0, p = 1.01325e5, Q = 100.0) + +sistem = structural_simplify(WaterTank) + +length(alg_equations(sistem)) + +equations(sistem) +defaults(sistem) + +u0 = [sistem.tank.Nᵢ[1] => 150.0, sistem.tank.Nᵢ[2] => 80.0, + sistem.tank.ControlVolumeState.T => 300.0] + +prob = ODEProblem(sistem, u0, (0.0, 10.0)); +prob.u0 + +sol = solve(prob, FBDF(autodiff = false)) + +plot(sol.t, sol[sistem.tank.ControlVolumeState.z[2, 2]]) + +initialization_equations(sistem) +equations(prob.f.initializationprob.f.sys) + + + + + + + + + + + + + + + + + + + +D(U) ~ InPort.h[1]*InPort.ṅ[1] + OutPort.h[1]*(OutPort.ṅ[1]) + +scalarize(D(nᵢ[:, 1]) .~ InPort.ṅ[1].*InPort.z₁ + (OutPort.ṅ[2].*OutPort.z₂ + OutPort.ṅ[3].*OutPort.z₃) .+ collect(rᵥ[:, 2:end]*V[2:end])) + +scalarize(rᵥ[:, 1] .~ sum(collect(rᵥ[:, 2:end]), dims = 2)) + +scalarize(rₐ[:, 1] .~ sum(collect(rₐ[:, 2:end]), dims = 2)) + +scalarize(nᵢ[:, 1] .~ sum(collect(nᵢ[:, 2:end]), dims = 2)) + +scalarize(sum(collect(ControlVolumeState.ϕ)) ~ 1.0) + +scalarize(ControlVolumeState.z .~ nᵢ ./ sum(collect(nᵢ), dims = 1)) + +ControlVolumeState.p ~ p + +[ControlVolumeState.ϕ[j - 1] ~ sum(collect(nᵢ[:, j]), dims = 1)./sum(collect(nᵢ[:, 1]), dims = 1) for j in 2:medium.FluidConstants.nphases] + +U ~ (OutPort.h[1] - ControlVolumeState.p/ControlVolumeState.ρ[1])*sum(collect(nᵢ[:, 1])) + +V[1] ~ sum(collect(V[2:end])) + +V[2]*ControlVolumeState.ρ[2] ~ sum(collect(nᵢ[:, 2])) + +V[3]*ControlVolumeState.ρ[3] ~ sum(collect(nᵢ[:, 3])) + + +# Outlet port properties + +[OutPort.h[j] ~ ρT_enthalpy(medium.EoSModel, ControlVolumeState.ρ[j], ControlVolumeState.T, collect(ControlVolumeState.z[:, j])) for j in 2:medium.FluidConstants.nphases] + +OutPort.h[1] ~ dot(collect(OutPort.h[2:end]), collect(ControlVolumeState.ϕ)) + +OutPort.p ~ ControlVolumeState.p + +scalarize(OutPort.z₁ .~ ControlVolumeState.z[:, 1]) +scalarize(OutPort.z₂ .~ ControlVolumeState.z[:, 2]) +scalarize(OutPort.z₃ .~ ControlVolumeState.z[:, 3]) \ No newline at end of file From bce5549bfd24e6367ab856d3568c6825938f133e Mon Sep 17 00:00:00 2001 From: Vinicius Santana Date: Thu, 1 May 2025 12:16:48 +0200 Subject: [PATCH 05/13] base components for control volume with surface and reaction --- ext/ClapeyronExt.jl | 14 ++-- src/Reactors/CSTR.jl | 104 ++++++++++++++++++++------ src/base/base_components.jl | 121 +++++++++++++++++++++++-------- src/base/materials.jl | 46 +++++++++--- test/base/fixed_pTzN_boundary.jl | 57 +++++++-------- 5 files changed, 241 insertions(+), 101 deletions(-) diff --git a/ext/ClapeyronExt.jl b/ext/ClapeyronExt.jl index fb69eb4..efb76ae 100644 --- a/ext/ClapeyronExt.jl +++ b/ext/ClapeyronExt.jl @@ -1,8 +1,8 @@ -module ClapeyronExt +#module ClapeyronExt -import ProcessSimulator -import Clapeyron -import Symbolics +#import ProcessSimulator +#import Clapeyron +#import Symbolics function EosBasedGuesses(EoSModel::M, p::V, T::V, z::D) where {M <: Clapeyron.ActivityModel, V <: Real, D <: AbstractArray{ <: Real}} @@ -76,12 +76,12 @@ function TP_flash(EoSModel::M, p, T, x) where M <: Clapeyron.EoSModel elseif vapor_check(EoSModel, p, T, x) xᵢⱼ = [ones(length(x))/length(x) x] - ϕ = [0.0, 1.0 - 1e-7] + ϕ = [0.0, 1.0] else xᵢⱼ = [x ones(length(x))/length(x)] - ϕ = [1.0 - 1e-7, 0.0] + ϕ = [1.0, 0.0] end @@ -182,6 +182,6 @@ end @register_symbolic dewP(model::Clapeyron.EoSModel, T, arr::AbstractVector) -end +#end diff --git a/src/Reactors/CSTR.jl b/src/Reactors/CSTR.jl index 611472c..000431c 100644 --- a/src/Reactors/CSTR.jl +++ b/src/Reactors/CSTR.jl @@ -1,38 +1,96 @@ -@component function CSTR(ms::FluidMedium, Reactions::Union{AbstractReaction, Vector{AbstractReaction}}; name, N_heats::Int, N_works::Int, flowtype = "const. volume") - - # Subsystems - I think that CSTR would be an extension of the TPControlVolume instead of a composition +@component function AdiabaticCSTR(;medium, reactions, P, W = 0.0, Q_rate = 0.0, n_out, flowbasis = :molar, phase = "liquid", name) - N_Reactions = length(Reactions) + @named CV = TwoPortControlVolume_(medium = medium) + @unpack Nᵢ, V, InPort, OutPort, ControlVolumeState, rₐ, rᵥ, Q, p, Wₛ = CV - @named cv = TwoPortControlVolume(ms; - N_heats = N_heats, - N_works = N_works, - phases = ["liquid"], - N_SinkSource = N_Reactions) + vars = @variables begin + cᵢ(t)[1:medium.FluidConstants.Nc], [description = "bulk concentrations"] #, unit=u"mol m^-3"] + X(t)[1:medium.FluidConstants.Nc], [description = "conversion"] #, unit=u"mol mol^-1"] + end - # Equations - eqs = Equation[ - ## Connector equations - [c2.xᵢ[i] ~ xᵢ[1, i] for i in 1:ms.N_c - 1]..., - c2.p ~ p, - c2.T ~ T, + #reactions = ifelse(length(reactions) == 1, [reactions], reactions) + + eqs = [ + scalarize(cᵢ .~ Nᵢ/V[1])... + Wₛ ~ W + scalarize(rₐ[:, 2:end] .~ 0.0)... + Q ~ Q_rate + ControlVolumeState.p ~ p + p ~ P + OutPort.ṅ[1] ~ OutPort.ṅ[2] + OutPort.ṅ[3] + ] - # Change of moles by each reaction - [ifelse(isa(Reactions, Vector), [rᵥ[j, :] .~ Rate(Reactions[i], V*xᵢ[1, :]..., T, V) for j in 1:N_reaction]..., rᵥ[1, :] .~ Rate(Reactions, V*xᵢ[1, :]..., T, V))]..., + if phase == "liquid" + eq_reaction = [ + scalarize(ControlVolumeState.z[:, 3] .~ ones(medium.FluidConstants.Nc)/medium.FluidConstants.Nc)... + ControlVolumeState.ϕ[1] ~ 1.0 + scalarize(rᵥ[:, 2] .~ sum(Rate.(reactions, cᵢ, ControlVolumeState.T)))... + scalarize(rᵥ[:, 3] .~ 0.0)... + OutPort.ṅ[2] ~ -XToMolar(n_out) + #D(V[2]) ~ 0.0 + OutPort.ṅ[3] ~ -0.0 + scalarize(X .~ (InPort.ṅ[2].*InPort.z₂ .+ OutPort.ṅ[2].*OutPort.z₂)./(InPort.ṅ[2].*InPort.z₂ .+ 1e-8))... + ] + elseif phase == "vapor" + eq_reaction = [ + ControlVolumeState.z[:, 2] ~ ones(medium.FluidConstants.Nc)/medium.FluidConstants.Nc + ControlVolumeState.ϕ[2] ~ 1.0 + scalarize(rᵥ[:, 2] .~ 0.0)... + scalarize(rᵥ[:, 3] .~ sum(Rate.(reactions, cᵢ, ControlVolumeState.T)))... + OutPort.ṅ[2] ~ -0.0 + OutPort.ṅ[3] ~ -XToMolar(n_out) + #V[3] ~ v + scalarize(X .~ (InPort.ṅ[3].*InPort.z₃ .+ OutPort.ṅ[3].*OutPort.z₃)./(InPort.ṅ[3].*InPort.z₃ .+ 1e-8))... + ] + end + pars = [] - ] + return extend(ODESystem([eqs...;eq_reaction...], t, collect(Iterators.flatten(vars)), pars; name), CV) + +end - if flowtype == "const. mass" - push!(eqs, 0.0 ~ cv.c1.n * sum(ms.Mw[i]*cv.c1.xᵢ[i] for i in 1:ms.N_c) + cv.c2.n * sum(ms.Mw[i]*cv.c2.xᵢ[i] for i in 1:ms.N_c)) - elseif flowtype == "const. volume" +reaction1 = PowerLawReaction(["water", "methanol"], [-1.0, 0.0], [2.0, 0.0], 1e-10, 10_000.0) - push!(eqs, D(V) ~ 0.0) +@component function Reactor(;medium, reactions, P, n_in, n_out, W = 0.0, Q_rate = 0.0, phase = "liquid", name) + systems = @named begin + inlet_stream = FixedBoundary_pTzn_(medium = medium, p = P, T = 300.15, z = [0.8, 0.2], ṅ = n_in) + tank = CSTR(medium = medium, reactions = reactions, P = P, n_out = n_out, W = W, Q_rate = Q_rate, phase = phase) end - return extend(ODESystem(eqs, t, vars, pars; name), cv) + vars = [] + + pars = [] + + connections = [ + connect(inlet_stream.OutPort, tank.InPort) + ] + + return ODESystem(connections, t, vars, pars; name, systems = [systems...]) + end +@named R_101 = Reactor(medium = medium, reactions = [reaction1], P = 101325.0, n_in = 10.0, n_out = 10.0, W = 0.0, Q_rate = 0.0, phase = "liquid") + +sistem = structural_simplify(R_101) + +#unknowns_ = unknowns(sistem) + +u0 = [sistem.tank.Nᵢ[1] => 60.0, sistem.tank.Nᵢ[2] => 60.0, + sistem.tank.ControlVolumeState.T => 300.0] + +guesses_ = [ +sistem.tank.V[2] => 0.2, +sistem.tank.V[3] => 0.0001, +sistem.tank.nᴸⱽ[2] => 0.0] + +prob = ODEProblem(sistem, u0, (0.0, 100.0), guesses = guesses_); + +sol = solve(prob, FBDF(autodiff = false)) + +plot(sol.t, sol[sistem.tank.X[1]]) + + diff --git a/src/base/base_components.jl b/src/base/base_components.jl index 925c836..35c342e 100644 --- a/src/base/base_components.jl +++ b/src/base/base_components.jl @@ -3,19 +3,19 @@ pars = [] vars = @variables begin - ϕ(t)[1:medium.FluidConstants.nphases - 1], [description = "phase fraction"] - ρ(t)[1:medium.FluidConstants.nphases], [description = "molar density"] - z(t)[1:medium.FluidConstants.Nc, 1:medium.FluidConstants.nphases], [description = "mole fraction", irreducible = true] + ϕ(t)[1:medium.Constants.nphases - 1], [description = "phase fraction"] + ρ(t)[1:medium.Constants.nphases], [description = "molar density"] + z(t)[1:medium.Constants.Nc, 1:medium.Constants.nphases], [description = "mole fraction", irreducible = true] p(t), [description = "pressure"] T(t), [description = "Temperature"] end eq = [ - [ρ[j] ~ PT_molar_density(medium.EoSModel, p, T, collect(z[:, j]), phase = medium.FluidConstants.phaseNames[j]) for j in 2:medium.FluidConstants.nphases]... - ρ[1] .~ 1.0/(sum([ϕ[j - 1]/ρ[j] for j in 2:medium.FluidConstants.nphases]))... + [ρ[j] ~ PT_molar_density(medium.EoSModel, p, T, collect(z[:, j]), phase = medium.Constants.phaseNames[j]) for j in 2:medium.Constants.nphases]... + ρ[1] .~ 1.0/(sum([ϕ[j - 1]/ρ[j] for j in 2:medium.Constants.nphases]))... ] - guesses = [z[i, j] => medium.Guesses.x[i, j] for i in 1:medium.FluidConstants.Nc, j in 1:medium.FluidConstants.nphases] + guesses = [z[i, j] => medium.Guesses.x[i, j] for i in 1:medium.Constants.Nc, j in 1:medium.Constants.nphases] ODESystem(eq, t, collect(Iterators.flatten(vars)), pars; guesses = guesses, name) @@ -26,11 +26,11 @@ end vars = @variables begin p(t), [description = "pressure"] - h(t)[1:medium.FluidConstants.nphases], [description = "molar enthalpy"] - z₁(t)[1:medium.FluidConstants.Nc], [description = "overall mole fraction"] - z₂(t)[1:medium.FluidConstants.Nc], [description = "dense state mole fraction"] - z₃(t)[1:medium.FluidConstants.Nc], [description = "dense mole fraction"] - ṅ(t)[1:medium.FluidConstants.nphases], [description = "molar flow"] + h(t)[1:medium.Constants.nphases], [description = "molar enthalpy"] + z₁(t)[1:medium.Constants.Nc], [description = "overall mole fraction"] + z₂(t)[1:medium.Constants.Nc], [description = "dense state mole fraction"] + z₃(t)[1:medium.Constants.Nc], [description = "dense mole fraction"] + ṅ(t)[1:medium.Constants.nphases], [description = "molar flow"] end pars = [] @@ -42,6 +42,70 @@ end end + +@connector function SurfaceConnector_(;medium, name) + + vars = @variables begin + T(t), [description = "Temperature"] + z(t)[1:medium.Constants.Nc], [description = "overall mole fraction"] + ϕₘ(t)[1:medium.Constants.Nc], [description = "Molar Flow Rate", connect = Flow] + ϕₕ(t), [description = "Heat Flow Rate", connect = Flow] + end + + pars = [] + + eqs = [] # This avoids "BoundsError: attempt to access 0-element Vector{Vector{Any}} at index [0]" + eq = eqs==[] ? Equation[] : eqs + + ODESystem(eq, t, collect(Iterators.flatten(vars)), pars; name) + +end + +@component function Surface(;medium, name) + + systems = @named begin + InPort = SurfaceConnector_(medium = medium) + OutPort = SurfaceConnector_(medium = medium) + end + + vars = @variables begin + A(t), [description = "Area"] + kₘ(t), [description = "mass transfer coefficient"] + kₕ(t), [description = "heat transfer coefficient"] + end + + eqs = [ + kₘ ~ mass_transfer_coefficient(medium) + kₕ ~ heat_transfer_coefficient(medium) + ϕₕ ~ A*kₕ*(InPort.T - OutPort.T) + scalarize(InPort.ϕₘ .~ A*kₘ*(InPort.z .- OutPort.z))... + InPort.ϕₘ + OutPort.ϕₘ ~ 0 + InPort.ϕₕ + OutPort.ϕₕ ~ 0 + ] + + ODESystem(eqs, t, collect(Iterators.flatten(vars)), pars; name, systems = [systems...]) + +end + + + +@component function AdsorptionInterface(;fluidmedium, solidmedium, name) + + systems = @named begin + SolidSurface = Surface(medium = solidmedium) + FluidSurface = Surface(medium = fluidmedium) + end + + eqs = [domain_connect(FluidSurface.OutPort, SolidSurface.InPort) + SolidSurface.InPort.z .~ loading(solidmedium.IsothermModel, FluidSurface.OutPort.z) + ] + + ODESystem(eqs, t, collect(Iterators.flatten(vars)), pars; name, systems = [systems...]) + +end + + + #### ------ ControlVolumes ------ @component function HeatConnector(;name) @@ -71,13 +135,13 @@ end end vars = @variables begin - rᵥ(t)[1:medium.FluidConstants.Nc, 1:medium.FluidConstants.nphases], [description = "mass source or sink - volumetric basis"] - rₐ(t)[1:medium.FluidConstants.Nc, 1:medium.FluidConstants.nphases], [description = "molar source or sink - through surface in contact with other phases"] - Nᵢ(t)[1:medium.FluidConstants.Nc], [description = "molar holdup"] - nᴸⱽ(t)[1:medium.FluidConstants.nphases - 1], [description = "molar holdup"] + rᵥ(t)[1:medium.Constants.Nc, 1:medium.Constants.nphases], [description = "mass source or sink - volumetric basis"] + rₐ(t)[1:medium.Constants.Nc, 1:medium.Constants.nphases], [description = "molar source or sink - through surface in contact with other phases"] + Nᵢ(t)[1:medium.Constants.Nc], [description = "molar holdup"] + nᴸⱽ(t)[1:medium.Constants.nphases - 1], [description = "molar holdup"] U(t), [description = "internal energy holdup"] p(t), [description = "pressure"] - V(t)[1:medium.FluidConstants.nphases], [description = "volume"] + V(t)[1:medium.Constants.nphases], [description = "volume"] Q(t), [description = "heat flux"] Wₛ(t), [description = "shaft work"] end @@ -110,9 +174,7 @@ end scalarize(ControlVolumeState.z[:, 1] .~ Nᵢ ./ sum(collect(Nᵢ)))... - ControlVolumeState.p ~ p - - #[ControlVolumeState.ϕ[1] ~ sum(collect(nᵢ[:, 2]), dims = 1)./sum(collect(nᵢ[:, 1]), dims = 1)]... + ControlVolumeState.ϕ[1] ~ nᴸⱽ[1]/sum(collect(nᴸⱽ)) # Control Volume properties @@ -128,28 +190,25 @@ end # Outlet port properties - [OutPort.h[j] ~ ρT_enthalpy(medium.EoSModel, ControlVolumeState.ρ[j], ControlVolumeState.T, collect(ControlVolumeState.z[:, j])) for j in 2:medium.FluidConstants.nphases]... + [OutPort.h[j] ~ ρT_enthalpy(medium.EoSModel, ControlVolumeState.ρ[j], ControlVolumeState.T, collect(ControlVolumeState.z[:, j])) for j in 2:medium.Constants.nphases]... OutPort.h[1] ~ dot(collect(OutPort.h[2:end]), collect(ControlVolumeState.ϕ)) - + OutPort.p ~ ControlVolumeState.p scalarize(OutPort.z₁ .~ ControlVolumeState.z[:, 1])... scalarize(OutPort.z₂ .~ ControlVolumeState.z[:, 2])... scalarize(OutPort.z₃ .~ ControlVolumeState.z[:, 3])... - scalarize(ControlVolumeState.z[:, 3] ~ flash_mol_fractions_vapor(medium.EoSModel, ControlVolumeState.p, ControlVolumeState.T, collect(ControlVolumeState.z[:, 1])))... - scalarize(nᴸⱽ[1]/sum(nᴸⱽ) ~ flash_vaporized_fraction(medium.EoSModel, ControlVolumeState.p, ControlVolumeState.T, collect(ControlVolumeState.z[:, 1]))[1]) - - ControlVolumeState.ϕ[1] ~ nᴸⱽ[1]/sum(nᴸⱽ) + # Specifics for the two-phase system - ] + #= ControlVolumeState.p ~ p + scalarize(ControlVolumeState.z[:, 3] ~ flash_mol_fractions_vapor(medium.EoSModel, ControlVolumeState.p, ControlVolumeState.T, collect(ControlVolumeState.z[:, 1])))... + scalarize(nᴸⱽ[1]/sum(nᴸⱽ) ~ flash_vaporized_fraction(medium.EoSModel, ControlVolumeState.p, ControlVolumeState.T, collect(ControlVolumeState.z[:, 1]))[1]) =# - guesses = [nᴸⱽ[2] => 5.0, V[2] => 5.0/medium.Guesses.ρ[2], V[3] => 5.0/medium.Guesses.ρ[3], ] - #guesses = [nᵢ[:, 2:end] => medium.Guesses.x[:, 2:end]] - return ODESystem(eqs, t, collect(Iterators.flatten(vars)), pars; name, guesses = guesses, systems = [systems...]) + return ODESystem(eqs, t, collect(Iterators.flatten(vars)), pars; name, systems = [systems...]) end @@ -173,7 +232,7 @@ end OutPort.ṅ[1] ~ ṅ OutPort.ṅ[2] ~ ControlVolumeState.ϕ[1]*ṅ OutPort.ṅ[3] ~ ControlVolumeState.ϕ[2]*ṅ - [OutPort.h[i] ~ ρT_enthalpy(medium.EoSModel, ControlVolumeState.ρ[i], ControlVolumeState.T, ControlVolumeState.z[:, i]) for i in 2:medium.FluidConstants.nphases]... + [OutPort.h[i] ~ ρT_enthalpy(medium.EoSModel, ControlVolumeState.ρ[i], ControlVolumeState.T, ControlVolumeState.z[:, i]) for i in 2:medium.Constants.nphases]... OutPort.h[1] ~ dot(collect(OutPort.h[2:end]), collect(ControlVolumeState.ϕ)) # State equations @@ -192,3 +251,5 @@ end end + + diff --git a/src/base/materials.jl b/src/base/materials.jl index 1d13f90..e35d981 100644 --- a/src/base/materials.jl +++ b/src/base/materials.jl @@ -8,19 +8,47 @@ abstract type AbstractReaction <: AbstractSinkSource end const R = 8.31446261815324 # J/(mol K) + +function XtoMolar(flowrate, medium, state, flowbasis) + if flowbasis == :molar + return flowrate + elseif flowbasis == :mass + return MasstoMolar(flowrate, medium, state) + elseif flowbasis == :volume + return VolumetoMolar(flowrate, medium, state) + else + error("Invalid flow basis: $flowbasis") + end +end + +function MasstoMolar(flowrate, medium::M, state::S) where {M <: AbstractFluidMedium} + M̄ = sum(state.z[:, 1] .* medium.FluidConstants.molarMass) #kg/mol + return flowrate / M̄ +end + +function VolumetoMolar(flowrate, medium::M, state::S) where {M <: AbstractFluidMedium} + return flowrate*state.ρ[1] +end + struct PowerLawReaction{T <: Real, Arr <: AbstractArray{T}} <: AbstractReaction - ν::Arr # Stoichiometry - n::Arr # Reaction order - A::T # Arrhenius constant - Eₐ::T # Activation energy + species::Array{String} # Reactants and Products names + ν::Arr # Stoichiometry + n::Arr # Reaction order + A::T # Arrhenius constant + Eₐ::T # Activation energy end -function Rate(SinkSource::PowerLawReaction, cᵢ, T, V) +function _Rate(SinkSource::PowerLawReaction, cᵢ, T) A, Eₐ, n, ν = SinkSource.A, SinkSource.Eₐ, SinkSource.n, SinkSource.ν - return A * exp(-Eₐ / (R * T)) * prod(ν[i]*cᵢ[i]^n[i] for i in eachindex(cᵢ))*V + r = A * exp(-Eₐ / (R * T)) * prod(cᵢ[i]^n[i] for i in eachindex(cᵢ)) + return r.*ν end +Rate(SinkSource, cᵢ, T) = _Rate(SinkSource, cᵢ, T) + +Broadcast.broadcasted(::typeof(Rate), reactions, cᵢ, T) = broadcast(_Rate, reactions, Ref(cᵢ), T) + struct LDFAdsorption{K <: AbstractArray} <: AbstractSinkSource k::K # Mass transfer coefficient in 1/s. end @@ -71,8 +99,8 @@ function EosBasedGuesses(EoSModel::M, p::V, T::V, z::D) where {M <: Any, V <: Re sol = TP_flash(EoSModel, p, T, z) ϕ = sol[1] x .= sol[2] - ρₗ = PT_molar_density(EoSModel, p, T, xᵢⱼ[:, 1], phase = "liquid") #Assumes only two phases - ρᵥ = PT_molar_density(EoSModel, p, T, xᵢⱼ[:, 2], phase = "vapor") + ρₗ = PT_molar_density(EoSModel, p, T, x[:, 1], phase = "liquid") #Assumes only two phases + ρᵥ = PT_molar_density(EoSModel, p, T, x[:, 2], phase = "vapor") ρₒᵥ = 1.0/(ϕ[1]/ρₗ + ϕ[2]/ρᵥ) ρ .= [ρₒᵥ, ρₗ, ρᵥ] @@ -113,7 +141,7 @@ function EosBasedGuesses(EoSModel::M, p::V, T::V, z::D) where {M <: Any, V <: Re end struct EoSBased{F <: BasicFluidConstants, E <: Any, G <: EosBasedGuesses} <: AbstractEoSBased - FluidConstants::F + Constants::F EoSModel::E Guesses::G end diff --git a/test/base/fixed_pTzN_boundary.jl b/test/base/fixed_pTzN_boundary.jl index 2f29b62..4454fd1 100644 --- a/test/base/fixed_pTzN_boundary.jl +++ b/test/base/fixed_pTzN_boundary.jl @@ -7,52 +7,34 @@ using ModelingToolkit: scalarize, equations, get_unknowns using NonlinearSolve #Building media -#model = Clapeyron.NRTL(["water", "methanol", "ethanol"], puremodel = PR(["water", "methanol", "ethanol"], idealmodel = ReidIdeal)) -#model = Clapeyron.PR(["water", "methanol"]) +ideal = JobackIdeal(["water", "methanol"]) +ideal.params.reference_state model = Clapeyron.PR(["water", "methanol"], idealmodel = ReidIdeal) -bubble_pressure(model, 298.15, [0.5, 0.5]) -dew_pressure(model, 298.15, [0.55, 0.5]) +bubble_pressure(model, 350.15, [0.8, 0.2]) +dew_pressure(model, 350.15, [0.8, 0.2]) #flash_cl = Clapeyron.tp_flash(model, 1.01325e5, 350.15, [0.33, 0.33, 0.34]) #enthalpy(model, 1.01325e5, 350.15, [0.8, 0.2], phase = "liquid") -h(p) = TP_flash(model, p, 298.15, [0.5, 0.5]) -sol = TP_flash(model, 101325.0, 350.15, [10.0, 5])[2] -rho = molar_density(model, 101325.0, 350.15, sol[:, 3]) -ρT_enthalpy(model, rho, 350.15, sol[:, 3]) -enthalpy(model, 101325.0, 350.15, sol[:, 3]) - -FiniteDifferences.central_fdm(3, 1)(h, 150_000.0) - -guess = EosBasedGuesses(model, 1.01325e5, 298.15, [0.8, 0.2]) +guess = EosBasedGuesses(model, 1.01325e5, 350.15, [0.8, 0.2]) medium = EoSBased(BasicFluidConstants([0.01801528, 0.1801528]), model, guess) -medium.Guesses - - -#= @named InPort = PhZConnector_(medium = medium) -@named OutPort = PhZConnector_(medium = medium) -@named ControlVolumeState = ρTz_ThermodynamicState_(medium = medium) =# +medium.Guesses.ρ ### ------ Reservoir test @named stream = FixedBoundary_pTzn_(medium = medium, p = 1.01325e5, T = 350.15, z = [.8, .2], ṅ = 10.0) -keys(guesses(stream)) |> collect - simple_stream = structural_simplify(stream) -#= u0 = [simple_stream.ControlVolumeState.z[1, 1] => 0.8, simple_stream.ControlVolumeState.z[2, 1] => 0.2 -,simple_stream.ControlVolumeState.z[1, 3] => 0.5, simple_stream.ControlVolumeState.z[2, 3] => 0.5] =# prob = SteadyStateProblem(simple_stream, []) @time sol = solve(prob, SSRootfind()) -sol[stream.ControlVolumeState.ϕ] +sol[stream.OutPort.ṅ] ### ------ ControlVolume - @component function HeatedTank_(;medium, Q̇, pressure, ṅ_out, name) @named CV = TwoPortControlVolume_(medium = medium) - @unpack OutPort, rₐ, rᵥ, Q, p, Wₛ = CV + @unpack ControlVolumeState, OutPort, rₐ, rᵥ, Q, p, Wₛ, nᴸⱽ = CV eqs = [ Wₛ ~ 0.0 @@ -63,6 +45,9 @@ sol[stream.ControlVolumeState.ϕ] OutPort.ṅ[1] ~ -ṅ_out OutPort.ṅ[2] ~ -ṅ_out OutPort.ṅ[3] ~ -1e-8 + ControlVolumeState.p ~ p + scalarize(ControlVolumeState.z[:, 3] ~ flash_mol_fractions_vapor(medium.EoSModel, ControlVolumeState.p, ControlVolumeState.T, collect(ControlVolumeState.z[:, 1])))... + scalarize(nᴸⱽ[1]/sum(nᴸⱽ) ~ flash_vaporized_fraction(medium.EoSModel, ControlVolumeState.p, ControlVolumeState.T, collect(ControlVolumeState.z[:, 1]))[1]) ] @@ -104,16 +89,24 @@ length(alg_equations(sistem)) equations(sistem) defaults(sistem) - -u0 = [sistem.tank.Nᵢ[1] => 150.0, sistem.tank.Nᵢ[2] => 80.0, +guesses_ = [ + sistem.tank.ControlVolumeState.z[1, 2] => 0.2, + sistem.tank.ControlVolumeState.z[2, 2] => 0.2, + sistem.tank.V[2] => 50.0, + sistem.tank.V[3] => 100.0, + sistem.tank.nᴸⱽ[2] => 50.0] + +u0 = [sistem.tank.Nᵢ[1] => 80.0, sistem.tank.Nᵢ[2] => 80.0, sistem.tank.ControlVolumeState.T => 300.0] -prob = ODEProblem(sistem, u0, (0.0, 10.0)); -prob.u0 +prob = ODEProblem(sistem, u0, (0.0, 100.0), guesses = guesses_); +ssprob = SteadyStateProblem(sistem, [guesses_...; u0]) + +sol = solve(prob, Rodas42(autodiff = false)) -sol = solve(prob, FBDF(autodiff = false)) +sol_ss = solve(ssprob, SSRootfind()) -plot(sol.t, sol[sistem.tank.ControlVolumeState.z[2, 2]]) +plot(sol.t, sol[sistem.tank.ControlVolumeState.T]) initialization_equations(sistem) equations(prob.f.initializationprob.f.sys) From c87645f331e42c02a793d7aa6a4a8350743adc04 Mon Sep 17 00:00:00 2001 From: Vinicius Santana Date: Thu, 30 Oct 2025 11:38:50 +0100 Subject: [PATCH 06/13] Refactor and testing --- .vscode/settings.json | 5 +- Project.toml | 25 +- ext/ClapeyronExt.jl | 187 ------- ext/EntropyScalingExt.jl | 14 + ext/ProcessSimulatorClapeyronExt.jl | 221 +++++++++ src/ProcessSimulator.jl | 25 +- src/Reactors/CSTR.jl | 185 +++++-- src/base/base_components.jl | 255 ---------- src/base/basecomponents.jl | 684 ++++++++++++++++++++++++++ src/base/materials.jl | 151 ------ src/base/print.jl | 539 ++++++++++++++++++++ src/base/solution_formatter.jl | 436 ++++++++++++++++ src/base/utils.jl | 29 -- src/fluid_handling/compressors.jl | 26 - src/fluid_handling/heat_exchangers.jl | 18 - src/pressure_drop/valve.jl | 156 ++++++ src/separation/Adsorption.jl | 112 +++++ src/separation/FlashDrum.jl | 70 +++ src/separation/distillation.jl | 19 - src/utils/FluidsProp.jl | 348 +++++++++++++ src/utils/Geometry.jl | 28 ++ src/utils/Reactions.jl | 320 ++++++++++++ src/utils/SolidsProp.jl | 109 ++++ src/utils/TransportProp.jl | 46 ++ src/utils/utils.jl | 5 + test/base/adsorbers_series.jl | 69 +++ test/base/fixed_pTzN_boundary.jl | 227 +++++---- test/base/simple_steady_state.jl | 113 ----- test/base/valve_test.jl | 55 +++ test/reactors/cstr.jl | 68 +++ test/reactors/simple_cstr.jl | 98 ---- test/runtests.jl | 7 +- test/separation/flash_drum_test.jl | 265 ++++++++++ 33 files changed, 3858 insertions(+), 1057 deletions(-) delete mode 100644 ext/ClapeyronExt.jl create mode 100644 ext/EntropyScalingExt.jl create mode 100644 ext/ProcessSimulatorClapeyronExt.jl delete mode 100644 src/base/base_components.jl create mode 100644 src/base/basecomponents.jl delete mode 100644 src/base/materials.jl create mode 100644 src/base/print.jl create mode 100644 src/base/solution_formatter.jl delete mode 100644 src/base/utils.jl delete mode 100644 src/fluid_handling/compressors.jl delete mode 100644 src/fluid_handling/heat_exchangers.jl create mode 100644 src/pressure_drop/valve.jl create mode 100644 src/separation/Adsorption.jl create mode 100644 src/separation/FlashDrum.jl delete mode 100644 src/separation/distillation.jl create mode 100644 src/utils/FluidsProp.jl create mode 100644 src/utils/Geometry.jl create mode 100644 src/utils/Reactions.jl create mode 100644 src/utils/SolidsProp.jl create mode 100644 src/utils/TransportProp.jl create mode 100644 src/utils/utils.jl create mode 100644 test/base/adsorbers_series.jl delete mode 100644 test/base/simple_steady_state.jl create mode 100644 test/base/valve_test.jl create mode 100644 test/reactors/cstr.jl delete mode 100644 test/reactors/simple_cstr.jl create mode 100644 test/separation/flash_drum_test.jl diff --git a/.vscode/settings.json b/.vscode/settings.json index 19d3de0..6a8164d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,8 @@ "github.copilot.enable": { "quarto": true, "julia": true - } + }, + "python-envs.defaultEnvManager": "ms-python.python:conda", + "python-envs.defaultPackageManager": "ms-python.python:conda", + "python-envs.pythonProjects": [] } \ No newline at end of file diff --git a/Project.toml b/Project.toml index 968921f..7e9f9ad 100644 --- a/Project.toml +++ b/Project.toml @@ -1,19 +1,34 @@ name = "ProcessSimulator" uuid = "8886c03b-4dde-4be1-b6ee-87d056f985b8" -authors = ["Chris Rackauckas", "Vinicius Viena Santana", "Sebastian Schmitt", "Andrés Riedemann", "Pierre Walker", "Avinash Subramanian"] -version = "1.0.0-DEV" +authors = ["Vinicius Viena Santana"] +version = "0.0.1-DEV" [deps] +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78" +PrettyTables = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d" +Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" +Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7" +UnicodePlots = "b8865327-cd53-5732-bb35-84acbb429228" + +[weakdeps] +Clapeyron = "7c7805af-46cc-48c9-995b-ed0ed2dc909a" + +[extensions] +ProcessSimulatorClapeyronExt = "Clapeyron" [compat] -ModelingToolkit = "9" +Clapeyron = "0.6.16" +LinearAlgebra = "1.11.0" +PrettyTables = "2.4.0" +Printf = "1.11.0" +Symbolics = "6.55.0" +UnicodePlots = "3.8.1" julia = "1.10" [extras] SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -DifferentialEquations = "0c46a032-eb83-5123-abaf-570d42b7fbaa" [targets] -test = ["Test", "SafeTestsets", "DifferentialEquations"] +test = ["Test", "SafeTestsets"] diff --git a/ext/ClapeyronExt.jl b/ext/ClapeyronExt.jl deleted file mode 100644 index efb76ae..0000000 --- a/ext/ClapeyronExt.jl +++ /dev/null @@ -1,187 +0,0 @@ -#module ClapeyronExt - -#import ProcessSimulator -#import Clapeyron -#import Symbolics - - -function EosBasedGuesses(EoSModel::M, p::V, T::V, z::D) where {M <: Clapeyron.ActivityModel, V <: Real, D <: AbstractArray{ <: Real}} - - ## Bubble and dew pressure - pᵇ = bubble_pressure(EoSModel, T, z)[1] - pᵈ = dew_pressure(EoSModel, T, z)[1] - pᵇᵈ = [pᵇ, pᵈ] - - Nc = length(z) - nᵢⱼ = zeros(V, Nc, 2) - x = zeros(V, Nc, 3) - ρ = zeros(V, 3) - h = zeros(V, 3) - - if p ≤ pᵇ && p ≥ pᵈ - sol = TP_flash(EoSModel, p, T, z) - nᵢⱼ .= sol[1] - x .= sol[2] - - ## Enthalpy - hₗ = enthalpy(EoSModel, p, T, x[:, 2], phase = "liquid") - hᵥ = enthalpy(EoSModel, p, T, x[:, 3], phase = "vapor") - ϕ = sum(nᵢⱼ, dims = 1)/sum(nᵢⱼ) - hₒᵥ = hₗ*ϕ[1] + hᵥ*ϕ[2] - h .= [hₒᵥ, hₗ, hᵥ] - - elseif p > pᵇ - nᵢⱼ[:, 1] .= z - x[:, 1:2] .= z - ϕ = sum(nᵢⱼ, dims = 1)/sum(nᵢⱼ) - ρ[2] = PT_molar_density(EoSModel, p, T, z, phase = "liquid") - ρ[3] = PT_molar_density(EoSModel, p, T, z, phase = "vapor") - ρ[1] = 1.0/(ϕ[1]/ρ[2] + ϕ[2]/ρ[3]) - hₗ = enthalpy(EoSModel, p, T, z, phase = :liquid) - hᵥ = enthalpy(EoSModel, p, T, z, phase = :vapor) - hₒᵥ = hₗ*ϕ[1] + hᵥ*ϕ[2] - h .= [hₒᵥ, hₗ, hᵥ] - - elseif p < pᵈ - nᵢⱼ[:, 2] .= z - x[:, [1, 3]] .= z - ϕ = sum(nᵢⱼ, dims = 1)/sum(nᵢⱼ) - ρ[2] = PT_molar_density(EoSModel, p, T, z, phase = "liquid") - ρ[3] = PT_molar_density(EoSModel, p, T, z, phase = "vapor") - ρ[1] = 1.0/(ϕ[1]/ρ[2] + ϕ[2]/ρ[3]) - hₗ = enthalpy(EoSModel, p, T, z, phase = :liquid) - hᵥ = enthalpy(EoSModel, p, T, z, phase = :vapor) - hₒᵥ = hₗ*ϕ[1] + hᵥ*ϕ[2] - h .= [hₒᵥ, hₗ, hᵥ] - end - - - return EosBasedGuesses(EoSModel, p, T, ρ, x, h, pᵇᵈ) -end - -function PT_molar_density(EoSModel::M, p, T, x; phase = "unknown") where M <: Clapeyron.EoSModel - Clapeyron.molar_density(EoSModel, p, T, x, phase = phase) -end - -#Assumes only two phases -function TP_flash(EoSModel::M, p, T, x) where M <: Clapeyron.EoSModel - - if two_phase_check(EoSModel, p, T, x) - - sol = Clapeyron.tp_flash(EoSModel, p, T, x, RRTPFlash()) - xᵢⱼ = transpose(sol[1]) |> Array - nᵢⱼ = transpose(sol[2]) |> Array - ϕ = sum(nᵢⱼ, dims = 1)/sum(nᵢⱼ) - - elseif vapor_check(EoSModel, p, T, x) - - xᵢⱼ = [ones(length(x))/length(x) x] - ϕ = [0.0, 1.0] - - else - - xᵢⱼ = [x ones(length(x))/length(x)] - ϕ = [1.0, 0.0] - - end - - return (ϕ, [x xᵢⱼ]) -end - -function two_phase_check(EoSModel::M, p, T, x) where M <: Clapeyron.EoSModel - istwophase = (p ≥ dewP(EoSModel, T, x) + 1e-3) && (p ≤ bubbleP(EoSModel, T, x) - 1e-3) ? true : false - return istwophase -end - -function vapor_check(EoSModel::M, p, T, x) where M <: Clapeyron.EoSModel - isvapor = (p < dewP(EoSModel, T, x)) ? true : false - return isvapor -end - -function flash_mol_fractions(EoSModel::M, p, T, x) where M <: Clapeyron.EoSModel - z = TP_flash(EoSModel, p, T, x)[2] - return z -end - -function flash_mol_fractions_liquid(EoSModel::M, p, T, x) where M <: Clapeyron.EoSModel - z = flash_mol_fractions(EoSModel, p, T, x)[:, 2] - return z -end - -function flash_mol_fractions_vapor(EoSModel::M, p, T, x) where M <: Clapeyron.EoSModel - z = flash_mol_fractions(EoSModel, p, T, x)[:, 3] - return z -end - -function flash_vaporized_fraction(EoSModel::M, p, T, x) where M <: Clapeyron.EoSModel - ϕ = TP_flash(EoSModel, p, T, x)[1] - return ϕ -end - -function bubbleP(EoSModel::M, T, x) where M <: Clapeyron.EoSModel - - if length(EoSModel.components) ≥ 2 - - Clapeyron.bubble_pressure(EoSModel, T, x)[1] - else - - Clapeyron.saturation_pressure(EoSModel, T)[1] - end -end - -function dewP(EoSModel::M, T, x) where M <: Clapeyron.EoSModel - - if length(EoSModel.components) ≥ 2 - - Clapeyron.dew_pressure(EoSModel, T, x)[1] - else - - Clapeyron.saturation_pressure(EoSModel, T)[1] - end -end - -function ρT_enthalpy(EoSModel::M, ρ, T, x) where M <: Clapeyron.EoSModel - return Clapeyron.VT_enthalpy(EoSModel, 1.0/ρ, T, x) -end - -function ρT_internal_energy(EoSModel::M, ρ, T, x) where M <: Clapeyron.EoSModel - return Clapeyron.VT_internal_energy(EoSModel, 1.0/ρ, T, x) -end - -function sat_temperature(EoSModel::M, p) where M <: Clapeyron.EoSModel - return Clapeyron.saturation_temperature(EoSModel, p) -end - -Symbolics.@register_array_symbolic TP_flash(model::Clapeyron.EoSModel, p, T, arr::AbstractVector) begin - size = (2,) - eltype = eltype(arr) -end - -Symbolics.@register_array_symbolic flash_mol_fractions_liquid(model::Clapeyron.EoSModel, p, T, arr::AbstractVector) begin - size = (length(arr), ) - eltype = eltype(arr) -end - -Symbolics.@register_array_symbolic flash_mol_fractions_vapor(model::Clapeyron.EoSModel, p, T, arr::AbstractVector) begin - size = (length(arr), ) - eltype = eltype(arr) -end - -Symbolics.@register_array_symbolic flash_vaporized_fraction(model::Clapeyron.EoSModel, p, T, arr::AbstractVector) begin - size = (2,) - eltype = eltype(arr) -end - -@register_symbolic sat_temperature(model::Clapeyron.EoSModel, p) - -@register_symbolic ρT_enthalpy(model::Clapeyron.EoSModel, ρ, T, arr::AbstractVector) - -@register_symbolic ρT_internal_energy(model::Clapeyron.EoSModel, ρ, T, arr::AbstractVector) - -@register_symbolic bubbleP(model::Clapeyron.EoSModel, T, arr::AbstractVector) - -@register_symbolic dewP(model::Clapeyron.EoSModel, T, arr::AbstractVector) - -#end - - diff --git a/ext/EntropyScalingExt.jl b/ext/EntropyScalingExt.jl new file mode 100644 index 0000000..159dd92 --- /dev/null +++ b/ext/EntropyScalingExt.jl @@ -0,0 +1,14 @@ +module EntropyScalingExt + +using EntropyScaling +using ProcessSimulator + +const PS = ProcessSimulator + + +function PS.viscosity(model::M, p, T, z) where M <: EntropyScaling.EoSModel + return EntropyScaling.viscosity(model, p, T, z) +end + + +end \ No newline at end of file diff --git a/ext/ProcessSimulatorClapeyronExt.jl b/ext/ProcessSimulatorClapeyronExt.jl new file mode 100644 index 0000000..3e51576 --- /dev/null +++ b/ext/ProcessSimulatorClapeyronExt.jl @@ -0,0 +1,221 @@ +module ProcessSimulatorClapeyronExt + +using ProcessSimulator +using Clapeyron +using Symbolics + +const PS = ProcessSimulator + +function PS.EosBasedGuesses(EoSModel::M, p::V, T::V, z::D, ::Val{:Pressure}) where {M <: Clapeyron.EoSModel, V <: Real, D <: AbstractArray{ <: Real}} + + sol = PS.TP_flash(EoSModel, p, T, z) + ϕ = sol[1] + x = sol[2] + ρ = zeros(V, 3) + h = zeros(V, 3) + + ## Enthalpy + hₗ = Clapeyron.enthalpy(EoSModel, p, T, x[:, 2], phase = "liquid") + hᵥ = Clapeyron.enthalpy(EoSModel, p, T, x[:, 3], phase = "vapor") + ρ[2] = PS.PT_molar_density(EoSModel, p, T, x[:, 2], phase = "liquid") + ρ[3] = PS.PT_molar_density(EoSModel, p, T, x[:, 3], phase = "vapor") + ρ[1] = 1.0/(ϕ[1]/ρ[2] + ϕ[2]/ρ[3]) + hₒᵥ = hₗ*ϕ[1] + hᵥ*ϕ[2] + h .= [hₒᵥ, hₗ, hᵥ] + + return PS.EosBasedGuesses(EoSModel, p, T, ρ, x, h, ϕ) +end + +function PS.EosBasedGuesses(EoSModel::M, V::K, T::K, N::D, ::Val{:Volume}) where {M <: Clapeyron.EoSModel, K <: Real, D <: AbstractArray{ <: Real}} + + # initialize properties + ρ = zeros(K, 3) + h = zeros(K, 3) + + res = Clapeyron.vt_flash(EoSModel, V, T, N) + nphases = length(res.compositions) + _x = N./sum(N) + p = pressure(res) + + if nphases ≤ 1 + v = first(res.volumes) + z_xᵢⱼ = vcat(_x, _x, _x) + h .= Clapeyron.VT_enthalpy(EoSModel, v, T, _x) + ρ .= 1.0/v + if p*v/(8.314*T) ≥ 0.5 #Very rought test for compressibility factor + ϕ = [0.0, 1.0] + else + ϕ = [1.0, 0.0] + end + else + ϕ = res.fractions/sum(res.fractions) + xᵢⱼ = mapreduce(x -> x, hcat, res.compositions) + vl = first(res.volumes) + vv = last(res.volumes) + + #Properties per phase + h[2] = Clapeyron.VT_enthalpy(EoSModel, vl, T, @view xᵢⱼ[:, 1]) + h[3] = Clapeyron.VT_enthalpy(EoSModel, vv, T, @view xᵢⱼ[:, 2]) + ρ[2] = 1.0/vl + ρ[3] = 1.0/vv + + ρ[1] = 1.0/(ϕ[1]/ρ[2] + ϕ[2]/ρ[3]) + h[1] = h[2]*ϕ[1] + h[3]*ϕ[2] + z_xᵢⱼ = hcat(_x, xᵢⱼ) + end + + return PS.EosBasedGuesses(EoSModel, p, T, ρ, z_xᵢⱼ, h, ϕ) +end + +function PS.PT_molar_density(EoSModel::M, p, T, x; phase = "unknown") where M <: Clapeyron.EoSModel + Clapeyron.molar_density(EoSModel, p, T, x, phase = phase) +end + +function PS.TP_flash(EoSModel::M, p, T, x; nonvolatiles = nothing, noncondensables = nothing) where M <: Clapeyron.EoSModel + + _x = abs.(x) + + #abs(v - vv_ideal) ≤ 1e-3 + #p*v/(8.314*T) ≥ 0.52 + + if PS.is_stable(EoSModel, p, T, _x) + + v = Clapeyron.volume(EoSModel, p, T, _x, phase = :unknown) + #vv_ideal = Clapeyron.volume(Clapeyron.idealmodel(EoSModel), p, T, _x) + + if p*v/(8.314*T) ≥ 0.5 #Very rought test for compressibility factor + xᵢⱼ = [_x _x] + ϕ = [0.0, 1.0] + + else + + xᵢⱼ = [_x _x] + ϕ = [1.0, 0.0] + end + + + else + sol = Clapeyron.tp_flash(EoSModel, p, T, _x, MichelsenTPFlash(equilibrium = :vle, nonvolatiles = nonvolatiles, noncondensables = noncondensables)) + xᵢⱼ = transpose(sol[1]) |> Array + nᵢⱼ = transpose(sol[2]) |> Array + ϕ = sum(nᵢⱼ, dims = 1)/sum(nᵢⱼ) + end + + return (ϕ, [_x xᵢⱼ]) +end + + + +function PS.TP_flash(EoSModel::M, p, T, x; nonvolatiles = nothing, noncondensables = nothing) where M <: Clapeyron.CompositeModel + + _x = abs.(x) + + if p > first(Clapeyron.bubble_pressure(EoSModel, T, _x)) + xᵢⱼ = [_x _x] + ϕ = [1.0, 0.0] + elseif p ≥ first(Clapeyron.dew_pressure(EoSModel, T, _x)) + sol = Clapeyron.tp_flash(EoSModel, p, T, _x, MichelsenTPFlash(equilibrium = :vle, nonvolatiles = nonvolatiles, noncondensables = noncondensables)) + xᵢⱼ = transpose(sol[1]) |> Array + nᵢⱼ = transpose(sol[2]) |> Array + ϕ = sum(nᵢⱼ, dims = 1)/sum(nᵢⱼ) + else + xᵢⱼ = [_x _x] + ϕ = [0.0, 1.0] + end + + return (ϕ, [_x xᵢⱼ]) +end + +function PS.is_stable(EoSModel::M, p, T, x) where M <: Clapeyron.EoSModel + return Clapeyron.isstable(EoSModel, p, T, x) +end + +function PS.is_stable(EoSModel::M, p, T, x) where M <: Clapeyron.CompositeModel + pᵦ = Clapeyron.bubble_pressure(EoSModel, T, x) + pᵢ = Clapeyron.dew_pressure(EoSModel, T, x) + if p < pᵦ || p > pᵢ + return false + else + return true + end +end + +function PS.is_stable(EoSModel::M, ρ, T, x) where M <: Clapeyron.IdealModel + return true +end + +function PS.is_VT_stable(EoSModel::M, v, T, x) where M <: Clapeyron.EoSModel + return Clapeyron.VT_isstable(EoSModel, v, T, x) +end + +function PS.is_VT_stable(EoSModel::M, v, T, x) where M <: Clapeyron.IdealModel + return true +end + +function PS.flash_mol_fractions(EoSModel::M, p, T, x) where M <: Clapeyron.EoSModel + z = PS.TP_flash(EoSModel, p, T, x)[2] + return z +end + +function PS.flash_mol_fractions_liquid(EoSModel::M, p, T, x) where M <: Clapeyron.EoSModel + z = PS.flash_mol_fractions(EoSModel, p, T, x)[:, 2] + return z +end + +function PS.flash_mol_fractions_vapor(EoSModel::M, p, T, x) where M <: Clapeyron.EoSModel + z = PS.flash_mol_fractions(EoSModel, p, T, x)[:, 3] + return z +end + +function PS.flash_vaporized_fraction(EoSModel::M, p, T, x) where M <: Clapeyron.EoSModel + ϕ = PS.TP_flash(EoSModel, p, T, x)[1] + return ϕ +end + +function PS.ρT_enthalpy(EoSModel::M, ρ, T, x) where M <: Clapeyron.EoSModel + return Clapeyron.VT_enthalpy(EoSModel, 1.0/ρ, T, x) +end + +function PS.pT_enthalpy(EoSModel::M, p, T, x, phase = :unknown) where M <: Clapeyron.EoSModel + return Clapeyron.enthalpy(EoSModel, p, T, x, phase = phase) +end + +function PS.ρT_internal_energy(EoSModel::M, ρ, T, x) where M <: Clapeyron.EoSModel + return Clapeyron.VT_internal_energy(EoSModel, 1.0/ρ, T, x) +end + +function PS.molecular_weight(model::M, z::AbstractVector) where M <: Clapeyron.EoSModel + return Clapeyron.molecular_weight(model, z) +end + + +Symbolics.@register_array_symbolic PS.TP_flash(model::Clapeyron.EoSModel, p, T, arr::AbstractVector) begin + size = (2,) + eltype = eltype(arr) +end + +Symbolics.@register_array_symbolic PS.flash_mol_fractions_liquid(model::Clapeyron.EoSModel, p, T, arr::AbstractVector) begin + size = (length(arr), ) + eltype = eltype(arr) +end + +Symbolics.@register_array_symbolic PS.flash_mol_fractions_vapor(model::Clapeyron.EoSModel, p, T, arr::AbstractVector) begin + size = (length(arr), ) + eltype = eltype(arr) +end + +Symbolics.@register_array_symbolic PS.flash_vaporized_fraction(model::Clapeyron.EoSModel, p, T, arr::AbstractVector) begin + size = (2,) + eltype = eltype(arr) +end + +@register_symbolic PS.ρT_enthalpy(model::Clapeyron.EoSModel, ρ, T, arr::AbstractVector) + +@register_symbolic PS.ρT_internal_energy(model::Clapeyron.EoSModel, ρ, T, arr::AbstractVector) + +@register_symbolic PS.pT_enthalpy(model::Clapeyron.EoSModel, p, T, arr::AbstractVector, sym::Union{Symbol, String}) + + +end + + diff --git a/src/ProcessSimulator.jl b/src/ProcessSimulator.jl index 2ae7646..066fb3f 100644 --- a/src/ProcessSimulator.jl +++ b/src/ProcessSimulator.jl @@ -1,19 +1,24 @@ module ProcessSimulator + +using LinearAlgebra using ModelingToolkit using ModelingToolkit: t_nounits as t, D_nounits as D using ModelingToolkit: scalarize, equations, get_unknowns +using Symbolics -# Base -include("base/materials.jl") -include("base/base_components.jl") -include("base/utils.jl") - -# Fluid handling -include("fluid_handling/compressors.jl") -include("fluid_handling/heat_exchangers.jl") +# Abstract types for unit operations +abstract type AbstractUnitOperation end +abstract type AbstractReactor <: AbstractUnitOperation end +abstract type AbstractSeparator <: AbstractUnitOperation end -# Reactors +# Base +include("utils/utils.jl") +include("base/basecomponents.jl") +include("pressure_drop/valve.jl") +include("separation/Adsorption.jl") +include("separation/FlashDrum.jl") include("reactors/CSTR.jl") - +include("base/print.jl") +include("base/solution_formatter.jl") end diff --git a/src/Reactors/CSTR.jl b/src/Reactors/CSTR.jl index 000431c..a674eaf 100644 --- a/src/Reactors/CSTR.jl +++ b/src/Reactors/CSTR.jl @@ -1,46 +1,76 @@ -@component function AdiabaticCSTR(;medium, reactions, P, W = 0.0, Q_rate = 0.0, n_out, flowbasis = :molar, phase = "liquid", name) +mutable struct DynamicCSTR{M <: AbstractFluidMedium, R <: AbstractReaction, S <: AbstractThermodynamicState} <: AbstractReactor + medium::M + state::S + reactionset::R + phase::S + odesystem + model_type::Symbol +end + +function DynamicCSTR(; medium, reactionset, state::S, W, Q, name) where S <: pTNVState + medium, state, phase = resolve_guess!(medium, state) + odesystem = DynamicCSTRModel(medium = medium, reactions = reactionset, state = state, W = W, Q = Q, phase = phase, name = name) + return DynamicCSTR(medium, state, reactionset, phase, odesystem, :CSTR) +end + + +@component function DynamicCSTRModel(;medium, reactions::PowerLawReaction, state, W = 0.0, Q = nothing, phase = "liquid", name) @named CV = TwoPortControlVolume_(medium = medium) - @unpack Nᵢ, V, InPort, OutPort, ControlVolumeState, rₐ, rᵥ, Q, p, Wₛ = CV + @unpack Nᵢ, V, InPort, OutPort, ControlVolumeState, rₐ, rᵥ, Wₛ = CV vars = @variables begin - cᵢ(t)[1:medium.FluidConstants.Nc], [description = "bulk concentrations"] #, unit=u"mol m^-3"] - X(t)[1:medium.FluidConstants.Nc], [description = "conversion"] #, unit=u"mol mol^-1"] + cᵢ(t)[1:medium.FluidConstants.Nc], [description = "bulk concentrations"] + X(t)[1:medium.FluidConstants.Nc], [description = "conversion"] end - - #reactions = ifelse(length(reactions) == 1, [reactions], reactions) + if !isnothing(Q) + q_eq = [CV.Q ~ Q] + else + q_eq = [] + end + eqs = [ scalarize(cᵢ .~ Nᵢ/V[1])... Wₛ ~ W scalarize(rₐ[:, 2:end] .~ 0.0)... - Q ~ Q_rate - ControlVolumeState.p ~ p - p ~ P - OutPort.ṅ[1] ~ OutPort.ṅ[2] + OutPort.ṅ[3] + CV.U ~ (ControlVolumeState.h[1] - ControlVolumeState.p/ControlVolumeState.ρ[1])*sum(collect(Nᵢ)); + q_eq... ] + + if phase == "liquid" + eq_reaction = [ - scalarize(ControlVolumeState.z[:, 3] .~ ones(medium.FluidConstants.Nc)/medium.FluidConstants.Nc)... - ControlVolumeState.ϕ[1] ~ 1.0 + + #Only liquid phase constraints + scalarize(ControlVolumeState.z[:, 3] .~ flash_mol_fractions_vapor(medium.EoSModel, ControlVolumeState.p, ControlVolumeState.T, collect(ControlVolumeState.z[:, 1])))... + + # Reaction kinetics scalarize(rᵥ[:, 2] .~ sum(Rate.(reactions, cᵢ, ControlVolumeState.T)))... scalarize(rᵥ[:, 3] .~ 0.0)... - OutPort.ṅ[2] ~ -XToMolar(n_out) - #D(V[2]) ~ 0.0 - OutPort.ṅ[3] ~ -0.0 + + #Conversion definition scalarize(X .~ (InPort.ṅ[2].*InPort.z₂ .+ OutPort.ṅ[2].*OutPort.z₂)./(InPort.ṅ[2].*InPort.z₂ .+ 1e-8))... + + ControlVolumeState.p ~ state.p #Liquid phase do not fix volume since liquid is incompressible + ] + elseif phase == "vapor" + eq_reaction = [ - ControlVolumeState.z[:, 2] ~ ones(medium.FluidConstants.Nc)/medium.FluidConstants.Nc - ControlVolumeState.ϕ[2] ~ 1.0 + #Only liquid phase constraints + scalarize(ControlVolumeState.z[:, 2] .~ flash_mol_fractions_liquid(medium.EoSModel, ControlVolumeState.p, ControlVolumeState.T, collect(ControlVolumeState.z[:, 1])))... + + # Reaction kinetics scalarize(rᵥ[:, 2] .~ 0.0)... scalarize(rᵥ[:, 3] .~ sum(Rate.(reactions, cᵢ, ControlVolumeState.T)))... - OutPort.ṅ[2] ~ -0.0 - OutPort.ṅ[3] ~ -XToMolar(n_out) - #V[3] ~ v + scalarize(X .~ (InPort.ṅ[3].*InPort.z₃ .+ OutPort.ṅ[3].*OutPort.z₃)./(InPort.ṅ[3].*InPort.z₃ .+ 1e-8))... + + CV.V[1] ~ state.V ] end @@ -51,46 +81,115 @@ end +#Base homogeneous CSTR +mutable struct SteadyStateCSTR{M <: AbstractFluidMedium, R <: AbstractReaction, S <: AbstractString} <: AbstractReactor + medium::M + state + reactionset::R + phase::S + odesystem + model_type::Symbol +end -reaction1 = PowerLawReaction(["water", "methanol"], [-1.0, 0.0], [2.0, 0.0], 1e-10, 10_000.0) +function SteadyStateCSTR(;medium, reactionset, limiting_reactant, state, W, Q, name) -@component function Reactor(;medium, reactions, P, n_in, n_out, W = 0.0, Q_rate = 0.0, phase = "liquid", name) + medium, state, phase = resolve_guess!(medium, state); - systems = @named begin - inlet_stream = FixedBoundary_pTzn_(medium = medium, p = P, T = 300.15, z = [0.8, 0.2], ṅ = n_in) - tank = CSTR(medium = medium, reactions = reactions, P = P, n_out = n_out, W = W, Q_rate = Q_rate, phase = phase) + if isnothing(limiting_reactant) + limiting_reactant = medium.Constants.iupacName[1] end - vars = [] + odesystem = SteadyStateCSTRModel(medium = medium, reactions = reactionset, + limiting_reactant = limiting_reactant, state = state, W = W, Q = Q, phase = phase, name = name) - pars = [] + if !isnothing(Q) #If heat is given use, else fix temperature and calculate heat + q_eq = [odesystem.Q ~ Q] + else + @unpack ControlVolumeState = odesystem + q_eq = [ControlVolumeState.T ~ state.T] + end - connections = [ - connect(inlet_stream.OutPort, tank.InPort) - ] + newsys = extend(System(q_eq, t, [], []; name), odesystem) + return SteadyStateCSTR(medium, state, reactionset, phase, newsys, :CSTR) +end - return ODESystem(connections, t, vars, pars; name, systems = [systems...]) +function ConversionSteadyStateCSTR(; medium, reactionset, limiting_reactant, state, W, Q, conversion, name) + cstr = SteadyStateCSTR(medium = medium, reactionset = reactionset, limiting_reactant = limiting_reactant, state = state, W = W, Q = Q, name = name) + odesys = cstr.odesystem + newsys = extend(System([odesys.X ~ conversion], t, [], []; name), odesys) + cstr.odesystem = newsys + return SteadyStateCSTR(cstr.medium, cstr.state, cstr.reactionset, cstr.phase, cstr.odesystem, :CSTR) +end +function FixedVolumeSteadyStateCSTR(; medium, reactionset, limiting_reactant, state, W, Q, volume, name) #Overwrites state volume and recalculate number of moles + cstr = SteadyStateCSTR(medium = medium, reactionset = reactionset, limiting_reactant = limiting_reactant, state = state, W = W, Q = Q, name = name) + odesys = cstr.odesystem + @unpack V = odesys + newsys = extend(System([V[1] ~ volume], t, [], []; name), odesys) #Fix overall volume to be V + cstr.odesystem = newsys + return SteadyStateCSTR(cstr.medium, cstr.state, cstr.reactionset, cstr.phase, cstr.odesystem, :CSTR) end -@named R_101 = Reactor(medium = medium, reactions = [reaction1], P = 101325.0, n_in = 10.0, n_out = 10.0, W = 0.0, Q_rate = 0.0, phase = "liquid") -sistem = structural_simplify(R_101) +@component function SteadyStateCSTRModel(;medium, reactions, limiting_reactant = nothing, state, W = 0.0, Q = nothing, phase = "liquid", name) + + @named CV = TwoPortControlVolume_SteadyState(medium = medium) + @unpack U, Nᵢ, V, InPort, OutPort, ControlVolumeState, rₐ, rᵥ, Wₛ = CV + + vars = @variables begin + cᵢ(t)[1:medium.Constants.Nc], [description = "bulk concentrations"] + X(t), [description = "Limiting reactant conversion"] + end + + + eqs = [ + scalarize(cᵢ .~ Nᵢ/V[1])... + Wₛ ~ W + scalarize(rₐ[:, 2:end] .~ 0.0)... + U ~ (OutPort.h[1] - ControlVolumeState.p/ControlVolumeState.ρ[1])*sum(collect(Nᵢ)) + ControlVolumeState.p ~ state.p + ] -#unknowns_ = unknowns(sistem) + limiting_index = findfirst(x -> x == limiting_reactant, medium.Constants.iupacName) -u0 = [sistem.tank.Nᵢ[1] => 60.0, sistem.tank.Nᵢ[2] => 60.0, - sistem.tank.ControlVolumeState.T => 300.0] + if phase == "liquid" -guesses_ = [ -sistem.tank.V[2] => 0.2, -sistem.tank.V[3] => 0.0001, -sistem.tank.nᴸⱽ[2] => 0.0] + eq_reaction = [ + + #Only liquid phase constraints + scalarize(ControlVolumeState.z[:, 3] .~ flash_mol_fractions_vapor(medium.EoSModel, ControlVolumeState.p, ControlVolumeState.T, collect(ControlVolumeState.z[:, 1])))... -prob = ODEProblem(sistem, u0, (0.0, 100.0), guesses = guesses_); + # Reaction kinetics + scalarize(rᵥ[:, 2] .~ Rate(reactions, cᵢ, ControlVolumeState.T))... + scalarize(rᵥ[:, 3] .~ 0.0)... -sol = solve(prob, FBDF(autodiff = false)) + #Conversion definition + scalarize(X ~ (InPort.ṅ[2].*InPort.z[limiting_index, 2] .+ OutPort.ṅ[2].*OutPort.z[limiting_index, 2])./(InPort.ṅ[2].*InPort.z[limiting_index, 2] .+ 1e-8)) + + ] -plot(sol.t, sol[sistem.tank.X[1]]) + elseif phase == "vapor" + + eq_reaction = [ + #Only vapor phase constraints + scalarize(ControlVolumeState.z[:, 2] .~ flash_mol_fractions_liquid(medium.EoSModel, ControlVolumeState.p, ControlVolumeState.T, collect(ControlVolumeState.z[:, 1])))... + + # Reaction kinetics + scalarize(rᵥ[:, 2] .~ 0.0)... + scalarize(rᵥ[:, 3] .~ sum(Rate.(reactions, cᵢ, ControlVolumeState.T)))... + + #Conversion definition + scalarize(X ~ (InPort.ṅ[3].*InPort.z[limiting_index, 3] .+ OutPort.ṅ[3].*OutPort.z[limiting_index, 3])./(InPort.ṅ[3].*InPort.z[limiting_index, 3] .+ 1e-8)) + + ] + + end + + pars = [] + + return extend(System([eqs...;eq_reaction...], t, collect(Iterators.flatten(vars)), pars; name), CV) + +end +export SteadyStateCSTR, DynamicCSTR, SteadyStateCSTRModel, DynamicCSTRModel, ConversionSteadyStateCSTR, FixedVolumeSteadyStateCSTR diff --git a/src/base/base_components.jl b/src/base/base_components.jl deleted file mode 100644 index 35c342e..0000000 --- a/src/base/base_components.jl +++ /dev/null @@ -1,255 +0,0 @@ -@component function ρTz_ThermodynamicState_(;medium, name) - - pars = [] - - vars = @variables begin - ϕ(t)[1:medium.Constants.nphases - 1], [description = "phase fraction"] - ρ(t)[1:medium.Constants.nphases], [description = "molar density"] - z(t)[1:medium.Constants.Nc, 1:medium.Constants.nphases], [description = "mole fraction", irreducible = true] - p(t), [description = "pressure"] - T(t), [description = "Temperature"] - end - - eq = [ - [ρ[j] ~ PT_molar_density(medium.EoSModel, p, T, collect(z[:, j]), phase = medium.Constants.phaseNames[j]) for j in 2:medium.Constants.nphases]... - ρ[1] .~ 1.0/(sum([ϕ[j - 1]/ρ[j] for j in 2:medium.Constants.nphases]))... - ] - - guesses = [z[i, j] => medium.Guesses.x[i, j] for i in 1:medium.Constants.Nc, j in 1:medium.Constants.nphases] - - ODESystem(eq, t, collect(Iterators.flatten(vars)), pars; guesses = guesses, name) - -end - - -@connector function PhZConnector_(;medium, name) - - vars = @variables begin - p(t), [description = "pressure"] - h(t)[1:medium.Constants.nphases], [description = "molar enthalpy"] - z₁(t)[1:medium.Constants.Nc], [description = "overall mole fraction"] - z₂(t)[1:medium.Constants.Nc], [description = "dense state mole fraction"] - z₃(t)[1:medium.Constants.Nc], [description = "dense mole fraction"] - ṅ(t)[1:medium.Constants.nphases], [description = "molar flow"] - end - - pars = [] - - eqs = [] # This avoids "BoundsError: attempt to access 0-element Vector{Vector{Any}} at index [0]" - eq = eqs==[] ? Equation[] : eqs - - ODESystem(eq, t, collect(Iterators.flatten(vars)), pars; name) - -end - - -@connector function SurfaceConnector_(;medium, name) - - vars = @variables begin - T(t), [description = "Temperature"] - z(t)[1:medium.Constants.Nc], [description = "overall mole fraction"] - ϕₘ(t)[1:medium.Constants.Nc], [description = "Molar Flow Rate", connect = Flow] - ϕₕ(t), [description = "Heat Flow Rate", connect = Flow] - end - - pars = [] - - eqs = [] # This avoids "BoundsError: attempt to access 0-element Vector{Vector{Any}} at index [0]" - eq = eqs==[] ? Equation[] : eqs - - ODESystem(eq, t, collect(Iterators.flatten(vars)), pars; name) - -end - -@component function Surface(;medium, name) - - systems = @named begin - InPort = SurfaceConnector_(medium = medium) - OutPort = SurfaceConnector_(medium = medium) - end - - vars = @variables begin - A(t), [description = "Area"] - kₘ(t), [description = "mass transfer coefficient"] - kₕ(t), [description = "heat transfer coefficient"] - end - - eqs = [ - kₘ ~ mass_transfer_coefficient(medium) - kₕ ~ heat_transfer_coefficient(medium) - ϕₕ ~ A*kₕ*(InPort.T - OutPort.T) - scalarize(InPort.ϕₘ .~ A*kₘ*(InPort.z .- OutPort.z))... - InPort.ϕₘ + OutPort.ϕₘ ~ 0 - InPort.ϕₕ + OutPort.ϕₕ ~ 0 - ] - - ODESystem(eqs, t, collect(Iterators.flatten(vars)), pars; name, systems = [systems...]) - -end - - - -@component function AdsorptionInterface(;fluidmedium, solidmedium, name) - - systems = @named begin - SolidSurface = Surface(medium = solidmedium) - FluidSurface = Surface(medium = fluidmedium) - end - - eqs = [domain_connect(FluidSurface.OutPort, SolidSurface.InPort) - SolidSurface.InPort.z .~ loading(solidmedium.IsothermModel, FluidSurface.OutPort.z) - ] - - ODESystem(eqs, t, collect(Iterators.flatten(vars)), pars; name, systems = [systems...]) - -end - - - -#### ------ ControlVolumes ------ - -@component function HeatConnector(;name) - vars = @variables begin - Q(t), [description = "heat flux"] #, unit=u"J s^-1"] - end - - return ODESystem(Equation[], t, vars, []; name) -end - -@component function WorkConnector(;name) - vars = @variables begin - W(t), [description = "power"] #, unit=u"J s^1"] - end - - return ODESystem(Equation[], t, vars, []; name) -end - - -@component function TwoPortControlVolume_(;medium, name) - - - systems = @named begin - InPort = PhZConnector_(medium = medium) - OutPort = PhZConnector_(medium = medium) - ControlVolumeState = ρTz_ThermodynamicState_(medium = medium) - end - - vars = @variables begin - rᵥ(t)[1:medium.Constants.Nc, 1:medium.Constants.nphases], [description = "mass source or sink - volumetric basis"] - rₐ(t)[1:medium.Constants.Nc, 1:medium.Constants.nphases], [description = "molar source or sink - through surface in contact with other phases"] - Nᵢ(t)[1:medium.Constants.Nc], [description = "molar holdup"] - nᴸⱽ(t)[1:medium.Constants.nphases - 1], [description = "molar holdup"] - U(t), [description = "internal energy holdup"] - p(t), [description = "pressure"] - V(t)[1:medium.Constants.nphases], [description = "volume"] - Q(t), [description = "heat flux"] - Wₛ(t), [description = "shaft work"] - end - - pars = [] - - - eqs = [ - - # Energy balance - - D(U) ~ InPort.h[1]*InPort.ṅ[1] + OutPort.h[1]*(OutPort.ṅ[1]) + Q + Wₛ - - # Mole balance - - scalarize(D(Nᵢ) .~ InPort.ṅ[1].*InPort.z₁ + (OutPort.ṅ[2].*OutPort.z₂ + OutPort.ṅ[3].*OutPort.z₃) .+ collect(rᵥ[:, 2:end]*V[2:end]))... - - scalarize(rᵥ[:, 1] .~ sum(collect(rᵥ[:, 2:end]), dims = 2))... - - scalarize(rₐ[:, 1] .~ sum(collect(rₐ[:, 2:end]), dims = 2))... - - scalarize(Nᵢ .~ nᴸⱽ[1]*ControlVolumeState.z[:, 2] + nᴸⱽ[2]*ControlVolumeState.z[:, 3])... - - scalarize(sum(Nᵢ) ~ sum(nᴸⱽ)) - - scalarize(sum(collect(ControlVolumeState.ϕ)) ~ 1.0) - - - # Thermodynamic state equations - - scalarize(ControlVolumeState.z[:, 1] .~ Nᵢ ./ sum(collect(Nᵢ)))... - - ControlVolumeState.ϕ[1] ~ nᴸⱽ[1]/sum(collect(nᴸⱽ)) - - - # Control Volume properties - - U ~ (OutPort.h[1] - ControlVolumeState.p/ControlVolumeState.ρ[1])*sum(collect(Nᵢ)) - - V[1] ~ sum(collect(V[2:end])) - - V[2]*ControlVolumeState.ρ[2] ~ nᴸⱽ[1] - - V[3]*ControlVolumeState.ρ[3] ~ nᴸⱽ[2] - - - # Outlet port properties - - [OutPort.h[j] ~ ρT_enthalpy(medium.EoSModel, ControlVolumeState.ρ[j], ControlVolumeState.T, collect(ControlVolumeState.z[:, j])) for j in 2:medium.Constants.nphases]... - - OutPort.h[1] ~ dot(collect(OutPort.h[2:end]), collect(ControlVolumeState.ϕ)) - - OutPort.p ~ ControlVolumeState.p - - scalarize(OutPort.z₁ .~ ControlVolumeState.z[:, 1])... - scalarize(OutPort.z₂ .~ ControlVolumeState.z[:, 2])... - scalarize(OutPort.z₃ .~ ControlVolumeState.z[:, 3])... - - # Specifics for the two-phase system - - #= ControlVolumeState.p ~ p - scalarize(ControlVolumeState.z[:, 3] ~ flash_mol_fractions_vapor(medium.EoSModel, ControlVolumeState.p, ControlVolumeState.T, collect(ControlVolumeState.z[:, 1])))... - scalarize(nᴸⱽ[1]/sum(nᴸⱽ) ~ flash_vaporized_fraction(medium.EoSModel, ControlVolumeState.p, ControlVolumeState.T, collect(ControlVolumeState.z[:, 1]))[1]) =# - - ] - - return ODESystem(eqs, t, collect(Iterators.flatten(vars)), pars; name, systems = [systems...]) - -end - -@component function FixedBoundary_pTzn_(;medium, p, T, z, ṅ, name) - - systems = @named begin - OutPort = PhZConnector_(medium = medium) - ControlVolumeState = ρTz_ThermodynamicState_(medium = medium) - end - - vars = [] - - pars = [] - - eqs = [ - # Port equations - OutPort.p ~ p - scalarize(OutPort.z₁ .~ z)... - scalarize(OutPort.z₂ .~ ControlVolumeState.z[:, 2])... - scalarize(OutPort.z₃ .~ ControlVolumeState.z[:, 3])... - OutPort.ṅ[1] ~ ṅ - OutPort.ṅ[2] ~ ControlVolumeState.ϕ[1]*ṅ - OutPort.ṅ[3] ~ ControlVolumeState.ϕ[2]*ṅ - [OutPort.h[i] ~ ρT_enthalpy(medium.EoSModel, ControlVolumeState.ρ[i], ControlVolumeState.T, ControlVolumeState.z[:, i]) for i in 2:medium.Constants.nphases]... - OutPort.h[1] ~ dot(collect(OutPort.h[2:end]), collect(ControlVolumeState.ϕ)) - - # State equations - scalarize(ControlVolumeState.z[:, 1] .~ z)... - scalarize(ControlVolumeState.z[:, 2] .~ flash_mol_fractions_liquid(medium.EoSModel, p, T, z))... - scalarize(ControlVolumeState.z[:, 3] .~ flash_mol_fractions_vapor(medium.EoSModel, p, T, z))... - ControlVolumeState.T ~ T - OutPort.ṅ[2] + OutPort.ṅ[3] ~ OutPort.ṅ[1] - ControlVolumeState.p ~ p - scalarize(ControlVolumeState.ϕ[1] ~ flash_vaporized_fraction(medium.EoSModel, p, T, z)[1]) - ] - - return ODESystem(eqs, t, vars, collect(Iterators.flatten(pars)); name, systems = [systems...]) - - -end - - - - diff --git a/src/base/basecomponents.jl b/src/base/basecomponents.jl new file mode 100644 index 0000000..d71622e --- /dev/null +++ b/src/base/basecomponents.jl @@ -0,0 +1,684 @@ +@component function ρTz_ThermodynamicState_(;medium, name) + + pars = [] + + vars = @variables begin + ϕ(t)[1:medium.Constants.nphases - 1], [description = "phase fraction"] + ρ(t)[1:medium.Constants.nphases], [description = "molar density"] + z(t)[1:medium.Constants.Nc, 1:medium.Constants.nphases], [description = "mole fraction", irreducible = true] + p(t), [description = "pressure"] + T(t), [description = "Temperature"] + end + + eq = [ + [ρ[j] ~ PT_molar_density(medium.EoSModel, p, T, collect(z[:, j]), phase = medium.Constants.phase_names[j]) for j in 2:medium.Constants.nphases]... + ρ[1] .~ 1.0/(sum([ϕ[j - 1]/ρ[j] for j in 2:medium.Constants.nphases]))... + ] + + if medium.Constants.nphases > 2 + eq_extra = [sum(collect(ϕ)) ~ 1.0 + ϕ[1] ~ flash_vaporized_fraction(medium.EoSModel, p, T, collect(z[:, 1]))[1]] + guesses_ϕ = [ϕ[j - 1] => medium.Guesses.ϕ[j - 1] for j in 2:medium.Constants.nphases] + + else + eq_extra = [] + guesses_ϕ = [] + end + + guesses_z = [z[i, j] => medium.Guesses.x[i, j] for i ∈ 1:medium.Constants.Nc, j ∈ 1:medium.Constants.nphases] + + guesses_p = [p => medium.Guesses.p] + + guesses_T = [T => medium.Guesses.T] + + + ODESystem([eq...; eq_extra...], t, collect(Iterators.flatten(vars)), pars; name, guesses = [guesses_z...; guesses_p...; guesses_T...; guesses_ϕ...]) + +end + + +@connector function PhZConnector_(;medium, name) + + vars = @variables begin + p(t), [description = "pressure"] + h(t)[1:medium.Constants.nphases], [description = "molar enthalpy"] + z(t)[1:medium.Constants.Nc, 1:medium.Constants.nphases], [description = "overall mole fraction"] + ṅ(t)[1:medium.Constants.nphases], [description = "molar flow", connect = Flow] + end + + pars = [] + + eqs = [] + + eq = eqs==[] ? Equation[] : eqs + + ODESystem(eq, t, collect(Iterators.flatten(vars)), pars; name) + +end + +@component function ConstantFlowRate(; medium, name, flowrate, flowbasis = :molar) + + pars = @parameters begin + flowrate = flowrate + end + + vars = [] + + systems = @named begin + port = PhZConnector_(medium = medium) + end + + eqs = [ + port.ṅ[1] ~ XtoMolar(flowrate, medium, nothing, flowbasis) + ] + + ODESystem(eqs, t, collect(Iterators.flatten(vars)), pars; systems, name) + +end + +@component function ConstantPressure(; medium, name, p) + + pars = @parameters begin + pressure = p + end + + vars = [] + + systems = @named begin + Port = PhZConnector_(medium = medium) + end + + eqs = [ + Port.p ~ pressure + ] + + ODESystem(eqs, t, collect(Iterators.flatten(vars)), pars; systems, name) +end + +@component function ConnHouse(; medium, name) + + pars = [] + + vars = [] + + systems = @named begin + InPort = PhZConnector_(medium = medium) + end + + System(Equation[], t, vars, pars; systems, name) + +end + + +@connector function SurfaceConnector_(;medium, name) + + vars = @variables begin + T(t), [description = "Temperature", output = true] + μ(t)[1:medium.Constants.Nc], [description = "Chemical potential or an estimate of it", output = true] + ϕₘ(t)[1:medium.Constants.Nc], [description = "Molar Flow Rate", output = true] + ϕₕ(t), [description = "Heat Flow Rate", output = true] + end + + pars = [] + + ODESystem(Equation[], t, collect(Iterators.flatten(vars)), pars; name) + +end + +@component function Surface(;medium, name) + + systems = @named begin + InPort = SurfaceConnector_(medium = medium) + OutPort = SurfaceConnector_(medium = medium) + end + + vars = @variables begin + kₘ(t)[1:medium.Constants.Nc], [description = "mass transfer coefficient"] + kₕ(t), [description = "heat transfer coefficient"] + end + + eqs = [ + scalarize(kₘ .~ mass_transfer_coefficient(medium, InPort.T, InPort.μ)) + kₕ ~ heat_transfer_coefficient(medium, InPort.T, InPort.μ) + InPort.ϕₕ ~ kₕ*(InPort.T - OutPort.T) + scalarize(InPort.ϕₘ .~ kₘ.*(InPort.μ .- OutPort.μ))... + scalarize(InPort.ϕₘ .+ OutPort.ϕₘ .~ 0) + InPort.ϕₕ + OutPort.ϕₕ ~ 0 + ] + + pars = [] + + ODESystem(eqs, t, collect(Iterators.flatten(vars)), pars; systems, name) + +end + + +@component function TwoPortControlVolume_(;medium, name) + + systems = @named begin + InPort = PhZConnector_(medium = medium) + OutPort = PhZConnector_(medium = medium) + ControlVolumeState = ρTz_ThermodynamicState_(medium = medium) + end + + vars = @variables begin + rᵥ(t)[1:medium.Constants.Nc, 1:medium.Constants.nphases], [description = "mass source or sink - volumetric basis"] + rₐ(t)[1:medium.Constants.Nc, 1:medium.Constants.nphases], [description = "molar source or sink - through surface in contact with other phases"] + Nᵢ(t)[1:medium.Constants.Nc], [description = "molar holdup"] + nᴸⱽ(t)[1:medium.Constants.nphases - 1], [description = "molar holdup in each phase, excluding the overall phase"] + U(t), [description = "internal energy holdup"] + V(t)[1:medium.Constants.nphases], [description = "volume"] + Q(t), [description = "heat flux"] + Wₛ(t), [description = "shaft work"] + end + + pars = [] + + + + eqs = [ + + # Energy balance + + D(U) ~ InPort.h[1]*InPort.ṅ[1] + OutPort.h[1]*(OutPort.ṅ[1]) + Q + Wₛ + + # Mole balances + + [D(Nᵢ[i]) ~ InPort.ṅ[1]*InPort.z[i, 1] + sum(dot(collect(OutPort.ṅ[2:end]), collect(OutPort.z[i:i, 2:end]))) + sum(collect(rᵥ[i, 2:end].*V[2:end])) + rₐ[i, 1] for i in 1:medium.Constants.Nc]... + + scalarize(rᵥ[:, 1] .~ sum(collect(rᵥ[:, 2:end]), dims = 2))... + + scalarize(rₐ[:, 1] .~ sum(collect(rₐ[:, 2:end]), dims = 2))... + + [Nᵢ[i] ~ sum(dot(nᴸⱽ, collect(ControlVolumeState.z[i, 2:end]))) for i ∈ 1:medium.Constants.Nc]... + + scalarize(sum(Nᵢ) ~ sum(nᴸⱽ)) + + scalarize(ControlVolumeState.z[:, 1] .~ Nᵢ ./ sum(collect(Nᵢ)))... + + ControlVolumeState.ϕ[1] ~ nᴸⱽ[1]/sum(collect(nᴸⱽ)) + + + # Control Volume properties + V[1] ~ sum(collect(V[2:end])) + V[2]*ControlVolumeState.ρ[2] ~ nᴸⱽ[1] + V[3]*ControlVolumeState.ρ[3] ~ nᴸⱽ[2] + + + # Outlet port properties + [OutPort.h[j] ~ ρT_enthalpy(medium.EoSModel, ControlVolumeState.ρ[j], ControlVolumeState.T, collect(ControlVolumeState.z[:, j])) for j in 2:medium.Constants.nphases]... + OutPort.h[1] ~ dot(collect(OutPort.h[2:end]), collect(ControlVolumeState.ϕ)) + + OutPort.p ~ ControlVolumeState.p + + scalarize(OutPort.z .~ ControlVolumeState.z)... + + OutPort.ṅ[2] ~ ControlVolumeState.ϕ[1]*OutPort.ṅ[1] + OutPort.ṅ[3] ~ ControlVolumeState.ϕ[2]*OutPort.ṅ[1] + + + ] + + return ODESystem(eqs, t, collect(Iterators.flatten(vars)), pars; name, systems = [systems...]) + +end + + +@component function TwoPortControlVolume_SteadyState(;medium, name) + + systems = @named begin + InPort = PhZConnector_(medium = medium) + OutPort = PhZConnector_(medium = medium) + ControlVolumeState = ρTz_ThermodynamicState_(medium = medium) + end + + vars = @variables begin + rᵥ(t)[1:medium.Constants.Nc, 1:medium.Constants.nphases], [description = "mass source or sink - volumetric basis"] + rₐ(t)[1:medium.Constants.Nc, 1:medium.Constants.nphases], [description = "molar source or sink - through surface in contact with other phases"] + Nᵢ(t)[1:medium.Constants.Nc], [description = "molar holdup"] + nᴸⱽ(t)[1:medium.Constants.nphases - 1], [description = "molar holdup in each phase, excluding the overall phase"] + U(t), [description = "internal energy holdup"] + V(t)[1:medium.Constants.nphases], [description = "volume"] + Q(t), [description = "heat flux"] + Wₛ(t), [description = "shaft work"] + end + + pars = [] + + eqs = [ + + # Steady-state energy balance (no accumulation) + 0 ~ InPort.h[1]*InPort.ṅ[1] + OutPort.h[1]*(OutPort.ṅ[1]) + Q + Wₛ + + # Steady-state mole balances (no accumulation) + [0 ~ InPort.ṅ[1]*InPort.z[i, 1] + sum(dot(collect(OutPort.ṅ[2:end]), collect(OutPort.z[i:i, 2:end]))) + sum(collect(rᵥ[i, 2:end].*V[2:end])) + rₐ[i, 1] for i in 1:medium.Constants.Nc]... + + scalarize(rᵥ[:, 1] .~ sum(collect(rᵥ[:, 2:end]), dims = 2))... + + scalarize(rₐ[:, 1] .~ sum(collect(rₐ[:, 2:end]), dims = 2))... + + [Nᵢ[i] ~ sum(dot(nᴸⱽ, collect(ControlVolumeState.z[i, 2:end]))) for i ∈ 1:medium.Constants.Nc]... + + scalarize(sum(Nᵢ) ~ sum(nᴸⱽ)) + + scalarize(ControlVolumeState.z[:, 1] .~ Nᵢ ./ sum(collect(Nᵢ)))... + + ControlVolumeState.ϕ[1] ~ nᴸⱽ[1]/sum(collect(nᴸⱽ)) + + + # Control Volume properties + V[1] ~ sum(collect(V[2:end])) + V[2]*ControlVolumeState.ρ[2] ~ nᴸⱽ[1] + V[3]*ControlVolumeState.ρ[3] ~ nᴸⱽ[2] + + + # Outlet port properties + #[OutPort.h[j] ~ ρT_enthalpy(medium.EoSModel, ControlVolumeState.ρ[j], ControlVolumeState.T, collect(ControlVolumeState.z[:, j])) for j in 2:medium.Constants.nphases]... + [OutPort.h[j] ~ pT_enthalpy(medium.EoSModel, + ControlVolumeState.p, ControlVolumeState.T, + collect(ControlVolumeState.z[:, j]), + medium.Constants.phase_names[j]) for j in 2:medium.Constants.nphases]... + + OutPort.h[1] ~ dot(collect(OutPort.h[2:end]), collect(ControlVolumeState.ϕ)) + + OutPort.p ~ ControlVolumeState.p + + scalarize(OutPort.z .~ ControlVolumeState.z)... + + OutPort.ṅ[2] ~ ControlVolumeState.ϕ[1]*OutPort.ṅ[1] + OutPort.ṅ[3] ~ ControlVolumeState.ϕ[2]*OutPort.ṅ[1] + + + ] + + return ODESystem(eqs, t, collect(Iterators.flatten(vars)), pars; name, systems = [systems...]) + +end + + +@component function ThreePortControlVolume_(;medium, name) + """ + Three-port control volume: 1 inlet, 2 separate outlets (liquid and vapor) + Designed for flash drums with phase-separated exits + """ + + systems = @named begin + InPort = PhZConnector_(medium = medium) + LiquidOutPort = PhZConnector_(medium = medium) + VaporOutPort = PhZConnector_(medium = medium) + ControlVolumeState = ρTz_ThermodynamicState_(medium = medium) + end + + vars = @variables begin + rᵥ(t)[1:medium.Constants.Nc, 1:medium.Constants.nphases], [description = "mass source or sink - volumetric basis"] + rₐ(t)[1:medium.Constants.Nc, 1:medium.Constants.nphases], [description = "molar source or sink - through surface"] + Nᵢ(t)[1:medium.Constants.Nc], [description = "molar holdup"] + nᴸⱽ(t)[1:medium.Constants.nphases - 1], [description = "molar holdup in each phase"] + U(t), [description = "internal energy holdup"] + V(t)[1:medium.Constants.nphases], [description = "volume"] + Q(t), [description = "heat flux"] + Wₛ(t), [description = "shaft work"] + end + + pars = [] + + eqs = [ + # Energy balance + D(U) ~ InPort.h[1]*InPort.ṅ[1] + + LiquidOutPort.h[1]*LiquidOutPort.ṅ[1] + + VaporOutPort.h[1]*VaporOutPort.ṅ[1] + Q + Wₛ + + # Component mole balances + [D(Nᵢ[i]) ~ InPort.ṅ[1]*InPort.z[i, 1] + + LiquidOutPort.ṅ[1]*LiquidOutPort.z[i, 1] + + VaporOutPort.ṅ[1]*VaporOutPort.z[i, 1] + + sum(collect(rᵥ[i, 2:end].*V[2:end])) + rₐ[i, 1] for i in 1:medium.Constants.Nc]... + + scalarize(rᵥ[:, 1] .~ sum(collect(rᵥ[:, 2:end]), dims = 2))... + scalarize(rₐ[:, 1] .~ sum(collect(rₐ[:, 2:end]), dims = 2))... + + # Holdup relationships + [Nᵢ[i] ~ sum(dot(nᴸⱽ, collect(ControlVolumeState.z[i, 2:end]))) for i ∈ 1:medium.Constants.Nc]... + scalarize(sum(Nᵢ) ~ sum(nᴸⱽ)) + + scalarize(ControlVolumeState.z[:, 1] .~ Nᵢ ./ (sum(collect(Nᵢ)) + 1e-10))... + ControlVolumeState.ϕ[1] ~ nᴸⱽ[1]/(sum(collect(nᴸⱽ)) + 1e-10) + + # Control Volume properties + V[1] ~ sum(collect(V[2:end])) + V[2]*ControlVolumeState.ρ[2] ~ nᴸⱽ[1] + V[3]*ControlVolumeState.ρ[3] ~ nᴸⱽ[2] + + # Liquid outlet - carries liquid phase (index 2) + LiquidOutPort.h[2] ~ ρT_enthalpy(medium.EoSModel, ControlVolumeState.ρ[2], ControlVolumeState.T, collect(ControlVolumeState.z[:, 2])) + LiquidOutPort.h[3] ~ ρT_enthalpy(medium.EoSModel, ControlVolumeState.ρ[3], ControlVolumeState.T, collect(ControlVolumeState.z[:, 3])) + LiquidOutPort.h[1] ~ LiquidOutPort.h[2] # Overall enthalpy = liquid enthalpy (pure liquid exit) + + # Liquid outlet composition - only liquid phase exits + scalarize(LiquidOutPort.z[:, 2] .~ ControlVolumeState.z[:, 2])... + scalarize(LiquidOutPort.z[:, 3] .~ ControlVolumeState.z[:, 3])... # Keep for consistency + scalarize(LiquidOutPort.z[:, 1] .~ ControlVolumeState.z[:, 2])... # Overall = liquid composition + + # Liquid outlet flows - only liquid phase + LiquidOutPort.ṅ[2] ~ LiquidOutPort.ṅ[1] # Total flow = liquid flow + LiquidOutPort.ṅ[3] ~ 1e-10 # Minimal vapor (numerically stable) + + # Vapor outlet - carries vapor phase (index 3) + VaporOutPort.h[2] ~ ρT_enthalpy(medium.EoSModel, ControlVolumeState.ρ[2], ControlVolumeState.T, collect(ControlVolumeState.z[:, 2])) + VaporOutPort.h[3] ~ ρT_enthalpy(medium.EoSModel, ControlVolumeState.ρ[3], ControlVolumeState.T, collect(ControlVolumeState.z[:, 3])) + VaporOutPort.h[1] ~ VaporOutPort.h[3] # Overall enthalpy = vapor enthalpy (pure vapor exit) + + VaporOutPort.p ~ ControlVolumeState.p # Vapor pressure = tank pressure + + # Vapor outlet composition - only vapor phase exits + scalarize(VaporOutPort.z[:, 2] .~ ControlVolumeState.z[:, 2])... # Keep for consistency + scalarize(VaporOutPort.z[:, 3] .~ ControlVolumeState.z[:, 3])... + scalarize(VaporOutPort.z[:, 1] .~ ControlVolumeState.z[:, 3])... # Overall = vapor composition + + # Vapor outlet flows - only vapor phase + VaporOutPort.ṅ[3] ~ VaporOutPort.ṅ[1] # Total flow = vapor flow + VaporOutPort.ṅ[2] ~ 1e-10 # Minimal liquid (numerically stable) + ] + + return ODESystem(eqs, t, collect(Iterators.flatten(vars)), pars; name, systems = [systems...]) +end + + +@component function ThreePortControlVolume_SteadyState(;medium, name) + """ + Three-port control volume (steady-state): 1 inlet, 2 separate outlets (liquid and vapor) + """ + + systems = @named begin + InPort = PhZConnector_(medium = medium) + LiquidOutPort = PhZConnector_(medium = medium) + VaporOutPort = PhZConnector_(medium = medium) + ControlVolumeState = ρTz_ThermodynamicState_(medium = medium) + end + + vars = @variables begin + rᵥ(t)[1:medium.Constants.Nc, 1:medium.Constants.nphases], [description = "mass source or sink - volumetric basis"] + rₐ(t)[1:medium.Constants.Nc, 1:medium.Constants.nphases], [description = "molar source or sink - through surface"] + Nᵢ(t)[1:medium.Constants.Nc], [description = "molar holdup"] + nᴸⱽ(t)[1:medium.Constants.nphases - 1], [description = "molar holdup in each phase"] + U(t), [description = "internal energy holdup"] + V(t)[1:medium.Constants.nphases], [description = "volume"] + Q(t), [description = "heat flux"] + Wₛ(t), [description = "shaft work"] + end + + pars = [] + + eqs = [ + # Steady-state energy balance + 0 ~ InPort.h[1]*InPort.ṅ[1] + + LiquidOutPort.h[1]*LiquidOutPort.ṅ[1] + + VaporOutPort.h[1]*VaporOutPort.ṅ[1] + Q + Wₛ + + # Steady-state component mole balances + [0 ~ InPort.ṅ[1]*InPort.z[i, 1] + + LiquidOutPort.ṅ[1]*LiquidOutPort.z[i, 1] + + VaporOutPort.ṅ[1]*VaporOutPort.z[i, 1] + + sum(collect(rᵥ[i, 2:end].*V[2:end])) + rₐ[i, 1] for i in 1:medium.Constants.Nc]... + + scalarize(rᵥ[:, 1] .~ sum(collect(rᵥ[:, 2:end]), dims = 2))... + scalarize(rₐ[:, 1] .~ sum(collect(rₐ[:, 2:end]), dims = 2))... + + # Holdup relationships + [Nᵢ[i] ~ sum(dot(nᴸⱽ, collect(ControlVolumeState.z[i, 2:end]))) for i ∈ 1:medium.Constants.Nc]... + scalarize(sum(Nᵢ) ~ sum(nᴸⱽ)) + + scalarize(ControlVolumeState.z[:, 1] .~ Nᵢ ./ (sum(collect(Nᵢ)) + 1e-10))... + ControlVolumeState.ϕ[1] ~ nᴸⱽ[1]/(sum(collect(nᴸⱽ)) + 1e-10) + + # Control Volume properties + V[1] ~ sum(collect(V[2:end])) + V[2]*ControlVolumeState.ρ[2] ~ nᴸⱽ[1] + V[3]*ControlVolumeState.ρ[3] ~ nᴸⱽ[2] + + # Liquid outlet properties + LiquidOutPort.h[2] ~ ρT_enthalpy(medium.EoSModel, ControlVolumeState.ρ[2], ControlVolumeState.T, collect(ControlVolumeState.z[:, 2])) + LiquidOutPort.h[3] ~ ρT_enthalpy(medium.EoSModel, ControlVolumeState.ρ[3], ControlVolumeState.T, collect(ControlVolumeState.z[:, 3])) + LiquidOutPort.h[1] ~ LiquidOutPort.h[2] + + LiquidOutPort.p ~ ControlVolumeState.p + scalarize(LiquidOutPort.z[:, 2] .~ ControlVolumeState.z[:, 2])... + scalarize(LiquidOutPort.z[:, 3] .~ ControlVolumeState.z[:, 3])... + scalarize(LiquidOutPort.z[:, 1] .~ ControlVolumeState.z[:, 2])... + + LiquidOutPort.ṅ[2] ~ LiquidOutPort.ṅ[1] + LiquidOutPort.ṅ[3] ~ 1e-10 + + # Vapor outlet properties + VaporOutPort.h[2] ~ ρT_enthalpy(medium.EoSModel, ControlVolumeState.ρ[2], ControlVolumeState.T, collect(ControlVolumeState.z[:, 2])) + VaporOutPort.h[3] ~ ρT_enthalpy(medium.EoSModel, ControlVolumeState.ρ[3], ControlVolumeState.T, collect(ControlVolumeState.z[:, 3])) + VaporOutPort.h[1] ~ VaporOutPort.h[3] + + VaporOutPort.p ~ ControlVolumeState.p # Vapor pressure = tank pressure + scalarize(VaporOutPort.z[:, 2] .~ ControlVolumeState.z[:, 2])... + scalarize(VaporOutPort.z[:, 3] .~ ControlVolumeState.z[:, 3])... + scalarize(VaporOutPort.z[:, 1] .~ ControlVolumeState.z[:, 3])... + + VaporOutPort.ṅ[3] ~ VaporOutPort.ṅ[1] + VaporOutPort.ṅ[2] ~ 1e-10 + ] + + return ODESystem(eqs, t, collect(Iterators.flatten(vars)), pars; name, systems = [systems...]) +end + + +@component function ClosedControlVolume_(;medium, name) + + systems = @named begin + ControlVolumeState = ρTz_ThermodynamicState_(medium = medium) + end + + vars = @variables begin + rᵥ(t)[1:medium.Constants.Nc, 1:medium.Constants.nphases], [description = "mass source or sink - volumetric basis"] + rₐ(t)[1:medium.Constants.Nc, 1:medium.Constants.nphases], [description = "molar source or sink - through surface in contact with other phases"] + Nᵢ(t)[1:medium.Constants.Nc], [description = "molar holdup"] + nᴸⱽ(t)[1:medium.Constants.nphases - 1], [description = "molar holdup in each phase, excluding the overall phase"] + U(t), [description = "internal energy holdup"] + V(t)[1:medium.Constants.nphases], [description = "volume"] + Q(t), [description = "heat flux"] + Wₛ(t), [description = "shaft work"] + end + + pars = [] + + eqs = [ + + # Energy balance + + D(U) ~ Q + Wₛ + + # Mole balance + + [D(Nᵢ[i]) ~ sum(collect(rᵥ[i, 2:end].*V[2:end])) + rₐ[i, 1] for i in 1:medium.Constants.Nc]... + + scalarize(rᵥ[:, 1] .~ sum(collect(rᵥ[:, 2:end]), dims = 2))... + + scalarize(rₐ[:, 1] .~ sum(collect(rₐ[:, 2:end]), dims = 2))... + + [Nᵢ[i] ~ sum(dot(nᴸⱽ, collect(ControlVolumeState.z[i, 2:end]))) for i ∈ 1:medium.Constants.Nc]... + + scalarize(sum(Nᵢ) ~ sum(nᴸⱽ)) + + + # Thermodynamic state equations + + scalarize(ControlVolumeState.z[:, 1] .~ Nᵢ ./ sum(collect(Nᵢ)))... + + ControlVolumeState.ϕ[1] ~ nᴸⱽ[1]/sum(collect(nᴸⱽ)) + + + # Control Volume properties + + V[1] ~ sum(collect(V[2:end])) + + + ] + + return ODESystem(eqs, t, collect(Iterators.flatten(vars)), pars; name, systems = [systems...]) + +end + +""" +Struct wrapper for Boundary_pTzn component +""" +mutable struct Boundary_pTzn{M <: AbstractFluidMedium, S <: AbstractThermodynamicState} + medium::M + state::S + flowrate::Real + flowbasis::Symbol + odesystem + model_type::Symbol +end + +""" + Boundary_pTzn(; medium, state = nothing, p = nothing, T = nothing, z = nothing, flowrate, flowbasis = :molar, name) + +Create a fixed boundary condition (inlet/outlet stream) with specified state and flowrate. + +# Arguments +- `medium`: Medium specification +- `state`: Thermodynamic state (e.g., pTzState, pTNVState) [optional if p, T, z provided] +- `p`: Pressure [Pa] [optional if state provided] +- `T`: Temperature [K] [optional if state provided] +- `z`: Mole fraction vector [optional if state provided] +- `flowrate`: Flow rate (basis specified by `flowbasis`) +- `flowbasis`: Flow basis (`:molar`, `:mass`, or `:volumetric`) [default: `:molar`] +- `name`: Component name + +# Returns +- `Boundary_pTzn` struct with embedded ODESystem and model_type = :Feed + +# Examples +```julia +# Using state object +state = pTzState(10*101325.0, 350.15, [0.4, 0.6, 0.0]) +@named S1 = Boundary_pTzn(medium = medium, state = state, flowrate = 100.0) + +# Using p, T, z directly +@named S1 = Boundary_pTzn(medium = medium, p = 10*101325.0, T = 350.15, + z = [0.4, 0.6, 0.0], flowrate = 100.0) +``` +""" +function Boundary_pTzn(; medium, state = nothing, p = nothing, T = nothing, z = nothing, flowrate, flowbasis = :molar, name) + # Determine which form of input was provided + if !isnothing(state) + # Use the provided state + if !isnothing(p) || !isnothing(T) || !isnothing(z) + error("Cannot specify both 'state' and individual 'p', 'T', 'z' parameters") + end + elseif !isnothing(p) && !isnothing(T) && !isnothing(z) + # Create state from p, T, z + state = pTNVState(p, T, z, :Pressure) + else + error("Must provide either 'state' or all of 'p', 'T', and 'z'") + end + + # Use resolve_guess! to update medium and state (just like CSTR) + medium, state, _ = resolve_guess!(medium, state) + + # Extract p, T, z from state (works with both pTzState and pTNVState) + p_val = state.p + T_val = state.T + z_val = if hasproperty(state, :z) + state.z + elseif hasproperty(state, :N) + # Convert molar amounts to mole fractions + state.N ./ sum(state.N) + else + error("State must have either 'z' (mole fractions) or 'N' (molar amounts)") + end + + odesystem = Boundary_pTzn_Model(medium = medium, p = p_val, T = T_val, z = z_val, flowrate = flowrate, flowbasis = flowbasis, name = name) + return Boundary_pTzn(medium, state, flowrate, flowbasis, odesystem, :Feed) +end + +@component function Boundary_pTzn_Model(;medium, p, T, z, flowrate, name, flowbasis = :molar) + + systems = @named begin + OutPort = PhZConnector_(medium = medium) + ControlVolumeState = ρTz_ThermodynamicState_(medium = medium) + end + + vars = [] + + pars = [] + + eqs = [ + # Port equations + OutPort.p ~ p + scalarize(OutPort.z[:, 1] .~ z)... + scalarize(OutPort.z[:, 2:end] .~ ControlVolumeState.z[:, 2:end])... + + OutPort.ṅ[1] ~ -XtoMolar(flowrate, medium, ControlVolumeState, flowbasis) + OutPort.ṅ[2] ~ ControlVolumeState.ϕ[1]*OutPort.ṅ[1] + OutPort.ṅ[3] ~ ControlVolumeState.ϕ[2]*OutPort.ṅ[1] + #OutPort.ṅ[2] + OutPort.ṅ[3] ~ OutPort.ṅ[1] #Same as ∑ϕ = 1 + + #[OutPort.h[i] ~ ρT_enthalpy(medium.EoSModel, ControlVolumeState.ρ[i], ControlVolumeState.T, ControlVolumeState.z[:, i]) for i in 2:medium.Constants.nphases]... + [OutPort.h[j] ~ pT_enthalpy(medium.EoSModel, ControlVolumeState.p, ControlVolumeState.T, collect(ControlVolumeState.z[:, j]), medium.Constants.phase_names[j]) for j in 2:medium.Constants.nphases]... + OutPort.h[1] ~ dot(collect(OutPort.h[2:end]), collect(ControlVolumeState.ϕ)) + + # State equations + scalarize(ControlVolumeState.z[:, 1] .~ z)... + scalarize(ControlVolumeState.z[:, 2] .~ flash_mol_fractions_liquid(medium.EoSModel, p, T, z))... + scalarize(ControlVolumeState.z[:, 3] .~ flash_mol_fractions_vapor(medium.EoSModel, p, T, z))... + ControlVolumeState.T ~ T + ControlVolumeState.p ~ p + #scalarize(ControlVolumeState.ϕ[1] ~ flash_vaporized_fraction(medium.EoSModel, p, T, z)[1]) + ] + + return ODESystem(eqs, t, vars, collect(Iterators.flatten(pars)); name, systems = [systems...]) + + +end + +@component function FixedBoundary_pTz_(;medium, p, T, z, name) + + systems = @named begin + OutPort = PhZConnector_(medium = medium) + ControlVolumeState = ρTz_ThermodynamicState_(medium = medium) + end + + vars = [] + + pars = [] + + eqs = [ + # Port equations + OutPort.p ~ p + scalarize(OutPort.z[:, 1] .~ z)... + scalarize(OutPort.z[:, 2:end] .~ ControlVolumeState.z[:, 2:end])... + + OutPort.ṅ[2] ~ ControlVolumeState.ϕ[1]*OutPort.ṅ[1] + OutPort.ṅ[3] ~ ControlVolumeState.ϕ[2]*OutPort.ṅ[1] + + + [OutPort.h[i] ~ ρT_enthalpy(medium.EoSModel, ControlVolumeState.ρ[i], ControlVolumeState.T, collect(ControlVolumeState.z[:, i])) for i in 2:medium.Constants.nphases]... + OutPort.h[1] ~ dot(collect(OutPort.h[2:end]), collect(ControlVolumeState.ϕ)) + + # State equations + scalarize(ControlVolumeState.z[:, 1] .~ z)... + scalarize(ControlVolumeState.z[:, 2] .~ flash_mol_fractions_liquid(medium.EoSModel, p, T, z))... + scalarize(ControlVolumeState.z[:, 3] .~ flash_mol_fractions_vapor(medium.EoSModel, p, T, z))... + ControlVolumeState.T ~ T + ControlVolumeState.p ~ p + + ] + + return ODESystem(eqs, t, vars, collect(Iterators.flatten(pars)); name, systems = [systems...]) + + +end + +export ρTz_ThermodynamicState_, PhZConnector_, ConstantFlowRate, ConstantPressure, ConnHouse, SurfaceConnector +export Surface, TwoPortControlVolume_, TwoPortControlVolume_SteadyState +export ThreePortControlVolume_, ThreePortControlVolume_SteadyState +export ClosedControlVolume_, Boundary_pTzn, Boundary_pTzn_Model, FixedBoundary_pTzn_, FixedBoundary_pTz_ + + + diff --git a/src/base/materials.jl b/src/base/materials.jl deleted file mode 100644 index e35d981..0000000 --- a/src/base/materials.jl +++ /dev/null @@ -1,151 +0,0 @@ -abstract type AbstractFluidMedium end - -abstract type AbstractEoSBased <: AbstractFluidMedium end - -abstract type AbstractSinkSource end - -abstract type AbstractReaction <: AbstractSinkSource end - -const R = 8.31446261815324 # J/(mol K) - - -function XtoMolar(flowrate, medium, state, flowbasis) - if flowbasis == :molar - return flowrate - elseif flowbasis == :mass - return MasstoMolar(flowrate, medium, state) - elseif flowbasis == :volume - return VolumetoMolar(flowrate, medium, state) - else - error("Invalid flow basis: $flowbasis") - end -end - -function MasstoMolar(flowrate, medium::M, state::S) where {M <: AbstractFluidMedium} - M̄ = sum(state.z[:, 1] .* medium.FluidConstants.molarMass) #kg/mol - return flowrate / M̄ -end - -function VolumetoMolar(flowrate, medium::M, state::S) where {M <: AbstractFluidMedium} - return flowrate*state.ρ[1] -end - -struct PowerLawReaction{T <: Real, Arr <: AbstractArray{T}} <: AbstractReaction - species::Array{String} # Reactants and Products names - ν::Arr # Stoichiometry - n::Arr # Reaction order - A::T # Arrhenius constant - Eₐ::T # Activation energy -end - - -function _Rate(SinkSource::PowerLawReaction, cᵢ, T) - A, Eₐ, n, ν = SinkSource.A, SinkSource.Eₐ, SinkSource.n, SinkSource.ν - r = A * exp(-Eₐ / (R * T)) * prod(cᵢ[i]^n[i] for i in eachindex(cᵢ)) - return r.*ν -end - -Rate(SinkSource, cᵢ, T) = _Rate(SinkSource, cᵢ, T) - -Broadcast.broadcasted(::typeof(Rate), reactions, cᵢ, T) = broadcast(_Rate, reactions, Ref(cᵢ), T) - -struct LDFAdsorption{K <: AbstractArray} <: AbstractSinkSource - k::K # Mass transfer coefficient in 1/s. -end - -struct BasicFluidConstants{S <: Union{AbstractString, Nothing}, M <: Union{Real, AbstractVector{<: Real}}, N <: Int, V <: Union{Nothing, AbstractVector{<: AbstractString}}} - iupacName::S # "Complete IUPAC name (or common name, if non-existent)"; - casRegistryNumber::S # "Chemical abstracts sequencing number (if it exists)"; - chemicalFormula::S # "Chemical formula"; - structureFormula::S # "Chemical structure formula"; - molarMass::M # "Molar mass"; - Nc::N # "Number of components"; - nphases::N # "Maximum number of phases"; - phaseNames::V # "Label of the phases" -end - -##That should be part of the PropertyModels package - - -function BasicFluidConstants(molarMass::M) where M <: AbstractVector{<: Real} - Nc = length(molarMass) - return BasicFluidConstants(nothing, nothing, nothing, nothing, molarMass, Nc, 3, ["overall", "liquid", "vapor"]) -end - -struct EosBasedGuesses{M <: Any, V <: Real, D <: AbstractArray{V}, F <: AbstractArray{V}} - EoSModel::M - p::V #Pressure - T::V #Temperature - ρ::D #Molar density per phase - x::F #Mole fraction per phase - h::D #Molar enthalpy per phase - pᵇᵈ::D #Bubble and dew pressure -end - -function EosBasedGuesses(EoSModel::M, p::V, T::V, z::D) where {M <: Any, V <: Real, D <: AbstractArray{ <: Real}} - - ## Bubble and dew pressure - pᵇ = bubble_pressure(EoSModel, T, z)[1] - pᵈ = dew_pressure(EoSModel, T, z)[1] - pᵇᵈ = [pᵇ, pᵈ] - - Nc = length(z) - nᵢⱼ = zeros(V, Nc, 2) - x = zeros(V, Nc, 3) - ρ = zeros(V, 3) - h = zeros(V, 3) - - if p ≤ pᵇ && p ≥ pᵈ - sol = TP_flash(EoSModel, p, T, z) - ϕ = sol[1] - x .= sol[2] - ρₗ = PT_molar_density(EoSModel, p, T, x[:, 1], phase = "liquid") #Assumes only two phases - ρᵥ = PT_molar_density(EoSModel, p, T, x[:, 2], phase = "vapor") - ρₒᵥ = 1.0/(ϕ[1]/ρₗ + ϕ[2]/ρᵥ) - ρ .= [ρₒᵥ, ρₗ, ρᵥ] - - ## Enthalpy - hₗ = ρT_enthalpy(EoSModel, ρ[2], T, x[:, 2]) - hᵥ = ρT_enthalpy(EoSModel, ρ[3], T, x[:, 3]) - hₒᵥ = hₗ*ϕ[1] + hᵥ*ϕ[2] - h .= [hₒᵥ, hₗ, hᵥ] - - elseif p > pᵇ - nᵢⱼ[:, 1] .= z - x[:, 1:2] .= z - ϕ = sum(nᵢⱼ, dims = 1)/sum(nᵢⱼ) - println(ϕ) - ρ[2] = PT_molar_density(EoSModel, p, T, z, phase = "liquid") - ρ[3] = PT_molar_density(EoSModel, p, T, z, phase = "vapor") - ρ[1] = 1.0/(ϕ[1]/ρ[2] + ϕ[2]/ρ[3]) - hₗ = ρT_enthalpy(EoSModel, ρ[2], T, z) - hᵥ = ρT_enthalpy(EoSModel, ρ[3], T, z) - hₒᵥ = hₗ*ϕ[1] + hᵥ*ϕ[2] - h .= [hₒᵥ, hₗ, hᵥ] - - elseif p < pᵈ - nᵢⱼ[:, 2] .= z - x[:, [1, 3]] .= z - ϕ = sum(nᵢⱼ, dims = 1)/sum(nᵢⱼ) - ρ[2] = PT_molar_density(EoSModel, p, T, z, phase = "liquid") - ρ[3] = PT_molar_density(EoSModel, p, T, z, phase = "vapor") - ρ[1] = 1.0/(ϕ[1]/ρ[2] + ϕ[2]/ρ[3]) - hₗ = ρT_enthalpy(EoSModel, ρ[2], T, z) - hᵥ = ρT_enthalpy(EoSModel, ρ[3], T, z) - hₒᵥ = hₗ*ϕ[1] + hᵥ*ϕ[2] - h .= [hₒᵥ, hₗ, hᵥ] - end - - - return EosBasedGuesses(EoSModel, p, T, ρ, x, h, pᵇᵈ) -end - -struct EoSBased{F <: BasicFluidConstants, E <: Any, G <: EosBasedGuesses} <: AbstractEoSBased - Constants::F - EoSModel::E - Guesses::G -end - - - - diff --git a/src/base/print.jl b/src/base/print.jl new file mode 100644 index 0000000..beb56de --- /dev/null +++ b/src/base/print.jl @@ -0,0 +1,539 @@ +using UnicodePlots +using Printf + +function Base.show(io::IO, valve::Valve) + # Header with valve type and phase + println(io, "Valve Model ($(valve.phase))") + println(io, "=" ^ 60) + + # Flow coefficient and opening setpoint + println(io, "Flow Coefficient (Cv): $(valve.Cv)") + println(io, "Valve Opening Setpoint: $(valve.opening_setpoint * 100)% open") + + # Pressure drop function representation + println(io, "\nPressure Drop Equation:") + println(io, " ΔP = f(Δp) where:") + + # Function representation with Unicode plot - with improved debugging + if hasproperty(valve, :f) + # Plot the function using UnicodePlots - only for positive pressure drops + try + # Test if the function can be evaluated + test_val = try + val = valve.f(100.0) + println(io, " • Sample evaluation: f(100.0) = $val") + val + catch e + println(io, " • Function evaluation failed: $(sprint(showerror, e))") + nothing + end + + if test_val !== nothing + # Only positive pressure drops are physically meaningful for a valve + x_range = range(1.0, 100_000.0, length=40) # Reduced number of points for stability + y_values = Float64[] + x_valid = Float64[] + + # Evaluate points individually to identify issues + for x in x_range + try + y = valve.f(x) + if !isnan(y) && !isinf(y) + push!(x_valid, x) + push!(y_values, y) + end + catch + # Skip problematic points + end + end + + # Check if we have any valid points + if !isempty(y_values) + # Create plot + plt = lineplot( + x_valid, + y_values, + title="Valve Function Behavior", + xlabel="Pressure Drop (Pa)", + ylabel="Flow Factor", + width=30, + height=10, + border=:ascii + ) + + # Print the plot + println(io, "\n • Function visualization (for positive pressure drops):") + show(io, plt) + println(io) + else + println(io, "\n • Function visualization unavailable (no valid points to plot)") + end + else + println(io, "\n • Function visualization unavailable (function evaluation failed)") + end + catch e + println(io, "\n • Function visualization unavailable: $(sprint(showerror, e))") + println(io, " $(typeof(e)): $(e)") + end + else + println(io, "\n • Function property not found on valve object") + end + + println(io, " • Flow equation: ṅ/ρ = opening·Cv·f(Pin - Pout)") + + # State guess section (thermodynamic state) + println(io, "\nThermodynamic State Guess:") + println(io, " • Pressure: $(valve.state.p) Pa") + println(io, " • Temperature: $(valve.state.T) K") + println(io, " • Composition: $(valve.state.z)") + + # Density information from medium's guesses if available + if hasproperty(valve.medium, :Guesses) && hasproperty(valve.medium.Guesses, :ρ) + println(io, " • Density:") + for i in eachindex(valve.medium.Guesses.ρ) + phase_name = i == 1 ? "overall" : (i == 2 ? "liquid" : "vapor") + println(io, " - $(phase_name): $(round(valve.medium.Guesses.ρ[i], digits=3)) mol/m³") + end + end + + # Molar flowrate guess + println(io, "\nMolar Flowrate Guess: $(valve.molar_flowrate_guess) mol/s") + + # Medium information with detailed breakdown + println(io, "\nMedium Details:") + if isa(valve.medium, EoSBased) + # EOS Model information + eos_type = split(string(typeof(valve.medium.EoSModel)), "{")[1] + println(io, " • EoS Model: $eos_type") + + # Components information + if hasproperty(valve.medium.Constants, :iupacName) && !isnothing(valve.medium.Constants.iupacName) + println(io, " • Components: $(valve.medium.Constants.iupacName)") + if hasproperty(valve.medium.Constants, :molarMass) && !isnothing(valve.medium.Constants.molarMass) + mw_str = join(["$(valve.medium.Constants.iupacName[i]): $(valve.medium.Constants.molarMass[i]) kg/mol" + for i in 1:length(valve.medium.Constants.iupacName)], ", ") + println(io, " • Molar masses: $mw_str") + end + end + + # Transport model breakdown + if hasproperty(valve.medium, :TransportModel) && !isnothing(valve.medium.TransportModel) + println(io, " • Transport Models:") + + # Mass transfer model + if hasproperty(valve.medium.TransportModel, :MassTransferModel) && + !isnothing(valve.medium.TransportModel.MassTransferModel) + mass_model = typeof(valve.medium.TransportModel.MassTransferModel) + println(io, " - Mass Transfer: $(split(string(mass_model), "{")[1])") + if isa(valve.medium.TransportModel.MassTransferModel, ConstantMassTransferCoeff) + println(io, " Coefficients: $(valve.medium.TransportModel.MassTransferModel.k)") + end + else + println(io, " - Mass Transfer: None") + end + + # Heat transfer model + if hasproperty(valve.medium.TransportModel, :HeatTransferModel) && + !isnothing(valve.medium.TransportModel.HeatTransferModel) + heat_model = typeof(valve.medium.TransportModel.HeatTransferModel) + println(io, " - Heat Transfer: $(split(string(heat_model), "{")[1])") + if isa(valve.medium.TransportModel.HeatTransferModel, ConstantHeatTransferCoeff) + println(io, " Coefficient: $(valve.medium.TransportModel.HeatTransferModel.k)") + end + else + println(io, " - Heat Transfer: None") + end + + # Viscosity model + if hasproperty(valve.medium.TransportModel, :ViscosityModel) && + !isnothing(valve.medium.TransportModel.ViscosityModel) + visc_model = typeof(valve.medium.TransportModel.ViscosityModel) + println(io, " - Viscosity: $(split(string(visc_model), "{")[1])") + else + println(io, " - Viscosity: None") + end + end + else + # For non-EoSBased media + medium_type = split(string(typeof(valve.medium)), "{")[1] + println(io, " • Type: $medium_type") + end + + # End with a separator + println(io, "=" ^ 60) +end + + + + +function Base.show(io::IO, model::SolidEoSModel) + # Header with type name + println(io, "SolidEoSModel") + println(io, "=" ^ 50) + + # Density section + println(io, "\nDensity Parameters:") + println(io, " • Base density (ρ0): $(round(model.ρ0, digits=3)) kg/m³") + println(io, " • Reference temperature (ρ_T0): $(round(model.ρ_T0, digits=3)) K") + + # Print density coefficients + println(io, " • Temperature coefficients:") + if all(x -> x === nothing || x == 0, model.coeffs_ρ) + println(io, " - No temperature dependence") + else + for (i, coef) in enumerate(model.coeffs_ρ) + if coef !== nothing + println(io, " - Order $(i): $(round(coef, digits=6))") + end + end + + # Show density equation + println(io, "\n • Density equation: ρ(T) = ρ0 + Σ coeff_i * ΔT^i") + print(io, " where ρ(T) = $(round(model.ρ0, digits=2))") + + for (i, coef) in enumerate(model.coeffs_ρ) + if coef !== nothing && coef != 0 + sign = coef > 0 ? " + " : " - " + print(io, "$(sign)$(round(abs(coef), digits=6)) * ΔT^$(i)") + end + end + println(io) + println(io, " with ΔT = T - $(round(model.ρ_T0, digits=2))") + end + + # Enthalpy section + println(io, "\nEnthalpy Parameters:") + println(io, " • Base enthalpy (h0): $(round(model.h0, digits=3)) J/mol") + println(io, " • Reference temperature (h_T0): $(round(model.h_T0, digits=3)) K") + + # Print enthalpy coefficients + println(io, " • Temperature coefficients:") + if all(x -> x === nothing || x == 0, model.coeffs_h) + println(io, " - No temperature dependence") + else + for (i, coef) in enumerate(model.coeffs_h) + if coef !== nothing + println(io, " - Order $(i): $(round(coef, digits=6))") + end + end + + # Show enthalpy equation + println(io, "\n • Enthalpy equation: h(T) = h0 + Σ coeff_i * ΔT^i") + print(io, " where h(T) = $(round(model.h0, digits=2))") + + for (i, coef) in enumerate(model.coeffs_h) + if coef !== nothing && coef != 0 + sign = coef > 0 ? " + " : " - " + print(io, "$(sign)$(round(abs(coef), digits=6)) * ΔT^$(i)") + end + end + println(io) + println(io, " with ΔT = T - $(round(model.h_T0, digits=2))") + end + + # End with separator + println(io, "=" ^ 50) +end + + +function Base.show(io::IO, fluid::EoSBased) + # Header with type + println(io, "EoSBased Fluid Medium") + println(io, "=" ^ 50) + + # Constants section + println(io, "\nFluid Constants:") + println(io, " • Number of components: $(fluid.Constants.Nc)") + println(io, " • Number of phases: $(fluid.Constants.nphases)") + + # Components information + if !isnothing(fluid.Constants.iupacName) + println(io, " • Components: $(fluid.Constants.iupacName)") + + # Molar masses if available + if !isnothing(fluid.Constants.molarMass) + println(io, " • Molar masses [kg/mol]:") + for i in 1:fluid.Constants.Nc + name = fluid.Constants.iupacName[i] + mass = fluid.Constants.molarMass[i] + println(io, " - $(name): $(round(mass, digits=6))") + end + end + end + + # EoS Model information + println(io, "\nEquation of State:") + eos_type = split(string(typeof(fluid.EoSModel)), "{")[1] + println(io, " • Model: $(eos_type)") + + # Transport model section + println(io, "\nTransport Models:") + + # Mass transfer model + if hasproperty(fluid.TransportModel, :MassTransferModel) && !isnothing(fluid.TransportModel.MassTransferModel) + mass_model = typeof(fluid.TransportModel.MassTransferModel) + println(io, " • Mass Transfer: $(split(string(mass_model), "{")[1])") + if isa(fluid.TransportModel.MassTransferModel, ConstantMassTransferCoeff) + println(io, " - Coefficients: $(fluid.TransportModel.MassTransferModel.k)") + end + end + + # Heat transfer model + if hasproperty(fluid.TransportModel, :HeatTransferModel) && !isnothing(fluid.TransportModel.HeatTransferModel) + heat_model = typeof(fluid.TransportModel.HeatTransferModel) + println(io, " • Heat Transfer: $(split(string(heat_model), "{")[1])") + if isa(fluid.TransportModel.HeatTransferModel, ConstantHeatTransferCoeff) + println(io, " - Coefficient: $(fluid.TransportModel.HeatTransferModel.k)") + end + end + + # Viscosity model + if hasproperty(fluid.TransportModel, :ViscosityModel) && !isnothing(fluid.TransportModel.ViscosityModel) + visc_model = typeof(fluid.TransportModel.ViscosityModel) + println(io, " • Viscosity: $(split(string(visc_model), "{")[1])") + end + + # Current state guesses + println(io, "\nCurrent State Guesses:") + println(io, " • Pressure: $(round(fluid.Guesses.p, digits=2)) Pa") + println(io, " • Temperature: $(round(fluid.Guesses.T, digits=2)) K") + + # Vapor fraction + if hasproperty(fluid.Guesses, :ϕ) && !isnothing(fluid.Guesses.ϕ) + println(io, " • Phase fractions: liquid=$(round(fluid.Guesses.ϕ[1], digits=4)), vapor=$(round(fluid.Guesses.ϕ[2], digits=4))") + end + + # Densities + println(io, " • Densities [mol/m³]:") + for i in eachindex(fluid.Guesses.ρ) + phase_name = i == 1 ? "overall" : (i == 2 ? "liquid" : "vapor") + println(io, " - $(phase_name): $(round(fluid.Guesses.ρ[i], digits=3))") + end + + # Enthalpies + println(io, " • Enthalpies [J/mol]:") + for i in eachindex(fluid.Guesses.h) + phase_name = i == 1 ? "overall" : (i == 2 ? "liquid" : "vapor") + println(io, " - $(phase_name): $(round(fluid.Guesses.h[i], digits=3))") + end + + # Compositions + println(io, " • Compositions:") + for i in 1:size(fluid.Guesses.x, 2) + phase_name = i == 1 ? "overall" : (i == 2 ? "liquid" : "vapor") + comp_str = join(["$(round(fluid.Guesses.x[j, i], digits=4))" for j in 1:fluid.Constants.Nc], ", ") + println(io, " - $(phase_name): [$(comp_str)]") + end + + # End with separator + println(io, "=" ^ 50) +end + + +# ============================================================================ +# Pretty Printing for PowerLawReaction +# ============================================================================ + +""" + Base.show(io::IO, rxn::PowerLawReaction) + +Pretty print a PowerLawReaction showing the reaction equation, kinetics, and parameters. +""" +function Base.show(io::IO, rxn::PowerLawReaction) + # Build reaction equation string + reactants = String[] + products = String[] + + for (i, sp) in enumerate(rxn.species) + ν_val = rxn.ν[i] + if ν_val < 0 + # Reactant + coeff = abs(ν_val) + if coeff ≈ 1.0 + push!(reactants, sp) + else + coeff_str = isinteger(coeff) ? string(Int(coeff)) : string(coeff) + push!(reactants, "$(coeff_str)$(sp)") + end + elseif ν_val > 0 + # Product + coeff = ν_val + if coeff ≈ 1.0 + push!(products, sp) + else + coeff_str = isinteger(coeff) ? string(Int(coeff)) : string(coeff) + push!(products, "$(coeff_str)$(sp)") + end + end + end + + reactant_str = join(reactants, " + ") + product_str = join(products, " + ") + equation = isempty(products) ? reactant_str : "$(reactant_str) → $(product_str)" + + # Build rate law string + rate_terms = String[] + overall_order = 0.0 + + for (i, sp) in enumerate(rxn.species) + n_val = rxn.n[i] + if n_val != 0 + overall_order += n_val + if n_val ≈ 1.0 + push!(rate_terms, "[$(sp)]") + else + order_str = isinteger(n_val) ? string(Int(n_val)) : string(n_val) + push!(rate_terms, "[$(sp)]^$(order_str)") + end + end + end + + rate_law = isempty(rate_terms) ? "1" : join(rate_terms, " × ") + + # Format Arrhenius parameters + A_str = @sprintf("%.3e", rxn.A) + Eₐ_str = @sprintf("%.3e", rxn.Eₐ) + + # Print everything nicely + println(io, "PowerLawReaction") + println(io, " Reaction: $(equation)") + println(io, " Rate Law: r = k(T) × $(rate_law)") + println(io, " where k(T) = A × exp(-Eₐ/RT)") + println(io, " ") + println(io, " Kinetic Parameters:") + println(io, " A = $(A_str) (pre-exponential factor)") + println(io, " Eₐ = $(Eₐ_str) J/mol (activation energy)") + println(io, " ") + println(io, " Overall Order: $(overall_order)") + println(io, " ") + println(io, " Species Details:") + + # Print table header + println(io, " ┌────────────────┬──────────────┬──────────────┐") + println(io, " │ Species │ Stoich. (ν) │ Order (n) │") + println(io, " ├────────────────┼──────────────┼──────────────┤") + + # Print each species + for (i, sp) in enumerate(rxn.species) + sp_padded = rpad(sp, 14) + ν_str = @sprintf("%.2f", rxn.ν[i]) + n_str = @sprintf("%.2f", rxn.n[i]) + ν_padded = lpad(ν_str, 12) + n_padded = lpad(n_str, 12) + println(io, " │ $(sp_padded) │ $(ν_padded) │ $(n_padded) │") + end + + print(io, " └────────────────┴──────────────┴──────────────┘") +end + +""" + Base.show(io::IO, ::MIME"text/plain", rxn::PowerLawReaction) + +Compact display for PowerLawReaction in REPL. +""" +function Base.show(io::IO, ::MIME"text/plain", rxn::PowerLawReaction) + show(io, rxn) +end + + +""" + Base.show(io::IO, network::PowerLawReactionSet) + +Pretty print a reaction network showing all reactions and the stoichiometric/order matrices. +""" +function Base.show(io::IO, network::PowerLawReactionSet) + Nc = length(network.species) + Nr = length(network.reactions) + + println(io, "PowerLawReactionSet") + println(io, " Number of species: $(Nc)") + println(io, " Number of reactions: $(Nr)") + println(io, " ") + println(io, " Reactions:") + + for j in 1:Nr + # Build equation string + reactants = String[] + products = String[] + + for i in 1:Nc + ν_val = network.ν[i, j] + sp = network.species[i] + + if ν_val < 0 + coeff = abs(ν_val) + if coeff ≈ 1.0 + push!(reactants, sp) + else + coeff_str = isinteger(coeff) ? string(Int(coeff)) : @sprintf("%.1f", coeff) + push!(reactants, "$(coeff_str)$(sp)") + end + elseif ν_val > 0 + coeff = ν_val + if coeff ≈ 1.0 + push!(products, sp) + else + coeff_str = isinteger(coeff) ? string(Int(coeff)) : @sprintf("%.1f", coeff) + push!(products, "$(coeff_str)$(sp)") + end + end + end + + reactant_str = join(reactants, " + ") + product_str = join(products, " + ") + equation = "$(reactant_str) → $(product_str)" + + A_str = @sprintf("%.2e", network.A[j]) + Eₐ_str = @sprintf("%.2e", network.Eₐ[j]) + + println(io, " R$(j): $(equation)") + println(io, " A = $(A_str), Eₐ = $(Eₐ_str) J/mol") + end + + println(io, " ") + println(io, " Stoichiometric Matrix ν (Nc × Nr):") + print_matrix(io, network.ν, network.species, ["R$j" for j in 1:Nr], " ") + + println(io, " ") + println(io, " Reaction Order Matrix n (Nc × Nr):") + print_matrix(io, network.n, network.species, ["R$j" for j in 1:Nr], " ") +end + +function Base.show(io::IO, ::MIME"text/plain", network::PowerLawReactionSet) + show(io, network) +end + +""" + print_matrix(io::IO, mat::Matrix, row_labels::Vector{String}, col_labels::Vector{String}, indent::String) + +Helper function to print a labeled matrix in a nice table format. +""" +function print_matrix(io::IO, mat::Matrix, row_labels::Vector{String}, col_labels::Vector{String}, indent::String) + Nrows, Ncols = size(mat) + + # Determine column widths + label_width = maximum(length.(row_labels)) + col_width = 10 + + # Print header + print(io, indent) + print(io, rpad("", label_width + 2)) + for label in col_labels + print(io, lpad(label, col_width)) + end + println(io) + + # Print separator + println(io, indent, "─"^(label_width + 2 + col_width * Ncols)) + + # Print rows + for i in 1:Nrows + print(io, indent) + print(io, rpad(row_labels[i], label_width + 2)) + for j in 1:Ncols + val_str = @sprintf("%.2f", mat[i, j]) + print(io, lpad(val_str, col_width)) + end + println(io) + end +end \ No newline at end of file diff --git a/src/base/solution_formatter.jl b/src/base/solution_formatter.jl new file mode 100644 index 0000000..6abceb1 --- /dev/null +++ b/src/base/solution_formatter.jl @@ -0,0 +1,436 @@ +using Printf +using PrettyTables + +""" + extract_cstr_solution(sol, sys, cstr_name::Symbol; time_index=:end, components::Vector{String}) + +Extract CSTR solution data from solved system and format as tables. +""" +function extract_cstr_solution(sol, sys, cstr_name::Symbol; time_index=:end, components::Vector{String}) + # Handle both ODESolution (has time) and NonlinearSolution (steady-state, no time) + t_idx = if hasproperty(sol, :t) + time_index == :end ? length(sol.t) : time_index + else + 1 # NonlinearSolution - just one solution point + end + + Nc = length(components) + + result = Dict{Symbol, Any}() + + # STREAM TABLE + stream_data = Matrix{Any}(undef, 7, 3) + stream_data[:, 1] = ["Temperature", "Pressure", "Total Molar Flow", + "Liquid Molar Flow", "Vapor Molar Flow", + "Molar Enthalpy", "Liquid Fraction"] + + # Inlet - Note: Ports don't have T directly, use ControlVolumeState for temperature + try + reactor = getproperty(sys, cstr_name) + stream_data[1, 2] = "N/A" # Temperature not available at port + stream_data[2, 2] = sol[reactor.InPort.p][t_idx] / 1e5 + stream_data[3, 2] = abs(sol[reactor.InPort.ṅ[1]][t_idx]) + stream_data[4, 2] = abs(sol[reactor.InPort.ṅ[2]][t_idx]) + stream_data[5, 2] = abs(sol[reactor.InPort.ṅ[3]][t_idx]) + stream_data[6, 2] = sol[reactor.InPort.h[1]][t_idx] + stream_data[7, 2] = "-" + catch e + @warn "Could not extract inlet stream data for $cstr_name" exception=(e, catch_backtrace()) + stream_data[:, 2] .= "N/A" + end + + # Outlet + try + reactor = getproperty(sys, cstr_name) + stream_data[1, 3] = sol[reactor.ControlVolumeState.T][t_idx] # Use reactor temperature + stream_data[2, 3] = sol[reactor.OutPort.p][t_idx] / 1e5 + stream_data[3, 3] = abs(sol[reactor.OutPort.ṅ[1]][t_idx]) + stream_data[4, 3] = abs(sol[reactor.OutPort.ṅ[2]][t_idx]) + stream_data[5, 3] = abs(sol[reactor.OutPort.ṅ[3]][t_idx]) + stream_data[6, 3] = sol[reactor.OutPort.h[1]][t_idx] + stream_data[7, 3] = "-" + catch e + @warn "Could not extract outlet stream data for $cstr_name" exception=(e, catch_backtrace()) + stream_data[:, 3] .= "N/A" + end + + result[:streams] = stream_data + + # Units + result[:stream_units] = ["K", "bar", "kmol/hr", "kmol/hr", "kmol/hr", "kJ/kmol", ""] + + # COMPOSITION TABLE + comp_data = Matrix{Any}(undef, Nc, 3) + reactor = getproperty(sys, cstr_name) + for (i, comp) in enumerate(components) + comp_data[i, 1] = comp + try + comp_data[i, 2] = sol[reactor.InPort.z[i,1]][t_idx] + catch + comp_data[i, 2] = "N/A" + end + try + comp_data[i, 3] = sol[reactor.OutPort.z[i,1]][t_idx] + catch + comp_data[i, 3] = "N/A" + end + end + result[:composition] = comp_data + + # EQUIPMENT TABLE + equip_data = Matrix{Any}(undef, 9, 2) + equip_data[:, 1] = ["Total Volume", "Liquid Volume", "Vapor Volume", + "Temperature", "Pressure", "Liquid Fraction", + "Internal Energy", "Heat Duty", "Shaft Work"] + + try + reactor = getproperty(sys, cstr_name) + equip_data[1, 2] = sol[reactor.V[1]][t_idx] + equip_data[2, 2] = sol[reactor.V[2]][t_idx] + equip_data[3, 2] = sol[reactor.V[3]][t_idx] + equip_data[4, 2] = sol[reactor.ControlVolumeState.T][t_idx] + equip_data[5, 2] = sol[reactor.ControlVolumeState.p][t_idx] / 1e5 + equip_data[6, 2] = sol[reactor.ControlVolumeState.ϕ[1]][t_idx] + equip_data[7, 2] = sol[reactor.U][t_idx] + equip_data[8, 2] = sol[reactor.Q][t_idx] + equip_data[9, 2] = sol[reactor.Wₛ][t_idx] + catch e + @warn "Could not extract equipment data for $cstr_name" exception=(e, catch_backtrace()) + equip_data[:, 2] .= "N/A" + end + result[:equipment] = equip_data + result[:equip_units] = ["m3", "m3", "m3", "K", "bar", "", "kJ", "kW", "kW"] + + # HOLDUP TABLE + holdup_data = Matrix{Any}(undef, Nc, 2) + reactor = getproperty(sys, cstr_name) + for (i, comp) in enumerate(components) + holdup_data[i, 1] = comp + try + holdup_data[i, 2] = sol[reactor.Nᵢ[i]][t_idx] + catch + holdup_data[i, 2] = "N/A" + end + end + result[:holdup] = holdup_data + + # CONVERSION TABLE + conversion_data = Matrix{Any}(undef, Nc, 2) + reactor = getproperty(sys, cstr_name) + for (i, comp) in enumerate(components) + conversion_data[i, 1] = comp + try + X_val = sol[reactor.X[i]][t_idx] + conversion_data[i, 2] = X_val * 100 + catch + conversion_data[i, 2] = "N/A" + end + end + result[:conversion] = conversion_data + + return result +end + +""" + extract_boundary_solution(sol, sys, boundary_name::Symbol; time_index=:end, components::Vector{String}) + +Extract fixed boundary solution data from solved system. +""" +function extract_boundary_solution(sol, sys, boundary_name::Symbol; time_index=:end, components::Vector{String}) + # Handle both ODESolution (has time) and NonlinearSolution (steady-state, no time) + t_idx = if hasproperty(sol, :t) + time_index == :end ? length(sol.t) : time_index + else + 1 # NonlinearSolution - just one solution point + end + + Nc = length(components) + result = Dict{Symbol, Any}() + + # STREAM TABLE - Only outlet for boundary + stream_data = Matrix{Any}(undef, 7, 2) + stream_data[:, 1] = ["Temperature", "Pressure", "Total Molar Flow", + "Liquid Molar Flow", "Vapor Molar Flow", + "Molar Enthalpy", "Liquid Fraction"] + + try + boundary = getproperty(sys, boundary_name) + stream_data[1, 2] = sol[boundary.ControlVolumeState.T][t_idx] + stream_data[2, 2] = sol[boundary.OutPort.p][t_idx] / 1e5 + stream_data[3, 2] = abs(sol[boundary.OutPort.ṅ[1]][t_idx]) + stream_data[4, 2] = abs(sol[boundary.OutPort.ṅ[2]][t_idx]) + stream_data[5, 2] = abs(sol[boundary.OutPort.ṅ[3]][t_idx]) + stream_data[6, 2] = sol[boundary.OutPort.h[1]][t_idx] + stream_data[7, 2] = sol[boundary.ControlVolumeState.ϕ[1]][t_idx] + catch e + @warn "Could not extract boundary stream data for $boundary_name" exception=(e, catch_backtrace()) + stream_data[:, 2] .= "N/A" + end + + result[:streams] = stream_data + result[:stream_units] = ["K", "bar", "kmol/hr", "kmol/hr", "kmol/hr", "kJ/kmol", ""] + + # COMPOSITION TABLE + comp_data = Matrix{Any}(undef, Nc, 2) + boundary = getproperty(sys, boundary_name) + for (i, comp) in enumerate(components) + comp_data[i, 1] = comp + try + comp_data[i, 2] = sol[boundary.OutPort.z[i,1]][t_idx] + catch + comp_data[i, 2] = "N/A" + end + end + result[:composition] = comp_data + + return result +end + +""" + print_boundary_report(sol, sys, boundary_name::Symbol, components::Vector{String}; time_index=:end) + +Print boundary stream report in Aspen Plus style. +""" +function print_boundary_report(sol, sys, boundary_name::Symbol, components::Vector{String}; time_index=:end) + data = extract_boundary_solution(sol, sys, boundary_name; time_index=time_index, components=components) + io = stdout + + # Title Block + println(io, "\n") + println(io, " " * "="^78) + println(io, " " * " "^78) + println(io, center_string("BLOCK: $(uppercase(String(boundary_name))) MODEL: BOUNDARY", 78)) + println(io, " " * " "^78) + println(io, " " * "="^78) + println(io) + + # OUTLET STREAM SECTION + println(io, "\n OUTLET MATERIAL STREAM") + println(io, " " * "-"^78) + pretty_table(io, data[:streams], + header = ["", "OUTLET"], + alignment = [:l, :r], + formatters = (ft_printf("%.4f", 2),), + tf = tf_simple) + + # COMPOSITION SECTION + println(io, "\n COMPOSITION (MOLE FRACTION)") + println(io, " " * "-"^78) + pretty_table(io, data[:composition], + header = ["Component", "Outlet"], + alignment = [:l, :r], + formatters = (ft_printf("%.6f", 2),), + tf = tf_simple) + + println(io, "\n" * " " * "="^78 * "\n") +end + +""" + print_cstr_report(sol, sys, cstr_name::Symbol, components::Vector{String}; time_index=:end) + +Print CSTR report in Aspen Plus style using PrettyTables. +""" +function print_cstr_report(sol, sys, cstr_name::Symbol, components::Vector{String}; time_index=:end) + data = extract_cstr_solution(sol, sys, cstr_name; time_index=time_index, components=components) + io = stdout + + # Title Block + println(io, "\n") + println(io, " " * "="^78) + println(io, " " * " "^78) + println(io, center_string("BLOCK: $(uppercase(String(cstr_name))) MODEL: RCSTR", 78)) + println(io, " " * " "^78) + println(io, " " * "="^78) + println(io) + + # INLET STREAMS SECTION + println(io, "\n INLET MATERIAL STREAM") + println(io, " " * "-"^78) + pretty_table(io, data[:streams], + header = ["", "INLET", "OUTLET"], + alignment = [:l, :r, :r], + formatters = (ft_printf("%.4f", [2, 3]),), + tf = tf_simple) + + # COMPOSITION SECTION + println(io, "\n COMPOSITION (MOLE FRACTION)") + println(io, " " * "-"^78) + pretty_table(io, data[:composition], + header = ["Component", "Inlet", "Outlet"], + alignment = [:l, :r, :r], + formatters = (ft_printf("%.6f", [2, 3]),), + tf = tf_simple) + + # EQUIPMENT SPECIFICATIONS + println(io, "\n EQUIPMENT SPECIFICATIONS") + println(io, " " * "-"^78) + pretty_table(io, data[:equipment], + header = ["Property", "Value"], + alignment = [:l, :r], + formatters = (ft_printf("%.6e", 2),), + tf = tf_simple) + + # COMPONENT HOLDUP + println(io, "\n COMPONENT HOLDUP (mol)") + println(io, " " * "-"^78) + pretty_table(io, data[:holdup], + header = ["Component", "Holdup"], + alignment = [:l, :r], + formatters = (ft_printf("%.4f", 2),), + tf = tf_simple) + + # CONVERSION + println(io, "\n CONVERSION") + println(io, " " * "-"^78) + pretty_table(io, data[:conversion], + header = ["Component", "Conversion (%)"], + alignment = [:l, :r], + formatters = (ft_printf("%.2f", 2),), + tf = tf_simple) + + println(io, "\n" * " " * "="^78 * "\n") +end + +""" + print_flowsheet_summary(sol, sys, unit_ops::Dict{Symbol, Symbol}, components::Vector{String}) + +Print complete flowsheet summary report (Aspen Plus style). + +# Arguments +- `sol`: Solution from solver +- `sys`: Simplified/compiled system +- `unit_ops`: Dict mapping unit names to model types (e.g., Dict(:R1 => :CSTR, :S1 => :Feed)) +- `components`: Vector of component names +""" +function print_flowsheet_summary(sol, sys, unit_ops::Dict{Symbol, Symbol}, components::Vector{String}) + io = stdout + + # Main Header + println(io, "\n\n") + println(io, " " * "="^78) + println(io, center_string("PROCESS SIMULATION RESULTS", 78)) + println(io, center_string("ProcessSimulator.jl - Equation-Based Flowsheet Simulator", 78)) + println(io, " " * "="^78) + + # Simulation Summary + println(io, "\n\n SIMULATION SUMMARY") + println(io, " " * "-"^78) + + summary_data = Matrix{Any}(undef, 4, 2) + summary_data[1, :] = ["Run Status", string(sol.retcode)] + summary_data[2, :] = ["Solution Time", "Steady State"] + summary_data[3, :] = ["Number of Components", length(components)] + summary_data[4, :] = ["Components", join(components, ", ")] + + pretty_table(io, summary_data, + header = ["Parameter", "Value"], + alignment = [:l, :l], + tf = tf_simple) + + # Unit Operations List + println(io, "\n\n UNIT OPERATIONS") + println(io, " " * "-"^78) + + units_data = Matrix{Any}(undef, length(unit_ops), 2) + for (idx, (name, type)) in enumerate(unit_ops) + units_data[idx, 1] = String(name) + units_data[idx, 2] = String(type) + end + + pretty_table(io, units_data, + header = ["Block ID", "Model"], + alignment = [:l, :l], + tf = tf_simple) + + # Print each unit operation report + for (unit_name, unit_type) in unit_ops + if unit_type == :CSTR + print_cstr_report(sol, sys, unit_name, components) + elseif unit_type == :Boundary || unit_type == :Feed + print_boundary_report(sol, sys, unit_name, components) + elseif unit_type == :FlashDrum + println(io, "\n BLOCK: $(uppercase(String(unit_name))) MODEL: FLASH") + println(io, " " * "-"^78) + println(io, " [Flash drum report not yet implemented]") + println(io) + else + println(io, "\n BLOCK: $(uppercase(String(unit_name))) MODEL: $(uppercase(String(unit_type)))") + println(io, " " * "-"^78) + println(io, " [Report formatter not yet implemented]") + println(io) + end + end + + # Footer + println(io, "\n" * " " * "="^78) + println(io, center_string("END OF REPORT", 78)) + println(io, " " * "="^78 * "\n") +end + +""" + center_string(s::String, width::Int) + +Center string within specified width. +""" +function center_string(s::String, width::Int) + len = length(s) + if len >= width + return s[1:width] + end + left_pad = div(width - len, 2) + right_pad = width - len - left_pad + return " "^left_pad * s * " "^right_pad +end + +""" + print_flowsheet_summary(sol, sys, components::Vector{String}, units...) + +Print complete flowsheet summary report (Aspen Plus style) with automatic model type detection. + +Automatically detects model types from unit operation structs that have a `model_type` field. + +# Arguments +- `sol`: Solution from solver +- `sys`: Simplified/compiled system +- `components`: Vector of component names +- `units...`: Variable number of unit operation objects (e.g., R1, S1, etc.) + +# Example +```julia +@named R1 = FixedVolumeSteadyStateCSTR(...) +@named S1 = FixedBoundary_pTzn_(...) + +# Automatically detects R1 is CSTR, S1 is boundary +print_flowsheet_summary(sol, simplified_sys, components, R1, S1) +``` +""" +function print_flowsheet_summary(sol, sys, components::Vector{String}, units...) + # Build unit_ops dictionary automatically from model_type field + unit_ops = Dict{Symbol, Symbol}() + + for unit in units + unit_name = Symbol(nameof(unit.odesystem)) + + # Try to get model_type from struct + if hasproperty(unit, :model_type) + unit_ops[unit_name] = unit.model_type + else + # Fallback: try to infer from type name + type_str = string(typeof(unit)) + if occursin("CSTR", type_str) + unit_ops[unit_name] = :CSTR + elseif occursin("Boundary", type_str) || occursin("Feed", type_str) + unit_ops[unit_name] = :Feed + elseif occursin("Flash", type_str) + unit_ops[unit_name] = :FlashDrum + else + unit_ops[unit_name] = :Unknown + end + end + end + + # Call the main formatter + print_flowsheet_summary(sol, sys, unit_ops, components) +end + +export extract_cstr_solution, print_cstr_report, extract_boundary_solution, print_boundary_report, print_flowsheet_summary diff --git a/src/base/utils.jl b/src/base/utils.jl deleted file mode 100644 index 1b0dd80..0000000 --- a/src/base/utils.jl +++ /dev/null @@ -1,29 +0,0 @@ -#= @component function Port(ms;phase="unknown", name) - @named c = PhXConnector(ms) - - vars = @variables begin - T(t), [description="inlet temperature"] #, unit=u"K"] - ϱ(t), [description="inlet density"] #, unit=u"mol m^-3"] - p(t), [description="inlet pressure"] #, unit=u"Pa"] - n(t), [description="inlet molar flow"] #, unit=u"mol s^-1"] - m(t), [description="inlet mass flow"] #, unit=u"kg s^-1"] - xᵢ(t)[1:ms.N_c], [description="inlet mole fractions"]#, unit=u"mol mol^-1"] - end - - eqs = Equation[ - # EOS - ϱ ~ ms.molar_density(p,T,xᵢ;phase=phase), - 1.0 ~ sum(collect(xᵢ)), - c.h ~ ms.VT_enthalpy(ϱ,T,xᵢ), - # Connector - T ~ c.T, - ϱ ~ c.ϱ, - p ~ c.p, - scalarize(xᵢ .~ c.xᵢ)..., - n ~ m / sum(ms.Mw[i] * xᵢ[i] for i in 1:ms.N_c), - 0.0 ~ n + c.n, - ] - - return ODESystem(eqs, t, collect(Iterators.flatten(vars)), []; name, systems=[c]) -end - =# \ No newline at end of file diff --git a/src/fluid_handling/compressors.jl b/src/fluid_handling/compressors.jl deleted file mode 100644 index 4f80798..0000000 --- a/src/fluid_handling/compressors.jl +++ /dev/null @@ -1,26 +0,0 @@ -@component function SimpleAdiabaticCompressor(ms::MaterialSource; name) - - # Subsystems - @named cv = SimpleControlVolume(ms;N_mcons=2,N_works=1) - - # Variables - vars = @variables begin - W(t), [description="power"] #, unit=u"J s^1"] - T2_s(t), [description="temperature"] #, unit=u"K"] - ϱ2_s(t), [description="density"] #, unit=u"mol m^-3"] - end - - pars = @parameters begin - ηᴱ = 1.0, [description="efficiency"] - end - - # Equations - eqs = [ - cv.w1.W ~ W, - ms.VT_entropy(cv.c1.ϱ,cv.c1.T,cv.c1.xᵢ) ~ ms.VT_entropy(cv.c2.ϱ,cv.c2.T,cv.c2.xᵢ), - ϱ2_s ~ ms.molar_density(cv.c2.p,T2_s,cv.c2.xᵢ), - ηᴱ ~ cv.w1.W / ((ms.VT_enthalpy(ϱ2_s,T2_s,cv.c2.xᵢ) - cv.c1.h)*cv.c1.n), - ] - - return ODESystem(eqs, t, vars, pars; name, systems=[cv]) -end \ No newline at end of file diff --git a/src/fluid_handling/heat_exchangers.jl b/src/fluid_handling/heat_exchangers.jl deleted file mode 100644 index f2affdd..0000000 --- a/src/fluid_handling/heat_exchangers.jl +++ /dev/null @@ -1,18 +0,0 @@ -@component function SimpleIsobaricHeatExchanger(ms::MaterialSource; name) - - # Subsystems - @named cv = SimpleControlVolume(ms;N_mcons=2,N_heats=1) - - # Variables - vars = @variables begin - Q(t), [description="heat flux"] #, unit=u"J s^-1"] - end - - # Equations - eqs = [ - cv.q1.Q ~ Q, - cv.c1.p ~ cv.c2.p, - ] - - return ODESystem(eqs, t, vars, []; systems=[cv], name) -end \ No newline at end of file diff --git a/src/pressure_drop/valve.jl b/src/pressure_drop/valve.jl new file mode 100644 index 0000000..22a9d5c --- /dev/null +++ b/src/pressure_drop/valve.jl @@ -0,0 +1,156 @@ + +abstract type AbstractValve end + +mutable struct Valve{M <: AbstractFluidMedium, C <: Real, F <: Function, S <: AbstractString, ST <: AbstractThermodynamicState} <: AbstractValve + medium::M + state::ST + molar_flowrate_guess::C + Cv::C + opening_setpoint::C + f::F + phase::S + odesystem +end + + +function Valve(; medium, state_guess::S, Cv, f, flowrate_guess = 1.0, flowbasis = :volume, name) where S <: pTzState + p, T, z = state_guess.p, state_guess.T, state_guess.z + medium.Guesses = EosBasedGuesses(medium.EoSModel, p, T, z, Val(:Pressure)) + phase = ifelse(medium.Guesses.ϕ[2] ≈ 1.0, "vapor", "liquid") + flowstate = rhoTzState(medium.Guesses.ρ[1], T, z) + opening_setpoint = 0.5 + molar_flowrate_guess = XtoMolar(flowrate_guess, medium, flowstate, flowbasis) + odesystem = Valve_(medium = medium, Cv = Cv, ΔP_f = f, setpoint = opening_setpoint, phase = phase, name = name) + return Valve(medium, state_guess, molar_flowrate_guess, Cv, opening_setpoint, f, phase, odesystem) +end + +@component function Valve_(;medium, Cv, ΔP_f, setpoint, phase, name) + + systems = @named begin + InPort = PhZConnector_(medium = medium) + OutPort = PhZConnector_(medium = medium) + ControlVolumeState = ρTz_ThermodynamicState_(medium = medium) + end + + pars = [] + + vars = @variables begin + opening_setpoint(t), [description = "Valve opening set point"] + opening(t), [description = "Valve actual opening"] + end + + if phase == "vapor" + + phase_eq = [scalarize(ControlVolumeState.z[:, 2] .~ flash_mol_fractions_liquid(medium.EoSModel, ControlVolumeState.p, ControlVolumeState.T, collect(ControlVolumeState.z[:, 1])))...] + + else + + phase_eq = [scalarize(ControlVolumeState.z[:, 3] .~ flash_mol_fractions_vapor(medium.EoSModel, ControlVolumeState.p, ControlVolumeState.T, collect(ControlVolumeState.z[:, 1])))...] + + end + + eqs = [ + + # Energy balance + + 0.0 ~ InPort.h[1]*InPort.ṅ[1] + OutPort.h[1]*(OutPort.ṅ[1]) + + # Mole balance per component + [0.0 ~ InPort.ṅ[1]*InPort.z[i, 1] + sum(dot(collect(OutPort.ṅ[2:end]), collect(OutPort.z[i:i, 2:end]))) for i in 1:medium.Constants.Nc]... + [InPort.z[i, 1] ~ OutPort.z[i, 1] for i in 1:medium.Constants.Nc]... + + 0.0 ~ InPort.ṅ[1] + OutPort.ṅ[1] #Mole balance + + + # Flow equation + OutPort.ṅ[1]/ControlVolumeState.ρ[1] ~ -opening*Cv*ΔP_f(InPort.p - OutPort.p) + OutPort.ṅ[2] ~ OutPort.ṅ[1]*ControlVolumeState.ϕ[1] + OutPort.ṅ[3] ~ OutPort.ṅ[1]*ControlVolumeState.ϕ[2] + + + # Outlet port properties + [OutPort.h[j] ~ ρT_enthalpy(medium.EoSModel, ControlVolumeState.ρ[j], ControlVolumeState.T, collect(ControlVolumeState.z[:, j])) for j in 2:medium.Constants.nphases]... + OutPort.h[1] ~ dot(collect(OutPort.h[2:end]), collect(ControlVolumeState.ϕ)) + + # Port properties + OutPort.p ~ ControlVolumeState.p + scalarize(OutPort.z .~ ControlVolumeState.z)... + + + D(opening) ~ -1.0*(opening - opening_setpoint) + opening_setpoint ~ setpoint + + ] + + return System([phase_eq...; eqs...], t, collect(Iterators.flatten(vars)), pars; systems, name) + +end + + +@component function ErgunDrop(;medium, solidmedium, tank, phase = "vapor", name) + + systems = @named begin + InPort = PhZConnector_(medium = medium) + OutPort = PhZConnector_(medium = medium) + ControlVolumeState = ρTz_ThermodynamicState_(medium = medium) + end + + pars = [] + + vars = [] + + if phase == "vapor" + + phase_eq = [scalarize(ControlVolumeState.z[:, 2] .~ flash_mol_fractions_liquid(medium.EoSModel, ControlVolumeState.p, ControlVolumeState.T, collect(ControlVolumeState.z[:, 1])))...] + + else + + phase_eq = [scalarize(ControlVolumeState.z[:, 3] .~ flash_mol_fractions_vapor(medium.EoSModel, ControlVolumeState.p, ControlVolumeState.T, collect(ControlVolumeState.z[:, 1])))...] + + end + + + #Imperative assignments + M̄ = molecular_weight(collect(ControlVolumeState.z[:, 1]), collect(ControlVolumeState.z[:, 1])) + vs = (InPort.ṅ[1]/ControlVolumeState.ρ[1]/M̄)/(π*(tank.D/2)^2) #imperative assignment + ε = tank.porosity + ΔL = tank.H + dp = solidmedium.Constants.particle_size*2.0 + μ = viscosity(medium, ControlVolumeState.p, abs(ControlVolumeState.T), collect(ControlVolumeState.z[:, 1])) + ΔP = (InPort.p - OutPort.p) + ρ = ControlVolumeState.ρ[1] + + + eqs = [ + + # Energy balance + + 0.0 ~ InPort.h[1]*InPort.ṅ[1] + OutPort.h[1]*(OutPort.ṅ[1]) + + # Mole balance per component + [0.0 ~ InPort.ṅ[1]*InPort.z[i, 1] + sum(dot(collect(OutPort.ṅ[2:end]), collect(OutPort.z[i:i, 2:end]))) for i in 1:medium.Constants.Nc]... + [InPort.z[i, 1] ~ OutPort.z[i, 1] for i in 1:medium.Constants.Nc]... + + 0.0 ~ InPort.ṅ[1] + OutPort.ṅ[1] #Mole balance + + + # Flow equation + ΔP ~ 150.0*μ*ΔL/dp^2*(1-ε)^2/ε^3*vs + 1.75*ΔL*ρ/dp*(1-ε)/ε^3*abs(vs)*vs + OutPort.ṅ[2] ~ OutPort.ṅ[1]*ControlVolumeState.ϕ[1] + OutPort.ṅ[3] ~ OutPort.ṅ[1]*ControlVolumeState.ϕ[2] + + + # Outlet port properties + [OutPort.h[j] ~ ρT_enthalpy(medium.EoSModel, ControlVolumeState.ρ[j], ControlVolumeState.T, collect(ControlVolumeState.z[:, j])) for j in 2:medium.Constants.nphases]... + OutPort.h[1] ~ dot(collect(OutPort.h[2:end]), collect(ControlVolumeState.ϕ)) + OutPort.p ~ ControlVolumeState.p + scalarize(OutPort.z .~ ControlVolumeState.z)... + + + ] + + return System([phase_eq...; eqs...], t, collect(Iterators.flatten(vars)), pars; systems, name) + +end + +export Valve, Valve_ \ No newline at end of file diff --git a/src/separation/Adsorption.jl b/src/separation/Adsorption.jl new file mode 100644 index 0000000..71bb000 --- /dev/null +++ b/src/separation/Adsorption.jl @@ -0,0 +1,112 @@ +@component function AdsorptionInterface(;fluidmedium, solidmedium, adsorbentmass, name) + + systems = @named begin + SolidSurface = Surface(medium = solidmedium) + FluidSurface = Surface(medium = fluidmedium) + end + + eqs = [ domain_connect(FluidSurface.OutPort, SolidSurface.InPort) + scalarize(SolidSurface.InPort.μ .~ adsorbentmass*loading(solidmedium.isotherm, collect(FluidSurface.OutPort.μ), FluidSurface.OutPort.T))... + FluidSurface.OutPort.T ~ SolidSurface.InPort.T + scalarize(FluidSurface.OutPort.ϕₘ + SolidSurface.InPort.ϕₘ .~ 0.0)... + FluidSurface.OutPort.ϕₕ + SolidSurface.InPort.ϕₕ ~ 0.0 + ] + + vars = [] + + pars = [] + + guess_T = [FluidSurface.OutPort.T => solidmedium.Guesses.T] + guess_μ = [FluidSurface.OutPort.μ[i] => fluidmedium.Guesses.x[i, 1]*fluidmedium.Guesses.p for i in 1:fluidmedium.Constants.Nc] + + System(eqs, t, collect(Iterators.flatten(vars)), pars; name, systems = [systems...], guesses = [guess_T..., guess_μ...]) + +end + + + +@component function WellMixedAdsorber(;fluidmedium, solidmedium, porosity, V, p, phase, name) + + mass_of_adsorbent = V * solidmedium.EoSModel.ρ_T0 * porosity + A = V*(1.0 - porosity)*area_per_volume(solidmedium) #Interfacial area + + systems = @named begin + mobilephase = TwoPortControlVolume_(medium = fluidmedium) + stationaryphase = ClosedControlVolume_(medium = solidmedium) + interface = AdsorptionInterface(fluidmedium = fluidmedium, solidmedium = solidmedium, adsorbentmass = mass_of_adsorbent) + end + + eqs = [ + + # Custom energy balance + mobilephase.U ~ (mobilephase.OutPort.h[1] - mobilephase.ControlVolumeState.p/mobilephase.ControlVolumeState.ρ[1])*sum(collect(mobilephase.Nᵢ)) + stationaryphase.U ~ (ρT_enthalpy(solidmedium.EoSModel, stationaryphase.ControlVolumeState.ρ[1], stationaryphase.ControlVolumeState.T, collect(stationaryphase.ControlVolumeState.z[:, 1])))*mass_of_adsorbent #Pressure is not relevant for internal energy of the particle. + + # Fluid Surface ports equalities + interface.FluidSurface.InPort.T ~ mobilephase.ControlVolumeState.T + mobilephase.Q ~ interface.FluidSurface.OutPort.ϕₕ*A + + # Solid Surface ports equalities + scalarize(interface.SolidSurface.OutPort.μ .~ stationaryphase.Nᵢ)... + scalarize(interface.SolidSurface.OutPort.T ~ stationaryphase.ControlVolumeState.T) + + # Heat and mass transfer + stationaryphase.Q ~ interface.SolidSurface.InPort.ϕₕ*A + sum(-collect(isosteric_heat(adsorbent.isotherm, interface.FluidSurface.OutPort.μ, interface.FluidSurface.OutPort.T).*stationaryphase.rₐ[:, end])) + scalarize(stationaryphase.rₐ[:, end] .~ interface.SolidSurface.InPort.ϕₘ)... + + # No Volumetric sink/source constraints + scalarize(mobilephase.rᵥ[:, 2:end] .~ 0.0)... + scalarize(stationaryphase.rᵥ[:, 2:end] .~ 0.0)... + + #Volume constraint + stationaryphase.V[1] ~ V*(1.0 - porosity) #Volume of stationary phase + + #Control Volume Pressure Equality + mobilephase.ControlVolumeState.p ~ stationaryphase.ControlVolumeState.p + + #No shaft work + stationaryphase.Wₛ ~ 0.0 + mobilephase.Wₛ ~ 0.0 + + ] + + if phase == "liquid" #MTK can't handle equation change in mid simulation, so pick one phase. + + phase_eqs = [ + + scalarize(mobilephase.rₐ[:, 2] .~ interface.FluidSurface.OutPort.ϕₘ*A)... + scalarize(mobilephase.rₐ[:, end] .~ 0.0)... + + scalarize(mobilephase.ControlVolumeState.z[:, end] .~ flash_mol_fractions_vapor(medium.EoSModel, mobilephase.ControlVolumeState.p, mobilephase.ControlVolumeState.T, collect(mobilephase.ControlVolumeState.z[:, 1])))... + + scalarize(interface.FluidSurface.InPort.μ .~ mobilephase.nᴸⱽ[1]/mobilephase.V[2])... #This is phase specific + + #Perfect pressure control (If you fix volume for liquid phase, it leads to problems as liquid phase is incompressible) + mobilephase.ControlVolumeState.p ~ p # Only relevant for liquid phase + + ] + + elseif phase == "vapor" + + phase_eqs = [ + + scalarize(mobilephase.rₐ[:, 2] .~ 0.0)... + scalarize(mobilephase.rₐ[:, end] .~ interface.FluidSurface.OutPort.ϕₘ*A)... + + scalarize(mobilephase.ControlVolumeState.z[:, 2] .~ flash_mol_fractions_liquid(medium.EoSModel, mobilephase.ControlVolumeState.p, mobilephase.ControlVolumeState.T, collect(mobilephase.ControlVolumeState.z[:, 1])))... + + scalarize(interface.FluidSurface.InPort.μ .~ mobilephase.ControlVolumeState.p*mobilephase.ControlVolumeState.z[:, end])... #Partial pressure in vapor phase/rigorously is fugacity + + #Volume Constraint + mobilephase.V[1] ~ V*(porosity) #Volume of mobile phase + ] + + end + + vars = [] + + pars = [] + + return System([eqs...; phase_eqs...], t, collect(Iterators.flatten(vars)), pars; name, systems = systems) + +end \ No newline at end of file diff --git a/src/separation/FlashDrum.jl b/src/separation/FlashDrum.jl new file mode 100644 index 0000000..c17227b --- /dev/null +++ b/src/separation/FlashDrum.jl @@ -0,0 +1,70 @@ + +# ==================== Dynamic Flash Drum ==================== + +mutable struct DynamicFlashDrum{M <: AbstractFluidMedium, S <: AbstractThermodynamicState, G <: AbstractTank} <: AbstractSeparator + medium::M + state::S + gemotry::G + odesystem +end + +function DynamicFlashDrum(; medium, state, geometry, Q, W, name) + medium, state, phase = resolve_guess!(medium, state) + odesystem = DynamicFlashDrumModel(medium = medium, state = state, Q = Q, W = W, name = name) + return DynamicFlashDrum(medium, state, geometry, odesystem) +end + +@component function DynamicFlashDrumModel(; medium, state, geometry, Q = 0.0, W = 0.0, name) + + @named CV = ThreePortControlVolume_(medium = medium) + @unpack Nᵢ, V, InPort, LiquidOutPort, VaporOutPort, ControlVolumeState, rₐ, rᵥ = CV + + vars = @variables begin + h_liquid(t), [description = "liquid level height (m)"] + g(t) + end + + pars = @parameters begin + g = 9.81, [description = "gravitational acceleration (m/s²)"] + end + + # Basic equations + eqs = [ + # No reactions or surface mass transfer + scalarize(rₐ[:, 2:end] .~ 0.0)... + scalarize(rᵥ[:, 2:end] .~ 0.0)... + + # Heat input + CV.Q ~ Q + + # Shaft work + CV.Wₛ ~ W + + scalarize(ControlVolumeState.z[:, end] .~ flash_mol_fractions_vapor(medium.EoSModel, ControlVolumeState.p, ControlVolumeState.T, collect(ControlVolumeState.z[:, 1])))... + + # Internal energy definition + CV.U ~ (ControlVolumeState.h[1] - ControlVolumeState.p/ControlVolumeState.ρ[1])*sum(collect(Nᵢ)) + + # Total volume constraint (design specification) + CV.V[1] ~ state.V + ] + + # Flash drum specific equations + eq_flash = [ + + # Liquid level calculation from liquid volume + h_liquid ~ V[2] / cross_section_area_(geometry) + + # Liquid outlet pressure includes hydrostatic head + # P_liquid = P_vapor + ρ_liquid * g * h_liquid + LiquidOutPort.p ~ ControlVolumeState.p + (ControlVolumeState.ρ[2] * molecular_weight(medium.EoSModel, collect(ControlVolumeState.z[:, 2]))) * g * h_liquid + ] + + pars = [] + + return extend(ODESystem([eqs...; eq_flash...], t, collect(Iterators.flatten(vars)), pars; name), CV) +end + + + +export DynamicFlashDrum diff --git a/src/separation/distillation.jl b/src/separation/distillation.jl deleted file mode 100644 index 7a7c41d..0000000 --- a/src/separation/distillation.jl +++ /dev/null @@ -1,19 +0,0 @@ -@component function DistillationColumn(ms::MaterialSource; N_stages, i_feeds, name) - # Subsystems - stages = [SimpleStage(ms; name="stage$i", add_flows=sum(i.==[1,N_stages])+sum(i.==i_feeds)) for i in 1:N_stages] - - # Connect stages - # ... - - return ODESystem(; name) -end - -@component function SimpleStage(ms::MaterialSource; name, add_flows) - # Subsystem - @named cv = TPControlVolume(ms; N_states=4) - - # VLE - # ... - - return ODESystem(; name) -end \ No newline at end of file diff --git a/src/utils/FluidsProp.jl b/src/utils/FluidsProp.jl new file mode 100644 index 0000000..210f259 --- /dev/null +++ b/src/utils/FluidsProp.jl @@ -0,0 +1,348 @@ +abstract type AbstractFluidMedium end + +abstract type AbstractEoSBased <: AbstractFluidMedium end + +abstract type AbstractThermodynamicState end + +mutable struct pTNVState{T <: Union{Real, Nothing}, K <: AbstractArray{<:T}} <: AbstractThermodynamicState + p::T + T::T + N::K + V +end + +function pTNVState(p, T, N; base = :Pressure) + if base == :Pressure + return pTNVState(p, T, N, nothing) + else + return pTNVState(nothing, T, N, V) + end +end + +mutable struct pTzState{T <: Real, V <: AbstractArray{<:T}} <: AbstractThermodynamicState + p::T + T::T + z::V +end + +mutable struct rhoTzState{T <: Real, V <: AbstractArray{<:T}} <: AbstractThermodynamicState + ρ::T + T::T + z::V +end + +export pTzState, pTNVState + +function XtoMolar(flowrate, medium, state, flowbasis) + if flowbasis == :molar + return flowrate + elseif flowbasis == :mass + return MasstoMolar(flowrate, medium, state.z[:, 1]) + elseif flowbasis == :volume + return VolumetoMolar(flowrate, state.ρ[1]) + else + error("Invalid flow basis: $flowbasis") + end +end + +function MasstoMolar(flowrate, medium, x) + M̄ = sum(x .* medium.FluidConstants.molarMass) #kg/mol + return flowrate / M̄ +end + +function VolumetoMolar(flowrate, density) + return flowrate./density +end + +struct BasicFluidConstants{S <: Union{AbstractString, Nothing, AbstractVector{<: AbstractString}}, M <: Union{Nothing, Real, AbstractVector{<: Real}}, N <: Int, V <: Union{Nothing, AbstractVector{<: AbstractString}}} + iupacName::S # "Complete IUPAC name (or common name, if non-existent)"; + molarMass::M # "Molar mass"; + Nc::N # "Number of components"; + nphases::N # "Allowed phases"; + phase_names::V # "Label of the phases" +end + + +struct EosBasedGuesses{M <: Any, V <: Real, D <: AbstractArray{V}, F <: AbstractArray{V}} + EoSModel::M + p::V #Pressure + T::V #Temperature + ρ::D #Molar density per phase + x::F #Mole fraction per phase + h::D #Molar enthalpy per phase + ϕ #vaporized fraction +end + + +""" + EoSBased{F<:BasicFluidConstants, E<:Any, G<:EosBasedGuesses, T<:TransportModel} <: AbstractEoSBased + +Represents a fluid medium characterized by an equation of state (EoS) model. + +# Fields +- `Constants::F`: Physical and chemical constants of the fluid components (molecular weights, names, etc.) +- `EoSModel::E`: Equation of state model used for thermodynamic calculations (e.g., Peng-Robinson, GERG-2008) +- `TransportModel::T`: Model for transport properties (viscosity, thermal conductivity, etc.) +- `Guesses::G`: Current state variable guesses (pressure, temperature, densities, compositions) + +# Examples +```julia +# Create a methane-ethane mixture with Peng-Robinson EoS +components = ["methane", "ethane"] +eos = PR(components) # Peng-Robinson EoS from Clapeyron.jl +medium = EoSBased(components, eos) + +# Create with a specific initial state (room temperature, slightly elevated pressure) +state = pTzState(5e5, 298.15, [0.7, 0.3]) # 5 bar, 25°C, 70% methane +medium_with_state = EoSBased(components, eos, state) + +# Create with custom transport models and state +mt_model = ConstantMassTransferCoeff([0.5, 0.6]) # Component-specific coefficients +ht_model = ConstantHeatTransferCoeff(15.0) # W/(m²·K) +visc_model = ChapmanEnskogModel(components) +transport = TransportModel(mt_model, ht_model, visc_model) +medium_full = EoSBased(components, eos, transport, state) + +# Access thermodynamic properties +ρ_liquid = medium.Guesses.ρ[2] # Liquid phase density (mol/m³) +h_vapor = medium.Guesses.h[3] # Vapor phase enthalpy (J/mol) +vapor_frac = medium.Guesses.ϕ[2] # Vapor fraction + +""" +mutable struct EoSBased{F <: BasicFluidConstants, E <: Any, G <: EosBasedGuesses, T <: TransportModel} <: AbstractEoSBased + Constants::F + EoSModel::E + TransportModel::T + Guesses::G +end + +# --- Main Outer Constructor --- +# This is the only constructor that should create the EoSBased instance directly. +function EoSBased(components::S, eosmodel, transportmodel::T, state::ST) where {S <: AbstractVector{<: AbstractString}, T <: TransportModel, ST <: pTzState} + constants = BasicFluidConstants(components) + _p, _T, _z = state.p, state.T, state.z + guesses = EosBasedGuesses(eosmodel, _p, _T, _z, Val(:Pressure)) + return EoSBased(constants, eosmodel, transportmodel, guesses) +end + + +# --- Convenience Constructors --- +# These constructors provide default values and then call the main constructor. + +# Constructor without a specified state (uses a default state) +function EoSBased(components::S, eosmodel, transportmodel::T) where {S <: AbstractVector{<: AbstractString}, T <: TransportModel} + default_state = pTzState(101325.0, 298.15, ones(length(components))/length(components)) + return EoSBased(components, eosmodel, transportmodel, default_state) +end + +# Constructor without a specified transport model (creates a default one) +function EoSBased(components::S, eosmodel, state::ST) where {S <: AbstractVector{<: AbstractString}, ST <: pTzState} + masstransfermodel = ConstantMassTransferCoeff(0.5*ones(length(components))) + heat_transfer_model = ConstantHeatTransferCoeff(10.0) + # Assuming you have a default viscosity model or can pass nothing + transportmodel = TransportModel(masstransfermodel, heat_transfer_model, nothing) + return EoSBased(components, eosmodel, transportmodel, state) +end + +# Constructor accepting pTNVState - converts N to z (mole fractions) +function EoSBased(components::S, eosmodel, state::ST) where {S <: AbstractVector{<: AbstractString}, ST <: pTNVState} + # Convert molar amounts to mole fractions + N_total = sum(state.N) + z = state.N ./ N_total + + # Create pTzState from pTNVState + ptz_state = pTzState(state.p, state.T, z) + + # Call the pTzState constructor + return EoSBased(components, eosmodel, ptz_state) +end + +# Constructor with pTNVState and transport model +function EoSBased(components::S, eosmodel, transportmodel::T, state::ST) where {S <: AbstractVector{<: AbstractString}, T <: TransportModel, ST <: pTNVState} + # Convert molar amounts to mole fractions + N_total = sum(state.N) + z = state.N ./ N_total + + # Create pTzState from pTNVState + ptz_state = pTzState(state.p, state.T, z) + + # Call the main constructor + return EoSBased(components, eosmodel, transportmodel, ptz_state) +end + +# Constructor with only components and EoS model (uses all defaults) +function EoSBased(components::S, eosmodel) where {S <: AbstractVector{<: AbstractString}} + default_state = pTzState(101325.0, 298.15, ones(length(components))/length(components)) + # This now calls the constructor that creates the default transport model + return EoSBased(components, eosmodel, default_state) +end + + +# --- Keyword Argument Constructors --- +# These simply forward the keyword arguments to the appropriate positional constructor. + +function EoSBased(; + components, + eosmodel, + transportmodel = nothing, + state = nothing +) + # This single function handles all keyword-based calls. + # It decides which positional constructor to call based on the provided arguments. + + if transportmodel !== nothing && state !== nothing + # All arguments provided + return EoSBased(components, eosmodel, transportmodel, state) + elseif transportmodel !== nothing && state === nothing + # transportmodel provided, but no state + return EoSBased(components, eosmodel, transportmodel) + elseif transportmodel === nothing && state !== nothing + # state provided, but no transportmodel + return EoSBased(components, eosmodel, state) + else # Both transportmodel and state are nothing + # Only components and eosmodel provided + return EoSBased(components, eosmodel) + end +end + +##That should be part of the PropertyModels package + +function BasicFluidConstants(iupacName) + Nc = length(iupacName) + return BasicFluidConstants(iupacName, nothing, Nc, 3, ["overall", "liquid", "vapor"]) +end + + +function EosBasedGuesses(EoSModel::M, p::V, T::V, z::D, ::Val{:Pressure}) where {M <: Any, V <: Real, D <: AbstractArray{ <: Real}} + + sol = TP_flash(EoSModel, p, T, z) + ϕ = sol[1] + x = sol[2] + ρ = zeros(V, 3) + h = zeros(V, 3) + + ## Enthalpy + hₗ = enthalpy(EoSModel, p, T, x[:, 2], phase = "liquid") + hᵥ = enthalpy(EoSModel, p, T, x[:, 3], phase = "vapor") + ρ[2] = PT_molar_density(EoSModel, p, T, x[:, 2], phase = "liquid") + ρ[3] = PT_molar_density(EoSModel, p, T, x[:, 3], phase = "vapor") + ρ[1] = 1.0/(ϕ[1]/ρ[2] + ϕ[2]/ρ[3]) + hₒᵥ = hₗ*ϕ[1] + hᵥ*ϕ[2] + h .= [hₒᵥ, hₗ, hᵥ] + + return EosBasedGuesses(EoSModel, p, T, ρ, x, h, ϕ) +end + +function EosBasedGuesses(EoSModel::M, V::K, T::K, N::D, ::Val{:Volume}) where {M <: Any, K <: Real, D <: AbstractArray{ <: Real}} + return nothing +end + +function resolve_guess!(medium, state) + phase = "unknown" + p, T, N, V = state.p, state.T, state.N, state.V + z = N./sum(N) + if isnothing(V) + medium.Guesses = EosBasedGuesses(medium.EoSModel, p, T, z, Val(:Pressure)) + phase = ifelse(medium.Guesses.ϕ[2] ≈ 1.0, "vapor", "liquid") + state.V = sum(N)./medium.Guesses.ρ[1] + elseif isnothing(p) + medium.Guesses = EosBasedGuesses(medium.EoSModel, V, T, N, Val(:Volume)) + phase = ifelse(medium.Guesses.ϕ[2] ≈ 1.0, "vapor", "liquid") + state.p = medium.Guesses.p + else + phase = ifelse(medium.Guesses.ϕ[2] ≈ 1.0, "vapor", "liquid") + end + return medium, state, phase +end + + +#Physicochemical properties + +function PT_molar_density(EoSModel, p, T, x; phase = "unknown") + return NaN +end + +function TP_flash(EoSModel, p, T, x) + return NaN +end + +function is_stable(EoSModel, p, T, x) + return true +end + +function is_VT_stable(EoSModel, v, T, x) + return true +end + +function flash_mol_fractions(EoSModel, p, T, x) + return NaN +end + +function flash_mol_fractions_liquid(EoSModel, p, T, x) + z = NaN + return z +end + +function flash_mol_fractions_vapor(EoSModel, p, T, x) + z = flash_mol_fractions(EoSModel, p, T, x)[:, 3] + return z +end + +function flash_vaporized_fraction(EoSModel, p, T, x) + ϕ = TP_flash(EoSModel, p, T, x)[1] + return ϕ +end + + +function ρT_enthalpy(EoSModel, ρ, T, x) + return NaN +end + +function pT_enthalpy(EoSModel, p, T, x; phase = :unknown) + return NaN +end + +function ρT_internal_energy(EoSModel, ρ, T, x) + return NaN +end + +function molecular_weight(model, z::AbstractVector) + return NaN +end + + +#Transport functions +function mass_transfer_coefficient(fluid::A, T = 273.15, x = ones(fluid.Constants.Nc)/Nc) where A <: AbstractFluidMedium + return mass_transfer_coefficient(fluid.TransportModel.MassTransferModel, fluid, T, x) +end + +function mass_transfer_coefficient(model::M, fluid::A, T, x) where {M <: ConstantMassTransferCoeff, A <: AbstractFluidMedium} + return model.k +end + +#write specific dispatches here +function heat_transfer_coefficient(model::M, fluid::A, T, x) where {M <: ConstantHeatTransferCoeff, A <: AbstractFluidMedium} + return model.k +end + +function heat_transfer_coefficient(fluid::A, T = 273.15, x = ones(fluid.Constants.Nc)/Nc) where A <: AbstractFluidMedium + return heat_transfer_coefficient(fluid.TransportModel.HeatTransferModel, fluid, T, x) +end + + +function viscosity(model::M, p, T, z) where M <: AbstractFluidMedium + return viscosity(model.TransportModel.ViscosityModel, p, T, z) +end + +function viscosity(model, p, T, z) + return NaN +end + + +export EoSBased +export is_stable, is_VT_stable, TP_flash, flash_mol_fractions, flash_mol_fractions_liquid, flash_mol_fractions_vapor, flash_vaporized_fraction, PT_molar_density, ρT_enthalpy, ρT_internal_energy, molecular_weight, pT_enthalpy +export mass_transfer_coefficient, heat_transfer_coefficient, viscosity + + + + diff --git a/src/utils/Geometry.jl b/src/utils/Geometry.jl new file mode 100644 index 0000000..1b343bf --- /dev/null +++ b/src/utils/Geometry.jl @@ -0,0 +1,28 @@ +abstract type AbstractTank end + +mutable struct CylindricalTank{V <: Real} <: AbstractTank + D::V #Diameter constant + L::V #Overall height + H::V #Height of each segment (defaults to 1 segment) + porosity::V #Porosity +end + +function CylindricalTank(;D, L, H = L, porosity = 0.5) + return CylindricalTank(D, L, H, porosity) +end + +function discretize!(tank::CylindricalTank, nseg::Int) + tank.H = tank.L/nseg +end + +function volume_(tank::CylindricalTank) + return π*(tank.D/2)^2*tank.H +end + +function surface_area_(tank::CylindricalTank) + return 2*π*(tank.D/2)*tank.H + π*(tank.D/2)^2 +end + +function cross_section_area_(tank::CylindricalTank) + return π*(tank.D/2)^2 +end diff --git a/src/utils/Reactions.jl b/src/utils/Reactions.jl new file mode 100644 index 0000000..0ca88a7 --- /dev/null +++ b/src/utils/Reactions.jl @@ -0,0 +1,320 @@ +abstract type AbstractSinkSource end + +abstract type AbstractReaction <: AbstractSinkSource end + +struct PowerLawReaction{T <: Real, Arr <: AbstractArray{T}} <: AbstractReaction + species::Array{String} # Reactants and Products names + ν::Arr # Stoichiometry + n::Arr # Reaction order + A::T # Arrhenius constant + Eₐ::T # Activation energy +end + +""" + PowerLawReaction(; stoichiometry::Dict, order::Dict, A, Eₐ, components=nothing) + +Construct a PowerLawReaction using dictionaries for stoichiometry and reaction order. + +# Arguments +- `stoichiometry::Dict{String, <:Real}`: Dictionary mapping component names to stoichiometric coefficients + - Negative for reactants, positive for products + - Example: `Dict("A" => -1, "B" => -1, "C" => 2)` for A + B → 2C +- `order::Dict{String, <:Real}`: Dictionary mapping component names to reaction orders + - Example: `Dict("A" => 1.0, "B" => 1.0)` for first order in A and B +- `A::Real`: Arrhenius pre-exponential factor (units depend on overall order) +- `Eₐ::Real`: Activation energy (J/mol) +- `components::Union{Nothing, Vector{String}}`: Optional ordered list of component names + - If provided, the reaction arrays will follow this exact order + - If not provided, species are sorted alphabetically + +# Examples +```julia +# With automatic ordering (alphabetical) +reaction = PowerLawReaction( + stoichiometry = Dict("A" => -1.0, "B" => -1.0, "C" => 2.0), + order = Dict("A" => 1.0, "B" => 1.0), + A = 1e10, + Eₐ = 50000.0 +) + +# With specified component order (matches medium definition) +components = ["ethylene oxide", "water", "ethylene glycol"] +reaction = PowerLawReaction( + stoichiometry = Dict("ethylene oxide" => -1.0, "water" => -1.0, "ethylene glycol" => 1.0), + order = Dict("ethylene oxide" => 1.0, "water" => 1.0), + A = 1e10, + Eₐ = 50000.0, + components = components +) +``` +""" +function PowerLawReaction(; stoichiometry::Dict{String, <:Real}, order::Dict{String, <:Real}, A::Real, Eₐ::Real, components::Union{Nothing, Vector{String}}=nothing) + # Determine species order + if components !== nothing + # Use the provided component order + species = components + + # Verify all species in stoichiometry/order are in components + all_rxn_species = unique([keys(stoichiometry)..., keys(order)...]) + for sp in all_rxn_species + if sp ∉ species + error("Species '$sp' in reaction is not in the provided components list: $components") + end + end + else + # Get all unique species from both dictionaries and sort alphabetically + all_species = unique([keys(stoichiometry)..., keys(order)...]) + species = sort(collect(all_species)) + end + + # Build stoichiometry array (default to 0 if not specified) + ν = [get(stoichiometry, sp, 0.0) for sp in species] + + # Build reaction order array (default to 0 if not specified) + n = [get(order, sp, 0.0) for sp in species] + + # Promote to common type + T = promote_type(eltype(ν), eltype(n), typeof(A), typeof(Eₐ)) + ν_arr = convert(Vector{T}, ν) + n_arr = convert(Vector{T}, n) + A_val = convert(T, A) + Eₐ_val = convert(T, Eₐ) + + return PowerLawReaction(species, ν_arr, n_arr, A_val, Eₐ_val) +end + + +function _Rate(SinkSource::PowerLawReaction, cᵢ, T) + A, Eₐ, n, ν = SinkSource.A, SinkSource.Eₐ, SinkSource.n, SinkSource.ν + r = A * exp(-Eₐ / (R * T)) * prod(cᵢ[i]^n[i] for i in eachindex(cᵢ)) + return r.*ν +end + +Rate(SinkSource, cᵢ, T) = _Rate(SinkSource, cᵢ, T) + +#= Broadcast.broadcasted(::typeof(Rate), reactions, cᵢ, T) = broadcast(_Rate, reactions, Ref(cᵢ), T) =# + + +# ============================================================================ +# Reaction Network Composition +# ============================================================================ + +""" + AbstractReactionSet + +Abstract type for collections of reactions forming a reaction network. +""" +abstract type AbstractReactionSet end + + +""" + PowerLawReactionSet{T <: Real} + +A collection of PowerLawReactions forming a reaction network. + +# Fields +- `species::Vector{String}`: All unique species in the network +- `reactions::Vector{PowerLawReaction}`: Individual reactions +- `ν::Matrix{T}`: Stoichiometric coefficient matrix (Nc × Nr) + - Rows: species, Columns: reactions + - ν[i,j] = stoichiometric coefficient of species i in reaction j +- `n::Matrix{T}`: Reaction order matrix (Nc × Nr) + - Rows: species, Columns: reactions + - n[i,j] = reaction order of species i in reaction j +- `A::Vector{T}`: Pre-exponential factors for each reaction +- `Eₐ::Vector{T}`: Activation energies for each reaction (J/mol) +""" +struct PowerLawReactionSet{T <: Real} <: AbstractReactionSet + species::Vector{String} + reactions::Vector{<:PowerLawReaction} + ν::Matrix{T} # Stoichiometric matrix (Nc × Nr) + n::Matrix{T} # Reaction order matrix (Nc × Nr) + A::Vector{T} # Pre-exponential factors + Eₐ::Vector{T} # Activation energies + + # Inner constructor to allow flexible reaction vector types + function PowerLawReactionSet(species::Vector{String}, + reactions::Vector{<:PowerLawReaction}, + ν::Matrix{T}, + n::Matrix{T}, + A::Vector{T}, + Eₐ::Vector{T}) where T <: Real + new{T}(species, reactions, ν, n, A, Eₐ) + end +end + +""" + _Rate(network::PowerLawReactionSet, cᵢ, T) + +Calculate net production rates for all species in a reaction network. + +# Arguments +- `network::PowerLawReactionSet`: Reaction network with Nc species and Nr reactions +- `cᵢ::AbstractVector`: Concentration vector (length Nc) +- `T::Real`: Temperature (K) + +# Returns +- `Vector`: Net production rate for each species (length Nc) + - rᵢ = Σⱼ νᵢⱼ * rⱼ where rⱼ is the rate of reaction j + +# Math +For each reaction j: + rⱼ = Aⱼ * exp(-Eₐⱼ/(R*T)) * ∏ᵢ cᵢ^nᵢⱼ + +Net rate for species i: + rᵢ = Σⱼ νᵢⱼ * rⱼ + +In matrix form: + r = ν * r_rxn +where r_rxn is the vector of reaction rates +""" +function _Rate(network::RE, cᵢ, T) where RE <: PowerLawReactionSet + Nc = length(network.species) # Number of components + Nr = length(network.reactions) # Number of reactions + + # Calculate individual reaction rates + r_rxn = zeros(eltype(cᵢ), Nr) + + for j in 1:Nr + # Rate constant: k_j = A_j * exp(-Eₐ_j / (R*T)) + k_j = network.A[j] * exp(-network.Eₐ[j] / (R * T)) + + # Concentration term: ∏ᵢ cᵢ^nᵢⱼ + conc_term = one(eltype(cᵢ)) + for i in 1:Nc + if network.n[i, j] != 0 + conc_term *= cᵢ[i]^network.n[i, j] + end + end + + # Reaction rate: r_j = k_j * ∏ᵢ cᵢ^nᵢⱼ + r_rxn[j] = k_j * conc_term + end + + # Net production rate for each species: rᵢ = Σⱼ νᵢⱼ * r_j + # This is a matrix-vector multiplication: r = ν * r_rxn + r_net = network.ν * r_rxn + + return r_net +end + +Rate(network::PowerLawReactionSet, cᵢ, T) = _Rate(network, cᵢ, T) + + + + +""" + PowerLawReactionSet(reactions::Vector{PowerLawReaction}) + +Compose multiple PowerLawReactions into a reaction network. + +# Arguments +- `reactions::Vector{PowerLawReaction}`: Vector of individual reactions + +# Returns +- `PowerLawReactionSet`: Unified reaction network with matrices + +# Examples +```julia +# Define individual reactions +rxn1 = PowerLawReaction( + stoichiometry = Dict("A" => -1.0, "B" => 1.0), + order = Dict("A" => 1.0), + A = 1e10, Eₐ = 50000.0 +) + +rxn2 = PowerLawReaction( + stoichiometry = Dict("B" => -1.0, "C" => 1.0), + order = Dict("B" => 1.0), + A = 5e8, Eₐ = 60000.0 +) + +# Compose into network +network = PowerLawReactionSet([rxn1, rxn2]) + +# Access matrices +network.ν # [Nc × Nr] stoichiometric matrix +network.n # [Nc × Nr] reaction order matrix +``` +""" +function PowerLawReactionSet(reactions::Vector{<:PowerLawReaction}) + if isempty(reactions) + error("Cannot create empty reaction set") + end + + # Collect all unique species across all reactions + all_species = String[] + for rxn in reactions + append!(all_species, rxn.species) + end + species = sort(unique(all_species)) + + Nc = length(species) # Number of components + Nr = length(reactions) # Number of reactions + + # Build species index map + species_idx = Dict(sp => i for (i, sp) in enumerate(species)) + + # Determine output type from reactions + T = promote_type([typeof(rxn.A) for rxn in reactions]..., + [typeof(rxn.Eₐ) for rxn in reactions]..., + [eltype(rxn.ν) for rxn in reactions]..., + [eltype(rxn.n) for rxn in reactions]...) + + # Initialize matrices + ν_matrix = zeros(T, Nc, Nr) + n_matrix = zeros(T, Nc, Nr) + A_vec = zeros(T, Nr) + Eₐ_vec = zeros(T, Nr) + + # Fill matrices from each reaction + for (j, rxn) in enumerate(reactions) + # Store kinetic parameters + A_vec[j] = rxn.A + Eₐ_vec[j] = rxn.Eₐ + + # Map species to global indices and fill matrices + for (k, sp) in enumerate(rxn.species) + i = species_idx[sp] + ν_matrix[i, j] = rxn.ν[k] + n_matrix[i, j] = rxn.n[k] + end + end + + return PowerLawReactionSet(species, reactions, ν_matrix, n_matrix, A_vec, Eₐ_vec) +end + +""" + PowerLawReactionSet(rxn1::PowerLawReaction, rxn2::PowerLawReaction, rxns::PowerLawReaction...) + +Compose multiple PowerLawReactions using varargs. + +# Examples +```julia +network = PowerLawReactionSet(rxn1, rxn2, rxn3) +``` +""" +function PowerLawReactionSet(rxn1::PowerLawReaction, rxn2::PowerLawReaction, rxns::PowerLawReaction...) + all_rxns = [rxn1, rxn2, rxns...] + return PowerLawReactionSet(all_rxns) +end + +""" + Base.:+(rxn1::PowerLawReaction, rxn2::PowerLawReaction) + +Compose two reactions using the + operator to form a reaction network. + +# Examples +```julia +network = rxn1 + rxn2 + rxn3 +``` +""" +function Base.:+(rxn1::PowerLawReaction, rxn2::PowerLawReaction) + return PowerLawReactionSet([rxn1, rxn2]) +end + +function Base.:+(set::PowerLawReactionSet, rxn::PowerLawReaction) + return PowerLawReactionSet([set.reactions..., rxn]) +end + +export AbstractReaction, AbstractReactionSet, PowerLawReaction, PowerLawReactionSet, Rate \ No newline at end of file diff --git a/src/utils/SolidsProp.jl b/src/utils/SolidsProp.jl new file mode 100644 index 0000000..9837f13 --- /dev/null +++ b/src/utils/SolidsProp.jl @@ -0,0 +1,109 @@ +abstract type AbstractSolidMedium end +abstract type AbstractSolidEoSModel end + +struct BasicMaterialConstants{S <: Union{AbstractString, Nothing}, N <: Int, V <: Union{Nothing, AbstractVector{<: AbstractString}}, + T <: Union{Real, Nothing}, T2 <: Union{Real, Nothing}} + adsorbent_name::S # "Complete IUPAC name (or common name, if non-existent)"; + solute_names::V # "Names of the solutes"; + Nc::N # "Number of components"; + nphases::N # "Maximum number of phases"; + phase_names::V # "Label of the phases" + particle_size::T # "Particle size of the adsorbent"; + pore_size::T2 # "Pore size of the adsorbent"; +end + +# This is the single, correct constructor. It replaces the two duplicate ones. +function BasicMaterialConstants(adsorbent_name, solute_names, phase_names, particle_size) + Nc = length(solute_names) + nphases = length(phase_names) + # This calls the inner constructor, providing `nothing` for the optional `pore_size`. + return BasicMaterialConstants(adsorbent_name, solute_names, Nc, nphases, phase_names, particle_size, nothing) +end + +struct SolidEoSModel{C <: Union{AbstractVector{<: Real}, AbstractVector{<: Nothing}}, T <: Real} <: AbstractSolidEoSModel + ρ0::T + ρ_T0::T + coeffs_ρ::C + h0::T + h_T0::T + coeffs_h::C +end + +function SolidEoSModel(;ρ0, ρ_T0, coeffs_ρ, h0, h_T0, coeffs_h) + return SolidEoSModel(ρ0, ρ_T0, coeffs_ρ, h0, h_T0, coeffs_h) +end + + +struct Adsorbent{I <: Any, + E <: Any, T <: Any, C <: Union{BasicMaterialConstants, Nothing}} <: AbstractSolidMedium + isotherm::I + EoSModel::E + TransportModel::T + Constants::C + Guesses +end + +struct SolidGuesses{S <: Real, X<:AbstractArray{<: S}} + p::S + T::S + x::X +end + +function Adsorbent(;adsorbent_name, components, particle_size, isotherm, EoSModel, transport_model) + if length(isotherm.isotherms) != length(components) + return "Isotherm and solute names must have the same length." + + else + constants = BasicMaterialConstants(adsorbent_name, components, ["overall", "particle"], particle_size) + guess = SolidGuesses(101325.0, 273.15, ones(constants.Nc, constants.nphases)./constants.Nc) + return Adsorbent(isotherm, EoSModel, transport_model, constants, guess) + end +end + + +function PT_molar_density(EoSModel::M, p, T, x; phase = "particle") where M <: SolidEoSModel + # Assumes fourth order polynomial dependency with T and that the adsorbed fluid does not affect the density. + coeffs = EoSModel.coeffs_ρ + Tref = EoSModel.ρ_T0 + ρ_ref = EoSModel.ρ0 + ΔT = T - Tref + return ρ_ref + coeffs[1]*ΔT+ coeffs[2]*ΔT^2 + coeffs[3]*ΔT^3 + coeffs[4]*ΔT^4 +end + + +function ρT_enthalpy(EoSModel::M, ρ, T, x; phase = "particle") where M <: SolidEoSModel + # Assumes fourth order polynomial dependency with T and that the adsorbed fluid does not affect the enthalpy (mostly ok). + coeffs = EoSModel.coeffs_h + Tref = EoSModel.h_T0 + h0 = EoSModel.h0 + ΔT = T - Tref + return h0 + coeffs[1]*ΔT + coeffs[2]*ΔT^2 + coeffs[3]*ΔT^3 + coeffs[4]*ΔT^4 +end + +function area_per_volume(solid::A, sphericity = 1.0) where A <: AbstractSolidMedium + # Returns the area per volume of the solid medium. + # Assumes spherical particles. + rₚ = solid.Constants.particle_size + return 3.0 ./ rₚ*sphericity +end + +function mass_transfer_coefficient(model::M, solid::A, T, x) where {M <: HomogeneousDiffusivityCoeff, A <: AbstractSolidMedium} + rₚ² = solid.Constants.particle_size^2 + Dₕ = model.Dh + return 15.0 .* Dₕ ./ rₚ² +end + +function mass_transfer_coefficient(solid::A, T = 273.15, x = ones(adsorbent.Constants.Nc)/Nc) where A <: AbstractSolidMedium + return mass_transfer_coefficient(solid.TransportModel.MassTransferModel, solid, T, x) +end + +#write specific dispatches here +function heat_transfer_coefficient(model::M, solid::A, T, x) where {M <: ConstantHeatTransferCoeff, A <: AbstractSolidMedium} + return model.k +end + +function heat_transfer_coefficient(solid::A, T = 273.15, x = ones(adsorbent.Constants.Nc)/Nc) where A <: AbstractSolidMedium + return heat_transfer_coefficient(solid.TransportModel.HeatTransferModel, solid, T, x) +end + + diff --git a/src/utils/TransportProp.jl b/src/utils/TransportProp.jl new file mode 100644 index 0000000..1d2b783 --- /dev/null +++ b/src/utils/TransportProp.jl @@ -0,0 +1,46 @@ +using ModelingToolkit + +const R = 8.31446261815324 # J/(mol K) + +abstract type AbstractMassTransferModel end + +abstract type AbstractViscosityModel end + +abstract type AbstractThermalConductivityModel end + +abstract type AbstractDiffusivityModel end + +abstract type AbstractHeatTransferModel end + +struct HomogeneousDiffusivityCoeff{V <: AbstractVector{<: Real}} <: AbstractMassTransferModel +Dh::V +end + +struct ConstantHeatTransferCoeff{V <: Real} <: AbstractHeatTransferModel + k::V # Heat transfer coefficient +end + +struct ConstantMassTransferCoeff{V <: AbstractVector{<: Real}} <: AbstractMassTransferModel + k::V # Mass transfer coefficient +end + +struct TransportModel{M <: Union{Nothing, AbstractMassTransferModel}, + H <: Union{Nothing, AbstractHeatTransferModel}, + V, + K, + D} + + MassTransferModel::M + HeatTransferModel::H + ViscosityModel::V + ThermalConductivityModel::K + DiffusivityModel::D +end + +function TransportModel(masstransfermodel, heattransfermodel) + return TransportModel(masstransfermodel, heattransfermodel, nothing, nothing, nothing) +end + +function TransportModel(masstransfermodel, heattransfermodel, viscositymodel) + return TransportModel(masstransfermodel, heattransfermodel, viscositymodel, nothing, nothing) +end \ No newline at end of file diff --git a/src/utils/utils.jl b/src/utils/utils.jl new file mode 100644 index 0000000..92de8f1 --- /dev/null +++ b/src/utils/utils.jl @@ -0,0 +1,5 @@ +include("TransportProp.jl") +include("FluidsProp.jl") +include("Reactions.jl") +include("SolidsProp.jl") +include("Geometry.jl") \ No newline at end of file diff --git a/test/base/adsorbers_series.jl b/test/base/adsorbers_series.jl new file mode 100644 index 0000000..6dd886e --- /dev/null +++ b/test/base/adsorbers_series.jl @@ -0,0 +1,69 @@ +using ModelingToolkit +using Clapeyron +using LinearAlgebra +using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkit: scalarize, equations, get_unknowns +using NonlinearSolve +using OrdinaryDiffEq +using EntropyScaling + +#Building media +components = ["carbon dioxide", "methane"] + +#model = cPR(components, idealmodel = ReidIdeal, translation = RackettTranslation(components)) + +model = ReidIdeal(components) + +p__ = 2.0*101325.0 # Pa +T__ = 273.15 + 35.0 # K +z__ = [0.2, 0.8] # Mole fractions + +#Fluid medium +guess = EosBasedGuesses(model, p__/2.0, T__, z__) +constants = BasicFluidConstants(components) +masstransfermodel = ConstantMassTransferCoeff([5e-1, 3e-1]); +heat_transfer_model = ConstantHeatTransferCoeff(10.0); +viscosity_model = ChapmanEnskogModel(components, ref = "Poling et al. (2001)") +fluid_transport_model = TransportModel(masstransfermodel, heat_transfer_model, viscosity_model) +medium = EoSBased(constants, model, fluid_transport_model, guess) + + +#Solid medium +## Solid Eos +adsorbenteos = SolidEoSModel(750.0, 273.15, [0.0, 0.0, 0.0, 0.0], 0.0, 273.15, [935.0, 0.0, 0.0, 0.0]) #J/kg/K + +# Isotherm +iso_c1 = LangmuirS1(1e-8, 1e-8, -30_000.1456) +iso_c2 = LangmuirS1(10.0, 1e-9, -30_000.1456) +all_iso = ExtendedLangmuir(iso_c1, iso_c2) + + +# Mass transfer in film +masstransfermodel = HomogeneousDiffusivityCoeff([0.0, 1e-8]) +heattransfermodel = ConstantHeatTransferCoeff(10.0) +transportmodel = TransportModel(masstransfermodel, heattransfermodel) + + +#Defining adsorbent model +adsorbent = Adsorbent(adsorbent_name = "XYZ", + particle_size = 2e-3, + components = components, + isotherm = all_iso, + EoSModel = adsorbenteos, + transport_model = transportmodel) + + +#Tank info +phase = "vapor" +tank = CylindricalTank(D = 0.2, L = 2.0, porosity = 0.4) +discretize!(tank, 4) +V = volume_(tank) + + +#Two well mixed adsorbers connected by ergun pressure drop +@named reservoir = FixedBoundary_pTz_(medium = medium, p = p__, T = T__, z = z__) +@named adsorber1 = WellMixedAdsorber(fluidmedium = medium, solidmedium = adsorbent, porosity = tank.porosity, p = p__, V = V, phase = phase) +@named pdrop2 = ErgunDrop(medium = medium, solidmedium = adsorbent, tank = tank, phase = phase) +@named adsorber2 = WellMixedAdsorber(fluidmedium = medium, solidmedium = adsorbent, porosity = tank.porosity, p = p__, V = V, phase = phase) +@named outletvalve = Valve_(medium = medium, Cv = 8.4e-6, ΔP_f = x -> √(x) , phase = phase) +@named pressure_sink = ConstantPressure(medium = medium, p = p__/2.0) diff --git a/test/base/fixed_pTzN_boundary.jl b/test/base/fixed_pTzN_boundary.jl index 4454fd1..41b28f8 100644 --- a/test/base/fixed_pTzN_boundary.jl +++ b/test/base/fixed_pTzN_boundary.jl @@ -1,169 +1,202 @@ using ModelingToolkit using Clapeyron -using DynamicQuantities -using LinearAlgebra, DifferentialEquations +using LinearAlgebra using ModelingToolkit: t_nounits as t, D_nounits as D using ModelingToolkit: scalarize, equations, get_unknowns using NonlinearSolve +using OrdinaryDiffEq #Building media -ideal = JobackIdeal(["water", "methanol"]) -ideal.params.reference_state +components = ["carbon dioxide", "methane"] -model = Clapeyron.PR(["water", "methanol"], idealmodel = ReidIdeal) -bubble_pressure(model, 350.15, [0.8, 0.2]) -dew_pressure(model, 350.15, [0.8, 0.2]) -#flash_cl = Clapeyron.tp_flash(model, 1.01325e5, 350.15, [0.33, 0.33, 0.34]) -#enthalpy(model, 1.01325e5, 350.15, [0.8, 0.2], phase = "liquid") +model = SRK(components, idealmodel = ReidIdeal) + +#model = ReidIdeal(components) + +p__ = 1.0*101325.0 # Pa +T__ = 273.15 + 25.0 # K +z__ = [0.5, 0.5] # Mole fractions + +masstransfermodel = ConstantMassTransferCoeff([5e-1, 3e-1]); #This assumes the same correlation for all interfaces you might have with the control volume. +heat_transfer_model = ConstantHeatTransferCoeff(10.0); +viscosity_model = ChapmanEnskogModel(components, ref = "Poling et al. (2001)") +fluid_transport_model = TransportModel(masstransfermodel, heat_transfer_model, viscosity_model) +medium = EoSBased(components = components, eosmodel = model, transportmodel = fluid_transport_model, state = pTzState(p__, T__, z__)) -guess = EosBasedGuesses(model, 1.01325e5, 350.15, [0.8, 0.2]) -medium = EoSBased(BasicFluidConstants([0.01801528, 0.1801528]), model, guess) -medium.Guesses.ρ ### ------ Reservoir test -@named stream = FixedBoundary_pTzn_(medium = medium, p = 1.01325e5, T = 350.15, z = [.8, .2], ṅ = 10.0) -simple_stream = structural_simplify(stream) +@named reservoir = FixedBoundary_pTzn_(medium = medium, p = p__, T = T__, z = z__, flowrate = -5e-4, flowbasis = :volume) +@named sink = ConnHouse(medium = medium) -prob = SteadyStateProblem(simple_stream, []) -@time sol = solve(prob, SSRootfind()) -sol[stream.OutPort.ṅ] +connections = [connect(reservoir.OutPort, sink.port)] -### ------ ControlVolume +@named sys = System(connections, t; systems = [reservoir, sink]) -@component function HeatedTank_(;medium, Q̇, pressure, ṅ_out, name) +prob = NonlinearProblem(simple_stream, guesses(simple_stream)) +@time sol = solve(prob, RobustMultiNewton()) +sol[reservoir.ControlVolumeState.ϕ] - @named CV = TwoPortControlVolume_(medium = medium) - @unpack ControlVolumeState, OutPort, rₐ, rᵥ, Q, p, Wₛ, nᴸⱽ = CV - eqs = [ - Wₛ ~ 0.0 - scalarize(rᵥ[:, 2:end] .~ 0.0)... - scalarize(rₐ[:, 2:end] .~ 0.0)... - Q ~ Q̇ - p ~ pressure - OutPort.ṅ[1] ~ -ṅ_out - OutPort.ṅ[2] ~ -ṅ_out - OutPort.ṅ[3] ~ -1e-8 - ControlVolumeState.p ~ p - scalarize(ControlVolumeState.z[:, 3] ~ flash_mol_fractions_vapor(medium.EoSModel, ControlVolumeState.p, ControlVolumeState.T, collect(ControlVolumeState.z[:, 1])))... - scalarize(nᴸⱽ[1]/sum(nᴸⱽ) ~ flash_vaporized_fraction(medium.EoSModel, ControlVolumeState.p, ControlVolumeState.T, collect(ControlVolumeState.z[:, 1]))[1]) - ] - pars = [] +#Testing adsorption interface +## Solid Eos +adsorbenteos = SolidEoSModel(750.0, 273.15, [0.0, 0.0, 0.0, 0.0], 0.0, 273.15, [935.0, 0.0, 0.0, 0.0]) #J/kg/K - vars = [] +# Isotherm +iso_c1 = LangmuirS1(1e-8, 1e-8, -30_000.1456) +iso_c2 = LangmuirS1(10.0, 1e-9, -30_000.1456) +all_iso = ExtendedLangmuir(iso_c1, iso_c2) - return extend(ODESystem(eqs, t, vars, pars; name), CV) -end +# Mass transfer in film +masstransfermodel = HomogeneousDiffusivityCoeff([0.0, 1e-8]) +heattransfermodel = ConstantHeatTransferCoeff(0.0) +transportmodel = TransportModel(masstransfermodel, heattransfermodel) -@named tank = HeatedTank_(medium = medium, Q̇ = 100.0, pressure = 1.01325e5, ṅ_out = 10.0) -@component function PerfectFlowHeatedTank_(; medium, ṅ_in, ṅ_out, p, Q, name) +#Defining adsorbent model +adsorbent = Adsorbent(adsorbent_name = "XYZ", + particle_size = 2e-3, + components = components, + isotherm = all_iso, + EoSModel = adsorbenteos, + transport_model = transportmodel) - systems = @named begin - pT_Boundary = FixedBoundary_pTzn_(medium = medium, p = p, T = 300.15, z = [0.8, 0.2], ṅ = ṅ_in) - tank = HeatedTank_(medium = medium, Q̇ = Q, pressure = p, ṅ_out = ṅ_out) - end - vars = [] +#Set constants +porosity = 0.5 +V = 5e-3*10.0 +#p = 101325.0 +phase = "vapor" +solidmedium = adsorbent +fluidmedium = medium +mass_of_adsorbent = V * solidmedium.EoSModel.ρ_T0 * porosity +A = V*(1.0 - porosity)*area_per_volume(solidmedium) #Interfacial area - pars = [] - connections = [ - connect(pT_Boundary.OutPort, tank.InPort) - ] - - return ODESystem(connections, t, vars, pars; name = name, systems = [systems...]) +_0 = 1e-8 +Ntot = 5.0 +guess_adsorber = EosBasedGuesses(model, Clapeyron.pressure(model, V, T__, [Ntot, _0]), T__, [Ntot, _0]/(Ntot + _0)) +medium_adsorber = EoSBased(constants, model, fluid_transport_model, guess_adsorber) +#Well mixed adsorber test in vapor phase with valve +@named boundary = FixedBoundary_pTzn_(medium = fluidmedium, p = p__, T = T__, z = z__, flowrate = -0.10, flowbasis = :molar) +@named tank = WellMixedAdsorber(fluidmedium = medium_adsorber, solidmedium = solidmedium, porosity = 0.5, p = p__, V = V, phase = "vapor") +@named valve = Valve_(medium = medium_adsorber, Cv = 8.4e-6, ΔP_f = x -> √(x) , phase = "vapor") +@named sink = ConstantPressure(medium = medium_adsorber, p = 0.2*p__) +@named flowsink = ConstantFlowRate(medium = fluidmedium, flowrate = 0.3, flowbasis = :molar) -end +topography = [connect(boundary.OutPort, tank.mobilephase.InPort), + connect(tank.mobilephase.OutPort, valve.InPort), + connect(valve.OutPort, sink.port) +] -@named WaterTank = PerfectFlowHeatedTank_(medium = medium, ṅ_in = 10.0, ṅ_out = 10.0, p = 1.01325e5, Q = 100.0) +#= perfect_flow = [connect(boundary.OutPort, tank.mobilephase.InPort), + connect(tank.mobilephase.OutPort, flowsink.port)] =# -sistem = structural_simplify(WaterTank) -length(alg_equations(sistem)) +@named flowsheet = System(topography, t, [], [], systems = [boundary, tank, valve, sink]) -equations(sistem) -defaults(sistem) -guesses_ = [ - sistem.tank.ControlVolumeState.z[1, 2] => 0.2, - sistem.tank.ControlVolumeState.z[2, 2] => 0.2, - sistem.tank.V[2] => 50.0, - sistem.tank.V[3] => 100.0, - sistem.tank.nᴸⱽ[2] => 50.0] +#@named perfect_flow_system = System(perfect_flow, t, [], [], systems = [tank, boundary, flowsink]) -u0 = [sistem.tank.Nᵢ[1] => 80.0, sistem.tank.Nᵢ[2] => 80.0, - sistem.tank.ControlVolumeState.T => 300.0] +ModelingToolkit.flatten_equations(equations(expand_connections(flowsheet))) -prob = ODEProblem(sistem, u0, (0.0, 100.0), guesses = guesses_); -ssprob = SteadyStateProblem(sistem, [guesses_...; u0]) +simple_flowsheet = mtkcompile(flowsheet) -sol = solve(prob, Rodas42(autodiff = false)) +alg_equations(simple_flowsheet) +unknowns(simple_flowsheet) -sol_ss = solve(ssprob, SSRootfind()) +see_g = guesses(simple_flowsheet) -plot(sol.t, sol[sistem.tank.ControlVolumeState.T]) -initialization_equations(sistem) -equations(prob.f.initializationprob.f.sys) +u0_flowsheet = [tank.mobilephase.Nᵢ[1] => Ntot, + tank.mobilephase.Nᵢ[2] => _0, + tank.mobilephase.ControlVolumeState.T => T__, + tank.stationaryphase.Nᵢ[1] => Ntot, + tank.stationaryphase.Nᵢ[2] => _0, + tank.stationaryphase.ControlVolumeState.T => T__, + valve.opening => 0.5] +missing_guesses = [ + tank.mobilephase.ControlVolumeState.p => guess_adsorber.p, + tank.mobilephase.V[2] => _0, + valve.InPort.ṅ[1] => 0.1 +] +prob = ODEProblem(simple_flowsheet, u0_flowsheet, (0.0, 15500.0), guesses = missing_guesses, use_scc = false) +@time sol = solve(prob, FBDF(autodiff = false), abstol = 1e-10, reltol = 1e-10) +sol[tank.mobilephase.Nᵢ] +sol[tank.mobilephase.InPort.ṅ[1]] +sol[tank.stationaryphase.Nᵢ[2]] +plot(sol.t, sol[valve.InPort.ṅ[1]], label = "total molar flow rate") +sol[tank.interface.SolidSurface.InPort.ϕₘ[1]] +sol[tank.mobilephase.ControlVolumeState.p] +sol[tank.mobilephase.V[1]] +sol[tank.interface.SolidSurface.InPort.ϕₘ[2]] .+ sol[tank.interface.FluidSurface.OutPort.ϕₘ[2]] +plot(sol.t, sol[tank.interface.FluidSurface.InPort.ϕₕ]) +plot(sol.t, sol[tank.interface.SolidSurface.InPort.ϕₘ[1]]) +plot(sol.t, sol[tank.interface.FluidSurface.InPort.ϕₘ[2]]) +plot(sol.t, sol[tank.stationaryphase.Q], label = "methane") +plot(sol.t, sol[tank.stationaryphase.Nᵢ[2]], label = "methane") +plot(sol.t, sol[tank.mobilephase.U], label = "mobile phase") +plot(sol.t, sol[tank.mobilephase.ControlVolumeState.T], label = "mobile phase") +plot(sol.t, sol[tank.mobilephase.Nᵢ[1]]) +plot(sol.t, sol[tank.mobilephase.ControlVolumeState.z[1, 1]], label = "mobile phase") +Clapeyron.pressure(model, V, 300.15, 0.22*[0.0, 0.0, 1.0]) +Clapeyron.volume(model, 101325.0, 300.15, 0.22*[0.0, 0.0, 1.0], phase = :stable) -D(U) ~ InPort.h[1]*InPort.ṅ[1] + OutPort.h[1]*(OutPort.ṅ[1]) -scalarize(D(nᵢ[:, 1]) .~ InPort.ṅ[1].*InPort.z₁ + (OutPort.ṅ[2].*OutPort.z₂ + OutPort.ṅ[3].*OutPort.z₃) .+ collect(rᵥ[:, 2:end]*V[2:end])) -scalarize(rᵥ[:, 1] .~ sum(collect(rᵥ[:, 2:end]), dims = 2)) -scalarize(rₐ[:, 1] .~ sum(collect(rₐ[:, 2:end]), dims = 2)) -scalarize(nᵢ[:, 1] .~ sum(collect(nᵢ[:, 2:end]), dims = 2)) -scalarize(sum(collect(ControlVolumeState.ϕ)) ~ 1.0) -scalarize(ControlVolumeState.z .~ nᵢ ./ sum(collect(nᵢ), dims = 1)) -ControlVolumeState.p ~ p -[ControlVolumeState.ϕ[j - 1] ~ sum(collect(nᵢ[:, j]), dims = 1)./sum(collect(nᵢ[:, 1]), dims = 1) for j in 2:medium.FluidConstants.nphases] -U ~ (OutPort.h[1] - ControlVolumeState.p/ControlVolumeState.ρ[1])*sum(collect(nᵢ[:, 1])) -V[1] ~ sum(collect(V[2:end])) - -V[2]*ControlVolumeState.ρ[2] ~ sum(collect(nᵢ[:, 2])) +### ---- +#= @named InPort = PhZConnector_(medium = medium) +@named OutPort = PhZConnector_(medium = medium) +@named ControlVolumeState = ρTz_ThermodynamicState_(medium = medium) +vars = @variables begin + rᵥ(t)[1:medium.Constants.Nc, 1:medium.Constants.nphases], [description = "mass source or sink - volumetric basis"] + rₐ(t)[1:medium.Constants.Nc, 1:medium.Constants.nphases], [description = "molar source or sink - through surface in contact with other phases"] + Nᵢ(t)[1:medium.Constants.Nc], [description = "molar holdup"] + nᴸⱽ(t)[1:medium.Constants.nphases - 1], [description = "molar holdup in each phase, excluding the overall phase"] + U(t), [description = "internal energy holdup"] + p(t), [description = "pressure"] + V(t)[1:medium.Constants.nphases], [description = "volume"] + A(t), [description = "control volume area"] + Q(t), [description = "heat flux"] + Wₛ(t), [description = "shaft work"] +end + +D(U) ~ InPort.h[1]*InPort.ṅ[1] + OutPort.h[1]*(OutPort.ṅ[1]) + Q + Wₛ +[D(Nᵢ[i]) ~ InPort.ṅ[1]*InPort.z[i, 1] + sum(dot(collect(OutPort.ṅ[2:end]), collect(OutPort.z[i:i, 2:end]))) + sum(collect(rᵥ[i, 2:end].*V[2:end])) + rₐ[i, 1]*A for i in 1:medium.Constants.Nc] + + +scalarize(sum(OutPort.ṅ[2:end].*OutPort.z[:, 2:end], dims = 2)) -V[3]*ControlVolumeState.ρ[3] ~ sum(collect(nᵢ[:, 3])) - - -# Outlet port properties +sum(dot(OutPort.ṅ[2:end], OutPort.z[i:i, 2:end])) -[OutPort.h[j] ~ ρT_enthalpy(medium.EoSModel, ControlVolumeState.ρ[j], ControlVolumeState.T, collect(ControlVolumeState.z[:, j])) for j in 2:medium.FluidConstants.nphases] - -OutPort.h[1] ~ dot(collect(OutPort.h[2:end]), collect(ControlVolumeState.ϕ)) - -OutPort.p ~ ControlVolumeState.p +scalarize(rᵥ[:, 1] .~ sum(collect(rᵥ[:, 2:end]), dims = 2)) =# -scalarize(OutPort.z₁ .~ ControlVolumeState.z[:, 1]) -scalarize(OutPort.z₂ .~ ControlVolumeState.z[:, 2]) -scalarize(OutPort.z₃ .~ ControlVolumeState.z[:, 3]) \ No newline at end of file diff --git a/test/base/simple_steady_state.jl b/test/base/simple_steady_state.jl deleted file mode 100644 index 7ee645e..0000000 --- a/test/base/simple_steady_state.jl +++ /dev/null @@ -1,113 +0,0 @@ -using ProcessSimulator -using ModelingToolkit, DifferentialEquations -using ModelingToolkit: t_nounits as t - -const PS = ProcessSimulator - -# Create material sources -Mw = 0.004 # kg/mol -R = 2.1e3*Mw # J/(mol K) -cₚ = 5.2e3*Mw # J/(mol K) -cᵥ = cₚ - R - -# Perfect gas equations -matsource = PS.MaterialSource("helium"; - Mw = 0.004, - molar_density = (p, T, x; kwargs...) -> p/(R*Mw*T), - VT_internal_energy = (ϱ, T, x) -> cᵥ*T , - VT_enthalpy = (ϱ, T, x) -> cₚ*T, - VT_entropy = (ϱ, T, x) -> cᵥ*log(T) + R*log(1/ϱ) -) - -# Compressor -@named inlet = PS.Port(matsource) -@named comp = PS.SimpleAdiabaticCompressor(matsource) -@named outlet = PS.Port(matsource) - -con_eqs_comp = [ - connect(inlet.c, comp.cv.c1), - connect(comp.cv.c2, outlet.c) -] -@named compressor_ = ODESystem(con_eqs_comp, t, [], []; systems=[inlet,comp,outlet]) - -inp_comp = [ - inlet.m => 1.0, - inlet.T => 300., - inlet.p => 1e5, - outlet.p => 1e6 -] -out_comp = [comp.W] -compressor,idx = structural_simplify(compressor_, (first.(inp_comp), out_comp)) - -u0_comp = [ - (u => 1. for u in unknowns(compressor))..., - comp.ηᴱ => 0.8 -] - -prob_comp = SteadyStateProblem(compressor,inp_comp,u0_comp) -sol_comp = solve(prob_comp) - -@test sol_comp[comp.W] ≈ 2.3933999e6 rtol=1e-5 - -# Create flowsheet -@named comp_12 = PS.SimpleAdiabaticCompressor(matsource) -@named s2 = PS.MaterialStream(matsource) -@named heat_22⁺ = PS.SimpleIsobaricHeatExchanger(matsource) -@named s2⁺ = PS.MaterialStream(matsource) -@named heat_2⁺3 = PS.SimpleIsobaricHeatExchanger(matsource) -@named s3 = PS.MaterialStream(matsource) -@named turb_34 = PS.SimpleAdiabaticCompressor(matsource) -@named s4 = PS.MaterialStream(matsource) -@named heat_44⁺ = PS.SimpleIsobaricHeatExchanger(matsource) -@named s4⁺ = PS.MaterialStream(matsource) -@named heat_4⁺1 = PS.SimpleIsobaricHeatExchanger(matsource) - -systems = [comp_12,heat_22⁺,heat_2⁺3,turb_34,heat_44⁺,heat_4⁺1] -streams = [inlet,s2,s2⁺,s3,s4,s4⁺,outlet] - -con_eqs = vcat( - connect(inlet.c, comp_12.cv.c1), - connect(comp_12.cv.c2, s2.c1), - connect(s2.c2, heat_22⁺.cv.c1), - connect(heat_22⁺.cv.c2, s2⁺.c1), - connect(s2⁺.c2, heat_2⁺3.cv.c1), - connect(heat_2⁺3.cv.c2, s3.c1), - connect(s3.c2, turb_34.cv.c1), - connect(turb_34.cv.c2, s4.c1), - connect(s4.c2, heat_44⁺.cv.c1), - connect(heat_44⁺.cv.c2, s4⁺.c1), - connect(s4⁺.c2, heat_4⁺1.cv.c1), - connect(heat_4⁺1.cv.c2, outlet.c) -) -append!(con_eqs,[0.0 ~ heat_2⁺3.Q + heat_4⁺1.Q]) - -@named flowsheet_ = ODESystem(con_eqs, t, [], []; systems=[systems...,streams...]) - -inp = [ - inlet.n => 1.0, - inlet.T => 298.15, - inlet.p => 1e5, - s2.p => 1e6, - s2⁺.T => 298.15, - s3.T => 253.15, - outlet.T => 298.15, - outlet.p => 1e5, -] -out = [ - heat_44⁺.Q, - comp_12.W, - turb_34.W, -] - -flowsheet,idx = structural_simplify(flowsheet_, (first.(inp), out)) - -u0 = [u => 1. for u in unknowns(flowsheet)] - -prob = SteadyStateProblem(flowsheet,inp,u0) -sol = solve(prob) - -ε_KM = abs(sol[heat_44⁺.Q])/abs(sol[turb_34.W] + sol[comp_12.W]) - -@test round(sol[s2.T],digits=2) ≈ 755.58 -@test round(sol[s4.T],digits=2) ≈ 99.89 -@test round(ε_KM,digits=3) ≈ 0.504 \ No newline at end of file diff --git a/test/base/valve_test.jl b/test/base/valve_test.jl new file mode 100644 index 0000000..a62df77 --- /dev/null +++ b/test/base/valve_test.jl @@ -0,0 +1,55 @@ +using ProcessSimulator +using Clapeyron +using ModelingToolkit +using OrdinaryDiffEq +using ModelingToolkit: t_nounits as t, D_nounits as D +using Test + + +@testset "isenthalpic valve" begin +#Building media +components = ["carbon dioxide", "methane"] + +model = cPR(components, idealmodel = ReidIdeal) + +p__ = 50.0*101325.0 # Pa +T__ = 273.15 + 25.0 # K +z__ = [0.5, 0.5] # Mole fractions + +medium = EoSBased(components = components, eosmodel = model) + +@named S1 = FixedBoundary_pTz_(medium = medium, p = p__, T = T__, z = z__) + +@named V1 = Valve(medium = medium, +state_guess = pTzState(0.5*p__, T__, z__), +Cv = 4.4e-6, +f = x -> x/sqrt(abs(x) + 1e-8), +flowrate_guess = 1e-3, +flowbasis = :volume) + + +@named P1 = ConstantPressure(medium = medium, p = 0.5*p__) + +connection_set = [ + connect(S1.OutPort, V1.odesystem.InPort), + connect(V1.odesystem.OutPort, P1.Port) +] + +@named sys = System(connection_set, t, [], []; systems = [S1, V1.odesystem, P1]) + + +simple_sys = mtkcompile(sys) + +u0 = [V1.odesystem.opening => 0.5] +valve_guesses = [V1.odesystem.InPort.ṅ[1] => V1.molar_flowrate_guess] +prob = ODEProblem(simple_sys, u0, (0.0, 0.001), guesses = valve_guesses, use_scc = false) +sol = solve(prob, FBDF(autodiff = false), abstol = 1e-6, reltol = 1e-6) + + +T_valve_out = first(sol[V1.odesystem.ControlVolumeState.T]) + +reference_T_valve_out = 290.39815 + +@test abs(reference_T_valve_out - T_valve_out)/reference_T_valve_out < 0.05 + +end \ No newline at end of file diff --git a/test/reactors/cstr.jl b/test/reactors/cstr.jl new file mode 100644 index 0000000..013a9c9 --- /dev/null +++ b/test/reactors/cstr.jl @@ -0,0 +1,68 @@ +using ProcessSimulator +using Clapeyron +using ModelingToolkit +using OrdinaryDiffEq +using NonlinearSolve +using ModelingToolkit: t_nounits as t, D_nounits as D +using Test + + +#Clapeyron model definition +components = ["ethylene oxide", "water", "ethylene glycol"] +idealmodel = CompositeModel(components, + liquid = RackettLiquid, + gas = ReidIdeal(components, reference_state = :formation), + saturation = DIPPR101Sat, + hvap = DIPPR106HVap) +activity = NRTL(components) + +model = CompositeModel(components, fluid = idealmodel, liquid = activity) + +bubble_pressure(model, 350.15, [0.4, 0.6, 0.0]) #Just to test if model is working +#Standard medium building (Will be overwritten inside each equipment with state or guesses) +medium = EoSBased(components = components, eosmodel = model) + + +@named S1 = Boundary_pTzn(medium = medium, p = 5*101325.0, T = 350.15, z = [0.4, 0.6, 0.0], flowrate = 100.0, flowbasis = :molar) + +rxn1 = PowerLawReaction(components = components, + stoichiometry = Dict("ethylene oxide" => -1.0, "water" => -1.0, "ethylene glycol" => 1.0), + order = Dict("ethylene oxide" => 1.0, "water" => 0.0), + A = 0.5, Eₐ = 0.0 +) + +reactor_state = pTNVState(5*101325.0, 350.15, ones(3)/3.0, base = :Pressure) + +@named R1 = FixedVolumeSteadyStateCSTR(medium = medium, reactionset = rxn1, + limiting_reactant = "ethylene oxide", + state = reactor_state, volume = 1.0, W = 0.0, Q = nothing) + + + +@named sink = ConnHouse(medium = medium) + +connection_set = [ + connect(S1.odesystem.OutPort, R1.odesystem.InPort), + connect(R1.odesystem.OutPort, sink.InPort) +] + +@named sys = System(connection_set, t, [], []; systems = [S1.odesystem, R1.odesystem, sink]) + +AdiabaticVolumeReactor = mtkcompile(sys) + +default_guesses = guesses(AdiabaticVolumeReactor) +guesses_Reactor = Dict( + R1.odesystem.V[3] => 1e-8, + R1.odesystem.OutPort.ṅ[1] => 100.0, +) #These guesses are problem specific + +merged_guesses = merge(default_guesses, guesses_Reactor) + +prob = NonlinearProblem(AdiabaticVolumeReactor, merged_guesses) + +@time sol = solve(prob, abstol = 1e-8, reltol = 1e-8) + +unit_ops = Dict(:R1 => :CSTR, :S1 => :Feed) + +print_flowsheet_summary(sol, AdiabaticVolumeReactor, unit_ops, components) +print_flowsheet_summary(sol, AdiabaticVolumeReactor, components, R1, S1) \ No newline at end of file diff --git a/test/reactors/simple_cstr.jl b/test/reactors/simple_cstr.jl deleted file mode 100644 index 4f5cb5e..0000000 --- a/test/reactors/simple_cstr.jl +++ /dev/null @@ -1,98 +0,0 @@ -using ProcessSimulator -using ModelingToolkit, DifferentialEquations -using ModelingToolkit: t_nounits as t - -const PS = ProcessSimulator - -# Material Parameters -ϱ = [14.8,55.3,13.7,24.7].*1e3 # mol/m^3 -cₚ = [35,18,46,19.5]*4.184 # J/mol/K - -# Reaction -ν = [-1.0, -1.0, 1.0, 0.0] -Δhᵣ = 20013*4.184 # J/mol - -matsource = PS.MaterialSource(["propylene oxide (A)","water (B)","propylene glycol (C)","methanol (M)"]; - Mw = [0.05808, 0.01801, 0.07609, 0.03204], - molar_density = (p, T, x; kwargs...) -> sum([ϱ[i]*x[i] for i in 1:4]), - VT_enthalpy = (ϱ, T, x) -> sum([cₚ[i]*x[i] for i in 1:4])*T, - VT_internal_energy = (ϱ, T, x) -> sum([cₚ[i]*x[i] for i in 1:4])*T, - reactions = [PS.Reaction( - ν = ν, - r = (p,T,x) -> 2.73e-4*exp(9059*(1/297-1/T))*x[1], - Δhᵣ = (T) -> Δhᵣ, - )], -) - -# Parameters and boundary conditions -V_cstr = 1.89 # m^3 -F = [36.3e3, 453.6e3, 0, 45.4e3]./3600 # mol/s - -# Initial conditions -T0 = 297. -n0 = 55.3e3*V_cstr -m0_inlet = F' * matsource.Mw - -# Heat exchanger -UA = 7262*4184/3600 # J/K/s -T1_coolant = 288.71 # K -cₚ_coolant = 18*4.184 # J/mol/K -n_coolant = 453.6e3/3600 # mol/s - -# Create flowsheet -@named inlet = PS.Port(matsource) -@named cstr = PS.CSTR(matsource;flowtype="const. mass") -@named outlet = PS.Port(matsource) - -# Connect the flowsheet -eqs = [ - connect(inlet.c,cstr.cv.c1), - connect(cstr.cv.c2,outlet.c), - cstr.Q ~ -n_coolant*cₚ_coolant*(cstr.cv.T-T1_coolant)*(1-exp(-UA/(n_coolant*cₚ_coolant))), -] - -@named flowsheet_ = ODESystem(eqs, t, [], [], systems=[inlet,cstr,outlet]) - -pars = [] - -inp = [ - inlet.T => 297.0, - inlet.p => 1e5, - inlet.n => sum(F), - inlet.xᵢ[1] => F[1]/sum(F), - inlet.xᵢ[2] => F[2]/sum(F), - inlet.xᵢ[3] => F[3]/sum(F), - outlet.p => 1e5, -] - -u0 = [ - cstr.cv.U => matsource.VT_internal_energy(NaN, T0, [0,1,0,0])*n0, - cstr.cv.nᵢ[1,1] => 0.0, - cstr.cv.nᵢ[1,2] => n0, - cstr.cv.nᵢ[1,3] => 0.0, - cstr.cv.nᵢ[1,4] => 0.0, - inlet.m => m0_inlet, - cstr.cv.T => 297., - cstr.cv.c2.n => 0.0, - outlet.m => 0., -] - -flowsheet,idx = structural_simplify(flowsheet_,(first.(inp),[])) - -prob = ODEProblem(flowsheet, u0, (0, 2).*3600., vcat(inp))#; guesses = [outlet.m => -m0_inlet]) -sol = solve(prob, QNDF(), abstol = 1e-6, reltol = 1e-6) - -(Tmax, iTmax) = findmax(sol[cstr.cv.T]) - -@test Tmax ≈ 356.149 atol=1e-3 -@test sol.t[iTmax] ≈ 2823.24 atol=1e-2 - -if isinteractive() - # Plots - using Plots - - ps = [plot(;framestyle=:box,xlabel="t / h",xlims=(0.0,2.0)) for i in 1:2] - plot!(ps[1],sol.t/3600,sol[cstr.cv.T],label="T",ylabel="T / K") - [plot!(ps[2],sol.t/3600, sol[cstr.cv.xᵢ[1,i]];label=matsource.components[i],ylabel="xᵢ / mol/mol") for i in 1:4] - plot(ps...;layout=(1,2),size=(800,400)) -end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index cb00bab..f63cd61 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,10 +2,7 @@ using ProcessSimulator using Test using SafeTestsets -@safetestset "Base components" begin - include("base/simple_steady_state.jl") +@safetestset "Valve" begin + include("base/valve_test.jl") end -@safetestset "Reactors" begin - include("reactors/simple_cstr.jl") -end diff --git a/test/separation/flash_drum_test.jl b/test/separation/flash_drum_test.jl new file mode 100644 index 0000000..1e6c700 --- /dev/null +++ b/test/separation/flash_drum_test.jl @@ -0,0 +1,265 @@ +using ProcessSimulator + + +# Load Clapeyron for thermodynamic properties +using Clapeyron + + + + +components = ["methane", "propane"] +eos = PR(components, idealmodel = ReidIdeal) # Peng-Robinson equation of state + +# ============================================================================= +# Simulation Scenario +# Flash drum originally at 30bar with equimolar methane/propane holdup (global) and a feed mix +# at different composition and vaporized fraction comes in at 30bar + + + +# ============================================================================= +# 2. FLASH DRUM SIZING +# ============================================================================= +# Design criteria for vapor-liquid separator: +# - Residence time: 5-10 minutes for liquid +# - L/D ratio: typically 3-5 for horizontal drums, 2-4 for vertical +# - Vapor velocity: limited to avoid entrainment (typically < 1 m/s) + +# Vessel sizing based on residence time and vapor velocity +initial_state = Clapeyron.qp_flash(eos, 0.5, 3e6, 100.0*[0.5, 0.5]) # 25°C, 30 bar, equimolar +volume(initial_state) #m^3 + + +# Estimate flash conditions at lower pressure +flash_pressure = 20e5 # 20 bar +flash_temp = initial_state.data.T # 263.15 K (-10°C) - colder to get more liquid + +# Rough estimate: at these conditions, expect ~50% vaporization +# For 5 min residence time with 50 mol/s liquid: +# Liquid volume needed ≈ 5 min × 60 s/min × 50 mol/s / ρ_liquid +# Assuming ρ_liquid ≈ 15000 mol/m³ (typical for light hydrocarbons) +estimated_liquid_holdup = 5 * 60 * 50 / 15000 # ≈ 1 m³ + +# Design vertical drum with L/D = 3 +drum_diameter = 1.5 # m +drum_height = 4.5 # m (L/D = 3) +drum_volume = π * (drum_diameter/2)^2 * drum_height # ≈ 7.95 m³ + +geometry = CylindricalTank(D = drum_diameter, L = drum_height) + +println("=== Flash Drum Design ===") +println("Diameter: $(drum_diameter) m") +println("Height: $(drum_height) m") +println("Total Volume: $(round(drum_volume, digits=2)) m³") +println("L/D ratio: $(round(drum_height/drum_diameter, digits=2))") +println() + + + # ============================================================================= + # 4. VALVE SIZING + # ============================================================================= + # Inlet valve: pressure drop from 30 bar to 20 bar + # Cv = Q / √(ΔP) where Q is flow and ΔP is pressure drop + # For gas: Cv = (Q_scfh / 963) × √(ρ/ΔP) × √(T/520) + # Simplified: use moderate Cv values + + inlet_valve_cv = 10.0 # Flow coefficient (imperial units equivalent) + liquid_valve_cv = 5.0 # Smaller valve for liquid control + vapor_valve_cv = 15.0 # Larger valve for vapor (lower density) + + # Valve pressure drop functions + # For simplicity: ΔP ∝ (flow)² / Cv² + function valve_pressure_drop(cv) + return (state, port) -> begin + # Simple pressure drop: ΔP = k × ṅ² / Cv² + # k is a constant, here we use 1e4 for reasonable pressure drops + k = 1e4 + return k * (port.ṅ[1])^2 / cv^2 + end + end + + # ============================================================================= + # 5. BUILD SYSTEM COMPONENTS + # ============================================================================= + + # Feed stream (high pressure) + @named feed = FixedBoundary_pTzn_( + medium = medium, + p = feed_pressure, + T = feed_temp, + z = feed_composition, + flowrate = feed_flowrate, + flowbasis = :molar + ) + + # Inlet valve (throttling valve) + inlet_valve_state = pTNVState( + flash_pressure, # Outlet pressure + flash_temp, + feed_composition .* feed_flowrate, + nothing + ) + + @named inlet_valve = Valve( + medium = medium, + Cv = inlet_valve_cv, + valve_equation = valve_pressure_drop(inlet_valve_cv), + name = "inlet_valve", + state = inlet_valve_state + ) + + # Flash drum + @named flash_drum = SteadyStateFlashDrum( + medium = medium, + state = flash_state, + Q = 0.0 # Adiabatic operation + ) + + # Liquid outlet valve + liquid_valve_pressure = flash_pressure - 2e5 # Drain to 18 bar + liquid_valve_state = pTNVState( + liquid_valve_pressure, + flash_temp, + [0.3, 0.7] .* 40.0, # Assuming liquid-rich in propane + nothing + ) + + @named liquid_valve = Valve( + medium = medium, + Cv = liquid_valve_cv, + valve_equation = valve_pressure_drop(liquid_valve_cv), + name = "liquid_valve", + state = liquid_valve_state + ) + + # Vapor outlet valve + vapor_valve_pressure = flash_pressure - 1e5 # Vapor to 19 bar + vapor_valve_state = pTNVState( + vapor_valve_pressure, + flash_temp, + [0.8, 0.2] .* 60.0, # Assuming vapor-rich in methane + nothing + ) + + @named vapor_valve = Valve( + medium = medium, + Cv = vapor_valve_cv, + valve_equation = valve_pressure_drop(vapor_valve_cv), + name = "vapor_valve", + state = vapor_valve_state + ) + + # Outlet pressure boundaries + @named liquid_sink = ConstantPressure(medium = medium, p = liquid_valve_pressure) + @named vapor_sink = ConstantPressure(medium = medium, p = vapor_valve_pressure) + + # ============================================================================= + # 6. CONNECT SYSTEM + # ============================================================================= + @named flash_system = ODESystem([ + # Feed -> Inlet Valve -> Flash Drum + connect(feed.OutPort, inlet_valve.InPort) + connect(inlet_valve.OutPort, flash_drum.InPort) + + # Flash Drum -> Outlet Valves -> Sinks + connect(flash_drum.LiquidOutPort, liquid_valve.InPort) + connect(flash_drum.VaporOutPort, vapor_valve.InPort) + connect(liquid_valve.OutPort, liquid_sink.Port) + connect(vapor_valve.OutPort, vapor_sink.Port) + ], t) + + # ============================================================================= + # 7. SIMPLIFY AND SOLVE + # ============================================================================= + println("=== Simplifying system ===") + sys_simplified = structural_simplify(flash_system) + + println("Number of equations: ", length(equations(sys_simplified))) + println("Number of unknowns: ", length(unknowns(sys_simplified))) + println() + + # Initial conditions + u0 = [ + sys_simplified.flash_drum.Nᵢ[1] => 100.0, # Initial methane holdup + sys_simplified.flash_drum.Nᵢ[2] => 100.0, # Initial propane holdup + sys_simplified.flash_drum.ControlVolumeState.T => flash_temp + ] + + # Guesses for algebraic variables + guesses = [ + sys_simplified.flash_drum.V[2] => 2.0, # Initial liquid volume (m³) + sys_simplified.flash_drum.V[3] => drum_volume - 2.0, # Initial vapor volume + sys_simplified.flash_drum.nᴸⱽ[1] => 100.0, # Initial liquid moles + sys_simplified.flash_drum.nᴸⱽ[2] => 100.0, # Initial vapor moles + sys_simplified.flash_drum.h_liquid => 1.5, # Initial liquid height (m) + ] + + # ============================================================================= + # 8. CREATE AND SOLVE PROBLEM + # ============================================================================= + println("=== Creating ODE problem ===") + tspan = (0.0, 500.0) # 500 seconds simulation + prob = ODEProblem(sys_simplified, u0, tspan, guesses = guesses) + + println("=== Solving system ===") + sol = solve(prob, FBDF(autodiff = false), abstol = 1e-8, reltol = 1e-6) + + # ============================================================================= + # 9. EXTRACT AND DISPLAY RESULTS + # ============================================================================= + println("\n=== STEADY-STATE RESULTS ===") + + # Flash drum properties + liquid_level = sol[sys_simplified.flash_drum.h_liquid][end] + liquid_volume = sol[sys_simplified.flash_drum.V[2]][end] + vapor_volume = sol[sys_simplified.flash_drum.V[3]][end] + + println("\nFlash Drum:") + println(" Liquid level: $(round(liquid_level, digits=3)) m") + println(" Liquid volume: $(round(liquid_volume, digits=3)) m³") + println(" Vapor volume: $(round(vapor_volume, digits=3)) m³") + println(" Liquid fraction: $(round(liquid_volume/drum_volume*100, digits=1))%") + + # Pressures + drum_pressure = sol[sys_simplified.flash_drum.ControlVolumeState.p][end] + liquid_outlet_p = sol[sys_simplified.flash_drum.LiquidOutPort.p][end] + vapor_outlet_p = sol[sys_simplified.flash_drum.VaporOutPort.p][end] + + println("\nPressures:") + println(" Drum vapor space: $(round(drum_pressure/1e5, digits=2)) bar") + println(" Liquid outlet: $(round(liquid_outlet_p/1e5, digits=2)) bar") + println(" Vapor outlet: $(round(vapor_outlet_p/1e5, digits=2)) bar") + println(" Hydrostatic head: $(round((liquid_outlet_p - vapor_outlet_p)/1e5, digits=3)) bar") + + # Flow rates + liquid_flow = sol[sys_simplified.flash_drum.LiquidOutPort.ṅ[1]][end] + vapor_flow = sol[sys_simplified.flash_drum.VaporOutPort.ṅ[1]][end] + + println("\nFlow Rates:") + println(" Feed: $(round(feed_flowrate, digits=2)) mol/s") + println(" Liquid outlet: $(round(abs(liquid_flow), digits=2)) mol/s") + println(" Vapor outlet: $(round(abs(vapor_flow), digits=2)) mol/s") + println(" Material balance: $(round(feed_flowrate + liquid_flow + vapor_flow, digits=4)) mol/s (should be ≈ 0)") + + # Compositions + println("\nCompositions (methane/propane):") + println(" Feed: $(feed_composition)") + liquid_comp_1 = sol[sys_simplified.flash_drum.LiquidOutPort.z[1,1]][end] + liquid_comp_2 = sol[sys_simplified.flash_drum.LiquidOutPort.z[2,1]][end] + println(" Liquid: [$(round(liquid_comp_1, digits=3)), $(round(liquid_comp_2, digits=3))]") + + vapor_comp_1 = sol[sys_simplified.flash_drum.VaporOutPort.z[1,1]][end] + vapor_comp_2 = sol[sys_simplified.flash_drum.VaporOutPort.z[2,1]][end] + println(" Vapor: [$(round(vapor_comp_1, digits=3)), $(round(vapor_comp_2, digits=3))]") + + # ============================================================================= + # 10. VERIFICATION TESTS + # ============================================================================= + @test sol.retcode == ReturnCode.Success + @test liquid_level > 0.0 && liquid_level < drum_height + @test liquid_outlet_p > vapor_outlet_p # Hydrostatic head check + @test abs(feed_flowrate + liquid_flow + vapor_flow) < 1.0 # Material balance + @test liquid_comp_2 > feed_composition[2] # Liquid enriched in heavy component (propane) + @test vapor_comp_1 > feed_composition[1] # Vapor enriched in light component (methane) + + println("\n=== ALL TESTS PASSED ===") \ No newline at end of file From 36ce2659e63fff682735ae1286250c6c599eecc2 Mon Sep 17 00:00:00 2001 From: Vinicius Santana Date: Thu, 30 Oct 2025 11:39:22 +0100 Subject: [PATCH 07/13] Refactored everything and adding tests - docs still pending --- test/base/valve_test.jl | 1 + test/runtests.jl | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/base/valve_test.jl b/test/base/valve_test.jl index a62df77..3a7c08c 100644 --- a/test/base/valve_test.jl +++ b/test/base/valve_test.jl @@ -6,6 +6,7 @@ using ModelingToolkit: t_nounits as t, D_nounits as D using Test + @testset "isenthalpic valve" begin #Building media components = ["carbon dioxide", "methane"] diff --git a/test/runtests.jl b/test/runtests.jl index f63cd61..551349b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,7 +2,7 @@ using ProcessSimulator using Test using SafeTestsets -@safetestset "Valve" begin +#= @safetestset "Valve" begin include("base/valve_test.jl") -end +end =# From 9a6f020eb4819217bd870369251c0710189fce84 Mon Sep 17 00:00:00 2001 From: Vinicius Santana Date: Thu, 30 Oct 2025 12:50:22 +0100 Subject: [PATCH 08/13] fixed valve and exports --- src/base/print.jl | 2 +- src/pressure_drop/valve.jl | 16 +- src/utils/SolidsProp.jl | 2 + src/utils/TransportProp.jl | 4 +- test/base/fixed_pTzN_boundary.jl | 25 +- test/base/valve_test.jl | 6 +- test/reactors/{cstr.jl => ss_cstr_test.jl} | 0 test/separation/flash_drum_test.jl | 265 --------------------- 8 files changed, 26 insertions(+), 294 deletions(-) rename test/reactors/{cstr.jl => ss_cstr_test.jl} (100%) diff --git a/src/base/print.jl b/src/base/print.jl index beb56de..2edee54 100644 --- a/src/base/print.jl +++ b/src/base/print.jl @@ -85,7 +85,7 @@ function Base.show(io::IO, valve::Valve) println(io, "\nThermodynamic State Guess:") println(io, " • Pressure: $(valve.state.p) Pa") println(io, " • Temperature: $(valve.state.T) K") - println(io, " • Composition: $(valve.state.z)") + println(io, " • Composition: $(valve.state.N ./ sum(valve.state.N))") # Density information from medium's guesses if available if hasproperty(valve.medium, :Guesses) && hasproperty(valve.medium.Guesses, :ρ) diff --git a/src/pressure_drop/valve.jl b/src/pressure_drop/valve.jl index 22a9d5c..461a4e9 100644 --- a/src/pressure_drop/valve.jl +++ b/src/pressure_drop/valve.jl @@ -10,18 +10,24 @@ mutable struct Valve{M <: AbstractFluidMedium, C <: Real, F <: Function, S <: Ab f::F phase::S odesystem + model_type::Symbol end -function Valve(; medium, state_guess::S, Cv, f, flowrate_guess = 1.0, flowbasis = :volume, name) where S <: pTzState - p, T, z = state_guess.p, state_guess.T, state_guess.z - medium.Guesses = EosBasedGuesses(medium.EoSModel, p, T, z, Val(:Pressure)) - phase = ifelse(medium.Guesses.ϕ[2] ≈ 1.0, "vapor", "liquid") +function Valve(; medium, state::S, Cv, f, flowrate_guess = 1.0, flowbasis = :volume, name) where S <: AbstractThermodynamicState + # Use resolve_guess! to update medium and state (consistent with CSTR and Boundary_pTzn) + medium, state, phase = resolve_guess!(medium, state) + + # Extract p, T, z from state + p = state.p + T = state.T + z = state.N/sum(state.N) + flowstate = rhoTzState(medium.Guesses.ρ[1], T, z) opening_setpoint = 0.5 molar_flowrate_guess = XtoMolar(flowrate_guess, medium, flowstate, flowbasis) odesystem = Valve_(medium = medium, Cv = Cv, ΔP_f = f, setpoint = opening_setpoint, phase = phase, name = name) - return Valve(medium, state_guess, molar_flowrate_guess, Cv, opening_setpoint, f, phase, odesystem) + return Valve(medium, state, molar_flowrate_guess, Cv, opening_setpoint, f, phase, odesystem, :Valve) end @component function Valve_(;medium, Cv, ΔP_f, setpoint, phase, name) diff --git a/src/utils/SolidsProp.jl b/src/utils/SolidsProp.jl index 9837f13..1bb9131 100644 --- a/src/utils/SolidsProp.jl +++ b/src/utils/SolidsProp.jl @@ -106,4 +106,6 @@ function heat_transfer_coefficient(solid::A, T = 273.15, x = ones(adsorbent.Cons return heat_transfer_coefficient(solid.TransportModel.HeatTransferModel, solid, T, x) end +export SolidEoSModel, Adsorbent + diff --git a/src/utils/TransportProp.jl b/src/utils/TransportProp.jl index 1d2b783..0469405 100644 --- a/src/utils/TransportProp.jl +++ b/src/utils/TransportProp.jl @@ -43,4 +43,6 @@ end function TransportModel(masstransfermodel, heattransfermodel, viscositymodel) return TransportModel(masstransfermodel, heattransfermodel, viscositymodel, nothing, nothing) -end \ No newline at end of file +end + +export HomogeneousDiffusivityCoeff, ConstantHeatTransferCoeff, ConstantMassTransferCoeff, TransportModel \ No newline at end of file diff --git a/test/base/fixed_pTzN_boundary.jl b/test/base/fixed_pTzN_boundary.jl index 41b28f8..1aefebc 100644 --- a/test/base/fixed_pTzN_boundary.jl +++ b/test/base/fixed_pTzN_boundary.jl @@ -1,17 +1,18 @@ +using ProcessSimulator using ModelingToolkit using Clapeyron -using LinearAlgebra using ModelingToolkit: t_nounits as t, D_nounits as D using ModelingToolkit: scalarize, equations, get_unknowns using NonlinearSolve using OrdinaryDiffEq +using EntropyScaling #Building media components = ["carbon dioxide", "methane"] -model = SRK(components, idealmodel = ReidIdeal) +#model = SRK(components, idealmodel = ReidIdeal) -#model = ReidIdeal(components) +model = ReidIdeal(components) p__ = 1.0*101325.0 # Pa T__ = 273.15 + 25.0 # K @@ -21,22 +22,7 @@ masstransfermodel = ConstantMassTransferCoeff([5e-1, 3e-1]); #This assumes the s heat_transfer_model = ConstantHeatTransferCoeff(10.0); viscosity_model = ChapmanEnskogModel(components, ref = "Poling et al. (2001)") fluid_transport_model = TransportModel(masstransfermodel, heat_transfer_model, viscosity_model) -medium = EoSBased(components = components, eosmodel = model, transportmodel = fluid_transport_model, state = pTzState(p__, T__, z__)) - - -### ------ Reservoir test - -@named reservoir = FixedBoundary_pTzn_(medium = medium, p = p__, T = T__, z = z__, flowrate = -5e-4, flowbasis = :volume) -@named sink = ConnHouse(medium = medium) - -connections = [connect(reservoir.OutPort, sink.port)] - -@named sys = System(connections, t; systems = [reservoir, sink]) - -prob = NonlinearProblem(simple_stream, guesses(simple_stream)) -@time sol = solve(prob, RobustMultiNewton()) -sol[reservoir.ControlVolumeState.ϕ] - +medium = EoSBased(components = components, eosmodel = model, transportmodel = fluid_transport_model) @@ -44,6 +30,7 @@ sol[reservoir.ControlVolumeState.ϕ] ## Solid Eos adsorbenteos = SolidEoSModel(750.0, 273.15, [0.0, 0.0, 0.0, 0.0], 0.0, 273.15, [935.0, 0.0, 0.0, 0.0]) #J/kg/K +using Langmuir # Isotherm iso_c1 = LangmuirS1(1e-8, 1e-8, -30_000.1456) iso_c2 = LangmuirS1(10.0, 1e-9, -30_000.1456) diff --git a/test/base/valve_test.jl b/test/base/valve_test.jl index 3a7c08c..f7356a3 100644 --- a/test/base/valve_test.jl +++ b/test/base/valve_test.jl @@ -7,7 +7,7 @@ using Test -@testset "isenthalpic valve" begin +#= @testset "isenthalpic valve" begin =# #Building media components = ["carbon dioxide", "methane"] @@ -22,7 +22,7 @@ medium = EoSBased(components = components, eosmodel = model) @named S1 = FixedBoundary_pTz_(medium = medium, p = p__, T = T__, z = z__) @named V1 = Valve(medium = medium, -state_guess = pTzState(0.5*p__, T__, z__), +state = pTNVState(0.5*p__, T__, z__), Cv = 4.4e-6, f = x -> x/sqrt(abs(x) + 1e-8), flowrate_guess = 1e-3, @@ -53,4 +53,4 @@ reference_T_valve_out = 290.39815 @test abs(reference_T_valve_out - T_valve_out)/reference_T_valve_out < 0.05 -end \ No newline at end of file +#= end =# \ No newline at end of file diff --git a/test/reactors/cstr.jl b/test/reactors/ss_cstr_test.jl similarity index 100% rename from test/reactors/cstr.jl rename to test/reactors/ss_cstr_test.jl diff --git a/test/separation/flash_drum_test.jl b/test/separation/flash_drum_test.jl index 1e6c700..e69de29 100644 --- a/test/separation/flash_drum_test.jl +++ b/test/separation/flash_drum_test.jl @@ -1,265 +0,0 @@ -using ProcessSimulator - - -# Load Clapeyron for thermodynamic properties -using Clapeyron - - - - -components = ["methane", "propane"] -eos = PR(components, idealmodel = ReidIdeal) # Peng-Robinson equation of state - -# ============================================================================= -# Simulation Scenario -# Flash drum originally at 30bar with equimolar methane/propane holdup (global) and a feed mix -# at different composition and vaporized fraction comes in at 30bar - - - -# ============================================================================= -# 2. FLASH DRUM SIZING -# ============================================================================= -# Design criteria for vapor-liquid separator: -# - Residence time: 5-10 minutes for liquid -# - L/D ratio: typically 3-5 for horizontal drums, 2-4 for vertical -# - Vapor velocity: limited to avoid entrainment (typically < 1 m/s) - -# Vessel sizing based on residence time and vapor velocity -initial_state = Clapeyron.qp_flash(eos, 0.5, 3e6, 100.0*[0.5, 0.5]) # 25°C, 30 bar, equimolar -volume(initial_state) #m^3 - - -# Estimate flash conditions at lower pressure -flash_pressure = 20e5 # 20 bar -flash_temp = initial_state.data.T # 263.15 K (-10°C) - colder to get more liquid - -# Rough estimate: at these conditions, expect ~50% vaporization -# For 5 min residence time with 50 mol/s liquid: -# Liquid volume needed ≈ 5 min × 60 s/min × 50 mol/s / ρ_liquid -# Assuming ρ_liquid ≈ 15000 mol/m³ (typical for light hydrocarbons) -estimated_liquid_holdup = 5 * 60 * 50 / 15000 # ≈ 1 m³ - -# Design vertical drum with L/D = 3 -drum_diameter = 1.5 # m -drum_height = 4.5 # m (L/D = 3) -drum_volume = π * (drum_diameter/2)^2 * drum_height # ≈ 7.95 m³ - -geometry = CylindricalTank(D = drum_diameter, L = drum_height) - -println("=== Flash Drum Design ===") -println("Diameter: $(drum_diameter) m") -println("Height: $(drum_height) m") -println("Total Volume: $(round(drum_volume, digits=2)) m³") -println("L/D ratio: $(round(drum_height/drum_diameter, digits=2))") -println() - - - # ============================================================================= - # 4. VALVE SIZING - # ============================================================================= - # Inlet valve: pressure drop from 30 bar to 20 bar - # Cv = Q / √(ΔP) where Q is flow and ΔP is pressure drop - # For gas: Cv = (Q_scfh / 963) × √(ρ/ΔP) × √(T/520) - # Simplified: use moderate Cv values - - inlet_valve_cv = 10.0 # Flow coefficient (imperial units equivalent) - liquid_valve_cv = 5.0 # Smaller valve for liquid control - vapor_valve_cv = 15.0 # Larger valve for vapor (lower density) - - # Valve pressure drop functions - # For simplicity: ΔP ∝ (flow)² / Cv² - function valve_pressure_drop(cv) - return (state, port) -> begin - # Simple pressure drop: ΔP = k × ṅ² / Cv² - # k is a constant, here we use 1e4 for reasonable pressure drops - k = 1e4 - return k * (port.ṅ[1])^2 / cv^2 - end - end - - # ============================================================================= - # 5. BUILD SYSTEM COMPONENTS - # ============================================================================= - - # Feed stream (high pressure) - @named feed = FixedBoundary_pTzn_( - medium = medium, - p = feed_pressure, - T = feed_temp, - z = feed_composition, - flowrate = feed_flowrate, - flowbasis = :molar - ) - - # Inlet valve (throttling valve) - inlet_valve_state = pTNVState( - flash_pressure, # Outlet pressure - flash_temp, - feed_composition .* feed_flowrate, - nothing - ) - - @named inlet_valve = Valve( - medium = medium, - Cv = inlet_valve_cv, - valve_equation = valve_pressure_drop(inlet_valve_cv), - name = "inlet_valve", - state = inlet_valve_state - ) - - # Flash drum - @named flash_drum = SteadyStateFlashDrum( - medium = medium, - state = flash_state, - Q = 0.0 # Adiabatic operation - ) - - # Liquid outlet valve - liquid_valve_pressure = flash_pressure - 2e5 # Drain to 18 bar - liquid_valve_state = pTNVState( - liquid_valve_pressure, - flash_temp, - [0.3, 0.7] .* 40.0, # Assuming liquid-rich in propane - nothing - ) - - @named liquid_valve = Valve( - medium = medium, - Cv = liquid_valve_cv, - valve_equation = valve_pressure_drop(liquid_valve_cv), - name = "liquid_valve", - state = liquid_valve_state - ) - - # Vapor outlet valve - vapor_valve_pressure = flash_pressure - 1e5 # Vapor to 19 bar - vapor_valve_state = pTNVState( - vapor_valve_pressure, - flash_temp, - [0.8, 0.2] .* 60.0, # Assuming vapor-rich in methane - nothing - ) - - @named vapor_valve = Valve( - medium = medium, - Cv = vapor_valve_cv, - valve_equation = valve_pressure_drop(vapor_valve_cv), - name = "vapor_valve", - state = vapor_valve_state - ) - - # Outlet pressure boundaries - @named liquid_sink = ConstantPressure(medium = medium, p = liquid_valve_pressure) - @named vapor_sink = ConstantPressure(medium = medium, p = vapor_valve_pressure) - - # ============================================================================= - # 6. CONNECT SYSTEM - # ============================================================================= - @named flash_system = ODESystem([ - # Feed -> Inlet Valve -> Flash Drum - connect(feed.OutPort, inlet_valve.InPort) - connect(inlet_valve.OutPort, flash_drum.InPort) - - # Flash Drum -> Outlet Valves -> Sinks - connect(flash_drum.LiquidOutPort, liquid_valve.InPort) - connect(flash_drum.VaporOutPort, vapor_valve.InPort) - connect(liquid_valve.OutPort, liquid_sink.Port) - connect(vapor_valve.OutPort, vapor_sink.Port) - ], t) - - # ============================================================================= - # 7. SIMPLIFY AND SOLVE - # ============================================================================= - println("=== Simplifying system ===") - sys_simplified = structural_simplify(flash_system) - - println("Number of equations: ", length(equations(sys_simplified))) - println("Number of unknowns: ", length(unknowns(sys_simplified))) - println() - - # Initial conditions - u0 = [ - sys_simplified.flash_drum.Nᵢ[1] => 100.0, # Initial methane holdup - sys_simplified.flash_drum.Nᵢ[2] => 100.0, # Initial propane holdup - sys_simplified.flash_drum.ControlVolumeState.T => flash_temp - ] - - # Guesses for algebraic variables - guesses = [ - sys_simplified.flash_drum.V[2] => 2.0, # Initial liquid volume (m³) - sys_simplified.flash_drum.V[3] => drum_volume - 2.0, # Initial vapor volume - sys_simplified.flash_drum.nᴸⱽ[1] => 100.0, # Initial liquid moles - sys_simplified.flash_drum.nᴸⱽ[2] => 100.0, # Initial vapor moles - sys_simplified.flash_drum.h_liquid => 1.5, # Initial liquid height (m) - ] - - # ============================================================================= - # 8. CREATE AND SOLVE PROBLEM - # ============================================================================= - println("=== Creating ODE problem ===") - tspan = (0.0, 500.0) # 500 seconds simulation - prob = ODEProblem(sys_simplified, u0, tspan, guesses = guesses) - - println("=== Solving system ===") - sol = solve(prob, FBDF(autodiff = false), abstol = 1e-8, reltol = 1e-6) - - # ============================================================================= - # 9. EXTRACT AND DISPLAY RESULTS - # ============================================================================= - println("\n=== STEADY-STATE RESULTS ===") - - # Flash drum properties - liquid_level = sol[sys_simplified.flash_drum.h_liquid][end] - liquid_volume = sol[sys_simplified.flash_drum.V[2]][end] - vapor_volume = sol[sys_simplified.flash_drum.V[3]][end] - - println("\nFlash Drum:") - println(" Liquid level: $(round(liquid_level, digits=3)) m") - println(" Liquid volume: $(round(liquid_volume, digits=3)) m³") - println(" Vapor volume: $(round(vapor_volume, digits=3)) m³") - println(" Liquid fraction: $(round(liquid_volume/drum_volume*100, digits=1))%") - - # Pressures - drum_pressure = sol[sys_simplified.flash_drum.ControlVolumeState.p][end] - liquid_outlet_p = sol[sys_simplified.flash_drum.LiquidOutPort.p][end] - vapor_outlet_p = sol[sys_simplified.flash_drum.VaporOutPort.p][end] - - println("\nPressures:") - println(" Drum vapor space: $(round(drum_pressure/1e5, digits=2)) bar") - println(" Liquid outlet: $(round(liquid_outlet_p/1e5, digits=2)) bar") - println(" Vapor outlet: $(round(vapor_outlet_p/1e5, digits=2)) bar") - println(" Hydrostatic head: $(round((liquid_outlet_p - vapor_outlet_p)/1e5, digits=3)) bar") - - # Flow rates - liquid_flow = sol[sys_simplified.flash_drum.LiquidOutPort.ṅ[1]][end] - vapor_flow = sol[sys_simplified.flash_drum.VaporOutPort.ṅ[1]][end] - - println("\nFlow Rates:") - println(" Feed: $(round(feed_flowrate, digits=2)) mol/s") - println(" Liquid outlet: $(round(abs(liquid_flow), digits=2)) mol/s") - println(" Vapor outlet: $(round(abs(vapor_flow), digits=2)) mol/s") - println(" Material balance: $(round(feed_flowrate + liquid_flow + vapor_flow, digits=4)) mol/s (should be ≈ 0)") - - # Compositions - println("\nCompositions (methane/propane):") - println(" Feed: $(feed_composition)") - liquid_comp_1 = sol[sys_simplified.flash_drum.LiquidOutPort.z[1,1]][end] - liquid_comp_2 = sol[sys_simplified.flash_drum.LiquidOutPort.z[2,1]][end] - println(" Liquid: [$(round(liquid_comp_1, digits=3)), $(round(liquid_comp_2, digits=3))]") - - vapor_comp_1 = sol[sys_simplified.flash_drum.VaporOutPort.z[1,1]][end] - vapor_comp_2 = sol[sys_simplified.flash_drum.VaporOutPort.z[2,1]][end] - println(" Vapor: [$(round(vapor_comp_1, digits=3)), $(round(vapor_comp_2, digits=3))]") - - # ============================================================================= - # 10. VERIFICATION TESTS - # ============================================================================= - @test sol.retcode == ReturnCode.Success - @test liquid_level > 0.0 && liquid_level < drum_height - @test liquid_outlet_p > vapor_outlet_p # Hydrostatic head check - @test abs(feed_flowrate + liquid_flow + vapor_flow) < 1.0 # Material balance - @test liquid_comp_2 > feed_composition[2] # Liquid enriched in heavy component (propane) - @test vapor_comp_1 > feed_composition[1] # Vapor enriched in light component (methane) - - println("\n=== ALL TESTS PASSED ===") \ No newline at end of file From 93722162a527b643bd222efcbe191f983e0f03d5 Mon Sep 17 00:00:00 2001 From: Vinicius Santana Date: Thu, 13 Nov 2025 12:43:32 +0100 Subject: [PATCH 09/13] Pre code for the TP flash example --- .gitignore | 2 + Project.toml | 2 +- src/Reactors/CSTR.jl | 22 ++++++---- src/base/basecomponents.jl | 8 ++-- src/separation/Adsorption.jl | 54 +++++++++++++++++++++--- src/separation/FlashDrum.jl | 67 ++++++++++++++++++++++++++++++ src/utils/Geometry.jl | 2 + src/utils/SolidsProp.jl | 2 +- test/base/fixed_pTzN_boundary.jl | 15 ++++--- test/base/valve_test.jl | 2 +- test/reactors/ss_cstr_test.jl | 2 +- test/separation/flash_drum_test.jl | 59 ++++++++++++++++++++++++++ 12 files changed, 207 insertions(+), 30 deletions(-) diff --git a/.gitignore b/.gitignore index 001c5ba..93aa8cc 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ /docs/build/ Manifest.toml .vscode/ +ManualEML.pdf +src/separation/steady_flash.md diff --git a/Project.toml b/Project.toml index 7e9f9ad..e361b84 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ProcessSimulator" uuid = "8886c03b-4dde-4be1-b6ee-87d056f985b8" -authors = ["Vinicius Viena Santana"] +authors = ["Vinicius Viena Santana", "Avinash Subramanian", "Chris Rackauckas"] version = "0.0.1-DEV" [deps] diff --git a/src/Reactors/CSTR.jl b/src/Reactors/CSTR.jl index a674eaf..0e766c8 100644 --- a/src/Reactors/CSTR.jl +++ b/src/Reactors/CSTR.jl @@ -102,9 +102,12 @@ function SteadyStateCSTR(;medium, reactionset, limiting_reactant, state, W, Q, n odesystem = SteadyStateCSTRModel(medium = medium, reactions = reactionset, limiting_reactant = limiting_reactant, state = state, W = W, Q = Q, phase = phase, name = name) - if !isnothing(Q) #If heat is given use, else fix temperature and calculate heat - q_eq = [odesystem.Q ~ Q] - else + _Q = copy(Q) + + if !isnothing(_Q) #If heat is given use, else fix temperature and calculate heat + @unpack Q = odesystem + q_eq = [Q ~ _Q] + else @unpack ControlVolumeState = odesystem q_eq = [ControlVolumeState.T ~ state.T] end @@ -113,10 +116,11 @@ function SteadyStateCSTR(;medium, reactionset, limiting_reactant, state, W, Q, n return SteadyStateCSTR(medium, state, reactionset, phase, newsys, :CSTR) end -function ConversionSteadyStateCSTR(; medium, reactionset, limiting_reactant, state, W, Q, conversion, name) +function ConversionSteadyStateCSTR(; medium, reactionset, limiting_reactant, state, W, Q, conversion = 0.5, name) cstr = SteadyStateCSTR(medium = medium, reactionset = reactionset, limiting_reactant = limiting_reactant, state = state, W = W, Q = Q, name = name) odesys = cstr.odesystem - newsys = extend(System([odesys.X ~ conversion], t, [], []; name), odesys) + @unpack X, ControlVolumeState = odesys + newsys = extend(System([X ~ conversion, ControlVolumeState.p ~ cstr.state.p], t, [], []; name), odesys) cstr.odesystem = newsys return SteadyStateCSTR(cstr.medium, cstr.state, cstr.reactionset, cstr.phase, cstr.odesystem, :CSTR) end @@ -124,8 +128,8 @@ end function FixedVolumeSteadyStateCSTR(; medium, reactionset, limiting_reactant, state, W, Q, volume, name) #Overwrites state volume and recalculate number of moles cstr = SteadyStateCSTR(medium = medium, reactionset = reactionset, limiting_reactant = limiting_reactant, state = state, W = W, Q = Q, name = name) odesys = cstr.odesystem - @unpack V = odesys - newsys = extend(System([V[1] ~ volume], t, [], []; name), odesys) #Fix overall volume to be V + @unpack V, ControlVolumeState = odesys + newsys = extend(System([V[1] ~ volume, ControlVolumeState.p ~ cstr.state.p], t, [], []; name), odesys) #Fix overall volume to be V cstr.odesystem = newsys return SteadyStateCSTR(cstr.medium, cstr.state, cstr.reactionset, cstr.phase, cstr.odesystem, :CSTR) end @@ -147,7 +151,7 @@ end Wₛ ~ W scalarize(rₐ[:, 2:end] .~ 0.0)... U ~ (OutPort.h[1] - ControlVolumeState.p/ControlVolumeState.ρ[1])*sum(collect(Nᵢ)) - ControlVolumeState.p ~ state.p + ] limiting_index = findfirst(x -> x == limiting_reactant, medium.Constants.iupacName) @@ -182,7 +186,7 @@ end scalarize(X ~ (InPort.ṅ[3].*InPort.z[limiting_index, 3] .+ OutPort.ṅ[3].*OutPort.z[limiting_index, 3])./(InPort.ṅ[3].*InPort.z[limiting_index, 3] .+ 1e-8)) ] - + end pars = [] diff --git a/src/base/basecomponents.jl b/src/base/basecomponents.jl index d71622e..fd34a51 100644 --- a/src/base/basecomponents.jl +++ b/src/base/basecomponents.jl @@ -65,11 +65,11 @@ end vars = [] systems = @named begin - port = PhZConnector_(medium = medium) + Port = PhZConnector_(medium = medium) end eqs = [ - port.ṅ[1] ~ XtoMolar(flowrate, medium, nothing, flowbasis) + Port.ṅ[1] ~ XtoMolar(flowrate, medium, nothing, flowbasis) ] ODESystem(eqs, t, collect(Iterators.flatten(vars)), pars; systems, name) @@ -428,8 +428,8 @@ end [Nᵢ[i] ~ sum(dot(nᴸⱽ, collect(ControlVolumeState.z[i, 2:end]))) for i ∈ 1:medium.Constants.Nc]... scalarize(sum(Nᵢ) ~ sum(nᴸⱽ)) - scalarize(ControlVolumeState.z[:, 1] .~ Nᵢ ./ (sum(collect(Nᵢ)) + 1e-10))... - ControlVolumeState.ϕ[1] ~ nᴸⱽ[1]/(sum(collect(nᴸⱽ)) + 1e-10) + scalarize(ControlVolumeState.z[:, 1] .~ Nᵢ ./ (sum(collect(Nᵢ))))... + ControlVolumeState.ϕ[1] ~ nᴸⱽ[1]/(sum(collect(nᴸⱽ))) # Control Volume properties V[1] ~ sum(collect(V[2:end])) diff --git a/src/separation/Adsorption.jl b/src/separation/Adsorption.jl index 71bb000..1e0f1ce 100644 --- a/src/separation/Adsorption.jl +++ b/src/separation/Adsorption.jl @@ -1,3 +1,5 @@ +abstract type AbstractAdsorber end + @component function AdsorptionInterface(;fluidmedium, solidmedium, adsorbentmass, name) systems = @named begin @@ -25,15 +27,55 @@ end -@component function WellMixedAdsorber(;fluidmedium, solidmedium, porosity, V, p, phase, name) +mutable struct WellMixedAdsorber{FM <: AbstractFluidMedium, SM <: AbstractFluidMedium, S <: AbstractThermodynamicState} <: AbstractAdsorber + fluidmedium::FM + solidmedium::SM + state::S + phase::String + odesystem + model_type::Symbol +end + +""" + WellMixedAdsorber(; fluidmedium, solidmedium, state, porosity, V, name) + +Creates a well-mixed adsorber with fluid and solid phases in equilibrium. + +# Arguments +- `fluidmedium`: Fluid medium specification +- `solidmedium`: Solid/adsorbent medium specification +- `state`: Thermodynamic state for the fluid phase +- `porosity`: Bed porosity (void fraction) +- `V`: Total volume [m³] +- `name`: Component name + +# Returns +- `WellMixedAdsorber` struct with embedded ODESystem and model_type = :Adsorber +""" +function WellMixedAdsorber(; fluidmedium, solidmedium, state::S, porosity, V, name) where S <: AbstractThermodynamicState + # Use resolve_guess! to update fluidmedium and state + fluidmedium, state, phase = resolve_guess!(fluidmedium, state) + + # Extract pressure from state + p = state.p + + odesystem = WellMixedAdsorber_Model(fluidmedium = fluidmedium, solidmedium = solidmedium, + porosity = porosity, V = V, p = p, phase = phase, name = name) + + return WellMixedAdsorber(fluidmedium, solidmedium, state, phase, odesystem, :Adsorber) +end + +@component function WellMixedAdsorber_Model(;fluidmedium, solidmedium, porosity, V, p, phase, name) mass_of_adsorbent = V * solidmedium.EoSModel.ρ_T0 * porosity A = V*(1.0 - porosity)*area_per_volume(solidmedium) #Interfacial area systems = @named begin + mobilephase = TwoPortControlVolume_(medium = fluidmedium) stationaryphase = ClosedControlVolume_(medium = solidmedium) interface = AdsorptionInterface(fluidmedium = fluidmedium, solidmedium = solidmedium, adsorbentmass = mass_of_adsorbent) + end eqs = [ @@ -51,7 +93,7 @@ end scalarize(interface.SolidSurface.OutPort.T ~ stationaryphase.ControlVolumeState.T) # Heat and mass transfer - stationaryphase.Q ~ interface.SolidSurface.InPort.ϕₕ*A + sum(-collect(isosteric_heat(adsorbent.isotherm, interface.FluidSurface.OutPort.μ, interface.FluidSurface.OutPort.T).*stationaryphase.rₐ[:, end])) + stationaryphase.Q ~ interface.SolidSurface.InPort.ϕₕ*A + sum(-collect(isosteric_heat(solidmedium.isotherm, interface.FluidSurface.OutPort.μ, interface.FluidSurface.OutPort.T).*stationaryphase.rₐ[:, end])) scalarize(stationaryphase.rₐ[:, end] .~ interface.SolidSurface.InPort.ϕₘ)... # No Volumetric sink/source constraints @@ -77,7 +119,7 @@ end scalarize(mobilephase.rₐ[:, 2] .~ interface.FluidSurface.OutPort.ϕₘ*A)... scalarize(mobilephase.rₐ[:, end] .~ 0.0)... - scalarize(mobilephase.ControlVolumeState.z[:, end] .~ flash_mol_fractions_vapor(medium.EoSModel, mobilephase.ControlVolumeState.p, mobilephase.ControlVolumeState.T, collect(mobilephase.ControlVolumeState.z[:, 1])))... + scalarize(mobilephase.ControlVolumeState.z[:, end] .~ flash_mol_fractions_vapor(fluidmedium.EoSModel, mobilephase.ControlVolumeState.p, mobilephase.ControlVolumeState.T, collect(mobilephase.ControlVolumeState.z[:, 1])))... scalarize(interface.FluidSurface.InPort.μ .~ mobilephase.nᴸⱽ[1]/mobilephase.V[2])... #This is phase specific @@ -93,7 +135,7 @@ end scalarize(mobilephase.rₐ[:, 2] .~ 0.0)... scalarize(mobilephase.rₐ[:, end] .~ interface.FluidSurface.OutPort.ϕₘ*A)... - scalarize(mobilephase.ControlVolumeState.z[:, 2] .~ flash_mol_fractions_liquid(medium.EoSModel, mobilephase.ControlVolumeState.p, mobilephase.ControlVolumeState.T, collect(mobilephase.ControlVolumeState.z[:, 1])))... + scalarize(mobilephase.ControlVolumeState.z[:, 2] .~ flash_mol_fractions_liquid(fluidmedium.EoSModel, mobilephase.ControlVolumeState.p, mobilephase.ControlVolumeState.T, collect(mobilephase.ControlVolumeState.z[:, 1])))... scalarize(interface.FluidSurface.InPort.μ .~ mobilephase.ControlVolumeState.p*mobilephase.ControlVolumeState.z[:, end])... #Partial pressure in vapor phase/rigorously is fugacity @@ -109,4 +151,6 @@ end return System([eqs...; phase_eqs...], t, collect(Iterators.flatten(vars)), pars; name, systems = systems) -end \ No newline at end of file +end + +export WellMixedAdsorber, WellMixedAdsorber_Model, AdsorptionInterface \ No newline at end of file diff --git a/src/separation/FlashDrum.jl b/src/separation/FlashDrum.jl index c17227b..c490dfd 100644 --- a/src/separation/FlashDrum.jl +++ b/src/separation/FlashDrum.jl @@ -68,3 +68,70 @@ end export DynamicFlashDrum + + +# ==================== Steady-State Flash Drum ==================== + +mutable struct SteadyStateFlashDrum{M <: AbstractFluidMedium, S <: AbstractThermodynamicState} <: AbstractSeparator + medium::M + state::S + odesystem +end + +function SteadyStateFlashDrum(; medium, state, Q, name) + medium, state, phase = resolve_guess!(medium, state) + odesystem = SteadyStateFlashDrumModel(medium = medium, state = state, Q = Q, name = name) + _Q = copy(Q) + + if !isnothing(_Q) + @unpack Q = odesystem + q_eq = [Q ~ _Q] + else + @unpack ControlVolumeState = odesystem + q_eq = [ControlVolumeState.T ~ state.T] + end + + newsys = extend(System(q_eq, t, [], []; name), odesystem) + return SteadyStateFlashDrum(medium, state, newsys) +end + +function FixedPressureSteadyStateFlashDrum(; medium, state, Q, pressure, name) + flash = SteadyStateFlashDrum(medium = medium, state = state, Q = Q, name = name) + odesys = flash.odesystem + @unpack ControlVolumeState = odesys + newsys = extend(System([ControlVolumeState.p ~ pressure], t, [], []; name), odesys) + flash.odesystem = newsys + return SteadyStateFlashDrum(flash.medium, flash.state, flash.odesystem) +end + +@component function SteadyStateFlashDrumModel(; medium, state, Q = nothing, name) + + @named CV = ThreePortControlVolume_SteadyState(medium = medium) + @unpack U, Nᵢ, V, nᴸⱽ, InPort, LiquidOutPort, VaporOutPort, ControlVolumeState, rₐ, rᵥ, Wₛ = CV + + vars = [] + + pars = [] + + # Basic equations + eqs = [ + # No reactions or surface mass transfer + scalarize(rₐ[:, 2:end] .~ 0.0)... + scalarize(rᵥ[:, 2:end] .~ 0.0)... + + # No shaft work in flash drum + Wₛ ~ 0.0 + + # Vapor-liquid equilibrium + scalarize(ControlVolumeState.z[:, end] .~ flash_mol_fractions_vapor(medium.EoSModel, ControlVolumeState.p, ControlVolumeState.T, collect(ControlVolumeState.z[:, 1])))... + + # Internal energy definition (both phases) + U ~ (LiquidOutPort.h[2] - ControlVolumeState.p/ControlVolumeState.ρ[2])*nᴸⱽ[1] + + (VaporOutPort.h[3] - ControlVolumeState.p/ControlVolumeState.ρ[3])*nᴸⱽ[2] + ] + + return extend(System([eqs...], t, collect(Iterators.flatten(vars)), pars; name), CV) +end + + +export SteadyStateFlashDrum, FixedPressureSteadyStateFlashDrum diff --git a/src/utils/Geometry.jl b/src/utils/Geometry.jl index 1b343bf..2f6a623 100644 --- a/src/utils/Geometry.jl +++ b/src/utils/Geometry.jl @@ -26,3 +26,5 @@ end function cross_section_area_(tank::CylindricalTank) return π*(tank.D/2)^2 end + +export CylindricalTank, volume_, surface_area_, cross_section_area_, discretize! \ No newline at end of file diff --git a/src/utils/SolidsProp.jl b/src/utils/SolidsProp.jl index 1bb9131..264e1af 100644 --- a/src/utils/SolidsProp.jl +++ b/src/utils/SolidsProp.jl @@ -106,6 +106,6 @@ function heat_transfer_coefficient(solid::A, T = 273.15, x = ones(adsorbent.Cons return heat_transfer_coefficient(solid.TransportModel.HeatTransferModel, solid, T, x) end -export SolidEoSModel, Adsorbent +export SolidEoSModel, Adsorbent, area_per_volume diff --git a/test/base/fixed_pTzN_boundary.jl b/test/base/fixed_pTzN_boundary.jl index 1aefebc..93b9c74 100644 --- a/test/base/fixed_pTzN_boundary.jl +++ b/test/base/fixed_pTzN_boundary.jl @@ -53,15 +53,14 @@ adsorbent = Adsorbent(adsorbent_name = "XYZ", #Set constants -porosity = 0.5 -V = 5e-3*10.0 -#p = 101325.0 -phase = "vapor" -solidmedium = adsorbent -fluidmedium = medium -mass_of_adsorbent = V * solidmedium.EoSModel.ρ_T0 * porosity -A = V*(1.0 - porosity)*area_per_volume(solidmedium) #Interfacial area +tank = CylindricalTank(D = 0.1, L = 0.5, porosity = 0.5) +volume_tank = volume_(tank) +mass_of_adsorbent = volume_tank * solidmedium.EoSModel.ρ_T0 * tank.porosity +A = volume_tank*(1.0 - tank.porosity)*area_per_volume(solidmedium) #Interfacial area + + +adsorbent.isotherm _0 = 1e-8 Ntot = 5.0 diff --git a/test/base/valve_test.jl b/test/base/valve_test.jl index f7356a3..c61913c 100644 --- a/test/base/valve_test.jl +++ b/test/base/valve_test.jl @@ -44,7 +44,7 @@ simple_sys = mtkcompile(sys) u0 = [V1.odesystem.opening => 0.5] valve_guesses = [V1.odesystem.InPort.ṅ[1] => V1.molar_flowrate_guess] prob = ODEProblem(simple_sys, u0, (0.0, 0.001), guesses = valve_guesses, use_scc = false) -sol = solve(prob, FBDF(autodiff = false), abstol = 1e-6, reltol = 1e-6) +@time sol = solve(prob, FBDF(autodiff = false), abstol = 1e-6, reltol = 1e-6) T_valve_out = first(sol[V1.odesystem.ControlVolumeState.T]) diff --git a/test/reactors/ss_cstr_test.jl b/test/reactors/ss_cstr_test.jl index 013a9c9..ae95556 100644 --- a/test/reactors/ss_cstr_test.jl +++ b/test/reactors/ss_cstr_test.jl @@ -48,7 +48,7 @@ connection_set = [ @named sys = System(connection_set, t, [], []; systems = [S1.odesystem, R1.odesystem, sink]) -AdiabaticVolumeReactor = mtkcompile(sys) +AdiabaticVolumeReactor = mtkcompile(sys, use_scc = true) default_guesses = guesses(AdiabaticVolumeReactor) guesses_Reactor = Dict( diff --git a/test/separation/flash_drum_test.jl b/test/separation/flash_drum_test.jl index e69de29..6d9e4ad 100644 --- a/test/separation/flash_drum_test.jl +++ b/test/separation/flash_drum_test.jl @@ -0,0 +1,59 @@ +using ProcessSimulator +using Clapeyron +using ModelingToolkit +using OrdinaryDiffEq +using NonlinearSolve +using ModelingToolkit: t_nounits as t, D_nounits as D +using Test + +# ==================== Steady-State Flash Drum Test ==================== +# Based on EMSO manual example (Section 3.2.4) +# Feed: 496.3 kmol/h at 338 K and 507.1 kPa +# Flash conditions: 2.5 atm, 315.87 K + +components = ["1,3-butadiene", "isobutene", "n-pentane", "1-pentene", "1-hexene", "benzene"] +model = PR(components, idealmodel = ReidIdeal) +medium = EoSBased(components = components, eosmodel = model) + +@named S1 = Boundary_pTzn( + medium = medium, + p = 507.1e3, + T = 338.0, + z = [0.2379, 0.3082, 0.09959, 0.1373, 0.08872, 0.1283], + flowrate = 496.3/3600, + flowbasis = :molar +) + +flash_state = pTNVState(2.5*101325.0, 315.87, [0.2379, 0.3082, 0.09959, 0.1373, 0.08872, 0.1283], base = :Pressure) + +@named FL1 = FixedPressureSteadyStateFlashDrum( + medium = medium, + state = flash_state, + pressure = 2.5*101325.0, + Q = 0.0 +) + +@named LiquidPort = ConnHouse(medium = medium) +@named VaporPort = ConnHouse(medium = medium) + +connection_set = [ + connect(S1.odesystem.OutPort, FL1.odesystem.InPort), + connect(FL1.odesystem.LiquidOutPort, LiquidPort.InPort), + connect(FL1.odesystem.VaporOutPort, VaporPort.InPort) +] + +@named sys = System(connection_set, t, [], []; + systems = [S1.odesystem, FL1.odesystem, LiquidPort, VaporPort]) + +SteadyStateFlash = mtkcompile(sys) + +default_guesses = guesses(SteadyStateFlash) +guesses_Flash = Dict( + FL1.odesystem.VaporOutPort.ṅ[1] => 396.3/3600, + FL1.odesystem.LiquidOutPort.ṅ[1] => 100.0/3600, +) +merged_guesses = merge(default_guesses, guesses_Flash) + +prob = NonlinearProblem(SteadyStateFlash, merged_guesses) +@time sol = solve(prob, abstol = 1e-8, reltol = 1e-8) + From fa8ba39f57f9111530c633f1f56c174e84e33582 Mon Sep 17 00:00:00 2001 From: Vinicius Santana Date: Thu, 13 Nov 2025 13:29:22 +0100 Subject: [PATCH 10/13] Complete refactor: steady-state flash drum, three-port architecture, enhanced base components - Added SteadyStateFlashDrum and FixedPressureSteadyStateFlashDrum - Implemented ThreePortControlVolume_ for phase separation - Enhanced base components with ConstantFlowRate, ConstantPressure, ConnHouse - Added comprehensive testing infrastructure - Restructured codebase with proper module organization - Removed obsolete files and database structure - Added solid properties and adsorption framework --- src/base/basecomponents.jl | 2 ++ test/separation/flash_drum_test.jl | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/base/basecomponents.jl b/src/base/basecomponents.jl index fd34a51..c3cd88d 100644 --- a/src/base/basecomponents.jl +++ b/src/base/basecomponents.jl @@ -448,6 +448,7 @@ end LiquidOutPort.ṅ[2] ~ LiquidOutPort.ṅ[1] LiquidOutPort.ṅ[3] ~ 1e-10 + LiquidOutPort.ṅ[1] ~ InPort.ṅ[1]*ControlVolumeState.ϕ[1] # Vapor outlet properties VaporOutPort.h[2] ~ ρT_enthalpy(medium.EoSModel, ControlVolumeState.ρ[2], ControlVolumeState.T, collect(ControlVolumeState.z[:, 2])) @@ -461,6 +462,7 @@ end VaporOutPort.ṅ[3] ~ VaporOutPort.ṅ[1] VaporOutPort.ṅ[2] ~ 1e-10 + VaporOutPort.ṅ[1] ~ InPort.ṅ[1]*(ControlVolumeState.ϕ[2]) ] return ODESystem(eqs, t, collect(Iterators.flatten(vars)), pars; name, systems = [systems...]) diff --git a/test/separation/flash_drum_test.jl b/test/separation/flash_drum_test.jl index 6d9e4ad..142df17 100644 --- a/test/separation/flash_drum_test.jl +++ b/test/separation/flash_drum_test.jl @@ -24,12 +24,13 @@ medium = EoSBased(components = components, eosmodel = model) flowbasis = :molar ) +#This is just a guess as in a flowsheet the real composition will depend on downstream conditions flash_state = pTNVState(2.5*101325.0, 315.87, [0.2379, 0.3082, 0.09959, 0.1373, 0.08872, 0.1283], base = :Pressure) @named FL1 = FixedPressureSteadyStateFlashDrum( medium = medium, state = flash_state, - pressure = 2.5*101325.0, + pressure = flash_state.p, Q = 0.0 ) From 383ea9da65509b472d36ff4ad1c21a0fdeb8c149 Mon Sep 17 00:00:00 2001 From: Vinicius Santana Date: Thu, 13 Nov 2025 16:09:48 +0100 Subject: [PATCH 11/13] Clean up leftover merge conflict directories and files - Remove src/Sources/, src/Valve/, src/reactors/ReactionManager/ - Remove test/Flash_test/, test/Reactor_tests/, test/Sources_tests/ - Ensure clean directory structure matches new architecture - Fix CI compilation issues with leftover conflict files --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index e361b84..b7b64b2 100644 --- a/Project.toml +++ b/Project.toml @@ -31,4 +31,4 @@ SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Test", "SafeTestsets"] +test = ["Test", "SafeTestsets", "Clapeyron"] From 9a84acdb42392d5ef1416c4653368d26cdcc1122 Mon Sep 17 00:00:00 2001 From: Vinicius Santana Date: Thu, 13 Nov 2025 17:11:38 +0100 Subject: [PATCH 12/13] Fix case sensitivity issue: move CSTR.jl to lowercase reactors directory - Move src/Reactors/CSTR.jl to src/reactors/CSTR.jl - Fixes CI compilation error where include("reactors/CSTR.jl") was looking for lowercase directory but file was in uppercase Reactors/ - Ensures consistent lowercase directory naming throughout project --- src/{Reactors => reactors}/CSTR.jl | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{Reactors => reactors}/CSTR.jl (100%) diff --git a/src/Reactors/CSTR.jl b/src/reactors/CSTR.jl similarity index 100% rename from src/Reactors/CSTR.jl rename to src/reactors/CSTR.jl From 6ffd91d813ac1e7332757a015a835d0b2a23c5ba Mon Sep 17 00:00:00 2001 From: Vinicius Santana Date: Wed, 10 Dec 2025 14:42:16 +0100 Subject: [PATCH 13/13] Beginning of the docs and adjusts for the flash example --- Project.toml | 2 +- docs/make.jl | 25 +- docs/src/api/base_components.md | 305 ++++++++++++++++++++++ docs/src/api/reactors.md | 323 +++++++++++++++++++++++ docs/src/api/separation.md | 386 ++++++++++++++++++++++++++++ docs/src/api/thermodynamics.md | 295 +++++++++++++++++++++ docs/src/examples/cstr.md | 277 ++++++++++++++++++++ docs/src/examples/flash_drum.md | 284 ++++++++++++++++++++ docs/src/getting_started.md | 91 +++++++ docs/src/guide/components.md | 124 +++++++++ docs/src/guide/media.md | 114 ++++++++ docs/src/guide/reactors.md | 134 ++++++++++ docs/src/guide/separation.md | 209 +++++++++++++++ docs/src/index.md | 55 +++- ext/ProcessSimulatorClapeyronExt.jl | 7 +- src/base/basecomponents.jl | 86 ++++++- src/reactors/CSTR.jl | 5 +- src/separation/FlashDrum.jl | 6 +- src/utils/FluidsProp.jl | 2 + test/separation/flash_drum_test.jl | 30 ++- 20 files changed, 2729 insertions(+), 31 deletions(-) create mode 100644 docs/src/api/base_components.md create mode 100644 docs/src/api/reactors.md create mode 100644 docs/src/api/separation.md create mode 100644 docs/src/api/thermodynamics.md create mode 100644 docs/src/examples/cstr.md create mode 100644 docs/src/examples/flash_drum.md create mode 100644 docs/src/getting_started.md create mode 100644 docs/src/guide/components.md create mode 100644 docs/src/guide/media.md create mode 100644 docs/src/guide/reactors.md create mode 100644 docs/src/guide/separation.md diff --git a/Project.toml b/Project.toml index b7b64b2..94bc503 100644 --- a/Project.toml +++ b/Project.toml @@ -31,4 +31,4 @@ SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Test", "SafeTestsets", "Clapeyron"] +test = ["Test", "SafeTestsets", "Clapeyron", "OrdinaryDiffEq"] diff --git a/docs/make.jl b/docs/make.jl index b7f13c9..de908be 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -6,18 +6,39 @@ DocMeta.setdocmeta!(ProcessSimulator, :DocTestSetup, :(using ProcessSimulator); makedocs(; modules = [ProcessSimulator], authors = "SciML", + repo = "https://github.com/SciML/ProcessSimulator.jl/blob/{commit}{path}#{line}", sitename = "ProcessSimulator.jl", format = Documenter.HTML(; + prettyurls = get(ENV, "CI", "false") == "true", canonical = "https://docs.sciml.ai/ProcessSimulator/stable/", edit_link = "main", - assets = String[] + assets = String[], + size_threshold = 512000, ), pages = [ "Home" => "index.md", + "Getting Started" => "getting_started.md", + "User Guide" => [ + "Media & Thermodynamics" => "guide/media.md", + "Components" => "guide/components.md", + "Reactors" => "guide/reactors.md", + "Separation Units" => "guide/separation.md", + ], + "Examples" => [ + "Flash Drum" => "examples/flash_drum.md", + "CSTR" => "examples/cstr.md", + ], + "API Reference" => [ + "Thermodynamics" => "api/thermodynamics.md", + "Base Components" => "api/base_components.md", + "Reactors" => "api/reactors.md", + "Separation" => "api/separation.md", + ], ] ) deploydocs(; repo = "github.com/SciML/ProcessSimulator.jl", - devbranch = "main" + devbranch = "main", + push_preview = true ) diff --git a/docs/src/api/base_components.md b/docs/src/api/base_components.md new file mode 100644 index 0000000..cebd03a --- /dev/null +++ b/docs/src/api/base_components.md @@ -0,0 +1,305 @@ +# Base Components API + +Reference documentation for base components, connectors, and control volumes. + +## Connectors + +### StreamConnector + +Two-phase stream connector for material and energy transfer. + +```@docs +StreamConnector +``` + +**Variables:** +- `ṅ[i]`: Component molar flow rates (mol/s) +- `Ḣ`: Enthalpy flow rate (W) + +**Example:** +```julia +@connector StreamConnector begin + ṅ(t)[1:n], [description = "Molar flow rate"] + Ḣ(t), [description = "Enthalpy flow rate"] +end +``` + +**Usage in Components:** +```julia +@named InPort = StreamConnector(n = length(components)) +@named OutPort = StreamConnector(n = length(components)) +``` + +## Boundaries + +### Boundary_pTzn + +Fixed pressure, temperature, composition, and flow rate boundary. + +```@docs +Boundary_pTzn +``` + +**Parameters:** +- `medium::EoSBased`: Thermodynamic medium +- `p::Real`: Pressure (Pa) +- `T::Real`: Temperature (K) +- `z::Vector{Real}`: Mole fractions +- `flowrate::Real`: Flow rate +- `flowbasis::Symbol`: `:molar` or `:mass` + +**Example:** +```julia +@named feed = Boundary_pTzn( + medium = medium, + p = 30e5, + T = 350.0, + z = [0.4, 0.6], + flowrate = 100.0, + flowbasis = :molar +) +``` + +**Connector:** +- `OutPort`: Stream connector providing the specified conditions + +### ConnHouse + +Connection house for collecting stream properties. + +```@docs +ConnHouse +``` + +Collects properties from a stream connector into a named tuple state. + +**Parameters:** +- `medium::EoSBased`: Thermodynamic medium + +**Example:** +```julia +@named product = ConnHouse(medium = medium) + +# After solving, access properties +p = sol[product.p_T_z_n.p] +T = sol[product.p_T_z_n.T] +z = [sol[product.p_T_z_n.z[i]] for i in 1:n] +n = sol[product.p_T_z_n.n] +``` + +**Connector:** +- `InPort`: Stream connector accepting inlet stream + +**Properties Collected:** +- `p`: Pressure +- `T`: Temperature +- `z[i]`: Mole fractions +- `n`: Total molar flow rate + +## Control Volumes + +### ThreePortControlVolume_SteadyState + +Three-port control volume for steady-state separation equipment. + +```@docs +ThreePortControlVolume_SteadyState +``` + +**Ports:** +- `InPort`: Feed inlet +- `VaporOutPort`: Vapor outlet (port 2) +- `LiquidOutPort`: Liquid outlet (port 3) + +**Parameters:** +- `medium::EoSBased`: Thermodynamic medium +- `state`: Initial state specification +- `Q`: Heat duty (W), `nothing` for calculated + +**Variables:** +- `V[i]`: Phase volumes (m³) +- `nᴸⱽ[i]`: Phase holdups (mol) +- `U`: Internal energy (J) +- `Nᵢ[i]`: Component inventories (mol) + +**Equations:** +1. Material balance: `ṅ_in[i] = ṅ_vapor[i] + ṅ_liquid[i]` +2. Energy balance: `Ḣ_in + Q = Ḣ_vapor + Ḣ_liquid` +3. VLE: Phase equilibrium for two-phase systems +4. Volume constraint: `V_total = V_vapor + V_liquid` + +**Example:** +```julia +initial_state = pTNVState(10e5, 300.0, [0.5, 0.5], base = :Pressure) + +@named cv = ThreePortControlVolume_SteadyState( + medium = medium, + state = initial_state +) +``` + +**Used in:** +- `FixedPressureSteadyStateFlashDrum` +- Custom separation equipment + +### TwoPortControlVolume_SteadyState + +Two-port control volume for steady-state equipment with single outlet. + +**Ports:** +- `InPort`: Feed inlet +- `OutPort`: Product outlet + +**Parameters:** +- `medium::EoSBased`: Thermodynamic medium +- `state`: Initial state specification +- `Q`: Heat duty (W), `nothing` for calculated +- `W`: Shaft work (W) + +**Variables:** +- `V`: Volume (m³) +- `n`: Holdup (mol) +- `U`: Internal energy (J) + +**Equations:** +1. Material balance: `ṅ_in[i] = ṅ_out[i]` +2. Energy balance: `Ḣ_in + Q + W = Ḣ_out` + +**Example:** +```julia +@named cv = TwoPortControlVolume_SteadyState( + medium = medium, + state = initial_state, + Q = 0.0, + W = 0.0 +) +``` + +**Used in:** +- `FixedVolumeSteadyStateCSTR` +- Heat exchangers +- Valves + +## State Specifications + +Components use state specifications for initial guesses: + +### pTNVState +```julia +state = pTNVState(p, T, z; base = :Pressure) +``` + +### pHNVState +```julia +state = pHNVState(p, H, z; base = :Pressure) +``` + +### TVNState +```julia +state = TVNState(T, V, z) +``` + +See [Thermodynamics API](thermodynamics.md) for details. + +## Helper Functions + +### connect + +ModelingToolkit connection function: + +```julia +connections = [ + connect(source.OutPort, destination.InPort) +] +``` + +### System + +Create MTK system from connections: + +```julia +@named sys = System( + connections, + t, + [], + []; + systems = [comp1.odesystem, comp2.odesystem, ...] +) +``` + +### mtkcompile + +Compile MTK system for solving: + +```julia +compiled = mtkcompile(sys) +compiled = mtkcompile(sys, use_scc = true) # Use strongly connected components +``` + +## Solving + +### Initial Guesses + +Get default guesses: +```julia +guess_dict = guesses(compiled_system) +``` + +Merge with custom guesses: +```julia +custom = Dict(var => value, ...) +merged = merge(guesses(compiled_system), custom) +``` + +### NonlinearProblem + +Create nonlinear problem: +```julia +prob = NonlinearProblem(compiled_system, guess_dict) +prob = NonlinearProblem(compiled_system, guess_dict, use_scc = true) +``` + +### Solve + +```julia +sol = solve(prob, NewtonRaphson()) +sol = solve(prob, FastShortcutNonlinearPolyalg()) +sol = solve(prob, NewtonRaphson(autodiff=AutoFiniteDiff())) +``` + +### Access Results + +```julia +# Variable value +value = sol[compiled_system.component.variable] + +# Array variable +values = [sol[compiled_system.component.array[i]] for i in 1:n] +``` + +## Output Formatting + +### print_flowsheet_summary + +Print formatted flowsheet results: + +```julia +# Using unit operations dictionary +unit_ops = Dict(:R1 => :CSTR, :F1 => :Feed) +print_flowsheet_summary(sol, compiled_sys, unit_ops, components) + +# Direct component specification +print_flowsheet_summary(sol, compiled_sys, components, comp1, comp2, ...) +``` + +Outputs: +- Component flow rates and compositions +- Temperatures and pressures +- Phase information +- Heat duties + +## See Also + +- [Components Guide](../guide/components.md) - Conceptual overview +- [Media Guide](../guide/media.md) - Thermodynamics integration +- [Flash Drum Example](../examples/flash_drum.md) - Using control volumes diff --git a/docs/src/api/reactors.md b/docs/src/api/reactors.md new file mode 100644 index 0000000..c7a4117 --- /dev/null +++ b/docs/src/api/reactors.md @@ -0,0 +1,323 @@ +# Reactors API + +Reference documentation for reactor components. + +## Continuous Stirred Tank Reactor (CSTR) + +### FixedVolumeSteadyStateCSTR + +Steady-state CSTR with fixed volume. + +```@docs +FixedVolumeSteadyStateCSTR +``` + +**Parameters:** +- `medium::EoSBased`: Thermodynamic medium +- `reactionset`: Single reaction or array of reactions +- `limiting_reactant::String`: Name of limiting reactant component (only relevant for single reactions but still required) +- `state`: Initial state specification (pTNVState, pHNVState, etc.) +- `volume::Real`: Reactor volume (m³) +- `W::Real`: Shaft work input (W), typically 0 for CSTR +- `Q`: Heat duty (W), `nothing` for adiabatic + +**Connectors:** +- `InPort`: Feed stream inlet +- `OutPort`: Product stream outlet + +**Variables:** +- `ControlVolumeState`: Thermodynamic state inside reactor + - `p`: Pressure (Pa) + - `T`: Temperature (K) + - `z[i]`: Mole fractions + - `ϕ[i]`: Vaporized Fraction +- `r[j]`: Reaction rates (mol/s) for each reaction +- `V`: Reactor volume (m³) +- `n`: Total molar holdup (mol) +- `U`: Internal energy (J) +- `Q`: Heat duty (W) + +**Equations:** + +Material balance: +``` +ṅ_in[i] + Σ(νᵢⱼ × r[j]) = ṅ_out[i] +``` + +Energy balance: +``` +Ḣ_in + Q + W + Σ(ΔH_rxn[j] × r[j]) = Ḣ_out +``` + +VLE (if two-phase): +``` +K[i] = y[i] / x[i] = ϕ_liquid[i] / ϕ_vapor[i] +``` + +**Example:** +```julia +# Define reaction +rxn = PowerLawReaction( + components = ["A", "B", "C"], + stoichiometry = Dict("A" => -1.0, "B" => -1.0, "C" => 1.0), + order = Dict("A" => 1.0, "B" => 1.0), + A = 1e5, + Eₐ = 50000.0 +) + +# Create CSTR +initial_state = pTNVState(10e5, 350.0, [0.33, 0.33, 0.34], base = :Pressure) + +@named reactor = FixedVolumeSteadyStateCSTR( + medium = medium, + reactionset = rxn, + limiting_reactant = "A", + state = initial_state, + volume = 1.0, + W = 0.0, + Q = nothing # Adiabatic +) +``` + +**Accessing Results:** +```julia +# Reactor conditions +T = sol[reactor.ControlVolumeState.T] +p = sol[reactor.ControlVolumeState.p] +z = [sol[reactor.ControlVolumeState.z[i]] for i in 1:n] + +# Reaction rates +rates = [sol[reactor.r[j]] for j in 1:n_reactions] + +# Heat duty (if Q was nothing) +Q = sol[reactor.Q] + +# Product flow +n_out = sol[reactor.OutPort.ṅ[1]] # Total outlet flow +``` + +## Reaction Definitions + +### PowerLawReaction + +Power law kinetic model. + +```@docs +PowerLawReaction +``` + +**Parameters:** +- `components::Vector{String}`: Component names +- `stoichiometry::Dict{String, Real}`: Stoichiometric coefficients (negative for reactants) +- `order::Dict{String, Real}`: Reaction orders for each component +- `A::Real`: Pre-exponential factor (units depend on overall order) +- `Eₐ::Real`: Activation energy (J/mol) + +**Rate Expression:** +``` +r = A × exp(-Eₐ/RT) × Π(Cᵢ^orderᵢ) +``` + +**Example:** +```julia +# A + 2B → C +rxn = PowerLawReaction( + components = ["A", "B", "C"], + stoichiometry = Dict( + "A" => -1.0, + "B" => -2.0, + "C" => 1.0 + ), + order = Dict( + "A" => 1.0, + "B" => 2.0, + "C" => 0.0 + ), + A = 1e6, # mol^(-2) m^6 s^-1 + Eₐ = 75000.0 # J/mol +) +``` + +### ArrheniusReaction + +Arrhenius kinetic model (alias for PowerLawReaction). + +**Example:** +```julia +rxn = ArrheniusReaction( + components = ["A", "B"], + stoichiometry = Dict("A" => -1.0, "B" => 1.0), + order = Dict("A" => 1.0), + A = 5e4, + Eₐ = 60000.0 +) +``` + +### Custom Reactions + +Extend the reaction framework: + +```julia +struct CustomReaction + components::Vector{String} + stoichiometry::Dict{String, Float64} + # Custom parameters +end + +function rate(rxn::CustomReaction, T, p, C) + # Implement custom rate law + # Return reaction rate (mol/s) +end +``` + +## Reaction Systems + +### ReactionSystem + +Container for multiple reactions. + +**Creating:** +```julia +# Multiple reactions +rxns = [rxn1, rxn2, rxn3] + +# Use in CSTR +@named reactor = FixedVolumeSteadyStateCSTR( + medium = medium, + reactionset = rxns, # Array of reactions + limiting_reactant = "A", + ... +) +``` + +**Accessing Individual Rates:** +```julia +r1 = sol[reactor.r[1]] # Rate of first reaction +r2 = sol[reactor.r[2]] # Rate of second reaction +``` + +## Conversion Calculations + +Calculate conversion from CSTR results: + +```julia +# Single reactant +function conversion(sol, reactor, feed, component_index) + n_in = sol[feed.odesystem.OutPort.ṅ[component_index]] + n_out = sol[reactor.OutPort.ṅ[component_index]] + return (n_in - n_out) / n_in * 100 # Percentage +end + +# Using compositions +function conversion_from_composition(z_feed, z_product, n_feed, n_product, i) + moles_in = z_feed[i] * n_feed + moles_out = z_product[i] * n_product + return (moles_in - moles_out) / moles_in * 100 +end +``` + +## Selectivity and Yield + +For multiple products: + +```julia +# Selectivity (moles of desired product / moles of limiting reactant consumed) +function selectivity(sol, reactor, feed, product_idx, reactant_idx) + reactant_consumed = sol[feed.odesystem.OutPort.ṅ[reactant_idx]] - + sol[reactor.OutPort.ṅ[reactant_idx]] + product_formed = sol[reactor.OutPort.ṅ[product_idx]] - + sol[feed.odesystem.OutPort.ṅ[product_idx]] + + return product_formed / reactant_consumed +end + +# Yield (moles of product / moles of reactant fed) +function yield(sol, reactor, feed, product_idx, reactant_idx) + product_formed = sol[reactor.OutPort.ṅ[product_idx]] - + sol[feed.odesystem.OutPort.ṅ[product_idx]] + reactant_fed = sol[feed.odesystem.OutPort.ṅ[reactant_idx]] + + return product_formed / reactant_fed * 100 # Percentage +end +``` + +## Heat Duty Calculation + +For non-adiabatic operation: + +```julia +# If Q is specified +@named reactor = FixedVolumeSteadyStateCSTR(..., Q = -50000.0) # 50 kW cooling + +# If Q is calculated (Q = nothing) +@named reactor = FixedVolumeSteadyStateCSTR(..., Q = nothing) +Q_required = sol[reactor.Q] # Heat duty needed for specified T +``` + +## Residence Time + +Calculate mean residence time: + +```julia +function residence_time(sol, reactor, component_index = 1) + V = sol[reactor.V] # Reactor volume + n = sol[reactor.n] # Total holdup + ṅ = sol[reactor.OutPort.ṅ[component_index]] + + # Molar residence time + τ_molar = n / ṅ # seconds + + # Volumetric residence time (approximate) + F = ṅ / sum([sol[reactor.OutPort.ṅ[i]] for i in 1:length(components)]) + τ_vol = V / F # m³/(mol/s) - needs conversion to time + + return τ_molar +end +``` + +## Advanced Topics + +### Temperature Control + +Implement temperature control via heat duty: + +```julia +# Target temperature +T_target = 360.0 # K + +# Solve with temperature constraint +# (Requires adding constraint to system) +``` + +### Pressure Drop + +Add pressure drop correlation: + +```julia +# Custom component with pressure drop +@component function CSTRWithPressureDrop(...) + @extend cv = TwoPortControlVolume_SteadyState(...) + + @equations begin + # Pressure drop equation + OutPort.p ~ InPort.p - ΔP + end +end +``` + +### Multiple Phases + +CSTR automatically handles VLE if conditions are appropriate: + +```julia +# Check phase state +ϕ_vapor = [sol[reactor.ControlVolumeState.ϕ[i, 2]] for i in 1:n] +ϕ_liquid = [sol[reactor.ControlVolumeState.ϕ[i, 3]] for i in 1:n] +``` + +## See Also + +- [Reactors Guide](../guide/reactors.md) - Conceptual overview +- [CSTR Example](../examples/cstr.md) - Complete example +- [Base Components API](base_components.md) - Control volume details +- [Thermodynamics API](thermodynamics.md) - Property calculations diff --git a/docs/src/api/separation.md b/docs/src/api/separation.md new file mode 100644 index 0000000..d4da4a0 --- /dev/null +++ b/docs/src/api/separation.md @@ -0,0 +1,386 @@ +# Separation Units API + +Reference documentation for separation equipment. + +## Flash Drums + +### FixedPressureSteadyStateFlashDrum + +Vapor-liquid flash drum operating at fixed pressure. + +```@docs +FixedPressureSteadyStateFlashDrum +``` + +**Parameters:** +- `medium::EoSBased`: Thermodynamic medium +- `state`: Initial state specification (pTNVState, pHNVState, TVNState) +- `pressure::Real`: Operating pressure (Pa) +- `Q`: Heat duty (W), `nothing` for calculated/adiabatic + +**Connectors:** +- `InPort`: Feed stream inlet +- `VaporOutPort`: Vapor product outlet (port 2) +- `LiquidOutPort`: Liquid product outlet (port 3) + +**Internal Components:** +- `ControlVolume`: ThreePortControlVolume_SteadyState +- `ControlVolumeState`: Thermodynamic state inside drum + +**Variables:** +- `p`: Operating pressure (Pa) - fixed +- `T`: Flash temperature (K) - calculated +- `V[2]`: Vapor volume (m³) +- `V[3]`: Liquid volume (m³) +- `nᴸⱽ[2]`: Vapor holdup (mol) +- `nᴸⱽ[3]`: Liquid holdup (mol) +- `Q`: Heat duty (W) +- `z[i]`: Overall composition +- `ϕ[i,j]`: Fugacity coefficients (j=2:vapor, j=3:liquid) + +**Equations:** + +Material balance: +``` +ṅ_in[i] = ṅ_vapor[i] + ṅ_liquid[i] +``` + +Energy balance: +``` +Ḣ_in + Q = Ḣ_vapor + Ḣ_liquid +``` + +VLE: +``` +K[i] = y[i] / x[i] = ϕ_liquid[i] / ϕ_vapor[i] +``` + +Pressure constraint: +``` +p = pressure (fixed) +``` + +**Example:** +```julia +# Initial guess +flash_state = pTNVState( + 10e5, # Pressure (Pa) + 300.0, # Temperature guess (K) + [0.3, 0.4, 0.3], # Composition + base = :Pressure +) + +# Flash drum +@named flash = FixedPressureSteadyStateFlashDrum( + medium = medium, + state = flash_state, + pressure = 10e5, # 10 bar + Q = nothing # Adiabatic +) + +# Connect +@named vapor = ConnHouse(medium = medium) +@named liquid = ConnHouse(medium = medium) + +connections = [ + connect(feed.odesystem.OutPort, flash.odesystem.InPort), + connect(flash.odesystem.VaporOutPort, vapor.InPort), + connect(flash.odesystem.LiquidOutPort, liquid.InPort) +] +``` + +**Accessing Results:** +```julia +# Flash conditions +T_flash = sol[flash.ControlVolumeState.T] +p_flash = sol[flash.ControlVolumeState.p] + +# Vapor product +T_vapor = sol[vapor.p_T_z_n.T] +y = [sol[vapor.p_T_z_n.z[i]] for i in 1:n] +n_vapor = sol[vapor.p_T_z_n.n] + +# Liquid product +T_liquid = sol[liquid.p_T_z_n.T] +x = [sol[liquid.p_T_z_n.z[i]] for i in 1:n] +n_liquid = sol[liquid.p_T_z_n.n] + +# Heat duty (if Q was nothing) +Q = sol[flash.Q] + +# Vapor fraction +β = n_vapor / (n_vapor + n_liquid) +``` + +## Flash Calculations + +### Vapor Fraction + +```julia +function vapor_fraction(sol, flash_drum, vapor_port, liquid_port) + n_V = sol[vapor_port.p_T_z_n.n] + n_L = sol[liquid_port.p_T_z_n.n] + return n_V / (n_V + n_L) +end +``` + +### K-Values + +Calculate equilibrium K-values: + +```julia +function k_values(sol, flash_drum, vapor_port, liquid_port, n_components) + y = [sol[vapor_port.p_T_z_n.z[i]] for i in 1:n_components] + x = [sol[liquid_port.p_T_z_n.z[i]] for i in 1:n_components] + return y ./ x +end +``` + +### Flash Quality + +For phase quality (0 = saturated liquid, 1 = saturated vapor): + +```julia +function flash_quality(sol, vapor_port, liquid_port) + n_V = sol[vapor_port.p_T_z_n.n] + n_L = sol[liquid_port.p_T_z_n.n] + + # Mass-based quality (requires MW) + # q = m_V / (m_V + m_L) + + # Molar-based quality + q = n_V / (n_V + n_L) + + return q +end +``` + +## Initial State Specifications + +Different flash specifications: + +### Isothermal Flash +```julia +# Known temperature, calculate Q +state = pTNVState(p_flash, T_known, z_feed, base = :Pressure) +@named flash = FixedPressureSteadyStateFlashDrum( + medium = medium, + state = state, + pressure = p_flash, + Q = nothing # Will be calculated +) +``` + +### Adiabatic Flash +```julia +# Unknown temperature, Q = 0 +state = pTNVState(p_flash, T_guess, z_feed, base = :Pressure) +@named flash = FixedPressureSteadyStateFlashDrum( + medium = medium, + state = state, + pressure = p_flash, + Q = 0.0 # Adiabatic +) +``` + +### Flash with Heat Input +```julia +# Known Q, calculate T +state = pTNVState(p_flash, T_guess, z_feed, base = :Pressure) +@named flash = FixedPressureSteadyStateFlashDrum( + medium = medium, + state = state, + pressure = p_flash, + Q = -100000.0 # 100 kW cooling +) +``` + +## Customization + +### Initial Guesses + +Improve convergence with better guesses: + +```julia +default_guesses = guesses(compiled_system) + +custom_guesses = Dict( + flash.odesystem.V[2] => 1.0 / flash.medium.Guesses.ρ[2], # Vapor volume + flash.odesystem.V[3] => 1.0 / flash.medium.Guesses.ρ[3], # Liquid volume + flash.odesystem.nᴸⱽ[2] => 1.0, # Vapor holdup + flash.odesystem.nᴸⱽ[3] => 1.0 # Liquid holdup +) + +merged = merge(default_guesses, custom_guesses) +prob = NonlinearProblem(compiled_system, merged) +``` + +### Solver Options + +Different solvers for difficult cases: + +```julia +# Newton-Raphson with finite differences +sol = solve(prob, NewtonRaphson(autodiff=AutoFiniteDiff())) + +# Trust region method +sol = solve(prob, TrustRegion()) + +# Automatic solver selection +sol = solve(prob, FastShortcutNonlinearPolyalg()) + +# With tolerances +sol = solve(prob, NewtonRaphson(), abstol=1e-8, reltol=1e-8) +``` + +## Validation + +### Energy Balance Check + +Verify energy conservation: + +```julia +function check_energy_balance(sol, flash, feed, vapor, liquid) + # Inlet enthalpy flow + H_in = sol[feed.odesystem.OutPort.Ḣ] + + # Outlet enthalpy flows + H_vapor = sol[flash.odesystem.VaporOutPort.Ḣ] + H_liquid = sol[flash.odesystem.LiquidOutPort.Ḣ] + + # Heat duty + Q = sol[flash.Q] + + # Check balance + error = abs(H_in + Q - H_vapor - H_liquid) + relative_error = error / abs(H_in) * 100 + + println("Energy Balance Error: ", relative_error, "%") + return relative_error < 0.1 # Less than 0.1% +end +``` + +### Material Balance Check + +Verify component conservation: + +```julia +function check_material_balance(sol, flash, feed, vapor, liquid, n_components) + for i in 1:n_components + n_in = sol[feed.odesystem.OutPort.ṅ[i]] + n_vapor = sol[flash.odesystem.VaporOutPort.ṅ[i]] + n_liquid = sol[flash.odesystem.LiquidOutPort.ṅ[i]] + + error = abs(n_in - n_vapor - n_liquid) / n_in * 100 + println("Component ", i, " balance error: ", error, "%") + end +end +``` + +## Advanced Topics + +### Multi-Stage Flash + +Cascade flash drums: + +```julia +@named flash1 = FixedPressureSteadyStateFlashDrum( + medium = medium, + state = state1, + pressure = 20e5, # High pressure + Q = 0.0 +) + +@named flash2 = FixedPressureSteadyStateFlashDrum( + medium = medium, + state = state2, + pressure = 10e5, # Lower pressure + Q = 0.0 +) + +@named flash3 = FixedPressureSteadyStateFlashDrum( + medium = medium, + state = state3, + pressure = 5e5, # Lowest pressure + Q = 0.0 +) + +connections = [ + # Feed to first flash + connect(feed.odesystem.OutPort, flash1.odesystem.InPort), + + # Liquid from flash1 to flash2 + connect(flash1.odesystem.LiquidOutPort, flash2.odesystem.InPort), + + # Liquid from flash2 to flash3 + connect(flash2.odesystem.LiquidOutPort, flash3.odesystem.InPort), + + # Collect products + connect(flash1.odesystem.VaporOutPort, vapor1.InPort), + connect(flash2.odesystem.VaporOutPort, vapor2.InPort), + connect(flash3.odesystem.VaporOutPort, vapor3.InPort), + connect(flash3.odesystem.LiquidOutPort, liquid_final.InPort) +] +``` + +### Flash Train Analysis + +Calculate overall recovery: + +```julia +function overall_recovery(sol, feed, product_ports, component_idx) + n_feed = sol[feed.odesystem.OutPort.ṅ[component_idx]] + + total_product = sum([ + sol[port.p_T_z_n.n] * sol[port.p_T_z_n.z[component_idx]] + for port in product_ports + ]) + + return total_product / n_feed * 100 +end +``` + +## Troubleshooting + +### Common Issues + +**Convergence Failure:** +- Adjust initial temperature guess +- Check if feed is single-phase at flash pressure +- Try different solver +- Verify EoS parameters exist for all components + +**Negative Mole Numbers:** +- Improve initial guesses +- Check thermodynamic consistency +- Verify feed composition sums to 1.0 + +**DimensionMismatch:** +- Ensure composition arrays match number of components +- Check connector dimensions + +### Debugging Tips + +```julia +# Check initial flash +using Clapeyron +(x, y, β), state = pt_flash(medium.eosmodel, p_flash, T_guess, z_feed) +println("Initial flash state: ", state) +println("Vapor fraction: ", β) +println("Vapor comp: ", y) +println("Liquid comp: ", x) + +# Check if feed is flashable +T_bubble, _ = bubble_temperature(medium.eosmodel, p_flash, z_feed) +T_dew, _ = dew_temperature(medium.eosmodel, p_flash, z_feed) +println("Bubble T: ", T_bubble, " K") +println("Dew T: ", T_dew, " K") +println("Flash T must be between these values for two-phase") +``` + +## See Also + +- [Separation Guide](../guide/separation.md) - Conceptual overview +- [Flash Drum Example](../examples/flash_drum.md) - Complete example +- [Base Components API](base_components.md) - Control volume details +- [Thermodynamics API](thermodynamics.md) - VLE calculations diff --git a/docs/src/api/thermodynamics.md b/docs/src/api/thermodynamics.md new file mode 100644 index 0000000..d648334 --- /dev/null +++ b/docs/src/api/thermodynamics.md @@ -0,0 +1,295 @@ +# Thermodynamics API + +Reference documentation for thermodynamic functions and state specifications. + +## Medium Definition + +```@docs +EoSBased +``` + +## State Specifications + +### Pressure-Temperature State + +```@docs +pTNVState +``` + +Specify state by pressure, temperature, composition, and optionally volume. + +**Parameters:** +- `p::Real`: Pressure (Pa) +- `T::Real`: Temperature (K) +- `z::Vector{Real}`: Mole fractions +- `base::Symbol`: Flash type (`:Pressure` or `:Temperature`) + +**Example:** +```julia +state = pTNVState(10e5, 300.0, [0.5, 0.5], base = :Pressure) +``` + +### Pressure-Enthalpy State + +```@docs +pHNVState +``` + +Specify state by pressure, enthalpy, and composition. + +**Parameters:** +- `p::Real`: Pressure (Pa) +- `H::Real`: Molar enthalpy (J/mol) +- `z::Vector{Real}`: Mole fractions +- `base::Symbol`: Flash type + +**Example:** +```julia +state = pHNVState(10e5, -50000.0, [0.5, 0.5], base = :Pressure) +``` + +### Temperature-Volume State + +```@docs +TVNState +``` + +Specify state by temperature, volume, and composition. + +**Parameters:** +- `T::Real`: Temperature (K) +- `V::Real`: Molar volume (m³/mol) +- `z::Vector{Real}`: Mole fractions + +**Example:** +```julia +state = TVNState(300.0, 0.001, [0.5, 0.5]) +``` + +## Thermodynamic Property Functions + +### VLE Calculations + +Calculate vapor-liquid equilibrium: + +```julia +# Bubble point +T_bubble, (x, y, β) = bubble_temperature(eos_model, p, z) +p_bubble, (x, y, β) = bubble_pressure(eos_model, T, z) + +# Dew point +T_dew, (x, y, β) = dew_temperature(eos_model, p, z) +p_dew, (x, y, β) = dew_pressure(eos_model, T, z) + +# PT flash +(x, y, β), state = pt_flash(eos_model, p, T, z) +``` + +**Returns:** +- `x`: Liquid composition +- `y`: Vapor composition +- `β`: Vapor fraction +- `state`: Flash state (`:liquid`, `:vapor`, `:two-phase`) + +### Enthalpy Calculations + +```julia +# Total enthalpy +H = enthalpy(eos_model, p, T, z) + +# Phase-specific enthalpy +H_liquid = enthalpy(eos_model, V_liquid, T, z, phase = :liquid) +H_vapor = enthalpy(eos_model, V_vapor, T, z, phase = :vapor) +``` + +### Entropy Calculations + +```julia +# Total entropy +S = entropy(eos_model, p, T, z) + +# Phase-specific entropy +S_liquid = entropy(eos_model, V_liquid, T, z, phase = :liquid) +``` + +### Volume Calculations + +```julia +# Molar volume +V = volume(eos_model, p, T, z) + +# Phase-specific volume +V_liquid = volume(eos_model, p, T, z, phase = :liquid) +V_vapor = volume(eos_model, p, T, z, phase = :vapor) +``` + +### Density Calculations + +```julia +# Mass density +ρ_mass = mass_density(eos_model, p, T, z) + +# Molar density +ρ_molar = molar_density(eos_model, p, T, z) +``` + +## Equation of State Models + +### Cubic EoS + +Supported cubic equations of state from Clapeyron.jl: + +**Peng-Robinson:** +```julia +eos = PR(components) +eos = PR(components, alpha = PRAlpha, mixing = vdW1fRule) +``` + +**Soave-Redlich-Kwong:** +```julia +eos = SRK(components) +eos = SRK(components, idealmodel = ReidIdeal) +``` + +**Redlich-Kwong:** +```julia +eos = RK(components) +``` + +### SAFT Models + +Statistical Associating Fluid Theory models: + +**PC-SAFT:** +```julia +eos = PCSAFT(components) +``` + +**SAFT-γ Mie:** +```julia +eos = SAFTgammaMie(components) +``` + +### Composite Models + +Combine different models for different phases: + +```julia +idealmodel = CompositeModel(components, + liquid = RackettLiquid, + gas = ReidIdeal(components, reference_state = :formation), + saturation = DIPPR101Sat, + hvap = DIPPR106HVap +) + +activity = NRTL(components) +model = CompositeModel(components, fluid = idealmodel, liquid = activity) +``` + +### Ideal Models + +**Ideal Gas:** +```julia +eos = IdealModel(components) +eos = ReidIdeal(components, reference_state = :formation) +``` + +## Transport Properties + +### Viscosity + +```julia +# Dynamic viscosity (Pa·s) +μ = viscosity(eos_model, p, T, z) + +# Phase-specific viscosity +μ_liquid = viscosity(eos_model, p, T, z, phase = :liquid) +μ_vapor = viscosity(eos_model, p, T, z, phase = :vapor) +``` + +### Thermal Conductivity + +```julia +# Thermal conductivity (W/m/K) +k = thermal_conductivity(eos_model, p, T, z) + +# Phase-specific thermal conductivity +k_liquid = thermal_conductivity(eos_model, p, T, z, phase = :liquid) +``` + +### Surface Tension + +```julia +# Surface tension (N/m) +σ = surface_tension(eos_model, T, z) +``` + +## Initial Guess Management + +### Resolve Guesses + +Automatically update thermodynamic guesses: + +```julia +resolve_guess!(medium, state) +``` + +This function: +1. Performs flash calculation based on state specification +2. Updates `medium.Guesses` with VLE results +3. Stores phase compositions, densities, and enthalpies + +**Called automatically by**: Boundary conditions and components during initialization + +### Manual Guess Creation + +Create custom guesses: + +```julia +guesses = EosBasedGuesses( + eos_model, + p, + T, + z, + Val(:Pressure) # or Val(:Temperature) +) +``` + +## Utility Functions + +### Component Properties + +```julia +# Molecular weight +MW = molar_mass(eos_model, z) + +# Critical properties +Tc = critical_temperature(eos_model) +pc = critical_pressure(eos_model) +Vc = critical_volume(eos_model) + +# Acentric factor +ω = acentric_factor(eos_model) +``` + +### Unit Conversions + +```julia +# Temperature +T_C = celsius(T_K) +T_K = kelvin(T_C) + +# Pressure +p_bar = bar(p_Pa) +p_Pa = pascal(p_bar) + +# Flow basis +n_molar = molar_flow(m_mass, MW) +m_mass = mass_flow(n_molar, MW) +``` + +## See Also + +- [Media Guide](../guide/media.md) - Conceptual overview of thermodynamics +- [Components Guide](../guide/components.md) - Using thermodynamics in components +- [Flash Drum Example](../examples/flash_drum.md) - VLE flash example diff --git a/docs/src/examples/cstr.md b/docs/src/examples/cstr.md new file mode 100644 index 0000000..5bf0a9f --- /dev/null +++ b/docs/src/examples/cstr.md @@ -0,0 +1,277 @@ +# CSTR Example + +This example demonstrates a continuous stirred tank reactor (CSTR) with chemical reaction. + +## Problem Statement + +Produce ethylene glycol by reacting ethylene oxide with water in an adiabatic CSTR: + +**Reaction:** +``` +C₂H₄O + H₂O → C₂H₆O₂ +(ethylene oxide + water → ethylene glycol) +``` + +**Feed Conditions:** +- Flow rate: 100 mol/s +- Pressure: 5 atm (505 kPa) +- Temperature: 350.15 K +- Composition: 40% ethylene oxide, 60% water + +**Reactor:** +- Volume: 1.0 m³ +- Operation: Adiabatic (Q = 0) +- Limiting reactant: Ethylene oxide + +## Setup + +Import required packages: + +```julia +using ProcessSimulator +using Clapeyron +using ModelingToolkit +using NonlinearSolve +using ModelingToolkit: t_nounits as t +``` + +## Define Thermodynamics + +This system requires a composite model for accurate liquid-phase activity: + +```julia +components = ["ethylene oxide", "water", "ethylene glycol"] + +# Composite ideal model for reference states +idealmodel = CompositeModel(components, + liquid = RackettLiquid, + gas = ReidIdeal(components, reference_state = :formation), + saturation = DIPPR101Sat, + hvap = DIPPR106HVap +) + +# NRTL activity coefficient model for liquid phase +activity = NRTL(components) + +# Complete composite model +model = CompositeModel(components, fluid = idealmodel, liquid = activity) + +# Create medium +medium = EoSBased(components = components, eosmodel = model) +``` + +**Why Composite Model?** +- **Ideal Gas**: Reid ideal gas for vapor phase +- **Liquid Activity**: NRTL model for non-ideal liquid interactions +- **Properties**: DIPPR correlations for saturation and heat of vaporization + +## Verify Thermodynamics + +Test the model before simulation: + +```julia +# Check bubble pressure calculation +p_bubble = bubble_pressure(model, 350.15, [0.4, 0.6, 0.0]) +println("Bubble pressure at 350.15 K: ", p_bubble[1]/1e5, " bar") +``` + +## Define Feed Stream + +Create the feed boundary: + +```julia +@named S1 = Boundary_pTzn( + medium = medium, + p = 5 * 101325.0, # 5 atm + T = 350.15, # 350.15 K + z = [0.4, 0.6, 0.0], # No glycol in feed + flowrate = 100.0, + flowbasis = :molar +) +``` + +## Define Reaction + +Create the power law reaction: + +```julia +rxn1 = PowerLawReaction( + components = components, + stoichiometry = Dict( + "ethylene oxide" => -1.0, + "water" => -1.0, + "ethylene glycol" => 1.0 + ), + order = Dict( + "ethylene oxide" => 1.0, + "water" => 0.0 + ), + A = 0.5, # Pre-exponential factor + Eₐ = 0.0 # Activation energy (J/mol) +) +``` + +**Reaction Kinetics:** +- Rate = A × [ethylene oxide]¹ × [water]⁰ +- First order in ethylene oxide, zero order in water +- No temperature dependence (Eₐ = 0) + +## Create Reactor + +Define initial state and reactor: + +```julia +# Initial state guess +reactor_state = pTNVState( + 5 * 101325.0, # Pressure (Pa) + 350.15, # Temperature (K) + ones(3) / 3.0, # Equal composition guess + base = :Pressure +) + +# CSTR +@named R1 = FixedVolumeSteadyStateCSTR( + medium = medium, + reactionset = rxn1, + limiting_reactant = "ethylene oxide", + state = reactor_state, + volume = 1.0, # 1 m³ + W = 0.0, # No shaft work + Q = nothing # Adiabatic +) +``` + +**Note**: `limiting_reactant` helps the solver by identifying which reactant determines conversion. + +## Define Product Stream + +```julia +@named sink = ConnHouse(medium = medium) +``` + +## Connect Components + +```julia +connection_set = [ + connect(S1.odesystem.OutPort, R1.odesystem.InPort), + connect(R1.odesystem.OutPort, sink.InPort) +] +``` + +Flowsheet diagram: +``` +Feed (S1) → CSTR (R1) → Product (sink) +``` + +## Build System + +```julia +@named sys = System(connection_set, t, [], []; + systems = [S1.odesystem, R1.odesystem, sink]) + +AdiabaticVolumeReactor = mtkcompile(sys) +``` + +## Set Initial Guesses + +Customize guesses for better convergence: + +```julia +default_guesses = guesses(AdiabaticVolumeReactor) +guesses_Reactor = Dict( + R1.odesystem.V[3] => 1e-8, # Small vapor volume + R1.odesystem.OutPort.ṅ[1] => 100.0 # Outlet flow rate +) +merged_guesses = merge(default_guesses, guesses_Reactor) +``` + +## Solve + +```julia +prob = NonlinearProblem(AdiabaticVolumeReactor, merged_guesses) +@time sol = solve(prob, abstol = 1e-8, reltol = 1e-8) +``` + +## Extract Results + +### Reactor Conditions + +```julia +T_reactor = sol[AdiabaticVolumeReactor.R1.ControlVolumeState.T] +p_reactor = sol[AdiabaticVolumeReactor.R1.ControlVolumeState.p] +Q_reactor = sol[AdiabaticVolumeReactor.R1.Q] + +println("Reactor Temperature: ", T_reactor, " K") +println("Reactor Pressure: ", p_reactor/1e5, " bar") +println("Heat Duty: ", Q_reactor) +``` + +### Conversion + +```julia +# Feed and product moles of ethylene oxide + +conversion = sol[AdiabaticVolumeReactor.R1.X]*100.0 + +println("\nEthylene Oxide Conversion: ", round(conversion, digits = 3), "%") +``` + +### Reaction Rate + +```julia +r_reaction = sol[AdiabaticVolumeReactor.R1.r[1]] # Reaction rate (mol/s) +println("Reaction Rate: ", r_reaction, " mol/s") +``` + +## Print Summary + +Use the built-in summary function: + +```julia +# Method 1: Using unit_ops dictionary +unit_ops = Dict(:R1 => :CSTR, :S1 => :Feed) +print_flowsheet_summary(sol, AdiabaticVolumeReactor, unit_ops, components) + +# Method 2: Direct component specification +print_flowsheet_summary(sol, AdiabaticVolumeReactor, components, R1, S1) +``` + +This provides a formatted table with: +- Stream compositions +- Flow rates +- Temperatures and pressures +- Phase information + +### CSTR Series + +Chain multiple reactors: + +```julia +@named R1 = FixedVolumeSteadyStateCSTR(...) +@named R2 = FixedVolumeSteadyStateCSTR(...) + +connections = [ + connect(feed.odesystem.OutPort, R1.odesystem.InPort), + connect(R1.odesystem.OutPort, R2.odesystem.InPort), + connect(R2.odesystem.OutPort, product.InPort) +] +``` + +## Troubleshooting + +### Convergence Issues + +1. **Adjust Initial Guesses**: Try different temperature/composition guesses +2. **Check Reaction Rate**: Ensure kinetics are reasonable (not too fast/slow) +3. **Verify Thermodynamics**: Test `bubble_pressure` and `flash` separately +4. **Use Different Solver**: Try `FastShortcutNonlinearPolyalg()` + +### Physical Constraints + +- **Positive Compositions**: Ensure all z[i] ≥ 0 +- **Temperature Limits**: Check for unreasonable temperature rise +- **Phase Stability**: Verify reactor operates in single phase if assumed + +## Complete Code + +The full code is available in `test/reactors/ss_cstr_test.jl`. diff --git a/docs/src/examples/flash_drum.md b/docs/src/examples/flash_drum.md new file mode 100644 index 0000000..2ea7c31 --- /dev/null +++ b/docs/src/examples/flash_drum.md @@ -0,0 +1,284 @@ +# Flash Drum Example + +This example demonstrates a complete vapor-liquid flash separation based on the EMSO manual example (Section 3.2.4). + +## Problem Statement + +Separate a hydrocarbon mixture in a flash drum: + +- **Feed**: 496.3 kmol/h at 338 K and 507.1 kPa +- **Flash Conditions**: 2.5 atm (253 kPa), adiabatic +- **Components**: 1,3-butadiene, isobutene, n-pentane, 1-pentene, 1-hexene, benzene + +The goal is to calculate the vapor and liquid product compositions, flow rates, and flash temperature. + +## Setup + +Import required packages: + +```julia +using ProcessSimulator +using Clapeyron +using ModelingToolkit +using NonlinearSolve +using ModelingToolkit: t_nounits as t +``` + +## Define Thermodynamics + +Create the equation of state model: + +```julia +components = [ + "1,3-butadiene", + "isobutene", + "n-pentane", + "1-pentene", + "1-hexene", + "benzene" +] + +# Soave-Redlich-Kwong EoS with Reid ideal gas +model = SRK(components, idealmodel = ReidIdeal) +medium = EoSBased(components = components, eosmodel = model) +``` + +**Why SRK?** The Soave-Redlich-Kwong equation of state provides good accuracy for light hydrocarbons at moderate pressures. + +## Define Feed Stream + +Create the feed boundary condition: + +```julia +@named S1 = Boundary_pTzn( + medium = medium, + p = 507.1e3, # 507.1 kPa + T = 338.0, # 338 K + z = [0.2379, 0.3082, 0.09959, 0.1373, 0.08872, 0.1283], + flowrate = 496.3/3600, # Convert kmol/h to kmol/s + flowbasis = :molar +) +``` + +The feed composition breakdown: +- 1,3-butadiene: 23.79% +- Isobutene: 30.82% +- n-pentane: 9.96% +- 1-pentene: 13.73% +- 1-hexene: 8.87% +- Benzene: 12.83% + +## Create Flash Drum + +Define the initial state guess: + +```julia +flash_state = pTNVState( + 2.5e5, # Flash pressure (Pa) + 315.87, # Initial temperature guess (K) + [0.2379, 0.3082, 0.09959, 0.1373, 0.08872, 0.1283], + base = :Pressure +) +``` + +Create the flash drum: + +```julia +@named FL1 = FixedPressureSteadyStateFlashDrum( + medium = medium, + state = flash_state, + pressure = flash_state.p, + Q = nothing # Adiabatic operation +) +``` + +**Note**: The composition guess uses the feed composition. In a larger flowsheet, this would depend on upstream conditions. + +## Define Product Streams + +Create connection houses for products: + +```julia +@named LiquidPort = ConnHouse(medium = medium) +@named VaporPort = ConnHouse(medium = medium) +``` + +## Connect Components + +Build the flowsheet connections: + +```julia +connection_set = [ + connect(S1.odesystem.OutPort, FL1.odesystem.InPort), + connect(FL1.odesystem.LiquidOutPort, LiquidPort.InPort), + connect(FL1.odesystem.VaporOutPort, VaporPort.InPort) +] +``` + +Flowsheet diagram: +``` +Feed (S1) → Flash Drum (FL1) → Vapor (VaporPort) + ↓ + Liquid (LiquidPort) +``` + +## Build System + +Create the complete system: + +```julia +@named sys = System(connection_set, t, [], []; + systems = [S1.odesystem, FL1.odesystem, LiquidPort, VaporPort]) + +SteadyStateFlash = mtkcompile(sys) +``` + +## Set Initial Guesses + +Customize initial guesses for better convergence: + +```julia +default_guesses = guesses(SteadyStateFlash) +guesses_Flash = Dict( + FL1.odesystem.V[2] => 1.0/FL1.medium.Guesses.ρ[2], + FL1.odesystem.V[3] => 1.0/FL1.medium.Guesses.ρ[3], + FL1.odesystem.nᴸⱽ[2] => 1.0, # Vapor holdup guess +) +merged_guesses = merge(default_guesses, guesses_Flash) +``` + +**Tip**: Volume guesses are based on estimated densities from the thermodynamic flash. + +## Solve + +Create and solve the nonlinear problem: + +```julia +prob = NonlinearProblem(SteadyStateFlash, merged_guesses, use_scc = true) +sol = solve(prob, NewtonRaphson(autodiff=AutoFiniteDiff())) +``` + +The `use_scc = true` option enables strongly connected components analysis for better performance on large systems. + +## Extract Results + +### Flash Drum Conditions + +```julia +T_flash = sol[SteadyStateFlash.FL1.ControlVolumeState.T] +p_flash = sol[SteadyStateFlash.FL1.ControlVolumeState.p] +Q_flash = sol[SteadyStateFlash.FL1.Q] + +println("Flash Temperature: ", T_flash, " K") +println("Flash Pressure: ", p_flash/1e5, " bar") +println("Heat Duty: ", Q_flash, " W") +``` + +### Product Compositions + +```julia +# Vapor composition +z_vapor = [sol[SteadyStateFlash.VaporPort.p_T_z_n.z[i]] for i in 1:6] +n_vapor = sol[SteadyStateFlash.VaporPort.p_T_z_n.n] + +# Liquid composition +z_liquid = [sol[SteadyStateFlash.LiquidPort.p_T_z_n.z[i]] for i in 1:6] +n_liquid = sol[SteadyStateFlash.LiquidPort.p_T_z_n.n] + +# Vapor fraction +β = n_vapor / (n_vapor + n_liquid) + +println("\nVapor Fraction: ", β) +println("\nVapor Composition:") +for (i, comp) in enumerate(components) + println(" ", comp, ": ", z_vapor[i]) +end + +println("\nLiquid Composition:") +for (i, comp) in enumerate(components) + println(" ", comp, ": ", z_liquid[i]) +end +``` + +### Flow Rates + +```julia +println("\nFlow Rates:") +println(" Vapor: ", n_vapor * 3600, " kmol/h") +println(" Liquid: ", n_liquid * 3600, " kmol/h") +``` + +## Expected Results + +For this system, you should obtain: + +- **Flash Temperature**: ~315-316 K (adiabatic cooling from feed) +- **Vapor Fraction**: Higher for lighter components (butadiene, isobutene) +- **Liquid Fraction**: Higher for heavier components (hexene, benzene) + +The light components (butadiene, isobutene) will be enriched in the vapor phase, while heavier components (hexene, benzene) will concentrate in the liquid phase. + +## Validation + +Compare with EMSO manual results (Section 3.2.4): + +```julia +# Expected values from EMSO +emso_T_flash = 315.87 # K +emso_vapor_fraction = 0.xx # Replace with actual value + +error_T = abs(T_flash - emso_T_flash) / emso_T_flash * 100 +println("\nTemperature Error: ", error_T, "%") +``` + +## Troubleshooting + +If convergence fails: + +1. **Adjust Temperature Guess**: Try temperatures between feed and flash conditions +2. **Change Solver**: Use `FastShortcutNonlinearPolyalg()` for automatic solver selection +3. **Check EoS Parameters**: Ensure Clapeyron has parameters for all components +4. **Verify Feed Conditions**: Ensure feed is subcooled or two-phase capable at flash pressure + +## Extensions + +### Add Heat Exchanger + +Pre-cool feed before flash: + +```julia +@named cooler = HeatExchanger( + medium = medium, + Q = -50000.0 # 50 kW cooling +) +``` + +### Multi-Stage Flash + +Create a flash train: + +```julia +# First flash at high pressure +@named FL1 = FixedPressureSteadyStateFlashDrum( + medium = medium, + state = state1, + pressure = 5e5 +) + +# Second flash at lower pressure +@named FL2 = FixedPressureSteadyStateFlashDrum( + medium = medium, + state = state2, + pressure = 2.5e5 +) + +# Connect liquid from FL1 to FL2 +connections = [ + connect(feed.odesystem.OutPort, FL1.odesystem.InPort), + connect(FL1.odesystem.LiquidOutPort, FL2.odesystem.InPort) +] +``` + +## Complete Code + +The full code is available in `test/separation/flash_drum_test.jl`. diff --git a/docs/src/getting_started.md b/docs/src/getting_started.md new file mode 100644 index 0000000..a59a936 --- /dev/null +++ b/docs/src/getting_started.md @@ -0,0 +1,91 @@ +# Getting Started + +This guide will help you get started with ProcessSimulator.jl for process modeling and simulation. + +## Installation + +ProcessSimulator.jl requires Julia 1.9 or later. Install it using the Julia package manager: + +```julia +using Pkg +Pkg.add("ProcessSimulator") +``` + +## Basic Concepts + +ProcessSimulator.jl uses a component-based modeling approach built on ModelingToolkit.jl: + +1. **Media**: Define thermodynamic models for fluids +2. **Components**: Build unit operations (reactors, separators) +3. **Connections**: Connect components via streams +4. **System**: Assemble and solve the complete flowsheet + +## Your First Simulation + +Let's create a simple steady-state flash drum: + +```julia +using ProcessSimulator +using Clapeyron +using ModelingToolkit +using NonlinearSolve +using ModelingToolkit: t_nounits as t + +# Step 1: Define the fluid medium +components = ["methane", "ethane"] +eos_model = PR(components) +medium = EoSBased(components = components, eosmodel = eos_model) + +# Step 2: Create a feed stream +@named feed = Boundary_pTzn( + medium = medium, + p = 50e5, # 50 bar + T = 300.0, # 300 K + z = [0.5, 0.5], # Equal molar composition + flowrate = 100.0, # mol/s + flowbasis = :molar +) + +# Step 3: Create a flash drum +flash_state = pTNVState(50e5, 300.0, [0.5, 0.5], base = :Pressure) +@named flash = FixedPressureSteadyStateFlashDrum( + medium = medium, + state = flash_state, + pressure = 30e5, # Flash at 30 bar + Q = nothing # Adiabatic +) + +# Step 4: Create outlet streams +@named liquid_out = ConnHouse(medium = medium) +@named vapor_out = ConnHouse(medium = medium) + +# Step 5: Connect the flowsheet +connections = [ + connect(feed.odesystem.OutPort, flash.odesystem.InPort), + connect(flash.odesystem.LiquidOutPort, liquid_out.InPort), + connect(flash.odesystem.VaporOutPort, vapor_out.InPort) +] + +# Step 6: Build and solve the system +@named sys = System(connections, t, [], []; + systems = [feed.odesystem, flash.odesystem, liquid_out, vapor_out]) + +compiled_sys = mtkcompile(sys) +prob = NonlinearProblem(compiled_sys, guesses(compiled_sys)) +sol = solve(prob, NewtonRaphson()) + +# Step 7: Extract results +T_flash = sol[flash.ControlVolumeState.T] +liquid_flow = sol[flash.LiquidOutPort.ṅ[1]] +vapor_flow = sol[flash.VaporOutPort.ṅ[1]] + +println("Flash Temperature: ", T_flash, " K") +println("Liquid Flow: ", liquid_flow, " mol/s") +println("Vapor Flow: ", vapor_flow, " mol/s") +``` + +## Next Steps + +- Learn about [Media & Thermodynamics](guide/media.md) +- Explore [Component Models](guide/components.md) +- See more [Examples](examples/flash_drum.md) diff --git a/docs/src/guide/components.md b/docs/src/guide/components.md new file mode 100644 index 0000000..077938d --- /dev/null +++ b/docs/src/guide/components.md @@ -0,0 +1,124 @@ +# Components + +ProcessSimulator.jl provides various component models for building process flowsheets. + +## Base Components + +### Connectors + +Connectors link unit operations via material streams: + +#### PhZConnector + +Standard connector for pressure-enthalpy-composition streams: + +```julia +@connector function PhZConnector_(;medium, name) + # Carries: pressure, enthalpy, composition, flow rates +end +``` + +### Boundary Conditions + +#### Fixed Stream Boundary + +Define inlet/outlet streams with fixed conditions: + +```julia +@named feed = Boundary_pTzn( + medium = medium, + p = 10e5, # Pressure (Pa) + T = 350.0, # Temperature (K) + z = [0.3, 0.4, 0.3], # Mole fractions + flowrate = 100.0, # Flow rate + flowbasis = :molar # :molar, :mass, or :volumetric +) +``` + +#### Connection House + +Simple connector for stream endpoints: + +```julia +@named outlet = ConnHouse(medium = medium) +``` + +## Control Volumes + +Control volumes represent the material and energy balances within a unit. + +### Two-Port Control Volume + +For single-inlet, single-outlet units: + +```julia +@named CV = TwoPortControlVolume_(medium = medium) +# Has: InPort, OutPort, ControlVolumeState +``` + +### Three-Port Control Volume + +For flash drums with separate liquid and vapor outlets: + +```julia +@named CV = ThreePortControlVolume_SteadyState(medium = medium) +# Has: InPort, LiquidOutPort, VaporOutPort, ControlVolumeState +``` + +## Connecting Components + +Use ModelingToolkit's `connect` function to link components: + +```julia +connections = [ + connect(feed.odesystem.OutPort, unit.odesystem.InPort), + connect(unit.odesystem.OutPort, product.InPort) +] + +@named sys = System(connections, t, [], []; + systems = [feed.odesystem, unit.odesystem, product]) +``` + +## Building Custom Components + +Create custom components using the `@component` macro: + +```julia +@component function MyUnit(; medium, name, parameter1) + # Define systems (control volumes, connectors) + systems = @named begin + InPort = PhZConnector_(medium = medium) + OutPort = PhZConnector_(medium = medium) + CV = TwoPortControlVolume_(medium = medium) + end + + # Define variables + vars = @variables begin + custom_var(t), [description = "Custom variable"] + end + + # Define parameters + pars = @parameters begin + param1 = parameter1 + end + + # Define equations + eqs = [ + # Your model equations here + OutPort.p ~ InPort.p - 1000.0 # Pressure drop example + ] + + return ODESystem(eqs, t, vars, pars; name, systems = [systems...]) +end +``` + +## State Variables + +All components have access to thermodynamic state through `ControlVolumeState`: + +- `p`: Pressure +- `T`: Temperature +- `z[:, j]`: Mole fractions (component i, phase j) +- `ϕ[j]`: Phase fractions +- `ρ[j]`: Molar densities +- `h[j]`: Molar enthalpies diff --git a/docs/src/guide/media.md b/docs/src/guide/media.md new file mode 100644 index 0000000..b7ffe1d --- /dev/null +++ b/docs/src/guide/media.md @@ -0,0 +1,114 @@ +# Media & Thermodynamics + +ProcessSimulator.jl uses Clapeyron.jl for thermodynamic property calculations. This page explains how to define and use fluid media. + +## Defining a Medium + +A medium represents the thermodynamic model for your fluids: + +```julia +using Clapeyron + +# Define components +components = ["methane", "ethane", "propane", "n-butane"] + +# Choose an equation of state +eos_model = PR(components) # Peng-Robinson + +# Create the medium +medium = EoSBased(components = components, eosmodel = eos_model) +``` + +## Available Equations of State + +ProcessSimulator.jl supports all Clapeyron.jl EoS models: + +- **Cubic EoS**: `PR`, `SRK`, `RK`, `vdW` +- **SAFT**: `PCSAFT`, `sPCSAFT`, `SAFTVRMie` +- **Activity Models**: `UNIFAC`, `NRTL`, `Wilson` + +```julia +# Peng-Robinson with Reid ideal gas +model = PR(components, idealmodel = ReidIdeal) + +# Soave-Redlich-Kwong +model = SRK(components) + +# PC-SAFT +model = PCSAFT(components) +``` + +## Thermodynamic States + +Define the state of a stream or unit: + +### Pressure-Temperature-Composition (pTz) + +```julia +state = pTNVState( + 5e5, # Pressure (Pa) + 350.0, # Temperature (K) + [0.3, 0.4, 0.3], # Mole fractions + base = :Pressure # Calculate from P,T,z +) +``` + +### Volume-Temperature-Composition (VTN) + +```julia +state = pTNVState( + nothing, # Pressure to be calculated + 350.0, # Temperature (K) + [10.0, 15.0, 5.0], # Molar amounts (mol) + base = :Volume # Calculate from V,T,N +) +state.V = 0.1 # Set volume (m³) +``` + +## Property Calculations + +The medium provides automatic property calculations: + +```julia +# After creating a flash calculation +medium, state, phase = resolve_guess!(medium, state) + +# Access properties from medium.Guesses +p = medium.Guesses.p # Pressure +T = medium.Guesses.T # Temperature +ρ = medium.Guesses.ρ # Densities [overall, liquid, vapor] +h = medium.Guesses.h # Enthalpies [overall, liquid, vapor] +x = medium.Guesses.x # Compositions [overall, liquid, vapor] +ϕ = medium.Guesses.ϕ # Phase fractions [liquid, vapor] +``` + +## Multi-Phase Equilibrium + +ProcessSimulator.jl automatically handles VLE calculations through Clapeyron.jl (At the moment): + +```julia +# Flash calculation at given P,T +sol = TP_flash(medium.EoSModel, p, T, z) +ϕ = sol[1] # Phase fractions +x = sol[2] # Phase compositions + +# Get phase properties +x_liquid = flash_mol_fractions_liquid(medium.EoSModel, p, T, z) +x_vapor = flash_mol_fractions_vapor(medium.EoSModel, p, T, z) +vapor_frac = flash_vaporized_fraction(medium.EoSModel, p, T, z) +``` + +## Transport Properties + +Define mass and heat transfer coefficients: + +```julia +medium = EoSBased( + components = components, + eosmodel = eos_model, + transportmodel = TransportModel( + mass_transfer = ConstantMassTransferCoeff([0.5, 0.5, 0.5]), + heat_transfer = ConstantHeatTransferCoeff(10.0) + ) +) +``` diff --git a/docs/src/guide/reactors.md b/docs/src/guide/reactors.md new file mode 100644 index 0000000..6d1e2f5 --- /dev/null +++ b/docs/src/guide/reactors.md @@ -0,0 +1,134 @@ +# Reactors + +ProcessSimulator.jl provides reactor models for chemical process simulation. + +## Continuous Stirred Tank Reactor (CSTR) + +The CSTR model represents a well-mixed reactor with chemical reactions. + +### Steady-State CSTR + +```julia +using ProcessSimulator + +# Define reaction system +@named rxn_system = ReactionSystem( + reactions = [ + Reaction(k_forward, [A], [B]) + ], + components = ["A", "B"], + medium = medium +) + +# Create CSTR +@named reactor = SteadyStateCSTR( + medium = medium, + state = initial_state, + V = 1.0, # Volume (m³) + Q = 0.0, # Heat input (W) + reaction_system = rxn_system +) +``` + +### Features + +- **Material Balance**: Component mole balances with reaction terms +- **Energy Balance**: Enthalpy balance with heat input and reaction heat +- **VLE**: Automatic vapor-liquid equilibrium calculations +- **Flexible Reactions**: Support for multiple reactions with various kinetics + +## Reaction Systems + +Define chemical reactions: + +```julia +@named rxn = ReactionSystem( + reactions = [ + Reaction(1e5, ["A", "B"], ["C"]), # Forward reaction + Reaction(1e3, ["C"], ["A", "B"]) # Reverse reaction + ], + components = ["A", "B", "C"], + medium = medium +) +``` + +### Reaction Kinetics + +- Power law kinetics +- Arrhenius temperature dependence +- Custom rate expressions + +## Dynamic CSTR + +For transient simulations: + +```julia +@named reactor = DynamicCSTR( + medium = medium, + state = initial_state, + V = 1.0, + Q_func = t -> 1000.0 * sin(t), # Time-varying heat input + reaction_system = rxn_system +) + +# Solve with ODE solver +using OrdinaryDiffEq +prob = ODEProblem(compiled_sys, u0, tspan) +sol = solve(prob, Rodas5()) +``` + +## Example: Exothermic Reaction + +```julia +# A → B (exothermic) +components = ["A", "B"] +eos = PR(components) +medium = EoSBased(components = components, eosmodel = eos) + +# Feed stream (pure A) +@named feed = Boundary_pTzn( + medium = medium, + p = 10e5, + T = 350.0, + z = [1.0, 0.0], + flowrate = 10.0, + flowbasis = :molar +) + +# Reaction: A → B with rate k = 1e5 s⁻¹ +@named rxn = ReactionSystem( + reactions = [Reaction(1e5, ["A"], ["B"])], + components = components, + medium = medium +) + +# Adiabatic CSTR +initial_state = pTNVState(10e5, 350.0, [0.5, 0.5], base = :Pressure) +@named cstr = SteadyStateCSTR( + medium = medium, + state = initial_state, + V = 0.1, + Q = 0.0, # Adiabatic + reaction_system = rxn +) + +# Connect and solve +@named product = ConnHouse(medium = medium) +connections = [ + connect(feed.odesystem.OutPort, cstr.odesystem.InPort), + connect(cstr.odesystem.OutPort, product.InPort) +] + +@named sys = System(connections, t, [], []; + systems = [feed.odesystem, cstr.odesystem, product]) + +compiled = mtkcompile(sys) +prob = NonlinearProblem(compiled, guesses(compiled)) +sol = solve(prob, NewtonRaphson()) + +# Results +conversion = (1 - sol[cstr.ControlVolumeState.z[1, 1]]) * 100 +T_reactor = sol[cstr.ControlVolumeState.T] +println("Conversion: ", conversion, "%") +println("Reactor Temperature: ", T_reactor, " K") +``` diff --git a/docs/src/guide/separation.md b/docs/src/guide/separation.md new file mode 100644 index 0000000..a74b7f5 --- /dev/null +++ b/docs/src/guide/separation.md @@ -0,0 +1,209 @@ +# Separation Units + +ProcessSimulator.jl provides models for vapor-liquid separation equipment. + +## Flash Drums + +Flash drums perform vapor-liquid separation at specified conditions. + +### Fixed Pressure Steady-State Flash Drum + +The most common flash drum configuration maintains constant pressure: + +```julia +using ProcessSimulator +using Clapeyron + +# Define components and thermodynamics +components = ["methane", "ethane", "propane"] +eos = PR(components) +medium = EoSBased(components = components, eosmodel = eos) + +# Feed stream +@named feed = Boundary_pTzn( + medium = medium, + p = 30e5, # 30 bar + T = 300.0, # 300 K + z = [0.3, 0.3, 0.4], + flowrate = 100.0, + flowbasis = :molar +) + +# Flash drum at 10 bar +initial_state = pTNVState(10e5, 280.0, [0.3, 0.3, 0.4], base = :Pressure) +@named flash = FixedPressureSteadyStateFlashDrum( + medium = medium, + state = initial_state, + p = 10e5, + Q = 0.0 # Adiabatic +) + +# Product streams +@named vapor = ConnHouse(medium = medium) +@named liquid = ConnHouse(medium = medium) + +# Connect +connections = [ + connect(feed.odesystem.OutPort, flash.odesystem.InPort), + connect(flash.odesystem.VaporOutPort, vapor.InPort), + connect(flash.odesystem.LiquidOutPort, liquid.InPort) +] + +# Build system +@named sys = System(connections, t, [], []; + systems = [feed.odesystem, flash.odesystem, vapor, liquid]) + +# Solve +compiled = mtkcompile(sys) +prob = NonlinearProblem(compiled, guesses(compiled)) +sol = solve(prob, NewtonRaphson()) +``` + +### Flash Drum Features + +- **Three-Port Control Volume**: Feed, vapor product, liquid product +- **VLE Calculations**: Automatic phase equilibrium using Clapeyron.jl +- **Energy Balance**: Adiabatic or with heat input/removal +- **Material Balance**: Component mole balances +- **Flexible Initial Guesses**: Multiple state specification options + +## Accessing Results + +Extract flash drum results from the solution: + +```julia +# Temperatures +T_flash = sol[flash.ControlVolumeState.T] +T_vapor = sol[vapor.p_T_z_n.T] +T_liquid = sol[liquid.p_T_z_n.T] + +# Pressures +p_flash = sol[flash.ControlVolumeState.p] + +# Compositions +z_vapor = [sol[vapor.p_T_z_n.z[i]] for i in 1:3] +z_liquid = [sol[liquid.p_T_z_n.z[i]] for i in 1:3] + +# Flow rates +n_vapor = sol[vapor.p_T_z_n.n] +n_liquid = sol[liquid.p_T_z_n.n] + +# Vapor fraction +vapor_fraction = n_vapor / (n_vapor + n_liquid) + +println("Flash Temperature: ", T_flash, " K") +println("Vapor Fraction: ", vapor_fraction) +println("Vapor Composition: ", z_vapor) +println("Liquid Composition: ", z_liquid) +``` + +## Initial State Specifications + +Flash drums require initial state guesses. Multiple options are available: + +### Pressure-Temperature-Composition-Volume State +```julia +state = pTNVState( + 10e5, # Pressure (Pa) + 280.0, # Temperature (K) + [0.3, 0.3, 0.4], # Composition + base = :Pressure # Use pressure-based flash +) +``` + +### Pressure-Enthalpy State +```julia +state = pHNVState( + 10e5, # Pressure (Pa) + -50000.0, # Enthalpy (J/mol) + [0.3, 0.3, 0.4], # Composition + base = :Pressure +) +``` + +### Temperature-Volume State +```julia +state = TVNState( + 280.0, # Temperature (K) + 0.001, # Molar volume (m³/mol) + [0.3, 0.3, 0.4] # Composition +) +``` + +## Operating Modes + +### Adiabatic Flash +No heat exchange with surroundings: +```julia +@named flash = FixedPressureSteadyStateFlashDrum( + medium = medium, + state = initial_state, + p = 10e5, + Q = 0.0 # Adiabatic +) +``` + +### Flash with Heat Input +Add or remove heat: +```julia +@named flash = FixedPressureSteadyStateFlashDrum( + medium = medium, + state = initial_state, + p = 10e5, + Q = -50000.0 # Remove 50 kW +) +``` + +## Complete Example + +See the [Flash Drum Example](../examples/flash_drum.md) for a detailed walkthrough of: +- Setting up components and thermodynamics +- Defining feed conditions +- Creating the flash drum +- Solving the flowsheet +- Analyzing results +- Validating against reference data + +## Other Separation Units + +### Adsorbers (Experimental) + +Adsorption units for gas purification: +```julia +@named adsorber = Adsorber( + medium = medium, + adsorbent = activated_carbon, + bed_volume = 1.0 +) +``` + +*Note: Adsorption models are under development.* + +## Troubleshooting + +### Convergence Issues + +If the flash drum doesn't converge: + +1. **Check Initial Guesses**: Ensure temperature and pressure are in reasonable range +2. **Verify EoS Model**: Some EoS models work better for certain mixtures +3. **Adjust Solver**: Try different nonlinear solvers: + ```julia + sol = solve(prob, FastShortcutNonlinearPolyalg()) + ``` +4. **Check Phase Stability**: Ensure feed conditions allow VLE + +### Common Errors + +- **DimensionMismatch**: Usually from incorrect composition array size +- **Flash Failed**: Initial guess too far from solution, adjust T or p guess +- **Negative Mole Numbers**: Unphysical solution, check model equations + +## Advanced Topics + +### Custom Flash Specifications + +Create custom flash configurations by extending base components: +- Fixed temperature flash drums +- Multi-stage flash trains +- Flash with chemical reactions diff --git a/docs/src/index.md b/docs/src/index.md index a55ee64..ff29ffd 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -2,13 +2,58 @@ CurrentModule = ProcessSimulator ``` -# ProcessSimulator +# ProcessSimulator.jl -Documentation for [ProcessSimulator](https://github.com/avinashresearch1/ProcessSimulator.jl). +ProcessSimulator.jl is a Julia package for process modeling and simulation using the ModelingToolkit.jl ecosystem. It provides a framework for building equation-oriented models of chemical processes with advanced thermodynamic property calculations. -```@index +## Features + +- **Thermodynamic Models**: Integration with Clapeyron.jl for equation of state (EoS) based property calculations +- **Component-Based Modeling**: Modular components for reactors, separators, and unit operations +- **Symbolic Equations**: Built on ModelingToolkit.jl for symbolic equation manipulation +- **Flexible Connections**: Stream-based connections between unit operations +- **Multiple Phases**: Support for multi-phase systems (liquid-vapor equilibrium) + +## Package Components + +### Core Modules + +- **Media & Thermodynamics**: EoS-based fluid property calculations +- **Base Components**: Control volumes, connectors, and boundary conditions +- **Reactors**: CSTR and other reactor models +- **Separation Units**: Flash drums, distillation columns (in development) + +## Quick Example + +```julia +using ProcessSimulator +using Clapeyron +using ModelingToolkit + +# Define components and EoS model +components = ["methane", "ethane", "propane"] +model = PR(components) +medium = EoSBased(components = components, eosmodel = model) + +# Create a flash drum +state = pTNVState(5e5, 300.0, [0.3, 0.4, 0.3], base = :Pressure) +@named flash = FixedPressureSteadyStateFlashDrum( + medium = medium, + state = state, + pressure = 3e5, + Q = nothing +) ``` -```@autodocs -Modules = [ProcessSimulator] +## Installation + +```julia +using Pkg +Pkg.add("ProcessSimulator") ``` + +## Getting Help + +- **Documentation**: This documentation site +- **Issues**: [GitHub Issues](https://github.com/SciML/ProcessSimulator.jl/issues) +- **Discussions**: [GitHub Discussions](https://github.com/SciML/ProcessSimulator.jl/discussions) diff --git a/ext/ProcessSimulatorClapeyronExt.jl b/ext/ProcessSimulatorClapeyronExt.jl index 3e51576..f196bb9 100644 --- a/ext/ProcessSimulatorClapeyronExt.jl +++ b/ext/ProcessSimulatorClapeyronExt.jl @@ -80,15 +80,14 @@ function PS.TP_flash(EoSModel::M, p, T, x; nonvolatiles = nothing, noncondensabl if PS.is_stable(EoSModel, p, T, _x) - v = Clapeyron.volume(EoSModel, p, T, _x, phase = :unknown) + #v = Clapeyron.volume(EoSModel, p, T, _x, phase = :unknown) #vv_ideal = Clapeyron.volume(Clapeyron.idealmodel(EoSModel), p, T, _x) - if p*v/(8.314*T) ≥ 0.5 #Very rought test for compressibility factor + if Clapeyron.identify_phase(EoSModel, p, T, _x) == :vapour xᵢⱼ = [_x _x] ϕ = [0.0, 1.0] else - xᵢⱼ = [_x _x] ϕ = [1.0, 0.0] end @@ -209,6 +208,8 @@ Symbolics.@register_array_symbolic PS.flash_vaporized_fraction(model::Clapeyron. eltype = eltype(arr) end +@register_symbolic PS.is_stable(model::Clapeyron.EoSModel, p, T, arr::AbstractVector) + @register_symbolic PS.ρT_enthalpy(model::Clapeyron.EoSModel, ρ, T, arr::AbstractVector) @register_symbolic PS.ρT_internal_energy(model::Clapeyron.EoSModel, ρ, T, arr::AbstractVector) diff --git a/src/base/basecomponents.jl b/src/base/basecomponents.jl index c3cd88d..257385f 100644 --- a/src/base/basecomponents.jl +++ b/src/base/basecomponents.jl @@ -5,7 +5,7 @@ vars = @variables begin ϕ(t)[1:medium.Constants.nphases - 1], [description = "phase fraction"] ρ(t)[1:medium.Constants.nphases], [description = "molar density"] - z(t)[1:medium.Constants.Nc, 1:medium.Constants.nphases], [description = "mole fraction", irreducible = true] + z(t)[1:medium.Constants.Nc, 1:medium.Constants.nphases], [description = "mole fraction", irreducible = false] p(t), [description = "pressure"] T(t), [description = "Temperature"] end @@ -387,6 +387,7 @@ end @component function ThreePortControlVolume_SteadyState(;medium, name) """ Three-port control volume (steady-state): 1 inlet, 2 separate outlets (liquid and vapor) + Full version with holdup variables - needed for VTN flash calculations """ systems = @named begin @@ -407,6 +408,78 @@ end Wₛ(t), [description = "shaft work"] end +@component function ThreePortControlVolume_SteadyState_PT(;medium, name) + """ + Simplified three-port control volume for PT flash (steady-state) + Eliminates holdup variables - only for PT flash where P,T are specified + """ + + systems = @named begin + InPort = PhZConnector_(medium = medium) + LiquidOutPort = PhZConnector_(medium = medium) + VaporOutPort = PhZConnector_(medium = medium) + ControlVolumeState = ρTz_ThermodynamicState_(medium = medium) + end + + vars = @variables begin + Q(t), [description = "heat flux"] + Wₛ(t), [description = "shaft work"] + end + + pars = [] + + eqs = [ + # Steady-state energy balance + 0 ~ InPort.h[1]*InPort.ṅ[1] + + LiquidOutPort.h[1]*LiquidOutPort.ṅ[1] + + VaporOutPort.h[1]*VaporOutPort.ṅ[1] + Q + Wₛ + + # Steady-state component mole balances (no reactions or surface transfer) + [0 ~ InPort.ṅ[1]*InPort.z[i, 1] + + LiquidOutPort.ṅ[1]*LiquidOutPort.z[i, 1] + + VaporOutPort.ṅ[1]*VaporOutPort.z[i, 1] for i in 1:medium.Constants.Nc]... + + # Overall composition in control volume equals inlet composition + scalarize(ControlVolumeState.z[:, 1] .~ InPort.z[:, 1])... + + # Liquid outlet properties (pure liquid stream) + LiquidOutPort.p ~ ControlVolumeState.p + scalarize(LiquidOutPort.z[:, 2] .~ ControlVolumeState.z[:, 2])... + scalarize(LiquidOutPort.z[:, 3] .~ ControlVolumeState.z[:, 3])... + scalarize(LiquidOutPort.z[:, 1] .~ ControlVolumeState.z[:, 2])... # Overall = liquid composition + + LiquidOutPort.h[2] ~ pT_enthalpy(medium.EoSModel, ControlVolumeState.p, ControlVolumeState.T, + collect(ControlVolumeState.z[:, 2]), "liquid") + LiquidOutPort.h[3] ~ pT_enthalpy(medium.EoSModel, ControlVolumeState.p, ControlVolumeState.T, + collect(ControlVolumeState.z[:, 3]), "vapor") + LiquidOutPort.h[1] ~ LiquidOutPort.h[2] # Pure liquid stream + + LiquidOutPort.ṅ[2] ~ LiquidOutPort.ṅ[1] # Total flow = liquid flow + LiquidOutPort.ṅ[3] ~ 0.0 # No vapor in liquid stream + + # Vapor outlet properties (pure vapor stream) + VaporOutPort.p ~ ControlVolumeState.p + scalarize(VaporOutPort.z[:, 2] .~ ControlVolumeState.z[:, 2])... + scalarize(VaporOutPort.z[:, 3] .~ ControlVolumeState.z[:, 3])... + scalarize(VaporOutPort.z[:, 1] .~ ControlVolumeState.z[:, 3])... # Overall = vapor composition + + VaporOutPort.h[2] ~ pT_enthalpy(medium.EoSModel, ControlVolumeState.p, ControlVolumeState.T, + collect(ControlVolumeState.z[:, 2]), "liquid") + VaporOutPort.h[3] ~ pT_enthalpy(medium.EoSModel, ControlVolumeState.p, ControlVolumeState.T, + collect(ControlVolumeState.z[:, 3]), "vapor") + VaporOutPort.h[1] ~ VaporOutPort.h[3] # Pure vapor stream + + VaporOutPort.ṅ[3] ~ VaporOutPort.ṅ[1] # Total flow = vapor flow + VaporOutPort.ṅ[2] ~ 0.0 # No liquid in vapor stream + + # Flow balance based on phase fractions (key equations!) + LiquidOutPort.ṅ[1] ~ -InPort.ṅ[1] * ControlVolumeState.ϕ[1] # Liquid flow out + VaporOutPort.ṅ[1] ~ -InPort.ṅ[1] * (1 - ControlVolumeState.ϕ[1]) # Vapor flow out + ] + + return ODESystem(eqs, t, collect(Iterators.flatten(vars)), pars; name, systems = [systems...]) +end + pars = [] eqs = [ @@ -447,8 +520,8 @@ end scalarize(LiquidOutPort.z[:, 1] .~ ControlVolumeState.z[:, 2])... LiquidOutPort.ṅ[2] ~ LiquidOutPort.ṅ[1] - LiquidOutPort.ṅ[3] ~ 1e-10 - LiquidOutPort.ṅ[1] ~ InPort.ṅ[1]*ControlVolumeState.ϕ[1] + LiquidOutPort.ṅ[3] ~ 1e-15 + #LiquidOutPort.ṅ[2] ~ InPort.ṅ[1]*ControlVolumeState.ϕ[1] # Vapor outlet properties VaporOutPort.h[2] ~ ρT_enthalpy(medium.EoSModel, ControlVolumeState.ρ[2], ControlVolumeState.T, collect(ControlVolumeState.z[:, 2])) @@ -461,8 +534,11 @@ end scalarize(VaporOutPort.z[:, 1] .~ ControlVolumeState.z[:, 3])... VaporOutPort.ṅ[3] ~ VaporOutPort.ṅ[1] - VaporOutPort.ṅ[2] ~ 1e-10 - VaporOutPort.ṅ[1] ~ InPort.ṅ[1]*(ControlVolumeState.ϕ[2]) + VaporOutPort.ṅ[2] ~ 1e-15 + + # Missing DOF equations: Overall flow balance and phase splitting + LiquidOutPort.ṅ[1] + InPort.ṅ[1] * ControlVolumeState.ϕ[1] ~ 0.0 + VaporOutPort.ṅ[1] + InPort.ṅ[1] * (ControlVolumeState.ϕ[2]) ~ 0.0 ] return ODESystem(eqs, t, collect(Iterators.flatten(vars)), pars; name, systems = [systems...]) diff --git a/src/reactors/CSTR.jl b/src/reactors/CSTR.jl index 0e766c8..244c073 100644 --- a/src/reactors/CSTR.jl +++ b/src/reactors/CSTR.jl @@ -102,9 +102,10 @@ function SteadyStateCSTR(;medium, reactionset, limiting_reactant, state, W, Q, n odesystem = SteadyStateCSTRModel(medium = medium, reactions = reactionset, limiting_reactant = limiting_reactant, state = state, W = W, Q = Q, phase = phase, name = name) - _Q = copy(Q) + - if !isnothing(_Q) #If heat is given use, else fix temperature and calculate heat + if !isnothing(Q) #If heat is given use, else fix temperature and calculate heat + _Q = copy(Q) @unpack Q = odesystem q_eq = [Q ~ _Q] else diff --git a/src/separation/FlashDrum.jl b/src/separation/FlashDrum.jl index c490dfd..e48ac44 100644 --- a/src/separation/FlashDrum.jl +++ b/src/separation/FlashDrum.jl @@ -81,9 +81,9 @@ end function SteadyStateFlashDrum(; medium, state, Q, name) medium, state, phase = resolve_guess!(medium, state) odesystem = SteadyStateFlashDrumModel(medium = medium, state = state, Q = Q, name = name) - _Q = copy(Q) - - if !isnothing(_Q) + + if !isnothing(Q) + _Q = copy(Q) @unpack Q = odesystem q_eq = [Q ~ _Q] else diff --git a/src/utils/FluidsProp.jl b/src/utils/FluidsProp.jl index 210f259..4a7f2c7 100644 --- a/src/utils/FluidsProp.jl +++ b/src/utils/FluidsProp.jl @@ -250,6 +250,8 @@ function resolve_guess!(medium, state) phase = ifelse(medium.Guesses.ϕ[2] ≈ 1.0, "vapor", "liquid") state.p = medium.Guesses.p else + # Both p and V are available - still update guesses with current composition + medium.Guesses = EosBasedGuesses(medium.EoSModel, p, T, z, Val(:Pressure)) phase = ifelse(medium.Guesses.ϕ[2] ≈ 1.0, "vapor", "liquid") end return medium, state, phase diff --git a/test/separation/flash_drum_test.jl b/test/separation/flash_drum_test.jl index 142df17..2574f38 100644 --- a/test/separation/flash_drum_test.jl +++ b/test/separation/flash_drum_test.jl @@ -12,26 +12,27 @@ using Test # Flash conditions: 2.5 atm, 315.87 K components = ["1,3-butadiene", "isobutene", "n-pentane", "1-pentene", "1-hexene", "benzene"] -model = PR(components, idealmodel = ReidIdeal) +model = SRK(components, idealmodel = ReidIdeal) medium = EoSBased(components = components, eosmodel = model) @named S1 = Boundary_pTzn( medium = medium, - p = 507.1e3, + p = 5.071e5, T = 338.0, z = [0.2379, 0.3082, 0.09959, 0.1373, 0.08872, 0.1283], flowrate = 496.3/3600, flowbasis = :molar ) -#This is just a guess as in a flowsheet the real composition will depend on downstream conditions -flash_state = pTNVState(2.5*101325.0, 315.87, [0.2379, 0.3082, 0.09959, 0.1373, 0.08872, 0.1283], base = :Pressure) +#[0.2379, 0.3082, 0.09959, 0.1373, 0.08872, 0.1283] +#This is just a guess as in a flowsheet the real composition will depend on upstream conditions +flash_state = pTNVState(2.5e5, 338.00, [0.2379, 0.3082, 0.09959, 0.1373, 0.08872, 0.1283], base = :Pressure) @named FL1 = FixedPressureSteadyStateFlashDrum( medium = medium, state = flash_state, - pressure = flash_state.p, - Q = 0.0 + pressure = flash_state.p, # Flash at 2.5 atm as per EMSO example + Q = nothing ) @named LiquidPort = ConnHouse(medium = medium) @@ -48,13 +49,22 @@ connection_set = [ SteadyStateFlash = mtkcompile(sys) +equations(SteadyStateFlash) + default_guesses = guesses(SteadyStateFlash) guesses_Flash = Dict( - FL1.odesystem.VaporOutPort.ṅ[1] => 396.3/3600, - FL1.odesystem.LiquidOutPort.ṅ[1] => 100.0/3600, + FL1.odesystem.V[2] => 1.0/FL1.medium.Guesses.ρ[2], + FL1.odesystem.V[3] => 1.0/FL1.medium.Guesses.ρ[3], + FL1.odesystem.nᴸⱽ[2] => 100.0, # Vapor holdup guess ) merged_guesses = merge(default_guesses, guesses_Flash) -prob = NonlinearProblem(SteadyStateFlash, merged_guesses) -@time sol = solve(prob, abstol = 1e-8, reltol = 1e-8) +prob = NonlinearProblem(SteadyStateFlash, merged_guesses, use_scc = true) +@time sol = solve(prob, NewtonRaphson(autodiff = AutoFiniteDiff()), abstol = 1e-8, reltol = 1e-8) +sol[SteadyStateFlash.FL1.Q] +sol[SteadyStateFlash.FL1.V] +sol[SteadyStateFlash.FL1.nᴸⱽ] +sol[SteadyStateFlash.FL1.ControlVolumeState.ϕ] +sol[SteadyStateFlash.FL1.ControlVolumeState.T] +sol[SteadyStateFlash.FL1.ControlVolumeState.z] \ No newline at end of file