CoolProp 8.0.0
An open-source fluid property and humid air property database
TabularBackends.cpp
Go to the documentation of this file.
1
2#if !defined(NO_TABULAR_BACKENDS)
3
4# include "TabularBackends.h"
5# include "CoolProp/CoolProp.h"
6# include <cmath>
7# include <sstream>
8# include <ctime>
9# include "miniz.h"
10# include <fstream>
11
14static const double Ainv_data[16 * 16] = {
15 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 3, 0, 0, -2,
16 -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, -2, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0,
17 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 3, 0, 0, -2, -1, 0,
18 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, -2, 0, 0, 1, 1, 0, 0, -3, 0, 3, 0, 0, 0, 0, 0, -2, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
19 -3, 0, 3, 0, 0, 0, 0, 0, -2, 0, -1, 0, 9, -9, -9, 9, 6, 3, -6, -3, 6, -6, 3, -3, 4, 2, 2, 1, -6, 6, 6, -6, -3, -3, 3, 3, -4,
20 4, -2, 2, -2, -2, -1, -1, 2, 0, -2, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, -2, 0, 0, 0, 0, 0, 1, 0,
21 1, 0, -6, 6, 6, -6, -4, -2, 4, 2, -3, 3, -3, 3, -2, -1, -2, -1, 4, -4, -4, 4, 2, 2, -2, -2, 2, -2, 2, -2, 1, 1, 1, 1};
22static Eigen::Matrix<double, 16, 16> Ainv(Ainv_data);
23
24static CoolProp::TabularDataLibrary library;
25
26namespace CoolProp {
27
34template <typename T>
35void load_table(T& table, const std::string& path_to_tables, const std::string& filename) {
36
37 double tic = clock();
38 std::string path_to_table = path_to_tables + "/" + filename;
39 if (get_debug_level() > 0) {
40 std::cout << format("Loading table: %s", path_to_table.c_str()) << '\n';
41 }
42 std::vector<char> raw;
43 try {
44 raw = get_binary_file_contents(path_to_table.c_str());
45 } catch (...) {
46 std::string err = format("Unable to load file %s", path_to_table.c_str());
47 if (get_debug_level() > 0) {
48 std::cout << "err:" << err << '\n';
49 }
50 throw UnableToLoadError(err);
51 }
52 std::vector<unsigned char> newBuffer(raw.size() * 5);
53 auto newBufferSize = static_cast<uLong>(newBuffer.size());
54 auto rawBufferSize = static_cast<mz_ulong>(raw.size());
55 int code = 0;
56 do {
57 code = uncompress((unsigned char*)(&(newBuffer[0])), &newBufferSize, (unsigned char*)(&(raw[0])), rawBufferSize);
58 if (code == Z_BUF_ERROR) {
59 // Output buffer is too small, make it bigger and try again
60 newBuffer.resize(newBuffer.size() * 5);
61 newBufferSize = static_cast<uLong>(newBuffer.size());
62 } else if (code != 0) { // Something else, a big problem
63 std::string err = format("Unable to uncompress file %s with miniz code %d", path_to_table.c_str(), code);
64 if (get_debug_level() > 0) {
65 std::cout << "uncompress err:" << err << '\n';
66 }
67 throw UnableToLoadError(err);
68 }
69 } while (code != 0);
70 // Copy the buffer from unsigned char to char (yuck)
71 std::vector<char> charbuffer(newBuffer.begin(), newBuffer.begin() + newBufferSize);
72 try {
73 msgpack::unpacked msg;
74 msgpack::unpack(msg, &(charbuffer[0]), charbuffer.size());
75 msgpack::object deserialized = msg.get();
76
77 // Call the class' deserialize function; if it is an invalid table, it will cause an exception to be thrown
78 table.deserialize(deserialized);
79 double toc = clock();
80 if (get_debug_level() > 0) {
81 std::cout << format("Loaded table: %s in %g sec.", path_to_table.c_str(), (toc - tic) / CLOCKS_PER_SEC) << '\n';
82 }
83 } catch (std::exception& e) {
84 std::string err = format("Unable to msgpack deserialize %s; err: %s", path_to_table.c_str(), e.what());
85 if (get_debug_level() > 0) {
86 std::cout << "err: " << err << '\n';
87 }
88 throw UnableToLoadError(err);
89 }
90}
91template <typename T>
92void write_table(const T& table, const std::string& path_to_tables, const std::string& name) {
93 msgpack::sbuffer sbuf;
94 msgpack::pack(sbuf, table);
95 std::string tabPath = std::string(path_to_tables + "/" + name + ".bin");
96 std::string zPath = tabPath + ".z";
97 std::vector<char> buffer(sbuf.size());
98 auto outSize = static_cast<uLong>(buffer.size());
99 compress((unsigned char*)(&(buffer[0])), &outSize, (unsigned char*)(sbuf.data()), static_cast<mz_ulong>(sbuf.size()));
100 std::ofstream ofs2(zPath.c_str(), std::ofstream::binary);
101 ofs2.write(&buffer[0], outSize);
102 ofs2.close();
103
104 if (CoolProp::get_config_bool(SAVE_RAW_TABLES)) {
105 std::ofstream ofs(tabPath.c_str(), std::ofstream::binary);
106 ofs.write(sbuf.data(), sbuf.size());
107 }
108}
109
110} // namespace CoolProp
111
112void CoolProp::PureFluidSaturationTableData::build(shared_ptr<CoolProp::AbstractState>& AS) {
113 const bool debug = get_debug_level() > 5 || false;
114 if (debug) {
115 std::cout << format("***********************************************\n");
116 std::cout << format(" Saturation Table (%s) \n", AS->name().c_str());
117 std::cout << format("***********************************************\n");
118 }
119 resize(N);
120 // ------------------------
121 // Actually build the table
122 // ------------------------
123 CoolPropDbl Tmin = std::max(AS->Ttriple(), AS->Tmin());
124 AS->update(QT_INPUTS, 0, Tmin);
125 CoolPropDbl p_triple = AS->p();
126 CoolPropDbl p = NAN, pmin = p_triple, pmax = 0.9999 * AS->p_critical();
127 for (std::size_t i = 0; i < N - 1; ++i) {
128 if (i == 0) {
129 CoolProp::set_config_bool(DONT_CHECK_PROPERTY_LIMITS, true);
130 }
131 // Log spaced
132 p = exp(log(pmin) + (log(pmax) - log(pmin)) / (N - 1) * i);
133
134 // Saturated liquid
135 try {
136 AS->update(PQ_INPUTS, p, 0);
137 pL[i] = p;
138 TL[i] = AS->T();
139 rhomolarL[i] = AS->rhomolar();
140 hmolarL[i] = AS->hmolar();
141 smolarL[i] = AS->smolar();
142 umolarL[i] = AS->umolar();
143 logpL[i] = log(p);
144 logrhomolarL[i] = log(rhomolarL[i]);
145 cpmolarL[i] = AS->cpmolar();
146 cvmolarL[i] = AS->cvmolar();
147 speed_soundL[i] = AS->speed_sound();
148 } catch (std::exception& e) {
149 // That failed for some reason, go to the next pair
150 if (debug) {
151 std::cout << " " << e.what() << '\n';
152 }
153 continue;
154 }
155 // Transport properties - if no transport properties, just keep going
156 try {
157 viscL[i] = AS->viscosity();
158 condL[i] = AS->conductivity();
159 logviscL[i] = log(viscL[i]);
160 } catch (std::exception& e) {
161 if (debug) {
162 std::cout << " " << e.what() << '\n';
163 }
164 }
165 // Saturated vapor
166 try {
167 AS->update(PQ_INPUTS, p, 1);
168 pV[i] = p;
169 TV[i] = AS->T();
170 rhomolarV[i] = AS->rhomolar();
171 hmolarV[i] = AS->hmolar();
172 smolarV[i] = AS->smolar();
173 umolarV[i] = AS->umolar();
174 logpV[i] = log(p);
175 logrhomolarV[i] = log(rhomolarV[i]);
176 cpmolarV[i] = AS->cpmolar();
177 cvmolarV[i] = AS->cvmolar();
178 speed_soundV[i] = AS->speed_sound();
179 } catch (std::exception& e) {
180 // That failed for some reason, go to the next pair
181 if (debug) {
182 std::cout << " " << e.what() << '\n';
183 }
184 continue;
185 }
186 // Transport properties - if no transport properties, just keep going
187 try {
188 viscV[i] = AS->viscosity();
189 condV[i] = AS->conductivity();
190 logviscV[i] = log(viscV[i]);
191 } catch (std::exception& e) {
192 if (debug) {
193 std::cout << " " << e.what() << '\n';
194 }
195 }
196 if (i == 0) {
197 CoolProp::set_config_bool(DONT_CHECK_PROPERTY_LIMITS, false);
198 }
199 }
200 // Last point is at the critical point
201 AS->update(PQ_INPUTS, AS->p_critical(), 1);
202 std::size_t i = N - 1;
203
204 pV[i] = AS->p();
205 TV[i] = AS->T();
206 rhomolarV[i] = AS->rhomolar();
207 hmolarV[i] = AS->hmolar();
208 smolarV[i] = AS->smolar();
209 umolarV[i] = AS->umolar();
210
211 pL[i] = AS->p();
212 TL[i] = AS->T();
213 rhomolarL[i] = AS->rhomolar();
214 hmolarL[i] = AS->hmolar();
215 smolarL[i] = AS->smolar();
216 umolarL[i] = AS->umolar();
217
218 logpV[i] = log(AS->p());
219 logrhomolarV[i] = log(rhomolarV[i]);
220
221 logpL[i] = log(AS->p());
222 logrhomolarL[i] = log(rhomolarL[i]);
223}
224
225void CoolProp::SinglePhaseGriddedTableData::build(shared_ptr<CoolProp::AbstractState>& AS) {
226 CoolPropDbl x = NAN, y = NAN;
227 const bool debug = get_debug_level() > 5 || false;
228
229 resize(Nx, Ny);
230
231 if (debug) {
232 std::cout << format("***********************************************\n");
233 std::cout << format(" Single-Phase Table (%s) \n", strjoin(AS->fluid_names(), "&").c_str());
234 std::cout << format("***********************************************\n");
235 }
236 // ------------------------
237 // Actually build the table
238 // ------------------------
239 for (std::size_t i = 0; i < Nx; ++i) {
240 // Calculate the x value
241 if (logx) {
242 // Log spaced
243 x = exp(log(xmin) + (log(xmax) - log(xmin)) / (Nx - 1) * i);
244 } else {
245 // Linearly spaced
246 x = xmin + (xmax - xmin) / (Nx - 1) * i;
247 }
248 xvec[i] = x;
249 for (std::size_t j = 0; j < Ny; ++j) {
250 // Calculate the x value
251 if (logy) {
252 // Log spaced
253 y = exp(log(ymin) + (log(ymax / ymin)) / (Ny - 1) * j);
254 } else {
255 // Linearly spaced
256 y = ymin + (ymax - ymin) / (Ny - 1) * j;
257 }
258 yvec[j] = y;
259
260 if (debug) {
261 std::cout << "x: " << x << " y: " << y << '\n';
262 }
263
264 // Generate the input pair
265 CoolPropDbl v1 = NAN, v2 = NAN;
266 input_pairs input_pair = generate_update_pair(xkey, x, ykey, y, v1, v2);
267
268 // --------------------
269 // Update the state
270 // --------------------
271 try {
272 AS->update(input_pair, v1, v2);
273 if (!ValidNumber(AS->rhomolar())) {
274 throw ValueError("rhomolar is invalid");
275 }
276 } catch (std::exception& e) {
277 // That failed for some reason, go to the next pair
278 if (debug) {
279 std::cout << " " << e.what() << '\n';
280 }
281 continue;
282 }
283
284 // Skip two-phase states - they will remain as _HUGE holes in the table
285 if (is_in_closed_range(0.0, 1.0, AS->Q())) {
286 if (debug) {
287 std::cout << " 2Phase" << '\n';
288 }
289 continue;
290 };
291
292 // --------------------
293 // State variables
294 // --------------------
295 T[i][j] = AS->T();
296 p[i][j] = AS->p();
297 rhomolar[i][j] = AS->rhomolar();
298 hmolar[i][j] = AS->hmolar();
299 smolar[i][j] = AS->smolar();
300 umolar[i][j] = AS->umolar();
301
302 // -------------------------
303 // Transport properties
304 // -------------------------
305 try {
306 visc[i][j] = AS->viscosity();
307 cond[i][j] = AS->conductivity();
308 } catch (std::exception&) { // NOLINT(bugprone-empty-catch)
309 // Failures will remain as holes in table
310 }
311
312 // ----------------------------------------
313 // First derivatives of state variables
314 // ----------------------------------------
315 dTdx[i][j] = AS->first_partial_deriv(iT, xkey, ykey);
316 dTdy[i][j] = AS->first_partial_deriv(iT, ykey, xkey);
317 dpdx[i][j] = AS->first_partial_deriv(iP, xkey, ykey);
318 dpdy[i][j] = AS->first_partial_deriv(iP, ykey, xkey);
319 drhomolardx[i][j] = AS->first_partial_deriv(iDmolar, xkey, ykey);
320 drhomolardy[i][j] = AS->first_partial_deriv(iDmolar, ykey, xkey);
321 dhmolardx[i][j] = AS->first_partial_deriv(iHmolar, xkey, ykey);
322 dhmolardy[i][j] = AS->first_partial_deriv(iHmolar, ykey, xkey);
323 dsmolardx[i][j] = AS->first_partial_deriv(iSmolar, xkey, ykey);
324 dsmolardy[i][j] = AS->first_partial_deriv(iSmolar, ykey, xkey);
325 dumolardx[i][j] = AS->first_partial_deriv(iUmolar, xkey, ykey);
326 dumolardy[i][j] = AS->first_partial_deriv(iUmolar, ykey, xkey);
327
328 // ----------------------------------------
329 // Second derivatives of state variables
330 // ----------------------------------------
331 d2Tdx2[i][j] = AS->second_partial_deriv(iT, xkey, ykey, xkey, ykey);
332 d2Tdxdy[i][j] = AS->second_partial_deriv(iT, xkey, ykey, ykey, xkey);
333 d2Tdy2[i][j] = AS->second_partial_deriv(iT, ykey, xkey, ykey, xkey);
334 d2pdx2[i][j] = AS->second_partial_deriv(iP, xkey, ykey, xkey, ykey);
335 d2pdxdy[i][j] = AS->second_partial_deriv(iP, xkey, ykey, ykey, xkey);
336 d2pdy2[i][j] = AS->second_partial_deriv(iP, ykey, xkey, ykey, xkey);
337 d2rhomolardx2[i][j] = AS->second_partial_deriv(iDmolar, xkey, ykey, xkey, ykey);
338 d2rhomolardxdy[i][j] = AS->second_partial_deriv(iDmolar, xkey, ykey, ykey, xkey);
339 d2rhomolardy2[i][j] = AS->second_partial_deriv(iDmolar, ykey, xkey, ykey, xkey);
340 d2hmolardx2[i][j] = AS->second_partial_deriv(iHmolar, xkey, ykey, xkey, ykey);
341 d2hmolardxdy[i][j] = AS->second_partial_deriv(iHmolar, xkey, ykey, ykey, xkey);
342 d2hmolardy2[i][j] = AS->second_partial_deriv(iHmolar, ykey, xkey, ykey, xkey);
343 d2smolardx2[i][j] = AS->second_partial_deriv(iSmolar, xkey, ykey, xkey, ykey);
344 d2smolardxdy[i][j] = AS->second_partial_deriv(iSmolar, xkey, ykey, ykey, xkey);
345 d2smolardy2[i][j] = AS->second_partial_deriv(iSmolar, ykey, xkey, ykey, xkey);
346 d2umolardx2[i][j] = AS->second_partial_deriv(iUmolar, xkey, ykey, xkey, ykey);
347 d2umolardxdy[i][j] = AS->second_partial_deriv(iUmolar, xkey, ykey, ykey, xkey);
348 d2umolardy2[i][j] = AS->second_partial_deriv(iUmolar, ykey, xkey, ykey, xkey);
349 }
350 }
351}
353 std::vector<std::string> fluids = AS->fluid_names();
354 std::vector<CoolPropDbl> fractions = AS->get_mole_fractions();
355 std::vector<std::string> components;
356 components.reserve(fluids.size());
357 for (std::size_t i = 0; i < fluids.size(); ++i) {
358 components.push_back(format("%s[%0.10Lf]", fluids[i].c_str(), fractions[i]));
359 }
360 std::string table_directory = get_home_dir() + "/.CoolProp/Tables/";
361 std::string alt_table_directory = get_config_string(ALTERNATIVE_TABLES_DIRECTORY);
362 if (!alt_table_directory.empty()) {
363 table_directory = alt_table_directory;
364 }
365 return table_directory + AS->backend_name() + "(" + strjoin(components, "&") + ")";
366}
367
369 std::string path_to_tables = this->path_to_tables();
370 make_dirs(path_to_tables);
371 dataset = library.get_set_of_tables(this->AS).first;
372 PackablePhaseEnvelopeData& phase_envelope = dataset->phase_envelope;
373 PureFluidSaturationTableData& pure_saturation = dataset->pure_saturation;
374 SinglePhaseGriddedTableData& single_phase_logph = dataset->single_phase_logph;
375 SinglePhaseGriddedTableData& single_phase_logpT = dataset->single_phase_logpT;
376 write_table(single_phase_logph, path_to_tables, "single_phase_logph");
377 write_table(single_phase_logpT, path_to_tables, "single_phase_logpT");
378 write_table(pure_saturation, path_to_tables, "pure_saturation");
379 write_table(phase_envelope, path_to_tables, "phase_envelope");
380}
382 auto [ds, loaded] = library.get_set_of_tables(this->AS);
383 dataset = ds;
384 if (!loaded) {
385 throw UnableToLoadError("Could not load tables");
386 }
387 if (get_debug_level() > 0) {
388 std::cout << "Tables loaded" << '\n';
389 }
390}
391
393 PhaseEnvelopeData& phase_envelope = dataset->phase_envelope;
394 PureFluidSaturationTableData& pure_saturation = dataset->pure_saturation;
395 double factor = 1.0;
396 mass_to_molar(key, factor, molar_mass());
397 if (is_mixture) {
398 return phase_envelope_sat(phase_envelope, key, iP, _p) * factor;
399 } else {
400 return pure_saturation.evaluate(key, _p, 1, cached_saturation_iL, cached_saturation_iV) * factor;
401 }
402}
404 PhaseEnvelopeData& phase_envelope = dataset->phase_envelope;
405 PureFluidSaturationTableData& pure_saturation = dataset->pure_saturation;
406 double factor = 1.0;
407 mass_to_molar(key, factor, molar_mass());
408 if (is_mixture) {
409 return phase_envelope_sat(phase_envelope, key, iP, _p) * factor;
410 } else {
411 return pure_saturation.evaluate(key, _p, 0, cached_saturation_iL, cached_saturation_iV) * factor;
412 }
413};
414
416 PhaseEnvelopeData& phase_envelope = dataset->phase_envelope;
417 if (using_single_phase_table) {
418 return _p;
419 } else {
420 if (is_mixture) {
421 return phase_envelope_sat(phase_envelope, iP, iT, _T);
422 } else {
423 return _p;
424 }
425 }
426}
428 PhaseEnvelopeData& phase_envelope = dataset->phase_envelope;
429 PureFluidSaturationTableData& pure_saturation = dataset->pure_saturation;
430 if (using_single_phase_table) {
431 switch (selected_table) {
432 case SELECTED_PH_TABLE:
433 return evaluate_single_phase_phmolar(iT, cached_single_phase_i, cached_single_phase_j);
434 case SELECTED_PT_TABLE:
435 return _T;
436 case SELECTED_NO_TABLE:
437 throw ValueError("table not selected");
438 }
439 return _HUGE; // not needed, will never be hit, just to make compiler happy
440 } else {
441 if (is_mixture) {
442 return phase_envelope_sat(phase_envelope, iT, iP, _p);
443 } else {
444 if (ValidNumber(_T)) {
445 return _T;
446 } else {
447 return pure_saturation.evaluate(iT, _p, _Q, cached_saturation_iL, cached_saturation_iV);
448 }
449 }
450 }
451}
453 PhaseEnvelopeData& phase_envelope = dataset->phase_envelope;
454 PureFluidSaturationTableData& pure_saturation = dataset->pure_saturation;
455 if (using_single_phase_table) {
456 switch (selected_table) {
457 case SELECTED_PH_TABLE:
458 return evaluate_single_phase_phmolar(iDmolar, cached_single_phase_i, cached_single_phase_j);
459 case SELECTED_PT_TABLE:
460 return evaluate_single_phase_pT(iDmolar, cached_single_phase_i, cached_single_phase_j);
461 case SELECTED_NO_TABLE:
462 throw ValueError("table not selected");
463 }
464 return _HUGE; // not needed, will never be hit, just to make compiler happy
465 } else {
466 if (is_mixture) {
467 return phase_envelope_sat(phase_envelope, iDmolar, iP, _p);
468 } else {
469 return pure_saturation.evaluate(iDmolar, _p, _Q, cached_saturation_iL, cached_saturation_iV);
470 }
471 }
472}
474 PhaseEnvelopeData& phase_envelope = dataset->phase_envelope;
475 PureFluidSaturationTableData& pure_saturation = dataset->pure_saturation;
476 if (using_single_phase_table) {
477 switch (selected_table) {
478 case SELECTED_PH_TABLE:
479 return _hmolar;
480 case SELECTED_PT_TABLE:
481 return evaluate_single_phase_pT(iHmolar, cached_single_phase_i, cached_single_phase_j);
482 case SELECTED_NO_TABLE:
483 throw ValueError("table not selected");
484 }
485 return _HUGE; // not needed, will never be hit, just to make compiler happy
486 } else {
487 if (is_mixture) {
488 return phase_envelope_sat(phase_envelope, iHmolar, iP, _p);
489 } else {
490 return pure_saturation.evaluate(iHmolar, _p, _Q, cached_saturation_iL, cached_saturation_iV);
491 }
492 }
493}
495 PhaseEnvelopeData& phase_envelope = dataset->phase_envelope;
496 PureFluidSaturationTableData& pure_saturation = dataset->pure_saturation;
497 if (using_single_phase_table) {
498 switch (selected_table) {
499 case SELECTED_PH_TABLE:
500 return evaluate_single_phase_phmolar(iSmolar, cached_single_phase_i, cached_single_phase_j);
501 case SELECTED_PT_TABLE:
502 return evaluate_single_phase_pT(iSmolar, cached_single_phase_i, cached_single_phase_j);
503 case SELECTED_NO_TABLE:
504 throw ValueError("table not selected");
505 }
506 return _HUGE; // not needed, will never be hit, just to make compiler happy
507 } else {
508 if (is_mixture) {
509 return phase_envelope_sat(phase_envelope, iSmolar, iP, _p);
510 } else {
511 return pure_saturation.evaluate(iSmolar, _p, _Q, cached_saturation_iL, cached_saturation_iV);
512 }
513 }
514}
516 PhaseEnvelopeData& phase_envelope = dataset->phase_envelope;
517 PureFluidSaturationTableData& pure_saturation = dataset->pure_saturation;
518 if (using_single_phase_table) {
519 switch (selected_table) {
520 case SELECTED_PH_TABLE:
521 return evaluate_single_phase_phmolar(iUmolar, cached_single_phase_i, cached_single_phase_j);
522 case SELECTED_PT_TABLE:
523 return evaluate_single_phase_pT(iUmolar, cached_single_phase_i, cached_single_phase_j);
524 case SELECTED_NO_TABLE:
525 throw ValueError("table not selected");
526 }
527 return _HUGE; // not needed, will never be hit, just to make compiler happy
528 } else {
529 if (is_mixture) {
530 // h = u + pv -> u = h - pv
531 CoolPropDbl hmolar = phase_envelope_sat(phase_envelope, iHmolar, iP, _p);
532 CoolPropDbl rhomolar = phase_envelope_sat(phase_envelope, iDmolar, iP, _p);
533 return hmolar - _p / rhomolar;
534 } else {
535 return pure_saturation.evaluate(iUmolar, _p, _Q, cached_saturation_iL, cached_saturation_iV);
536 }
537 }
538}
540 PhaseEnvelopeData& phase_envelope = dataset->phase_envelope;
541 PureFluidSaturationTableData& pure_saturation = dataset->pure_saturation;
542 if (using_single_phase_table) {
543 return calc_first_partial_deriv(iHmolar, iT, iP);
544 } else {
545 if (is_mixture) {
546 return phase_envelope_sat(phase_envelope, iCpmolar, iP, _p);
547 } else {
548 return pure_saturation.evaluate(iCpmolar, _p, _Q, cached_saturation_iL, cached_saturation_iV);
549 }
550 }
551}
553 PhaseEnvelopeData& phase_envelope = dataset->phase_envelope;
554 PureFluidSaturationTableData& pure_saturation = dataset->pure_saturation;
555 if (using_single_phase_table) {
556 return calc_first_partial_deriv(iUmolar, iT, iDmolar);
557 } else {
558 if (is_mixture) {
559 return phase_envelope_sat(phase_envelope, iCvmolar, iP, _p);
560 } else {
561 return pure_saturation.evaluate(iCvmolar, _p, _Q, cached_saturation_iL, cached_saturation_iV);
562 }
563 }
564}
565
567 PhaseEnvelopeData& phase_envelope = dataset->phase_envelope;
568 PureFluidSaturationTableData& pure_saturation = dataset->pure_saturation;
569 if (using_single_phase_table) {
570 switch (selected_table) {
571 case SELECTED_PH_TABLE:
572 return evaluate_single_phase_phmolar_transport(iviscosity, cached_single_phase_i, cached_single_phase_j);
573 case SELECTED_PT_TABLE:
574 return evaluate_single_phase_pT_transport(iviscosity, cached_single_phase_i, cached_single_phase_j);
575 case SELECTED_NO_TABLE:
576 throw ValueError("table not selected");
577 }
578 return _HUGE; // not needed, will never be hit, just to make compiler happy
579 } else {
580 if (is_mixture) {
581 return phase_envelope_sat(phase_envelope, iviscosity, iP, _p);
582 } else {
583 return pure_saturation.evaluate(iviscosity, _p, _Q, cached_saturation_iL, cached_saturation_iV);
584 }
585 }
586}
588 PhaseEnvelopeData& phase_envelope = dataset->phase_envelope;
589 PureFluidSaturationTableData& pure_saturation = dataset->pure_saturation;
590 if (using_single_phase_table) {
591 switch (selected_table) {
592 case SELECTED_PH_TABLE:
593 return evaluate_single_phase_phmolar_transport(iconductivity, cached_single_phase_i, cached_single_phase_j);
594 case SELECTED_PT_TABLE:
595 return evaluate_single_phase_pT_transport(iconductivity, cached_single_phase_i, cached_single_phase_j);
596 case SELECTED_NO_TABLE:
597 throw ValueError("table not selected");
598 }
599 return _HUGE; // not needed, will never be hit, just to make compiler happy
600 } else {
601 if (is_mixture) {
602 return phase_envelope_sat(phase_envelope, iconductivity, iP, _p);
603 } else {
604 return pure_saturation.evaluate(iconductivity, _p, _Q, cached_saturation_iL, cached_saturation_iV);
605 }
606 }
607}
609 PhaseEnvelopeData& phase_envelope = dataset->phase_envelope;
610 PureFluidSaturationTableData& pure_saturation = dataset->pure_saturation;
611 if (using_single_phase_table) {
612 return sqrt(1 / molar_mass() * cpmolar() / cvmolar() * first_partial_deriv(iP, iDmolar, iT));
613 } else {
614 if (is_mixture) {
615 return phase_envelope_sat(phase_envelope, ispeed_sound, iP, _p);
616 } else {
617 return pure_saturation.evaluate(ispeed_sound, _p, _Q, cached_saturation_iL, cached_saturation_iV);
618 }
619 }
620}
622 if (using_single_phase_table) {
623 CoolPropDbl dOf_dx = NAN, dOf_dy = NAN, dWrt_dx = NAN, dWrt_dy = NAN, dConstant_dx = NAN, dConstant_dy = NAN;
624
625 // If a mass-based parameter is provided, get a conversion factor and change the key to the molar-based key
626 double Of_conversion_factor = 1.0, Wrt_conversion_factor = 1.0, Constant_conversion_factor = 1.0, MM = AS->molar_mass();
627 mass_to_molar(Of, Of_conversion_factor, MM);
628 mass_to_molar(Wrt, Wrt_conversion_factor, MM);
629 mass_to_molar(Constant, Constant_conversion_factor, MM);
630
631 switch (selected_table) {
632 case SELECTED_PH_TABLE: {
633 dOf_dx = evaluate_single_phase_phmolar_derivative(Of, cached_single_phase_i, cached_single_phase_j, 1, 0);
634 dOf_dy = evaluate_single_phase_phmolar_derivative(Of, cached_single_phase_i, cached_single_phase_j, 0, 1);
635 dWrt_dx = evaluate_single_phase_phmolar_derivative(Wrt, cached_single_phase_i, cached_single_phase_j, 1, 0);
636 dWrt_dy = evaluate_single_phase_phmolar_derivative(Wrt, cached_single_phase_i, cached_single_phase_j, 0, 1);
637 dConstant_dx = evaluate_single_phase_phmolar_derivative(Constant, cached_single_phase_i, cached_single_phase_j, 1, 0);
638 dConstant_dy = evaluate_single_phase_phmolar_derivative(Constant, cached_single_phase_i, cached_single_phase_j, 0, 1);
639 break;
640 }
641 case SELECTED_PT_TABLE: {
642 dOf_dx = evaluate_single_phase_pT_derivative(Of, cached_single_phase_i, cached_single_phase_j, 1, 0);
643 dOf_dy = evaluate_single_phase_pT_derivative(Of, cached_single_phase_i, cached_single_phase_j, 0, 1);
644 dWrt_dx = evaluate_single_phase_pT_derivative(Wrt, cached_single_phase_i, cached_single_phase_j, 1, 0);
645 dWrt_dy = evaluate_single_phase_pT_derivative(Wrt, cached_single_phase_i, cached_single_phase_j, 0, 1);
646 dConstant_dx = evaluate_single_phase_pT_derivative(Constant, cached_single_phase_i, cached_single_phase_j, 1, 0);
647 dConstant_dy = evaluate_single_phase_pT_derivative(Constant, cached_single_phase_i, cached_single_phase_j, 0, 1);
648 break;
649 }
650 case SELECTED_NO_TABLE:
651 throw ValueError("table not selected");
652 }
653 double val = (dOf_dx * dConstant_dy - dOf_dy * dConstant_dx) / (dWrt_dx * dConstant_dy - dWrt_dy * dConstant_dx);
654 return val * Of_conversion_factor / Wrt_conversion_factor;
655 } else {
656 throw ValueError(format("Inputs [rho: %g mol/m3, T: %g K, p: %g Pa] are two-phase; cannot use single-phase derivatives", _rhomolar, _T, _p));
657 }
658};
659
661 PureFluidSaturationTableData& pure_saturation = dataset->pure_saturation;
662 if (AS->get_mole_fractions().size() > 1) {
663 throw ValueError("calc_first_saturation_deriv not available for mixtures");
664 }
665 if (std::abs(_Q) < 1e-6) {
666 return pure_saturation.first_saturation_deriv(Of1, Wrt1, 0, keyed_output(Wrt1), cached_saturation_iL);
667 } else if (std::abs(_Q - 1) < 1e-6) {
668 return pure_saturation.first_saturation_deriv(Of1, Wrt1, 1, keyed_output(Wrt1), cached_saturation_iV);
669 } else {
670 throw ValueError(format("Quality [%Lg] must be either 0 or 1 to within 1 ppm", _Q));
671 }
672}
674 PureFluidSaturationTableData& pure_saturation = dataset->pure_saturation;
675 if (Of == iDmolar && Wrt == iHmolar && Constant == iP) {
676 CoolPropDbl rhoL = pure_saturation.evaluate(iDmolar, _p, 0, cached_saturation_iL, cached_saturation_iV);
677 CoolPropDbl rhoV = pure_saturation.evaluate(iDmolar, _p, 1, cached_saturation_iL, cached_saturation_iV);
678 CoolPropDbl hL = pure_saturation.evaluate(iHmolar, _p, 0, cached_saturation_iL, cached_saturation_iV);
679 CoolPropDbl hV = pure_saturation.evaluate(iHmolar, _p, 1, cached_saturation_iL, cached_saturation_iV);
680 return -POW2(rhomolar()) * (1 / rhoV - 1 / rhoL) / (hV - hL);
681 } else if (Of == iDmass && Wrt == iHmass && Constant == iP) {
682 return first_two_phase_deriv(iDmolar, iHmolar, iP) * POW2(molar_mass());
683 } else if (Of == iDmolar && Wrt == iP && Constant == iHmolar) {
684 // v = 1/rho; drhodv = -rho^2; dvdrho = -1/rho^2
685 CoolPropDbl rhoL = pure_saturation.evaluate(iDmolar, _p, 0, cached_saturation_iL, cached_saturation_iV);
686 CoolPropDbl rhoV = pure_saturation.evaluate(iDmolar, _p, 1, cached_saturation_iL, cached_saturation_iV);
687 CoolPropDbl hL = pure_saturation.evaluate(iHmolar, _p, 0, cached_saturation_iL, cached_saturation_iV);
688 CoolPropDbl hV = pure_saturation.evaluate(iHmolar, _p, 1, cached_saturation_iL, cached_saturation_iV);
689 CoolPropDbl dvdrhoL = -1 / POW2(rhoL);
690 CoolPropDbl dvdrhoV = -1 / POW2(rhoV);
691 CoolPropDbl dvL_dp = dvdrhoL * pure_saturation.first_saturation_deriv(iDmolar, iP, 0, _p, cached_saturation_iL);
692 CoolPropDbl dvV_dp = dvdrhoV * pure_saturation.first_saturation_deriv(iDmolar, iP, 1, _p, cached_saturation_iV);
693 CoolPropDbl dhL_dp = pure_saturation.first_saturation_deriv(iHmolar, iP, 0, _p, cached_saturation_iL);
694 CoolPropDbl dhV_dp = pure_saturation.first_saturation_deriv(iHmolar, iP, 1, _p, cached_saturation_iV);
695 CoolPropDbl dxdp_h = (Q() * dhV_dp + (1 - Q()) * dhL_dp) / (hL - hV);
696 CoolPropDbl dvdp_h = dvL_dp + dxdp_h * (1 / rhoV - 1 / rhoL) + Q() * (dvV_dp - dvL_dp);
697 return -POW2(rhomolar()) * dvdp_h;
698 } else if (Of == iDmass && Wrt == iP && Constant == iHmass) {
699 return first_two_phase_deriv(iDmolar, iP, iHmolar) * molar_mass();
700 } else {
701 throw ValueError("These inputs are not supported to calc_first_two_phase_deriv");
702 }
703}
704
706 // Note: If you need all three values (drho_dh__p, drho_dp__h and rho_spline),
707 // you should calculate drho_dp__h first to avoid duplicate calculations.
708
709 bool drho_dh__p = false;
710 bool drho_dp__h = false;
711 bool rho_spline = false;
712
713 if (Of == iDmolar && Wrt == iHmolar && Constant == iP) {
714 drho_dh__p = true;
715 if (_drho_spline_dh__constp) return _drho_spline_dh__constp;
716 } else if (Of == iDmass && Wrt == iHmass && Constant == iP) {
717 return first_two_phase_deriv_splined(iDmolar, iHmolar, iP, x_end) * POW2(molar_mass());
718 } else if (Of == iDmolar && Wrt == iP && Constant == iHmolar) {
719 drho_dp__h = true;
720 if (_drho_spline_dp__consth) return _drho_spline_dp__consth;
721 } else if (Of == iDmass && Wrt == iP && Constant == iHmass) {
722 return first_two_phase_deriv_splined(iDmolar, iP, iHmolar, x_end) * molar_mass();
723 }
724 // Add the special case for the splined density
725 else if (Of == iDmolar && Wrt == iDmolar && Constant == iDmolar) {
726 rho_spline = true;
727 if (_rho_spline) return _rho_spline;
728 } else if (Of == iDmass && Wrt == iDmass && Constant == iDmass) {
729 return first_two_phase_deriv_splined(iDmolar, iDmolar, iDmolar, x_end) * molar_mass();
730 } else {
731 throw ValueError("These inputs are not supported to calc_first_two_phase_deriv");
732 }
733
734 if (_Q > x_end) {
735 throw ValueError(format("Q [%g] is greater than x_end [%Lg]", _Q, x_end).c_str());
736 }
737 if (_phase != iphase_twophase) {
738 throw ValueError(format("state is not two-phase"));
739 }
740
741 // TODO: replace AS with a cloned instance of TTSE or BICUBIC, add "clone()" as mandatory function in base class
742 //shared_ptr<TabularBackend>
743 // Liq(this->clone())),
744 // End(this->clone()));
745 //
746 //Liq->specify_phase(iphase_liquid);
747 //Liq->_Q = -1;
748 //Liq->update_DmolarT_direct(SatL->rhomolar(), SatL->T());
749 //End->update(QT_INPUTS, x_end, SatL->T());
750
751 // Start with quantities needed for all calculations
752 PureFluidSaturationTableData& pure_saturation = dataset->pure_saturation;
753 CoolPropDbl hL = pure_saturation.evaluate(iHmolar, _p, 0, cached_saturation_iL, cached_saturation_iV);
754 CoolPropDbl hV = pure_saturation.evaluate(iHmolar, _p, 1, cached_saturation_iL, cached_saturation_iV);
755 CoolPropDbl hE = pure_saturation.evaluate(iHmolar, _p, x_end, cached_saturation_iL, cached_saturation_iV);
756
757 CoolPropDbl dL = pure_saturation.evaluate(iDmolar, _p, 0, cached_saturation_iL, cached_saturation_iV);
758 CoolPropDbl dV = pure_saturation.evaluate(iDmolar, _p, 1, cached_saturation_iL, cached_saturation_iV);
759 CoolPropDbl dE = pure_saturation.evaluate(iDmolar, _p, x_end, cached_saturation_iL, cached_saturation_iV);
760
761 CoolPropDbl Delta = Q() * (hV - hL);
762 CoolPropDbl Delta_end = hE - hL;
763
764 CoolPropDbl TL = pure_saturation.evaluate(iT, _p, 0, cached_saturation_iL, cached_saturation_iV);
765 CoolPropDbl TE = pure_saturation.evaluate(iT, _p, x_end, cached_saturation_iL, cached_saturation_iV);
766
767 // TODO: We cheat here and fake access to a derived class.
768 // Liquid state
769 AS->specify_phase(iphase_liquid);
770 AS->update(DmolarT_INPUTS, dL, TL);
771 CoolPropDbl drho_dh_liq__constp = AS->first_partial_deriv(iDmolar, iHmolar, iP);
772 CoolPropDbl d2rhodhdp_liq = AS->second_partial_deriv(iDmolar, iHmolar, iP, iP, iHmolar);
773 // End of spline
774 AS->specify_phase(iphase_twophase);
775 AS->update(DmolarT_INPUTS, dE, TE);
776 CoolPropDbl drho_dh_end__constp = AS->first_partial_deriv(iDmolar, iHmolar, iP);
777 CoolPropDbl d2rhodhdp_end = AS->second_partial_deriv(iDmolar, iHmolar, iP, iP, iHmolar);
778
779 // Spline coordinates a, b, c, d
780 CoolPropDbl Abracket = (2 * dL - 2 * dE + Delta_end * (drho_dh_liq__constp + drho_dh_end__constp));
781 CoolPropDbl a = 1 / POW3(Delta_end) * Abracket;
782 CoolPropDbl b = 3 / POW2(Delta_end) * (-dL + dE) - 1 / Delta_end * (drho_dh_end__constp + 2 * drho_dh_liq__constp);
783 CoolPropDbl c = drho_dh_liq__constp;
784 CoolPropDbl d = dL;
785
786 // Either the spline value or drho/dh|p can be directly evaluated now
787 _rho_spline = a * POW3(Delta) + b * POW2(Delta) + c * Delta + d;
788 _drho_spline_dh__constp = 3 * a * POW2(Delta) + 2 * b * Delta + c;
789 if (rho_spline) return _rho_spline;
790 if (drho_dh__p) return _drho_spline_dh__constp;
791
792 // It's drho/dp|h
793 // ... calculate some more things
794
795 // Derivatives *along* the saturation curve using the special internal method
796 CoolPropDbl dhL_dp_sat = pure_saturation.first_saturation_deriv(iHmolar, iP, 0, _p, cached_saturation_iL);
797 CoolPropDbl dhV_dp_sat = pure_saturation.first_saturation_deriv(iHmolar, iP, 1, _p, cached_saturation_iV);
798 CoolPropDbl drhoL_dp_sat = pure_saturation.first_saturation_deriv(iDmolar, iP, 0, _p, cached_saturation_iL);
799 CoolPropDbl drhoV_dp_sat = pure_saturation.first_saturation_deriv(iDmolar, iP, 1, _p, cached_saturation_iV);
800 //CoolPropDbl rhoV = SatV->keyed_output(rho_key);
801 //CoolPropDbl rhoL = SatL->keyed_output(rho_key);
802 CoolPropDbl drho_dp_end = POW2(dE) * (x_end / POW2(dV) * drhoV_dp_sat + (1 - x_end) / POW2(dL) * drhoL_dp_sat);
803
804 // Faking single-phase
805 //CoolPropDbl drho_dp__consth_liq = Liq->first_partial_deriv(rho_key, p_key, h_key);
806 //CoolPropDbl d2rhodhdp_liq = Liq->second_partial_deriv(rho_key, h_key, p_key, p_key, h_key); // ?
807
808 // Derivatives at the end point
809 // CoolPropDbl drho_dp__consth_end = End->calc_first_two_phase_deriv(rho_key, p_key, h_key);
810 //CoolPropDbl d2rhodhdp_end = End->calc_second_two_phase_deriv(rho_key, h_key, p_key, p_key, h_key);
811
812 // Reminder:
813 // Delta = Q()*(hV-hL) = h-hL
814 // Delta_end = x_end*(hV-hL);
815 CoolPropDbl d_Delta_dp__consth = -dhL_dp_sat;
816 CoolPropDbl d_Delta_end_dp__consth = x_end * (dhV_dp_sat - dhL_dp_sat);
817
818 // First pressure derivative at constant h of the coefficients a,b,c,d
819 // CoolPropDbl Abracket = (2*rho_liq - 2*rho_end + Delta_end * (drho_dh_liq__constp + drho_dh_end));
820 CoolPropDbl d_Abracket_dp_consth = (2 * drhoL_dp_sat - 2 * drho_dp_end + Delta_end * (d2rhodhdp_liq + d2rhodhdp_end)
821 + d_Delta_end_dp__consth * (drho_dh_liq__constp + drho_dh_end__constp));
822 CoolPropDbl da_dp = 1 / POW3(Delta_end) * d_Abracket_dp_consth + Abracket * (-3 / POW4(Delta_end) * d_Delta_end_dp__consth);
823 CoolPropDbl db_dp = -6 / POW3(Delta_end) * d_Delta_end_dp__consth * (dE - dL) + 3 / POW2(Delta_end) * (drho_dp_end - drhoL_dp_sat)
824 + (1 / POW2(Delta_end) * d_Delta_end_dp__consth) * (drho_dh_end__constp + 2 * drho_dh_liq__constp)
825 - (1 / Delta_end) * (d2rhodhdp_end + 2 * d2rhodhdp_liq);
826 CoolPropDbl dc_dp = d2rhodhdp_liq;
827 CoolPropDbl dd_dp = drhoL_dp_sat;
828
829 _drho_spline_dp__consth =
830 (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;
831 if (drho_dp__h) return _drho_spline_dp__consth;
832
833 throw ValueError("Something went wrong in TabularBackend::calc_first_two_phase_deriv_splined");
834 return _HUGE;
835}
836
837namespace {
840enum FastEvalOutputKind : unsigned char
841{
842 fe_unsupported = 0,
843 fe_single_phase = 1, // iT, iP, iDmolar, iHmolar, iSmolar, iUmolar
844 fe_transport = 2, // iviscosity, iconductivity
845};
846
847FastEvalOutputKind classify_fast_eval_output(CoolProp::parameters key) {
848 using namespace CoolProp;
849 switch (key) {
850 case iT:
851 case iP:
852 case iDmolar:
853 case iHmolar:
854 case iSmolar:
855 case iUmolar:
856 return fe_single_phase;
857 case iviscosity:
858 case iconductivity:
859 return fe_transport;
860 default:
861 return fe_unsupported;
862 }
863}
864} // namespace
865
866void CoolProp::TabularBackend::fast_evaluate(CoolProp::input_pairs input_pair, const double* val1, const double* val2, std::size_t N_inputs,
867 const CoolProp::parameters* outputs, std::size_t N_outputs, double* out_buffer,
868 std::size_t out_buffer_size, int* status_flags, std::size_t status_flags_size,
869 CoolProp::phases imposed_phase) {
870 // Pre-loop validation. These conditions are programmer errors (mismatched
871 // buffers, unsupported keys), not data errors — throw so misuse fails loud.
872 if (N_outputs != 0 && N_inputs > std::numeric_limits<std::size_t>::max() / N_outputs) {
873 throw ValueError(format("fast_evaluate: N_inputs * N_outputs would overflow size_t (N_inputs=%zu, N_outputs=%zu)", N_inputs, N_outputs));
874 }
875 const std::size_t required_out = N_inputs * N_outputs;
876 if (out_buffer_size < required_out) {
877 throw ValueError(format("fast_evaluate: out_buffer_size=%zu < required %zu (N_inputs * N_outputs)", out_buffer_size, required_out));
878 }
879 if (status_flags_size < N_inputs) {
880 throw ValueError(format("fast_evaluate: status_flags_size=%zu < required %zu (N_inputs)", status_flags_size, N_inputs));
881 }
882 if (N_inputs == 0) return;
883 // Null-pointer check BEFORE the N_outputs==0 early-write path: that path
884 // dereferences status_flags, so the check has to dominate it.
885 if (val1 == nullptr || val2 == nullptr || outputs == nullptr || out_buffer == nullptr || status_flags == nullptr) {
886 throw ValueError("fast_evaluate: null pointer argument");
887 }
888 if (N_outputs == 0) {
889 for (std::size_t k = 0; k < N_inputs; ++k)
890 status_flags[k] = CoolProp::fast_evaluate_ok;
891 return;
892 }
893
894 const bool is_ph = (input_pair == HmolarP_INPUTS);
895 const bool is_pT = (input_pair == PT_INPUTS);
896 if (!is_ph && !is_pT) {
897 throw ValueError(
898 format("fast_evaluate: input_pair %s not supported (use HmolarP_INPUTS or PT_INPUTS)", get_input_pair_short_desc(input_pair).c_str()));
899 }
900
901 // Validate output keys up-front (caller passes typically a few; fits on stack)
902 constexpr std::size_t MAX_FE_OUTPUTS = 64;
903 if (N_outputs > MAX_FE_OUTPUTS) {
904 throw ValueError(format("fast_evaluate: N_outputs=%zu exceeds compile-time limit %zu", N_outputs, MAX_FE_OUTPUTS));
905 }
906 FastEvalOutputKind output_kinds[MAX_FE_OUTPUTS];
907 for (std::size_t o = 0; o < N_outputs; ++o) {
908 output_kinds[o] = classify_fast_eval_output(outputs[o]);
909 if (output_kinds[o] == fe_unsupported) {
910 throw ValueError(format("fast_evaluate: output[%zu]=%s not supported", o, get_parameter_information(outputs[o], "short").c_str()));
911 }
912 }
913
914 // Build tables if not already built; this is the only "lazy alloc" we tolerate
915 // here, and it happens at most once per AS lifetime — outside the hot loop.
916 check_tables();
917
918 // The two tables are concrete derived types of SinglePhaseGriddedTableData;
919 // bind through a pointer to the common base for the conditional.
920 SinglePhaseGriddedTableData* table_ptr = is_ph ? static_cast<SinglePhaseGriddedTableData*>(&dataset->single_phase_logph)
921 : static_cast<SinglePhaseGriddedTableData*>(&dataset->single_phase_logpT);
922 SinglePhaseGriddedTableData& table = *table_ptr;
923 const std::vector<std::vector<CellCoeffs>>& coeffs = is_ph ? dataset->coeffs_ph : dataset->coeffs_pT;
924
925 // Apply imposed phase for the duration of the call; restore at end. (The
926 // existing per-output kernels do not consult imposed_phase_index — they
927 // operate on the (i,j) we already located — but downstream callers and
928 // diagnostic paths do, so keeping it in sync avoids surprises.)
929 const phases saved_imposed_phase = imposed_phase_index;
930 if (imposed_phase != iphase_not_imposed) {
931 imposed_phase_index = imposed_phase;
932 }
933
934 const double NaN = std::numeric_limits<double>::quiet_NaN();
935
936 auto fill_nan_row = [&](std::size_t k) {
937 for (std::size_t o = 0; o < N_outputs; ++o) {
938 out_buffer[k * N_outputs + o] = NaN;
939 }
940 };
941
942 for (std::size_t k = 0; k < N_inputs; ++k) {
943 // x is the table's primary axis (logh for ph table, logT-equivalent for pT)
944 // and y is the secondary (logp). The table internally translates from
945 // (h,p) or (T,p) → (x,y); we just pass the raw inputs.
946 const double v1 = val1[k];
947 const double v2 = val2[k];
948 const double x = is_ph ? v1 : v2; // logph: x=h ; logpT: x=T
949 const double y = is_ph ? v2 : v1; // y is always p
950
951 if (!table.native_inputs_are_in_range(x, y)) {
952 status_flags[k] = fast_evaluate_out_of_range;
953 fill_nan_row(k);
954 continue;
955 }
956
957 std::size_t i = 0, j = 0;
958 try {
959 find_native_nearest_good_indices(table, coeffs, x, y, i, j);
960 } catch (const std::exception&) {
961 // Cell exists but has no valid neighbour (e.g., deep inside the table's
962 // two-phase notch with no alternate). Report and skip.
963 status_flags[k] = fast_evaluate_out_of_range;
964 fill_nan_row(k);
965 continue;
966 }
967
968 // The per-output kernels in BicubicBackend / TTSEBackend read from
969 // _hmolar/_p (ph) or _T/_p (pT) to recompute the normalised cell coords.
970 // We must set those before each call; we restore at the end of the
971 // function so the AS's cached state is not silently polluted.
972 if (is_ph) {
973 _hmolar = v1;
974 _p = v2;
975 } else {
976 _T = v2;
977 _p = v1;
978 }
979
980 bool eval_failed = false;
981 for (std::size_t o = 0; o < N_outputs; ++o) {
982 const parameters out_key = outputs[o];
983 const FastEvalOutputKind kind = output_kinds[o];
984 try {
985 double val;
986 if (kind == fe_transport) {
987 val = is_ph ? evaluate_single_phase_phmolar_transport(out_key, i, j) : evaluate_single_phase_pT_transport(out_key, i, j);
988 } else {
989 // For the (h,p) table requesting iHmolar, or (T,p) table requesting
990 // iT/iP, the answer is just the input — short-circuit to skip
991 // pointless polynomial evaluation. iP is always one of the inputs.
992 if (out_key == iP) {
993 val = is_ph ? v2 : v1;
994 } else if (is_ph && out_key == iHmolar) {
995 val = v1;
996 } else if (is_pT && out_key == iT) {
997 val = v2;
998 } else {
999 val = is_ph ? evaluate_single_phase_phmolar(out_key, i, j) : evaluate_single_phase_pT(out_key, i, j);
1000 }
1001 }
1002 out_buffer[k * N_outputs + o] = val;
1003 } catch (const std::exception&) {
1004 eval_failed = true;
1005 out_buffer[k * N_outputs + o] = NaN;
1006 }
1007 }
1008 // API contract: when status_flags[k] is non-zero, the entire row is NaN.
1009 // The per-output catch above only NaNs the specific failing output;
1010 // finish the job here so callers don't see partial rows.
1011 if (eval_failed) {
1012 fill_nan_row(k);
1013 status_flags[k] = fast_evaluate_internal_error;
1014 } else {
1015 status_flags[k] = fast_evaluate_ok;
1016 }
1017 }
1018
1019 // Restore phase override and clear the AS cache so no per-point residue
1020 // leaks into subsequent keyed_output() / T() / p() calls on this instance.
1021 imposed_phase_index = saved_imposed_phase;
1022 clear();
1023}
1024
1025void CoolProp::TabularBackend::update(CoolProp::input_pairs input_pair, double val1, double val2) {
1026
1027 if (get_debug_level() > 0) {
1028 std::cout << format("update(%s,%g,%g)\n", get_input_pair_short_desc(input_pair).c_str(), val1, val2);
1029 }
1030
1031 // Clear cached variables
1032 clear();
1033
1034 // Convert to mass-based units if necessary
1035 CoolPropDbl ld_value1 = val1, ld_value2 = val2;
1036 mass_to_molar_inputs(input_pair, ld_value1, ld_value2);
1037 val1 = ld_value1;
1038 val2 = ld_value2;
1039
1040 // Check the tables, build if necessary
1041 check_tables();
1042
1043 // Flush the cached indices (set to large number)
1044 cached_single_phase_i = std::numeric_limits<std::size_t>::max();
1045 cached_single_phase_j = std::numeric_limits<std::size_t>::max();
1046 cached_saturation_iL = std::numeric_limits<std::size_t>::max();
1047 cached_saturation_iV = std::numeric_limits<std::size_t>::max();
1048
1049 // To start, set quality to value that is impossible
1050 _Q = -1000;
1051
1052 PureFluidSaturationTableData& pure_saturation = dataset->pure_saturation;
1053 PhaseEnvelopeData& phase_envelope = dataset->phase_envelope;
1054 SinglePhaseGriddedTableData& single_phase_logph = dataset->single_phase_logph;
1055 SinglePhaseGriddedTableData& single_phase_logpT = dataset->single_phase_logpT;
1056
1057 switch (input_pair) {
1058 case HmolarP_INPUTS: {
1059 _hmolar = val1;
1060 _p = val2;
1061 if (!single_phase_logph.native_inputs_are_in_range(_hmolar, _p)) {
1062 // Use the AbstractState instance
1063 using_single_phase_table = false;
1064 if (get_debug_level() > 5) {
1065 std::cout << "inputs are not in range";
1066 }
1067 throw ValueError(format("inputs are not in range, hmolar=%Lg, p=%Lg", static_cast<CoolPropDbl>(_hmolar), _p));
1068 } else {
1069 using_single_phase_table = true; // Use the table !
1070 std::size_t iL = std::numeric_limits<std::size_t>::max(), iV = std::numeric_limits<std::size_t>::max(), iclosest = 0;
1071 CoolPropDbl hL = 0, hV = 0;
1072 SimpleState closest_state;
1073 bool is_two_phase = false;
1074 // If phase is imposed, use it, but only if it's single phase:
1075 // - Imposed two phase still needs to determine saturation limits by calling pure_saturation.is_inside().
1076 // - There's no speed increase to be gained by imposing two phase.
1077 if ((imposed_phase_index == iphase_not_imposed) || (imposed_phase_index == iphase_twophase)) {
1078 if (is_mixture) {
1079 is_two_phase = PhaseEnvelopeRoutines::is_inside(phase_envelope, iP, _p, iHmolar, _hmolar, iclosest, closest_state);
1080 } else {
1081 is_two_phase = pure_saturation.is_inside(iP, _p, iHmolar, _hmolar, iL, iV, hL, hV);
1082 }
1083 }
1084 // Phase determined or imposed, now interpolate results
1085 if (is_two_phase) {
1086 using_single_phase_table = false;
1087 _Q = (static_cast<double>(_hmolar) - hL) / (hV - hL);
1088 if (!is_in_closed_range(0.0, 1.0, static_cast<double>(_Q))) {
1089 throw ValueError(
1090 format("vapor quality is not in (0,1) for hmolar: %g p: %g, hL: %g hV: %g ", static_cast<double>(_hmolar), _p, hL, hV));
1091 } else {
1092 cached_saturation_iL = iL;
1093 cached_saturation_iV = iV;
1094 _phase = iphase_twophase;
1095 }
1096 } else {
1097 selected_table = SELECTED_PH_TABLE;
1098 // Find and cache the indices i, j
1099 find_native_nearest_good_indices(single_phase_logph, dataset->coeffs_ph, _hmolar, _p, cached_single_phase_i,
1100 cached_single_phase_j);
1101 // Recalculate the phase
1102 recalculate_singlephase_phase();
1103 }
1104 }
1105 break;
1106 }
1107 case PT_INPUTS: {
1108 _p = val1;
1109 _T = val2;
1110 if (!single_phase_logpT.native_inputs_are_in_range(_T, _p)) {
1111 // Use the AbstractState instance
1112 using_single_phase_table = false;
1113 if (get_debug_level() > 5) {
1114 std::cout << "inputs are not in range";
1115 }
1116 throw ValueError(format("inputs are not in range, p=%g Pa, T=%g K", _p, _T));
1117 } else {
1118 using_single_phase_table = true; // Use the table !
1119 std::size_t iL = 0, iV = 0, iclosest = 0;
1120 CoolPropDbl TL = 0, TV = 0;
1121 SimpleState closest_state;
1122 bool is_two_phase = false;
1123 // Phase is imposed, use it
1124 if (imposed_phase_index != iphase_not_imposed) {
1125 is_two_phase = (imposed_phase_index == iphase_twophase);
1126 } else {
1127 if (is_mixture) {
1128 is_two_phase = PhaseEnvelopeRoutines::is_inside(phase_envelope, iP, _p, iT, _T, iclosest, closest_state);
1129 } else {
1130 is_two_phase = pure_saturation.is_inside(iP, _p, iT, _T, iL, iV, TL, TV);
1131 }
1132 }
1133 if (is_two_phase) {
1134 using_single_phase_table = false;
1135 throw ValueError(format("P,T with TTSE cannot be two-phase for now"));
1136 } else {
1137 selected_table = SELECTED_PT_TABLE;
1138 // Find and cache the indices i, j
1139 find_native_nearest_good_indices(single_phase_logpT, dataset->coeffs_pT, _T, _p, cached_single_phase_i, cached_single_phase_j);
1140
1141 if (imposed_phase_index != iphase_not_imposed) {
1142 double rhoc = rhomolar_critical();
1143 // Lambda: validate the bumped cell. If the imposed-phase walk lands on a
1144 // table cell with no bicubic coefficients (i.e., inside the two-phase
1145 // notch of the table), evaluate_single_phase_pT will dereference an
1146 // empty alpha[] vector and segfault. Resolve to a good neighbour or
1147 // throw cleanly. Same fix idiom as the non-imposed branch below.
1148 auto ensure_bumped_cell_valid = [&](const char* context) {
1149 const auto& cell = dataset->coeffs_pT[cached_single_phase_i][cached_single_phase_j];
1150 if (!cell.valid()) {
1151 if (auto alt = cell.get_alternate()) {
1152 auto [ai, aj] = *alt;
1153 cached_single_phase_i = ai;
1154 cached_single_phase_j = aj;
1155 } else {
1156 throw ValueError(
1157 format("Imposed phase %s: bumped cell (%zu, %zu) has no bicubic coefficients and no good neighbour "
1158 "(p=%g Pa, T=%g K)",
1159 context, cached_single_phase_i, cached_single_phase_j, _p, static_cast<double>(_T)));
1160 }
1161 }
1162 };
1163 if (imposed_phase_index == iphase_liquid && cached_single_phase_i > 0) {
1164 // We want a liquid solution, but we got a vapor solution
1165 if (_p < this->AS->p_critical()) {
1166 while (cached_single_phase_i > 0
1167 && single_phase_logpT.rhomolar[cached_single_phase_i + 1][cached_single_phase_j] < rhoc) {
1168 // Bump to lower temperature
1169 cached_single_phase_i--;
1170 }
1171 ensure_bumped_cell_valid("liquid");
1172 double rho = evaluate_single_phase_pT(iDmolar, cached_single_phase_i, cached_single_phase_j);
1173 if (rho < rhoc) {
1174 // Didn't work
1175 throw ValueError("Bump unsuccessful");
1176 } else {
1177 _rhomolar = rho;
1178 }
1179 }
1180 } else if ((imposed_phase_index == iphase_gas || imposed_phase_index == iphase_supercritical_gas)
1181 && cached_single_phase_i > 0) {
1182
1183 // We want a gas solution, but we got a liquid solution
1184 if (_p < this->AS->p_critical()) {
1185 while (cached_single_phase_i > 0
1186 && single_phase_logpT.rhomolar[cached_single_phase_i][cached_single_phase_j + 1] > rhoc) {
1187 // Bump to lower temperature
1188 cached_single_phase_i++;
1189 }
1190 ensure_bumped_cell_valid("gas");
1191 double rho = evaluate_single_phase_pT(iDmolar, cached_single_phase_i, cached_single_phase_j);
1192 if (rho > rhoc) {
1193 // Didn't work
1194 throw ValueError("Bump unsuccessful");
1195 } else {
1196 _rhomolar = rho;
1197 }
1198 }
1199 }
1200 } else {
1201
1202 // If p < pc, you might be getting a liquid solution when you want a vapor solution or vice versa
1203 // if you are very close to the saturation curve, so we figure out what the saturation temperature
1204 // is for the given pressure
1205 if (_p < this->AS->p_critical()) {
1206 double Ts = pure_saturation.evaluate(iT, _p, _Q, iL, iV);
1207 double TL = single_phase_logpT.T[cached_single_phase_i][cached_single_phase_j];
1208 double TR = single_phase_logpT.T[cached_single_phase_i + 1][cached_single_phase_j];
1209 if (TL < Ts && Ts < TR) {
1210 if (_T < Ts) {
1211 if (cached_single_phase_i == 0) {
1212 throw ValueError(format("P, T are near saturation, but cannot move the cell to the left"));
1213 }
1214 // It's liquid, move the cell to the left
1215 cached_single_phase_i--;
1216 } else {
1217 if (cached_single_phase_i > single_phase_logpT.Nx - 2) {
1218 throw ValueError(format("P,T are near saturation, but cannot move the cell to the right"));
1219 }
1220 // It's vapor, move to the right
1221 cached_single_phase_i++;
1222 }
1223 // The bumped cell may land in the table's two-phase region where
1224 // CellCoeffs has no bicubic alpha[] populated. Subsequent
1225 // evaluate_single_phase dereferences alpha[] without checking
1226 // validity → segfault (#1950: N2 BICUBIC at PT=2e5,78 K).
1227 // Resolve the same way find_native_nearest_good_indices does:
1228 // try the cell's alternate; otherwise throw cleanly.
1229 const auto& bumped_cell = dataset->coeffs_pT[cached_single_phase_i][cached_single_phase_j];
1230 if (!bumped_cell.valid()) {
1231 if (auto alt = bumped_cell.get_alternate()) {
1232 auto [ai, aj] = *alt;
1233 cached_single_phase_i = ai;
1234 cached_single_phase_j = aj;
1235 } else {
1236 throw ValueError(
1237 format("P,T near saturation: bumped cell (%zu, %zu) has no bicubic coefficients and no good neighbour "
1238 "(p=%g Pa, T=%g K, Tsat=%g K)",
1239 cached_single_phase_i, cached_single_phase_j, _p, static_cast<double>(_T), Ts));
1240 }
1241 }
1242 }
1243 }
1244 }
1245 // Recalculate the phase
1246 recalculate_singlephase_phase();
1247 }
1248 }
1249 break;
1250 }
1251 case PUmolar_INPUTS:
1252 case PSmolar_INPUTS:
1253 case DmolarP_INPUTS: {
1254 CoolPropDbl otherval = NAN;
1255 parameters otherkey;
1256 switch (input_pair) {
1257 case PUmolar_INPUTS:
1258 _p = val1;
1259 _umolar = val2;
1260 otherval = val2;
1261 otherkey = iUmolar;
1262 break;
1263 case PSmolar_INPUTS:
1264 _p = val1;
1265 _smolar = val2;
1266 otherval = val2;
1267 otherkey = iSmolar;
1268 break;
1269 case DmolarP_INPUTS:
1270 _rhomolar = val1;
1271 _p = val2;
1272 otherval = val1;
1273 otherkey = iDmolar;
1274 break;
1275 default:
1276 throw ValueError("Bad (impossible) pair");
1277 }
1278
1279 using_single_phase_table = true; // Use the table (or first guess is that it is single-phase)!
1280 std::size_t iL = std::numeric_limits<std::size_t>::max(), iV = std::numeric_limits<std::size_t>::max();
1281 CoolPropDbl zL = 0, zV = 0;
1282 std::size_t iclosest = 0;
1283 SimpleState closest_state;
1284 bool is_two_phase = false;
1285 // If phase is imposed, use it, but only if it's single phase:
1286 // - Imposed two phase still needs to determine saturation limits by calling is_inside().
1287 // - There's no speed increase to be gained by imposing two phase.
1288 if ((imposed_phase_index == iphase_not_imposed) || (imposed_phase_index == iphase_twophase)) {
1289 if (is_mixture) {
1290 is_two_phase = PhaseEnvelopeRoutines::is_inside(phase_envelope, iP, _p, otherkey, otherval, iclosest, closest_state);
1291 if (is_two_phase) {
1292 std::vector<std::pair<std::size_t, std::size_t>> intersect =
1293 PhaseEnvelopeRoutines::find_intersections(phase_envelope, iP, _p);
1294 if (intersect.size() < 2) {
1295 throw ValueError(format("p [%g Pa] is not within phase envelope", _p));
1296 }
1297 iV = intersect[0].first;
1298 iL = intersect[1].first;
1299 zL = PhaseEnvelopeRoutines::evaluate(phase_envelope, otherkey, iP, _p, iL);
1300 zV = PhaseEnvelopeRoutines::evaluate(phase_envelope, otherkey, iP, _p, iV);
1301 }
1302 } else {
1303 is_two_phase = pure_saturation.is_inside(iP, _p, otherkey, otherval, iL, iV, zL, zV);
1304 }
1305 }
1306 // Phase determined or imposed, now interpolate results
1307 if (is_two_phase) {
1308 using_single_phase_table = false;
1309 if (otherkey == iDmolar) {
1310 _Q = (1 / otherval - 1 / zL) / (1 / zV - 1 / zL);
1311 } else {
1312 _Q = (otherval - zL) / (zV - zL);
1313 }
1314 if (!is_in_closed_range(0.0, 1.0, static_cast<double>(_Q))) {
1315
1316 throw ValueError(format("vapor quality is not in (0,1) for %s: %g p: %g", get_parameter_information(otherkey, "short").c_str(),
1317 otherval, static_cast<double>(_p)));
1318 } else if (!is_mixture) {
1319 cached_saturation_iL = iL;
1320 cached_saturation_iV = iV;
1321 }
1322 _phase = iphase_twophase;
1323 } else {
1324 selected_table = SELECTED_PH_TABLE;
1325 // Find and cache the indices i, j
1326 find_nearest_neighbor(single_phase_logph, dataset->coeffs_ph, iP, _p, otherkey, otherval, cached_single_phase_i,
1327 cached_single_phase_j);
1328 // Now find hmolar given P, X for X in Smolar, Umolar, Dmolar
1329 invert_single_phase_x(single_phase_logph, dataset->coeffs_ph, otherkey, otherval, _p, cached_single_phase_i, cached_single_phase_j);
1330 // Recalculate the phase
1331 recalculate_singlephase_phase();
1332 }
1333 break;
1334 }
1335 case SmolarT_INPUTS:
1336 case DmolarT_INPUTS: {
1337 CoolPropDbl otherval = NAN;
1338 parameters otherkey;
1339 switch (input_pair) {
1340 case SmolarT_INPUTS:
1341 _smolar = val1;
1342 _T = val2;
1343 otherval = val1;
1344 otherkey = iSmolar;
1345 break;
1346 case DmolarT_INPUTS:
1347 _rhomolar = val1;
1348 _T = val2;
1349 otherval = val1;
1350 otherkey = iDmolar;
1351 break;
1352 default:
1353 throw ValueError("Bad (impossible) pair");
1354 }
1355
1356 using_single_phase_table = true; // Use the table (or first guess is that it is single-phase)!
1357 std::size_t iL = std::numeric_limits<std::size_t>::max(), iV = std::numeric_limits<std::size_t>::max();
1358 CoolPropDbl zL = 0, zV = 0;
1359 std::size_t iclosest = 0;
1360 SimpleState closest_state;
1361 bool is_two_phase = false;
1362 // If phase is imposed, use it, but only if it's single phase:
1363 // - Imposed two phase still needs to determine saturation limits by calling is_inside().
1364 // - There's no speed increase to be gained by imposing two phase.
1365 if ((imposed_phase_index == iphase_not_imposed) || (imposed_phase_index == iphase_twophase)) {
1366 if (is_mixture) {
1367 is_two_phase = PhaseEnvelopeRoutines::is_inside(phase_envelope, iT, _T, otherkey, otherval, iclosest, closest_state);
1368 if (is_two_phase) {
1369 std::vector<std::pair<std::size_t, std::size_t>> intersect =
1370 PhaseEnvelopeRoutines::find_intersections(phase_envelope, iT, _T);
1371 if (intersect.size() < 2) {
1372 throw ValueError(format("T [%g K] is not within phase envelope", _T));
1373 }
1374 iV = intersect[0].first;
1375 iL = intersect[1].first;
1376 zL = PhaseEnvelopeRoutines::evaluate(phase_envelope, otherkey, iT, _T, iL);
1377 zV = PhaseEnvelopeRoutines::evaluate(phase_envelope, otherkey, iT, _T, iV);
1378 }
1379 } else {
1380 is_two_phase = pure_saturation.is_inside(iT, _T, otherkey, otherval, iL, iV, zL, zV);
1381 }
1382 }
1383 if (is_two_phase) {
1384 using_single_phase_table = false;
1385 if (otherkey == iDmolar) {
1386 _Q = (1 / otherval - 1 / zL) / (1 / zV - 1 / zL);
1387 } else {
1388 _Q = (otherval - zL) / (zV - zL);
1389 }
1390 if (!is_in_closed_range(0.0, 1.0, static_cast<double>(_Q))) {
1391 throw ValueError(format("vapor quality is not in (0,1) for %s: %g T: %g", get_parameter_information(otherkey, "short").c_str(),
1392 otherval, static_cast<double>(_T)));
1393 } else if (!is_mixture) {
1394 cached_saturation_iL = iL;
1395 cached_saturation_iV = iV;
1396 _p = pure_saturation.evaluate(iP, _T, _Q, iL, iV);
1397 } else {
1398 // Mixture
1399 std::vector<std::pair<std::size_t, std::size_t>> intersect = PhaseEnvelopeRoutines::find_intersections(phase_envelope, iT, _T);
1400 if (intersect.size() < 2) {
1401 throw ValueError(format("T [%g K] is not within phase envelope", _T));
1402 }
1403 iV = intersect[0].first;
1404 iL = intersect[1].first;
1405 CoolPropDbl pL = PhaseEnvelopeRoutines::evaluate(phase_envelope, iP, iT, _T, iL);
1406 CoolPropDbl pV = PhaseEnvelopeRoutines::evaluate(phase_envelope, iP, iT, _T, iV);
1407 _p = _Q * pV + (1 - _Q) * pL;
1408 }
1409 } else {
1410 selected_table = SELECTED_PT_TABLE;
1411 // Find and cache the indices i, j
1412 find_nearest_neighbor(single_phase_logpT, dataset->coeffs_pT, iT, _T, otherkey, otherval, cached_single_phase_i,
1413 cached_single_phase_j);
1414 // Now find the y variable (Dmolar or Smolar in this case)
1415 invert_single_phase_y(single_phase_logpT, dataset->coeffs_pT, otherkey, otherval, _T, cached_single_phase_i, cached_single_phase_j);
1416 // Recalculate the phase
1417 recalculate_singlephase_phase();
1418 }
1419 break;
1420 }
1421 case PQ_INPUTS: {
1422 std::size_t iL = 0, iV = 0;
1423 _p = val1;
1424 _Q = val2;
1425 using_single_phase_table = false;
1426 if (!is_in_closed_range(0.0, 1.0, static_cast<double>(_Q))) {
1427 throw ValueError(format("vapor quality [%g] is not in (0,1)", static_cast<double>(val2)));
1428 } else {
1429 CoolPropDbl TL = _HUGE, TV = _HUGE;
1430 if (is_mixture) {
1431 std::vector<std::pair<std::size_t, std::size_t>> intersect = PhaseEnvelopeRoutines::find_intersections(phase_envelope, iP, _p);
1432 if (intersect.size() < 2) {
1433 throw ValueError(format("p [%g Pa] is not within phase envelope", _p));
1434 }
1435 iV = intersect[0].first;
1436 iL = intersect[1].first;
1437 TL = PhaseEnvelopeRoutines::evaluate(phase_envelope, iT, iP, _p, iL);
1438 TV = PhaseEnvelopeRoutines::evaluate(phase_envelope, iT, iP, _p, iV);
1439 } else {
1440 bool it_is_inside = pure_saturation.is_inside(iP, _p, iQ, _Q, iL, iV, TL, TV);
1441 if (!it_is_inside) {
1442 throw ValueError("Not possible to determine whether pressure is inside or not");
1443 }
1444 }
1445 _T = _Q * TV + (1 - _Q) * TL;
1446 cached_saturation_iL = iL;
1447 cached_saturation_iV = iV;
1448 _phase = iphase_twophase;
1449 }
1450 break;
1451 }
1452 case QT_INPUTS: {
1453 std::size_t iL = 0, iV = 0;
1454 _Q = val1;
1455 _T = val2;
1456
1457 using_single_phase_table = false;
1458 if (!is_in_closed_range(0.0, 1.0, static_cast<double>(_Q))) {
1459 throw ValueError(format("vapor quality [%g] is not in (0,1)", static_cast<double>(val1)));
1460 } else {
1461 CoolPropDbl pL = _HUGE, pV = _HUGE;
1462 if (is_mixture) {
1463 std::vector<std::pair<std::size_t, std::size_t>> intersect = PhaseEnvelopeRoutines::find_intersections(phase_envelope, iT, _T);
1464 if (intersect.size() < 2) {
1465 throw ValueError(format("T [%g K] is not within phase envelope", _T));
1466 }
1467 iV = intersect[0].first;
1468 iL = intersect[1].first;
1469 pL = PhaseEnvelopeRoutines::evaluate(phase_envelope, iP, iT, _T, iL);
1470 pV = PhaseEnvelopeRoutines::evaluate(phase_envelope, iP, iT, _T, iV);
1471 } else {
1472 (void)pure_saturation.is_inside(iT, _T, iQ, _Q, iL, iV, pL, pV);
1473 }
1474 _p = _Q * pV + (1 - _Q) * pL;
1475 cached_saturation_iL = iL;
1476 cached_saturation_iV = iV;
1477 _phase = iphase_twophase;
1478 }
1479 break;
1480 }
1481 default:
1482 throw ValueError("Sorry, but this set of inputs is not supported for Tabular backend");
1483 }
1484}
1485
1486void CoolProp::TabularDataSet::write_tables(const std::string& path_to_tables) {
1487 make_dirs(path_to_tables);
1488 write_table(single_phase_logph, path_to_tables, "single_phase_logph");
1489 write_table(single_phase_logpT, path_to_tables, "single_phase_logpT");
1490 write_table(pure_saturation, path_to_tables, "pure_saturation");
1491 write_table(phase_envelope, path_to_tables, "phase_envelope");
1492}
1493
1494void CoolProp::TabularDataSet::load_tables(const std::string& path_to_tables, shared_ptr<CoolProp::AbstractState>& AS) {
1495 single_phase_logph.AS = AS;
1496 single_phase_logpT.AS = AS;
1497 pure_saturation.AS = AS;
1498 single_phase_logph.set_limits();
1499 single_phase_logpT.set_limits();
1500 load_table(single_phase_logph, path_to_tables, "single_phase_logph.bin.z");
1501 load_table(single_phase_logpT, path_to_tables, "single_phase_logpT.bin.z");
1502 load_table(pure_saturation, path_to_tables, "pure_saturation.bin.z");
1503 load_table(phase_envelope, path_to_tables, "phase_envelope.bin.z");
1504 tables_loaded = true;
1505 if (get_debug_level() > 0) {
1506 std::cout << "Tables loaded" << '\n';
1507 }
1508};
1509
1510void CoolProp::TabularDataSet::build_tables(shared_ptr<CoolProp::AbstractState>& AS) {
1511 // Pure or pseudo-pure fluid
1512 if (AS->get_mole_fractions().size() == 1) {
1513 pure_saturation.build(AS);
1514 } else {
1515 // Call function to actually construct the phase envelope
1516 AS->build_phase_envelope("");
1517 // Copy constructed phase envelope into this class
1518 PhaseEnvelopeData PED = AS->get_phase_envelope_data();
1519 // Convert into packable form
1520 phase_envelope.copy_from_nonpackable(PED);
1521 // Resize so that it will load properly
1522 pure_saturation.resize(pure_saturation.N);
1523 }
1524 single_phase_logph.build(AS);
1525 single_phase_logpT.build(AS);
1526 tables_loaded = true;
1527}
1528
1530std::pair<CoolProp::TabularDataSet*, bool> CoolProp::TabularDataLibrary::get_set_of_tables(shared_ptr<AbstractState>& AS) {
1531 const std::string path = path_to_tables(AS);
1532 const int cfg_Nx = get_config_int(TABULAR_NX);
1533 const int cfg_Ny = get_config_int(TABULAR_NY);
1534 // Try to find tabular set if it is already loaded
1535 auto it = data.find(path);
1536 if (it != data.end()) {
1537 // Verify the cached dataset's grid matches the current TABULAR_NX/TABULAR_NY
1538 // config; if not, evict so we rebuild at the requested resolution rather than
1539 // handing back a mis-sized table (would risk OOB reads downstream in BICUBIC/TTSE
1540 // coefficient lookups).
1541 const TabularDataSet& cached = it->second;
1542 const bool grid_matches =
1543 (static_cast<int>(cached.single_phase_logph.Nx) == cfg_Nx && static_cast<int>(cached.single_phase_logph.Ny) == cfg_Ny
1544 && static_cast<int>(cached.single_phase_logpT.Nx) == cfg_Nx && static_cast<int>(cached.single_phase_logpT.Ny) == cfg_Ny);
1545 if (grid_matches) {
1546 return {&(it->second), it->second.tables_loaded};
1547 }
1548 if (get_debug_level() > 0) {
1549 std::cout << format("TABULAR_NX/NY changed (cached %zux%zu, config %dx%d); evicting cached dataset for %s\n",
1550 cached.single_phase_logph.Nx, cached.single_phase_logph.Ny, cfg_Nx, cfg_Ny, path.c_str());
1551 }
1552 data.erase(it);
1553 }
1554 // Not in the map (or just evicted) -- build a fresh entry
1555 TabularDataSet set;
1556 data.insert(std::pair<std::string, TabularDataSet>(path, set));
1557 TabularDataSet& dataset = data[path];
1558 bool loaded = false;
1559 try {
1560 if (!dataset.tables_loaded) {
1561 dataset.load_tables(path, AS);
1562 }
1563 loaded = true;
1564 } catch (std::exception&) {
1565 loaded = false;
1566 }
1567 return {&(dataset), loaded};
1568}
1569
1570void CoolProp::TabularDataSet::build_coeffs(SinglePhaseGriddedTableData& table, std::vector<std::vector<CellCoeffs>>& coeffs) {
1571 if (!coeffs.empty()) {
1572 return;
1573 }
1574 const bool debug = get_debug_level() > 5 || false;
1575 const int param_count = 6;
1576 parameters param_list[param_count] = {iDmolar, iT, iSmolar, iHmolar, iP, iUmolar};
1577 std::vector<std::vector<double>>*f = nullptr, *fx = nullptr, *fy = nullptr, *fxy = nullptr;
1578
1579 clock_t t1 = clock();
1580
1581 // Resize the coefficient structures
1582 coeffs.resize(table.Nx - 1, std::vector<CellCoeffs>(table.Ny - 1));
1583
1584 int valid_cell_count = 0;
1585 for (auto param : param_list) {
1586 if (param == table.xkey || param == table.ykey) {
1587 continue;
1588 } // Skip tables that match either of the input variables
1589
1590 switch (param) {
1591 case iT:
1592 f = &(table.T);
1593 fx = &(table.dTdx);
1594 fy = &(table.dTdy);
1595 fxy = &(table.d2Tdxdy);
1596 break;
1597 case iP:
1598 f = &(table.p);
1599 fx = &(table.dpdx);
1600 fy = &(table.dpdy);
1601 fxy = &(table.d2pdxdy);
1602 break;
1603 case iDmolar:
1604 f = &(table.rhomolar);
1605 fx = &(table.drhomolardx);
1606 fy = &(table.drhomolardy);
1607 fxy = &(table.d2rhomolardxdy);
1608 break;
1609 case iSmolar:
1610 f = &(table.smolar);
1611 fx = &(table.dsmolardx);
1612 fy = &(table.dsmolardy);
1613 fxy = &(table.d2smolardxdy);
1614 break;
1615 case iHmolar:
1616 f = &(table.hmolar);
1617 fx = &(table.dhmolardx);
1618 fy = &(table.dhmolardy);
1619 fxy = &(table.d2hmolardxdy);
1620 break;
1621 case iUmolar:
1622 f = &(table.umolar);
1623 fx = &(table.dumolardx);
1624 fy = &(table.dumolardy);
1625 fxy = &(table.d2umolardxdy);
1626 break;
1627 default:
1628 throw ValueError("Invalid variable type to build_coeffs");
1629 }
1630 for (std::size_t i = 0; i < table.Nx - 1; ++i) // -1 since we have one fewer cells than nodes
1631 {
1632 for (std::size_t j = 0; j < table.Ny - 1; ++j) // -1 since we have one fewer cells than nodes
1633 {
1634 if (ValidNumber((*f)[i][j]) && ValidNumber((*f)[i + 1][j]) && ValidNumber((*f)[i][j + 1]) && ValidNumber((*f)[i + 1][j + 1])) {
1635
1636 // This will hold the scaled f values for the cell
1637 Eigen::Matrix<double, 16, 1> F;
1638 // The output values (do not require scaling
1639 F(0) = (*f)[i][j];
1640 F(1) = (*f)[i + 1][j];
1641 F(2) = (*f)[i][j + 1];
1642 F(3) = (*f)[i + 1][j + 1];
1643 // Scaling parameter
1644 // d(f)/dxhat = df/dx * dx/dxhat, where xhat = (x-x_i)/(x_{i+1}-x_i)
1645 coeffs[i][j].dx_dxhat = table.xvec[i + 1] - table.xvec[i];
1646 double dx_dxhat = coeffs[i][j].dx_dxhat;
1647 F(4) = (*fx)[i][j] * dx_dxhat;
1648 F(5) = (*fx)[i + 1][j] * dx_dxhat;
1649 F(6) = (*fx)[i][j + 1] * dx_dxhat;
1650 F(7) = (*fx)[i + 1][j + 1] * dx_dxhat;
1651 // Scaling parameter
1652 // d(f)/dyhat = df/dy * dy/dyhat, where yhat = (y-y_j)/(y_{j+1}-y_j)
1653 coeffs[i][j].dy_dyhat = table.yvec[j + 1] - table.yvec[j];
1654 double dy_dyhat = coeffs[i][j].dy_dyhat;
1655 F(8) = (*fy)[i][j] * dy_dyhat;
1656 F(9) = (*fy)[i + 1][j] * dy_dyhat;
1657 F(10) = (*fy)[i][j + 1] * dy_dyhat;
1658 F(11) = (*fy)[i + 1][j + 1] * dy_dyhat;
1659 // Cross derivatives are doubly scaled following the examples above
1660 F(12) = (*fxy)[i][j] * dy_dyhat * dx_dxhat;
1661 F(13) = (*fxy)[i + 1][j] * dy_dyhat * dx_dxhat;
1662 F(14) = (*fxy)[i][j + 1] * dy_dyhat * dx_dxhat;
1663 F(15) = (*fxy)[i + 1][j + 1] * dy_dyhat * dx_dxhat;
1664 // Calculate the alpha coefficients
1665 Eigen::MatrixXd alpha = Ainv.transpose() * F; // 16x1; Watch out for the transpose!
1666 std::vector<double> valpha = eigen_to_vec1D(alpha);
1667 coeffs[i][j].set(param, valpha);
1668 coeffs[i][j].set_valid();
1669 valid_cell_count++;
1670 } else {
1671 coeffs[i][j].set_invalid();
1672 }
1673 }
1674 }
1675 double elapsed = (clock() - t1) / ((double)CLOCKS_PER_SEC);
1676 if (debug) {
1677 std::cout << format("Calculated bicubic coefficients for %d good cells in %g sec.\n", valid_cell_count, elapsed);
1678 }
1679 std::size_t remap_count = 0;
1680 // Now find invalid cells and give them pointers to a neighboring cell that works
1681 for (std::size_t i = 0; i < table.Nx - 1; ++i) // -1 since we have one fewer cells than nodes
1682 {
1683 for (std::size_t j = 0; j < table.Ny - 1; ++j) // -1 since we have one fewer cells than nodes
1684 {
1685 // Not a valid cell
1686 if (!coeffs[i][j].valid()) {
1687 // Offsets that we are going to try in order (left, right, top, bottom, diagonals)
1688 int xoffsets[] = {-1, 1, 0, 0, -1, 1, 1, -1};
1689 int yoffsets[] = {0, 0, 1, -1, -1, -1, 1, 1};
1690 // Length of offset
1691 std::size_t N = sizeof(xoffsets) / sizeof(xoffsets[0]);
1692 for (std::size_t k = 0; k < N; ++k) {
1693 std::size_t iplus = i + xoffsets[k];
1694 std::size_t jplus = j + yoffsets[k];
1695 if (0 < iplus && iplus < table.Nx - 1 && 0 < jplus && jplus < table.Ny - 1 && coeffs[iplus][jplus].valid()) {
1696 coeffs[i][j].set_alternate(iplus, jplus);
1697 remap_count++;
1698 if (debug) {
1699 std::cout << format("Mapping %d,%d to %d,%d\n", i, j, iplus, jplus);
1700 }
1701 break;
1702 }
1703 }
1704 }
1705 }
1706 }
1707 if (debug) {
1708 std::cout << format("Remapped %d cells\n", remap_count);
1709 }
1710 }
1711}
1712
1713# if defined(ENABLE_CATCH)
1714# include <catch2/catch_all.hpp>
1715
1716// Defined global so we only load once
1717static shared_ptr<CoolProp::AbstractState> ASHEOS, ASTTSE, ASBICUBIC;
1718
1719/* Use a fixture so that loading of the tables from memory only happens once in the initializer */
1720class TabularFixture
1721{
1722 public:
1723 TabularFixture() = default;
1724 void setup() {
1725 if (ASHEOS.get() == nullptr) {
1726 ASHEOS.reset(CoolProp::AbstractState::factory("HEOS", "Water"));
1727 }
1728 if (ASTTSE.get() == nullptr) {
1729 ASTTSE.reset(CoolProp::AbstractState::factory("TTSE&HEOS", "Water"));
1730 }
1731 if (ASBICUBIC.get() == nullptr) {
1732 ASBICUBIC.reset(CoolProp::AbstractState::factory("BICUBIC&HEOS", "Water"));
1733 }
1734 }
1735};
1736TEST_CASE_METHOD(TabularFixture, "Tests for tabular backends with water", "[Tabular]") {
1737 SECTION("first_saturation_deriv invalid quality") {
1738 setup();
1739 ASBICUBIC->update(CoolProp::PQ_INPUTS, 101325, 0.5);
1740 CHECK_THROWS(ASBICUBIC->first_saturation_deriv(CoolProp::iP, CoolProp::iT));
1741 ASTTSE->update(CoolProp::PQ_INPUTS, 101325, 0.5);
1742 CHECK_THROWS(ASTTSE->first_saturation_deriv(CoolProp::iP, CoolProp::iT));
1743 }
1744
1745 SECTION("first_saturation_deriv dp/dT") {
1746 setup();
1747 ASHEOS->update(CoolProp::PQ_INPUTS, 101325, 1);
1748 CoolPropDbl expected = ASHEOS->first_saturation_deriv(CoolProp::iP, CoolProp::iT);
1749 ASTTSE->update(CoolProp::PQ_INPUTS, 101325, 1);
1750 CoolPropDbl actual_TTSE = ASTTSE->first_saturation_deriv(CoolProp::iP, CoolProp::iT);
1751 ASTTSE->update(CoolProp::PQ_INPUTS, 101325, 1);
1752 CoolPropDbl actual_BICUBIC = ASTTSE->first_saturation_deriv(CoolProp::iP, CoolProp::iT);
1753 CAPTURE(expected);
1754 CAPTURE(actual_TTSE);
1755 CAPTURE(actual_BICUBIC);
1756 CHECK(std::abs((expected - actual_TTSE) / expected) < 1e-6);
1757 CHECK(std::abs((expected - actual_BICUBIC) / expected) < 1e-6);
1758 }
1759 SECTION("first_saturation_deriv dDmolar/dP") {
1760 setup();
1761 ASHEOS->update(CoolProp::PQ_INPUTS, 101325, 1);
1762 CoolPropDbl expected = ASHEOS->first_saturation_deriv(CoolProp::iDmolar, CoolProp::iP);
1763 ASTTSE->update(CoolProp::PQ_INPUTS, 101325, 1);
1764 CoolPropDbl actual_TTSE = ASTTSE->first_saturation_deriv(CoolProp::iDmolar, CoolProp::iP);
1765 ASBICUBIC->update(CoolProp::PQ_INPUTS, 101325, 1);
1766 CoolPropDbl actual_BICUBIC = ASBICUBIC->first_saturation_deriv(CoolProp::iDmolar, CoolProp::iP);
1767 CAPTURE(expected);
1768 CAPTURE(actual_TTSE);
1769 CAPTURE(actual_BICUBIC);
1770 CHECK(std::abs((expected - actual_TTSE) / expected) < 1e-6);
1771 CHECK(std::abs((expected - actual_BICUBIC) / expected) < 1e-6);
1772 }
1773 SECTION("first_saturation_deriv dDmass/dP") {
1774 setup();
1775 ASHEOS->update(CoolProp::PQ_INPUTS, 101325, 1);
1776 CoolPropDbl expected = ASHEOS->first_saturation_deriv(CoolProp::iDmass, CoolProp::iP);
1777 ASTTSE->update(CoolProp::PQ_INPUTS, 101325, 1);
1778 CoolPropDbl actual_TTSE = ASTTSE->first_saturation_deriv(CoolProp::iDmass, CoolProp::iP);
1779 ASBICUBIC->update(CoolProp::PQ_INPUTS, 101325, 1);
1780 CoolPropDbl actual_BICUBIC = ASBICUBIC->first_saturation_deriv(CoolProp::iDmass, CoolProp::iP);
1781 CAPTURE(expected);
1782 CAPTURE(actual_TTSE);
1783 CAPTURE(actual_BICUBIC);
1784 CHECK(std::abs((expected - actual_TTSE) / expected) < 1e-6);
1785 CHECK(std::abs((expected - actual_BICUBIC) / expected) < 1e-6);
1786 }
1787 SECTION("first_saturation_deriv dHmass/dP") {
1788 setup();
1789 ASHEOS->update(CoolProp::PQ_INPUTS, 101325, 1);
1790 CoolPropDbl expected = ASHEOS->first_saturation_deriv(CoolProp::iHmass, CoolProp::iP);
1791 ASTTSE->update(CoolProp::PQ_INPUTS, 101325, 1);
1792 CoolPropDbl actual_TTSE = ASTTSE->first_saturation_deriv(CoolProp::iHmass, CoolProp::iP);
1793 ASBICUBIC->update(CoolProp::PQ_INPUTS, 101325, 1);
1794 CoolPropDbl actual_BICUBIC = ASBICUBIC->first_saturation_deriv(CoolProp::iHmass, CoolProp::iP);
1795 CAPTURE(expected);
1796 CAPTURE(actual_TTSE);
1797 CAPTURE(actual_BICUBIC);
1798 CHECK(std::abs((expected - actual_TTSE) / expected) < 1e-6);
1799 CHECK(std::abs((expected - actual_BICUBIC) / expected) < 1e-6);
1800 }
1801 SECTION("first_saturation_deriv dHmass/dP w/ QT as inputs") {
1802 setup();
1803 ASHEOS->update(CoolProp::QT_INPUTS, 1, 370);
1804 CoolPropDbl expected = ASHEOS->first_saturation_deriv(CoolProp::iHmass, CoolProp::iP);
1805 ASTTSE->update(CoolProp::QT_INPUTS, 1, 370);
1806 CoolPropDbl actual_TTSE = ASTTSE->first_saturation_deriv(CoolProp::iHmass, CoolProp::iP);
1807 ASBICUBIC->update(CoolProp::QT_INPUTS, 1, 370);
1808 CoolPropDbl actual_BICUBIC = ASBICUBIC->first_saturation_deriv(CoolProp::iHmass, CoolProp::iP);
1809 CAPTURE(expected);
1810 CAPTURE(actual_TTSE);
1811 CAPTURE(actual_BICUBIC);
1812 CHECK(std::abs((expected - actual_TTSE) / expected) < 1e-6);
1813 CHECK(std::abs((expected - actual_BICUBIC) / expected) < 1e-6);
1814 }
1815 SECTION("first_two_phase_deriv dDmolar/dP|Hmolar") {
1816 setup();
1817 ASHEOS->update(CoolProp::PQ_INPUTS, 101325, 0.1);
1818 CoolPropDbl expected = ASHEOS->first_two_phase_deriv(CoolProp::iDmolar, CoolProp::iP, CoolProp::iHmolar);
1819 ASTTSE->update(CoolProp::PQ_INPUTS, 101325, 0.1);
1820 CoolPropDbl actual_TTSE = ASTTSE->first_two_phase_deriv(CoolProp::iDmolar, CoolProp::iP, CoolProp::iHmolar);
1821 ASBICUBIC->update(CoolProp::PQ_INPUTS, 101325, 0.1);
1822 CoolPropDbl actual_BICUBIC = ASBICUBIC->first_two_phase_deriv(CoolProp::iDmolar, CoolProp::iP, CoolProp::iHmolar);
1823 CAPTURE(expected);
1824 CAPTURE(actual_TTSE);
1825 CAPTURE(actual_BICUBIC);
1826 CHECK(std::abs((expected - actual_TTSE) / expected) < 1e-6);
1827 CHECK(std::abs((expected - actual_BICUBIC) / expected) < 1e-6);
1828 }
1829 SECTION("first_two_phase_deriv dDmass/dP|Hmass") {
1830 setup();
1831 ASHEOS->update(CoolProp::PQ_INPUTS, 101325, 0.1);
1832 CoolPropDbl expected = ASHEOS->first_two_phase_deriv(CoolProp::iDmass, CoolProp::iP, CoolProp::iHmass);
1833 ASTTSE->update(CoolProp::PQ_INPUTS, 101325, 0.1);
1834 CoolPropDbl actual_TTSE = ASTTSE->first_two_phase_deriv(CoolProp::iDmass, CoolProp::iP, CoolProp::iHmass);
1835 ASBICUBIC->update(CoolProp::PQ_INPUTS, 101325, 0.1);
1836 CoolPropDbl actual_BICUBIC = ASBICUBIC->first_two_phase_deriv(CoolProp::iDmass, CoolProp::iP, CoolProp::iHmass);
1837 CAPTURE(expected);
1838 CAPTURE(actual_TTSE);
1839 CAPTURE(actual_BICUBIC);
1840 CHECK(std::abs((expected - actual_TTSE) / expected) < 1e-6);
1841 CHECK(std::abs((expected - actual_BICUBIC) / expected) < 1e-6);
1842 }
1843 SECTION("first_two_phase_deriv dDmass/dHmass|P") {
1844 setup();
1845 ASHEOS->update(CoolProp::PQ_INPUTS, 101325, 0.1);
1846 CoolPropDbl expected = ASHEOS->first_two_phase_deriv(CoolProp::iDmass, CoolProp::iHmass, CoolProp::iP);
1847 ASTTSE->update(CoolProp::PQ_INPUTS, 101325, 0.1);
1848 CoolPropDbl actual_TTSE = ASTTSE->first_two_phase_deriv(CoolProp::iDmass, CoolProp::iHmass, CoolProp::iP);
1849 ASBICUBIC->update(CoolProp::PQ_INPUTS, 101325, 0.1);
1850 CoolPropDbl actual_BICUBIC = ASBICUBIC->first_two_phase_deriv(CoolProp::iDmass, CoolProp::iHmass, CoolProp::iP);
1851 CAPTURE(expected);
1852 CAPTURE(actual_TTSE);
1853 CAPTURE(actual_BICUBIC);
1854 CHECK(std::abs((expected - actual_TTSE) / expected) < 1e-6);
1855 CHECK(std::abs((expected - actual_BICUBIC) / expected) < 1e-6);
1856 }
1857 SECTION("first_partial_deriv dHmass/dT|P") {
1858 setup();
1859 ASHEOS->update(CoolProp::PT_INPUTS, 101325, 300);
1860 double expected = ASHEOS->cpmass();
1861 ASTTSE->update(CoolProp::PT_INPUTS, 101325, 300);
1862 double dhdT_TTSE = ASTTSE->first_partial_deriv(CoolProp::iHmass, CoolProp::iT, CoolProp::iP);
1863 ASBICUBIC->update(CoolProp::PT_INPUTS, 101325, 300);
1864 double dhdT_BICUBIC = ASBICUBIC->first_partial_deriv(CoolProp::iHmass, CoolProp::iT, CoolProp::iP);
1865 CAPTURE(expected);
1866 CAPTURE(dhdT_TTSE);
1867 CAPTURE(dhdT_BICUBIC);
1868 CHECK(std::abs((expected - dhdT_TTSE) / expected) < 1e-4);
1869 CHECK(std::abs((expected - dhdT_BICUBIC) / expected) < 1e-4);
1870 }
1871 SECTION("first_partial_deriv dHmolar/dT|P") {
1872 setup();
1873 ASHEOS->update(CoolProp::PT_INPUTS, 101325, 300);
1874 double expected = ASHEOS->cpmolar();
1875 ASTTSE->update(CoolProp::PT_INPUTS, 101325, 300);
1876 double dhdT_TTSE = ASTTSE->first_partial_deriv(CoolProp::iHmolar, CoolProp::iT, CoolProp::iP);
1877 ASBICUBIC->update(CoolProp::PT_INPUTS, 101325, 300);
1878 double dhdT_BICUBIC = ASBICUBIC->first_partial_deriv(CoolProp::iHmolar, CoolProp::iT, CoolProp::iP);
1879 CAPTURE(expected);
1880 CAPTURE(dhdT_TTSE);
1881 CAPTURE(dhdT_BICUBIC);
1882 CHECK(std::abs((expected - dhdT_TTSE) / expected) < 1e-4);
1883 CHECK(std::abs((expected - dhdT_BICUBIC) / expected) < 1e-4);
1884 }
1885 SECTION("check isentropic process") {
1886 setup();
1887 double T0 = 300;
1888 double p0 = 1e5;
1889 double p1 = 1e6;
1890 ASHEOS->update(CoolProp::PT_INPUTS, p0, 300);
1891 double s0 = ASHEOS->smolar();
1892 ASHEOS->update(CoolProp::PSmolar_INPUTS, p1, s0);
1893 double expected = ASHEOS->T();
1894 ASTTSE->update(CoolProp::PSmolar_INPUTS, p1, s0);
1895 double actual_TTSE = ASTTSE->T();
1896 ASBICUBIC->update(CoolProp::PSmolar_INPUTS, p1, s0);
1897 double actual_BICUBIC = ASBICUBIC->T();
1898 CAPTURE(expected);
1899 CAPTURE(actual_TTSE);
1900 CAPTURE(actual_BICUBIC);
1901 CHECK(std::abs((expected - actual_TTSE) / expected) < 1e-2);
1902 CHECK(std::abs((expected - actual_BICUBIC) / expected) < 1e-2);
1903 }
1904 SECTION("check D=1 mol/m3, T=500 K inputs") {
1905 setup();
1906 double d = 1;
1907 CAPTURE(d);
1908 ASHEOS->update(CoolProp::DmolarT_INPUTS, d, 500);
1909 double expected = ASHEOS->p();
1910 ASTTSE->update(CoolProp::DmolarT_INPUTS, d, 500);
1911 double actual_TTSE = ASTTSE->p();
1912 ASBICUBIC->update(CoolProp::DmolarT_INPUTS, d, 500);
1913 double actual_BICUBIC = ASBICUBIC->p();
1914 CAPTURE(expected);
1915 CAPTURE(actual_TTSE);
1916 CAPTURE(actual_BICUBIC);
1917 CHECK(std::abs((expected - actual_TTSE) / expected) < 1e-3);
1918 CHECK(std::abs((expected - actual_BICUBIC) / expected) < 1e-3);
1919 }
1920
1921 SECTION("fast_evaluate single point matches keyed_output (PT, BICUBIC)") {
1922 setup();
1923 const double T = 400, p = 1e6;
1924 // Reference via the standard path
1925 ASHEOS->update(CoolProp::PT_INPUTS, p, T);
1926 const double ref_D = ASHEOS->rhomolar();
1927 const double ref_H = ASHEOS->hmolar();
1928 const double ref_S = ASHEOS->smolar();
1929
1930 const double val1[1] = {p};
1931 const double val2[1] = {T};
1933 double buf[3] = {0, 0, 0};
1934 int status[1] = {-1};
1935 ASBICUBIC->fast_evaluate(CoolProp::PT_INPUTS, val1, val2, 1, outs, 3, buf, 3, status, 1);
1936 CAPTURE(status[0]);
1937 CHECK(status[0] == CoolProp::fast_evaluate_ok);
1938 CHECK(std::abs((ref_D - buf[0]) / ref_D) < 1e-3);
1939 CHECK(std::abs((ref_H - buf[1]) / ref_H) < 1e-3);
1940 CHECK(std::abs((ref_S - buf[2]) / ref_S) < 1e-3);
1941 }
1942
1943 SECTION("fast_evaluate batch — Hmolar/p inputs over a range (BICUBIC)") {
1944 setup();
1945 constexpr std::size_t N = 32;
1946 const double p = 5e5;
1947 std::vector<double> hs(N), ps(N, p);
1948 // Pick a band of single-phase enthalpies above saturation at 500 kPa.
1949 ASHEOS->update(CoolProp::PQ_INPUTS, p, 1.0);
1950 const double h_satV = ASHEOS->hmolar();
1951 for (std::size_t k = 0; k < N; ++k) {
1952 hs[k] = h_satV + 100.0 + k * 50.0; // walk above saturation
1953 }
1955 std::vector<double> buf(2 * N, 0);
1956 std::vector<int> status(N, -1);
1957 ASBICUBIC->fast_evaluate(CoolProp::HmolarP_INPUTS, hs.data(), ps.data(), N, outs, 2, buf.data(), buf.size(), status.data(), status.size());
1958 for (std::size_t k = 0; k < N; ++k) {
1959 CAPTURE(k);
1960 CAPTURE(status[k]);
1961 CHECK(status[k] == CoolProp::fast_evaluate_ok);
1962 // Compare against the cached path
1963 ASBICUBIC->update(CoolProp::HmolarP_INPUTS, hs[k], p);
1964 CHECK(std::abs((ASBICUBIC->T() - buf[2 * k + 0]) / ASBICUBIC->T()) < 1e-9);
1965 CHECK(std::abs((ASBICUBIC->rhomolar() - buf[2 * k + 1]) / ASBICUBIC->rhomolar()) < 1e-9);
1966 }
1967 }
1968
1969 SECTION("fast_evaluate validates buffer sizes") {
1970 setup();
1971 const double v1[1] = {1e6};
1972 const double v2[1] = {400};
1974 double buf[2] = {0, 0};
1975 int status[1] = {0};
1976 // out_buffer too small (1 instead of N*M=2)
1977 CHECK_THROWS(ASBICUBIC->fast_evaluate(CoolProp::PT_INPUTS, v1, v2, 1, outs, 2, buf, 1, status, 1));
1978 // status_flags too small (0 instead of N=1)
1979 CHECK_THROWS(ASBICUBIC->fast_evaluate(CoolProp::PT_INPUTS, v1, v2, 1, outs, 2, buf, 2, status, 0));
1980 // Unsupported input pair
1981 CHECK_THROWS(ASBICUBIC->fast_evaluate(CoolProp::QT_INPUTS, v1, v2, 1, outs, 2, buf, 2, status, 1));
1982 }
1983
1984 SECTION("fast_evaluate out-of-range point reports NaN + flag, others ok") {
1985 setup();
1986 const double v1[2] = {1e6, 1e20}; // p: 1 MPa (ok), 1e20 Pa (out of range)
1987 const double v2[2] = {400, 400};
1988 const CoolProp::parameters outs[1] = {CoolProp::iDmolar};
1989 double buf[2] = {-1, -1};
1990 int status[2] = {-1, -1};
1991 ASBICUBIC->fast_evaluate(CoolProp::PT_INPUTS, v1, v2, 2, outs, 1, buf, 2, status, 2);
1992 CHECK(status[0] == CoolProp::fast_evaluate_ok);
1993 CHECK(status[1] == CoolProp::fast_evaluate_out_of_range);
1994 CHECK(std::isnan(buf[1]));
1995 CHECK(buf[0] > 0); // a sensible density value
1996 }
1997}
1998
1999TEST_CASE("fast_evaluate works with IF97 backend", "[fast_evaluate][IF97]") {
2000 using namespace CoolProp;
2001 std::shared_ptr<AbstractState> AS(AbstractState::factory("IF97", "Water"));
2002
2003 SECTION("PT batch matches the cached path") {
2004 constexpr std::size_t N = 16;
2005 std::vector<double> ps(N), Ts(N);
2006 for (std::size_t k = 0; k < N; ++k) {
2007 ps[k] = 1e5 + k * 1e4;
2008 Ts[k] = 500 + k * 5.0; // superheated vapor / supercritical band
2009 }
2010 const parameters outs[3] = {iDmass, iHmass, iSmass};
2011 std::vector<double> buf(3 * N, 0);
2012 std::vector<int> status(N, -1);
2013 AS->fast_evaluate(PT_INPUTS, ps.data(), Ts.data(), N, outs, 3, buf.data(), buf.size(), status.data(), status.size());
2014 for (std::size_t k = 0; k < N; ++k) {
2015 CAPTURE(k);
2016 CHECK(status[k] == fast_evaluate_ok);
2017 AS->update(PT_INPUTS, ps[k], Ts[k]);
2018 CHECK(std::abs((AS->rhomass() - buf[3 * k + 0]) / AS->rhomass()) < 1e-12);
2019 CHECK(std::abs((AS->hmass() - buf[3 * k + 1]) / AS->hmass()) < 1e-12);
2020 CHECK(std::abs((AS->smass() - buf[3 * k + 2]) / AS->smass()) < 1e-12);
2021 }
2022 }
2023}
2024
2025TEST_CASE("fast_evaluate is not implemented for HEOS backend", "[fast_evaluate][HEOS]") {
2026 using namespace CoolProp;
2027 std::shared_ptr<AbstractState> AS(AbstractState::factory("HEOS", "Water"));
2028 const double v1[1] = {1e6};
2029 const double v2[1] = {400};
2030 const parameters outs[1] = {iDmolar};
2031 double buf[1] = {0};
2032 int status[1] = {0};
2033 CHECK_THROWS(AS->fast_evaluate(PT_INPUTS, v1, v2, 1, outs, 1, buf, 1, status, 1));
2034}
2035# endif // ENABLE_CATCH
2036
2037#endif // !defined(NO_TABULAR_BACKENDS)