peclet.voro 1.0.4
Device-native moving-particle Voronoi dynamics
Loading...
Searching...
No Matches
ot_optimizer.hpp
Go to the documentation of this file.
1
30#ifndef PECLET_VORO_OT_OPTIMIZER_HPP
31#define PECLET_VORO_OT_OPTIMIZER_HPP
32
33#include <cmath>
34#include <cstdio>
35#include <Kokkos_Core.hpp>
36#include <string>
37#include <vector>
38
39#include "peclet/core/common/view.hpp"
40#include "peclet/core/solver/csr_bicgstab.hpp" // MomentumOp + MomentumSolver (face-CSR BiCGStab)
41#include "peclet/voro/sdf.hpp"
43
44namespace peclet::voro {
45
46struct OtResult {
47 int iters = 0;
48 double maxVolErr = 0.0;
49 double meanVolErr = 0.0;
50 bool converged = false;
51 long nEmpty = 0;
52};
53
54namespace detail {
55template <class Real>
56std::vector<Real> toHostVec(const Kokkos::View<Real*, peclet::core::MemSpace>& v) {
57 auto m = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), v);
58 return std::vector<Real>(m.data(), m.data() + m.extent(0));
59}
60template <class T>
61std::vector<T> toHostVecT(const Kokkos::View<T*, peclet::core::MemSpace>& v) {
62 auto m = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), v);
63 return std::vector<T>(m.data(), m.data() + m.extent(0));
64}
65} // namespace detail
66
81template <class Real, class Sdf = NoSdf>
82OtResult otVolumeControl(std::vector<Real>& pos, std::vector<Real>& weight,
83 const std::vector<Real>& vsetIn, const Real L[3], int N, int sw,
84 const Sdf& sdf, int maxNewton, Real tol, Real reg = Real(1e-6),
85 Real damp = Real(1), bool verbose = false) {
86 using peclet::core::Index;
87 using peclet::core::MemSpace;
88 using MView = peclet::core::View<double>;
89 const Real Larr[3] = {L[0], L[1], L[2]};
90
91 Kokkos::View<Real*, MemSpace> dpos("ot.pos", 3 * N), dw("ot.w", N);
92 Kokkos::deep_copy(dpos, Kokkos::View<const Real*, Kokkos::HostSpace>(pos.data(), 3 * N));
93 Kokkos::View<long*, MemSpace> gd;
94
95 peclet::core::solver::MomentumSolver solver;
96 solver.setJacobi(2, 0.7); // damped-Jacobi preconditioner (no AMG on the irregular graph yet)
97
98 double sumVset = 0;
99 for (int i = 0; i < N; ++i)
100 sumVset += vsetIn[i];
101
102 // Build the power tessellation at weights `wv`; return its result and the volume error statistics
103 // (targets renormalised so Σ V_set = Σ V — L's constant nullspace requires Σ residual = 0).
104 auto evaluate = [&](const std::vector<Real>& wv, std::vector<double>& vol, double& maxErr,
105 double& meanErr, double& resL2, long& nEmpty, double& scale) {
106 Kokkos::deep_copy(dw, Kokkos::View<const Real*, Kokkos::HostSpace>(wv.data(), N));
107 auto res = buildTessellation<Real, true, Sdf>(dpos, dw, N, Larr, sw, N, gd, sdf,
108 /*withForceGeom=*/true);
109 vol = detail::toHostVec<Real>(res.view.cellVolume);
110 double sumV = 0;
111 nEmpty = 0;
112 for (int i = 0; i < N; ++i) {
113 sumV += vol[i];
114 if (vol[i] <= 0.0)
115 ++nEmpty;
116 }
117 scale = (sumVset > 0) ? sumV / sumVset : 1.0;
118 maxErr = 0;
119 meanErr = 0;
120 double s2 = 0;
121 for (int i = 0; i < N; ++i) {
122 const double e = std::fabs(vol[i] - vsetIn[i] * scale);
123 maxErr = std::max(maxErr, e);
124 meanErr += e;
125 s2 += e * e;
126 }
127 meanErr /= N;
128 resL2 = std::sqrt(s2); // OT gradient norm — the damped-Newton merit (KMT)
129 return res;
130 };
131
132 OtResult R;
133 std::vector<double> vol;
134 double maxErr, meanErr, resL2, scale;
135 long nEmpty;
136 auto res = evaluate(weight, vol, maxErr, meanErr, resL2, nEmpty, scale);
137
138 for (int it = 0; it < maxNewton; ++it) {
139 R.iters = it;
140 R.maxVolErr = maxErr;
141 R.meanVolErr = meanErr;
142 R.nEmpty = nEmpty;
143 if (verbose)
144 std::printf(" [ot] iter %2d maxVolErr=%.3e meanVolErr=%.3e nEmpty=%ld\n", it, maxErr,
145 meanErr, nEmpty);
146 if (maxErr < tol) {
147 R.converged = true;
148 break;
149 }
150
151 // assemble L (graph Laplacian, edge weight A_ij/(2 d_ij)) from the current facet CSR.
152 auto off = detail::toHostVecT<int>(res.view.cellFacetOffset);
153 auto cnt = detail::toHostVecT<int>(res.view.cellFacetCount);
154 auto nbr = detail::toHostVecT<peclet::voro::gid_t>(res.view.facetNeighbor);
155 auto area = detail::toHostVec<Real>(res.view.facetArea);
156 auto conn = detail::toHostVec<Real>(res.view.facetConnVec);
157 const int nFacets = (int)nbr.size();
158 std::vector<double> diag(N, 0.0), coef, r(N);
159 std::vector<Index> start(N + 1, 0), fnbr;
160 coef.reserve(nFacets);
161 fnbr.reserve(nFacets);
162 double sumDiag = 0;
163 for (int i = 0; i < N; ++i) {
164 start[i] = (Index)fnbr.size();
165 const int fend = off[i] + cnt[i];
166 for (int f = off[i]; f < fend && f < nFacets; ++f) {
167 const long j = (long)nbr[f];
168 if (j < 0 || j >= N)
169 continue; // boundary / SDF wall facet: no DOF coupling
170 const double Aij = std::sqrt(area[3 * f] * area[3 * f] + area[3 * f + 1] * area[3 * f + 1] +
171 area[3 * f + 2] * area[3 * f + 2]);
172 const double dij = std::sqrt(conn[3 * f] * conn[3 * f] + conn[3 * f + 1] * conn[3 * f + 1] +
173 conn[3 * f + 2] * conn[3 * f + 2]);
174 if (dij <= 0.0 || Aij <= 0.0)
175 continue;
176 const double w = Aij / (2.0 * dij);
177 diag[i] += w;
178 coef.push_back(-w);
179 fnbr.push_back((Index)j);
180 }
181 sumDiag += diag[i];
182 }
183 start[N] = (Index)fnbr.size();
184
185 if (verbose && it == 0) { // FD-validate dV/dw against the assembled L (one cell/neighbour)
186 const int c = N / 2;
187 long jn = -1;
188 double Acf = 0;
189 for (int f = off[c]; f < off[c] + cnt[c] && f < nFacets; ++f) {
190 const long j = (long)nbr[f];
191 if (j < 0 || j >= N)
192 continue;
193 const double Aij = std::sqrt(area[3 * f] * area[3 * f] + area[3 * f + 1] * area[3 * f + 1] +
194 area[3 * f + 2] * area[3 * f + 2]);
195 const double dij = std::sqrt(conn[3 * f] * conn[3 * f] + conn[3 * f + 1] * conn[3 * f + 1] +
196 conn[3 * f + 2] * conn[3 * f + 2]);
197 jn = j;
198 Acf = -Aij / (2 * dij); // analytic dV_c/dw_j = L_cj
199 break;
200 }
201 if (jn >= 0) {
202 const double dd = 1e-6;
203 std::vector<Real> wp = weight;
204 wp[jn] += (Real)dd;
205 std::vector<double> vp;
206 double a, b, cq, sq;
207 long ne;
208 evaluate(wp, vp, a, b, cq, ne, sq);
209 const double fd = (vp[c] - vol[c]) / dd;
210 std::printf(" FD dV_%d/dw_%ld: analytic=%.4e fd=%.4e (dV_c/dw_c analytic=%.4e)\n", c,
211 jn, Acf, fd, diag[c]);
212 }
213 }
214
215 const double eps = reg * (sumDiag / N); // tiny regularisation (numerical safety)
216 for (int i = 0; i < N; ++i) {
217 diag[i] += eps;
218 r[i] = vsetIn[i] * scale - vol[i]; // Newton RHS: L δw = (V_set − V)
219 }
220 // Pin node 0 (Dirichlet gauge): the Laplacian is singular (constant-weight nullspace — a global
221 // weight shift is a null move for the power diagram), so fix δw_0 = 0 by making row 0 the
222 // identity. This makes L SPD/nonsingular for the Krylov solve, without changing the diagram.
223 for (Index f = start[0]; f < start[1]; ++f)
224 coef[f] = 0.0;
225 diag[0] = 1.0;
226 r[0] = 0.0;
227
228 peclet::core::solver::MomentumOp op;
229 op.n = (Index)N;
230 op.diag = peclet::core::toDevice(diag, "ot.diag");
231 op.faceStart = peclet::core::toDevice(start, "ot.start");
232 op.faceNbr = peclet::core::toDevice(fnbr, "ot.nbr");
233 op.faceCoef = peclet::core::toDevice(coef, "ot.coef");
234 MView db("ot.rhs", N), ddw("ot.dw", N);
235 Kokkos::deep_copy(db, Kokkos::View<const double*, Kokkos::HostSpace>(r.data(), N));
236 Kokkos::deep_copy(ddw, 0.0);
237 auto sr = solver.solveBiCGStab(op, ddw, MView(db), 2000, 1e-10);
238 auto dwv = detail::toHostVec<double>(ddw);
239 if (verbose) {
240 double dwnorm = 0;
241 for (int i = 0; i < N; ++i)
242 dwnorm = std::max(dwnorm, std::fabs(dwv[i]));
243 std::printf(" solve: %d iters res0=%.2e res=%.2e |dw|inf=%.3e\n", sr.iters, sr.res0,
244 sr.res, dwnorm);
245 }
246
247 // Exact line minimisation of the convex OT energy g(w) along the Newton direction: g is convex,
248 // so g'(α) = (V(w+αδw) − V_set)·δw is increasing with g'(0) < 0. Bisect for the α where g' = 0
249 // (the energy minimum along δw), capping α to keep every cell non-empty. Using g' (not
250 // ‖V−Vset‖) is the fix for the stall — the gradient NORM need not decrease along a valid
251 // energy-descent step, but the directional derivative crossing zero exactly locates the line
252 // minimum.
253 std::vector<Real> wtry(N);
254 std::vector<double> volT;
255 double maxErrT, meanErrT, resL2T, scaleT;
256 long nEmptyT;
258 auto probe = [&](double alpha, double& dd) -> bool { // returns feasible; sets dd = g'(α)
259 for (int i = 0; i < N; ++i)
260 wtry[i] = weight[i] + (Real)(alpha * dwv[i]);
261 resT = evaluate(wtry, volT, maxErrT, meanErrT, resL2T, nEmptyT, scaleT);
262 dd = 0;
263 for (int i = 0; i < N; ++i)
264 dd += (volT[i] - vsetIn[i] * scaleT) * dwv[i];
265 return nEmptyT == 0;
266 };
267 // cap α at the Newton point `damp` and shrink until feasible (no empty cells).
268 double aHi = damp, ddHi = 0;
269 bool feasHi = probe(aHi, ddHi);
270 for (int g = 0; !feasHi && aHi > 1e-6 && g < 40; ++g) {
271 aHi *= 0.5;
272 feasHi = probe(aHi, ddHi);
273 }
274 if (!feasHi)
275 break; // cannot take any feasible step
276 double aAcc = aHi; // still descending at the cap ⇒ take the full feasible step
277 if (ddHi >= 0.0) { // g' changed sign inside [0, aHi] ⇒ bisect for the line minimum g' = 0
278 double aLo = 0.0;
279 for (int b = 0; b < 24; ++b) {
280 const double aMid = 0.5 * (aLo + aHi);
281 double ddMid;
282 const bool feas = probe(aMid, ddMid);
283 if (feas && ddMid < 0.0)
284 aLo = aMid;
285 else
286 aHi = aMid;
287 }
288 aAcc = aLo;
289 }
290 if (aAcc <= 0.0)
291 break; // at the minimum (or stuck)
292 double ddAcc;
293 probe(aAcc, ddAcc); // re-evaluate at the accepted α to capture the state
294 weight = wtry;
295 res = resT;
296 vol = volT;
297 maxErr = maxErrT;
298 meanErr = meanErrT;
299 resL2 = resL2T;
300 nEmpty = nEmptyT;
301 scale = scaleT;
302 if (verbose)
303 std::printf(" accepted alpha=%.4f resL2=%.4e\n", aAcc, resL2);
304 }
305 return R;
306}
307
308} // namespace peclet::voro
309
310#endif // PECLET_VORO_OT_OPTIMIZER_HPP
KOKKOS_INLINE_FUNCTION Dual< Real, K > dnum(Real c)
Definition convex_cell.hpp:53
std::vector< Real > toHostVec(const Kokkos::View< Real *, peclet::core::MemSpace > &v)
Definition ot_optimizer.hpp:56
std::vector< T > toHostVecT(const Kokkos::View< T *, peclet::core::MemSpace > &v)
Definition ot_optimizer.hpp:61
Device-native Voronoi tessellation, geometry and finite-volume physics.
Definition convex_cell.hpp:41
OtResult otVolumeControl(std::vector< Real > &pos, std::vector< Real > &weight, const std::vector< Real > &vsetIn, const Real L[3], int N, int sw, const Sdf &sdf, int maxNewton, Real tol, Real reg=Real(1e-6), Real damp=Real(1), bool verbose=false)
Definition ot_optimizer.hpp:82
Definition ot_optimizer.hpp:46
double meanVolErr
mean |V_i − V_set_i|
Definition ot_optimizer.hpp:49
double maxVolErr
max_i |V_i − V_set_i|
Definition ot_optimizer.hpp:48
long nEmpty
buried/empty cells at the final state (should be 0 for a feasible target)
Definition ot_optimizer.hpp:51
int iters
Definition ot_optimizer.hpp:47
bool converged
Definition ot_optimizer.hpp:50
Definition tessellator.hpp:69