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