CoolProp 8.0.0
An open-source fluid property and humid air property database
Region.cpp
Go to the documentation of this file.
2
3#include <cmath>
4#include <limits>
5#include <stdexcept>
6
7namespace CoolProp {
8namespace region {
9
10namespace {
11
12Region::BBox compute_bbox(const AxisTransform& primary, const BoundaryCurve& b_lo, const BoundaryCurve& b_hi) noexcept {
13 const auto lo_bounds = b_lo.bounds();
14 const auto hi_bounds = b_hi.bounds();
15 return {primary.a_lo, primary.a_hi, lo_bounds.first, hi_bounds.second};
16}
17
18} // namespace
19
20// Sentinel BBox that aabb_contains will never report a hit on (a_hi
21// strictly less than a_lo). Used for moved-from instances so they
22// fail the AABB filter without anyone having to add an "is_alive"
23// check to the hot path.
24constexpr Region::BBox kEmptyBBox = {1.0, -1.0, 1.0, -1.0};
25
26Region::Region(AxisTransform primary, std::unique_ptr<BoundaryCurve> b_lo, std::unique_ptr<BoundaryCurve> b_hi, AxisScale secondary_scale)
27 : primary_(primary),
28 secondary_scale_(secondary_scale),
29 b_lo_(std::move(b_lo)),
30 b_hi_(std::move(b_hi)),
31 bbox_((b_lo_ && b_hi_) ? compute_bbox(primary_, *b_lo_, *b_hi_) : kEmptyBBox) {
32 if (!b_lo_ || !b_hi_) {
33 throw std::invalid_argument("Region: b_lo and b_hi BoundaryCurves must be non-null");
34 }
35 // The secondary axis only supports the two scales whose forward/
36 // inverse maps are closed-form on dynamic [b_lo(a), b_hi(a)] bounds.
37 // POWER / POWER_LO are anchored to a fixed critical-scaling endpoint
38 // and are primary-axis-only.
39 if (secondary_scale_ != AxisScale::LINEAR && secondary_scale_ != AxisScale::LOG) {
40 throw std::invalid_argument("Region: secondary axis scale must be LINEAR or LOG");
41 }
42}
43
44Region::Region(Region&& other) noexcept
45 : primary_(other.primary_),
46 secondary_scale_(other.secondary_scale_),
47 b_lo_(std::move(other.b_lo_)),
48 b_hi_(std::move(other.b_hi_)),
49 bbox_(other.bbox_) {
50 other.bbox_ = kEmptyBBox;
51}
52
53Region& Region::operator=(Region&& other) noexcept {
54 if (this != &other) {
55 primary_ = other.primary_;
56 secondary_scale_ = other.secondary_scale_;
57 b_lo_ = std::move(other.b_lo_);
58 b_hi_ = std::move(other.b_hi_);
59 bbox_ = other.bbox_;
60 other.bbox_ = kEmptyBBox;
61 }
62 return *this;
63}
64
65bool Region::curve_contains(double a, double b) const noexcept {
66 // curve_contains is sign-only — it just decides whether b is
67 // bracketed by the two curve values at a, not the precise value
68 // of either curve. Route through eval_fast so SA-backed curves
69 // can use their precomputed linear-interp surrogate (~5 ns vs
70 // ~80 ns for the full SA composition). Precision-critical
71 // callers (to_normalized / from_normalized) keep using eval().
72 const double b_lo_val = b_lo_->eval_fast(a);
73 const double b_hi_val = b_hi_->eval_fast(a);
74 return b >= b_lo_val && b <= b_hi_val;
75}
76
77std::pair<double, double> Region::to_normalized(double a, double b) const noexcept {
78 const double xi = primary_.forward(a);
79 const double b_lo_val = b_lo_->eval(a);
80 const double b_hi_val = b_hi_->eval(a);
81 // Apply the secondary scale to the value and both bounds, then
82 // normalise linearly in the transformed coordinate. LINEAR is the
83 // identity (g = id); LOG uses g = std::log (b_lo_val > 0 holds for
84 // every density / pressure secondary axis that opts into LOG — a
85 // non-positive argument yields NaN, treated as an out-of-region miss).
86 const double gb = (secondary_scale_ == AxisScale::LOG) ? std::log(b) : b;
87 const double g_lo = (secondary_scale_ == AxisScale::LOG) ? std::log(b_lo_val) : b_lo_val;
88 const double g_hi = (secondary_scale_ == AxisScale::LOG) ? std::log(b_hi_val) : b_hi_val;
89 const double span = g_hi - g_lo;
90 // Guard against a degenerate boundary span: if the two curves
91 // converge at this a (e.g. a critical-point pinch), eta is
92 // undefined. Return NaN so callers see an obvious failure rather
93 // than a silently-wrong eta = 0 that propagates into an SVD lookup
94 // and produces garbage with no signal.
95 const double tol = std::numeric_limits<double>::epsilon() * (1.0 + std::abs(g_lo) + std::abs(g_hi));
96 const double eta = (std::abs(span) <= tol) ? std::numeric_limits<double>::quiet_NaN() : ((gb - g_lo) / span);
97 return {xi, eta};
98}
99
100std::pair<double, double> Region::from_normalized(double xi, double eta) const noexcept {
101 const double a = primary_.inverse(xi);
102 const double b_lo_val = b_lo_->eval(a);
103 const double b_hi_val = b_hi_->eval(a);
104 // Inverse of to_normalized's secondary map. LINEAR: b = b_lo +
105 // eta·(b_hi − b_lo). LOG: interpolate in log space then exponentiate,
106 // i.e. b = b_lo·(b_hi / b_lo)^eta — the geometric mean weighting that
107 // gives the ideal-gas tail uniform-in-decade sample spacing.
108 double b = 0.0;
109 if (secondary_scale_ == AxisScale::LOG) {
110 const double g_lo = std::log(b_lo_val);
111 const double g_hi = std::log(b_hi_val);
112 b = std::exp(g_lo + eta * (g_hi - g_lo));
113 } else {
114 b = b_lo_val + eta * (b_hi_val - b_lo_val);
115 }
116 return {a, b};
117}
118
119} // namespace region
120} // namespace CoolProp