49std::pair<std::vector<double>, std::vector<double>> make_grid_axes(std::size_t NT, std::size_t NR) {
50 constexpr double kPadEnd = 0.001;
51 std::vector<double> xn_grid(NT);
52 std::vector<double> xi_grid(NR);
53 for (std::size_t i = 0; i < NT; ++i) {
54 const double t =
static_cast<double>(i) /
static_cast<double>(NT - 1);
55 const double eta_cheb = 0.5 * (1.0 - std::cos(
M_PI * t));
56 xn_grid[i] = kPadEnd + (1.0 - 2.0 * kPadEnd) * eta_cheb;
58 for (std::size_t j = 0; j < NR; ++j) {
59 xi_grid[j] =
static_cast<double>(j) /
static_cast<double>(NR - 1);
61 return {std::move(xn_grid), std::move(xi_grid)};
73std::vector<std::vector<double>> sample_grid(
::CoolProp::AbstractState& heos,
const SurfaceSpec& spec,
const region::Region& region) {
74 const std::size_t NT = spec.NT;
75 const std::size_t NR = spec.NR;
76 const std::size_t n_props = spec.properties.size();
78 const auto [xn_grid, xi_grid] = make_grid_axes(NT, NR);
81 std::vector<std::vector<double>> M(n_props);
83 m.assign(NT * NR, std::nan(
""));
95 const auto ab = region.from_normalized(xi_grid[j], xn_grid[i]);
96 const double a = ab.first;
97 const double b = ab.second;
99 spec.update_state(src, a, b);
100 if (src.Q() > 0.0 && src.Q() < 1.0) {
103 for (std::size_t p = 0; p < n_props; ++p) {
104 const double v = spec.read_property(src, spec.properties[p].key);
105 if (!std::isfinite(v)) {
111 const double stored = (spec.properties[p].transform ==
svd::OutputTransform::EXP) ? (v > 0.0 ? std::log(v) : std::nan(
"")) : v;
112 M[p][i * NR + j] = stored;
128 auto resolve_thread_count = []() -> std::size_t {
130 if (cfg == 1)
return 1;
133 const unsigned hw = std::thread::hardware_concurrency();
134 return hw <= 1 ? 1 : static_cast<std::size_t>(hw);
136 return static_cast<std::size_t
>(cfg);
138 const std::size_t n_threads_cfg = resolve_thread_count();
139 const bool can_parallelise = n_threads_cfg > 1 && spec.source_backend !=
"REFPROP" && !spec.source_backend.empty() && !spec.fluid_name.empty();
141 if (!can_parallelise) {
142 for (std::size_t j = 0; j < NR; ++j) {
143 for (std::size_t i = 0; i < NT; ++i) {
144 sample_cell(i, j, heos);
159 const std::size_t n_threads = std::min(n_threads_cfg, NT);
160 std::atomic<bool> worker_factory_failed{
false};
161 auto worker = [&](std::size_t i_lo, std::size_t i_hi) {
162 std::unique_ptr<::CoolProp::AbstractState> local_src;
166 worker_factory_failed.store(
true, std::memory_order_relaxed);
169 for (std::size_t i = i_lo; i < i_hi; ++i) {
170 for (std::size_t j = 0; j < NR; ++j) {
171 sample_cell(i, j, *local_src);
175 std::vector<std::thread> workers;
176 workers.reserve(n_threads);
177 const std::size_t chunk = (NT + n_threads - 1) / n_threads;
178 for (std::size_t t = 0; t < n_threads; ++t) {
179 const std::size_t i_lo = t * chunk;
180 const std::size_t i_hi = std::min(i_lo + chunk, NT);
181 if (i_lo >= i_hi)
break;
182 workers.emplace_back(worker, i_lo, i_hi);
184 for (
auto& w : workers) {
187 if (worker_factory_failed.load(std::memory_order_relaxed)) {
188 throw std::runtime_error(
"build_surface: failed to construct per-thread source AbstractState (parallel sampling)");
196 for (std::size_t i = 0; i < NT; ++i) {
197 std::size_t first_good = NR, last_good = NR;
198 for (std::size_t j = 0; j < NR; ++j) {
199 if (std::isfinite(m[i * NR + j])) {
200 if (first_good == NR) {
206 if (first_good == NR) {
209 for (std::size_t j = 0; j < first_good; ++j) {
210 m[i * NR + j] = m[i * NR + first_good];
212 for (std::size_t j = last_good + 1; j < NR; ++j) {
213 m[i * NR + j] = m[i * NR + last_good];
215 for (std::size_t j = first_good + 1; j < last_good; ++j) {
216 if (std::isfinite(m[i * NR + j])) {
219 std::size_t k_lo = j;
220 while (k_lo > 0 && !std::isfinite(m[i * NR + k_lo - 1])) {
224 std::size_t k_hi = j;
225 while (k_hi < NR && !std::isfinite(m[i * NR + k_hi])) {
228 const double t = (xi_grid[j] - xi_grid[k_lo]) / (xi_grid[k_hi] - xi_grid[k_lo]);
229 m[i * NR + j] = (1.0 - t) * m[i * NR + k_lo] + t * m[i * NR + k_hi];
235 std::vector<double> finite_vals;
236 finite_vals.reserve(m.size());
238 if (std::isfinite(v)) {
239 finite_vals.push_back(v);
242 if (finite_vals.size() < m.size()) {
243 if (finite_vals.empty()) {
249 throw std::runtime_error(
"build_surface: all-NaN property matrix in region (HEOS sampling produced no finite values)");
251 std::nth_element(finite_vals.begin(), finite_vals.begin() +
static_cast<std::ptrdiff_t
>(finite_vals.size() / 2), finite_vals.end());
252 const double median = finite_vals[finite_vals.size() / 2];
253 for (
double& v : m) {
254 if (!std::isfinite(v)) {
268 throw std::invalid_argument(
"build_surface: SurfaceSpec.regions is empty");
271 throw std::invalid_argument(
"build_surface: SurfaceSpec.properties is empty");
274 throw std::invalid_argument(
"build_surface: SurfaceSpec is missing update_state / read_property callbacks");
279 if (spec.
NT < 2 || spec.
NR < 2) {
280 throw std::invalid_argument(
"build_surface: SurfaceSpec.NT and .NR must each be >= 2");
283 throw std::invalid_argument(
"build_surface: SurfaceSpec.rank must be >= 1");
287 std::vector<::CoolProp::parameters> prop_keys;
290 prop_keys.push_back(p.key);
294 const auto [xn_grid, xi_grid] = make_grid_axes(spec.
NT, spec.
NR);
296 for (std::size_t r = 0; r < spec.
regions.size(); ++r) {
303 const auto M_per_prop = sample_grid(heos, spec, region);
304 const std::size_t region_idx = surface.
add_region(std::move(region));
307 for (std::size_t p = 0; p < spec.
properties.size(); ++p) {
316 std::printf(
" region %zu property %d: SVD built (rank=%d)\n", region_idx,
static_cast<int>(spec.
properties[p].key), spec.
rank);