CoolProp 8.0.0
An open-source fluid property and humid air property database
SVDSurface.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cmath>
5#include <limits>
6#include <stdexcept>
7#include <utility>
8
9namespace CoolProp {
10namespace sbtl {
11
12SVDSurface::SVDSurface(std::string fluid_name, ::CoolProp::input_pairs input_pair, std::vector<::CoolProp::parameters> properties)
13 : fluid_name_(std::move(fluid_name)), input_pair_(input_pair), properties_(std::move(properties)) {
14 if (properties_.empty()) {
15 throw std::invalid_argument("SVDSurface: properties list must be non-empty");
16 }
17 // Detect duplicates in the property list — silently accepting them
18 // means later add_region_property_svd calls would land in the wrong
19 // slot.
20 auto sorted = properties_;
21 std::sort(sorted.begin(), sorted.end());
22 if (std::adjacent_find(sorted.begin(), sorted.end()) != sorted.end()) {
23 throw std::invalid_argument("SVDSurface: duplicate property in properties list");
24 }
25}
26
28 if (sealed_) {
29 throw std::logic_error("SVDSurface: add_region called after seal()");
30 }
31 const std::size_t idx = atlas_.add(std::move(region));
32 // Resize per-region storage to match.
33 decomps_.emplace_back();
34 decomps_.back().resize(properties_.size());
35 evaluators_.emplace_back();
36 evaluators_.back().resize(properties_.size());
37 return idx;
38}
39
41 if (sealed_) {
42 throw std::logic_error("SVDSurface: add_region_property_svd called after seal()");
43 }
44 if (region_idx >= decomps_.size()) {
45 throw std::out_of_range("SVDSurface: region_idx out of range");
46 }
47 const std::size_t pidx = property_index(prop);
48 if (decomps_[region_idx][pidx]) {
49 throw std::logic_error("SVDSurface: decomposition for this (region, property) already registered");
50 }
51 decomps_[region_idx][pidx] = std::make_unique<svd::SVDDecomposition>(std::move(decomp));
52}
53
55 if (sealed_) {
56 return;
57 }
58 // Validate every region has every property.
59 for (std::size_t r = 0; r < decomps_.size(); ++r) {
60 for (std::size_t p = 0; p < properties_.size(); ++p) {
61 if (!decomps_[r][p]) {
62 throw std::logic_error("SVDSurface::seal: region " + std::to_string(r) + " is missing a decomposition for property index "
63 + std::to_string(p));
64 }
65 }
66 }
67 // eval_with_region_multi reuses one SVDEvalContext across every
68 // per-property evaluator in a region, which is only valid when
69 // every decomposition in that region shares an identical
70 // (x_grid, y_grid). SVDSurfaceFactory builds them together so
71 // this holds by construction, but a hand-built surface or one
72 // loaded from disk could violate it — validate once at seal()
73 // rather than trusting it forever.
74 for (std::size_t r = 0; r < decomps_.size(); ++r) {
75 const auto& x_ref = decomps_[r][0]->x_grid;
76 const auto& y_ref = decomps_[r][0]->y_grid;
77 for (std::size_t p = 1; p < properties_.size(); ++p) {
78 if (decomps_[r][p]->x_grid != x_ref || decomps_[r][p]->y_grid != y_ref) {
79 throw std::logic_error("SVDSurface::seal: region " + std::to_string(r) + " property index " + std::to_string(p)
80 + " has (x_grid, y_grid) that differ from the region's first property — "
81 "eval_with_region_multi requires identical grids across the region");
82 }
83 }
84 }
85 // Build evaluators against the heap-resident decompositions. The
86 // address of each unique_ptr's pointee is stable for the lifetime
87 // of the unique_ptr (and the surface).
88 for (std::size_t r = 0; r < decomps_.size(); ++r) {
89 for (std::size_t p = 0; p < properties_.size(); ++p) {
90 evaluators_[r][p] = std::make_unique<svd::SVDEvaluator>(*decomps_[r][p]);
91 }
92 }
93 sealed_ = true;
94}
95
96std::size_t SVDSurface::region_count() const noexcept {
97 return atlas_.size();
98}
99
101 return std::find(properties_.begin(), properties_.end(), prop) != properties_.end();
102}
103
104std::size_t SVDSurface::property_index(::CoolProp::parameters prop) const {
105 const auto it = std::find(properties_.begin(), properties_.end(), prop);
106 if (it == properties_.end()) {
107 throw std::invalid_argument("SVDSurface: property is not tabulated on this surface");
108 }
109 return static_cast<std::size_t>(std::distance(properties_.begin(), it));
110}
111
113 if (region_idx >= decomps_.size()) {
114 throw std::out_of_range("SVDSurface::decomposition: region_idx out of range");
115 }
116 const std::size_t pidx = property_index(prop);
117 if (!decomps_[region_idx][pidx]) {
118 throw std::logic_error("SVDSurface::decomposition: decomposition is null (surface not sealed?)");
119 }
120 return *decomps_[region_idx][pidx];
121}
122
123SVDSurface::ResolvedPoint SVDSurface::resolve(double a, double b) const noexcept {
124 const int region_idx = atlas_.find_region(a, b);
125 if (region_idx < 0) {
126 return ResolvedPoint{-1, std::numeric_limits<double>::quiet_NaN(), std::numeric_limits<double>::quiet_NaN()};
127 }
128 const auto [xi, eta] = atlas_.region(static_cast<std::size_t>(region_idx)).to_normalized(a, b);
129 // SVD convention (matches Phase 2a's e2e tool): x-axis = eta
130 // (secondary normalised), y-axis = xi (primary normalised).
131 return ResolvedPoint{region_idx, eta, xi};
132}
133
134double SVDSurface::eval(::CoolProp::parameters prop, double a, double b) const {
135 if (!sealed_) {
136 throw std::logic_error("SVDSurface::eval called before seal()");
137 }
138 const auto resolved = resolve(a, b);
139 if (resolved.region_idx < 0) {
140 return std::numeric_limits<double>::quiet_NaN();
141 }
142 return eval_with_region(prop, resolved.region_idx, resolved.svd_x, resolved.svd_y);
143}
144
145double SVDSurface::eval_with_region(::CoolProp::parameters prop, int region_idx, double svd_x, double svd_y) const {
146 if (!sealed_) {
147 throw std::logic_error("SVDSurface::eval_with_region called before seal()");
148 }
149 if (region_idx < 0 || static_cast<std::size_t>(region_idx) >= evaluators_.size()) {
150 throw std::out_of_range("SVDSurface::eval_with_region: region_idx out of range");
151 }
152 const std::size_t pidx = property_index(prop);
153 return evaluators_[static_cast<std::size_t>(region_idx)][pidx]->eval(svd_x, svd_y);
154}
155
156void SVDSurface::eval_with_region_multi(int region_idx, double svd_x, double svd_y, const ::CoolProp::parameters* props, std::size_t n,
157 double* out) const {
158 if (!sealed_) {
159 throw std::logic_error("SVDSurface::eval_with_region_multi called before seal()");
160 }
161 if (region_idx < 0 || static_cast<std::size_t>(region_idx) >= evaluators_.size()) {
162 throw std::out_of_range("SVDSurface::eval_with_region_multi: region_idx out of range");
163 }
164 if (n == 0) {
165 return;
166 }
167 const auto& region_evals = evaluators_[static_cast<std::size_t>(region_idx)];
168 // All per-property evaluators in this region share the same
169 // (x_grid, y_grid) by construction (SVDSurfaceFactory builds them
170 // together), so a single make_context() call covers all `n`
171 // outputs. Pick the first property's evaluator as the context
172 // source — picking the same one every time is fine, but using
173 // props[0] avoids surprising readers who'd expect the prop they
174 // asked for to drive the context.
175 const std::size_t first_pidx = property_index(props[0]);
176 const svd::SVDEvalContext ctx = region_evals[first_pidx]->make_context(svd_x, svd_y);
177 out[0] = region_evals[first_pidx]->eval_with_context(ctx);
178 for (std::size_t k = 1; k < n; ++k) {
179 const std::size_t pidx = property_index(props[k]);
180 out[k] = region_evals[pidx]->eval_with_context(ctx);
181 }
182}
183
184} // namespace sbtl
185} // namespace CoolProp