38enum class CurveKind : std::uint8_t
42 PIECEWISE_CHEBYSHEV = 2,
57template <
typename Packer>
58void pack_curve(Packer& pk,
const region::BoundaryCurve& curve) {
61 if (
const auto* c =
dynamic_cast<const region::ConstantCurve*
>(&curve)) {
62 const auto s = c->state();
64 pk.pack(
static_cast<std::uint8_t
>(CurveKind::CONSTANT));
70 if (
const auto* c =
dynamic_cast<const region::CubicSplineCurve*
>(&curve)) {
71 const auto s = c->state();
73 pk.pack(
static_cast<std::uint8_t
>(CurveKind::CUBIC_SPLINE));
81 if (
const auto* c =
dynamic_cast<const region::PiecewiseChebyshevCurve*
>(&curve)) {
82 const auto s = c->state();
84 pk.pack(
static_cast<std::uint8_t
>(CurveKind::PIECEWISE_CHEBYSHEV));
87 pk.pack(
static_cast<std::uint8_t
>(s.scale));
88 pk.pack_array(s.pieces.size());
89 for (
const auto& p : s.pieces) {
93 pk.pack(p.inv_half_span);
96 pk.pack(p.deriv_coeffs);
102 if (
const auto* c =
dynamic_cast<const region::SuperancillaryBoundaryCurve*
>(&curve)) {
103 const auto s = c->state();
109 pk.pack(
static_cast<std::uint8_t
>(CurveKind::SUPERANCILLARY));
112 pk.pack(
static_cast<std::int8_t
>(s.prop_key));
113 pk.pack(
static_cast<std::int16_t
>(s.Q));
114 pk.pack(s.output_scale);
119 if (
const auto* c =
dynamic_cast<const region::SuperancillaryTemperatureBoundaryCurve*
>(&curve)) {
120 const auto s = c->state();
125 pk.pack(
static_cast<std::uint8_t
>(CurveKind::SUPERANCILLARY_T));
128 pk.pack(
static_cast<std::int8_t
>(s.prop_key));
129 pk.pack(
static_cast<std::int16_t
>(s.Q));
130 pk.pack(s.output_scale);
135 throw std::runtime_error(
"SVDSurfaceSerializer: unknown BoundaryCurve subclass");
138template <
typename Packer>
139void pack_decomp(Packer& pk, std::size_t region_idx,
::CoolProp::parameters prop_key,
const svd::SVDDecomposition& d) {
141 pk.pack(
static_cast<std::uint32_t
>(region_idx));
142 pk.pack(
static_cast<std::int32_t
>(prop_key));
146 pk.pack(
static_cast<std::uint8_t
>(d.out_transform));
147 pk.pack(
static_cast<std::uint8_t
>(d.slope_source));
157template <
typename Packer>
158void pack_region(Packer& pk,
const region::Region& r) {
159 const auto& axis = r.primary();
161 pk.pack(
static_cast<std::uint8_t
>(axis.scale));
164 pk.pack(axis.a_lo_t);
165 pk.pack(axis.a_hi_t);
166 pk.pack(axis.inv_span_t);
167 pack_curve(pk, r.b_lo());
168 pack_curve(pk, r.b_hi());
171 pk.pack(
static_cast<std::uint8_t
>(r.secondary_scale()));
174template <
typename Packer>
175void pack_surface(Packer& pk,
const SVDSurface& s) {
177 pk.pack(
static_cast<std::int32_t
>(s.input_pair()));
179 const auto& props = s.properties();
180 pk.pack_array(props.size());
181 for (
const auto& p : props) {
190 pk.pack(
static_cast<std::int32_t
>(p));
193 const std::size_t n_regions = s.region_count();
194 pk.pack_array(n_regions);
195 for (std::size_t r = 0; r < n_regions; ++r) {
196 pack_region(pk, s.atlas().region(r));
200 pk.pack_array(n_regions * props.size());
201 for (std::size_t r = 0; r < n_regions; ++r) {
202 for (
const auto& p : props) {
203 pack_decomp(pk, r, p, s.decomposition(r, p));
220T as(
const msgpack::object& o) {
224void check_array(
const msgpack::object& o, std::size_t expected_min,
const char* what) {
225 if (o.type != msgpack::type::ARRAY) {
226 throw std::runtime_error(std::string(
"SVDSurfaceSerializer: expected array for ") + what);
228 if (o.via.array.size < expected_min) {
229 throw std::runtime_error(std::string(
"SVDSurfaceSerializer: array for ") + what +
" too short");
233std::unique_ptr<region::BoundaryCurve> unpack_curve(
const msgpack::object& o,
234 const std::shared_ptr<region::SuperancillaryBoundaryCurve::SuperAncillary_t>& sa) {
235 check_array(o, 1,
"curve");
236 const auto kind =
static_cast<CurveKind
>(as<std::uint8_t>(o.via.array.ptr[0]));
238 case CurveKind::CONSTANT: {
239 check_array(o, 4,
"constant curve");
240 const auto a_lo = as<double>(o.via.array.ptr[1]);
241 const auto a_hi = as<double>(o.via.array.ptr[2]);
242 const auto b = as<double>(o.via.array.ptr[3]);
243 return std::make_unique<region::ConstantCurve>(a_lo, a_hi, b);
245 case CurveKind::CUBIC_SPLINE: {
246 check_array(o, 6,
"cubic spline curve");
247 region::CubicSplineCurve::State s;
248 s.a = as<std::vector<double>>(o.via.array.ptr[1]);
249 s.b = as<std::vector<double>>(o.via.array.ptr[2]);
250 s.M = as<std::vector<double>>(o.via.array.ptr[3]);
251 s.b_min = as<double>(o.via.array.ptr[4]);
252 s.b_max = as<double>(o.via.array.ptr[5]);
255 case CurveKind::PIECEWISE_CHEBYSHEV: {
256 check_array(o, 7,
"piecewise chebyshev curve");
257 region::PiecewiseChebyshevCurve::State s;
258 s.a_lo = as<double>(o.via.array.ptr[1]);
259 s.a_hi = as<double>(o.via.array.ptr[2]);
261 const auto& pieces_obj = o.via.array.ptr[4];
262 check_array(pieces_obj, 1,
"chebyshev pieces");
263 s.pieces.reserve(pieces_obj.via.array.size);
264 for (std::uint32_t i = 0; i < pieces_obj.via.array.size; ++i) {
265 const auto& po = pieces_obj.via.array.ptr[i];
266 check_array(po, 6,
"chebyshev piece");
267 region::PiecewiseChebyshevCurve::PieceState ps;
268 ps.t_lo = as<double>(po.via.array.ptr[0]);
269 ps.t_hi = as<double>(po.via.array.ptr[1]);
270 ps.inv_half_span = as<double>(po.via.array.ptr[2]);
271 ps.t_mid = as<double>(po.via.array.ptr[3]);
272 ps.coeffs = as<std::vector<double>>(po.via.array.ptr[4]);
273 ps.deriv_coeffs = as<std::vector<double>>(po.via.array.ptr[5]);
274 s.pieces.push_back(std::move(ps));
276 s.b_min = as<double>(o.via.array.ptr[5]);
277 s.b_max = as<double>(o.via.array.ptr[6]);
280 case CurveKind::SUPERANCILLARY: {
281 check_array(o, 8,
"superancillary curve");
283 throw std::runtime_error(
"SVDSurfaceSerializer: stream contains a SuperAncillary-backed curve but no SuperAncillary handle is "
284 "available for the fluid (source backend must be HEOS for re-hydration)");
286 region::SuperancillaryBoundaryCurve::State s{};
287 s.p_min = as<double>(o.via.array.ptr[1]);
288 s.p_max = as<double>(o.via.array.ptr[2]);
289 s.prop_key =
static_cast<char>(as<std::int8_t>(o.via.array.ptr[3]));
290 s.Q =
static_cast<short>(as<std::int16_t>(o.via.array.ptr[4]));
291 s.output_scale = as<double>(o.via.array.ptr[5]);
292 s.b_min = as<double>(o.via.array.ptr[6]);
293 s.b_max = as<double>(o.via.array.ptr[7]);
296 case CurveKind::SUPERANCILLARY_T: {
297 check_array(o, 8,
"superancillary-T curve");
299 throw std::runtime_error(
"SVDSurfaceSerializer: stream contains a SuperAncillary-backed curve but no SuperAncillary handle is "
300 "available for the fluid (source backend must be HEOS for re-hydration)");
302 region::SuperancillaryTemperatureBoundaryCurve::State s{};
303 s.T_min = as<double>(o.via.array.ptr[1]);
304 s.T_max = as<double>(o.via.array.ptr[2]);
305 s.prop_key =
static_cast<char>(as<std::int8_t>(o.via.array.ptr[3]));
306 s.Q =
static_cast<short>(as<std::int16_t>(o.via.array.ptr[4]));
307 s.output_scale = as<double>(o.via.array.ptr[5]);
308 s.b_min = as<double>(o.via.array.ptr[6]);
309 s.b_max = as<double>(o.via.array.ptr[7]);
313 throw std::runtime_error(
"SVDSurfaceSerializer: unknown curve kind");
316region::Region unpack_region(
const msgpack::object& o,
const std::shared_ptr<region::SuperancillaryBoundaryCurve::SuperAncillary_t>& sa) {
320 check_array(o, 9,
"region");
321 const auto scale =
static_cast<region::AxisScale>(as<std::uint8_t>(o.via.array.ptr[0]));
322 const auto a_lo = as<double>(o.via.array.ptr[1]);
323 const auto a_hi = as<double>(o.via.array.ptr[2]);
328 auto b_lo = unpack_curve(o.via.array.ptr[6], sa);
329 auto b_hi = unpack_curve(o.via.array.ptr[7], sa);
330 const auto secondary =
static_cast<region::AxisScale>(as<std::uint8_t>(o.via.array.ptr[8]));
331 return {axis, std::move(b_lo), std::move(b_hi), secondary};
334svd::SVDDecomposition unpack_decomp(
const msgpack::object& o, std::size_t* out_region_idx,
::CoolProp::parameters* out_prop) {
335 check_array(o, 14,
"decomp");
336 *out_region_idx =
static_cast<std::size_t
>(as<std::uint32_t>(o.via.array.ptr[0]));
338 svd::SVDDecomposition d;
339 d.NX = as<std::int32_t>(o.via.array.ptr[2]);
340 d.NY = as<std::int32_t>(o.via.array.ptr[3]);
341 d.rank = as<std::int32_t>(o.via.array.ptr[4]);
343 d.slope_source =
static_cast<svd::SlopeSource>(as<std::uint8_t>(o.via.array.ptr[6]));
344 d.x_grid = as<std::vector<double>>(o.via.array.ptr[7]);
345 d.y_grid = as<std::vector<double>>(o.via.array.ptr[8]);
346 d.U = as<std::vector<double>>(o.via.array.ptr[9]);
347 d.dU_dx = as<std::vector<double>>(o.via.array.ptr[10]);
348 d.V_S = as<std::vector<double>>(o.via.array.ptr[11]);
349 d.dV_S_dy = as<std::vector<double>>(o.via.array.ptr[12]);
350 d.S = as<std::vector<double>>(o.via.array.ptr[13]);
361std::shared_ptr<region::SuperancillaryBoundaryCurve::SuperAncillary_t> acquire_superanc_for(
const std::string& fluid_name)
noexcept {
365 if (heos ==
nullptr)
return nullptr;
372 heos->ensure_caloric_superancillaries();
382SVDSurface unpack_surface(
const std::string& fluid_name,
const msgpack::object& o) {
383 check_array(o, 4,
"surface");
386 const auto& props_obj = o.via.array.ptr[1];
387 check_array(props_obj, 0,
"properties");
388 std::vector<::CoolProp::parameters> properties;
389 properties.reserve(props_obj.via.array.size);
390 for (std::uint32_t i = 0; i < props_obj.via.array.size; ++i) {
391 const auto& pe = props_obj.via.array.ptr[i];
392 check_array(pe, 1,
"property entry");
393 properties.push_back(
static_cast<::
CoolProp::parameters>(as<std::int32_t>(pe.via.array.ptr[0])));
396 SVDSurface surface(fluid_name, input_pair, properties);
401 const auto sa = acquire_superanc_for(fluid_name);
403 const auto& regions_obj = o.via.array.ptr[2];
404 check_array(regions_obj, 0,
"regions");
405 for (std::uint32_t i = 0; i < regions_obj.via.array.size; ++i) {
406 surface.add_region(unpack_region(regions_obj.via.array.ptr[i], sa));
409 const auto& decomps_obj = o.via.array.ptr[3];
410 check_array(decomps_obj, 0,
"decomps");
411 for (std::uint32_t i = 0; i < decomps_obj.via.array.size; ++i) {
412 std::size_t r_idx = 0;
414 auto decomp = unpack_decomp(decomps_obj.via.array.ptr[i], &r_idx, &prop);
415 surface.add_region_property_svd(r_idx, prop, std::move(decomp));
424std::vector<char> zlib_compress(
const msgpack::sbuffer& sbuf) {
428 std::vector<char> out(sbuf.size() + (sbuf.size() / 1000) + 128);
429 auto out_size =
static_cast<mz_ulong
>(out.size());
431 const int code = compress(
reinterpret_cast<unsigned char*
>(out.data()), &out_size,
reinterpret_cast<const unsigned char*
>(sbuf.data()),
432 static_cast<mz_ulong
>(sbuf.size()));
435 throw std::runtime_error(std::string(
"SVDSurfaceSerializer: zlib compress failed (code ") + std::to_string(code) +
")");
437 out.resize(out_size);
441std::vector<char> zlib_uncompress(
const std::vector<char>& compressed) {
444 std::vector<char> out(compressed.size() * 5);
445 auto out_size =
static_cast<mz_ulong
>(out.size());
446 auto in_size =
static_cast<mz_ulong
>(compressed.size());
452 uncompress(
reinterpret_cast<unsigned char*
>(out.data()), &out_size,
reinterpret_cast<const unsigned char*
>(compressed.data()), in_size);
454 if (code == Z_BUF_ERROR) {
456 throw std::runtime_error(
"SVDSurfaceSerializer: zlib uncompress would not fit after 8 retries");
458 out.resize(out.size() * 2);
459 out_size =
static_cast<mz_ulong
>(out.size());
460 }
else if (code != Z_OK) {
461 throw std::runtime_error(std::string(
"SVDSurfaceSerializer: zlib uncompress failed (code ") + std::to_string(code) +
")");
463 }
while (code != Z_OK);
464 out.resize(out_size);
474 throw std::logic_error(
"SVDSurfaceSerializer::save: surface must be sealed");
476 msgpack::sbuffer sbuf;
477 msgpack::packer<msgpack::sbuffer> pk(sbuf);
479 pk.pack(std::string(
"SVDS"));
480 pk.pack(
static_cast<std::int32_t
>(
kRevision));
486 pack_surface(pk, surface);
487 return zlib_compress(sbuf);
491 const auto plain = zlib_uncompress(compressed);
493 msgpack::object_handle oh;
494 msgpack::unpack(oh, plain.data(), plain.size());
495 const msgpack::object& root = oh.get();
497 check_array(root, 4,
"root");
498 const auto magic = as<std::string>(root.via.array.ptr[0]);
499 if (magic !=
"SVDS") {
500 throw std::runtime_error(
"SVDSurfaceSerializer::load: bad magic (expected 'SVDS', got '" + magic +
"')");
502 const auto revision = as<std::int32_t>(root.via.array.ptr[1]);
504 throw std::runtime_error(
"SVDSurfaceSerializer::load: revision mismatch (expected " + std::to_string(
kRevision) +
", got "
505 + std::to_string(revision) +
")");
507 const auto fluid_name = as<std::string>(root.via.array.ptr[2]);
509 const auto& surfaces_obj = root.via.array.ptr[3];
510 check_array(surfaces_obj, 1,
"surfaces");
511 if (surfaces_obj.via.array.size != 1) {
512 throw std::runtime_error(
"SVDSurfaceSerializer::load: expected exactly 1 surface (got " + std::to_string(surfaces_obj.via.array.size) +
")");
514 return unpack_surface(fluid_name, surfaces_obj.via.array.ptr[0]);
520 const auto compressed =
save(surface);
525 std::ifstream in(path, std::ios::binary | std::ios::ate);
527 throw std::runtime_error(
"SVDSurfaceSerializer::load_from_file: cannot open " + path);
529 const auto size = in.tellg();
530 in.seekg(0, std::ios::beg);
531 std::vector<char> buf(
static_cast<std::size_t
>(size));
532 if (!in.read(buf.data(), size)) {
533 throw std::runtime_error(
"SVDSurfaceSerializer::load_from_file: read failed for " + path);
546 std::string dir = alt.empty() ? (
::get_home_dir() +
"/.CoolProp/SVDTables") : alt;
548 std::filesystem::create_directories(dir, ec);
555 std::fprintf(stderr,
"SVDSurfaceSerializer: could not create cache dir %s: %s\n", dir.c_str(), ec.message().c_str());
560 if (!dir.empty() && dir.back() !=
'/' && dir.back() !=
'\\') {
573 auto unsafe = [](
const std::string& s) {
574 return s.empty() || s.find(
'/') != std::string::npos || s.find(
'\\') != std::string::npos || s.find(
"..") != std::string::npos;
576 if (unsafe(fluid_name)) {
577 throw std::invalid_argument(
"SVDSurfaceSerializer::default_cache_path: invalid fluid_name (must be a bare component name)");
579 if (unsafe(source_backend)) {
580 throw std::invalid_argument(
"SVDSurfaceSerializer::default_cache_path: invalid source_backend");
588 for (
char c : opthash) {
589 const bool ok = (c >=
'0' && c <=
'9') || (c >=
'a' && c <=
'z') || c ==
'_';
591 throw std::invalid_argument(
"SVDSurfaceSerializer::default_cache_path: opthash must match [0-9a-z_]+");
594 if (opthash.empty()) {
595 throw std::invalid_argument(
"SVDSurfaceSerializer::default_cache_path: opthash must be non-empty");
604 return default_cache_dir() + fluid_name +
"." + source_backend +
"." + pair_name +
"." + opthash +
".svd.bin.z";