CoolProp 8.0.0
An open-source fluid property and humid air property database
HelmholtzEOSMixtureBackend.cpp
Go to the documentation of this file.
1/*
2 * AbstractBackend.cpp
3 *
4 * Created on: 20 Dec 2013
5 * Author: jowr
6 */
7
8#include <cmath>
9#include <memory>
10
11#if defined(_MSC_VER)
12# define _CRTDBG_MAP_ALLOC
13# ifndef _CRT_SECURE_NO_WARNINGS
14# define _CRT_SECURE_NO_WARNINGS
15# endif
16# include <crtdbg.h>
17# include <sys/stat.h>
18#else
19# include <sys/stat.h>
20#endif
21
22#include <string>
23//#include "CoolProp/CoolProp.h"
24
25#include "CoolProp/CoolProp.h"
27#include "HelmholtzEOSBackend.h"
28#include "Fluids/FluidLibrary.h"
32#include "boost/math/tools/toms748_solve.hpp"
33#include "VLERoutines.h"
34#include "FlashRoutines.h"
35#include "TransportRoutines.h"
36#include "MixtureDerivatives.h"
38#include "ReducingFunctions.h"
39#include "MixtureParameters.h"
41#include "MixtureParameters.h"
42#include <atomic>
43#include <cstdlib>
44
45// Instrumentation only: counts the number of EOS derivative-cache evaluations
46// across all HEOS instances. Atomic so concurrent calls do not race (#2844).
47static std::atomic<int> deriv_counter{0};
48
49namespace CoolProp {
50
52{
53 public:
54 AbstractState* get_AbstractState(const std::vector<std::string>& fluid_names) override {
55 if (fluid_names.size() == 1) {
56 return new HelmholtzEOSBackend(fluid_names[0]);
57 } else {
58 return new HelmholtzEOSMixtureBackend(fluid_names);
59 }
60 };
61};
62// This static initialization will cause the generator to register.
63// See REFPROPMixtureBackend.cpp for the cert-err58-cpp rationale — the
64// constructor inserts a single entry into a std::map at program start;
65// if allocation fails here the process is unrecoverable anyway.
66// NOLINTNEXTLINE(cert-err58-cpp)
68
69HelmholtzEOSMixtureBackend::HelmholtzEOSMixtureBackend() : is_pure_or_pseudopure(false), N(0) {
71
73 // Reset the residual Helmholtz energy class
74 residual_helmholtz = std::make_shared<ResidualHelmholtz>();
75}
76HelmholtzEOSMixtureBackend::HelmholtzEOSMixtureBackend(const std::vector<std::string>& component_names, bool generate_SatL_and_SatV) {
77 std::vector<CoolPropFluid> components(component_names.size());
78 for (unsigned int i = 0; i < components.size(); ++i) {
79 components[i] = get_library().get(component_names[i]);
80 }
81
82 // Reset the residual Helmholtz energy class
83 residual_helmholtz = std::make_shared<ResidualHelmholtz>();
84
85 // Set the components and associated flags. Explicit scope so the
86 // virtual-from-ctor call isn't flagged — derived overrides aren't
87 // active yet, so we want exactly THIS class's set_components.
89
90 // Set the phase to default unknown value
92}
93HelmholtzEOSMixtureBackend::HelmholtzEOSMixtureBackend(const std::vector<CoolPropFluid>& components, bool generate_SatL_and_SatV) {
94
95 // Reset the residual Helmholtz energy class
96 residual_helmholtz = std::make_shared<ResidualHelmholtz>();
97
98 // Set the components and associated flags (explicit scope — see
99 // the string-name ctor above for the rationale).
101
102 // Set the phase to default unknown value
104}
105void HelmholtzEOSMixtureBackend::set_components(const std::vector<CoolPropFluid>& components, bool generate_SatL_and_SatV) {
106
107 // Drop the cached ECS transport reference fluids: they are tied to the
108 // OLD component's reference-fluid name, so a post-construction component
109 // swap must rebuild them. (No-op at construction, where they are null.)
112
113 // Copy the components
114 this->components = components;
115 this->N = components.size();
116
117 is_pure_or_pseudopure = (components.size() == 1);
119 mole_fractions = std::vector<CoolPropDbl>(1, 1);
120 std::vector<std::vector<double>> ones(1, std::vector<double>(1, 1));
121 Reducing = std::make_shared<GERG2008ReducingFunction>(components, ones, ones, ones, ones);
122 // Explicit scope: set_components is called from the ctor (see
123 // HelmholtzEOSMixtureBackend ctor above), so derived overrides
124 // aren't active yet and these virtuals would dispatch to this
125 // class anyway. Make that explicit.
128 } else {
129 // Set the mixture parameters - binary pair reducing functions, departure functions, F_ij, etc.
131 }
132
134
135 // Top-level class can hold copies of the base saturation classes,
136 // saturation classes cannot hold copies of the saturation classes.
137 // Explicit scope on get_copy: this method is called from the ctor
138 // (transitively) and clang-analyzer flags the implicit virtual
139 // dispatch — but derived overrides aren't active yet, so we want
140 // exactly THIS class's get_copy regardless.
141 if (generate_SatL_and_SatV) {
143 SatL->specify_phase(iphase_liquid);
144 linked_states.push_back(SatL);
145 SatL->clear();
147 SatV->specify_phase(iphase_gas);
148 SatV->clear();
149 linked_states.push_back(SatV);
150 }
151}
152void HelmholtzEOSMixtureBackend::set_mole_fractions(const std::vector<CoolPropDbl>& mf) {
153 if (mf.size() != N) {
154 throw ValueError(format("size of mole fraction vector [%d] does not equal that of component vector [%d]", mf.size(), N));
155 }
156 // Copy values without reallocating memory
157 this->mole_fractions = mf; // Most effective copy
158 this->resize(N); // No reallocation of this->mole_fractions happens
160};
162 residual_helmholtz.reset(source->residual_helmholtz->copy_ptr());
163 if (source->Reducing) {
164 Reducing.reset(source->Reducing->copy());
165 }
166 // Recurse into linked states of the class
167 for (auto& state : linked_states) {
168 state->sync_linked_states(source);
169 }
170}
172 // Set up the class with these components
173 auto* ptr = new HelmholtzEOSMixtureBackend(components, generate_SatL_and_SatV);
174 // Recursively walk into linked states, setting the departure and reducing terms
175 // to be equal to the parent (this instance)
176 ptr->sync_linked_states(this);
177 return ptr;
178};
179void HelmholtzEOSMixtureBackend::set_mass_fractions(const std::vector<CoolPropDbl>& mass_fractions) {
180 if (mass_fractions.size() != N) {
181 throw ValueError(format("size of mass fraction vector [%d] does not equal that of component vector [%d]", mass_fractions.size(), N));
182 }
183 std::vector<CoolPropDbl> moles;
184 CoolPropDbl sum_moles = 0.0;
185 CoolPropDbl tmp = 0.0;
186 for (unsigned int i = 0; i < components.size(); ++i) {
187 tmp = mass_fractions[i] / components[i].molar_mass();
188 moles.push_back(tmp);
189 sum_moles += tmp;
190 }
191 std::vector<CoolPropDbl> mole_fractions;
192 mole_fractions.reserve(moles.size());
193 for (const auto& m : moles) {
194 mole_fractions.push_back(m / sum_moles);
195 }
196 this->set_mole_fractions(mole_fractions);
197};
199 this->mole_fractions.resize(N);
200 this->K.resize(N);
201 this->lnK.resize(N);
202 for (auto& state : linked_states) {
203 state->N = N;
204 state->resize(N);
205 }
206}
209 // For mixtures, use the reducing density as a cheap proxy.
210 // The true critical point triggers calc_all_critical_points()
211 // (an expensive, uncached solver) for each of p_critical(),
212 // T_critical(), and rhomolar_critical().
213 if (rhomolar() > rhomolar_reducing()) {
215 } else {
217 }
218 return;
219 }
220 try {
221 if (p() > p_critical()) {
222 if (T() > T_critical()) {
224 } else {
226 }
227 } else {
228 if (T() > T_critical()) {
230 } else {
231 // Liquid or vapor
232 if (rhomolar() > rhomolar_critical()) {
234 } else {
236 }
237 }
238 }
239 } catch (...) {
240 if (rhomolar() > rhomolar_reducing()) {
242 } else {
244 }
245 }
246}
247std::string HelmholtzEOSMixtureBackend::fluid_param_string(const std::string& ParamName) {
249 if (!ParamName.compare("name")) {
250 return cpfluid.name;
251 } else if (!ParamName.compare("aliases")) {
252 return strjoin(cpfluid.aliases, get_config_string(LIST_STRING_DELIMITER));
253 } else if (!ParamName.compare("aliases_bar")) {
254 return strjoin(cpfluid.aliases, "|");
255 } else if (!ParamName.compare("CAS") || !ParamName.compare("CAS_number")) {
256 return cpfluid.CAS;
257 } else if (!ParamName.compare("formula")) {
258 return cpfluid.formula;
259 } else if (!ParamName.compare("ASHRAE34")) {
260 return cpfluid.environment.ASHRAE34;
261 } else if (!ParamName.compare("REFPROPName") || !ParamName.compare("REFPROP_name") || !ParamName.compare("REFPROPname")) {
262 return cpfluid.REFPROPname;
263 } else if (ParamName.find("BibTeX") == 0) // Starts with "BibTeX"
264 {
265 std::vector<std::string> parts = strsplit(ParamName, '-');
266 if (parts.size() != 2) {
267 throw ValueError(format("Unable to parse BibTeX string %s", ParamName.c_str()));
268 }
269 const std::string& key = parts[1];
270 if (!key.compare("EOS")) {
271 return cpfluid.EOS().BibTeX_EOS;
272 } else if (!key.compare("CP0")) {
273 return cpfluid.EOS().BibTeX_CP0;
274 } else if (!key.compare("VISCOSITY")) {
275 return cpfluid.transport.BibTeX_viscosity;
276 } else if (!key.compare("CONDUCTIVITY")) {
277 return cpfluid.transport.BibTeX_conductivity;
278 } else if (!key.compare("ECS_LENNARD_JONES")) {
279 throw NotImplementedError();
280 } else if (!key.compare("ECS_VISCOSITY_FITS")) {
281 throw NotImplementedError();
282 } else if (!key.compare("ECS_CONDUCTIVITY_FITS")) {
283 throw NotImplementedError();
284 } else if (!key.compare("SURFACE_TENSION")) {
285 return cpfluid.ancillaries.surface_tension.BibTeX;
286 } else if (!key.compare("MELTING_LINE")) {
287 return cpfluid.ancillaries.melting_line.BibTeX;
288 } else {
289 throw CoolProp::KeyError(format("Bad key to get_BibTeXKey [%s]", key.c_str()));
290 }
291 } else if (ParamName.find("pure") == 0) {
292 if (is_pure()) {
293 return "true";
294 } else {
295 return "false";
296 }
297 } else if (ParamName == "INCHI" || ParamName == "InChI" || ParamName == "INCHI_STRING") {
298 return cpfluid.InChI;
299 } else if (ParamName == "INCHI_Key" || ParamName == "InChIKey" || ParamName == "INCHIKEY") {
300 return cpfluid.InChIKey;
301 } else if (ParamName == "2DPNG_URL") {
302 return cpfluid.TwoDPNG_URL;
303 } else if (ParamName == "SMILES" || ParamName == "smiles") {
304 return cpfluid.smiles;
305 } else if (ParamName == "CHEMSPIDER_ID") {
306 return format("%d", cpfluid.ChemSpider_id);
307 } else if (ParamName == "JSON") {
308 return get_fluid_as_JSONstring(cpfluid.CAS);
309 } else {
310 throw ValueError(format("fluid parameter [%s] is invalid", ParamName.c_str()));
311 }
312}
313
314std::shared_ptr<EquationOfState::SuperAncillary_t> HelmholtzEOSMixtureBackend::get_superanc() {
315 if (!is_pure()) {
316 throw CoolProp::ValueError("Only available for pure (and not pseudo-pure) fluids");
317 }
318 return components[0].EOS().get_superanc();
319}
320
322 if (!is_pure()) {
323 return;
324 }
325 auto superanc_ptr = get_superanc();
326 if (!superanc_ptr) {
327 return;
328 }
329 // Callbacks evaluate h(T, rho) and s(T, rho) using the EOS in its current
330 // configuration (including the active reference state). The rho values
331 // come from the existing rho_sat superancillary at the Chebyshev nodes —
332 // see SuperAncillary::add_variable. Building H, S and U together so we
333 // amortize the EOS calls; all derive from the same alpha0/alphar evaluation.
334 // The mutex inside ensure_HSU_under_lock serializes concurrent first-call
335 // builds across HEOS instances that share this fluid's SuperAncillary
336 // (#2773 review C2).
337 auto h_callable = [this](double T, double rhomolar) -> double { return this->calc_hmolar_nocache(T, rhomolar); };
338 auto s_callable = [this](double T, double rhomolar) -> double { return this->calc_smolar_nocache(T, rhomolar); };
339 auto u_callable = [this](double T, double rhomolar) -> double { return this->calc_umolar_nocache(T, rhomolar); };
340 // Stamp the cache with the *total* alpha0 offset — sum of Core (parse-
341 // time-immutable) and user-mutable offsets, scaled by the alpha0
342 // prefactor. This way the shift formula in resolve_T_via_superancillary
343 // remains correct even if Core were ever mutated post-parse, and even
344 // for fluids with a non-unity prefactor (currently none in the bundled
345 // library, but the JSON path is live). See #2773.
346 const auto [caller_a1, caller_a2] = FlashRoutines::alpha0_offset_total(*this);
347 superanc_ptr->ensure_HSU_under_lock(caller_a1, caller_a2, h_callable, s_callable, u_callable);
348}
349
350double HelmholtzEOSMixtureBackend::get_fluid_parameter_double(const size_t i, const std::string& parameter) {
351 if (i >= N) {
352 throw ValueError(format("Index i [%d] is out of bounds. Must be between 0 and %d.", i, N - 1));
353 }
354 auto superanc_ptr = get_superanc();
355 if (parameter.find("SUPERANC::") == 0) {
356 if (superanc_ptr) {
357 auto& superanc = *superanc_ptr;
358 std::string key = parameter.substr(10);
359 if (key == "pmax") {
360 return superanc.get_pmax();
361 } else if (key == "pmin") {
362 return superanc.get_pmin();
363 } else if (key == "Tmin") {
364 return superanc.get_Tmin();
365 } else if (key == "Tcrit_num") {
366 return superanc.get_Tcrit_num();
367 } else if (key == "caloric_build_count") {
368 // Number of times the caloric superancillary has been built.
369 // Used by tests to verify that multi-instance / multi-ref-state
370 // usage doesn't trigger thrashing rebuilds. See #2773.
371 return static_cast<double>(superanc.get_caloric_build_count());
372 } else {
373 throw ValueError(format("Superancillary parameter [%s] is invalid", key.c_str()));
374 }
375 } else {
376 throw ValueError(format("Superancillary not available for this fluid"));
377 }
378 } else {
379 throw ValueError(format("fluid parameter [%s] is invalid", parameter.c_str()));
380 }
381}
382
383void HelmholtzEOSMixtureBackend::apply_simple_mixing_rule(std::size_t i, std::size_t j, const std::string& model) {
384 // bound-check indices
385 if (i >= N) {
386 if (j >= N) {
387 throw ValueError(format("Both indices i [%d] and j [%d] are out of bounds. Must be between 0 and %d.", i, j, N - 1));
388 } else {
389 throw ValueError(format("Index i [%d] is out of bounds. Must be between 0 and %d.", i, N - 1));
390 }
391 } else if (j >= N) {
392 throw ValueError(format("Index j [%d] is out of bounds. Must be between 0 and %d.", j, N - 1));
393 }
394 if (model == "linear") {
396 double gammaT = 0.5 * (Tc1 + Tc2) / sqrt(Tc1 * Tc2);
398 double gammaV = 4.0 * (1 / rhoc1 + 1 / rhoc2) / pow(pow(rhoc1, -1.0 / 3.0) + pow(rhoc2, -1.0 / 3.0), 3);
399 set_binary_interaction_double(i, j, "betaT", 1.0);
400 set_binary_interaction_double(i, j, "gammaT", gammaT);
401 set_binary_interaction_double(i, j, "betaV", 1.0);
402 set_binary_interaction_double(i, j, "gammaV", gammaV);
403 } else if (model == "Lorentz-Berthelot") {
404 set_binary_interaction_double(i, j, "betaT", 1.0);
405 set_binary_interaction_double(i, j, "gammaT", 1.0);
406 set_binary_interaction_double(i, j, "betaV", 1.0);
407 set_binary_interaction_double(i, j, "gammaV", 1.0);
408 } else {
409 throw ValueError(format("mixing rule [%s] is not understood", model.c_str()));
410 }
411}
413void HelmholtzEOSMixtureBackend::set_binary_interaction_double(const std::size_t i, const std::size_t j, const std::string& parameter,
414 const double value) {
415 // bound-check indices
416 if (i >= N) {
417 if (j >= N) {
418 throw ValueError(format("Both indices i [%d] and j [%d] are out of bounds. Must be between 0 and %d.", i, j, N - 1));
419 } else {
420 throw ValueError(format("Index i [%d] is out of bounds. Must be between 0 and %d.", i, N - 1));
421 }
422 } else if (j >= N) {
423 throw ValueError(format("Index j [%d] is out of bounds. Must be between 0 and %d.", j, N - 1));
424 }
425 if (parameter == "Fij") {
426 residual_helmholtz->Excess.F[i][j] = value;
427 residual_helmholtz->Excess.F[j][i] = value;
428 } else {
429 Reducing->set_binary_interaction_double(i, j, parameter, value);
430 }
432 for (auto& state : linked_states) {
433 state->set_binary_interaction_double(i, j, parameter, value);
434 }
435};
437double HelmholtzEOSMixtureBackend::get_binary_interaction_double(const std::size_t i, const std::size_t j, const std::string& parameter) {
438 // bound-check indices
439 if (i >= N) {
440 if (j >= N) {
441 throw ValueError(format("Both indices i [%d] and j [%d] are out of bounds. Must be between 0 and %d.", i, j, N - 1));
442 } else {
443 throw ValueError(format("Index i [%d] is out of bounds. Must be between 0 and %d.", i, N - 1));
444 }
445 } else if (j >= N) {
446 throw ValueError(format("Index j [%d] is out of bounds. Must be between 0 and %d.", j, N - 1));
447 }
448 if (parameter == "Fij") {
449 return residual_helmholtz->Excess.F[i][j];
450 } else {
451 return Reducing->get_binary_interaction_double(i, j, parameter);
452 }
453};
455//std::string HelmholtzEOSMixtureBackend::get_binary_interaction_string(const std::string &CAS1, const std::string &CAS2, const std::string &parameter){
456// return CoolProp::get_mixture_binary_pair_data(CAS1, CAS2, parameter);
457//}
459void HelmholtzEOSMixtureBackend::set_binary_interaction_string(const std::size_t i, const std::size_t j, const std::string& parameter,
460 const std::string& value) {
461 // bound-check indices
462 if (i >= N) {
463 if (j >= N) {
464 throw ValueError(format("Both indices i [%d] and j [%d] are out of bounds. Must be between 0 and %d.", i, j, N - 1));
465 } else {
466 throw ValueError(format("Index i [%d] is out of bounds. Must be between 0 and %d.", i, N - 1));
467 }
468 } else if (j >= N) {
469 throw ValueError(format("Index j [%d] is out of bounds. Must be between 0 and %d.", j, N - 1));
470 }
471 if (parameter == "function") {
472 residual_helmholtz->Excess.DepartureFunctionMatrix[i][j].reset(get_departure_function(value));
473 residual_helmholtz->Excess.DepartureFunctionMatrix[j][i].reset(get_departure_function(value));
474 } else {
475 throw ValueError(format("Cannot process this string parameter [%s] in set_binary_interaction_string", parameter.c_str()));
476 }
478 for (auto& state : linked_states) {
479 state->set_binary_interaction_string(i, j, parameter, value);
480 }
481};
482
483void HelmholtzEOSMixtureBackend::calc_change_EOS(const std::size_t i, const std::string& EOS_name) {
484
485 if (i < components.size()) {
486 CoolPropFluid& fluid = components[i];
487 EquationOfState& EOS = fluid.EOSVector[0];
488
489 if (EOS_name == "SRK" || EOS_name == "Peng-Robinson") {
490
491 // Get the parameters for the cubic EOS
492 CoolPropDbl Tc = EOS.reduce.T;
493 CoolPropDbl pc = EOS.reduce.p;
494 CoolPropDbl rhomolarc = EOS.reduce.rhomolar;
495 CoolPropDbl acentric = EOS.acentric;
496 CoolPropDbl R = 8.3144598;
497
498 // Remove the residual part
499 EOS.alphar.empty_the_EOS();
500 // Set the contribution
501 shared_ptr<AbstractCubic> ac;
502 if (EOS_name == "SRK") {
503 ac = std::make_shared<SRK>(Tc, pc, acentric, R);
504 } else {
505 ac = std::make_shared<PengRobinson>(Tc, pc, acentric, R);
506 }
507 ac->set_Tr(Tc);
508 ac->set_rhor(rhomolarc);
510 } else if (EOS_name == "XiangDeiters") {
511
512 // Get the parameters for the EOS
513 CoolPropDbl Tc = EOS.reduce.T;
514 CoolPropDbl pc = EOS.reduce.p;
515 CoolPropDbl rhomolarc = EOS.reduce.rhomolar;
516 CoolPropDbl acentric = EOS.acentric;
517 CoolPropDbl R = 8.3144598;
518
519 // Remove the residual part
520 EOS.alphar.empty_the_EOS();
521 // Set the Xiang & Deiters contribution
522 EOS.alphar.XiangDeiters = ResidualHelmholtzXiangDeiters(Tc, pc, rhomolarc, acentric, R);
523 } else {
524 throw ValueError(format("EOS name [%s] is invalid; valid options are SRK, Peng-Robinson, XiangDeiters", EOS_name.c_str()));
525 }
526 } else {
527 throw ValueError(format("Index [%d] is invalid", i));
528 }
529 // Now do the same thing to the saturated liquid and vapor instances if possible
530 if (this->SatL) SatL->change_EOS(i, EOS_name);
531 if (this->SatV) SatV->change_EOS(i, EOS_name);
532}
534 // Clear the phase envelope data
536 // Build the phase envelope
537 PhaseEnvelopeRoutines::build(*this, type);
538 // Finalize the phase envelope
540};
542 // Build the matrix of binary-pair reducing functions
544}
546 CoolPropFluid& component = components[0];
547 EquationOfState& EOS = component.EOSVector[0];
548
549 // Clear the state class
550 clear();
551
552 // Calculate the new enthalpy and entropy values
554 EOS.hs_anchor.hmolar = hmolar();
555 EOS.hs_anchor.smolar = smolar();
556
557 // Calculate the new enthalpy and entropy values at the reducing state
559 EOS.reduce.hmolar = hmolar();
560 EOS.reduce.smolar = smolar();
561
562 // Clear again just to be sure
563 clear();
564}
567 if (!state.compare("hs_anchor")) {
568 return components[0].EOS().hs_anchor;
569 } else if (!state.compare("max_sat_T")) {
570 return components[0].EOS().max_sat_T;
571 } else if (!state.compare("max_sat_p")) {
572 return components[0].EOS().max_sat_p;
573 } else if (!state.compare("reducing")) {
574 return components[0].EOS().reduce;
575 } else if (!state.compare("critical")) {
576 return components[0].crit;
577 } else if (!state.compare("triple_liquid")) {
578 return components[0].triple_liquid;
579 } else if (!state.compare("triple_vapor")) {
580 return components[0].triple_vapor;
581 } else {
582 throw ValueError(format("This state [%s] is invalid to calc_state", state.c_str()));
583 }
584 } else {
585 if (!state.compare("critical")) {
586 return _critical;
587 } else {
588 throw ValueError(format("calc_state not supported for mixtures"));
589 }
590 }
591};
594 return components[0].EOS().acentric;
595 } else {
596 throw ValueError("acentric factor cannot be calculated for mixtures");
597 }
598}
601 return components[0].gas_constant();
602 } else {
603 if (get_config_bool(NORMALIZE_GAS_CONSTANTS)) {
604 return get_config_double(R_U_CODATA);
605 } else {
606 // mass fraction weighted average of the components
607 double summer = 0;
608 for (unsigned int i = 0; i < components.size(); ++i) {
609 summer += mole_fractions[i] * components[i].gas_constant();
610 }
611 return summer;
612 }
613 }
614}
616 double summer = 0;
617 for (unsigned int i = 0; i < components.size(); ++i) {
618 summer += mole_fractions[i] * components[i].molar_mass();
619 }
620 return summer;
621}
623 if (is_pure()) {
624 const double mm = molar_mass();
625 return {mm, mm};
626 }
627 const std::vector<CoolPropDbl> x = mole_fractions_liquid();
628 const std::vector<CoolPropDbl> y = mole_fractions_vapor();
629 if (x.size() != components.size() || y.size() != components.size()) {
630 throw ValueError("phase composition vectors do not match component count");
631 }
632 double MM_l = 0;
633 double MM_v = 0;
634 for (std::size_t i = 0; i < components.size(); ++i) {
635 const double mm_i = components[i].molar_mass();
636 MM_l += static_cast<double>(x[i]) * mm_i;
637 MM_v += static_cast<double>(y[i]) * mm_i;
638 }
639 return {MM_l, MM_v};
640}
643 if (param == iP && given == iT) {
644 // p = f(T), direct evaluation
645 switch (Q) {
646 case 0:
647 return components[0].ancillaries.pL.evaluate(value);
648 case 1:
649 return components[0].ancillaries.pV.evaluate(value);
650 default:
651 break; // falls through to the "Q invalid" throw below
652 }
653 } else if (param == iT && given == iP) {
654 // T = f(p), inverse evaluation
655 switch (Q) {
656 case 0:
657 return components[0].ancillaries.pL.invert(value);
658 case 1:
659 return components[0].ancillaries.pV.invert(value);
660 default:
661 break; // falls through to the "Q invalid" throw below
662 }
663 } else if (param == iDmolar && given == iT) {
664 // rho = f(T), inverse evaluation
665 switch (Q) {
666 case 0:
667 return components[0].ancillaries.rhoL.evaluate(value);
668 case 1:
669 return components[0].ancillaries.rhoV.evaluate(value);
670 default:
671 break; // falls through to the "Q invalid" throw below
672 }
673 } else if (param == iT && given == iDmolar) {
674 // T = f(rho), inverse evaluation
675 switch (Q) {
676 case 0:
677 return components[0].ancillaries.rhoL.invert(value);
678 case 1:
679 return components[0].ancillaries.rhoV.invert(value);
680 default:
681 break; // falls through to the "Q invalid" throw below
682 }
683 } else if (param == isurface_tension && given == iT) {
684 return components[0].ancillaries.surface_tension.evaluate(value);
685 } else {
686 throw ValueError(format("calc of %s given %s is invalid in calc_saturation_ancillary", get_parameter_information(param, "short").c_str(),
687 get_parameter_information(given, "short").c_str()));
688 }
689
690 throw ValueError(format("Q [%d] is invalid in calc_saturation_ancillary", Q));
691 } else {
692 throw NotImplementedError(format("calc_saturation_ancillary not implemented for mixtures"));
693 }
694}
695
698 return components[0].ancillaries.melting_line.evaluate(param, given, value);
699 } else {
700 throw NotImplementedError(format("calc_melting_line not implemented for mixtures"));
701 }
702}
705 if ((_phase == iphase_twophase) || (_phase == iphase_critical_point)) { // if within the two phase region or at critical point
706 return components[0].ancillaries.surface_tension.evaluate(T()); // calculate surface tension and return
707 } else { // else state point not in the two phase region
708 throw ValueError(format("surface tension is only defined within the two-phase region; Try PQ or QT inputs")); // throw error
709 }
710 } else {
711 throw NotImplementedError(format("surface tension not implemented for mixtures"));
712 }
713}
716 CoolPropDbl eta_dilute = NAN;
717 switch (components[0].transport.viscosity_dilute.type) {
720 break;
723 break;
726 break;
729 break;
732 break;
735 break;
738 break;
741 break;
742 default:
743 throw ValueError(
744 format("dilute viscosity type [%d] is invalid for fluid %s", components[0].transport.viscosity_dilute.type, name().c_str()));
745 }
746 return eta_dilute;
747 } else {
748 throw NotImplementedError(format("dilute viscosity not implemented for mixtures"));
749 }
750}
752 CoolPropDbl eta_dilute = calc_viscosity_dilute(), initial_density = 0, residual = 0;
753 return calc_viscosity_background(eta_dilute, initial_density, residual);
754}
756
757 switch (components[0].transport.viscosity_initial.type) {
760 CoolPropDbl rho = rhomolar();
761 initial_density = eta_dilute * B_eta_initial * rho; //TODO: Check units once AMTG
762 break;
763 }
766 break;
767 }
769 break;
770 }
771 }
772
773 // Higher order terms
774 switch (components[0].transport.viscosity_higher_order.type) {
777 break;
780 break;
783 break;
786 break;
789 break;
792 break;
795 break;
798 break;
801 break;
802 default:
803 throw ValueError(
804 format("higher order viscosity type [%d] is invalid for fluid %s", components[0].transport.viscosity_dilute.type, name().c_str()));
805 }
806
807 return initial_density + residual;
808}
809
812 CoolPropDbl dilute = 0, initial_density = 0, residual = 0, critical = 0;
813 calc_viscosity_contributions(dilute, initial_density, residual, critical);
814 return dilute + initial_density + residual + critical;
815 } else {
816 set_warning_string("Mixture model for viscosity is highly approximate");
817 CoolPropDbl summer = 0;
818 for (std::size_t i = 0; i < mole_fractions.size(); ++i) {
819 shared_ptr<HelmholtzEOSBackend> HEOS = std::make_shared<HelmholtzEOSBackend>(components[i]);
820 HEOS->update(DmolarT_INPUTS, _rhomolar, _T);
821 summer += mole_fractions[i] * log(HEOS->viscosity());
822 }
823 return exp(summer);
824 }
825}
827 CoolPropDbl& critical) {
829 // Reset the variables
830 dilute = 0;
831 initial_density = 0;
832 residual = 0;
833 critical = 0;
834
835 // Get a reference for code cleanness
836 CoolPropFluid& component = components[0];
837
838 if (!component.transport.viscosity_model_provided) {
839 throw ValueError(format("Viscosity model is not available for this fluid"));
840 }
841
842 // Check if using ECS
843 if (component.transport.viscosity_using_ECS) {
844 // Build the reference fluid once and cache it — viscosity_ECS resets
845 // its state on every call (conformal_state_solver + update_DmolarT_direct),
846 // so reusing the backend is safe and avoids deep-copying the entire
847 // reference-fluid EOS on every viscosity evaluation.
849 std::string fluid_name = component.transport.viscosity_ecs.reference_fluid;
850 std::vector<std::string> names(1, fluid_name);
851 viscosity_ecs_reference_state = std::make_shared<HelmholtzEOSMixtureBackend>(names);
852 }
853 // Get the viscosity using ECS and stick in the critical value
855 return;
856 }
857
858 // Check if using Chung model
859 if (component.transport.viscosity_using_Chung) {
860 // Get the viscosity using ECS and stick in the critical value
861 critical = TransportRoutines::viscosity_Chung(*this);
862 return;
863 }
864
865 // Check if using rho*sr model
866 if (component.transport.viscosity_using_rhosr) {
867 // Get the viscosity using rho*sr model and stick in the critical value
868 critical = TransportRoutines::viscosity_rhosr(*this);
869 return;
870 }
871
873 // Evaluate hardcoded model and stick in the critical value
874 switch (component.transport.hardcoded_viscosity) {
877 break;
880 break;
883 break;
886 break;
889 break;
892 break;
895 break;
898 break;
899 default:
900 throw ValueError(
901 format("hardcoded viscosity type [%d] is invalid for fluid %s", component.transport.hardcoded_viscosity, name().c_str()));
902 }
903 return;
904 }
905
906 // -------------------------
907 // Normal evaluation
908 // -------------------------
909
910 // Dilute part
911 dilute = calc_viscosity_dilute();
912
913 // Background viscosity given by the sum of the initial density dependence and higher order terms
914 calc_viscosity_background(dilute, initial_density, residual);
915
916 // Critical part (no fluids have critical enhancement for viscosity currently)
917 critical = 0;
918 } else {
919 throw ValueError("calc_viscosity_contributions invalid for mixtures");
920 }
921}
923 CoolPropDbl& critical) {
925 // Reset the variables
926 dilute = 0;
927 initial_density = 0;
928 residual = 0;
929 critical = 0;
930
931 // Get a reference for code cleanness
932 CoolPropFluid& component = components[0];
933
934 if (!component.transport.conductivity_model_provided) {
935 throw ValueError(format("Thermal conductivity model is not available for this fluid"));
936 }
937
938 // Check if using ECS
939 if (component.transport.conductivity_using_ECS) {
940 // Build the reference fluid once and cache it — conductivity_ECS resets
941 // its state on every call, so reusing the backend is safe and avoids
942 // deep-copying the entire reference-fluid EOS on every conductivity
943 // evaluation (the dominant cost of SVDSBTL surface builds for ECS fluids).
945 std::string fluid_name = component.transport.conductivity_ecs.reference_fluid;
946 std::vector<std::string> name(1, fluid_name);
947 conductivity_ecs_reference_state = std::make_shared<HelmholtzEOSMixtureBackend>(name);
948 }
949 // Get the conductivity using ECS and store in initial_density (not normally used);
950 initial_density = TransportRoutines::conductivity_ECS(*this, *conductivity_ecs_reference_state); // Warning: not actually initial_density
951 return;
952 }
953
955 // Evaluate hardcoded model and deposit in initial_density variable
956 // Warning: not actually initial_density
957 switch (component.transport.hardcoded_conductivity) {
960 break;
963 break;
965 initial_density = TransportRoutines::conductivity_hardcoded_R23(*this);
966 break;
969 break;
972 break;
973 default:
974 throw ValueError(format("hardcoded conductivity type [%d] is invalid for fluid %s",
975 components[0].transport.hardcoded_conductivity, name().c_str()));
976 }
977 return;
978 }
979
980 // -------------------------
981 // Normal evaluation
982 // -------------------------
983
984 // Dilute part
985 switch (component.transport.conductivity_dilute.type) {
988 break;
991 break;
994 break;
997 break;
1000 break;
1002 dilute = 0.0;
1003 break;
1004 default:
1005 throw ValueError(
1006 format("dilute conductivity type [%d] is invalid for fluid %s", components[0].transport.conductivity_dilute.type, name().c_str()));
1007 }
1008
1009 // Residual part
1010 residual = calc_conductivity_background();
1011
1012 // Critical part
1013 switch (component.transport.conductivity_critical.type) {
1016 break;
1019 break;
1022 break;
1024 critical = 0.0;
1025 break;
1028 break;
1029 default:
1030 throw ValueError(
1031 format("critical conductivity type [%d] is invalid for fluid %s", components[0].transport.viscosity_dilute.type, name().c_str()));
1032 }
1033 } else {
1034 throw ValueError("calc_conductivity_contributions invalid for mixtures");
1035 }
1036};
1037
1039 // Residual part
1040 CoolPropDbl lambda_residual = _HUGE;
1041 switch (components[0].transport.conductivity_residual.type) {
1044 break;
1047 break;
1048 default:
1049 throw ValueError(
1050 format("residual conductivity type [%d] is invalid for fluid %s", components[0].transport.conductivity_residual.type, name().c_str()));
1051 }
1052 return lambda_residual;
1053}
1056 CoolPropDbl dilute = 0, initial_density = 0, residual = 0, critical = 0;
1057 calc_conductivity_contributions(dilute, initial_density, residual, critical);
1058 return dilute + initial_density + residual + critical;
1059 } else {
1060 set_warning_string("Mixture model for conductivity is highly approximate");
1061 CoolPropDbl summer = 0;
1062 for (std::size_t i = 0; i < mole_fractions.size(); ++i) {
1063 shared_ptr<HelmholtzEOSBackend> HEOS = std::make_shared<HelmholtzEOSBackend>(components[i]);
1064 HEOS->update(DmolarT_INPUTS, _rhomolar, _T);
1065 summer += mole_fractions[i] * HEOS->conductivity();
1066 }
1067 return summer;
1068 }
1069}
1070void HelmholtzEOSMixtureBackend::calc_conformal_state(const std::string& reference_fluid, CoolPropDbl& T, CoolPropDbl& rhomolar) {
1071 // Construct as the derived HelmholtzEOSBackend, then implicit upcast
1072 // to the shared_ptr<HelmholtzEOSMixtureBackend> base. Single allocation;
1073 // virtual destructor on the base ensures correct cleanup.
1074 shared_ptr<CoolProp::HelmholtzEOSMixtureBackend> REF = std::make_shared<CoolProp::HelmholtzEOSBackend>(reference_fluid);
1075
1076 if (T < 0 && rhomolar < 0) {
1077 // Collect some parameters
1078 CoolPropDbl Tc = T_critical(), Tc0 = REF->T_critical(), rhocmolar = rhomolar_critical(), rhocmolar0 = REF->rhomolar_critical();
1079
1080 // Starting guess values for shape factors
1081 CoolPropDbl theta = 1;
1082 CoolPropDbl phi = 1;
1083
1084 // The equivalent substance reducing ratios
1085 CoolPropDbl f = Tc / Tc0 * theta;
1086 CoolPropDbl h = rhocmolar0 / rhocmolar * phi; // Must be the ratio of MOLAR densities!!
1087
1088 // Starting guesses for conformal state
1089 T = this->T() / f;
1090 rhomolar = this->rhomolar() * h;
1091 }
1092
1094}
1096 double summer = 0;
1097 for (unsigned int i = 0; i < components.size(); ++i) {
1098 summer += mole_fractions[i] * components[i].EOS().Ttriple;
1099 }
1100 return summer;
1101}
1103 double summer = 0;
1104 for (unsigned int i = 0; i < components.size(); ++i) {
1105 summer += mole_fractions[i] * components[i].EOS().ptriple;
1106 }
1107 return summer;
1108}
1110 if (components.size() != 1) {
1111 throw ValueError(format("calc_name is only valid for pure and pseudo-pure fluids, %d components", components.size()));
1112 } else {
1113 return components[0].name;
1114 }
1115}
1116
1118 if ((key == iDmolar) && _rhoLmolar) return _rhoLmolar;
1119 if (!SatL) throw ValueError("The saturated liquid state has not been set.");
1120 return SatL->keyed_output(key);
1121}
1123 if ((key == iDmolar) && _rhoVmolar) return _rhoVmolar;
1124 if (!SatV) throw ValueError("The saturated vapor state has not been set.");
1125 return SatV->keyed_output(key);
1126}
1127
1128void HelmholtzEOSMixtureBackend::calc_ideal_curve(const std::string& type, std::vector<double>& T, std::vector<double>& p) {
1129 if (type == "Joule-Thomson") {
1130 JouleThomsonCurveTracer JTCT(this, 1e5, 800);
1131 JTCT.trace(T, p);
1132 } else if (type == "Joule-Inversion") {
1133 JouleInversionCurveTracer JICT(this, 1e5, 800);
1134 JICT.trace(T, p);
1135 } else if (type == "Ideal") {
1136 IdealCurveTracer ICT(this, 1e5, 800);
1137 ICT.trace(T, p);
1138 } else if (type == "Boyle") {
1139 BoyleCurveTracer BCT(this, 1e5, 800);
1140 BCT.trace(T, p);
1141 } else {
1142 throw ValueError(format("Invalid ideal curve type: %s", type.c_str()));
1143 }
1144};
1146 std::vector<std::string> out;
1147 out.reserve(components.size());
1148 for (auto& component : components) {
1149 out.push_back(component.name);
1150 }
1151 return out;
1152}
1154 if (components.size() != 1) {
1155 throw ValueError(format("For now, calc_ODP is only valid for pure and pseudo-pure fluids, %d components", components.size()));
1156 } else {
1157 CoolPropDbl v = components[0].environment.ODP;
1158 if (!ValidNumber(v) || v < 0) {
1159 throw ValueError(format("ODP value is not specified or invalid"));
1160 }
1161 return v;
1162 }
1163}
1165 if (components.size() != 1) {
1166 throw ValueError(format("For now, calc_GWP20 is only valid for pure and pseudo-pure fluids, %d components", components.size()));
1167 } else {
1168 CoolPropDbl v = components[0].environment.GWP20;
1169 if (!ValidNumber(v) || v < 0) {
1170 throw ValueError(format("GWP20 value is not specified or invalid"));
1171 }
1172 return v;
1173 }
1174}
1176 if (components.size() != 1) {
1177 throw ValueError(format("For now, calc_GWP100 is only valid for pure and pseudo-pure fluids, %d components", components.size()));
1178 } else {
1179 CoolPropDbl v = components[0].environment.GWP100;
1180 if (!ValidNumber(v) || v < 0) {
1181 throw ValueError(format("GWP100 value is not specified or invalid"));
1182 }
1183 return v;
1184 }
1185}
1187 if (components.size() != 1) {
1188 throw ValueError(format("For now, calc_GWP500 is only valid for pure and pseudo-pure fluids, %d components", components.size()));
1189 } else {
1190 CoolPropDbl v = components[0].environment.GWP500;
1191 if (!ValidNumber(v) || v < 0) {
1192 throw ValueError(format("GWP500 value is not specified or invalid"));
1193 }
1194 return v;
1195 }
1196}
1198 if (components.size() != 1) {
1199 std::vector<CriticalState> critpts = calc_all_critical_points();
1200 if (critpts.size() == 1) {
1201 //if (!critpts[0].stable){ throw ValueError(format("found one critical point but critical point is not stable")); }
1202 return critpts[0].T;
1203 } else {
1204 throw ValueError(format("critical point finding routine found %d critical points", static_cast<int>(critpts.size())));
1205 }
1206 } else {
1207 if (get_config_bool(ENABLE_SUPERANCILLARIES) && is_pure()) {
1208 auto superanc_ptr = get_superanc();
1209 if (superanc_ptr) {
1210 return superanc_ptr->get_Tcrit_num(); // from the numerical critical point satisfying dp/drho|T = d2p/drho2|T = 0
1211 }
1212 }
1213 return components[0].crit.T;
1214 }
1215}
1217 if (components.size() != 1) {
1218 std::vector<CriticalState> critpts = calc_all_critical_points();
1219 if (critpts.size() == 1) {
1220 //if (!critpts[0].stable){ throw ValueError(format("found one critical point but critical point is not stable")); }
1221 return critpts[0].p;
1222 } else {
1223 throw ValueError(format("critical point finding routine found %d critical points", static_cast<int>(critpts.size())));
1224 }
1225 } else {
1226 if (get_config_bool(ENABLE_SUPERANCILLARIES) && is_pure()) {
1227 auto superanc_ptr = get_superanc();
1228 if (superanc_ptr) {
1229 return superanc_ptr->get_pmax(); // from the numerical critical point satisfying dp/drho|T = d2p/drho2|T = 0
1230 }
1231 }
1232 return components[0].crit.p;
1233 }
1234}
1236 if (components.size() != 1) {
1237 std::vector<CriticalState> critpts = calc_all_critical_points();
1238 if (critpts.size() == 1) {
1239 //if (!critpts[0].stable){ throw ValueError(format("found one critical point but critical point is not stable")); }
1240 return critpts[0].rhomolar;
1241 } else {
1242 throw ValueError(format("critical point finding routine found %d critical points", static_cast<int>(critpts.size())));
1243 }
1244 } else {
1245 if (get_config_bool(ENABLE_SUPERANCILLARIES) && is_pure()) {
1246 auto superanc_ptr = get_superanc();
1247 if (superanc_ptr) {
1248 return superanc_ptr->get_rhocrit_num(); // from the numerical critical point satisfying dp/drho|T = d2p/drho2|T = 0
1249 }
1250 }
1251 return components[0].crit.rhomolar;
1252 }
1253}
1256 if (components[0].EOS().pseudo_pure) {
1257 return components[0].EOS().max_sat_p.p;
1258 } else {
1259 return p_critical();
1260 }
1261 } else {
1262 throw ValueError("calc_pmax_sat not yet defined for mixtures");
1263 }
1264}
1267 if (components[0].EOS().pseudo_pure) {
1268 double Tmax_sat = components[0].EOS().max_sat_T.T;
1269 if (!ValidNumber(Tmax_sat)) {
1270 return T_critical();
1271 } else {
1272 return Tmax_sat;
1273 }
1274 } else {
1275 return T_critical();
1276 }
1277 } else {
1278 throw ValueError("calc_Tmax_sat not yet defined for mixtures");
1279 }
1280}
1281
1284 Tmin_satL = components[0].EOS().sat_min_liquid.T;
1285 Tmin_satV = components[0].EOS().sat_min_vapor.T;
1286 return;
1287 } else {
1288 throw ValueError("calc_Tmin_sat not yet defined for mixtures");
1289 }
1290}
1291
1294 pmin_satL = components[0].EOS().sat_min_liquid.p;
1295 pmin_satV = components[0].EOS().sat_min_vapor.p;
1296 return;
1297 } else {
1298 throw ValueError("calc_pmin_sat not yet defined for mixtures");
1299 }
1300}
1301
1302// Minimum allowed saturation temperature the maximum of the saturation temperatures of liquid and vapor
1303// For pure fluids, both values are the same, for pseudo-pure they are probably the same, for mixtures they are definitely not the same
1304
1306 double summer = 0;
1307 for (unsigned int i = 0; i < components.size(); ++i) {
1308 summer += mole_fractions[i] * components[i].EOS().limits.Tmax;
1309 }
1310 return summer;
1311}
1313 double summer = 0;
1314 for (unsigned int i = 0; i < components.size(); ++i) {
1315 summer += mole_fractions[i] * components[i].EOS().limits.Tmin;
1316 }
1317 return summer;
1318}
1320 double summer = 0;
1321 for (unsigned int i = 0; i < components.size(); ++i) {
1322 summer += mole_fractions[i] * components[i].EOS().limits.pmax;
1323 }
1324 return summer;
1325}
1326
1328 clear();
1329 _Q = Q;
1330 _T = T;
1331 if (!is_pure()) {
1332 throw ValueError(format("Is not a pure fluid"));
1333 }
1334 if (!get_config_bool(ENABLE_SUPERANCILLARIES)) {
1335 throw ValueError(format("Superancillaries are not enabled"));
1336 }
1337 auto superanc_ptr = get_superanc();
1338 if (!superanc_ptr) {
1339 throw ValueError(format("Superancillaries not available for this fluid"));
1340 }
1341
1342 auto& superanc = *superanc_ptr;
1343 CoolPropDbl Tcrit_num = superanc.get_Tcrit_num();
1344 if (T > Tcrit_num) {
1345 throw ValueError(format("Temperature to QT_flash [%0.8Lg K] may not be above the numerical critical point of %0.15Lg K", T, Tcrit_num));
1346 }
1347 auto rhoL = superanc.eval_sat(T, 'D', 0);
1348 auto rhoV = superanc.eval_sat(T, 'D', 1);
1349 auto p = superanc.eval_sat(T, 'P', 1);
1350 SatL->update_TDmolarP_unchecked(T, rhoL, p);
1351 SatV->update_TDmolarP_unchecked(T, rhoV, p);
1352 _p = p;
1353 _rhomolar = 1 / (Q / rhoV + (1 - Q) / rhoL);
1355
1356 post_update(false /*optional_checks*/);
1357}
1358
1360
1361 clear();
1362
1364 _T = T;
1365 _p = p;
1366
1367 // Cleanup
1368 bool optional_checks = false;
1369 post_update(optional_checks);
1370}
1371
1373 // TODO: This is just a quick fix for #878 - should be done more systematically
1374 const CoolPropDbl rhomolar_min = 0;
1375 const CoolPropDbl T_min = 0;
1376
1377 if (rhomolar < rhomolar_min) {
1378 throw ValueError(format("The molar density of %f mol/m3 is below the minimum of %f mol/m3", rhomolar, rhomolar_min));
1379 }
1380
1381 if (T < T_min) {
1382 throw ValueError(format("The temperature of %f K is below the minimum of %f K", T, T_min));
1383 }
1384
1386 // Set up the state
1387 pre_update(pair, rhomolar, T);
1388
1390 _T = T;
1391 _p = calc_pressure();
1392
1393 // Cleanup
1394 bool optional_checks = false;
1395 post_update(optional_checks);
1396}
1397
1400 // Set up the state
1401 pre_update(pair, hmolar, Q);
1402
1403 _hmolar = hmolar;
1404 _Q = Q;
1405 FlashRoutines::HQ_flash(*this, Tguess);
1406
1407 // Cleanup
1408 post_update();
1409}
1411 this->_hmolar = HEOS.hmolar();
1412 this->_smolar = HEOS.smolar();
1413 this->_T = HEOS.T();
1414 this->_umolar = HEOS.umolar();
1415 this->_p = HEOS.p();
1416 this->_rhomolar = HEOS.rhomolar();
1417 this->_Q = HEOS.Q();
1418 this->_phase = HEOS.phase();
1419
1420 // Copy the derivatives as well
1421}
1424 // Set up the state
1425 pre_update(pair, p, T);
1426
1427 // Do the flash call to find rho = f(T,p)
1428 CoolPropDbl rhomolar = solver_rho_Tp(T, p, rhomolar_guess);
1429
1430 // Update the class with the new calculated density
1432
1433 // Skip the cleanup, already done in update_DmolarT_direct
1434}
1435
1437 // Clear the state
1438 clear();
1439
1440 if (is_pure_or_pseudopure == false && mole_fractions.size() == 0) {
1441 throw ValueError("Mole fractions must be set");
1442 }
1443
1444 // If the inputs are in mass units, convert them to molar units
1445 mass_to_molar_inputs(input_pair, value1, value2);
1446
1447 // Set the mole-fraction weighted gas constant for the mixture
1448 // (or the pure/pseudo-pure fluid) if it hasn't been set yet
1449 gas_constant();
1450
1451 // Calculate and cache the reducing state
1453}
1454
1455void HelmholtzEOSMixtureBackend::update(CoolProp::input_pairs input_pair, double value1, double value2) {
1456 // Mass-quality input pair on a true mixture: solve iteratively for Qmolar
1457 // before delegating to the molar-pair flash. Pure / pseudo-pure go through
1458 // mass_to_molar_inputs in the existing flow (handled below).
1459 if (CoolProp::is_Qmass_pair(input_pair) && !is_pure()) {
1460 update_Qmass_pair(input_pair, value1, value2);
1461 return;
1462 }
1463
1464 if (get_debug_level() > 10) {
1465 std::cout << format("%s (%d): update called with (%d: (%s), %g, %g)", __FILE__, __LINE__, input_pair,
1466 get_input_pair_short_desc(input_pair).c_str(), value1, value2)
1467 << '\n';
1468 }
1469
1470 CoolPropDbl ld_value1 = value1, ld_value2 = value2;
1471 pre_update(input_pair, ld_value1, ld_value2);
1472 value1 = ld_value1;
1473 value2 = ld_value2;
1474
1475 switch (input_pair) {
1476 case PT_INPUTS:
1477 _p = value1;
1478 _T = value2;
1480 break;
1481 case DmolarT_INPUTS:
1482 _rhomolar = value1;
1483 _T = value2;
1485 break;
1486 case SmolarT_INPUTS:
1487 _smolar = value1;
1488 _T = value2;
1490 break;
1491 case HmolarT_INPUTS:
1492 _hmolar = value1;
1493 _T = value2;
1495 break;
1496 case TUmolar_INPUTS:
1497 _T = value1;
1498 _umolar = value2;
1500 break;
1501 case DmolarP_INPUTS:
1502 _rhomolar = value1;
1503 _p = value2;
1505 break;
1507 _rhomolar = value1;
1508 _hmolar = value2;
1510 break;
1512 _rhomolar = value1;
1513 _smolar = value2;
1515 break;
1517 _rhomolar = value1;
1518 _umolar = value2;
1520 break;
1521 case HmolarP_INPUTS:
1522 _hmolar = value1;
1523 _p = value2;
1525 break;
1526 case PSmolar_INPUTS:
1527 _p = value1;
1528 _smolar = value2;
1530 break;
1531 case PUmolar_INPUTS:
1532 _p = value1;
1533 _umolar = value2;
1535 break;
1537 _hmolar = value1;
1538 _smolar = value2;
1540 break;
1541 // Validate quality BEFORE assigning to _Q so a thrown exception
1542 // does not leave the cached state half-mutated. Without this, a
1543 // subsequent hmass() / smass() / etc. would use the stale SatL/
1544 // SatV pointers from a prior valid update plus the new bad _Q
1545 // and return a meaningless extrapolated value (#2195).
1546 case QT_INPUTS:
1547 if ((value1 < 0) || (value1 > 1)) throw CoolProp::OutOfRangeError("Input vapor quality [Q] must be between 0 and 1");
1548 _Q = value1;
1549 _T = value2;
1551 break;
1552 case PQ_INPUTS:
1553 if ((value2 < 0) || (value2 > 1)) throw CoolProp::OutOfRangeError("Input vapor quality [Q] must be between 0 and 1");
1554 _p = value1;
1555 _Q = value2;
1557 break;
1558 case QSmolar_INPUTS:
1559 if ((value1 < 0) || (value1 > 1)) throw CoolProp::OutOfRangeError("Input vapor quality [Q] must be between 0 and 1");
1560 _Q = value1;
1561 _smolar = value2;
1563 break;
1564 case HmolarQ_INPUTS:
1565 if ((value2 < 0) || (value2 > 1)) throw CoolProp::OutOfRangeError("Input vapor quality [Q] must be between 0 and 1");
1566 _hmolar = value1;
1567 _Q = value2;
1569 break;
1570 case DmolarQ_INPUTS:
1571 if ((value2 < 0) || (value2 > 1)) throw CoolProp::OutOfRangeError("Input vapor quality [Q] must be between 0 and 1");
1572 _rhomolar = value1;
1573 _Q = value2;
1575 break;
1576 default:
1577 throw ValueError(format("This pair of inputs [%s] is not yet supported", get_input_pair_short_desc(input_pair).c_str()));
1578 }
1579
1580 post_update();
1581}
1582const std::vector<CoolPropDbl> HelmholtzEOSMixtureBackend::calc_mass_fractions() {
1583 // mass fraction is mass_i/total_mass;
1584 CoolPropDbl mm = molar_mass();
1585 std::vector<CoolPropDbl>& mole_fractions = get_mole_fractions_ref();
1586 std::vector<CoolPropDbl> mass_fractions(mole_fractions.size());
1587 for (std::size_t i = 0; i < mole_fractions.size(); ++i) {
1588 double mmi = get_fluid_constant(i, imolar_mass);
1589 mass_fractions[i] = mmi * (mole_fractions[i]) / mm;
1590 }
1591 return mass_fractions;
1592}
1593
1595 const GuessesStructure& guesses) {
1596 if (get_debug_level() > 10) {
1597 std::cout << format("%s (%d): update called with (%d: (%s), %g, %g)", __FILE__, __LINE__, input_pair,
1598 get_input_pair_short_desc(input_pair).c_str(), value1, value2)
1599 << '\n';
1600 }
1601
1602 CoolPropDbl ld_value1 = value1, ld_value2 = value2;
1603 pre_update(input_pair, ld_value1, ld_value2);
1604 value1 = ld_value1;
1605 value2 = ld_value2;
1606
1607 switch (input_pair) {
1608 case PQ_INPUTS:
1609 _p = value1;
1610 _Q = value2;
1612 break;
1613 case QT_INPUTS:
1614 _Q = value1;
1615 _T = value2;
1617 break;
1618 case PT_INPUTS:
1619 _p = value1;
1620 _T = value2;
1622 break;
1623 case DmolarQ_INPUTS:
1624 _rhomolar = value1;
1625 _Q = value2;
1626 if ((_Q < 0) || (_Q > 1)) throw CoolProp::OutOfRangeError("Input vapor quality [Q] must be between 0 and 1");
1628 break;
1629 case HmolarQ_INPUTS:
1630 _hmolar = value1;
1631 _Q = value2;
1632 if ((_Q < 0) || (_Q > 1)) throw CoolProp::OutOfRangeError("Input vapor quality [Q] must be between 0 and 1");
1634 break;
1635 case QSmolar_INPUTS:
1636 _Q = value1;
1637 _smolar = value2;
1638 if ((_Q < 0) || (_Q > 1)) throw CoolProp::OutOfRangeError("Input vapor quality [Q] must be between 0 and 1");
1640 break;
1641 default:
1642 throw ValueError(format("This pair of inputs [%s] is not yet supported", get_input_pair_short_desc(input_pair).c_str()));
1643 }
1644 post_update();
1645}
1646
1648 // Check the values that must always be set
1649 //if (_p < 0){ throw ValueError("p is less than zero");}
1650 if (!ValidNumber(_p)) {
1651 throw ValueError("p is not a valid number");
1652 }
1653 //if (_T < 0){ throw ValueError("T is less than zero");}
1654 if (!ValidNumber(_T)) {
1655 throw ValueError("T is not a valid number");
1656 }
1657 if (_rhomolar < 0) {
1658 throw ValueError("rhomolar is less than zero");
1659 }
1660 if (!ValidNumber(_rhomolar)) {
1661 throw ValueError("rhomolar is not a valid number");
1662 }
1663
1664 if (optional_checks) {
1665 if (!ValidNumber(_Q)) {
1666 throw ValueError("Q is not a valid number");
1667 }
1668 if (_phase == iphase_unknown) {
1669 throw ValueError("_phase is unknown");
1670 }
1671 }
1672
1673 // Set the reduced variables
1674 _tau = _reducing.T / _T;
1676
1677 // Update the terms in the excess contribution
1678 if (!is_pure_or_pseudopure) {
1679 residual_helmholtz->Excess.update(_tau, _delta);
1680 }
1681}
1682
1684 return 1 / rhomolar_reducing() * calc_alphar_deriv_nocache(0, 1, mole_fractions, _tau, 1e-12);
1685}
1688 CoolPropDbl dtau_dT = -red.T / pow(_T, 2);
1689 return 1 / red.rhomolar * calc_alphar_deriv_nocache(1, 1, mole_fractions, _tau, 1e-12) * dtau_dT;
1690}
1692 return 1 / pow(rhomolar_reducing(), 2) * calc_alphar_deriv_nocache(0, 2, mole_fractions, _tau, 1e-12);
1693}
1696 CoolPropDbl dtau_dT = -red.T / pow(_T, 2);
1697 return 1 / pow(red.rhomolar, 2) * calc_alphar_deriv_nocache(1, 2, mole_fractions, _tau, 1e-12) * dtau_dT;
1698}
1700 /*
1701 Determine the phase given p and one other state variable
1702 */
1703 saturation_called = false;
1704
1705 // Reference declaration to save indexing
1706 CoolPropFluid& component = components[0];
1707
1708 // Maximum saturation temperature - Equal to critical pressure for pure fluids
1709 CoolPropDbl psat_max = calc_pmax_sat();
1710
1711 double T_crit_ = T_critical(), rhomolar_crit_ = rhomolar_critical();
1712 auto smolar_critical = [this, &T_crit_, &rhomolar_crit_]() { return this->calc_smolar_nocache(T_crit_, rhomolar_crit_); };
1713 auto hmolar_critical = [this, &T_crit_, &rhomolar_crit_]() { return this->calc_hmolar_nocache(T_crit_, rhomolar_crit_); };
1714 auto umolar_critical = [this, &T_crit_, &rhomolar_crit_]() { return this->calc_umolar_nocache(T_crit_, rhomolar_crit_); };
1715
1716 // Check supercritical pressure
1717 if (_p > psat_max) {
1718 _Q = 1e9;
1719 switch (other) {
1720 case iT: {
1721 // Check for the presence of the melting line (skipped when the caller has
1722 // opted out of property-limit checks, mirroring the subcritical path below).
1723 // Also skip when _p is below the lower bound of the melting-line fit
1724 // (i.e. below the triple-point pressure); the fit is undefined there and
1725 // the fluid must be gas or gas+solid, never liquid.
1726 if (has_melting_line() && !get_config_bool(DONT_CHECK_PROPERTY_LIMITS) && _p > calc_melting_line(iP_min, -1, -1)) {
1727 double Tm = melting_line(iT, iP, _p);
1728 if (_T < Tm - 0.001) {
1729 throw ValueError(format("For now, we don't support T [%g K] below Tmelt(p) [%g K]", _T, Tm));
1730 }
1731 }
1732 if (_T > T_crit_) {
1734 return;
1735 } else {
1737 return;
1738 }
1739 }
1740 case iDmolar: {
1741 if (_rhomolar < rhomolar_crit_) {
1743 return;
1744 } else {
1746 return;
1747 }
1748 }
1749 case iSmolar: {
1750 if (_smolar.pt() > smolar_critical()) {
1752 return;
1753 } else {
1755 return;
1756 }
1757 }
1758 case iHmolar: {
1759 if (_hmolar.pt() > hmolar_critical()) {
1761 return;
1762 } else {
1764 return;
1765 }
1766 }
1767 case iUmolar: {
1768 if (_umolar.pt() > umolar_critical()) {
1770 return;
1771 } else {
1773 return;
1774 }
1775 }
1776 default: {
1777 throw ValueError("supercritical pressure but other invalid for now");
1778 }
1779 }
1780 }
1781 // Check between triple point pressure and psat_max
1782 else if (_p >= components[0].EOS().ptriple * 0.9999 && _p <= psat_max) {
1783
1784 // First try the superancillaries, use them to determine the state if you can
1785 if (get_config_bool(ENABLE_SUPERANCILLARIES) && is_pure()) {
1786 auto superanc_ptr = get_superanc();
1787 // Superancillaries are enabled and available, they will be used to determine the phase
1788 if (superanc_ptr) {
1789 auto& superanc = *superanc_ptr;
1790 CoolPropDbl pmax_num = superanc.get_pmax();
1791 if (_p > pmax_num) {
1792 throw ValueError(
1793 format("Pressure to PQ_flash [%0.8Lg Pa] may not be above the numerical critical point of %0.15Lg Pa", _p, pmax_num));
1794 }
1795 auto Tsat = superanc.get_T_from_p(_p);
1796 auto rhoL = superanc.eval_sat(Tsat, 'D', 0);
1797 auto rhoV = superanc.eval_sat(Tsat, 'D', 1);
1798 auto psat = _p;
1799
1800 if (other == iT) {
1801 if (value < Tsat - 100 * DBL_EPSILON) {
1802 if (has_melting_line() && !get_config_bool(DONT_CHECK_PROPERTY_LIMITS) && _p > calc_melting_line(iP_min, -1, -1)) {
1803 double Tm = melting_line(iT, iP, _p);
1804 if (_T < Tm - 0.001) {
1805 throw ValueError(format("For now, we don't support T [%g K] below Tmelt(p) [%g K]", _T, Tm));
1806 }
1807 }
1809 _Q = -1000;
1810 return;
1811 } else if (value > Tsat + 100 * DBL_EPSILON) {
1812 this->_phase = iphase_gas;
1813 _Q = 1000;
1814 return;
1815 } else {
1816 this->_phase = iphase_twophase;
1817 }
1818 }
1819 SatL->update_TDmolarP_unchecked(Tsat, rhoL, psat);
1820 SatV->update_TDmolarP_unchecked(Tsat, rhoV, psat);
1821 double Q = NAN;
1822 switch (other) {
1823 case iDmolar:
1824 Q = (1 / value - 1 / SatL->rhomolar()) / (1 / SatV->rhomolar() - 1 / SatL->rhomolar());
1825 break;
1826 case iSmolar:
1827 Q = (value - SatL->smolar()) / (SatV->smolar() - SatL->smolar());
1828 break;
1829 case iHmolar:
1830 Q = (value - SatL->hmolar()) / (SatV->hmolar() - SatL->hmolar());
1831 break;
1832 case iUmolar:
1833 Q = (value - SatL->umolar()) / (SatV->umolar() - SatL->umolar());
1834 break;
1835 default:
1836 throw ValueError(format("bad input for other"));
1837 }
1838 // Start off by setting variables based on assumption that the state is two-phase
1839 if (Q < -1e-9) {
1840 this->_phase = iphase_liquid;
1841 SatL->clear();
1842 SatV->clear();
1843 _Q = -1000;
1844 _TLanc = Tsat;
1845 } else if (Q > 1 + 1e-9) {
1846 this->_phase = iphase_gas;
1847 SatL->clear();
1848 SatV->clear();
1849 _Q = 1000;
1850 } else {
1851 _T = Tsat;
1852 _Q = Q;
1853 _rhomolar = 1 / (_Q / SatV->rhomolar() + (1 - _Q) / SatL->rhomolar());
1854 this->_phase = iphase_twophase;
1855 }
1856 return;
1857 }
1858 }
1859
1860 // Then fall back to normal ancillaries, use them to determine the state if you can
1861
1862 // Calculate dew and bubble temps from the ancillaries (everything needs them)
1863 _TLanc = components[0].ancillaries.pL.invert(_p);
1864 _TVanc = components[0].ancillaries.pV.invert(_p);
1865
1866 bool definitely_two_phase = false;
1867
1868 // Try using the ancillaries for P,H,S if they are there
1869 switch (other) {
1870 case iT: {
1871
1872 if (has_melting_line() && _p > calc_melting_line(iP_min, -1, -1)) {
1873 double Tm = melting_line(iT, iP, _p);
1874 if (get_config_bool(DONT_CHECK_PROPERTY_LIMITS)) {
1876 } else {
1877 if (_T < Tm - 0.001) {
1878 throw ValueError(format("For now, we don't support T [%g K] below Tmelt(p) [%g K]", _T, Tm));
1879 }
1880 }
1881 } else {
1882 if (get_config_bool(DONT_CHECK_PROPERTY_LIMITS)) {
1884 } else {
1885 if (_T < Tmin() - 0.001) {
1886 throw ValueError(format("For now, we don't support T [%g K] below Tmin(saturation) [%g K]", _T, Tmin()));
1887 }
1888 }
1889 }
1890
1891 CoolPropDbl T_vap = 0.1 + static_cast<double>(_TVanc);
1892 CoolPropDbl T_liq = -0.1 + static_cast<double>(_TLanc);
1893
1894 if (value > T_vap) {
1895 this->_phase = iphase_gas;
1896 _Q = -1000;
1897 return;
1898 } else if (value < T_liq) {
1899 this->_phase = iphase_liquid;
1900 _Q = 1000;
1901 return;
1902 }
1903 break;
1904 }
1905 case iHmolar: {
1906 if (!component.ancillaries.hL.enabled()) {
1907 break;
1908 }
1909 // Ancillaries are h-h_anchor, so add back h_anchor
1910 CoolPropDbl h_liq = component.ancillaries.hL.evaluate(_TLanc) + component.EOS().hs_anchor.hmolar;
1911 CoolPropDbl h_liq_error_band = component.ancillaries.hL.get_max_abs_error();
1912 CoolPropDbl h_vap = h_liq + component.ancillaries.hLV.evaluate(_TLanc);
1913 CoolPropDbl h_vap_error_band = h_liq_error_band + component.ancillaries.hLV.get_max_abs_error();
1914
1915 // HelmholtzEOSMixtureBackend HEOS(components);
1916 // HEOS.update(QT_INPUTS, 1, _TLanc);
1917 // double h1 = HEOS.hmolar();
1918 // HEOS.update(QT_INPUTS, 0, _TLanc);
1919 // double h0 = HEOS.hmolar();
1920
1921 // Check if in range given the accuracy of the fit
1922 if (value > h_vap + h_vap_error_band) {
1923 this->_phase = iphase_gas;
1924 _Q = -1000;
1925 return;
1926 } else if (value < h_liq - h_liq_error_band) {
1927 this->_phase = iphase_liquid;
1928 _Q = 1000;
1929 return;
1930 } else if (value > h_liq + h_liq_error_band && value < h_vap - h_vap_error_band) {
1931 definitely_two_phase = true;
1932 }
1933 break;
1934 }
1935 case iSmolar: {
1936 if (!component.ancillaries.sL.enabled()) {
1937 break;
1938 }
1939 // Ancillaries are s-s_anchor, so add back s_anchor
1940 CoolPropDbl s_anchor = component.EOS().hs_anchor.smolar;
1941 CoolPropDbl s_liq = component.ancillaries.sL.evaluate(_TLanc) + s_anchor;
1942 CoolPropDbl s_liq_error_band = component.ancillaries.sL.get_max_abs_error();
1943 CoolPropDbl s_vap = s_liq + component.ancillaries.sLV.evaluate(_TVanc);
1944 CoolPropDbl s_vap_error_band = s_liq_error_band + component.ancillaries.sLV.get_max_abs_error();
1945
1946 // Check if in range given the accuracy of the fit
1947 if (value > s_vap + s_vap_error_band) {
1948 this->_phase = iphase_gas;
1949 _Q = -1000;
1950 return;
1951 } else if (value < s_liq - s_liq_error_band) {
1952 this->_phase = iphase_liquid;
1953 _Q = 1000;
1954 return;
1955 } else if (value > s_liq + s_liq_error_band && value < s_vap - s_vap_error_band) {
1956 definitely_two_phase = true;
1957 }
1958 break;
1959 }
1960 case iUmolar: {
1961 if (!component.ancillaries.hL.enabled()) {
1962 break;
1963 }
1964 // u = h-p/rho
1965
1966 // Ancillaries are h-h_anchor, so add back h_anchor
1967 CoolPropDbl h_liq = component.ancillaries.hL.evaluate(_TLanc) + component.EOS().hs_anchor.hmolar;
1968 CoolPropDbl h_liq_error_band = component.ancillaries.hL.get_max_abs_error();
1969 CoolPropDbl h_vap = h_liq + component.ancillaries.hLV.evaluate(_TLanc);
1970 CoolPropDbl h_vap_error_band = h_liq_error_band + component.ancillaries.hLV.get_max_abs_error();
1971 CoolPropDbl rho_vap = component.ancillaries.rhoV.evaluate(_TVanc);
1972 CoolPropDbl rho_liq = component.ancillaries.rhoL.evaluate(_TLanc);
1973 CoolPropDbl u_liq = h_liq - _p / rho_liq;
1974 CoolPropDbl u_vap = h_vap - _p / rho_vap;
1975 CoolPropDbl u_liq_error_band = 1.5 * h_liq_error_band; // Most of error is in enthalpy
1976 CoolPropDbl u_vap_error_band = 1.5 * h_vap_error_band; // Most of error is in enthalpy
1977
1978 // Check if in range given the accuracy of the fit
1979 if (value > u_vap + u_vap_error_band) {
1980 this->_phase = iphase_gas;
1981 _Q = -1000;
1982 return;
1983 } else if (value < u_liq - u_liq_error_band) {
1984 this->_phase = iphase_liquid;
1985 _Q = 1000;
1986 return;
1987 } else if (value > u_liq + u_liq_error_band && value < u_vap - u_vap_error_band) {
1988 definitely_two_phase = true;
1989 }
1990 break;
1991 }
1992 default: {
1993 }
1994 }
1995
1996 // Now either density is an input, or an ancillary for h,s,u is missing
1997 // Always calculate the densities using the ancillaries
1998 if (!definitely_two_phase) {
2001 CoolPropDbl rho_vap = 0.95 * static_cast<double>(_rhoVanc);
2002 CoolPropDbl rho_liq = 1.05 * static_cast<double>(_rhoLanc);
2003 switch (other) {
2004 case iDmolar: {
2005 if (value < rho_vap) {
2006 this->_phase = iphase_gas;
2007 return;
2008 } else if (value > rho_liq) {
2009 this->_phase = iphase_liquid;
2010 return;
2011 }
2012 break;
2013 }
2014 default:
2015 break; // non-density inputs fall through to the full VLE path below
2016 }
2017 }
2018
2019 if (!is_pure_or_pseudopure) {
2020 throw ValueError("possibly two-phase inputs not supported for mixtures for now");
2021 }
2022
2023 // The slow full VLE calculation is required
2024 {
2025
2026 // Actually have to use saturation information sadly
2027 // For the given pressure, find the saturation state
2028 // Run the saturation routines to determine the saturation densities and pressures
2030 HEOS._p = this->_p;
2031 HEOS._Q = 0; // ?? What is the best to do here? Doesn't matter for our purposes since pure fluid
2033
2034 // We called the saturation routines, so HEOS.SatL and HEOS.SatV are now updated
2035 // with the saturated liquid and vapor values, which can therefore be used in
2036 // the other solvers
2037 saturation_called = true;
2038
2039 CoolPropDbl Q = NAN;
2040
2041 if (other == iT) {
2042 if (value < HEOS.SatL->T() - 100 * DBL_EPSILON) {
2043 this->_phase = iphase_liquid;
2044 _Q = -1000;
2045 return;
2046 } else if (value > HEOS.SatV->T() + 100 * DBL_EPSILON) {
2047 this->_phase = iphase_gas;
2048 _Q = 1000;
2049 return;
2050 } else {
2051 this->_phase = iphase_twophase;
2052 }
2053 }
2054 switch (other) {
2055 case iDmolar:
2056 Q = (1 / value - 1 / HEOS.SatL->rhomolar()) / (1 / HEOS.SatV->rhomolar() - 1 / HEOS.SatL->rhomolar());
2057 break;
2058 case iSmolar:
2059 Q = (value - HEOS.SatL->smolar()) / (HEOS.SatV->smolar() - HEOS.SatL->smolar());
2060 break;
2061 case iHmolar:
2062 Q = (value - HEOS.SatL->hmolar()) / (HEOS.SatV->hmolar() - HEOS.SatL->hmolar());
2063 break;
2064 case iUmolar:
2065 Q = (value - HEOS.SatL->umolar()) / (HEOS.SatV->umolar() - HEOS.SatL->umolar());
2066 break;
2067 default:
2068 throw ValueError(format("bad input for other"));
2069 }
2070 // TODO: Check the speed penalty of these calls
2071 // Update the states
2072 if (this->SatL) this->SatL->update(DmolarT_INPUTS, HEOS.SatL->rhomolar(), HEOS.SatL->T());
2073 if (this->SatV) this->SatV->update(DmolarT_INPUTS, HEOS.SatV->rhomolar(), HEOS.SatV->T());
2074 // Update the two-Phase variables
2075 _rhoLmolar = HEOS.SatL->rhomolar();
2076 _rhoVmolar = HEOS.SatV->rhomolar();
2077
2078 //
2079 if (Q < -1e-9) {
2080 this->_phase = iphase_liquid;
2081 _Q = -1000;
2082 return;
2083 } else if (Q > 1 + 1e-9) {
2084 this->_phase = iphase_gas;
2085 _Q = 1000;
2086 return;
2087 } else {
2088 this->_phase = iphase_twophase;
2089 }
2090
2091 _Q = Q;
2092 // Load the outputs
2093 _T = _Q * HEOS.SatV->T() + (1 - _Q) * HEOS.SatL->T();
2094 _rhomolar = 1 / (_Q / HEOS.SatV->rhomolar() + (1 - _Q) / HEOS.SatL->rhomolar());
2095 return;
2096 }
2097 } else if (_p < components[0].EOS().ptriple * 0.9999) {
2098 if (other == iT) {
2099 if (_T > std::max(Tmin(), Ttriple())) {
2101 } else {
2102 if (get_config_bool(DONT_CHECK_PROPERTY_LIMITS)) {
2104 } else {
2105 throw NotImplementedError(format("For now, we don't support p [%g Pa] below ptriple [%g Pa] when T [%g] is less than Tmin [%g]",
2106 _p, components[0].EOS().ptriple, _T, std::max(Tmin(), Ttriple())));
2107 }
2108 }
2109 } else {
2111 }
2112 } else {
2113 throw ValueError(format("The pressure [%g Pa] cannot be used in p_phase_determination", _p));
2114 }
2115}
2117 class Residual : public FuncWrapper1D
2118 {
2119 public:
2121 Residual(HelmholtzEOSMixtureBackend& HEOS) : HEOS(&HEOS) {};
2122 double call(double T) override {
2123 HEOS->update(QT_INPUTS, 1, T);
2124 // dTdp_along_sat
2125 double dTdp_along_sat =
2126 HEOS->T() * (1 / HEOS->SatV->rhomolar() - 1 / HEOS->SatL->rhomolar()) / (HEOS->SatV->hmolar() - HEOS->SatL->hmolar());
2127 // dsdT_along_sat;
2128 return HEOS->SatV->first_partial_deriv(iSmolar, iT, iP) + HEOS->SatV->first_partial_deriv(iSmolar, iP, iT) / dTdp_along_sat;
2129 }
2130 };
2132 shared_ptr<CoolProp::HelmholtzEOSMixtureBackend> HEOS_copy = std::make_shared<CoolProp::HelmholtzEOSMixtureBackend>(get_components());
2133 Residual resid(*HEOS_copy);
2134 const CoolProp::SimpleState& tripleV = HEOS_copy->get_components()[0].triple_vapor;
2135 double v1 = resid.call(hsat_max.T);
2136 double v2 = resid.call(tripleV.T);
2137 // If there is a sign change, there is a maxima, otherwise there is no local maxima/minima
2138 if (v1 * v2 < 0) {
2139 Brent(resid, hsat_max.T, tripleV.T, DBL_EPSILON, 1e-8, 30);
2140 ssat_max.T = resid.HEOS->T();
2141 ssat_max.p = resid.HEOS->p();
2142 ssat_max.rhomolar = resid.HEOS->rhomolar();
2143 ssat_max.hmolar = resid.HEOS->hmolar();
2144 ssat_max.smolar = resid.HEOS->smolar();
2146 } else {
2148 }
2149 }
2150}
2152 class Residualhmax : public FuncWrapper1D
2153 {
2154 public:
2156 Residualhmax(HelmholtzEOSMixtureBackend& HEOS) : HEOS(&HEOS) {};
2157 double call(double T) override {
2158 HEOS->update(QT_INPUTS, 1, T);
2159 // dTdp_along_sat
2160 double dTdp_along_sat =
2161 HEOS->T() * (1 / HEOS->SatV->rhomolar() - 1 / HEOS->SatL->rhomolar()) / (HEOS->SatV->hmolar() - HEOS->SatL->hmolar());
2162 // dhdT_along_sat;
2163 return HEOS->SatV->first_partial_deriv(iHmolar, iT, iP) + HEOS->SatV->first_partial_deriv(iHmolar, iP, iT) / dTdp_along_sat;
2164 }
2165 };
2166 if (!hsat_max.is_valid()) {
2167 shared_ptr<CoolProp::HelmholtzEOSMixtureBackend> HEOS_copy = std::make_shared<CoolProp::HelmholtzEOSMixtureBackend>(get_components());
2168 Residualhmax residhmax(*HEOS_copy);
2169 Brent(residhmax, T_critical() - 0.1, HEOS_copy->Ttriple() + 1, DBL_EPSILON, 1e-8, 30);
2170 hsat_max.T = residhmax.HEOS->T();
2171 hsat_max.p = residhmax.HEOS->p();
2172 hsat_max.rhomolar = residhmax.HEOS->rhomolar();
2173 hsat_max.hmolar = residhmax.HEOS->hmolar();
2174 hsat_max.smolar = residhmax.HEOS->smolar();
2175 }
2176}
2178 if (!ValidNumber(value)) {
2179 throw ValueError(format("value to T_phase_determination_pure_or_pseudopure is invalid"));
2180 };
2181
2182 double T_crit_ = T_critical(), p_crit_ = p_critical(), rhomolar_crit_ = rhomolar_critical();
2183 auto smolar_critical = [this, &T_crit_, &rhomolar_crit_]() { return this->calc_smolar_nocache(T_crit_, rhomolar_crit_); };
2184 auto hmolar_critical = [this, &T_crit_, &rhomolar_crit_]() { return this->calc_hmolar_nocache(T_crit_, rhomolar_crit_); };
2185 auto umolar_critical = [this, &T_crit_, &rhomolar_crit_]() { return this->calc_umolar_nocache(T_crit_, rhomolar_crit_); };
2186
2187 // Check for the presence of the melting line (skipped when the caller has opted out
2188 // of property-limit checks, mirroring p_phase_determination_pure_or_pseudopure)
2189 if (other == iP && has_melting_line() && !get_config_bool(DONT_CHECK_PROPERTY_LIMITS) && value > calc_melting_line(iP_min, -1, -1)) {
2190 double Tm = melting_line(iT, iP, value);
2191 if (_T < Tm - 0.001) {
2192 throw ValueError(format("For now, we don't support T [%g K] below Tmelt(p) [%g K]", _T, Tm));
2193 }
2194 }
2195
2196 // T is known, another input P, T, H, S, U is given (all molar)
2197 if (_T < T_crit_ && _p > p_crit_) {
2198 // Only ever true if (other = iP); otherwise _p = -HUGE
2200 } else if (std::abs(_T - T_crit_) < 10 * DBL_EPSILON) // Exactly at Tcrit
2201 {
2202 switch (other) {
2203 case iDmolar:
2204 if (std::abs(_rhomolar - rhomolar_crit_) < 10 * DBL_EPSILON) {
2206 break;
2207 } else if (_rhomolar > rhomolar_crit_) {
2209 break;
2210 } else {
2212 break;
2213 }
2214 case iP: {
2215 if (std::abs(_p - p_crit_) < 10 * DBL_EPSILON) {
2217 break;
2218 } else if (_p > p_crit_) {
2220 break;
2221 } else {
2223 break;
2224 }
2225 }
2226 default:
2227 throw ValueError(format("T=Tcrit; invalid input for other to T_phase_determination_pure_or_pseudopure"));
2228 }
2229 } else if (_T < T_crit_) // Gas, 2-Phase, Liquid, or Supercritical Liquid Region
2230 {
2231 if (get_config_bool(ENABLE_SUPERANCILLARIES) && is_pure()) {
2232 auto superanc_ptr = get_superanc();
2233 // Superancillaries are enabled and available, they will be used to determine the phase
2234 if (superanc_ptr) {
2235 auto& superanc = *superanc_ptr;
2236 auto rhoL = superanc.eval_sat(_T, 'D', 0);
2237 auto rhoV = superanc.eval_sat(_T, 'D', 1);
2238 auto psat = superanc.eval_sat(_T, 'P', 1);
2239 _rhoLanc = rhoL;
2240 _rhoVanc = rhoV;
2241 _rhoLmolar = rhoL;
2242 _rhoVmolar = rhoV;
2243
2244 if (other == iP) {
2245 if (std::abs(psat / value - 1) < 1e-6) {
2246 throw ValueError(
2247 format("Saturation pressure [%g Pa] corresponding to T [%g K] is within 1e-4 %% of given p [%Lg Pa]", psat, _T, value));
2248 } else if (value < psat) {
2250 _Q = -1000;
2251 } else if (value > psat) {
2253 _Q = 1000;
2254 }
2255 return;
2256 }
2257 double Q = -1;
2258 if (other == iDmolar) {
2259 // Special case density as an input
2260 Q = (1 / value - 1 / rhoL) / (1 / rhoV - 1 / rhoL);
2261 if (Q <= 0) {
2263 _Q = -1000;
2264 } else if (Q >= 1) {
2266 _Q = 1000;
2267 } else {
2269 _p = psat;
2270 this->_Q = Q;
2271 SatL->update(DmolarT_INPUTS, rhoL, _T);
2272 SatV->update(DmolarT_INPUTS, rhoV, _T);
2273 }
2274 _rhomolar = value;
2275 return;
2276 }
2277
2278 SatL->update_TDmolarP_unchecked(_T, rhoL, psat);
2279 SatV->update_TDmolarP_unchecked(_T, rhoV, psat);
2280
2281 switch (other) {
2282 case iDmolar:
2283 Q = (1 / value - 1 / SatL->rhomolar()) / (1 / SatV->rhomolar() - 1 / SatL->rhomolar());
2284 break;
2285 case iSmolar:
2286 Q = (value - SatL->smolar()) / (SatV->smolar() - SatL->smolar());
2287 break;
2288 case iHmolar:
2289 Q = (value - SatL->hmolar()) / (SatV->hmolar() - SatL->hmolar());
2290 break;
2291 case iUmolar:
2292 Q = (value - SatL->umolar()) / (SatV->umolar() - SatL->umolar());
2293 break;
2294 default:
2295 throw ValueError(format("bad input for other"));
2296 }
2297
2298 if (Q < 0) {
2299 this->_phase = iphase_liquid;
2300 SatL->clear();
2301 SatV->clear();
2302 _Q = -1000;
2303
2304 } else if (Q > 1) {
2305 this->_phase = iphase_gas;
2306 SatL->clear();
2307 SatV->clear();
2308 _Q = 1000;
2309 } else {
2311 _p = psat;
2312 _Q = Q;
2313 _rhomolar = 1 / (_Q / rhoV + (1 - _Q) / rhoL);
2314 }
2315 return;
2316 }
2317 }
2318
2319 // Start to think about the saturation stuff
2320 // First try to use the ancillary equations if you are far enough away
2321 // You know how accurate the ancillary equations are thanks to using CoolProp code to refit them
2322 switch (other) {
2323 case iP: {
2324 _pLanc = components[0].ancillaries.pL.evaluate(_T);
2325 _pVanc = components[0].ancillaries.pV.evaluate(_T);
2326 CoolPropDbl p_vap = 0.98 * static_cast<double>(_pVanc);
2327 CoolPropDbl p_liq = 1.02 * static_cast<double>(_pLanc);
2328
2329 if (value < p_vap) {
2330 this->_phase = iphase_gas;
2331 _Q = -1000;
2332 return;
2333 } else if (value > p_liq) {
2334 this->_phase = iphase_liquid;
2335 _Q = 1000;
2336 return;
2337 } else if (!is_pure()) // pseudo-pure
2338 {
2339 // For pseudo-pure fluids, the ancillary pressure curves are the official
2340 // arbiter of the phase
2341 if (value > static_cast<CoolPropDbl>(_pLanc)) {
2342 this->_phase = iphase_liquid;
2343 _Q = 1000;
2344 return;
2345 } else if (value < static_cast<CoolPropDbl>(_pVanc)) {
2346 this->_phase = iphase_gas;
2347 _Q = -1000;
2348 return;
2349 } else {
2350 throw ValueError("Two-phase inputs not supported for pseudo-pure for now");
2351 }
2352 }
2353 break;
2354 }
2355 default: {
2356 // Always calculate the densities using the ancillaries
2357 _rhoVanc = components[0].ancillaries.rhoV.evaluate(_T);
2358 _rhoLanc = components[0].ancillaries.rhoL.evaluate(_T);
2359 CoolPropDbl rho_vap = 0.95 * static_cast<double>(_rhoVanc);
2360 CoolPropDbl rho_liq = 1.05 * static_cast<double>(_rhoLanc);
2361 switch (other) {
2362 case iDmolar: {
2363 if (value < rho_vap) {
2364 this->_phase = iphase_gas;
2365 return;
2366 } else if (value > rho_liq) {
2367 this->_phase = iphase_liquid;
2368 return;
2369 } else {
2370 // Next we check the vapor quality based on the ancillary values
2371 double Qanc = (1 / value - 1 / static_cast<double>(_rhoLanc))
2372 / (1 / static_cast<double>(_rhoVanc) - 1 / static_cast<double>(_rhoLanc));
2373 // If the vapor quality is significantly inside the two-phase zone, stop, we are definitely two-phase
2374 if (value > 0.95 * rho_liq || value < 1.05 * rho_vap) {
2375 // Definitely single-phase
2376 _phase = iphase_liquid; // Needed for direct update call
2377 _Q = -1000; // Needed for direct update call
2378 update_DmolarT_direct(value, _T);
2379 CoolPropDbl pL = components[0].ancillaries.pL.evaluate(_T);
2380 if (Qanc < 0.01 && _p > pL * 1.05 && first_partial_deriv(iP, iDmolar, iT) > 0
2383 _Q = -1000;
2384 return;
2385 } else if (Qanc > 1.01) {
2386 break;
2387 } else {
2389 _p = _HUGE;
2390 }
2391 }
2392 }
2393 break;
2394 }
2395 default: {
2396 if (!this->SatL || !this->SatV) {
2397 throw ValueError(format("The saturation properties are needed in T_phase_determination_pure_or_pseudopure"));
2398 }
2399 // If it is not density, update the states
2400 SatV->update(DmolarT_INPUTS, rho_vap, _T);
2401 SatL->update(DmolarT_INPUTS, rho_liq, _T);
2402
2403 // First we check ancillaries
2404 switch (other) {
2405 case iSmolar: {
2406 if (value > SatV->calc_smolar()) {
2407 this->_phase = iphase_gas;
2408 return;
2409 }
2410 if (value < SatL->calc_smolar()) {
2411 this->_phase = iphase_liquid;
2412 return;
2413 }
2414 break;
2415 }
2416 case iHmolar: {
2417 if (value > SatV->calc_hmolar()) {
2418 this->_phase = iphase_gas;
2419 return;
2420 } else if (value < SatL->calc_hmolar()) {
2421 this->_phase = iphase_liquid;
2422 return;
2423 }
2424 break;
2425 }
2426 case iUmolar: {
2427 if (value > SatV->calc_umolar()) {
2428 this->_phase = iphase_gas;
2429 return;
2430 } else if (value < SatL->calc_umolar()) {
2431 this->_phase = iphase_liquid;
2432 return;
2433 }
2434 break;
2435 }
2436 default:
2437 throw ValueError(format("invalid input for other to T_phase_determination_pure_or_pseudopure"));
2438 }
2439 }
2440 }
2441 }
2442 }
2443
2444 // Actually have to use saturation information sadly
2445 // For the given temperature, find the saturation state
2446 // Run the saturation routines to determine the saturation densities and pressures
2450
2451 CoolPropDbl Q = NAN;
2452
2453 if (other == iP) {
2454 if (value > HEOS.SatL->p() * (1e-6 + 1)) {
2455 this->_phase = iphase_liquid;
2456 _Q = -1000;
2457 return;
2458 } else if (value < HEOS.SatV->p() * (1 - 1e-6)) {
2459 this->_phase = iphase_gas;
2460 _Q = 1000;
2461 return;
2462 } else {
2463 throw ValueError(
2464 format("Saturation pressure [%g Pa] corresponding to T [%g K] is within 1e-4 %% of given p [%Lg Pa]", HEOS.SatL->p(), _T, value));
2465 }
2466 }
2467
2468 switch (other) {
2469 case iDmolar:
2470 Q = (1 / value - 1 / HEOS.SatL->rhomolar()) / (1 / HEOS.SatV->rhomolar() - 1 / HEOS.SatL->rhomolar());
2471 break;
2472 case iSmolar:
2473 Q = (value - HEOS.SatL->smolar()) / (HEOS.SatV->smolar() - HEOS.SatL->smolar());
2474 break;
2475 case iHmolar:
2476 Q = (value - HEOS.SatL->hmolar()) / (HEOS.SatV->hmolar() - HEOS.SatL->hmolar());
2477 break;
2478 case iUmolar:
2479 Q = (value - HEOS.SatL->umolar()) / (HEOS.SatV->umolar() - HEOS.SatL->umolar());
2480 break;
2481 default:
2482 throw ValueError(format("bad input for other"));
2483 }
2484
2485 // Update the states
2486 if (this->SatL) this->SatL->update(DmolarT_INPUTS, HEOS.SatL->rhomolar(), HEOS.SatL->T());
2487 if (this->SatV) this->SatV->update(DmolarT_INPUTS, HEOS.SatV->rhomolar(), HEOS.SatV->T());
2488 // Update the two-Phase variables
2489 _rhoLmolar = HEOS.SatL->rhomolar();
2490 _rhoVmolar = HEOS.SatV->rhomolar();
2491
2492 if (Q < 0) {
2493 this->_phase = iphase_liquid;
2494 _Q = -1;
2495 return;
2496 } else if (Q > 1) {
2497 this->_phase = iphase_gas;
2498 _Q = 1;
2499 return;
2500 } else {
2501 this->_phase = iphase_twophase;
2502 }
2503 _Q = Q;
2504 // Load the outputs
2505 _p = _Q * HEOS.SatV->p() + (1 - _Q) * HEOS.SatL->p();
2506 _rhomolar = 1 / (_Q / HEOS.SatV->rhomolar() + (1 - _Q) / HEOS.SatL->rhomolar());
2507 return;
2508 } else if (_T > T_crit_ && _T > components[0].EOS().Ttriple) // Supercritical or Supercritical Gas Region
2509 {
2510 _Q = 1e9;
2511 switch (other) {
2512 case iP: {
2513 if (_p > p_crit_) {
2515 return;
2516 } else {
2518 return;
2519 }
2520 }
2521 case iDmolar: {
2522 if (_rhomolar > rhomolar_crit_) {
2524 return;
2525 } else {
2527 return;
2528 }
2529 }
2530 case iSmolar: {
2531 if (_smolar.pt() > smolar_critical()) {
2533 return;
2534 } else {
2536 return;
2537 }
2538 }
2539 case iHmolar: {
2540 if (_hmolar.pt() > hmolar_critical()) {
2542 return;
2543 } else {
2545 return;
2546 }
2547 }
2548 case iUmolar: {
2549 if (_umolar.pt() > umolar_critical()) {
2551 return;
2552 } else {
2554 return;
2555 }
2556 }
2557 default: {
2558 throw ValueError("supercritical temp but other invalid for now");
2559 }
2560 }
2561 } else {
2562 throw ValueError(format("For now, we don't support T [%g K] below Ttriple [%g K]", _T, components[0].EOS().Ttriple));
2563 }
2564}
2566 CoolPropDbl T = HEOS->T(), rho = HEOS->rhomolar(), rhor = HEOS->get_reducing_state().rhomolar, Tr = HEOS->get_reducing_state().T,
2567 dT_dtau = -pow(T, 2) / Tr, R = HEOS->gas_constant(), delta = rho / rhor, tau = Tr / T;
2568
2569 switch (index) {
2570 case iT:
2571 dT = 1;
2572 drho = 0;
2573 break;
2574 case iDmolar:
2575 dT = 0;
2576 drho = 1;
2577 break;
2578 case iDmass:
2579 dT = 0;
2580 drho = HEOS->molar_mass();
2581 break;
2582 case iP: {
2583 // dp/drho|T
2584 drho = R * T * (1 + 2 * delta * HEOS->dalphar_dDelta() + pow(delta, 2) * HEOS->d2alphar_dDelta2());
2585 // dp/dT|rho
2586 dT = rho * R * (1 + delta * HEOS->dalphar_dDelta() - tau * delta * HEOS->d2alphar_dDelta_dTau());
2587 break;
2588 }
2589 case iHmass:
2590 case iHmolar: {
2591 // dh/dT|rho
2592 dT = R
2593 * (-pow(tau, 2) * (HEOS->d2alpha0_dTau2() + HEOS->d2alphar_dTau2())
2594 + (1 + delta * HEOS->dalphar_dDelta() - tau * delta * HEOS->d2alphar_dDelta_dTau()));
2595 // dh/drhomolar|T
2596 drho =
2597 T * R / rho * (tau * delta * HEOS->d2alphar_dDelta_dTau() + delta * HEOS->dalphar_dDelta() + pow(delta, 2) * HEOS->d2alphar_dDelta2());
2598 if (index == iHmass) {
2599 // dhmolar/drhomolar|T * dhmass/dhmolar where dhmass/dhmolar = 1/mole mass
2600 drho /= HEOS->molar_mass();
2601 dT /= HEOS->molar_mass();
2602 }
2603 break;
2604 }
2605 case iSmass:
2606 case iSmolar: {
2607 // ds/dT|rho
2608 dT = R / T * (-pow(tau, 2) * (HEOS->d2alpha0_dTau2() + HEOS->d2alphar_dTau2()));
2609 // ds/drho|T
2610 drho = R / rho * (-(1 + delta * HEOS->dalphar_dDelta() - tau * delta * HEOS->d2alphar_dDelta_dTau()));
2611 if (index == iSmass) {
2612 // ds/drho|T / drhomass/drhomolar where drhomass/drhomolar = mole mass
2613 drho /= HEOS->molar_mass();
2614 dT /= HEOS->molar_mass();
2615 }
2616 break;
2617 }
2618 case iUmass:
2619 case iUmolar: {
2620 // du/dT|rho
2621 dT = R * (-pow(tau, 2) * (HEOS->d2alpha0_dTau2() + HEOS->d2alphar_dTau2()));
2622 // du/drho|T
2623 drho = HEOS->T() * R / rho * (tau * delta * HEOS->d2alphar_dDelta_dTau());
2624 if (index == iUmass) {
2625 // du/drho|T / drhomass/drhomolar where drhomass/drhomolar = mole mass
2626 drho /= HEOS->molar_mass();
2627 dT /= HEOS->molar_mass();
2628 }
2629 break;
2630 }
2631 case iTau:
2632 dT = 1 / dT_dtau;
2633 drho = 0;
2634 break;
2635 case iDelta:
2636 dT = 0;
2637 drho = 1 / rhor;
2638 break;
2639 default:
2640 throw ValueError(format("input to get_dT_drho[%s] is invalid", get_parameter_information(index, "short").c_str()));
2641 }
2642}
2643
2646 CoolPropDbl delta = rhomolar / reducing.rhomolar;
2647 CoolPropDbl tau = reducing.T / T;
2648
2649 // Calculate derivative
2650 int nTau = 0, nDelta = 1;
2652
2653 // Get pressure
2654 return rhomolar * gas_constant() * T * (1 + delta * dalphar_dDelta);
2655}
2657 CoolPropDbl& light, CoolPropDbl& heavy) {
2658
2660 class dpdrho_resid : public FuncWrapper1DWithTwoDerivs
2661 {
2662 public:
2664 CoolPropDbl T, p, delta, rhor, tau, R_u;
2665
2667 : HEOS(HEOS),
2668 T(T),
2669 p(p),
2670 delta(_HUGE),
2671 rhor(HEOS->get_reducing_state().rhomolar),
2672 tau(HEOS->get_reducing_state().T / T),
2673 R_u(HEOS->gas_constant()) {}
2674 double call(double rhomolar) override {
2675 delta = rhomolar / rhor; // needed for derivative
2677 // dp/drho|T
2678 return R_u * T * (1 + 2 * delta * HEOS->dalphar_dDelta() + POW2(delta) * HEOS->d2alphar_dDelta2());
2679 };
2680 double deriv(double rhomolar) override {
2681 // d2p/drho2|T
2682 return R_u * T / rhor * (2 * HEOS->dalphar_dDelta() + 4 * delta * HEOS->d2alphar_dDelta2() + POW2(delta) * HEOS->calc_d3alphar_dDelta3());
2683 };
2684 double second_deriv(double rhomolar) override {
2685 // d3p/drho3|T
2686 return R_u * T / POW2(rhor)
2687 * (6 * HEOS->d2alphar_dDelta2() + 6 * delta * HEOS->d3alphar_dDelta3() + POW2(delta) * HEOS->calc_d4alphar_dDelta4());
2688 };
2689 };
2690 dpdrho_resid resid(this, T, p);
2691 light = -1;
2692 heavy = -1;
2693 try {
2694 light = Halley(resid, 1e-6, 1e-8, 100);
2695 double d2pdrho2__constT = resid.deriv(light);
2696 if (d2pdrho2__constT > 0) {
2697 // Not possible since curvature should be negative
2698 throw CoolProp::ValueError("curvature cannot be positive");
2699 }
2700 } catch (std::exception& e) {
2701 if (get_debug_level() > 5) {
2702 std::cout << e.what() << '\n';
2703 };
2704 light = -1;
2705 }
2706
2707 if (light < 0) {
2708 try {
2709 // Now we are going to do something VERY slow - increase density until curvature is positive
2710 double rho = 1e-6;
2711 for (std::size_t counter = 0; counter <= 100; counter++) {
2712 resid.call(rho); // Updates the state
2713 double curvature = resid.deriv(rho);
2714 if (curvature > 0) {
2715 light = rho;
2716 break;
2717 }
2718 rho *= 2;
2719 }
2720 } catch (...) { // NOLINT(bugprone-empty-catch)
2721 // Slow-path probe gave up; `light` stays at -1 and the
2722 // light/heavy < 0 checks below classify this as
2723 // ZERO_STATIONARY_POINTS.
2724 }
2725 }
2726
2727 // First try a "normal" calculation of the stationary point on the liquid side
2728 // 4 fixed iters (omega = 0.7, 0.5, 0.3, 0.1) — no FP accumulation concern.
2729 for (double omega = 0.7; omega > 0; omega -= 0.2) { // NOLINT(cert-flp30-c)
2730 try {
2731 resid.options.add_number("omega", omega);
2732 heavy = Halley(resid, rhomax, 1e-8, 100);
2733 double d2pdrho2__constT = resid.deriv(heavy);
2734 if (d2pdrho2__constT < 0) {
2735 // Not possible since curvature should be positive
2736 throw CoolProp::ValueError("curvature cannot be negative");
2737 }
2738 break; // Jump out, we got a good solution
2739 } catch (std::exception& e) {
2740 if (get_debug_level() > 5) {
2741 std::cout << e.what() << '\n';
2742 };
2743 heavy = -1;
2744 }
2745 }
2746
2747 if (heavy < 0) {
2748 try {
2749 // Now we are going to do something VERY slow - decrease density until curvature is negative or pressure is negative
2750 double rho = rhomax;
2751 for (std::size_t counter = 0; counter <= 100; counter++) {
2752 resid.call(rho); // Updates the state
2753 double curvature = resid.deriv(rho);
2754 if (curvature < 0 || this->p() < 0) {
2755 heavy = rho;
2756 break;
2757 }
2758 rho /= 1.1;
2759 }
2760 } catch (...) { // NOLINT(bugprone-empty-catch)
2761 // Mirror of the light-side slow path above — `heavy` stays
2762 // at -1 and the classification below treats it as no
2763 // stationary point.
2764 }
2765 }
2766
2767 if (light > 0 && heavy > 0) {
2768 // Found two stationary points, done!
2770 }
2771 // If no solution is found for dpdrho|T=0 starting at high and low densities,
2772 // then try to do a bounded solver to see if you can find any solutions. If you
2773 // can't, p = f(rho) is probably monotonic (supercritical?), and the bounds are
2774 else if (light < 0 && heavy < 0) {
2775 double dpdrho_min = resid.call(1e-10);
2776 double dpdrho_max = resid.call(rhomax);
2777 if (dpdrho_max * dpdrho_min > 0) {
2779 } else {
2780 throw CoolProp::ValueError("zero stationary points -- does this make sense?");
2781 }
2782 } else {
2784 }
2785}
2786// Define the residual to be driven to zero
2788{
2789 public:
2792
2794 : HEOS(HEOS),
2795 T(T),
2796 p(p),
2797 delta(_HUGE),
2798 rhor(HEOS->get_reducing_state().rhomolar),
2799 tau(HEOS->get_reducing_state().T / T),
2800 R_u(HEOS->gas_constant()) {}
2801 double call(double rhomolar) override {
2802 delta = rhomolar / rhor; // needed for derivative
2803 HEOS->update_DmolarT_direct(rhomolar, T);
2804 CoolPropDbl peos = HEOS->p();
2805 return (peos - p) / p;
2806 };
2807 double deriv(double rhomolar) override {
2808 // dp/drho|T / pspecified
2809 return R_u * T * (1 + 2 * delta * HEOS->dalphar_dDelta() + POW2(delta) * HEOS->d2alphar_dDelta2()) / p;
2810 };
2811 double second_deriv(double rhomolar) override {
2812 // d2p/drho2|T / pspecified
2813 return R_u * T / rhor * (2 * HEOS->dalphar_dDelta() + 4 * delta * HEOS->d2alphar_dDelta2() + POW2(delta) * HEOS->calc_d3alphar_dDelta3()) / p;
2814 };
2815 double third_deriv(double rhomolar) override {
2816 // d3p/drho3|T / pspecified
2817 return R_u * T / POW2(rhor)
2819 };
2820};
2822 return 0.9 / SRK_covolume();
2823}
2824
2826 double b = 0;
2827 for (std::size_t i = 0; i < mole_fractions.size(); ++i) {
2828 // Get the parameters for the cubic EOS
2830 CoolPropDbl R = 8.3144598;
2831 b += mole_fractions[i] * 0.08664034999649577215890158147700 * R * Tc / pc;
2832 }
2833 return b;
2834}
2836 // Find the densities along the isotherm where dpdrho|T = 0 (if you can)
2837 CoolPropDbl light = -1, heavy = -1;
2838 StationaryPointReturnFlag retval = solver_dpdrho0_Tp(T, p, rhomolar_max, light, heavy);
2839
2840 // Define the solver class
2841 SolverTPResid resid(this, T, p);
2842
2843 if (retval == ZERO_STATIONARY_POINTS) {
2844 // It's monotonic (no stationary points found), so do the full bounded solver
2845 // for the density
2846 double rho = Brent(resid, 1e-10, rhomolar_max, DBL_EPSILON, 1e-8, 100);
2847 return rho;
2848 } else if (retval == TWO_STATIONARY_POINTS_FOUND) {
2849
2850 // Calculate the pressures at the min and max densities where dpdrho|T = 0
2851 double p_at_rhomin_stationary = calc_pressure_nocache(T, light);
2852 double p_at_rhomax_stationary = calc_pressure_nocache(T, heavy);
2853
2854 double rho_liq = -1, rho_vap = -1;
2855 if (p > p_at_rhomax_stationary) {
2856 int counter = 0;
2857 for (/* init above, for debugging */; counter <= 10; counter++) {
2858 // Bump up rhomax if needed to bound the given pressure
2859 double p_at_rhomax = calc_pressure_nocache(T, rhomolar_max);
2860 if (p_at_rhomax < p) {
2861 rhomolar_max *= 1.05;
2862 } else {
2863 break;
2864 }
2865 }
2866 // Look for liquid root starting at stationary point density
2867 rho_liq = Brent(resid, heavy, rhomolar_max, DBL_EPSILON, 1e-8, 100);
2868 }
2869
2870 if (p < p_at_rhomin_stationary) {
2871 // Look for vapor root starting at stationary point density
2872 rho_vap = Brent(resid, light, 1e-10, DBL_EPSILON, 1e-8, 100);
2873 }
2874
2875 if (rho_vap > 0 && rho_liq > 0) {
2876 // Both densities are the same
2877 if (std::abs(rho_vap - rho_liq) < 1e-10) {
2878 // return one of them
2879 return rho_vap;
2880 } else {
2881 // Two solutions found, keep the one with lower Gibbs energy
2882 double gibbsmolar_vap = calc_gibbsmolar_nocache(T, rho_vap);
2883 double gibbsmolar_liq = calc_gibbsmolar_nocache(T, rho_liq);
2884 if (gibbsmolar_liq < gibbsmolar_vap) {
2885 return rho_liq;
2886 } else {
2887 return rho_vap;
2888 }
2889 }
2890 } else if (rho_vap < 0 && rho_liq > 0) {
2891 // Liquid root found, return it
2892 return rho_liq;
2893 } else if (rho_vap > 0 && rho_liq < 0) {
2894 // Vapor root found, return it
2895 return rho_vap;
2896 } else {
2897 throw CoolProp::ValueError(format("No density solutions for T=%g,p=%g,z=%s", T, p,
2898 vec_to_string(static_cast<std::vector<double>>(mole_fractions), "%0.12g").c_str()));
2899 }
2900 } else {
2901 throw CoolProp::ValueError(format("One stationary point (not good) for T=%g,p=%g,z=%s", T, p,
2902 vec_to_string(static_cast<std::vector<double>>(mole_fractions), "%0.12g").c_str()));
2903 }
2904};
2905
2907 phases phase;
2908
2909 SolverTPResid resid(this, T, p);
2910
2911 // Check if the phase is imposed
2913 // Use the imposed phase index
2915 else
2916 // Use the phase index in the class
2917 phase = _phase;
2918
2919 if (rhomolar_guess < 0) // Not provided
2920 {
2921 // Calculate a guess value using SRK equation of state
2922 rhomolar_guess = solver_rho_Tp_SRK(T, p, phase);
2923
2924 // A gas-like phase, ideal gas might not be the perfect model, but probably good enough
2926 if (rhomolar_guess < 0 || !ValidNumber(rhomolar_guess)) // If the guess is bad, probably high temperature, use ideal gas
2927 {
2928 rhomolar_guess = p / (gas_constant() * T);
2929 }
2930 } else if (phase == iphase_liquid) {
2931 double rhomolar = NAN;
2933 // It's liquid at subcritical pressure, we can use ancillaries as guess value
2934 auto _rhoLancval = static_cast<CoolPropDbl>(components[0].ancillaries.rhoL.evaluate(T));
2935 try {
2936 // First we try with Halley's method starting at saturated liquid
2937 rhomolar = Halley(resid, _rhoLancval, 1e-8, 100);
2940 throw ValueError("Liquid density is invalid");
2941 }
2942 } catch (std::exception&) {
2943 // Next we try with a Brent method bounded solver since the function is 1-1
2944 rhomolar = Brent(resid, _rhoLancval * 0.9, _rhoLancval * 1.3, DBL_EPSILON, 1e-8, 100);
2945 if (!ValidNumber(rhomolar)) {
2946 throw ValueError();
2947 }
2948 }
2949 } else {
2950 // Try with 4th order Householder method starting at a very high density
2951 rhomolar = Householder4(&resid, 3 * rhomolar_reducing(), 1e-8, 100);
2952 }
2953 return rhomolar;
2954 } else if (phase == iphase_supercritical_liquid) {
2955 auto rhoLancval = static_cast<CoolPropDbl>(components[0].ancillaries.rhoL.evaluate(T));
2956 auto rhoLtripleancval = static_cast<CoolPropDbl>(components[0].ancillaries.rhoL.evaluate(Ttriple()));
2957
2958 // Next we try with a Brent method bounded solver since the function should be 1-1 in most cases
2959 // But some EOS have a maximum in pressure so if rhoc*4 is after the maximum in pressure, this method will fail
2960 // and fall back to a narrower range of densities
2961 try {
2962 double rhomolar = Brent(resid, rhoLancval * 0.99, rhomolar_critical() * 4, DBL_EPSILON, 1e-8, 100);
2963 if (!ValidNumber(rhomolar)) {
2964 throw ValueError();
2965 }
2966 return rhomolar;
2967 } catch (...) {
2968 try {
2969 double rhomolar = Brent(resid, rhoLancval * 0.99, rhoLtripleancval * 1.1, DBL_EPSILON, 1e-8, 100);
2970 if (!ValidNumber(rhomolar)) {
2971 throw ValueError();
2972 }
2973 return rhomolar;
2974 } catch (...) {
2975 // Robust dense-branch TOMS748 fallback (CoolProp-r1w7, critical-region
2976 // "p is not a valid number"). Near the critical isotherm the ancillary-
2977 // anchored brackets above start near rho_c -- inside the van der Waals loop,
2978 // where p(rho) is non-monotonic and Brent can return a non-finite density
2979 // (-> NaN pressure, caught only by post_update). Build a monotonic bracket
2980 // on the STABLE dense branch by walking the upper bound up until it brackets
2981 // p_target, then the lower bound down until p < p_target, and TOMS748-solve.
2982 // The walk does NOT require a spinodal, so it is also correct for EOS whose
2983 // isotherms are monotonic (no van der Waals loop, e.g. some nitrogen models).
2984 // limits.rhomax is unreliable (0/unset for some fluids, e.g. R21), so anchor
2985 // the upper bound on the reducing density and bump it up until it brackets p.
2986 const double pTarget = p;
2987 // Whole-fallback guard: every calc_pressure_nocache probe below (the bracket
2988 // walk, the fa/fb endpoint evals, and the TOMS748 interior probes) is wrapped
2989 // so a non-finite/throwing EOS evaluation degrades to the ValueError at the
2990 // end rather than escaping solver_rho_Tp as an unexpected exception type.
2991 double rhoMolarFallback = NAN;
2992 try {
2993 double rhoHi = 5 * rhomolar_reducing();
2994 for (int i = 0; i < 60 && calc_pressure_nocache(T, rhoHi) < pTarget; ++i) {
2995 rhoHi *= 1.5;
2996 }
2997 double rhoLo = rhoHi;
2998 bool bracketed = false;
2999 for (int i = 0; i < 400; ++i) {
3000 rhoLo /= 1.05;
3001 if (rhoLo <= 1e-10) {
3002 break;
3003 }
3004 if (calc_pressure_nocache(T, rhoLo) < pTarget) {
3005 bracketed = true;
3006 break;
3007 }
3008 }
3009 if (bracketed) {
3010 auto f = [this, T, pTarget](double rho) { return this->calc_pressure_nocache(T, rho) - pTarget; };
3011 double fa = f(rhoLo), fb = f(rhoHi);
3012 if (ValidNumber(fa) && ValidNumber(fb) && fa * fb < 0) {
3013 boost::math::uintmax_t max_iter = 100;
3014 auto bracket =
3015 boost::math::tools::toms748_solve(f, rhoLo, rhoHi, fa, fb, boost::math::tools::eps_tolerance<double>(48), max_iter);
3016 const double rho = 0.5 * (bracket.first + bracket.second);
3017 if (ValidNumber(rho) && rho > 0) {
3018 rhoMolarFallback = rho;
3019 }
3020 }
3021 }
3022 } catch (...) { // NOLINT(bugprone-empty-catch) -- an EOS evaluation in the fallback threw; degrade to the ValueError below
3023 }
3024 if (ValidNumber(rhoMolarFallback) && rhoMolarFallback > 0) {
3025 // Restore the live state at the converged density: the failed Brent attempts
3026 // above left _p corrupted (NaN) via update_DmolarT_direct, and the bracketing
3027 // above used calc_pressure_nocache (no state write).
3028 update_DmolarT_direct(rhoMolarFallback, T);
3029 return rhoMolarFallback;
3030 }
3031 throw ValueError(format("solver_rho_Tp could not find a supercritical_liquid density for T=%Lg, p=%Lg", T, pTarget));
3032 }
3033 }
3034 }
3035 }
3036
3037 try {
3038 double rhomolar = Householder4(resid, rhomolar_guess, 1e-8, 20);
3039 if (!ValidNumber(rhomolar) || rhomolar < 0) {
3040 throw ValueError();
3041 }
3042 if (phase == iphase_liquid) {
3043 double dpdrho = first_partial_deriv(iP, iDmolar, iT);
3044 double d2pdrho2 = second_partial_deriv(iP, iDmolar, iT, iDmolar, iT);
3045 if (dpdrho < 0 || d2pdrho2 < 0) {
3046 // Try again with a larger density in order to end up at the right solution
3047 rhomolar = Householder4(resid, 3 * rhomolar_reducing(), 1e-8, 100);
3048 return rhomolar;
3049 }
3050 } else if (phase == iphase_gas) {
3051 double dpdrho = first_partial_deriv(iP, iDmolar, iT);
3052 double d2pdrho2 = second_partial_deriv(iP, iDmolar, iT, iDmolar, iT);
3053 if (dpdrho < 0 || d2pdrho2 > 0) {
3054 // Try again with a tiny density in order to end up at the right solution
3055 rhomolar = Householder4(resid, 1e-6, 1e-8, 100);
3056 return rhomolar;
3057 }
3058 }
3059 return rhomolar;
3060 } catch (std::exception& e) {
3062 double rhomolar = Brent(resid, 1e-10, 3 * rhomolar_reducing(), DBL_EPSILON, 1e-8, 100);
3063 return rhomolar;
3064 } else if (is_pure_or_pseudopure && T > T_critical()) {
3065 try {
3066 double rhomolar = Brent(resid, 1e-10, 5 * rhomolar_reducing(), DBL_EPSILON, 1e-8, 100);
3067 return rhomolar;
3068
3069 } catch (...) {
3070 double rhomolar = Householder4(resid, 3 * rhomolar_reducing(), 1e-8, 100);
3071 return rhomolar;
3072 }
3073 }
3074 throw ValueError(format("solver_rho_Tp was unable to find a solution for T=%10Lg, p=%10Lg, with guess value %10Lg with error: %s", T, p,
3075 rhomolar_guess, e.what()));
3076 }
3077}
3079 CoolPropDbl rhomolar = NAN, R_u = gas_constant(), a = 0, b = 0, k_ij = 0;
3080
3081 for (std::size_t i = 0; i < components.size(); ++i) {
3082 CoolPropDbl Tci = components[i].EOS().reduce.T, pci = components[i].EOS().reduce.p, acentric_i = components[i].EOS().acentric;
3083 CoolPropDbl m_i = 0.480 + 1.574 * acentric_i - 0.176 * pow(acentric_i, 2);
3084 CoolPropDbl b_i = 0.08664034999649577215890158147700 * R_u * Tci / pci;
3085 b += mole_fractions[i] * b_i;
3086
3087 CoolPropDbl a_i = 0.42748023335403414043900347952220 * pow(R_u * Tci, 2) / pci * pow(1 + m_i * (1 - sqrt(T / Tci)), 2);
3088
3089 for (std::size_t j = 0; j < components.size(); ++j) {
3090 CoolPropDbl Tcj = components[j].EOS().reduce.T, pcj = components[j].EOS().reduce.p, acentric_j = components[j].EOS().acentric;
3091 CoolPropDbl m_j = 0.480 + 1.574 * acentric_j - 0.176 * pow(acentric_j, 2);
3092
3093 CoolPropDbl a_j = 0.42748023335403414043900347952220 * pow(R_u * Tcj, 2) / pcj * pow(1 + m_j * (1 - sqrt(T / Tcj)), 2);
3094
3095 k_ij = 0;
3096 //if (i == j){
3097 // k_ij = 0;
3098 //}
3099 //else{
3100 // k_ij = 0;
3101 //}
3102
3103 a += mole_fractions[i] * mole_fractions[j] * sqrt(a_i * a_j) * (1 - k_ij);
3104 }
3105 }
3106
3107 CoolPropDbl A = a * p / pow(R_u * T, 2);
3108 CoolPropDbl B = b * p / (R_u * T);
3109
3110 //Solve the cubic for solutions for Z = p/(rho*R*T)
3111 double Z0 = NAN, Z1 = NAN, Z2 = NAN;
3112 int Nsolns = 0;
3113 solve_cubic(1, -1, A - B - B * B, -A * B, Nsolns, Z0, Z1, Z2);
3114
3115 // Determine the guess value
3116 if (Nsolns == 1) {
3117 rhomolar = p / (Z0 * R_u * T);
3118 } else {
3119 CoolPropDbl rhomolar0 = p / (Z0 * R_u * T);
3120 CoolPropDbl rhomolar1 = p / (Z1 * R_u * T);
3121 CoolPropDbl rhomolar2 = p / (Z2 * R_u * T);
3122
3123 // Check if only one solution is positive, return the solution if that is the case
3124 if (rhomolar0 > 0 && rhomolar1 <= 0 && rhomolar2 <= 0) {
3125 return rhomolar0;
3126 }
3127 if (rhomolar0 <= 0 && rhomolar1 > 0 && rhomolar2 <= 0) {
3128 return rhomolar1;
3129 }
3130 if (rhomolar0 <= 0 && rhomolar1 <= 0 && rhomolar2 > 0) {
3131 return rhomolar2;
3132 }
3133
3134 switch (phase) {
3135 case iphase_liquid:
3137 rhomolar = max3(rhomolar0, rhomolar1, rhomolar2);
3138 break;
3139 case iphase_gas:
3142 rhomolar = min3(rhomolar0, rhomolar1, rhomolar2);
3143 break;
3144 default:
3145 throw ValueError("Bad phase to solver_rho_Tp_SRK");
3146 };
3147 }
3148 return rhomolar;
3149}
3150
3152 // Calculate the reducing parameters
3154 _tau = _reducing.T / _T;
3155
3156 // Calculate derivative if needed
3157 CoolPropDbl dar_dDelta = dalphar_dDelta();
3158 CoolPropDbl R_u = gas_constant();
3159
3160 // Get pressure
3161 _p = _rhomolar * R_u * _T * (1 + _delta.pt() * dar_dDelta);
3162
3163 //std::cout << format("p: %13.12f %13.12f %10.9f %10.9f %10.9f %10.9f %g\n",_T,_rhomolar,_tau,_delta,mole_fractions[0],dar_dDelta,_p);
3164 //if (_p < 0){
3165 // throw ValueError("Pressure is less than zero");
3166 //}
3167
3168 return static_cast<CoolPropDbl>(_p);
3169}
3171 // Calculate the reducing parameters
3174
3175 // Calculate derivatives if needed, or just use cached values
3176 // Calculate derivative if needed
3180 CoolPropDbl R_u = gas_constant();
3181
3182 // Get molar enthalpy
3183 return R_u * T * (1 + tau * (da0_dTau + dar_dTau) + delta * dar_dDelta);
3184}
3186 if (get_debug_level() >= 50)
3187 std::cout << format("HelmholtzEOSMixtureBackend::calc_hmolar: 2phase: %d T: %g rhomomolar: %g", isTwoPhase(), _T, _rhomolar) << '\n';
3188 if (isTwoPhase()) {
3189 if (!this->SatL || !this->SatV) throw ValueError(format("The saturation properties are needed for the two-phase properties"));
3190 if (std::abs(_Q) < DBL_EPSILON) {
3191 _hmolar = SatL->hmolar();
3192 } else if (std::abs(_Q - 1) < DBL_EPSILON) {
3193 _hmolar = SatV->hmolar();
3194 } else {
3195 _hmolar = _Q * SatV->hmolar() + (1 - _Q) * SatL->hmolar();
3196 }
3197 return static_cast<CoolPropDbl>(_hmolar);
3198 } else if (isHomogeneousPhase()) {
3199 // Calculate the reducing parameters
3201 _tau = _reducing.T / _T;
3202
3203 // Calculate derivatives if needed, or just use cached values
3204 CoolPropDbl da0_dTau = dalpha0_dTau();
3205 CoolPropDbl dar_dTau = dalphar_dTau();
3206 CoolPropDbl dar_dDelta = dalphar_dDelta();
3207 CoolPropDbl R_u = gas_constant();
3208
3209 // Get molar enthalpy
3210 _hmolar = R_u * _T * (1 + _tau.pt() * (da0_dTau + dar_dTau) + _delta.pt() * dar_dDelta);
3211
3212 return static_cast<CoolPropDbl>(_hmolar);
3213 } else {
3214 throw ValueError(format("phase is invalid in calc_hmolar"));
3215 }
3216}
3218 // Calculate the reducing parameters
3221
3222 // Calculate derivatives if needed, or just use cached values
3223 // Calculate derivative if needed
3228 CoolPropDbl R_u = gas_constant();
3229
3230 // Get molar entropy
3231 return R_u * (tau * (da0_dTau + dar_dTau) - a0 - ar);
3232}
3234 if (isTwoPhase()) {
3235 if (!this->SatL || !this->SatV) throw ValueError(format("The saturation properties are needed for the two-phase properties"));
3236 if (std::abs(_Q) < DBL_EPSILON) {
3237 _smolar = SatL->smolar();
3238 } else if (std::abs(_Q - 1) < DBL_EPSILON) {
3239 _smolar = SatV->smolar();
3240 } else {
3241 _smolar = _Q * SatV->smolar() + (1 - _Q) * SatL->smolar();
3242 }
3243 return static_cast<CoolPropDbl>(_smolar);
3244 } else if (isHomogeneousPhase()) {
3245 // Calculate the reducing parameters
3247 _tau = _reducing.T / _T;
3248
3249 // Calculate derivatives if needed, or just use cached values
3251 CoolPropDbl da0_dTau =
3252 ders.dalphar_dtau; // Note: while the naming here refers to alphar for historical reasons, it is actually the ideal-gas part
3253 CoolPropDbl a0 = ders.alphar; // Note: while the naming here refers to alphar for historical reasons, it is actually the ideal-gas part
3254
3255 CoolPropDbl ar = alphar();
3256 CoolPropDbl dar_dTau = dalphar_dTau();
3257 CoolPropDbl R_u = gas_constant();
3258
3259 // Get molar entropy
3260 _smolar = R_u * (_tau.pt() * (da0_dTau + dar_dTau) - a0 - ar);
3261
3262 return static_cast<CoolPropDbl>(_smolar);
3263 } else {
3264 throw ValueError(format("phase is invalid in calc_smolar"));
3265 }
3266}
3268 // Calculate the reducing parameters
3271
3272 // Calculate derivatives
3275 CoolPropDbl R_u = gas_constant();
3276
3277 // Get molar internal energy
3278 return R_u * T * tau * (da0_dTau + dar_dTau);
3279}
3281 if (isTwoPhase()) {
3282 if (!this->SatL || !this->SatV) throw ValueError(format("The saturation properties are needed for the two-phase properties"));
3283 if (std::abs(_Q) < DBL_EPSILON) {
3284 _umolar = SatL->umolar();
3285 } else if (std::abs(_Q - 1) < DBL_EPSILON) {
3286 _umolar = SatV->umolar();
3287 } else {
3288 _umolar = _Q * SatV->umolar() + (1 - _Q) * SatL->umolar();
3289 }
3290 return static_cast<CoolPropDbl>(_umolar);
3291 } else if (isHomogeneousPhase()) {
3292 // Calculate the reducing parameters
3294 _tau = _reducing.T / _T;
3295
3296 // Calculate derivatives if needed, or just use cached values
3297 CoolPropDbl da0_dTau = dalpha0_dTau();
3298 CoolPropDbl dar_dTau = dalphar_dTau();
3299 CoolPropDbl R_u = gas_constant();
3300
3301 // Get molar internal energy
3302 _umolar = R_u * _T * _tau.pt() * (da0_dTau + dar_dTau);
3303
3304 return static_cast<CoolPropDbl>(_umolar);
3305 } else {
3306 throw ValueError(format("phase is invalid in calc_umolar"));
3307 }
3308}
3310 // Calculate the reducing parameters
3312 _tau = _reducing.T / _T;
3313
3314 // Calculate derivatives if needed, or just use cached values
3315 CoolPropDbl d2ar_dTau2 = d2alphar_dTau2();
3316 CoolPropDbl d2a0_dTau2 = d2alpha0_dTau2();
3317 CoolPropDbl R_u = gas_constant();
3318
3319 // Get cv
3320 _cvmolar = -R_u * pow(_tau.pt(), 2) * (d2ar_dTau2 + d2a0_dTau2);
3321
3322 return static_cast<double>(_cvmolar);
3323}
3325 // Calculate the reducing parameters
3327 _tau = _reducing.T / _T;
3328
3329 // Calculate derivatives if needed, or just use cached values
3330 CoolPropDbl d2a0_dTau2 = d2alpha0_dTau2();
3331 CoolPropDbl dar_dDelta = dalphar_dDelta();
3332 CoolPropDbl d2ar_dDelta2 = d2alphar_dDelta2();
3333 CoolPropDbl d2ar_dDelta_dTau = d2alphar_dDelta_dTau();
3334 CoolPropDbl d2ar_dTau2 = d2alphar_dTau2();
3335 CoolPropDbl R_u = gas_constant();
3336
3337 // Get cp
3338 _cpmolar = R_u
3339 * (-pow(_tau.pt(), 2) * (d2ar_dTau2 + d2a0_dTau2)
3340 + pow(1 + _delta.pt() * dar_dDelta - _delta.pt() * _tau.pt() * d2ar_dDelta_dTau, 2)
3341 / (1 + 2 * _delta.pt() * dar_dDelta + pow(_delta.pt(), 2) * d2ar_dDelta2));
3342
3343 return static_cast<double>(_cpmolar);
3344}
3346 // Calculate the reducing parameters
3348 _tau = _reducing.T / _T;
3349
3350 // Calculate derivatives if needed, or just use cached values
3351 CoolPropDbl d2a0_dTau2 = d2alpha0_dTau2();
3352 CoolPropDbl R_u = gas_constant();
3353
3354 // Get cp of the ideal gas
3355 return R_u * (1 + (-pow(_tau.pt(), 2)) * d2a0_dTau2);
3356}
3358 if (isTwoPhase()) {
3359 if (std::abs(_Q) < DBL_EPSILON) {
3360 return SatL->speed_sound();
3361 } else if (std::abs(_Q - 1) < DBL_EPSILON) {
3362 return SatV->speed_sound();
3363 } else {
3364 throw ValueError(format("Speed of sound is not defined for two-phase states because it depends on the distribution of phases."));
3365 }
3366 } else if (isHomogeneousPhase()) {
3367 // Calculate the reducing parameters
3369 _tau = _reducing.T / _T;
3370
3371 // Calculate derivatives if needed, or just use cached values
3372 CoolPropDbl d2a0_dTau2 = d2alpha0_dTau2();
3373 CoolPropDbl dar_dDelta = dalphar_dDelta();
3374 CoolPropDbl d2ar_dDelta2 = d2alphar_dDelta2();
3375 CoolPropDbl d2ar_dDelta_dTau = d2alphar_dDelta_dTau();
3376 CoolPropDbl d2ar_dTau2 = d2alphar_dTau2();
3377 CoolPropDbl R_u = gas_constant();
3378 CoolPropDbl mm = molar_mass();
3379
3380 // Get speed of sound
3381 _speed_sound = sqrt(
3382 R_u * _T / mm
3383 * (1 + 2 * _delta.pt() * dar_dDelta + pow(_delta.pt(), 2) * d2ar_dDelta2
3384 - pow(1 + _delta.pt() * dar_dDelta - _delta.pt() * _tau.pt() * d2ar_dDelta_dTau, 2) / (pow(_tau.pt(), 2) * (d2ar_dTau2 + d2a0_dTau2))));
3385
3386 return static_cast<CoolPropDbl>(_speed_sound);
3387 } else {
3388 throw ValueError(format("phase is invalid in calc_speed_sound"));
3389 }
3390}
3391
3393 // Calculate the reducing parameters
3396
3397 // Calculate derivatives
3401
3402 // Get molar gibbs function
3403 return gas_constant() * T * (1 + a0 + ar + delta * dar_dDelta);
3404}
3406 if (isTwoPhase()) {
3407 if (!this->SatL || !this->SatV) throw ValueError(format("The saturation properties are needed for the two-phase properties"));
3408 _gibbsmolar = _Q * SatV->gibbsmolar() + (1 - _Q) * SatL->gibbsmolar();
3409 return static_cast<CoolPropDbl>(_gibbsmolar);
3410 } else if (isHomogeneousPhase()) {
3411 // Calculate the reducing parameters
3413 _tau = _reducing.T / _T;
3414
3415 // Calculate derivatives if needed, or just use cached values
3416 CoolPropDbl ar = alphar();
3417 CoolPropDbl a0 = alpha0();
3418 CoolPropDbl dar_dDelta = dalphar_dDelta();
3419 CoolPropDbl R_u = gas_constant();
3420
3421 // Get molar gibbs function
3422 _gibbsmolar = R_u * _T * (1 + a0 + ar + _delta.pt() * dar_dDelta);
3423
3424 return static_cast<CoolPropDbl>(_gibbsmolar);
3425 } else {
3426 throw ValueError(format("phase is invalid in calc_gibbsmolar"));
3427 }
3428}
3431 _umolar_excess = this->umolar();
3432 _volumemolar_excess = 1 / this->rhomolar();
3433 for (std::size_t i = 0; i < components.size(); ++i) {
3434 transient_pure_state = std::make_shared<HelmholtzEOSBackend>(components[i].name);
3435 transient_pure_state->update(PT_INPUTS, p(), T());
3436 double x_i = mole_fractions[i];
3437 double R = gas_constant();
3438 _gibbsmolar_excess = static_cast<double>(_gibbsmolar_excess) - x_i * (transient_pure_state->gibbsmolar() + R * T() * log(x_i));
3439 _hmolar_excess = static_cast<double>(_hmolar_excess) - x_i * transient_pure_state->hmolar();
3440 _umolar_excess = static_cast<double>(_umolar_excess) - x_i * transient_pure_state->umolar();
3441 _smolar_excess = static_cast<double>(_smolar_excess) - x_i * (transient_pure_state->smolar() - R * log(x_i));
3442 _volumemolar_excess = static_cast<double>(_volumemolar_excess) - x_i / transient_pure_state->rhomolar();
3443 }
3444 _helmholtzmolar_excess = static_cast<double>(_umolar_excess) - _T * static_cast<double>(_smolar_excess);
3445}
3446
3448 if (isTwoPhase()) {
3449 if (!this->SatL || !this->SatV) throw ValueError(format("The saturation properties are needed for the two-phase properties"));
3450 _helmholtzmolar = _Q * SatV->helmholtzmolar() + (1 - _Q) * SatL->helmholtzmolar();
3451 return static_cast<CoolPropDbl>(_helmholtzmolar);
3452 } else if (isHomogeneousPhase()) {
3453 // Calculate the reducing parameters
3455 _tau = _reducing.T / _T;
3456
3457 // Calculate derivatives if needed, or just use cached values
3458 CoolPropDbl ar = alphar();
3459 CoolPropDbl a0 = alpha0();
3460 CoolPropDbl R_u = gas_constant();
3461
3462 // Get molar Helmholtz energy
3463 _helmholtzmolar = R_u * _T * (a0 + ar);
3464
3465 return static_cast<CoolPropDbl>(_helmholtzmolar);
3466 } else {
3467 throw ValueError(format("phase is invalid in calc_helmholtzmolar"));
3468 }
3469}
3472 if (isTwoPhase()) {
3473 // phi_i = f_i / (x_i * p). At VLE f_i^L == f_i^V but x_i^L != x_i^V, so
3474 // phi_i^L != phi_i^V and no convex combination is physically meaningful for
3475 // the overall two-phase state. Force callers to evaluate on SatL or SatV.
3476 throw ValueError(format("fugacity_coefficient is not well-defined in the two-phase region; evaluate on SatL or SatV instead"));
3477 } else if (isHomogeneousPhase()) {
3478 return exp(MixtureDerivatives::ln_fugacity_coefficient(*this, i, xN_flag));
3479 } else {
3480 throw ValueError(format("phase is invalid in calc_fugacity_coefficient"));
3481 }
3482}
3485 CoolPropDbl localFug;
3486 if (isTwoPhase()) {
3487 if (!this->SatL || !this->SatV) throw ValueError(format("The saturation properties are needed for the two-phase properties"));
3488 if (std::abs(_Q) < DBL_EPSILON) {
3489 localFug = SatL->fugacity(i);
3490 } else if (std::abs(_Q - 1) < DBL_EPSILON) {
3491 localFug = SatV->fugacity(i);
3492 } else {
3493 localFug = _Q * SatV->fugacity(i) + (1 - _Q) * SatL->fugacity(i);
3494 }
3495 return static_cast<CoolPropDbl>(localFug);
3496 } else if (isHomogeneousPhase()) {
3497 return MixtureDerivatives::fugacity_i(*this, i, xN_flag);
3498 } else {
3499 throw ValueError(format("phase is invalid in calc_fugacity"));
3500 }
3501}
3504 if (isTwoPhase()) {
3505 // mu_i^L == mu_i^V at VLE (equality of chemical potentials is the equilibrium
3506 // condition), so the Q-weighted form collapses to either sat-state value up to
3507 // flash tolerance. Mirrors the calc_fugacity pattern from PR #2345.
3508 if (!this->SatL || !this->SatV) throw ValueError(format("The saturation properties are needed for the two-phase properties"));
3509 if (std::abs(_Q) < DBL_EPSILON) {
3510 return SatL->chemical_potential(i);
3511 } else if (std::abs(_Q - 1) < DBL_EPSILON) {
3512 return SatV->chemical_potential(i);
3513 } else {
3514 return _Q * SatV->chemical_potential(i) + (1 - _Q) * SatL->chemical_potential(i);
3515 }
3516 } else if (isHomogeneousPhase()) {
3517 double Tci = get_fluid_constant(i, iT_critical);
3518 double rhoci = get_fluid_constant(i, irhomolar_critical);
3519 double dnar_dni__const_T_V_nj = MixtureDerivatives::dnalphar_dni__constT_V_nj(*this, i, xN_flag);
3520 double dna0_dni__const_T_V_nj =
3521 components[i].EOS().alpha0.base(tau() * (Tci / T_reducing()), delta() / (rhoci / rhomolar_reducing())) + 1 + log(mole_fractions[i]);
3522 return gas_constant() * T() * (dna0_dni__const_T_V_nj + dnar_dni__const_T_V_nj);
3523 } else {
3524 throw ValueError(format("phase is invalid in calc_chemical_potential"));
3525 }
3526}
3528 return 2
3529 - rhomolar()
3532}
3533
3534SimpleState HelmholtzEOSMixtureBackend::calc_reducing_state_nocache(const std::vector<CoolPropDbl>& mole_fractions) {
3535 SimpleState reducing;
3537 reducing = components[0].EOS().reduce;
3538 } else {
3539 reducing.T = Reducing->Tr(mole_fractions);
3540 reducing.rhomolar = Reducing->rhormolar(mole_fractions);
3541 }
3542 return reducing;
3543}
3545 if (get_mole_fractions_ref().empty()) {
3546 throw ValueError("Mole fractions must be set before calling calc_reducing_state");
3547 }
3550 _crit = _reducing;
3551}
3552void HelmholtzEOSMixtureBackend::calc_all_alphar_deriv_cache(const std::vector<CoolPropDbl>& mole_fractions, const CoolPropDbl& tau,
3553 const CoolPropDbl& delta) {
3554 deriv_counter.fetch_add(1, std::memory_order_relaxed);
3555 bool cache_values = true;
3556 HelmholtzDerivatives derivs = residual_helmholtz->all(*this, get_mole_fractions_ref(), tau, delta, cache_values);
3557 _alphar = derivs.alphar;
3558 _dalphar_dDelta = derivs.dalphar_ddelta;
3559 _dalphar_dTau = derivs.dalphar_dtau;
3560 _d2alphar_dDelta2 = derivs.d2alphar_ddelta2;
3561 _d2alphar_dDelta_dTau = derivs.d2alphar_ddelta_dtau;
3562 _d2alphar_dTau2 = derivs.d2alphar_dtau2;
3563 _d3alphar_dDelta3 = derivs.d3alphar_ddelta3;
3564 _d3alphar_dDelta2_dTau = derivs.d3alphar_ddelta2_dtau;
3565 _d3alphar_dDelta_dTau2 = derivs.d3alphar_ddelta_dtau2;
3566 _d3alphar_dTau3 = derivs.d3alphar_dtau3;
3567 _d4alphar_dDelta4 = derivs.d4alphar_ddelta4;
3568 _d4alphar_dDelta3_dTau = derivs.d4alphar_ddelta3_dtau;
3569 _d4alphar_dDelta2_dTau2 = derivs.d4alphar_ddelta2_dtau2;
3570 _d4alphar_dDelta_dTau3 = derivs.d4alphar_ddelta_dtau3;
3571 _d4alphar_dTau4 = derivs.d4alphar_dtau4;
3572}
3573
3574CoolPropDbl HelmholtzEOSMixtureBackend::calc_alphar_deriv_nocache(const int nTau, const int nDelta, const std::vector<CoolPropDbl>& mole_fractions,
3575 const CoolPropDbl& tau, const CoolPropDbl& delta) {
3576 bool cache_values = false;
3577 HelmholtzDerivatives derivs = residual_helmholtz->all(*this, mole_fractions, tau, delta, cache_values);
3578 return derivs.get(nTau, nDelta);
3579}
3580CoolPropDbl HelmholtzEOSMixtureBackend::calc_alpha0_deriv_nocache(const int nTau, const int nDelta, const std::vector<CoolPropDbl>& mole_fractions,
3581 const CoolPropDbl& tau, const CoolPropDbl& delta, const CoolPropDbl& Tr,
3582 const CoolPropDbl& rhor) {
3583 CoolPropDbl val = NAN;
3584 if (components.size() == 0) {
3585 throw ValueError("No alpha0 derivatives are available");
3586 }
3588 EquationOfState& E = components[0].EOS();
3589 // In the case of cubics, we need to use the shifted tau^*=Tc/T and delta^*=rho/rhoc
3590 // rather than tau=Tr/T and delta=rho/rhor
3591 // For multiparameter EOS, this changes nothing because Tc/Tr = 1 and rhoc/rhor = 1
3593
3594 // Cache the reducing temperature in some terms that need it (GERG-2004 models)
3595 E.alpha0.set_Tred(Tc);
3596 double taustar = Tc / Tr * tau, deltastar = rhor / rhomolarc * delta;
3597 if (nTau == 0 && nDelta == 0) {
3598 val = E.base0(taustar, deltastar);
3599 } else if (nTau == 0 && nDelta == 1) {
3600 val = E.dalpha0_dDelta(taustar, deltastar);
3601 } else if (nTau == 1 && nDelta == 0) {
3602 val = E.dalpha0_dTau(taustar, deltastar);
3603 } else if (nTau == 0 && nDelta == 2) {
3604 val = E.d2alpha0_dDelta2(taustar, deltastar);
3605 } else if (nTau == 1 && nDelta == 1) {
3606 val = E.d2alpha0_dDelta_dTau(taustar, deltastar);
3607 } else if (nTau == 2 && nDelta == 0) {
3608 val = E.d2alpha0_dTau2(taustar, deltastar);
3609 } else if (nTau == 0 && nDelta == 3) {
3610 val = E.d3alpha0_dDelta3(taustar, deltastar);
3611 } else if (nTau == 1 && nDelta == 2) {
3612 val = E.d3alpha0_dDelta2_dTau(taustar, deltastar);
3613 } else if (nTau == 2 && nDelta == 1) {
3614 val = E.d3alpha0_dDelta_dTau2(taustar, deltastar);
3615 } else if (nTau == 3 && nDelta == 0) {
3616 val = E.d3alpha0_dTau3(taustar, deltastar);
3617 } else {
3618 throw ValueError();
3619 }
3620 val *= pow(rhor / rhomolarc, nDelta);
3621 val /= pow(Tr / Tc, nTau);
3622 if (!ValidNumber(val)) {
3623 //calc_alpha0_deriv_nocache(nTau,nDelta,mole_fractions,tau,delta,Tr,rhor);
3624 throw ValueError(format("calc_alpha0_deriv_nocache returned invalid number with inputs nTau: %d, nDelta: %d, tau: %Lg, delta: %Lg", nTau,
3625 nDelta, tau, delta));
3626 } else {
3627 return val;
3628 }
3629 } else {
3630 // See Table B5, GERG 2008 from Kunz Wagner, JCED, 2012
3631 std::size_t N = mole_fractions.size();
3632 CoolPropDbl summer = 0;
3633 CoolPropDbl tau_i = NAN, delta_i = NAN, rho_ci = NAN, T_ci = NAN;
3634 CoolPropDbl Rmix = gas_constant();
3635 for (unsigned int i = 0; i < N; ++i) {
3636
3640 tau_i = T_ci * tau / Tr;
3641 delta_i = delta * rhor / rho_ci;
3642 CoolPropDbl Rratio = Rcomponent / Rmix;
3643
3644 // Cache the reducing temperature in some terms that need it (GERG-2004 models)
3645 components[i].EOS().alpha0.set_Tred(Tr);
3646
3647 if (nTau == 0 && nDelta == 0) {
3648 double logxi = (std::abs(mole_fractions[i]) > DBL_EPSILON) ? log(mole_fractions[i]) : 0;
3649 summer += mole_fractions[i] * Rratio * (components[i].EOS().base0(tau_i, delta_i) + logxi);
3650 } else if (nTau == 0 && nDelta == 1) {
3651 summer += mole_fractions[i] * Rratio * rhor / rho_ci * components[i].EOS().dalpha0_dDelta(tau_i, delta_i);
3652 } else if (nTau == 1 && nDelta == 0) {
3653 summer += mole_fractions[i] * Rratio * T_ci / Tr * components[i].EOS().dalpha0_dTau(tau_i, delta_i);
3654 } else if (nTau == 0 && nDelta == 2) {
3655 summer += mole_fractions[i] * Rratio * pow(rhor / rho_ci, 2) * components[i].EOS().d2alpha0_dDelta2(tau_i, delta_i);
3656 } else if (nTau == 1 && nDelta == 1) {
3657 summer += mole_fractions[i] * Rratio * rhor / rho_ci * T_ci / Tr * components[i].EOS().d2alpha0_dDelta_dTau(tau_i, delta_i);
3658 } else if (nTau == 2 && nDelta == 0) {
3659 summer += mole_fractions[i] * Rratio * pow(T_ci / Tr, 2) * components[i].EOS().d2alpha0_dTau2(tau_i, delta_i);
3660 } else {
3661 throw ValueError();
3662 }
3663 }
3664 return summer;
3665 }
3666}
3667
3669 const CoolPropDbl& tau, const CoolPropDbl& delta,
3670 const CoolPropDbl& Tr, const CoolPropDbl& rhor) {
3671 if (components.size() == 0) {
3672 throw ValueError("No alpha0 derivatives are available");
3673 }
3675 EquationOfState& E = components[0].EOS();
3676 // In the case of cubics, we need to use the shifted tau^*=Tc/T and delta^*=rho/rhoc
3677 // rather than tau=Tr/T and delta=rho/rhor
3678 // For multiparameter EOS, this changes nothing because Tc/Tr = 1 and rhoc/rhor = 1
3680
3681 // Cache the reducing temperature in some terms that need it (GERG-2004 models)
3682 E.alpha0.set_Tred(Tc);
3683 double taustar = Tc / Tr * tau, deltastar = rhor / rhomolarc * delta;
3684 return E.alpha0.all(taustar, deltastar, false);
3685 } else {
3687
3688 // See Table B5, GERG 2008 from Kunz Wagner, JCED, 2012
3689 std::size_t N = mole_fractions.size();
3690 CoolPropDbl summer_00 = 0, summer_01 = 0, summer_10 = 0, summer_02 = 0, summer_11 = 0, summer_20 = 0;
3691 CoolPropDbl tau_i = NAN, delta_i = NAN, rho_ci = NAN, T_ci = NAN;
3692 CoolPropDbl Rmix = gas_constant();
3693 for (unsigned int i = 0; i < N; ++i) {
3694
3698 tau_i = T_ci * tau / Tr;
3699 delta_i = delta * rhor / rho_ci;
3700 CoolPropDbl Rratio = Rcomponent / Rmix;
3701
3702 // Cache the reducing temperature in some terms that need it (GERG-2004 models)
3703 components[i].EOS().alpha0.set_Tred(Tr);
3704 {
3705 // All the ideal-gas derivatives for the pure component
3706 auto pure = components[i].EOS().alpha0.all(tau_i, delta_i);
3707
3708 double logxi = (std::abs(mole_fractions[i]) > DBL_EPSILON) ? log(mole_fractions[i]) : 0;
3709 // Note: you see alphar here, that is a book-keeping artifact, it is really alpha for ideal gas
3710 summer_00 += mole_fractions[i] * Rratio * (pure.alphar + logxi);
3711 summer_01 += mole_fractions[i] * Rratio * rhor / rho_ci * pure.dalphar_ddelta;
3712 summer_10 += mole_fractions[i] * Rratio * T_ci / Tr * pure.dalphar_dtau;
3713 summer_02 += mole_fractions[i] * Rratio * pow(rhor / rho_ci, 2) * pure.d2alphar_ddelta2;
3714 summer_11 += mole_fractions[i] * Rratio * rhor / rho_ci * T_ci / Tr * pure.d2alphar_ddelta_dtau;
3715 summer_20 += mole_fractions[i] * Rratio * pow(T_ci / Tr, 2) * pure.d2alphar_dtau2;
3716 }
3717 }
3718 // Note: you see alphar here, that is a book-keeping artifact, it is really alpha for ideal gas
3719 ders.alphar = summer_00;
3720 ders.dalphar_ddelta = summer_01;
3721 ders.dalphar_dtau = summer_10;
3722 ders.d2alphar_ddelta2 = summer_02;
3723 ders.d2alphar_ddelta_dtau = summer_11;
3724 ders.d2alphar_dtau2 = summer_20;
3725
3726 return ders;
3727 }
3728}
3729
3732 return static_cast<CoolPropDbl>(_alphar);
3733}
3736 return static_cast<CoolPropDbl>(_dalphar_dDelta);
3737}
3740 return static_cast<CoolPropDbl>(_dalphar_dTau);
3741}
3744 return static_cast<CoolPropDbl>(_d2alphar_dTau2);
3745}
3748 return static_cast<CoolPropDbl>(_d2alphar_dDelta_dTau);
3749}
3752 return static_cast<CoolPropDbl>(_d2alphar_dDelta2);
3753}
3756 return static_cast<CoolPropDbl>(_d3alphar_dDelta3);
3757}
3760 return static_cast<CoolPropDbl>(_d3alphar_dDelta2_dTau);
3761}
3764 return static_cast<CoolPropDbl>(_d3alphar_dDelta_dTau2);
3765}
3768 return static_cast<CoolPropDbl>(_d3alphar_dTau3);
3769}
3770
3773 return static_cast<CoolPropDbl>(_d4alphar_dDelta4);
3774}
3777 return static_cast<CoolPropDbl>(_d4alphar_dDelta3_dTau);
3778}
3781 return static_cast<CoolPropDbl>(_d4alphar_dDelta2_dTau2);
3782}
3785 return static_cast<CoolPropDbl>(_d4alphar_dDelta_dTau3);
3786}
3789 return static_cast<CoolPropDbl>(_d4alphar_dTau4);
3790}
3791
3793 const int nTau = 0, nDelta = 0;
3795}
3797 const int nTau = 0, nDelta = 1;
3799}
3801 const int nTau = 1, nDelta = 0;
3803}
3805 const int nTau = 0, nDelta = 2;
3807}
3809 const int nTau = 1, nDelta = 1;
3811}
3813 const int nTau = 2, nDelta = 0;
3815}
3817 const int nTau = 0, nDelta = 3;
3819}
3821 const int nTau = 1, nDelta = 2;
3823}
3825 const int nTau = 2, nDelta = 1;
3827}
3829 const int nTau = 3, nDelta = 0;
3831}
3834 // Derivative of temperature w.r.t. pressure ALONG the saturation curve
3835 CoolPropDbl dTdP_sat;
3837 dTdP_sat = T() * (1 / SatV.rhomolar() - 1 / SatL.rhomolar()) / (SatV.hmolar() - SatL.hmolar());
3838 } else {
3839 // Mixture: dT/dP along the bubble (Q=0) or dew (Q=1) curve
3840 // (Gernert thesis 3.96 and 3.97). The compositions used as
3841 // weights are those of the OTHER phase.
3842 std::vector<CoolPropDbl> x = SatL.get_mole_fractions();
3843 std::vector<CoolPropDbl> y = SatV.get_mole_fractions();
3844 CoolPropDbl dQ_dPsat = 0, dQ_dTsat = 0;
3846 if (_Q == 0) {
3847 for (std::size_t i = 0; i < N; ++i) {
3848 dQ_dPsat += y[i]
3851 dQ_dTsat += y[i]
3854 }
3855 } else if (_Q == 1) {
3856 for (std::size_t i = 0; i < N; ++i) {
3857 dQ_dPsat += x[i]
3860 dQ_dTsat += x[i]
3863 }
3864 } else {
3865 throw ValueError(format("calc_first_saturation_deriv requires Q==0 (bubble) or Q==1 (dew) for mixtures; got Q=%g", _Q));
3866 }
3867 dTdP_sat = -dQ_dPsat / dQ_dTsat;
3868 }
3869
3870 // "Trivial" inputs
3871 if (Of1 == iT && Wrt1 == iP) {
3872 return dTdP_sat;
3873 } else if (Of1 == iP && Wrt1 == iT) {
3874 return 1 / dTdP_sat;
3875 }
3876 // Derivative taken with respect to T
3877 else if (Wrt1 == iT) {
3878 return first_partial_deriv(Of1, iT, iP) + first_partial_deriv(Of1, iP, iT) / dTdP_sat;
3879 }
3880 // Derivative taken with respect to p
3881 else if (Wrt1 == iP) {
3882 return first_partial_deriv(Of1, iP, iT) + first_partial_deriv(Of1, iT, iP) * dTdP_sat;
3883 } else {
3884 throw ValueError(
3885 format("Not possible to take first saturation derivative with respect to %s", get_parameter_information(Wrt1, "short").c_str()));
3886 }
3887}
3889 if (!this->SatL || !this->SatV) throw ValueError(format("The saturation properties are needed for calc_first_saturation_deriv"));
3890 return calc_first_saturation_deriv(Of1, Wrt1, this->get_SatL(), this->get_SatV());
3891}
3893 if (!this->SatL || !this->SatV) throw ValueError(format("The saturation properties are needed for calc_second_saturation_deriv"));
3894 if (Wrt1 == iP && Wrt2 == iP) {
3895 CoolPropDbl dydT_constp = this->first_partial_deriv(Of1, iT, iP);
3896 CoolPropDbl d2ydTdp = this->second_partial_deriv(Of1, iT, iP, iP, iT);
3897 CoolPropDbl d2ydp2_constT = this->second_partial_deriv(Of1, iP, iT, iP, iT);
3898 CoolPropDbl d2ydT2_constp = this->second_partial_deriv(Of1, iT, iP, iT, iP);
3899
3900 CoolPropDbl dTdp_along_sat = calc_first_saturation_deriv(iT, iP);
3901 CoolPropDbl dvdrhoL = -1 / POW2(SatL->rhomolar());
3902 CoolPropDbl dvdrhoV = -1 / POW2(SatV->rhomolar());
3903 CoolPropDbl DELTAv = 1 / SatV->rhomolar() - 1 / SatL->rhomolar();
3904 CoolPropDbl dDELTAv_dT_constp = dvdrhoV * SatV->first_partial_deriv(iDmolar, iT, iP) - dvdrhoL * SatL->first_partial_deriv(iDmolar, iT, iP);
3905 CoolPropDbl dDELTAv_dp_constT = dvdrhoV * SatV->first_partial_deriv(iDmolar, iP, iT) - dvdrhoL * SatL->first_partial_deriv(iDmolar, iP, iT);
3906 CoolPropDbl DELTAh = SatV->hmolar() - SatL->hmolar();
3907 CoolPropDbl dDELTAh_dT_constp = SatV->first_partial_deriv(iHmolar, iT, iP) - SatL->first_partial_deriv(iHmolar, iT, iP);
3908 CoolPropDbl dDELTAh_dp_constT = SatV->first_partial_deriv(iHmolar, iP, iT) - SatL->first_partial_deriv(iHmolar, iP, iT);
3909 CoolPropDbl ddT_dTdp_along_sat_constp = (DELTAh * (_T * dDELTAv_dT_constp + DELTAv) - _T * DELTAv * dDELTAh_dT_constp) / POW2(DELTAh);
3910 CoolPropDbl ddp_dTdp_along_sat_constT = (DELTAh * (_T * dDELTAv_dp_constT) - _T * DELTAv * dDELTAh_dp_constT) / POW2(DELTAh);
3911
3912 double ddp_dydpsigma = d2ydp2_constT + dydT_constp * ddp_dTdp_along_sat_constT + d2ydTdp * dTdp_along_sat;
3913 double ddT_dydpsigma = d2ydTdp + dydT_constp * ddT_dTdp_along_sat_constp + d2ydT2_constp * dTdp_along_sat;
3914 return ddp_dydpsigma + ddT_dydpsigma * dTdp_along_sat;
3915 } else {
3916 throw ValueError(format("Currently, only possible to take second saturation derivative w.r.t. P (both times)"));
3917 }
3918}
3919
3921 parameters Constant2) {
3922 if (!this->SatL || !this->SatV) throw ValueError(format("The saturation properties are needed for calc_second_two_phase_deriv"));
3923
3924 if (Of == iDmolar
3925 && ((Wrt1 == iHmolar && Constant1 == iP && Wrt2 == iP && Constant2 == iHmolar)
3926 || (Wrt2 == iHmolar && Constant2 == iP && Wrt1 == iP && Constant1 == iHmolar))) {
3927 parameters h_key = iHmolar, rho_key = iDmolar, p_key = iP;
3928 // taking the derivative of (drho/dv)*(dv/dh|p) with respect to p with h constant
3929 CoolPropDbl dv_dh_constp = calc_first_two_phase_deriv(rho_key, h_key, p_key) / (-POW2(rhomolar()));
3930 CoolPropDbl drhomolar_dp__consth = first_two_phase_deriv(rho_key, p_key, h_key);
3931
3932 // Calculate the derivative of dvdh|p with respect to p at constant h
3933 CoolPropDbl dhL_dp_sat = SatL->calc_first_saturation_deriv(h_key, p_key, *SatL, *SatV);
3934 CoolPropDbl dhV_dp_sat = SatV->calc_first_saturation_deriv(h_key, p_key, *SatL, *SatV);
3935 CoolPropDbl drhoL_dp_sat = SatL->calc_first_saturation_deriv(rho_key, p_key, *SatL, *SatV);
3936 CoolPropDbl drhoV_dp_sat = SatV->calc_first_saturation_deriv(rho_key, p_key, *SatL, *SatV);
3937 CoolPropDbl numerator = 1 / SatV->keyed_output(rho_key) - 1 / SatL->keyed_output(rho_key);
3938 CoolPropDbl denominator = SatV->keyed_output(h_key) - SatL->keyed_output(h_key);
3939 CoolPropDbl dnumerator = -1 / POW2(SatV->keyed_output(rho_key)) * drhoV_dp_sat + 1 / POW2(SatL->keyed_output(rho_key)) * drhoL_dp_sat;
3940 CoolPropDbl ddenominator = dhV_dp_sat - dhL_dp_sat;
3941 CoolPropDbl d_dvdh_dp__consth = (denominator * dnumerator - numerator * ddenominator) / POW2(denominator);
3942 return -POW2(rhomolar()) * d_dvdh_dp__consth + dv_dh_constp * (-2 * rhomolar()) * drhomolar_dp__consth;
3943 } else if (Of == iDmass
3944 && ((Wrt1 == iHmass && Constant1 == iP && Wrt2 == iP && Constant2 == iHmass)
3945 || (Wrt2 == iHmass && Constant2 == iP && Wrt1 == iP && Constant1 == iHmass))) {
3946 parameters h_key = iHmass, rho_key = iDmass, p_key = iP;
3947 CoolPropDbl rho = keyed_output(rho_key);
3948 // taking the derivative of (drho/dv)*(dv/dh|p) with respect to p with h constant
3949 CoolPropDbl dv_dh_constp = calc_first_two_phase_deriv(rho_key, h_key, p_key) / (-POW2(rho));
3950 CoolPropDbl drho_dp__consth = first_two_phase_deriv(rho_key, p_key, h_key);
3951
3952 // Calculate the derivative of dvdh|p with respect to p at constant h
3953 CoolPropDbl dhL_dp_sat = SatL->calc_first_saturation_deriv(h_key, p_key, *SatL, *SatV);
3954 CoolPropDbl dhV_dp_sat = SatV->calc_first_saturation_deriv(h_key, p_key, *SatL, *SatV);
3955 CoolPropDbl drhoL_dp_sat = SatL->calc_first_saturation_deriv(rho_key, p_key, *SatL, *SatV);
3956 CoolPropDbl drhoV_dp_sat = SatV->calc_first_saturation_deriv(rho_key, p_key, *SatL, *SatV);
3957 CoolPropDbl numerator = 1 / SatV->keyed_output(rho_key) - 1 / SatL->keyed_output(rho_key);
3958 CoolPropDbl denominator = SatV->keyed_output(h_key) - SatL->keyed_output(h_key);
3959 CoolPropDbl dnumerator = -1 / POW2(SatV->keyed_output(rho_key)) * drhoV_dp_sat + 1 / POW2(SatL->keyed_output(rho_key)) * drhoL_dp_sat;
3960 CoolPropDbl ddenominator = dhV_dp_sat - dhL_dp_sat;
3961 CoolPropDbl d_dvdh_dp__consth = (denominator * dnumerator - numerator * ddenominator) / POW2(denominator);
3962 return -POW2(rho) * d_dvdh_dp__consth + dv_dh_constp * (-2 * rho) * drho_dp__consth;
3963 } else {
3964 throw ValueError();
3965 }
3966}
3968 if (!this->SatL || !this->SatV) throw ValueError(format("The saturation properties are needed for calc_first_two_phase_deriv"));
3969 if (Of == iDmolar && Wrt == iHmolar && Constant == iP) {
3970 return -POW2(rhomolar()) * (1 / SatV->rhomolar() - 1 / SatL->rhomolar()) / (SatV->hmolar() - SatL->hmolar());
3971 } else if (Of == iDmass && Wrt == iHmass && Constant == iP) {
3972 return -POW2(rhomass()) * (1 / SatV->rhomass() - 1 / SatL->rhomass()) / (SatV->hmass() - SatL->hmass());
3973 } else if (Of == iDmolar && Wrt == iP && Constant == iHmolar) {
3974 // v = 1/rho; drhodv = -rho^2; dvdrho = -1/rho^2
3975 CoolPropDbl dvdrhoL = -1 / POW2(SatL->rhomolar());
3976 CoolPropDbl dvdrhoV = -1 / POW2(SatV->rhomolar());
3977 CoolPropDbl dvL_dp = dvdrhoL * SatL->calc_first_saturation_deriv(iDmolar, iP, *SatL, *SatV);
3978 CoolPropDbl dvV_dp = dvdrhoV * SatV->calc_first_saturation_deriv(iDmolar, iP, *SatL, *SatV);
3979 CoolPropDbl dhL_dp = SatL->calc_first_saturation_deriv(iHmolar, iP, *SatL, *SatV);
3980 CoolPropDbl dhV_dp = SatV->calc_first_saturation_deriv(iHmolar, iP, *SatL, *SatV);
3981 CoolPropDbl dxdp_h = (Q() * dhV_dp + (1 - Q()) * dhL_dp) / (SatL->hmolar() - SatV->hmolar());
3982 CoolPropDbl dvdp_h = dvL_dp + dxdp_h * (1 / SatV->rhomolar() - 1 / SatL->rhomolar()) + Q() * (dvV_dp - dvL_dp);
3983 return -POW2(rhomolar()) * dvdp_h;
3984 } else if (Of == iDmass && Wrt == iP && Constant == iHmass) {
3985 // v = 1/rho; drhodv = -rho^2; dvdrho = -1/rho^2
3986 CoolPropDbl dvdrhoL = -1 / POW2(SatL->rhomass());
3987 CoolPropDbl dvdrhoV = -1 / POW2(SatV->rhomass());
3988 CoolPropDbl dvL_dp = dvdrhoL * SatL->calc_first_saturation_deriv(iDmass, iP, *SatL, *SatV);
3989 CoolPropDbl dvV_dp = dvdrhoV * SatV->calc_first_saturation_deriv(iDmass, iP, *SatL, *SatV);
3990 CoolPropDbl dhL_dp = SatL->calc_first_saturation_deriv(iHmass, iP, *SatL, *SatV);
3991 CoolPropDbl dhV_dp = SatV->calc_first_saturation_deriv(iHmass, iP, *SatL, *SatV);
3992 CoolPropDbl dxdp_h = (Q() * dhV_dp + (1 - Q()) * dhL_dp) / (SatL->hmass() - SatV->hmass());
3993 CoolPropDbl dvdp_h = dvL_dp + dxdp_h * (1 / SatV->rhomass() - 1 / SatL->rhomass()) + Q() * (dvV_dp - dvL_dp);
3994 return -POW2(rhomass()) * dvdp_h;
3995 } else {
3996 throw ValueError("These inputs are not supported to calc_first_two_phase_deriv");
3997 }
3998}
4000 // Note: If you need all three values (drho_dh__p, drho_dp__h and rho_spline),
4001 // you should calculate drho_dp__h first to avoid duplicate calculations.
4002
4003 bool drho_dh__p = false;
4004 bool drho_dp__h = false;
4005 bool rho_spline = false;
4006
4007 if (Of == iDmolar && Wrt == iHmolar && Constant == iP) {
4008 drho_dh__p = true;
4010 } else if (Of == iDmass && Wrt == iHmass && Constant == iP) {
4012 } else if (Of == iDmolar && Wrt == iP && Constant == iHmolar) {
4013 drho_dp__h = true;
4015 } else if (Of == iDmass && Wrt == iP && Constant == iHmass) {
4017 }
4018 // Add the special case for the splined density
4019 else if (Of == iDmolar && Wrt == iDmolar && Constant == iDmolar) {
4020 rho_spline = true;
4021 if (_rho_spline) return _rho_spline;
4022 } else if (Of == iDmass && Wrt == iDmass && Constant == iDmass) {
4024 } else {
4025 throw ValueError("These inputs are not supported to calc_first_two_phase_deriv");
4026 }
4027
4028 if (!this->SatL || !this->SatV) throw ValueError(format("The saturation properties are needed for calc_first_two_phase_deriv_splined"));
4029 if (_Q > x_end) {
4030 throw ValueError(format("Q [%g] is greater than x_end [%Lg]", _Q, x_end).c_str());
4031 }
4032 if (_phase != iphase_twophase) {
4033 throw ValueError(format("state is not two-phase"));
4034 }
4035
4036 shared_ptr<HelmholtzEOSMixtureBackend> Liq = std::make_shared<HelmholtzEOSMixtureBackend>(this->get_components());
4037 shared_ptr<HelmholtzEOSMixtureBackend> End = std::make_shared<HelmholtzEOSMixtureBackend>(this->get_components());
4038
4039 Liq->specify_phase(iphase_liquid);
4040 Liq->_Q = -1;
4041 Liq->update_DmolarT_direct(SatL->rhomolar(), SatL->T());
4042 End->update(QT_INPUTS, x_end, SatL->T());
4043
4044 CoolPropDbl Delta = Q() * (SatV->keyed_output(iHmolar) - SatL->keyed_output(iHmolar));
4045 CoolPropDbl Delta_end = End->keyed_output(iHmolar) - SatL->keyed_output(iHmolar);
4046
4047 // At the end of the zone to which spline is applied
4048 CoolPropDbl drho_dh_end = End->calc_first_two_phase_deriv(iDmolar, iHmolar, iP);
4049 CoolPropDbl rho_end = End->keyed_output(iDmolar);
4050
4051 // Faking single-phase
4052 CoolPropDbl rho_liq = Liq->keyed_output(iDmolar);
4053 CoolPropDbl drho_dh_liq__constp = Liq->first_partial_deriv(iDmolar, iHmolar, iP);
4054
4055 // Spline coordinates a, b, c, d
4056 CoolPropDbl Abracket = (2 * rho_liq - 2 * rho_end + Delta_end * (drho_dh_liq__constp + drho_dh_end));
4057 CoolPropDbl a = 1 / POW3(Delta_end) * Abracket;
4058 CoolPropDbl b = 3 / POW2(Delta_end) * (-rho_liq + rho_end) - 1 / Delta_end * (drho_dh_end + 2 * drho_dh_liq__constp);
4059 CoolPropDbl c = drho_dh_liq__constp;
4060 CoolPropDbl d = rho_liq;
4061
4062 // Either the spline value or drho/dh|p can be directly evaluated now
4063 _rho_spline = a * POW3(Delta) + b * POW2(Delta) + c * Delta + d;
4064 _drho_spline_dh__constp = 3 * a * POW2(Delta) + 2 * b * Delta + c;
4065 if (rho_spline) return _rho_spline;
4066 if (drho_dh__p) return _drho_spline_dh__constp;
4067
4068 // It's drho/dp|h
4069 // ... calculate some more things
4070
4071 // Derivatives *along* the saturation curve using the special internal method
4072 CoolPropDbl dhL_dp_sat = SatL->calc_first_saturation_deriv(iHmolar, iP, *SatL, *SatV);
4073 CoolPropDbl dhV_dp_sat = SatV->calc_first_saturation_deriv(iHmolar, iP, *SatL, *SatV);
4074 CoolPropDbl drhoL_dp_sat = SatL->calc_first_saturation_deriv(iDmolar, iP, *SatL, *SatV);
4075 CoolPropDbl drhoV_dp_sat = SatV->calc_first_saturation_deriv(iDmolar, iP, *SatL, *SatV);
4076 CoolPropDbl rhoV = SatV->keyed_output(iDmolar);
4077 CoolPropDbl rhoL = SatL->keyed_output(iDmolar);
4078 CoolPropDbl drho_dp_end = POW2(End->keyed_output(iDmolar)) * (x_end / POW2(rhoV) * drhoV_dp_sat + (1 - x_end) / POW2(rhoL) * drhoL_dp_sat);
4079
4080 // Faking single-phase
4081 //CoolPropDbl drho_dp__consth_liq = Liq->first_partial_deriv(iDmolar, iP, iHmolar);
4082 CoolPropDbl d2rhodhdp_liq = Liq->second_partial_deriv(iDmolar, iHmolar, iP, iP, iHmolar); // ?
4083
4084 // Derivatives at the end point
4085 // CoolPropDbl drho_dp__consth_end = End->calc_first_two_phase_deriv(iDmolar, iP, iHmolar);
4086 CoolPropDbl d2rhodhdp_end = End->calc_second_two_phase_deriv(iDmolar, iHmolar, iP, iP, iHmolar);
4087
4088 // Reminder:
4089 // Delta = Q()*(hV-hL) = h-hL
4090 // Delta_end = x_end*(hV-hL);
4091 CoolPropDbl d_Delta_dp__consth = -dhL_dp_sat;
4092 CoolPropDbl d_Delta_end_dp__consth = x_end * (dhV_dp_sat - dhL_dp_sat);
4093
4094 // First pressure derivative at constant h of the coefficients a,b,c,d
4095 // CoolPropDbl Abracket = (2*rho_liq - 2*rho_end + Delta_end * (drho_dh_liq__constp + drho_dh_end));
4096 CoolPropDbl d_Abracket_dp_consth = (2 * drhoL_dp_sat - 2 * drho_dp_end + Delta_end * (d2rhodhdp_liq + d2rhodhdp_end)
4097 + d_Delta_end_dp__consth * (drho_dh_liq__constp + drho_dh_end));
4098 CoolPropDbl da_dp = 1 / POW3(Delta_end) * d_Abracket_dp_consth + Abracket * (-3 / POW4(Delta_end) * d_Delta_end_dp__consth);
4099 CoolPropDbl db_dp = -6 / POW3(Delta_end) * d_Delta_end_dp__consth * (rho_end - rho_liq) + 3 / POW2(Delta_end) * (drho_dp_end - drhoL_dp_sat)
4100 + (1 / POW2(Delta_end) * d_Delta_end_dp__consth) * (drho_dh_end + 2 * drho_dh_liq__constp)
4101 - (1 / Delta_end) * (d2rhodhdp_end + 2 * d2rhodhdp_liq);
4102 CoolPropDbl dc_dp = d2rhodhdp_liq;
4103 CoolPropDbl dd_dp = drhoL_dp_sat;
4104
4106 (3 * a * POW2(Delta) + 2 * b * Delta + c) * d_Delta_dp__consth + POW3(Delta) * da_dp + POW2(Delta) * db_dp + Delta * dc_dp + dd_dp;
4107 if (drho_dp__h) return _drho_spline_dp__consth;
4108
4109 throw ValueError("Something went wrong in HelmholtzEOSMixtureBackend::calc_first_two_phase_deriv_splined");
4110 return _HUGE;
4111}
4112
4114 class Resid : public FuncWrapperND
4115 {
4116 public:
4118 double L1, M1;
4119 Eigen::MatrixXd Lstar, Mstar;
4120 Resid(HelmholtzEOSMixtureBackend& HEOS) : HEOS(HEOS), L1(_HUGE), M1(_HUGE) {};
4121 std::vector<double> call(const std::vector<double>& tau_delta) override {
4122 double rhomolar = tau_delta[1] * HEOS.rhomolar_reducing();
4123 double T = HEOS.T_reducing() / tau_delta[0];
4126 Mstar = MixtureDerivatives::Mstar(HEOS, XN_INDEPENDENT, Lstar);
4127 std::vector<double> o(2);
4128 o[0] = Lstar.determinant();
4129 o[1] = Mstar.determinant();
4130 return o;
4131 };
4132 std::vector<std::vector<double>> Jacobian(const std::vector<double>& x) override {
4133 std::size_t N = x.size();
4134 std::vector<std::vector<double>> J(N, std::vector<double>(N, 0));
4135 Eigen::MatrixXd adjL = adjugate(Lstar), adjM = adjugate(Mstar), dLdTau = MixtureDerivatives::dLstar_dX(HEOS, XN_INDEPENDENT, iTau),
4137 dMdTau = MixtureDerivatives::dMstar_dX(HEOS, XN_INDEPENDENT, iTau, Lstar, dLdTau),
4138 dMdDelta = MixtureDerivatives::dMstar_dX(HEOS, XN_INDEPENDENT, iDelta, Lstar, dLdDelta);
4139
4140 J[0][0] = (adjL * dLdTau).trace();
4141 J[0][1] = (adjL * dLdDelta).trace();
4142 J[1][0] = (adjM * dMdTau).trace();
4143 J[1][1] = (adjM * dMdDelta).trace();
4144 return J;
4145 }
4147 std::vector<std::vector<double>> numerical_Jacobian(const std::vector<double>& x) {
4148 std::size_t N = x.size();
4149 std::vector<double> rplus, rminus, xp;
4150 std::vector<std::vector<double>> J(N, std::vector<double>(N, 0));
4151
4152 double epsilon = 0.0001;
4153
4154 // Build the Jacobian by column
4155 for (std::size_t i = 0; i < N; ++i) {
4156 xp = x;
4157 xp[i] += epsilon;
4158 rplus = call(xp);
4159 xp[i] -= 2 * epsilon;
4160 rminus = call(xp);
4161
4162 for (std::size_t j = 0; j < N; ++j) {
4163 J[j][i] = (rplus[j] - rminus[j]) / (2 * epsilon);
4164 }
4165 }
4166 std::cout << J[0][0] << " " << J[0][1] << '\n';
4167 std::cout << J[1][0] << " " << J[1][1] << '\n';
4168 return J;
4169 };
4170 };
4171 Resid resid(*this);
4172 std::vector<double> x, tau_delta(2);
4173 tau_delta[0] = T_reducing() / T0;
4174 tau_delta[1] = rho0 / rhomolar_reducing();
4175 x = NDNewtonRaphson_Jacobian(&resid, tau_delta, 1e-10, 100);
4176 _critical.T = T_reducing() / x[0];
4179
4180 CriticalState critical;
4181 critical.T = _critical.T;
4182 critical.p = _critical.p;
4183 critical.rhomolar = _critical.rhomolar;
4184 if (_critical.p < 0) {
4185 critical.stable = false;
4186 } else {
4187 if (get_config_bool(ASSUME_CRITICAL_POINT_STABLE)) {
4188 critical.stable = true;
4189 } else {
4190 // Otherwise we try to check stability with TPD-based analysis
4191 StabilityRoutines::StabilityEvaluationClass stability_tester(*this);
4192 critical.stable = stability_tester.is_stable();
4193 }
4194 }
4195 return critical;
4196}
4197
4202{
4203 public:
4205 const double delta;
4208 : HEOS(HEOS), delta(delta0), _call(_HUGE), _deriv(_HUGE), _second_deriv(_HUGE) {};
4209 double call(double tau) override {
4210 double rhomolar = HEOS.rhomolar_reducing() * delta, T = HEOS.T_reducing() / tau;
4211 HEOS.update_DmolarT_direct(rhomolar, T);
4213 return _call;
4214 }
4215 double deriv(double tau) override {
4216 Eigen::MatrixXd adjL = adjugate(MixtureDerivatives::Lstar(HEOS, XN_INDEPENDENT)),
4218 _deriv = (adjL * dLdTau).trace();
4219 return _deriv;
4220 };
4221 double second_deriv(double tau) override {
4222 Eigen::MatrixXd Lstar = MixtureDerivatives::Lstar(HEOS, XN_INDEPENDENT),
4224 d2LstardTau2 = MixtureDerivatives::d2Lstar_dX2(HEOS, XN_INDEPENDENT, iTau, iTau), adjL = adjugate(Lstar),
4225 dadjLstardTau = adjugate_derivative(Lstar, dLstardTau);
4226 _second_deriv = (dLstardTau * dadjLstardTau + adjL * d2LstardTau2).trace();
4227 return _second_deriv;
4228 };
4229};
4230
4234{
4235 public:
4237 double delta, tau,
4244 std::vector<CoolProp::CriticalState> critical_points;
4248 bool
4250 L0CurveTracer(HelmholtzEOSMixtureBackend& HEOS, double tau0, double delta0)
4251 : HEOS(HEOS),
4252 delta(delta0),
4253 tau(tau0),
4254 M1_last(_HUGE),
4255 theta_last(_HUGE),
4256 R_delta_tracer(0.1),
4257 R_tau_tracer(0.1),
4259 find_critical_points(true) {
4260
4262
4264 };
4265 /***
4266 \brief Update values for tau, delta
4267 @param theta The angle
4268 @param tau The old value of tau
4269 @param delta The old value of delta
4270 @param tau_new The new value of tau
4271 @param delta_new The new value of delta
4272 */
4273 void get_tau_delta(const double theta, const double tau, const double delta, double& tau_new, double& delta_new) {
4274 tau_new = tau + R_tau * cos(theta);
4275 delta_new = delta + R_delta * sin(theta);
4276 };
4277 /***
4278 \brief Calculate the value of L1
4279 @param theta The angle
4280 */
4281 double call(double theta) override {
4282 double tau_new = NAN, delta_new = NAN;
4283 this->get_tau_delta(theta, tau, delta, tau_new, delta_new);
4284 double rhomolar = HEOS.rhomolar_reducing() * delta_new, T = HEOS.T_reducing() / tau_new;
4285 HEOS.update_DmolarT_direct(rhomolar, T);
4287 adjLstar = adjugate(Lstar);
4290 double L1 = Lstar.determinant();
4291 return L1;
4292 };
4293 /***
4294 \brief Calculate the first partial derivative of L1 with respect to the angle
4295 @param theta The angle
4296 */
4297 double deriv(double theta) override {
4298 double dL1_dtau = (adjLstar * dLstardTau).trace(), dL1_ddelta = (adjLstar * dLstardDelta).trace();
4299 return -R_tau * sin(theta) * dL1_dtau + R_delta * cos(theta) * dL1_ddelta;
4300 };
4301
4302 void trace() {
4303 bool debug = (get_debug_level() > 0) | false;
4304 double theta = NAN;
4305 for (int i = 0; i < 300; ++i) {
4306 if (i == 0) {
4307 // In the first iteration, search all angles in the positive delta direction using a
4308 // bounded solver with a very small radius in order to not hit other L1*=0 contours
4309 // that are in the vicinity
4310 R_tau = 0.001;
4311 R_delta = 0.001;
4312 theta = Brent(this, 0, M_PI, DBL_EPSILON, 1e-10, 100);
4313 } else {
4314 // In subsequent iterations, you already have an excellent guess for the direction to
4315 // be searching in, use Newton's method to refine the solution since we also
4316 // have an analytic solution for the derivative
4319 try {
4320 theta = Newton(this, theta_last, 1e-10, 100);
4321 } catch (...) {
4322 if (N_critical_points > 0 && delta > 1.5 * critical_points[0].rhomolar / HEOS.rhomolar_reducing()) {
4323 // Stopping the search - probably we have a kink in the L1*=0 contour
4324 // caused by poor low-temperature behavior
4325 continue;
4326 }
4327 }
4328
4329 // If the solver takes a U-turn, going in the opposite direction of travel
4330 // this is not a good thing, and force a Brent's method solver call to make sure we keep
4331 // tracing in the same direction
4332 if (std::abs(angle_difference(theta, theta_last)) > M_PI / 2.0) {
4333 // We have found at least one critical point and we are now well above the density
4334 // associated with the first critical point
4335 if (N_critical_points > 0 && delta > 1.2 * critical_points[0].rhomolar / HEOS.rhomolar_reducing()) {
4336 // Stopping the search - probably we have a kink in the L1*=0 contour
4337 // caused by poor low-temperature behavior
4338 continue;
4339 } else {
4340 theta = Brent(this, theta_last - M_PI / 3.5, theta_last + M_PI / 3.5, DBL_EPSILON, 1e-10, 100);
4341 }
4342 }
4343 }
4344
4345 // Calculate the second criticality condition
4346 double M1 = MixtureDerivatives::Mstar(HEOS, XN_INDEPENDENT, Lstar).determinant();
4347 double p_MPa = HEOS.p() / 1e6;
4348
4349 // Calculate the new tau and delta at the new point
4350 double tau_new = NAN, delta_new = NAN;
4351 this->get_tau_delta(theta, tau, delta, tau_new, delta_new);
4352
4353 // Stop if bounds are exceeded
4354 if (p_MPa > 500 || HEOS.get_critical_is_terminated(delta_new, tau_new)) {
4355 break;
4356 }
4357
4358 // If the sign of M1 and the previous value of M1 have different signs, it means that
4359 // you have bracketed a critical point, run the full critical point solver to
4360 // find the critical point and store it
4361 // Only enabled if find_critical_points is true (the default)
4362 if (i > 0 && M1 * M1_last < 0 && find_critical_points) {
4363 double rhomolar = HEOS.rhomolar_reducing() * (delta + delta_new) / 2.0, T = HEOS.T_reducing() / ((tau + tau_new) / 2.0);
4365 critical_points.push_back(crit);
4367 if (debug) {
4368 std::cout << HEOS.get_mole_fractions()[0] << " " << crit.rhomolar << " " << crit.T << " " << p_MPa << '\n';
4369 }
4370 }
4371
4372 // Update the storage values
4373 this->tau = tau_new;
4374 this->delta = delta_new;
4375 this->M1_last = M1;
4376 this->theta_last = theta;
4377
4378 this->spinodal_values.tau.push_back(tau_new);
4379 this->spinodal_values.delta.push_back(delta_new);
4380 this->spinodal_values.M1.push_back(M1);
4381 }
4382 };
4383};
4384
4386 Eigen::MatrixXd Lstar = MixtureDerivatives::Lstar(*this, XN_INDEPENDENT);
4387 Eigen::MatrixXd Mstar = MixtureDerivatives::Mstar(*this, XN_INDEPENDENT, Lstar);
4388 L1star = Lstar.determinant();
4389 M1star = Mstar.determinant();
4390};
4391
4393 R_delta = 0.025;
4394 R_tau = 0.1;
4395}
4396std::vector<CoolProp::CriticalState> HelmholtzEOSMixtureBackend::_calc_all_critical_points(bool find_critical_points) {
4397 // Populate the temporary class used to calculate the critical point(s)
4399 critical_state->set_mole_fractions(this->get_mole_fractions_ref());
4400
4401 // Specify state to be something homogeneous to shortcut phase evaluation
4402 critical_state->specify_phase(iphase_gas);
4403
4404 double delta0 = _HUGE, tau0 = _HUGE;
4405 critical_state->get_critical_point_starting_values(delta0, tau0);
4406
4407 OneDimObjective resid_L0(*critical_state, delta0);
4408
4409 // If the derivative of L1star with respect to tau is positive,
4410 // tau needs to be increased such that we sit on the other
4411 // side of the d(L1star)/dtau = 0 contour
4412 resid_L0.call(tau0);
4413 int bump_count = 0;
4414 while (resid_L0.deriv(tau0) > 0 && bump_count < 3) {
4415 tau0 *= 1.1;
4416 bump_count++;
4417 }
4418 double tau_L0 = Halley(resid_L0, tau0, 1e-10, 100);
4419 //double T0 = T_reducing()/tau_L0;
4420 //double rho0 = delta0*rhomolar_reducing();
4421
4422 L0CurveTracer tracer(*critical_state, tau_L0, delta0);
4423 tracer.find_critical_points = find_critical_points;
4424
4425 double R_delta = 0, R_tau = 0;
4426 critical_state->get_critical_point_search_radii(R_delta, R_tau);
4427 tracer.R_delta_tracer = R_delta;
4428 tracer.R_tau_tracer = R_tau;
4429 tracer.trace();
4430
4431 this->spinodal_values = tracer.spinodal_values;
4432
4433 return tracer.critical_points;
4434}
4435
4436double HelmholtzEOSMixtureBackend::calc_tangent_plane_distance(const double T, const double p, const std::vector<double>& w,
4437 const double rhomolar_guess) {
4438
4439 const std::vector<CoolPropDbl>& z = this->get_mole_fractions_ref();
4440 if (w.size() != z.size()) {
4441 throw ValueError(format("Trial composition vector size [%d] is not the same as bulk composition [%d]", w.size(), z.size()));
4442 }
4443 add_TPD_state();
4444 TPD_state->set_mole_fractions(w);
4445
4446 CoolPropDbl rho = TPD_state->solver_rho_Tp_global(T, p, 0.9 / TPD_state->SRK_covolume());
4447 TPD_state->update_DmolarT_direct(rho, T);
4448
4449 double summer = 0;
4450 for (std::size_t i = 0; i < w.size(); ++i) {
4451 summer +=
4453 }
4454 return summer;
4455}
4456
4458 // Ok, we are faking a little bit here, hijacking the code for critical points, but skipping evaluation of critical points
4459 bool find_critical_points = false;
4460 _calc_all_critical_points(find_critical_points);
4461}
4462
4463void HelmholtzEOSMixtureBackend::set_reference_stateS(const std::string& reference_state) {
4464 for (auto& component : components) {
4465 CoolProp::HelmholtzEOSMixtureBackend HEOS(std::vector<CoolPropFluid>(1, component));
4466 if (!reference_state.compare("IIR")) {
4467 if (HEOS.Ttriple() > 273.15) {
4468 throw ValueError(format("Cannot use IIR reference state; Ttriple [%Lg] is greater than 273.15 K", HEOS.Ttriple()));
4469 }
4470 HEOS.update(QT_INPUTS, 0, 273.15);
4471
4472 // Get current values for the enthalpy and entropy
4473 double deltah = HEOS.hmass() - 200000; // offset from 200000 J/kg enthalpy
4474 double deltas = HEOS.smass() - 1000; // offset from 1000 J/kg/K entropy
4475 double delta_a1 = deltas / (HEOS.gas_constant() / HEOS.molar_mass());
4476 double delta_a2 = -deltah / (HEOS.gas_constant() / HEOS.molar_mass() * HEOS.get_reducing_state().T);
4477 // Change the value in the library for the given fluid
4478 set_fluid_enthalpy_entropy_offset(component, delta_a1, delta_a2, "IIR");
4479 if (get_debug_level() > 0) {
4480 std::cout << format("set offsets to %0.15g and %0.15g\n", delta_a1, delta_a2);
4481 }
4482 } else if (!reference_state.compare("ASHRAE")) {
4483 if (HEOS.Ttriple() > 233.15) {
4484 throw ValueError(format("Cannot use ASHRAE reference state; Ttriple [%Lg] is greater than than 233.15 K", HEOS.Ttriple()));
4485 }
4486 HEOS.update(QT_INPUTS, 0, 233.15);
4487
4488 // Get current values for the enthalpy and entropy
4489 double deltah = HEOS.hmass() - 0; // offset from 0 J/kg enthalpy
4490 double deltas = HEOS.smass() - 0; // offset from 0 J/kg/K entropy
4491 double delta_a1 = deltas / (HEOS.gas_constant() / HEOS.molar_mass());
4492 double delta_a2 = -deltah / (HEOS.gas_constant() / HEOS.molar_mass() * HEOS.get_reducing_state().T);
4493 // Change the value in the library for the given fluid
4494 set_fluid_enthalpy_entropy_offset(component, delta_a1, delta_a2, "ASHRAE");
4495 if (get_debug_level() > 0) {
4496 std::cout << format("set offsets to %0.15g and %0.15g\n", delta_a1, delta_a2);
4497 }
4498 } else if (!reference_state.compare("NBP")) {
4499 if (HEOS.p_triple() > 101325) {
4500 throw ValueError(format("Cannot use NBP reference state; p_triple [%Lg Pa] is greater than than 101325 Pa", HEOS.p_triple()));
4501 }
4502 // Saturated liquid boiling point at 1 atmosphere
4503 HEOS.update(PQ_INPUTS, 101325, 0);
4504
4505 double deltah = HEOS.hmass() - 0; // offset from 0 kJ/kg enthalpy
4506 double deltas = HEOS.smass() - 0; // offset from 0 kJ/kg/K entropy
4507 double delta_a1 = deltas / (HEOS.gas_constant() / HEOS.molar_mass());
4508 double delta_a2 = -deltah / (HEOS.gas_constant() / HEOS.molar_mass() * HEOS.get_reducing_state().T);
4509 // Change the value in the library for the given fluid
4510 set_fluid_enthalpy_entropy_offset(component, delta_a1, delta_a2, "NBP");
4511 if (get_debug_level() > 0) {
4512 std::cout << format("set offsets to %0.15g and %0.15g\n", delta_a1, delta_a2);
4513 }
4514 } else if (!reference_state.compare("DEF")) {
4515 set_fluid_enthalpy_entropy_offset(component, 0, 0, "DEF");
4516 } else if (!reference_state.compare("RESET")) {
4517 set_fluid_enthalpy_entropy_offset(component, 0, 0, "RESET");
4518 } else {
4519 throw ValueError(format("reference state string is invalid: [%s]", reference_state.c_str()));
4520 }
4521 }
4522}
4523
4529void HelmholtzEOSMixtureBackend::set_reference_stateD(double T, double rhomolar, double hmolar0, double smolar0) {
4530 for (auto& component : components) {
4531 CoolProp::HelmholtzEOSMixtureBackend HEOS(std::vector<CoolPropFluid>(1, component));
4532
4533 // Skip the cache and phase calculation because we are given a temperature and density directly;
4534 // just evaluate the EOS
4537
4538 // Get current values for the enthalpy and entropy
4539 double deltah = hmolar - hmolar0; // offset from specified enthalpy in J/mol
4540 double deltas = smolar - smolar0; // offset from specified entropy in J/mol/K
4541 double delta_a1 = deltas / (HEOS.gas_constant());
4542 double delta_a2 = -deltah / (HEOS.gas_constant() * HEOS.get_reducing_state().T);
4543 set_fluid_enthalpy_entropy_offset(component, delta_a1, delta_a2, "custom");
4544 }
4545}
4546
4548 const std::string& ref) {
4549 component.EOS().alpha0.EnthalpyEntropyOffset.set(delta_a1, delta_a2, ref);
4550
4551 // Note: cached caloric superancillaries are not invalidated here. They
4552 // are reference-state-agnostic via the shift trick — the offset's effect
4553 // on h(T) and s(T) along the saturation curve is exactly a constant
4554 // (Δh = R·T_red·Δa2, Δs = −R·Δa1), and the (a1, a2) stamp recorded at
4555 // first build lets resolve_T_via_superancillary translate the user's
4556 // target into the cache's frame at query time. See #2773.
4557
4558 shared_ptr<CoolProp::HelmholtzEOSBackend> HEOS = std::make_shared<CoolProp::HelmholtzEOSBackend>(component);
4559 HEOS->specify_phase(iphase_gas); // Something homogeneous;
4560 // Calculate the new enthalpy and entropy values
4561 HEOS->update(DmolarT_INPUTS, component.EOS().hs_anchor.rhomolar, component.EOS().hs_anchor.T);
4562 component.EOS().hs_anchor.hmolar = HEOS->hmolar();
4563 component.EOS().hs_anchor.smolar = HEOS->smolar();
4564
4565 double f = (HEOS->name() == "Water" || HEOS->name() == "CarbonDioxide") ? 1.00001 : 1.0;
4566
4567 // Calculate the new enthalpy and entropy values at the reducing state
4568 HEOS->update(DmolarT_INPUTS, component.EOS().reduce.rhomolar * f, component.EOS().reduce.T * f);
4569 component.EOS().reduce.hmolar = HEOS->hmolar();
4570 component.EOS().reduce.smolar = HEOS->smolar();
4571
4572 // Calculate the new enthalpy and entropy values at the critical state
4573 HEOS->update(DmolarT_INPUTS, component.crit.rhomolar * f, component.crit.T * f);
4574 component.crit.hmolar = HEOS->hmolar();
4575 component.crit.smolar = HEOS->smolar();
4576
4577 // Calculate the new enthalpy and entropy values
4578 HEOS->update(DmolarT_INPUTS, component.triple_liquid.rhomolar, component.triple_liquid.T);
4579 component.triple_liquid.hmolar = HEOS->hmolar();
4580 component.triple_liquid.smolar = HEOS->smolar();
4581
4582 // Calculate the new enthalpy and entropy values
4583 HEOS->update(DmolarT_INPUTS, component.triple_vapor.rhomolar, component.triple_vapor.T);
4584 component.triple_vapor.hmolar = HEOS->hmolar();
4585 component.triple_vapor.smolar = HEOS->smolar();
4586
4587 if (!HEOS->is_pure()) {
4588 // Calculate the new enthalpy and entropy values
4589 HEOS->update(DmolarT_INPUTS, component.EOS().max_sat_T.rhomolar, component.EOS().max_sat_T.T);
4590 component.EOS().max_sat_T.hmolar = HEOS->hmolar();
4591 component.EOS().max_sat_T.smolar = HEOS->smolar();
4592 // Calculate the new enthalpy and entropy values
4593 HEOS->update(DmolarT_INPUTS, component.EOS().max_sat_p.rhomolar, component.EOS().max_sat_p.T);
4594 component.EOS().max_sat_p.hmolar = HEOS->hmolar();
4595 component.EOS().max_sat_p.smolar = HEOS->smolar();
4596 }
4597}
4598
4599} /* namespace CoolProp */