1+ classdef EquationStatePengRobinson < combustiontoolbox .core .EquationState
2+ % The :mat:class:`EquationStatePengRobinson` class implements the
3+ % Peng-Robinson equation of state for real gases.
4+ %
5+ % Example:
6+ % eos = EquationStatePengRobinson();
7+ %
8+ % See also: :mat:class:`EquationState`, :mat:class:`Mixture`
9+
10+ properties (Access = public )
11+ tol0 = 1e-8 ; % Tolerance for root finding
12+ end
13+
14+ properties (Access = private )
15+ cachedListSpecies % Cell array of strings to validate the cache
16+ temperatureCritical % Critical temperatures of all species [K]
17+ pressureCritical % Critical pressures of all species [Pa]
18+ acentricFactor % Acentric factors of all species [-]
19+ FLAG_VALID % Flag array identifying species with valid PR data
20+ end
21+
22+ properties (Constant , Access = private )
23+ R0 = combustiontoolbox.common.Constants.R0; % Universal gas constant [J/(K mol)]
24+ end
25+
26+ methods (Access = public )
27+
28+ function pressure = getPressure(obj , temperature , molarVolume , molarFractions , chemicalSystem , varargin )
29+ % Compute pressure [Pa] using the Peng-Robinson equation of state, namely:
30+ %
31+ % .. math::
32+ %
33+ % `P = \\frac{RT}{V - b} - \\frac{a}{V^2 + 2bV - b^2},`
34+ %
35+ % where :math:`a` and :math:`b` are mixture parameters computed using
36+ % van der Waals one-fluid mixing rules.
37+ %
38+ %
39+ % Args:
40+ % obj (EquationStatePengRobinson): Equation of state object
41+ % temperature (float): Temperature of the mixture [K]
42+ % molarVolume (float): Molar volume of the mixture [m3/mol]
43+ % molarFractions (float): Molar fractions of the species in the mixture
44+ % chemicalSystem (ChemicalSystem): Chemical system object containing species data
45+ %
46+ % Returns:
47+ % pressure (float): Pressure of the mixture [Pa]
48+ %
49+ % Example:
50+ % P = getPressure(obj, 300, 0.024, [0.5, 0.5], chemicalSystem)
51+
52+ % Compute mixture parameters
53+ [a_mix , b_mix , ~ , ~ ] = obj .getMixtureParameters(temperature , molarFractions , chemicalSystem );
54+
55+ % Compute pressure [Pa]
56+ pressure = (obj .R0 * temperature ) / (molarVolume - b_mix ) - ...
57+ a_mix / (molarVolume ^ 2 + 2 * b_mix * molarVolume - b_mix ^ 2 );
58+ end
59+
60+ function molarVolume = getVolume(obj , temperature , pressure , molarFractions , chemicalSystem , varargin )
61+ % Compute gas-phase molar volume [m3/mol] by solving the Peng-Robinson
62+ % cubic equation of state for the given temperature and pressure
63+ %
64+ % Args:
65+ % obj (EquationStatePengRobinson): Equation of state object
66+ % temperature (float): Temperature of the mixture [K]
67+ % pressure (float): Pressure of the mixture [Pa]
68+ % molarFractions (float): Molar fractions of the species in the mixture
69+ % chemicalSystem (ChemicalSystem): Chemical system object containing species data
70+ %
71+ % Returns:
72+ % molarVolume (float): Molar volume of the mixture [m3/mol]
73+ %
74+ % Example:
75+ % V = getVolume(obj, 300, 1e5, [0.5, 0.5], chemicalSystem)
76+
77+ % Compute mixture parameters
78+ [a_mix , b_mix , ~ , ~ ] = obj .getMixtureParameters(temperature , molarFractions , chemicalSystem );
79+
80+ % Dimensionless coefficients A and B
81+ A = (a_mix * pressure ) / (obj .R0^ 2 * temperature ^ 2 );
82+ B = (b_mix * pressure ) / (obj .R0 * temperature );
83+
84+ % Cubic coefficients for Z^3 + c2*Z^2 + c1*Z + c0 = 0
85+ coeffs = [1.0 , -(1.0 - B ), (A - 2 * B - 3 * B ^ 2 ), -(A * B - B ^ 2 - B ^ 3 )];
86+
87+ % Solve for Z and pick the largest real root (gas phase)
88+ Z_roots = roots(coeffs );
89+ Z_real = real(Z_roots(abs(imag(Z_roots )) < obj .tol0));
90+
91+ if isempty(Z_real )
92+ error(' EquationStatePengRobinson:getVolume' , ' No real roots found for Z.' );
93+ end
94+
95+ Z_gas = max(Z_real );
96+
97+ % Compute molar volume [m3/mol]
98+ molarVolume = (Z_gas * obj .R0 * temperature ) / pressure ;
99+ end
100+
101+ function [dPdV_T , dPdT_V ] = getPressureDerivativesDimensional(obj , temperature , pressure , molarVolume , molarFractions , chemicalSystem , varargin )
102+ % Compute dimensional partial pressure derivatives for the mixture assuming frozen chemistry
103+ %
104+ % Args:
105+ % obj (EquationStatePengRobinson): Equation of state object
106+ % temperature (float): Temperature of the mixture [K]
107+ % pressure (float): Pressure of the mixture [Pa]
108+ % molarVolume (float): Molar volume of the mixture [m3/mol]
109+ % molarFractions (float): Molar fractions of the species in the mixture
110+ % chemicalSystem (ChemicalSystem): Chemical system object containing species data
111+ %
112+ % Returns:
113+ % Tuple containing
114+ %
115+ % * dPdV_T (float): Partial derivative of pressure with respect to volume at constant temperature [Pa/(m3/mol)]
116+ % * dPdT_V (float): Partial derivative of pressure with respect to temperature at constant volume [Pa/K]
117+ %
118+ % Example:
119+ % [dPdV_T, dPdT_V] = getPressureDerivativesDimensional(obj, 300, 1e5, 0.024, [0.5, 0.5], chemicalSystem)
120+
121+ % Compute mixture parameters
122+ [a_mix , b_mix , dadT_mix , ~ ] = obj .getMixtureParameters(temperature , molarFractions , chemicalSystem );
123+
124+ % Compute dimensional pressure derivatives
125+ dPdV_T = -(obj .R0 * temperature ) / (molarVolume - b_mix )^2 + (2 * a_mix * (molarVolume + b_mix )) / (molarVolume ^ 2 + 2 * b_mix * molarVolume - b_mix ^ 2 )^2 ;
126+ dPdT_V = obj .R0 / (molarVolume - b_mix ) - dadT_mix / (molarVolume ^ 2 + 2 * b_mix * molarVolume - b_mix ^ 2 );
127+ end
128+
129+ function [heatCapacityPressureDeparture , enthalpyDeparture , entropyDeparture ] = getDepartureFunctions(obj , temperature , pressure , molarVolume , molarFractions , chemicalSystem , varargin )
130+ % Compute thermodynamic departure functions for the mixture using the Peng-Robinson equation of state
131+ %
132+ % Args:
133+ % obj (EquationStatePengRobinson): Equation of state object
134+ % temperature (float): Temperature of the mixture [K]
135+ % pressure (float): Pressure of the mixture [Pa]
136+ % molarVolume (float): Molar volume of the mixture [m3/mol]
137+ % molarFractions (float): Molar fractions of the species in the mixture
138+ % chemicalSystem (ChemicalSystem): Chemical system object containing species data
139+ %
140+ % Returns:
141+ % Tuple containing
142+ %
143+ % * heatCapacityPressureDeparture (float): Heat capacity at constant pressure departure [J/(mol-K)]
144+ % * enthalpyDeparture (float): Enthalpy departure [J/mol]
145+ % * entropyDeparture (float): Entropy departure [J/(mol-K)]
146+ %
147+ % Example:
148+ % [dcp, dh, ds] = getDepartureFunctions(obj, 300, 1e5, 0.024, [0.5, 0.5], chemicalSystem)
149+
150+ % Compute mixture parameters
151+ [a_mix , b_mix , dadT_mix , d2adT2_mix ] = obj .getMixtureParameters(temperature , molarFractions , chemicalSystem );
152+
153+ % If mixture behaves ideally (b_mix is zero), return zeros for all departure functions
154+ if b_mix < 1e- 15
155+ heatCapacityPressureDeparture = 0 ;
156+ enthalpyDeparture = 0 ;
157+ entropyDeparture = 0 ;
158+ return
159+ end
160+
161+ Z = obj .getCompressibilityFactor(temperature , pressure , molarVolume );
162+ B = (b_mix * pressure ) / (obj .R0 * temperature );
163+
164+ % Common terms for departure functions
165+ arg = (Z + (1 + sqrt(2 )) * B ) / (Z + (1 - sqrt(2 )) * B );
166+ logTerm = log(max(arg , 1e- 12 ));
167+ denom = 2 * sqrt(2 ) * b_mix ;
168+
169+ % Enthalpy departure [J/mol]
170+ enthalpyDeparture = obj .R0 * temperature * (Z - 1 ) + ((temperature * dadT_mix - a_mix ) / denom ) * logTerm ;
171+
172+ % Entropy departure [J/(mol-K)]
173+ entropyDeparture = obj .R0 * log(max(Z - B , 1e- 12 )) + (dadT_mix / denom ) * logTerm ;
174+
175+ % Heat capacity at constant volume departure [J/(mol-K)]
176+ heatCapacityVolumeDeparture = (temperature * d2adT2_mix / denom ) * logTerm ;
177+
178+ % Compute pressure derivatives
179+ [dPdV_T , dPdT_V ] = obj .getPressureDerivativesDimensional(temperature , pressure , molarVolume , molarFractions , chemicalSystem , varargin{: });
180+
181+ % Heat capacity at constant pressure departure [J/(mol-K)]
182+ heatCapacityPressureDeparture = heatCapacityVolumeDeparture + (-temperature * (dPdT_V ^ 2 ) / dPdV_T ) - obj .R0;
183+ end
184+
185+ end
186+
187+ methods (Access = private )
188+
189+ function initializeCache(obj , chemicalSystem )
190+ % Cache the database values once to avoid field lookups during iterative solver loops
191+ %
192+ % Args:
193+ % obj (EquationStatePengRobinson): Equation of state object
194+ % chemicalSystem (ChemicalSystem): Chemical system object containing species data
195+ %
196+ % Example:
197+ % obj.initializeCache(chemicalSystem)
198+
199+ % Definitions
200+ listSpecies = chemicalSystem .listSpecies;
201+ numSpecies = chemicalSystem .numSpecies;
202+
203+ % Preallocate arrays
204+ Tc = zeros(1 , numSpecies );
205+ Pc = zeros(1 , numSpecies );
206+ omega = zeros(1 , numSpecies );
207+
208+ % Extract Species objects from the chemical system
209+ species = chemicalSystem .species;
210+ for i = 1 : numSpecies
211+ name = listSpecies{i };
212+ Tc(i ) = species.(name ).Tcritical;
213+ Pc(i ) = species.(name ).Pcritical;
214+ omega(i ) = species.(name ).acentricFactor;
215+ end
216+
217+ obj.temperatureCritical = Tc ; % [K]
218+ obj.pressureCritical = Pc * 1e5 ; % [Pa]
219+ obj.acentricFactor = omega ; % [-]
220+
221+ % Identify species with valid PR data (not NaN and > 0)
222+ obj.FLAG_VALID = ~isnan(Tc ) & (Tc > 0 ) & ~isnan(Pc ) & (Pc > 0 );
223+
224+ % Cache the list of species to validate future calls
225+ obj.cachedListSpecies = listSpecies ;
226+ end
227+
228+ function [a_mix , b_mix , dadT_mix , d2adT2_mix ] = getMixtureParameters(obj , temperature , molarFractions , chemicalSystem )
229+ % Compute mixture parameters using van der Waals one-fluid mixing rules
230+ %
231+ % Args:
232+ % obj (EquationStatePengRobinson): Equation of state object
233+ % temperature (float): Temperature of the mixture [K]
234+ % molarFractions (float): Molar fractions of the species in the mixture
235+ % chemicalSystem (ChemicalSystem): Chemical system object containing species data
236+ %
237+ % Returns:
238+ % Tuple containing
239+ %
240+ % * a_mix (float): Mixture attraction parameter [J-m3/mol^2]
241+ % * b_mix (float): Mixture co-volume parameter [m3/mol]
242+ % * dadT_mix (float): First temperature derivative of a_mix [J-m3/(mol^2 K)]
243+ % * d2adT2_mix (float): Second temperature derivative of a_mix [J-m3/(mol^2 K^2)]
244+ %
245+ % Example:
246+ % [a_mix, b_mix, dadT_mix, d2adT2_mix] = getMixtureParameters(obj, 300, [0.5, 0.5], chemicalSystem)
247+
248+ % Rebuild cache if empty or if species list changed/reordered
249+ if isempty(obj .cachedListSpecies) || ~isequal(obj .cachedListSpecies, chemicalSystem .listSpecies)
250+ obj .initializeCache(chemicalSystem );
251+ end
252+
253+ % Find species that are both active (>0) AND have valid PR data
254+ FLAG_ACTIVE = (molarFractions(: ) > 0 ) & obj .FLAG_VALID(: );
255+
256+ % If no real species are present, mixture is purely ideal
257+ if ~any(FLAG_ACTIVE )
258+ a_mix = 0 ; b_mix = 0 ; dadT_mix = 0 ; d2adT2_mix = 0 ;
259+ return ;
260+ end
261+
262+ % Extract data for active species
263+ X_active = molarFractions(FLAG_ACTIVE );
264+ Tc = obj .temperatureCritical(FLAG_ACTIVE );
265+ Pc = obj .pressureCritical(FLAG_ACTIVE );
266+ omega = obj .acentricFactor(FLAG_ACTIVE );
267+
268+ % Pure species parameters
269+ kappa = 0.37464 + 1.54226 * omega - 0.26992 * omega .^ 2 ;
270+ Tr = temperature ./ Tc ;
271+ sqrtTr = sqrt(Tr );
272+ alpha = (1 + kappa .* (1 - sqrtTr )).^2 ;
273+
274+ a_i = 0.45724 * (obj .R0^ 2 * Tc .^ 2 ./ Pc ) .* alpha ;
275+ b_i = 0.07780 * (obj .R0 * Tc ./ Pc );
276+
277+ % Temperature derivatives
278+ dalpha_dT = - kappa ./ (sqrtTr .* Tc ) .* (1 + kappa .* (1 - sqrtTr ));
279+ d2alpha_dT2 = kappa .* (kappa + 1 ) ./ (2 * Tc .^ 2 .* Tr .^(3 / 2 ));
280+
281+ a0 = 0.45724 * (obj .R0^ 2 * Tc .^ 2 ./ Pc );
282+ da_dT_i = a0 .* dalpha_dT ;
283+ d2a_dT2_i = a0 .* d2alpha_dT2 ;
284+
285+ % Apply mixing rules
286+ b_mix = dot(X_active , b_i );
287+
288+ sqrt_a_i = sqrt(max(a_i , 1e- 20 ));
289+ S1 = dot(X_active , sqrt_a_i );
290+ a_mix = S1 ^ 2 ;
291+
292+ S2 = dot(X_active , da_dT_i ./ (2 * sqrt_a_i ));
293+ dadT_mix = 2 * S1 * S2 ;
294+
295+ S3 = dot(X_active , (d2a_dT2_i ./ (2 * sqrt_a_i )) - (da_dT_i .^ 2 ./ (4 * sqrt_a_i .^ 3 )));
296+ d2adT2_mix = 2 * S2 ^ 2 + 2 * S1 * S3 ;
297+ end
298+
299+ function [temperatureCritical_mix , pressureCritical_mix , acentricFactor_mix ] = getPseudoCriticalProperties(obj , molarFractions , chemicalSystem )
300+ % Computes pseudo-critical properties for multi-component mixtures
301+ %
302+ % Args:
303+ % obj (EquationStatePengRobinson): Equation of state object
304+ % molarFractions (float): Molar fractions of the species in the mixture
305+ % chemicalSystem (ChemicalSystem): Chemical system object containing species data
306+ %
307+ % Returns:
308+ % Tuple containing
309+ %
310+ % * temperatureCritical_mix (float): Pseudo-critical temperature of the mixture [K]
311+ % * pressureCritical_mix (float): Pseudo-critical pressure of the mixture [Pa]
312+ % * acentricFactor_mix (float): Pseudo-critical acentric factor of the mixture [-]
313+ %
314+ % Example:
315+ % [temperatureCritical_mix, pressureCritical_mix, acentricFactor_mix] = getPseudoCriticalProperties(obj, [0.5, 0.5], chemicalSystem)
316+
317+ if isempty(obj .cachedListSpecies) || ~isequal(obj .cachedListSpecies, chemicalSystem .listSpecies)
318+ obj .initializeCache(chemicalSystem );
319+ end
320+
321+ % Definitions
322+ mask = (molarFractions(: ) > 0 ) & obj .FLAG_VALID(: );
323+ Xi = molarFractions(mask );
324+ Tc_i = obj .temperatureCritical(mask );
325+ Pc_i = obj .pressureCritical(mask );
326+ omega_i = obj .acentricFactor(mask );
327+
328+ % a and b at critical point (alpha = 1)
329+ a_i_tc = 0.45724 * (obj .R0^ 2 * Tc_i .^ 2 ./ Pc_i );
330+ b_i = 0.07780 * (obj .R0 * Tc_i ./ Pc_i );
331+
332+ % Mixture parameters at critical condition
333+ a_mix_tc = ( dot(Xi , sqrt(a_i_tc )) )^2 ;
334+ b_mix = dot(Xi , b_i );
335+
336+ % Back-calculate pseudo-critical T and P
337+ temperatureCritical_mix = (a_mix_tc * 0.07780 ) / (b_mix * 0.45724 * obj .R0);
338+ pressureCritical_mix = (0.07780 * obj .R0 * temperatureCritical_mix ) / b_mix ;
339+ acentricFactor_mix = dot(Xi , omega_i );
340+ end
341+
342+ end
343+ end
0 commit comments