CoolProp 8.0.0
An open-source fluid property and humid air property database
SVDBuilder.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cmath>
5#include <stdexcept>
6
7// Use Eigen's BDC SVD: divide-and-conquer is more efficient than Jacobi
8// for the modest-sized (NX*NY ~ a few * 10^5) matrices we deal with.
9#include <Eigen/Core>
10#include <Eigen/SVD>
11
13
14namespace CoolProp {
15namespace svd {
16
17namespace {
18
19// Central-difference slopes on a (possibly non-uniform) grid.
20//
21// m_0 = (y_1 - y_0) / (x_1 - x_0)
22// m_{N-1} = (y_{N-1} - y_{N-2}) / (x_{N-1} - x_{N-2})
23// m_i = (y_{i+1} - y_{i-1}) / (x_{i+1} - x_{i-1}) for 1 <= i <= N-2
24std::vector<double> fd_slopes(const std::vector<double>& x, const std::vector<double>& y) {
25 const std::size_t n = x.size();
26 std::vector<double> m(n, 0.0);
27 if (n < 2) {
28 return m;
29 }
30 m[0] = (y[1] - y[0]) / (x[1] - x[0]);
31 m[n - 1] = (y[n - 1] - y[n - 2]) / (x[n - 1] - x[n - 2]);
32 for (std::size_t i = 1; i + 1 < n; ++i) {
33 m[i] = (y[i + 1] - y[i - 1]) / (x[i + 1] - x[i - 1]);
34 }
35 return m;
36}
37
38// Slopes from a natural cubic spline through (x, y). Reuses the
39// general-purpose CubicSplineCurve so we don't keep two copies of the
40// tridiagonal solver around.
41std::vector<double> spline_slopes(const std::vector<double>& x, const std::vector<double>& y) {
43 const std::size_t n = x.size();
44 std::vector<double> m(n, 0.0);
45 for (std::size_t i = 0; i < n; ++i) {
46 m[i] = spline->eval_da(x[i]);
47 }
48 return m;
49}
50
51// Monotone cubic (PCHIP / Fritsch-Carlson) slopes. Each interior slope
52// is constructed so the resulting Hermite cubic is monotone on every
53// segment when the input data is monotone. At smooth maxima/minima the
54// slope is forced to zero to prevent overshoot.
55//
56// References:
57// Fritsch & Carlson (1980), SIAM J. Numer. Anal. 17(2): 238-246.
58std::vector<double> pchip_slopes(const std::vector<double>& x, const std::vector<double>& y) {
59 const std::size_t n = x.size();
60 std::vector<double> m(n, 0.0);
61 if (n < 2) {
62 return m;
63 }
64 if (n == 2) {
65 const double slope = (y[1] - y[0]) / (x[1] - x[0]);
66 m[0] = slope;
67 m[1] = slope;
68 return m;
69 }
70 std::vector<double> h(n - 1, 0.0);
71 std::vector<double> delta(n - 1, 0.0);
72 for (std::size_t i = 0; i + 1 < n; ++i) {
73 h[i] = x[i + 1] - x[i];
74 delta[i] = (y[i + 1] - y[i]) / h[i];
75 }
76 // Interior slopes via Fritsch-Carlson weighted harmonic mean.
77 for (std::size_t i = 1; i + 1 < n; ++i) {
78 if (delta[i - 1] * delta[i] <= 0.0) {
79 m[i] = 0.0;
80 } else {
81 const double w1 = 2.0 * h[i] + h[i - 1];
82 const double w2 = h[i] + 2.0 * h[i - 1];
83 m[i] = (w1 + w2) / (w1 / delta[i - 1] + w2 / delta[i]);
84 }
85 }
86 // Endpoint slopes via three-point Steffen-style formulas, clamped to
87 // preserve monotonicity at the boundary.
88 m[0] = ((2.0 * h[0] + h[1]) * delta[0] - h[0] * delta[1]) / (h[0] + h[1]);
89 m[n - 1] = ((2.0 * h[n - 2] + h[n - 3]) * delta[n - 2] - h[n - 2] * delta[n - 3]) / (h[n - 2] + h[n - 3]);
90 // Clamp endpoint slopes so they don't introduce non-monotonicity.
91 if (m[0] * delta[0] <= 0.0) {
92 m[0] = 0.0;
93 } else if (std::abs(m[0]) > 3.0 * std::abs(delta[0])) {
94 m[0] = 3.0 * delta[0];
95 }
96 if (m[n - 1] * delta[n - 2] <= 0.0) {
97 m[n - 1] = 0.0;
98 } else if (std::abs(m[n - 1]) > 3.0 * std::abs(delta[n - 2])) {
99 m[n - 1] = 3.0 * delta[n - 2];
100 }
101 return m;
102}
103
104std::vector<double> compute_slopes(const std::vector<double>& x, const std::vector<double>& y, SlopeSource src) {
105 switch (src) {
107 return fd_slopes(x, y);
109 return spline_slopes(x, y);
111 return pchip_slopes(x, y);
112 }
113 return fd_slopes(x, y); // unreachable
114}
115
116} // namespace
117
118SVDDecomposition build_svd(const std::vector<double>& x_grid, const std::vector<double>& y_grid, const std::vector<double>& M,
119 const SVDBuildOptions& opts) {
120 const std::size_t NX = x_grid.size();
121 const std::size_t NY = y_grid.size();
122 if (NX < 2 || NY < 2) {
123 throw std::invalid_argument("build_svd: x_grid and y_grid must each have >= 2 entries");
124 }
125 if (M.size() != NX * NY) {
126 throw std::invalid_argument("build_svd: M must be NX * NY entries in row-major order");
127 }
128 for (std::size_t i = 1; i < NX; ++i) {
129 if (!(x_grid[i] > x_grid[i - 1])) {
130 throw std::invalid_argument("build_svd: x_grid must be strictly increasing");
131 }
132 }
133 for (std::size_t j = 1; j < NY; ++j) {
134 if (!(y_grid[j] > y_grid[j - 1])) {
135 throw std::invalid_argument("build_svd: y_grid must be strictly increasing");
136 }
137 }
138 const std::size_t full_rank = std::min(NX, NY);
139 if (opts.rank <= 0 || static_cast<std::size_t>(opts.rank) > full_rank) {
140 throw std::invalid_argument("build_svd: rank must be in (0, min(NX, NY)]");
141 }
142 for (const double v : M) {
143 if (!std::isfinite(v)) {
144 throw std::invalid_argument("build_svd: M contains a non-finite entry");
145 }
146 }
147
148 const auto r = static_cast<std::size_t>(opts.rank);
149
150 // Eigen matrix view over M (row-major).
151 using RowMajorMat = Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>;
152 Eigen::Map<const RowMajorMat> M_map(M.data(), static_cast<Eigen::Index>(NX), static_cast<Eigen::Index>(NY));
153 // Eigen 3.5 / 5.0+ promoted ComputeThinU/V from a runtime constructor
154 // argument to a template parameter on BDCSVD; the older form is
155 // EIGEN_DEPRECATED in 3.5+ and slated for removal in the next major.
156 // Use the template form when the installed Eigen supports it and
157 // fall back to the runtime form on older installs.
158#if defined(EIGEN_WORLD_VERSION) && (EIGEN_WORLD_VERSION > 3 || (EIGEN_WORLD_VERSION == 3 && EIGEN_MAJOR_VERSION >= 5))
159 Eigen::BDCSVD<Eigen::MatrixXd, Eigen::ComputeThinU | Eigen::ComputeThinV> svd(M_map);
160#else
161 Eigen::BDCSVD<Eigen::MatrixXd> svd(M_map, Eigen::ComputeThinU | Eigen::ComputeThinV);
162#endif
163 if (svd.info() != Eigen::Success) {
164 throw std::runtime_error("build_svd: BDCSVD failed to converge");
165 }
166
167 const Eigen::MatrixXd& U_full = svd.matrixU(); // NX x min(NX,NY)
168 const Eigen::MatrixXd& V_full = svd.matrixV(); // NY x min(NX,NY)
169 const Eigen::VectorXd& s_full = svd.singularValues(); // min(NX,NY)
170
172 d.NX = static_cast<std::int32_t>(NX);
173 d.NY = static_cast<std::int32_t>(NY);
174 d.rank = opts.rank;
176 d.slope_source = opts.slope_source;
177 d.x_grid = x_grid;
178 d.y_grid = y_grid;
179 d.S.resize(r);
180 for (std::size_t k = 0; k < r; ++k) {
181 d.S[k] = s_full(static_cast<Eigen::Index>(k));
182 }
183 // U: row-major (NX, r).
184 d.U.assign(NX * r, 0.0);
185 for (std::size_t i = 0; i < NX; ++i) {
186 for (std::size_t k = 0; k < r; ++k) {
187 d.U[i * r + k] = U_full(static_cast<Eigen::Index>(i), static_cast<Eigen::Index>(k));
188 }
189 }
190 // V_S: row-major (NY, r) with sigma_k folded in.
191 d.V_S.assign(NY * r, 0.0);
192 for (std::size_t j = 0; j < NY; ++j) {
193 for (std::size_t k = 0; k < r; ++k) {
194 d.V_S[j * r + k] = V_full(static_cast<Eigen::Index>(j), static_cast<Eigen::Index>(k)) * d.S[k];
195 }
196 }
197
198 // Per-mode 1D slopes for the Hermite kernel.
199 d.dU_dx.assign(NX * r, 0.0);
200 d.dV_S_dy.assign(NY * r, 0.0);
201 std::vector<double> u_col(NX);
202 std::vector<double> v_col(NY);
203 for (std::size_t k = 0; k < r; ++k) {
204 for (std::size_t i = 0; i < NX; ++i) {
205 u_col[i] = d.U[i * r + k];
206 }
207 const std::vector<double> u_slopes = compute_slopes(x_grid, u_col, opts.slope_source);
208 for (std::size_t i = 0; i < NX; ++i) {
209 d.dU_dx[i * r + k] = u_slopes[i];
210 }
211 for (std::size_t j = 0; j < NY; ++j) {
212 v_col[j] = d.V_S[j * r + k];
213 }
214 const std::vector<double> v_slopes = compute_slopes(y_grid, v_col, opts.slope_source);
215 for (std::size_t j = 0; j < NY; ++j) {
216 d.dV_S_dy[j * r + k] = v_slopes[j];
217 }
218 }
219
220 return d;
221}
222
223} // namespace svd
224} // namespace CoolProp