CoolProp 8.0.0
An open-source fluid property and humid air property database
nanobind_interface.cxx
Go to the documentation of this file.
1#ifdef NANOBIND
2
3# include "CoolProp/CoolProp.h"
8# include "CoolProp/numerics/numerics.h" // ValidNumber: scalar PropsSI/HAPropsSI raise on non-finite (Cython parity)
12
13# include <nanobind/nanobind.h>
14# include <nanobind/stl/string.h>
15# include <nanobind/stl/string_view.h> // get_parameter_information takes std::string_view
16# include <nanobind/stl/vector.h>
17# include <nanobind/stl/tuple.h>
18# include <nanobind/stl/pair.h>
19# include <nanobind/stl/map.h>
20# include <nanobind/ndarray.h> // vectorized PropsSI returns a numpy array
21# include <algorithm>
22# include <array>
23namespace nb = nanobind;
24
25CoolProp::AbstractState* factory(const std::string& backend, const std::string& fluid_names) {
26 return CoolProp::AbstractState::factory(backend, fluid_names);
27}
28
29// Split a delimited param string into a list, mirroring the str.split() used by
30// the Cython high-level convenience wrappers below (FluidsList, get_aliases).
31static std::vector<std::string> _split_str(const std::string& s, char delim) {
32 std::vector<std::string> out;
33 std::string cur;
34 for (char c : s) {
35 if (c == delim) {
36 out.push_back(cur);
37 cur.clear();
38 } else {
39 cur += c;
40 }
41 }
42 // Keep Python str.split() semantics for non-empty input (trailing/empty
43 // tokens preserved), but return [] -- not [""] -- for an empty input, so
44 // e.g. an empty FluidsList/aliases string yields an empty list.
45 if (!cur.empty() || !s.empty()) {
46 out.push_back(cur);
47 }
48 return out;
49}
50
51// Scalar-or-sequence -> vector<double> for the vectorized PropsSI/HAPropsSI
52// overloads; is_seq reports whether the input was array-like (vs a scalar).
53//
54// Mirrors the legacy Cython `iterable()` test: a 1-D list/tuple/ndarray is a
55// sequence; a 0-D array, a numpy scalar (np.float64/np.int64), a Python int or
56// float -- anything with __float__ -- is a SCALAR. Crucially this keeps the
57// canonical int-literal call PropsSI("T","P",101325,...) on the scalar path so
58// it returns a float, not a 1-element ndarray (bd CoolProp-r9sq.21).
59static std::vector<double> _to_vec(nb::handle o, bool& is_seq) {
60 if (nb::hasattr(o, "ndim")) { // numpy array / scalar: dispatch on ndim
61 long ndim = nb::cast<long>(nb::getattr(o, "ndim"));
62 if (ndim > 1) {
63 // Reject multi-dimensional input rather than silently flattening it.
64 PyErr_SetString(PyExc_ValueError, "vectorized PropsSI/HAPropsSI input is not one-dimensional");
65 throw nb::python_error();
66 }
67 if (ndim == 0) { // 0-D array is a scalar
68 is_seq = false;
69 return {nb::cast<double>(nb::float_(o))};
70 }
71 is_seq = true; // 1-D ndarray
72 return nb::cast<std::vector<double>>(o);
73 }
74 if (nb::isinstance<nb::list>(o) || nb::isinstance<nb::tuple>(o)) {
75 is_seq = true;
76 return nb::cast<std::vector<double>>(o);
77 }
78 // Scalar: Python int/float, numpy scalar, or anything with __float__. Coerce
79 // through nb::float_ (PyNumber_Float) so int/np.float64 are accepted as scalars.
80 is_seq = false;
81 return {nb::cast<double>(nb::float_(o))};
82}
83
84// Raise a Python ValueError carrying CoolProp's global error string if x is not
85// finite -- the legacy scalar PropsSI/HAPropsSI raised ValueError rather than
86// returning inf/nan (bd CoolProp-r9sq.21/.22).
87// A single output name -> 1-element vector; a list/tuple/ndarray of names ->
88// the corresponding vector<string>. Drives the multi-output PropsSI overload
89// (first argument a sequence of outputs), mirroring the legacy Cython
90// iterable(in1) branch that builds vin1 from a string-or-sequence in1.
91static std::vector<std::string> _to_str_vec(nb::handle o) {
92 if (nb::isinstance<nb::str>(o)) {
93 return {nb::cast<std::string>(o)};
94 }
95 // list/tuple/ndarray of strings (or any other iterable of str): iterate so a
96 // numpy '<U..' string array works as well as a Python list/tuple.
97 std::vector<std::string> out;
98 for (nb::handle item : o) {
99 out.push_back(nb::cast<std::string>(item));
100 }
101 return out;
102}
103
104static void _raise_if_invalid(double x) {
105 // Qualified: this helper is at file scope, before init_CoolProp's `using namespace CoolProp`.
106 // (ValidNumber lives in the global namespace; get_global_param_string in CoolProp.)
107 if (!ValidNumber(x)) {
108 PyErr_SetString(PyExc_ValueError, CoolProp::get_global_param_string("errstring").c_str());
109 throw nb::python_error();
110 }
111}
112
113// Broadcast a size-1 vector up to length n; lengths other than 1 or n are an error.
114static void _broadcast_to(std::vector<double>& v, std::size_t n, const char* which) {
115 if (v.size() == n) {
116 return;
117 }
118 if (v.size() == 1) {
119 double val = v[0]; // copy first: v[0] aliases the vector that assign() clears
120 v.assign(n, val);
121 return;
122 }
123 // Match the legacy Cython wrapper, which raises TypeError on a length mismatch.
124 PyErr_SetString(PyExc_TypeError, (std::string("vectorized input ") + which + " has an incompatible length").c_str());
125 throw nb::python_error();
126}
127
128// ---- State C-ABI capsule (bridge for the frozen Cython `State` compat shim) --
129// The opaque handle carries a shared_ptr<AbstractState> directly.
130namespace {
131using _CAPI_SP = std::shared_ptr<CoolProp::AbstractState>;
132thread_local std::string g_capi_error; // message for the most recent failure; "" == none
133
134// Stash an error message without ever throwing out of the extern "C" frame (the
135// assignment allocates, hence the catch). On OOM the message is left empty and
136// the shim falls back to its own generic text.
137void capi_set_error(const char* msg) noexcept {
138 try {
139 g_capi_error = (msg != nullptr) ? msg : "unknown C++ exception";
140 } catch (...) {
141 g_capi_error.clear(); // clear() is noexcept
142 }
143}
144void* capi_make(const char* backend, const char* fluids) {
145 try {
146 auto* p = new _CAPI_SP(CoolProp::AbstractState::factory(backend, fluids));
147 g_capi_error.clear();
148 return p;
149 } catch (const std::exception& e) {
150 capi_set_error(e.what());
151 return nullptr;
152 } catch (...) {
153 capi_set_error(nullptr);
154 return nullptr;
155 }
156}
157void capi_destroy(void* h) {
158 try {
159 delete static_cast<_CAPI_SP*>(h);
160 } catch (...) { // ~AbstractState is effectively noexcept; defensive against future changes
161 }
162}
163void capi_update(void* h, long input_pair, double v1, double v2) {
164 if (h == nullptr) {
165 capi_set_error("update: null handle");
166 return;
167 }
168 try {
169 (*static_cast<_CAPI_SP*>(h))->update(static_cast<CoolProp::input_pairs>(input_pair), v1, v2);
170 g_capi_error.clear();
171 } catch (const std::exception& e) {
172 capi_set_error(e.what());
173 } catch (...) {
174 capi_set_error(nullptr);
175 }
176}
177double capi_keyed_output(void* h, long key) {
178 if (h == nullptr) {
179 capi_set_error("keyed_output: null handle");
180 return NAN;
181 }
182 try {
183 double v = (*static_cast<_CAPI_SP*>(h))->keyed_output(static_cast<CoolProp::parameters>(key));
184 g_capi_error.clear();
185 return v;
186 } catch (const std::exception& e) {
187 capi_set_error(e.what());
188 return NAN;
189 } catch (...) {
190 capi_set_error(nullptr);
191 return NAN;
192 }
193}
194double capi_first_partial_deriv(void* h, long Of, long Wrt, long Constant) {
195 if (h == nullptr) {
196 capi_set_error("first_partial_deriv: null handle");
197 return NAN;
198 }
199 try {
200 double v = (*static_cast<_CAPI_SP*>(h))
201 ->first_partial_deriv(static_cast<CoolProp::parameters>(Of), static_cast<CoolProp::parameters>(Wrt),
202 static_cast<CoolProp::parameters>(Constant));
203 g_capi_error.clear();
204 return v;
205 } catch (const std::exception& e) {
206 capi_set_error(e.what());
207 return NAN;
208 } catch (...) {
209 capi_set_error(nullptr);
210 return NAN;
211 }
212}
213const char* capi_last_error() {
214 return g_capi_error.empty() ? nullptr : g_capi_error.c_str();
215}
216void capi_set_mole_fractions(void* h, const double* z, long n) {
217 // Validate the C-ABI pointers/length before constructing or dereferencing
218 // (a null handle/fractions or negative length from any consumer must not crash).
219 if (h == nullptr) {
220 capi_set_error("set_mole_fractions: null handle");
221 return;
222 }
223 if (n < 0) {
224 capi_set_error("set_mole_fractions: negative length");
225 return;
226 }
227 if (n > 0 && z == nullptr) {
228 capi_set_error("set_mole_fractions: null fractions pointer");
229 return;
230 }
231 try {
232 std::vector<double> fractions(z, z + n); // binds the vector<double> set_mole_fractions overload
233 (*static_cast<_CAPI_SP*>(h))->set_mole_fractions(fractions);
234 g_capi_error.clear();
235 } catch (const std::exception& e) {
236 capi_set_error(e.what());
237 } catch (...) {
238 capi_set_error(nullptr);
239 }
240}
241void capi_specify_phase(void* h, long phase) {
242 if (h == nullptr) {
243 capi_set_error("specify_phase: null handle");
244 return;
245 }
246 try {
247 (*static_cast<_CAPI_SP*>(h))->specify_phase(static_cast<CoolProp::phases>(phase));
248 g_capi_error.clear();
249 } catch (const std::exception& e) {
250 capi_set_error(e.what());
251 } catch (...) {
252 capi_set_error(nullptr);
253 }
254}
255void capi_unspecify_phase(void* h) {
256 if (h == nullptr) {
257 capi_set_error("unspecify_phase: null handle");
258 return;
259 }
260 try {
261 (*static_cast<_CAPI_SP*>(h))->unspecify_phase();
262 g_capi_error.clear();
263 } catch (const std::exception& e) {
264 capi_set_error(e.what());
265 } catch (...) {
266 capi_set_error(nullptr);
267 }
268}
269const CoolProp_StateCAPI g_state_capi = {
270 capi_make, capi_destroy, capi_update, capi_keyed_output, capi_first_partial_deriv, capi_last_error, capi_set_mole_fractions,
271 capi_specify_phase, capi_unspecify_phase};
272} // namespace
273
274// Bind the superancillary classes (Chebyshev rootfinding building blocks +
275// the SuperAncillary saturation evaluator). 1:1 mirror of the legacy Cython
276// surface in wrappers/Python/CoolProp/CoolProp.pyx, with the raw-pointer C
277// methods wrapped to take/return contiguous 1-D numpy arrays. ArrayType is
278// std::vector<double> to match the legacy bindings. See CoolProp-1tbe.5.
279static void init_superancillary(nb::module_& m) {
280 namespace sa = CoolProp::superancillary;
281 using SAArray = std::vector<double>;
282 using ChebExp = sa::ChebyshevExpansion<SAArray>;
283 using ChebApprox1D = sa::ChebyshevApproximation1D<SAArray>;
284 using SuperAnc = sa::SuperAncillary<SAArray>;
285 using ArrD = nb::ndarray<double, nb::ndim<1>, nb::c_contig>;
286 using ArrSz = nb::ndarray<std::size_t, nb::ndim<1>, nb::c_contig>;
287
288 // The two POD structs returned by ChebyshevApproximation1D::monotonic_intervals().
289 nb::class_<sa::MonotonicExpansionMatch>(m, "MonotonicExpansionMatch")
290 .def_ro("idx", &sa::MonotonicExpansionMatch::idx)
291 .def_ro("ymin", &sa::MonotonicExpansionMatch::ymin)
292 .def_ro("ymax", &sa::MonotonicExpansionMatch::ymax)
293 .def_ro("xmin", &sa::MonotonicExpansionMatch::xmin)
294 .def_ro("xmax", &sa::MonotonicExpansionMatch::xmax);
295
296 nb::class_<sa::IntervalMatch>(m, "IntervalMatch")
297 .def_ro("expansioninfo", &sa::IntervalMatch::expansioninfo)
298 .def_ro("xmin", &sa::IntervalMatch::xmin)
299 .def_ro("xmax", &sa::IntervalMatch::xmax)
300 .def_ro("ymin", &sa::IntervalMatch::ymin)
301 .def_ro("ymax", &sa::IntervalMatch::ymax);
302
303 nb::class_<ChebExp>(m, "ChebyshevExpansion")
304 .def(nb::init<double, double, SAArray>(), nb::arg("xmin"), nb::arg("xmax"), nb::arg("coef"))
305 .def("xmin", &ChebExp::xmin)
306 .def("xmax", &ChebExp::xmax)
307 .def("coeff", [](const ChebExp& e) { return e.coeff(); })
308 .def(
309 "eval_many",
310 [](const ChebExp& e, ArrD x, ArrD y) {
311 if (x.shape(0) != y.shape(0)) {
312 throw nb::value_error("x and y are not the same size");
313 }
314 e.eval_manyC<double>(x.data(), y.data(), x.shape(0));
315 },
316 nb::arg("x"), nb::arg("y"))
317 .def("solve_for_x", &ChebExp::solve_for_x, nb::arg("y"), nb::arg("a"), nb::arg("b"), nb::arg("bits"), nb::arg("max_iter"),
318 nb::arg("boundstytol"))
319 .def(
320 "solve_for_x_many",
321 [](const ChebExp& e, ArrD y, double a, double b, unsigned int bits, std::size_t max_iter, double boundstytol, ArrD x, ArrSz counts) {
322 if (y.shape(0) != x.shape(0) || y.shape(0) != counts.shape(0)) {
323 throw nb::value_error("y, x and counts are not the same size");
324 }
325 e.solve_for_x_manyC<double, std::size_t>(y.data(), y.shape(0), a, b, bits, max_iter, boundstytol, x.data(), counts.data());
326 },
327 nb::arg("y"), nb::arg("a"), nb::arg("b"), nb::arg("bits"), nb::arg("max_iter"), nb::arg("boundstytol"), nb::arg("x"), nb::arg("counts"));
328
329 nb::class_<ChebApprox1D>(m, "ChebyshevApproximation1D")
330 // By-value in nb::init: nanobind materializes the vector from the Python
331 // list and passes it as an rvalue, binding the C++ `vector&&` ctor. (An
332 // `nb::init<vector&&>` mis-handles the rvalue-ref caster and segfaults.)
333 .def(nb::init<std::vector<ChebExp>>(), nb::arg("expansions"))
334 .def("xmin", &ChebApprox1D::xmin)
335 .def("xmax", &ChebApprox1D::xmax)
336 .def("is_monotonic", &ChebApprox1D::is_monotonic)
337 .def(
338 "eval_many",
339 [](const ChebApprox1D& a, ArrD x, ArrD y) {
340 if (x.shape(0) != y.shape(0)) {
341 throw nb::value_error("x and y are not the same size");
342 }
343 a.eval_manyC<double>(x.data(), y.data(), x.shape(0));
344 },
345 nb::arg("x"), nb::arg("y"))
346 .def("get_x_for_y", &ChebApprox1D::get_x_for_y, nb::arg("y"), nb::arg("bits"), nb::arg("max_iter"), nb::arg("boundstytol"))
347 .def(
348 "count_x_for_y_many",
349 [](const ChebApprox1D& a, ArrD y, unsigned int bits, std::size_t max_iter, double boundstytol, ArrSz counts) {
350 if (y.shape(0) != counts.shape(0)) {
351 throw nb::value_error("y and counts are not the same size");
352 }
353 a.count_x_for_y_manyC<double, std::size_t>(y.data(), y.shape(0), bits, max_iter, boundstytol, counts.data());
354 },
355 nb::arg("y"), nb::arg("bits"), nb::arg("max_iter"), nb::arg("boundstytol"), nb::arg("counts"))
356 .def("monotonic_intervals", [](const ChebApprox1D& a) { return a.get_monotonic_intervals(); });
357
358 nb::class_<SuperAnc>(m, "SuperAncillary")
359 .def(nb::init<const std::string&>(), nb::arg("json_as_string"))
360 .def(
361 "eval_sat",
362 [](const SuperAnc& s, double T, const std::string& prop, short Q) {
363 if (prop.empty()) {
364 throw nb::value_error("prop must be a non-empty string");
365 }
366 return s.eval_sat(T, prop[0], Q);
367 },
368 nb::arg("T"), nb::arg("prop"), nb::arg("Q"))
369 .def(
370 "eval_sat_many",
371 [](const SuperAnc& s, ArrD T, const std::string& prop, short Q, ArrD y) {
372 if (prop.empty()) {
373 throw nb::value_error("prop must be a non-empty string");
374 }
375 if (T.shape(0) != y.shape(0)) {
376 throw nb::value_error("T and y are not the same size");
377 }
378 s.eval_sat_manyC<double>(T.data(), T.shape(0), prop[0], Q, y.data());
379 },
380 nb::arg("T"), nb::arg("prop"), nb::arg("Q"), nb::arg("y"));
381}
382
383void init_CoolProp(nb::module_& m) {
384 using namespace CoolProp;
385
386 // Map every CoolProp C++ exception to a Python ValueError, matching the
387 // legacy Cython wrapper (which declared `except +ValueError` on every
388 // bound method). Without this, nanobind's default translator sends
389 // CoolPropBaseError -- which derives from std::exception, not one of the
390 // std types nanobind special-cases -- to RuntimeError, silently breaking
391 // the canonical `try: ... except ValueError` pattern around a failed
392 // AbstractState.update() (incl. CoolProp's own shipped Plots code).
393 // See CoolProp-1tbe.2.
394 nb::register_exception_translator([](const std::exception_ptr& p, void* /*payload*/) {
395 try {
396 std::rethrow_exception(p);
397 } catch (const CoolProp::CoolPropBaseError& e) {
398 PyErr_SetString(PyExc_ValueError, e.what());
399 }
400 });
401
402 nb::class_<SimpleState>(m, "SimpleState")
403 .def(nb::init<>())
404 .def_rw("T", &SimpleState::T)
405 .def_rw("p", &SimpleState::p)
406 .def_rw("rhomolar", &SimpleState::rhomolar);
407
408 nb::class_<GuessesStructure>(m, "GuessesStructure")
409 .def(nb::init<>())
410 .def_rw("T", &GuessesStructure::T)
411 .def_rw("p", &GuessesStructure::p)
412 .def_rw("rhomolar", &GuessesStructure::rhomolar)
413 .def_rw("hmolar", &GuessesStructure::hmolar)
414 .def_rw("smolar", &GuessesStructure::smolar)
415 .def_rw("rhomolar_liq", &GuessesStructure::rhomolar_liq)
416 .def_rw("rhomolar_vap", &GuessesStructure::rhomolar_vap)
417 .def_rw("x", &GuessesStructure::x)
418 .def_rw("y", &GuessesStructure::y)
419 .def("clear", &GuessesStructure::clear);
420
421 nb::class_<CriticalState, SimpleState>(m, "CriticalState").def(nb::init<>()).def_rw("stable", &CriticalState::stable);
422
423 nb::class_<PhaseEnvelopeData>(m, "PhaseEnvelopeData")
424 .def(nb::init<>())
425 .def_rw("K", &PhaseEnvelopeData::K)
426 .def_rw("lnK", &PhaseEnvelopeData::lnK)
427 .def_rw("x", &PhaseEnvelopeData::x)
428 .def_rw("y", &PhaseEnvelopeData::y)
429 .def_rw("T", &PhaseEnvelopeData::T)
430 .def_rw("p", &PhaseEnvelopeData::p)
431 .def_rw("lnT", &PhaseEnvelopeData::lnT)
432 .def_rw("lnp", &PhaseEnvelopeData::lnp)
433 .def_rw("rhomolar_liq", &PhaseEnvelopeData::rhomolar_liq)
434 .def_rw("rhomolar_vap", &PhaseEnvelopeData::rhomolar_vap)
435 .def_rw("lnrhomolar_liq", &PhaseEnvelopeData::lnrhomolar_liq)
436 .def_rw("lnrhomolar_vap", &PhaseEnvelopeData::lnrhomolar_vap)
437 .def_rw("hmolar_liq", &PhaseEnvelopeData::hmolar_liq)
438 .def_rw("hmolar_vap", &PhaseEnvelopeData::hmolar_vap)
439 .def_rw("smolar_liq", &PhaseEnvelopeData::smolar_liq)
440 .def_rw("smolar_vap", &PhaseEnvelopeData::smolar_vap)
441 .def_rw("Q", &PhaseEnvelopeData::Q)
442 .def_rw("cpmolar_liq", &PhaseEnvelopeData::cpmolar_liq)
443 .def_rw("cpmolar_vap", &PhaseEnvelopeData::cpmolar_vap)
444 .def_rw("cvmolar_liq", &PhaseEnvelopeData::cvmolar_liq)
445 .def_rw("cvmolar_vap", &PhaseEnvelopeData::cvmolar_vap)
446 .def_rw("viscosity_liq", &PhaseEnvelopeData::viscosity_liq)
447 .def_rw("viscosity_vap", &PhaseEnvelopeData::viscosity_vap)
448 .def_rw("conductivity_liq", &PhaseEnvelopeData::conductivity_liq)
449 .def_rw("conductivity_vap", &PhaseEnvelopeData::conductivity_vap)
450 .def_rw("speed_sound_vap", &PhaseEnvelopeData::speed_sound_vap);
451
452 // See http://stackoverflow.com/a/148610 and http://stackoverflow.com/questions/147267/easy-way-to-use-variables-of-enum-types-as-string-in-c#202511
453 nb::enum_<configuration_keys>(m, "configuration_keys", nb::is_arithmetic())
454# define X(Enum, String, Default, Desc) .value(String, configuration_keys::Enum)
456# undef X
457 .export_values();
458
459 nb::enum_<parameters>(m, "parameters", nb::is_arithmetic())
460 .value("igas_constant", parameters::igas_constant)
461 .value("imolar_mass", parameters::imolar_mass)
462 .value("iacentric_factor", parameters::iacentric_factor)
463 .value("irhomolar_reducing", parameters::irhomolar_reducing)
464 .value("irhomolar_critical", parameters::irhomolar_critical)
465 .value("iT_reducing", parameters::iT_reducing)
466 .value("iT_critical", parameters::iT_critical)
467 .value("irhomass_reducing", parameters::irhomass_reducing)
468 .value("irhomass_critical", parameters::irhomass_critical)
469 .value("iP_critical", parameters::iP_critical)
470 .value("iP_reducing", parameters::iP_reducing)
471 .value("iT_triple", parameters::iT_triple)
472 .value("iP_triple", parameters::iP_triple)
473 .value("iT_min", parameters::iT_min)
474 .value("iT_max", parameters::iT_max)
475 .value("iP_max", parameters::iP_max)
476 .value("iP_min", parameters::iP_min)
477 .value("idipole_moment", parameters::idipole_moment)
478 .value("iT", parameters::iT)
479 .value("iP", parameters::iP)
480 .value("iQ", parameters::iQ)
481 .value("iTau", parameters::iTau)
482 .value("iDelta", parameters::iDelta)
483 .value("iDmolar", parameters::iDmolar)
484 .value("iHmolar", parameters::iHmolar)
485 .value("iSmolar", parameters::iSmolar)
486 .value("iCpmolar", parameters::iCpmolar)
487 .value("iCp0molar", parameters::iCp0molar)
488 .value("iCvmolar", parameters::iCvmolar)
489 .value("iUmolar", parameters::iUmolar)
490 .value("iGmolar", parameters::iGmolar)
491 .value("iHelmholtzmolar", parameters::iHelmholtzmolar)
492 .value("iSmolar_residual", parameters::iSmolar_residual)
493 .value("iHmolar_residual", parameters::iHmolar_residual)
494 .value("iGmolar_residual", parameters::iGmolar_residual)
495 .value("iDmass", parameters::iDmass)
496 .value("iHmass", parameters::iHmass)
497 .value("iSmass", parameters::iSmass)
498 .value("iCpmass", parameters::iCpmass)
499 .value("iCp0mass", parameters::iCp0mass)
500 .value("iCvmass", parameters::iCvmass)
501 .value("iUmass", parameters::iUmass)
502 .value("iGmass", parameters::iGmass)
503 .value("iHelmholtzmass", parameters::iHelmholtzmass)
504 .value("iviscosity", parameters::iviscosity)
505 .value("iconductivity", parameters::iconductivity)
506 .value("isurface_tension", parameters::isurface_tension)
507 .value("iPrandtl", parameters::iPrandtl)
508 .value("ispeed_sound", parameters::ispeed_sound)
509 .value("iisothermal_compressibility", parameters::iisothermal_compressibility)
510 .value("iisobaric_expansion_coefficient", parameters::iisobaric_expansion_coefficient)
511 .value("ifundamental_derivative_of_gas_dynamics", parameters::ifundamental_derivative_of_gas_dynamics)
512 .value("ialphar", parameters::ialphar)
513 .value("idalphar_ddelta_consttau", parameters::idalphar_ddelta_consttau)
514 .value("idalpha0_dtau_constdelta", parameters::idalpha0_dtau_constdelta)
515 .value("iBvirial", parameters::iBvirial)
516 .value("iCvirial", parameters::iCvirial)
517 .value("idBvirial_dT", parameters::idBvirial_dT)
518 .value("idCvirial_dT", parameters::idCvirial_dT)
519 .value("iZ", parameters::iZ)
520 .value("iPIP", parameters::iPIP)
521 .value("ifraction_min", parameters::ifraction_min)
522 .value("ifraction_max", parameters::ifraction_max)
523 .value("iT_freeze", parameters::iT_freeze)
524 .value("iGWP20", parameters::iGWP20)
525 .value("iGWP100", parameters::iGWP100)
526 .value("iGWP500", parameters::iGWP500)
527 .value("iFH", parameters::iFH)
528 .value("iHH", parameters::iHH)
529 .value("iPH", parameters::iPH)
530 .value("iODP", parameters::iODP)
531 .value("iPhase", parameters::iPhase)
532 // bd CoolProp-r9sq.23: values the hand-written enum had drifted from
533 // DataStructures.h (mass-basis quality, ideal-gas decompositions, the
534 // isentropic-expansion + alpha0 derivatives).
535 .value("INVALID_PARAMETER", parameters::INVALID_PARAMETER)
536 .value("iQmass", parameters::iQmass)
537 .value("iHmolar_idealgas", parameters::iHmolar_idealgas)
538 .value("iSmolar_idealgas", parameters::iSmolar_idealgas)
539 .value("iUmolar_idealgas", parameters::iUmolar_idealgas)
540 .value("iHmass_idealgas", parameters::iHmass_idealgas)
541 .value("iSmass_idealgas", parameters::iSmass_idealgas)
542 .value("iUmass_idealgas", parameters::iUmass_idealgas)
543 .value("iisentropic_expansion_coefficient", parameters::iisentropic_expansion_coefficient)
544 .value("idalphar_dtau_constdelta", parameters::idalphar_dtau_constdelta)
545 .value("ialpha0", parameters::ialpha0)
546 .value("idalpha0_ddelta_consttau", parameters::idalpha0_ddelta_consttau)
547 .value("id2alpha0_ddelta2_consttau", parameters::id2alpha0_ddelta2_consttau)
548 .value("id3alpha0_ddelta3_consttau", parameters::id3alpha0_ddelta3_consttau)
549 .value("iundefined_parameter", parameters::iundefined_parameter)
550 .export_values();
551
552 nb::enum_<input_pairs>(m, "input_pairs", nb::is_arithmetic())
553 .value("QT_INPUTS", input_pairs::QT_INPUTS)
554 .value("PQ_INPUTS", input_pairs::PQ_INPUTS)
555 .value("QSmolar_INPUTS", input_pairs::QSmolar_INPUTS)
556 .value("QSmass_INPUTS", input_pairs::QSmass_INPUTS)
557 .value("HmolarQ_INPUTS", input_pairs::HmolarQ_INPUTS)
558 .value("HmassQ_INPUTS", input_pairs::HmassQ_INPUTS)
559 .value("DmolarQ_INPUTS", input_pairs::DmolarQ_INPUTS)
560 .value("DmassQ_INPUTS", input_pairs::DmassQ_INPUTS)
561 .value("PT_INPUTS", input_pairs::PT_INPUTS)
562 .value("DmassT_INPUTS", input_pairs::DmassT_INPUTS)
563 .value("DmolarT_INPUTS", input_pairs::DmolarT_INPUTS)
564 .value("HmolarT_INPUTS", input_pairs::HmolarT_INPUTS)
565 .value("HmassT_INPUTS", input_pairs::HmassT_INPUTS)
566 .value("SmolarT_INPUTS", input_pairs::SmolarT_INPUTS)
567 .value("SmassT_INPUTS", input_pairs::SmassT_INPUTS)
568 .value("TUmolar_INPUTS", input_pairs::TUmolar_INPUTS)
569 .value("TUmass_INPUTS", input_pairs::TUmass_INPUTS)
570 .value("DmassP_INPUTS", input_pairs::DmassP_INPUTS)
571 .value("DmolarP_INPUTS", input_pairs::DmolarP_INPUTS)
572 .value("HmassP_INPUTS", input_pairs::HmassP_INPUTS)
573 .value("HmolarP_INPUTS", input_pairs::HmolarP_INPUTS)
574 .value("PSmass_INPUTS", input_pairs::PSmass_INPUTS)
575 .value("PSmolar_INPUTS", input_pairs::PSmolar_INPUTS)
576 .value("PUmass_INPUTS", input_pairs::PUmass_INPUTS)
577 .value("PUmolar_INPUTS", input_pairs::PUmolar_INPUTS)
578 .value("HmassSmass_INPUTS", input_pairs::HmassSmass_INPUTS)
579 .value("HmolarSmolar_INPUTS", input_pairs::HmolarSmolar_INPUTS)
580 .value("SmassUmass_INPUTS", input_pairs::SmassUmass_INPUTS)
581 .value("SmolarUmolar_INPUTS", input_pairs::SmolarUmolar_INPUTS)
582 .value("DmassHmass_INPUTS", input_pairs::DmassHmass_INPUTS)
583 .value("DmolarHmolar_INPUTS", input_pairs::DmolarHmolar_INPUTS)
584 .value("DmassSmass_INPUTS", input_pairs::DmassSmass_INPUTS)
585 .value("DmolarSmolar_INPUTS", input_pairs::DmolarSmolar_INPUTS)
586 .value("DmassUmass_INPUTS", input_pairs::DmassUmass_INPUTS)
587 .value("DmolarUmolar_INPUTS", input_pairs::DmolarUmolar_INPUTS)
588 // bd CoolProp-r9sq.23: the mass-basis quality pairs (needed for
589 // AbstractState.update(QmassT_INPUTS, ...)) + INVALID were missing.
590 .value("INPUT_PAIR_INVALID", input_pairs::INPUT_PAIR_INVALID)
591 .value("QmassT_INPUTS", input_pairs::QmassT_INPUTS)
592 .value("PQmass_INPUTS", input_pairs::PQmass_INPUTS)
593 .value("QmassSmolar_INPUTS", input_pairs::QmassSmolar_INPUTS)
594 .value("QmassSmass_INPUTS", input_pairs::QmassSmass_INPUTS)
595 .value("HmolarQmass_INPUTS", input_pairs::HmolarQmass_INPUTS)
596 .value("HmassQmass_INPUTS", input_pairs::HmassQmass_INPUTS)
597 .value("DmolarQmass_INPUTS", input_pairs::DmolarQmass_INPUTS)
598 .value("DmassQmass_INPUTS", input_pairs::DmassQmass_INPUTS)
599 .export_values();
600
601 nb::enum_<phases>(m, "phases", nb::is_arithmetic())
602 .value("iphase_liquid", phases::iphase_liquid)
603 .value("iphase_supercritical", phases::iphase_supercritical)
604 .value("iphase_supercritical_gas", phases::iphase_supercritical_gas)
605 .value("iphase_supercritical_liquid", phases::iphase_supercritical_liquid)
606 .value("iphase_critical_point", phases::iphase_critical_point)
607 .value("iphase_gas", phases::iphase_gas)
608 .value("iphase_twophase", phases::iphase_twophase)
609 .value("iphase_unknown", phases::iphase_unknown)
610 .value("iphase_not_imposed", phases::iphase_not_imposed)
611 .export_values();
612
613 // bd CoolProp-r9sq.23: enums the legacy interface exposed but the nanobind
614 // interface did not bind at all.
615 nb::enum_<fluid_types>(m, "fluid_types", nb::is_arithmetic())
616 .value("FLUID_TYPE_PURE", fluid_types::FLUID_TYPE_PURE)
617 .value("FLUID_TYPE_PSEUDOPURE", fluid_types::FLUID_TYPE_PSEUDOPURE)
618 .value("FLUID_TYPE_REFPROP", fluid_types::FLUID_TYPE_REFPROP)
619 .value("FLUID_TYPE_INCOMPRESSIBLE_LIQUID", fluid_types::FLUID_TYPE_INCOMPRESSIBLE_LIQUID)
620 .value("FLUID_TYPE_INCOMPRESSIBLE_SOLUTION", fluid_types::FLUID_TYPE_INCOMPRESSIBLE_SOLUTION)
621 .value("FLUID_TYPE_UNDEFINED", fluid_types::FLUID_TYPE_UNDEFINED)
622 .export_values();
623
624 nb::enum_<fast_evaluate_status>(m, "fast_evaluate_status", nb::is_arithmetic())
625 .value("fast_evaluate_ok", fast_evaluate_status::fast_evaluate_ok)
626 .value("fast_evaluate_out_of_range", fast_evaluate_status::fast_evaluate_out_of_range)
627 .value("fast_evaluate_two_phase_disallowed", fast_evaluate_status::fast_evaluate_two_phase_disallowed)
628 .value("fast_evaluate_unsupported_input", fast_evaluate_status::fast_evaluate_unsupported_input)
629 .value("fast_evaluate_unsupported_output", fast_evaluate_status::fast_evaluate_unsupported_output)
630 .value("fast_evaluate_internal_error", fast_evaluate_status::fast_evaluate_internal_error)
631 .export_values();
632
633 // bd CoolProp-r9sq.24: register SpinodalData so get_spinodal_data() (below)
634 // can convert its return value instead of raising a cast error.
635 nb::class_<SpinodalData>(m, "SpinodalData")
636 .def(nb::init<>())
637 .def_ro("tau", &SpinodalData::tau)
638 .def_ro("delta", &SpinodalData::delta)
639 .def_ro("M1", &SpinodalData::M1);
640
641 // bd CoolProp-r9sq.18/.24: the bound state structs use the C++ names
642 // (CriticalState, GuessesStructure, PhaseEnvelopeData, SpinodalData); retain
643 // the legacy Cython Py*-prefixed names as backwards-compatibility aliases so
644 // downstream code (e.g. CoolProp.Plots.Common imports + constructs
645 // PyCriticalState / PyGuessesStructure) keeps working against the v8 core.
646 m.attr("PyCriticalState") = m.attr("CriticalState");
647 m.attr("PyGuessesStructure") = m.attr("GuessesStructure");
648 m.attr("PyPhaseEnvelopeData") = m.attr("PhaseEnvelopeData");
649 m.attr("PySpinodalData") = m.attr("SpinodalData");
650
651 // bd CoolProp-r9sq.28: register AbstractState as a real TYPE with a factory
652 // constructor (nb::new_), not a module-level factory FUNCTION returning a
653 // private _AbstractState. So `AbstractState("HEOS","Water")` still builds a
654 // state AND `isinstance(x, AbstractState)` works (it previously raised
655 // "arg 2 must be a type", forcing callers like Plots/Common.py to reach into
656 // the private class). nb::new_ binds __new__ to `factory` + a no-op __init__.
657 nb::class_<AbstractState>(m, "AbstractState")
658 .def(nb::new_(&factory))
659 .def("set_T", &AbstractState::set_T)
660 .def("backend_name", &AbstractState::backend_name)
661 .def("using_mole_fractions", &AbstractState::using_mole_fractions)
662 .def("using_mass_fractions", &AbstractState::using_mass_fractions)
663 .def("using_volu_fractions", &AbstractState::using_volu_fractions)
664 .def("set_mole_fractions", &AbstractState::set_mole_fractions)
665 .def("set_mass_fractions", &AbstractState::set_mass_fractions)
666 .def("set_volu_fractions", &AbstractState::set_volu_fractions)
667 .def("mole_fractions_liquid", &AbstractState::mole_fractions_liquid)
668 .def("mole_fractions_liquid_double", &AbstractState::mole_fractions_liquid_double)
669 .def("mole_fractions_vapor", &AbstractState::mole_fractions_vapor)
670 .def("mole_fractions_vapor_double", &AbstractState::mole_fractions_vapor_double)
671 .def("get_mole_fractions", &AbstractState::get_mole_fractions)
672 .def("get_mass_fractions", &AbstractState::get_mass_fractions)
673 .def("update", &AbstractState::update)
674 .def("update_with_guesses", &AbstractState::update_with_guesses)
675 .def("available_in_high_level", &AbstractState::available_in_high_level)
676 .def("build_options_json", &AbstractState::build_options_json) // bd CoolProp-r9sq.25
677 .def("fluid_param_string", &AbstractState::fluid_param_string)
678 .def("fluid_names", &AbstractState::fluid_names)
679 .def("set_binary_interaction_double", (void(AbstractState::*)(const std::string&, const std::string&, const std::string&, const double))
680 & AbstractState::set_binary_interaction_double)
681 .def("set_binary_interaction_double", (void(AbstractState::*)(const std::size_t, const std::size_t, const std::string&, const double))
682 & AbstractState::set_binary_interaction_double)
683 .def("set_binary_interaction_string", (void(AbstractState::*)(const std::string&, const std::string&, const std::string&, const std::string&))
684 & AbstractState::set_binary_interaction_string)
685 .def("set_binary_interaction_string", (void(AbstractState::*)(const std::size_t, const std::size_t, const std::string&, const std::string&))
686 & AbstractState::set_binary_interaction_string)
687 .def("get_binary_interaction_double",
688 (double(AbstractState::*)(const std::string&, const std::string&, const std::string&)) & AbstractState::get_binary_interaction_double)
689 .def("get_binary_interaction_double",
690 (double(AbstractState::*)(const std::size_t, const std::size_t, const std::string&)) & AbstractState::get_binary_interaction_double)
691 .def("get_binary_interaction_string", &AbstractState::get_binary_interaction_string)
692 .def("apply_simple_mixing_rule", &AbstractState::apply_simple_mixing_rule)
693 .def("set_fluid_parameter_double", &AbstractState::set_fluid_parameter_double)
694 .def("clear", &AbstractState::clear)
695 .def("get_reducing_state", &AbstractState::get_reducing_state)
696 .def("get_state", &AbstractState::get_state)
697 .def("Tmin", &AbstractState::Tmin)
698 .def("Tmax", &AbstractState::Tmax)
699 .def("pmax", &AbstractState::pmax)
700 .def("Ttriple", &AbstractState::Ttriple)
701 .def("phase", &AbstractState::phase)
702 .def("specify_phase", &AbstractState::specify_phase)
703 .def("unspecify_phase", &AbstractState::unspecify_phase)
704 .def("T_critical", &AbstractState::T_critical)
705 .def("p_critical", &AbstractState::p_critical)
706 .def("rhomolar_critical", &AbstractState::rhomolar_critical)
707 .def("rhomass_critical", &AbstractState::rhomass_critical)
708 .def("all_critical_points", &AbstractState::all_critical_points)
709 .def("build_spinodal", &AbstractState::build_spinodal)
710 .def("get_spinodal_data", &AbstractState::get_spinodal_data)
711 .def("criticality_contour_values",
712 [](AbstractState& AS) {
713 double L, M;
715 return nb::make_tuple(L, M);
716 })
717 .def("tangent_plane_distance", &AbstractState::tangent_plane_distance)
718 .def("T_reducing", &AbstractState::T_reducing)
719 .def("rhomolar_reducing", &AbstractState::rhomolar_reducing)
720 .def("rhomass_reducing", &AbstractState::rhomass_reducing)
721 .def("p_triple", &AbstractState::p_triple)
722 .def("name", &AbstractState::name)
723 .def("dipole_moment", &AbstractState::dipole_moment)
724 .def("keyed_output", &AbstractState::keyed_output)
725 .def("trivial_keyed_output", &AbstractState::trivial_keyed_output)
726 // bd CoolProp-r9sq.25: zero-allocation vectorized batch path (tabular / IF97
727 // backends). Caller-allocated, shape-validated contiguous buffers, mirroring
728 // the legacy Cython fast_evaluate; out is (N_inputs, N_outputs), filled in place.
729 .def(
730 "fast_evaluate",
731 [](AbstractState& AS, input_pairs input_pair, nb::ndarray<const double, nb::ndim<1>, nb::c_contig> val1,
732 nb::ndarray<const double, nb::ndim<1>, nb::c_contig> val2, nb::ndarray<const int, nb::ndim<1>, nb::c_contig> outputs,
733 nb::ndarray<double, nb::ndim<2>, nb::c_contig> out, nb::ndarray<int, nb::ndim<1>, nb::c_contig> status, phases imposed_phase) {
734 std::size_t N = val1.shape(0);
735 std::size_t M = outputs.shape(0);
736 if (val2.shape(0) != N) {
737 throw nb::value_error("val1 and val2 must have the same length");
738 }
739 if (out.shape(0) != N || out.shape(1) != M) {
740 throw nb::value_error("out must have shape (N_inputs, N_outputs)");
741 }
742 if (status.shape(0) != N) {
743 throw nb::value_error("status must have length N_inputs");
744 }
745 if (N == 0) {
746 return;
747 }
748 if (M == 0) { // nothing to compute per point; C++ contract: status=ok, no output writes
749 for (std::size_t k = 0; k < N; ++k) {
750 status.data()[k] = 0;
751 }
752 return;
753 }
754 AS.fast_evaluate(input_pair, val1.data(), val2.data(), N, reinterpret_cast<const parameters*>(outputs.data()), M, out.data(), N * M,
755 status.data(), N, imposed_phase);
756 },
757 nb::arg("input_pair"), nb::arg("val1"), nb::arg("val2"), nb::arg("outputs"), nb::arg("out"), nb::arg("status"),
758 nb::arg("imposed_phase") = phases::iphase_not_imposed)
759 .def("saturated_liquid_keyed_output", &AbstractState::saturated_liquid_keyed_output)
760 .def("saturated_vapor_keyed_output", &AbstractState::saturated_vapor_keyed_output)
761 .def("T", &AbstractState::T)
762 .def("rhomolar", &AbstractState::rhomolar)
763 .def("rhomass", &AbstractState::rhomass)
764 .def("p", &AbstractState::p)
765 .def("Q", &AbstractState::Q)
766 .def("Qmass", &AbstractState::Qmass)
767 .def("tau", &AbstractState::tau)
768 .def("delta", &AbstractState::delta)
769 .def("molar_mass", &AbstractState::molar_mass)
770 .def("acentric_factor", &AbstractState::acentric_factor)
771 .def("gas_constant", &AbstractState::gas_constant)
772 .def("Bvirial", &AbstractState::Bvirial)
773 .def("dBvirial_dT", &AbstractState::dBvirial_dT)
774 .def("Cvirial", &AbstractState::Cvirial)
775 .def("dCvirial_dT", &AbstractState::dCvirial_dT)
776 .def("compressibility_factor", &AbstractState::compressibility_factor)
777 .def("hmolar", &AbstractState::hmolar)
778 .def("hmass", &AbstractState::hmass)
779 .def("hmolar_excess", &AbstractState::hmolar_excess)
780 .def("hmass_excess", &AbstractState::hmass_excess)
781 .def("smolar", &AbstractState::smolar)
782 .def("smass", &AbstractState::smass)
783 .def("smolar_excess", &AbstractState::smolar_excess)
784 .def("smass_excess", &AbstractState::smass_excess)
785 .def("umolar", &AbstractState::umolar)
786 .def("umass", &AbstractState::umass)
787 .def("umolar_excess", &AbstractState::umolar_excess)
788 .def("umass_excess", &AbstractState::umass_excess)
789 .def("cpmolar", &AbstractState::cpmolar)
790 .def("cpmass", &AbstractState::cpmass)
791 .def("cp0molar", &AbstractState::cp0molar)
792 .def("cp0mass", &AbstractState::cp0mass)
793 .def("cvmolar", &AbstractState::cvmolar)
794 .def("cvmass", &AbstractState::cvmass)
795 .def("gibbsmolar", &AbstractState::gibbsmolar)
796 .def("gibbsmass", &AbstractState::gibbsmass)
797 .def("gibbsmolar_excess", &AbstractState::gibbsmolar_excess)
798 .def("gibbsmass_excess", &AbstractState::gibbsmass_excess)
799 .def("helmholtzmolar", &AbstractState::helmholtzmolar)
800 .def("helmholtzmass", &AbstractState::helmholtzmass)
801 .def("helmholtzmolar_excess", &AbstractState::helmholtzmolar_excess)
802 .def("helmholtzmass_excess", &AbstractState::helmholtzmass_excess)
803 .def("volumemolar_excess", &AbstractState::volumemolar_excess)
804 .def("volumemass_excess", &AbstractState::volumemass_excess)
805 // q2sh: Cython-parity additions -- idealgas/residual decompositions
806 .def("hmolar_idealgas", &AbstractState::hmolar_idealgas)
807 .def("hmass_idealgas", &AbstractState::hmass_idealgas)
808 .def("hmolar_residual", &AbstractState::hmolar_residual)
809 .def("smolar_idealgas", &AbstractState::smolar_idealgas)
810 .def("smass_idealgas", &AbstractState::smass_idealgas)
811 .def("smolar_residual", &AbstractState::smolar_residual)
812 .def("umolar_idealgas", &AbstractState::umolar_idealgas)
813 .def("umass_idealgas", &AbstractState::umass_idealgas)
814 .def("gibbsmolar_residual", &AbstractState::gibbsmolar_residual)
815 .def("neff", &AbstractState::neff)
816 // q2sh: fluid-constant / cubic-alpha / superancillary accessors
817 .def("get_fluid_constant", &AbstractState::get_fluid_constant)
818 .def("get_fluid_parameter_double", &AbstractState::get_fluid_parameter_double)
819 .def("set_cubic_alpha_C", &AbstractState::set_cubic_alpha_C)
820 .def("update_QT_pure_superanc", &AbstractState::update_QT_pure_superanc)
821 .def("speed_sound", &AbstractState::speed_sound)
822 .def("isothermal_compressibility", &AbstractState::isothermal_compressibility)
823 .def("isobaric_expansion_coefficient", &AbstractState::isobaric_expansion_coefficient)
824 .def("fugacity_coefficient", &AbstractState::fugacity_coefficient)
825 .def("fugacity", &AbstractState::fugacity)
826 .def("chemical_potential", &AbstractState::chemical_potential)
827 .def("fundamental_derivative_of_gas_dynamics", &AbstractState::fundamental_derivative_of_gas_dynamics)
828 .def("PIP", &AbstractState::PIP)
829 // bd CoolProp-r9sq.24: out-reference params -> return a (T, rho) tuple.
830 .def("true_critical_point",
831 [](AbstractState& AS) {
832 double T = 0, rho = 0;
833 AS.true_critical_point(T, rho);
834 return nb::make_tuple(T, rho);
835 })
836 .def("ideal_curve",
837 [](AbstractState& AS, const std::string& name) {
838 std::vector<double> T, p;
839 AS.ideal_curve(name, T, p);
840 return nb::make_tuple(T, p);
841 })
842 .def("first_partial_deriv", &AbstractState::first_partial_deriv)
843 .def("second_partial_deriv", &AbstractState::second_partial_deriv)
844 .def("first_saturation_deriv", &AbstractState::first_saturation_deriv)
845 .def("second_saturation_deriv", &AbstractState::second_saturation_deriv)
846 .def("first_two_phase_deriv", &AbstractState::first_two_phase_deriv)
847 .def("second_two_phase_deriv", &AbstractState::second_two_phase_deriv)
848 .def("first_two_phase_deriv_splined", &AbstractState::first_two_phase_deriv_splined)
849 .def("build_phase_envelope", &AbstractState::build_phase_envelope)
850 .def("get_phase_envelope_data", &AbstractState::get_phase_envelope_data)
851 .def("has_melting_line", &AbstractState::has_melting_line)
852 .def("melting_line", &AbstractState::melting_line)
853 .def("saturation_ancillary", &AbstractState::saturation_ancillary)
854 .def("viscosity", &AbstractState::viscosity)
855 // bd CoolProp-r9sq.24: out-reference params -> return the legacy dict.
856 .def("viscosity_contributions",
857 [](AbstractState& AS) {
858 CoolPropDbl dilute = 0, initial_density = 0, residual = 0, critical = 0;
859 AS.viscosity_contributions(dilute, initial_density, residual, critical);
860 nb::dict d;
861 d["dilute"] = static_cast<double>(dilute);
862 d["initial_density"] = static_cast<double>(initial_density);
863 d["residual"] = static_cast<double>(residual);
864 d["critical"] = static_cast<double>(critical);
865 return d;
866 })
867 .def("conductivity", &AbstractState::conductivity)
868 .def("conductivity_contributions",
869 [](AbstractState& AS) {
870 CoolPropDbl dilute = 0, initial_density = 0, residual = 0, critical = 0;
871 AS.conductivity_contributions(dilute, initial_density, residual, critical);
872 nb::dict d;
873 d["dilute"] = static_cast<double>(dilute);
874 d["initial_density"] = static_cast<double>(initial_density);
875 d["residual"] = static_cast<double>(residual);
876 d["critical"] = static_cast<double>(critical);
877 return d;
878 })
879 .def("surface_tension", &AbstractState::surface_tension)
880 .def("Prandtl", &AbstractState::Prandtl)
881 // bd CoolProp-r9sq.24: T/rho are in/out -> return dict(T, rhomolar) like legacy.
882 .def("conformal_state",
883 [](AbstractState& AS, const std::string& reference_fluid, double T, double rho) {
884 CoolPropDbl T0 = T, rho0 = rho;
885 AS.conformal_state(reference_fluid, T0, rho0);
886 nb::dict d;
887 d["T"] = static_cast<double>(T0);
888 d["rhomolar"] = static_cast<double>(rho0);
889 return d;
890 })
891 .def("change_EOS", &AbstractState::change_EOS)
892 .def("alpha0", &AbstractState::alpha0)
893 .def("dalpha0_dDelta", &AbstractState::dalpha0_dDelta)
894 .def("dalpha0_dTau", &AbstractState::dalpha0_dTau)
895 .def("d2alpha0_dDelta2", &AbstractState::d2alpha0_dDelta2)
896 .def("d2alpha0_dDelta_dTau", &AbstractState::d2alpha0_dDelta_dTau)
897 .def("d2alpha0_dTau2", &AbstractState::d2alpha0_dTau2)
898 .def("d3alpha0_dTau3", &AbstractState::d3alpha0_dTau3)
899 .def("d3alpha0_dDelta_dTau2", &AbstractState::d3alpha0_dDelta_dTau2)
900 .def("d3alpha0_dDelta2_dTau", &AbstractState::d3alpha0_dDelta2_dTau)
901 .def("d3alpha0_dDelta3", &AbstractState::d3alpha0_dDelta3)
902 .def("alphar", &AbstractState::alphar)
903 .def("dalphar_dDelta", &AbstractState::dalphar_dDelta)
904 .def("dalphar_dTau", &AbstractState::dalphar_dTau)
905 .def("d2alphar_dDelta2", &AbstractState::d2alphar_dDelta2)
906 .def("d2alphar_dDelta_dTau", &AbstractState::d2alphar_dDelta_dTau)
907 .def("d2alphar_dTau2", &AbstractState::d2alphar_dTau2)
908 .def("d3alphar_dDelta3", &AbstractState::d3alphar_dDelta3)
909 .def("d3alphar_dDelta2_dTau", &AbstractState::d3alphar_dDelta2_dTau)
910 .def("d3alphar_dDelta_dTau2", &AbstractState::d3alphar_dDelta_dTau2)
911 .def("d3alphar_dTau3", &AbstractState::d3alphar_dTau3)
912 .def("d4alphar_dDelta4", &AbstractState::d4alphar_dDelta4)
913 .def("d4alphar_dDelta3_dTau", &AbstractState::d4alphar_dDelta3_dTau)
914 .def("d4alphar_dDelta2_dTau2", &AbstractState::d4alphar_dDelta2_dTau2)
915 .def("d4alphar_dDelta_dTau3", &AbstractState::d4alphar_dDelta_dTau3)
916 .def("d4alphar_dTau4", &AbstractState::d4alphar_dTau4);
917
918 // NOTE: `AbstractState(backend, fluids)` is the class constructor registered
919 // above via nb::new_(&factory) -- no separate module-level factory function
920 // (bd CoolProp-r9sq.28), so isinstance(x, AbstractState) works.
921
922 m.def("get_config_as_json_string", &get_config_as_json_string);
923 m.def("set_config_as_json_string", &set_config_as_json_string);
924 m.def("config_key_description", (std::string(*)(configuration_keys)) & config_key_description);
925 m.def("config_key_description", (std::string(*)(const std::string&)) & config_key_description);
926 m.def("set_config_string", &set_config_string);
927 m.def("set_config_double", &set_config_double);
928 m.def("set_departure_functions", &set_departure_functions);
929 m.def("set_config_bool", &set_config_bool);
930 m.def("get_config_string", &get_config_string);
931 m.def("get_config_double", &get_config_double);
932 m.def("get_config_bool", &get_config_bool);
933 m.def("get_config_int", &get_config_int);
934 m.def("set_config_int", &set_config_int);
935 m.def("get_parameter_information", &get_parameter_information);
936 m.def("get_parameter_index", &get_parameter_index);
937 m.def("get_phase_index", &get_phase_index);
938 m.def("is_trivial_parameter", &is_trivial_parameter);
939 // generate_update_pair has two out-reference params and returns the input_pair;
940 // wrap it to return (input_pair, out1, out2) like the legacy Cython wrapper
941 // (binding &generate_update_pair<double> directly exposes an unusable signature).
942 m.def("generate_update_pair", [](parameters key1, double value1, parameters key2, double value2) {
943 double out1 = 0.0, out2 = 0.0;
944 input_pairs pair = generate_update_pair<double>(key1, value1, key2, value2, out1, out2);
945 return nb::make_tuple(pair, out1, out2);
946 });
947 m.def("Props1SI", &Props1SI);
948 // Scalar PropsSI (all-float args): raise ValueError on a non-finite result
949 // rather than silently returning inf/nan, matching the legacy Cython wrapper
950 // (bd CoolProp-r9sq.21).
951 m.def("PropsSI", [](const std::string& Output, const std::string& Name1, double Prop1, const std::string& Name2, double Prop2,
952 const std::string& FluidName) {
953 double val = PropsSI(Output, Name1, Prop1, Name2, Prop2, FluidName);
954 _raise_if_invalid(val);
955 return val;
956 });
957 // Vectorized PropsSI: list/ndarray Prop1/Prop2 (with scalar broadcast) dispatch
958 // to the C++ PropsSImulti path. Returns a numpy array for array inputs, but a
959 // plain float when every input was scalar -- so the canonical int-literal call
960 // PropsSI("T","P",101325,"Q",0,"Water") (whose int args do NOT match the all-double
961 // scalar overload above and so bind here) returns a float, not a 1-element ndarray
962 // (bd CoolProp-r9sq.21).
963 m.def("PropsSI",
964 [](const std::string& Output, const std::string& Name1, nb::object Prop1, const std::string& Name2, nb::object Prop2,
965 const std::string& FluidName) -> nb::object {
966 bool s1 = false, s2 = false;
967 std::vector<double> v1 = _to_vec(Prop1, s1);
968 std::vector<double> v2 = _to_vec(Prop2, s2);
969 bool any_seq = s1 || s2;
970 // Empty state inputs -> empty array (mirrors the legacy #2417 short-circuit).
971 if ((s1 && v1.empty()) || (s2 && v2.empty())) {
972 auto* empty = new double[1];
973 nb::capsule owner(empty, [](void* p) noexcept { delete[] static_cast<double*>(p); });
974 return nb::cast(nb::ndarray<nb::numpy, double>(empty, {static_cast<std::size_t>(0)}, owner));
975 }
976 std::size_t n = std::max(v1.size(), v2.size());
977 _broadcast_to(v1, n, "Prop1");
978 _broadcast_to(v2, n, "Prop2");
979 std::string backend, fluid;
980 extract_backend(FluidName, backend, fluid);
981 std::vector<double> fractions{1.0};
982 std::string delimited = extract_fractions(fluid, fractions);
983 std::vector<std::string> fluids = _split_str(delimited, '&');
984 std::vector<std::vector<double>> out = PropsSImulti({Output}, Name1, v1, Name2, v2, backend, fluids, fractions);
985 if (out.empty() || out[0].empty()) {
986 // Legacy raised ValueError (not RuntimeError) on a failed evaluation.
987 PyErr_SetString(PyExc_ValueError, get_global_param_string("errstring").c_str());
988 throw nb::python_error();
989 }
990 // All-scalar inputs -> a scalar float, guarded for finiteness like the
991 // all-float scalar overload above.
992 if (!any_seq) {
993 _raise_if_invalid(out[0][0]);
994 return nb::float_(out[0][0]);
995 }
996 // PropsSImulti returns a matrix indexed [input][output]; a single Output
997 // means one column, so take out[i][0] across the inputs.
998 std::size_t n_out = out.size();
999 auto* data = new double[n_out];
1000 for (std::size_t i = 0; i < n_out; ++i) {
1001 data[i] = out[i][0];
1002 }
1003 nb::capsule owner(data, [](void* p) noexcept { delete[] static_cast<double*>(p); });
1004 return nb::cast(nb::ndarray<nb::numpy, double>(data, {n_out}, owner));
1005 });
1006 // Multi-output vectorized PropsSI: the FIRST argument is a sequence of output
1007 // names (list/tuple/ndarray of str). This is the legacy Cython `iterable(in1)`
1008 // branch, which dispatches to PropsSImulti and returns np.squeeze(np.array(out))
1009 // of the [input][output] matrix. Registered AFTER the single-string overloads
1010 // so a plain str Output still binds to those (this one only catches a sequence
1011 // first argument). GitHub #3145 / bd CoolProp-9eoj: restore the legacy
1012 // PropsSI(["Dmass","viscosity"], "T", [...], "P", ..., "Water") matrix form.
1013 m.def("PropsSI",
1014 [](nb::object Output, const std::string& Name1, nb::object Prop1, const std::string& Name2, nb::object Prop2,
1015 const std::string& FluidName) -> nb::object {
1016 std::vector<std::string> outputs = _to_str_vec(Output);
1017 bool s1 = false, s2 = false;
1018 std::vector<double> v1 = _to_vec(Prop1, s1);
1019 std::vector<double> v2 = _to_vec(Prop2, s2);
1020 // Empty state inputs -> empty array (mirrors the legacy #2417 short-circuit).
1021 if ((s1 && v1.empty()) || (s2 && v2.empty())) {
1022 auto* empty = new double[1];
1023 nb::capsule owner(empty, [](void* p) noexcept { delete[] static_cast<double*>(p); });
1024 return nb::cast(nb::ndarray<nb::numpy, double>(empty, {static_cast<std::size_t>(0)}, owner));
1025 }
1026 std::size_t n = std::max(v1.size(), v2.size());
1027 _broadcast_to(v1, n, "Prop1");
1028 _broadcast_to(v2, n, "Prop2");
1029 std::string backend, fluid;
1030 extract_backend(FluidName, backend, fluid);
1031 std::vector<double> fractions{1.0};
1032 std::string delimited = extract_fractions(fluid, fractions);
1033 std::vector<std::string> fluids = _split_str(delimited, '&');
1034 std::vector<std::vector<double>> out = PropsSImulti(outputs, Name1, v1, Name2, v2, backend, fluids, fractions);
1035 if (out.empty() || out[0].empty()) {
1036 // Legacy raised ValueError (not RuntimeError) on a failed evaluation.
1037 PyErr_SetString(PyExc_ValueError, get_global_param_string("errstring").c_str());
1038 throw nb::python_error();
1039 }
1040 // out is the [input][output] matrix: nrows inputs, m_out outputs. Take
1041 // the row count from the returned matrix (not the pre-call broadcast n)
1042 // so the buffer and shape can never disagree with what PropsSImulti gave
1043 // back, mirroring the single-output overload above. Lay it out row-major
1044 // and apply the legacy np.squeeze rule -- drop any singleton axis, but
1045 // never collapse below 1-D (ndarray_or_iterable's ndim==0 -> reshape(-1)
1046 // guard). So (n,m)->(n,m); (n,1)->(n,); (1,m)->(m,); (1,1)->(1,).
1047 std::size_t nrows = out.size();
1048 std::size_t m_out = out[0].size();
1049 auto* data = new double[std::max<std::size_t>(1, nrows * m_out)];
1050 nb::capsule owner(data, [](void* p) noexcept { delete[] static_cast<double*>(p); });
1051 for (std::size_t i = 0; i < nrows; ++i) {
1052 for (std::size_t j = 0; j < m_out; ++j) {
1053 data[i * m_out + j] = out[i][j];
1054 }
1055 }
1056 std::vector<std::size_t> shape;
1057 if (nrows > 1) {
1058 shape.push_back(nrows);
1059 }
1060 if (m_out > 1) {
1061 shape.push_back(m_out);
1062 }
1063 if (shape.empty()) {
1064 shape.push_back(1); // (1,1) collapses to a 1-element 1-D array, not a scalar
1065 }
1066 return nb::cast(nb::ndarray<nb::numpy, double>(data, shape.size(), shape.data(), owner));
1067 });
1068 // Legacy compatibility: the Cython PropsSI also accepted the 2-arg trivial
1069 // form PropsSI("Tcrit", "Water") (order-lenient). Restore it as an overload
1070 // dispatching to Props1SI; raise ValueError on a non-finite result (parity).
1071 m.def("PropsSI", [](const std::string& Output, const std::string& FluidName) {
1072 double val = Props1SI(Output, FluidName);
1073 _raise_if_invalid(val);
1074 return val;
1075 });
1076 m.def("PhaseSI", &PhaseSI);
1077 m.def("PropsSImulti", &PropsSImulti);
1078 m.def("get_global_param_string", &get_global_param_string);
1079 m.def("get_debug_level", &get_debug_level);
1080 m.def("set_debug_level", &set_debug_level);
1081 m.def("get_fluid_param_string", &get_fluid_param_string);
1082 // bd CoolProp-r9sq.24: these have C++ out-reference params; bind the legacy
1083 // one-string-in, tuple-out forms (extract_backend("REFPROP::Water") -> (backend,
1084 // fluid); extract_fractions("R32&R125[0.7]") -> (fluids_list, fractions)).
1085 m.def("extract_backend", [](const std::string& in_str) {
1086 std::string backend, fluid;
1087 extract_backend(in_str, backend, fluid);
1088 return nb::make_tuple(backend, fluid);
1089 });
1090 m.def("extract_fractions", [](const std::string& in_str) {
1091 std::vector<double> fractions;
1092 std::string delimited = extract_fractions(in_str, fractions);
1093 return nb::make_tuple(_split_str(delimited, '&'), fractions);
1094 });
1095 m.def("set_reference_stateS", &set_reference_stateS);
1096 m.def("set_reference_stateD", &set_reference_stateD);
1097 m.def("saturation_ancillary", &saturation_ancillary);
1098 m.def("add_fluids_as_JSON", &add_fluids_as_JSON);
1099 // Scalar HAPropsSI (all-float args): raise ValueError on a non-finite result,
1100 // matching the legacy Cython wrapper (bd CoolProp-r9sq.22).
1101 m.def("HAPropsSI",
1102 [](const std::string& Output, const std::string& N1, double V1, const std::string& N2, double V2, const std::string& N3, double V3) {
1103 double val = HumidAir::HAPropsSI(Output, N1, V1, N2, V2, N3, V3);
1104 _raise_if_invalid(val);
1105 return val;
1106 });
1107 // Vectorized HAPropsSI: array inputs loop over the scalar C++ HAPropsSI. Returns
1108 // a float when every input was scalar (so HAPropsSI('H','T',298.15,'P',101325,'R',0.5)
1109 // with an int pressure returns a float, not a 1-element list), a numpy array when any
1110 // input was an ndarray (preserving the array type), else a list -- matching the legacy
1111 // Cython wrapper (bd CoolProp-r9sq.22).
1112 m.def("HAPropsSI",
1113 [](const std::string& Output, const std::string& N1, nb::object V1, const std::string& N2, nb::object V2, const std::string& N3,
1114 nb::object V3) -> nb::object {
1115 bool s1 = false, s2 = false, s3 = false;
1116 std::vector<double> v1 = _to_vec(V1, s1), v2 = _to_vec(V2, s2), v3 = _to_vec(V3, s3);
1117 bool any_seq = s1 || s2 || s3;
1118 bool any_ndarray = (s1 && nb::hasattr(V1, "ndim")) || (s2 && nb::hasattr(V2, "ndim")) || (s3 && nb::hasattr(V3, "ndim"));
1119 std::size_t n = std::max({v1.size(), v2.size(), v3.size()});
1120 _broadcast_to(v1, n, "Input1");
1121 _broadcast_to(v2, n, "Input2");
1122 _broadcast_to(v3, n, "Input3");
1123 std::vector<double> out(n);
1124 for (std::size_t i = 0; i < n; ++i) {
1125 out[i] = HumidAir::HAPropsSI(Output, N1, v1[i], N2, v2[i], N3, v3[i]);
1126 // Legacy HAPropsSI raised ValueError on the FIRST non-finite result for
1127 // vector inputs too (HumidAirProp.pyx), not just the all-scalar case --
1128 // validate every element so a NaN/inf cannot pass silently through an
1129 // array/list result (CoolProp-1tbe.11).
1130 _raise_if_invalid(out[i]);
1131 }
1132 if (!any_seq) {
1133 return nb::float_(out[0]);
1134 }
1135 if (any_ndarray) {
1136 auto* data = new double[n != 0u ? n : 1u];
1137 for (std::size_t i = 0; i < n; ++i) {
1138 data[i] = out[i];
1139 }
1140 nb::capsule owner(data, [](void* p) noexcept { delete[] static_cast<double*>(p); });
1141 return nb::cast(nb::ndarray<nb::numpy, double>(data, {n}, owner));
1142 }
1143 return nb::cast(out); // list input -> list output
1144 });
1145 // HAProps (non-SI humid air) intentionally NOT bound -- removed for v8 (SI-only); use HAPropsSI.
1146 m.def("HAProps_Aux", [](std::string out_string, double T, double p, double psi_w) {
1147 std::array<char, 1000> units{};
1148 double out = HumidAir::HAProps_Aux(out_string.c_str(), T, p, psi_w, units.data());
1149 return nb::make_tuple(out, std::string(units.data()));
1150 });
1151 m.def("cair_sat", &HumidAir::cair_sat);
1152 m.def("get_mixture_binary_pair_data", &get_mixture_binary_pair_data);
1153 m.def("set_mixture_binary_pair_data", &set_mixture_binary_pair_data);
1154 m.def("apply_simple_mixing_rule", &apply_simple_mixing_rule);
1155
1156 // ---- q2sh: remaining Cython-parity module functions ---------------------
1157 // Lower-level C++-backed wrappers
1158 m.def("set_interaction_parameters", &set_interaction_parameters);
1159 m.def("set_predefined_mixtures", &set_predefined_mixtures);
1160 m.def("get_mixture_binary_pair_pcsaft", &get_mixture_binary_pair_pcsaft);
1161 m.def("set_mixture_binary_pair_pcsaft", &set_mixture_binary_pair_pcsaft);
1162 // Higher-level convenience wrappers (mirror the Cython module surface; the
1163 // lower-level get_global_param_string / get_fluid_param_string stay above).
1164 m.def("FluidsList", []() { return _split_str(get_global_param_string("FluidsList"), ','); });
1165 m.def("get_aliases", [](const std::string& Fluid) { return _split_str(get_fluid_param_string(Fluid, "aliases_bar"), '|'); });
1166 m.def("get_REFPROPname", [](const std::string& Fluid) { return get_fluid_param_string(Fluid, "REFPROP_name"); });
1167 m.def("get_BibTeXKey", [](const std::string& Fluid, const std::string& key) { return get_fluid_param_string(Fluid, "BibTeX-" + key); });
1168 m.def("get_errstr", []() { return get_global_param_string("errstring"); });
1169 // set_reference_state: keep the low-level D/S binds above and add the unified
1170 // (FluidName, *args) dispatcher that the Cython module exposes.
1171 m.def("set_reference_state", [](const std::string& FluidName, nb::args args) {
1172 if (args.size() == 1) {
1173 set_reference_stateS(FluidName, nb::cast<std::string>(args[0]));
1174 } else if (args.size() == 4) {
1175 // Coerce via the Python __float__ protocol (nb::float_ -> PyNumber_Float)
1176 // so numpy scalars / 1-element arrays (e.g. a density from PropsSI) are
1177 // accepted, matching the legacy Cython `<double>obj` coercion. nb::cast<double>
1178 // would raise std::bad_cast on a non-float object (bd CoolProp-r9sq.16).
1179 // Take an nb::handle explicitly: MSVC won't function-style-cast an
1180 // args[] accessor straight to nb::float_, but accessor->handle is implicit.
1181 auto _as_double = [](nb::handle h) { return nb::cast<double>(nb::float_(h)); };
1182 set_reference_stateD(FluidName, _as_double(args[0]), _as_double(args[1]), _as_double(args[2]), _as_double(args[3]));
1183 } else {
1184 throw std::invalid_argument("Invalid number of inputs to set_reference_state");
1185 }
1186 });
1187
1188 // Chebyshev rootfinding + SuperAncillary saturation evaluator classes.
1189 init_superancillary(m);
1190}
1191
1192# if defined(COOLPROP_NANOBIND_MODULE)
1193NB_MODULE(CoolProp, m) {
1194 init_CoolProp(m);
1195 // Export the State C-ABI table so the frozen Cython `State` shim can forward
1196 // through it (PDSim cimport compatibility without a Cython AbstractState).
1197 m.attr("_capi") = nb::capsule(&g_state_capi, "CoolProp._capi");
1198}
1199# endif
1200
1201#endif