peclet.voro 1.0.4
Device-native moving-particle Voronoi dynamics
Loading...
Searching...
No Matches
mesh_optimizer.hpp
Go to the documentation of this file.
1
20#ifndef PECLET_VORO_MESH_OPTIMIZER_HPP
21#define PECLET_VORO_MESH_OPTIMIZER_HPP
22
23#include <algorithm>
24#include <array>
25#include <cmath>
26#include <cstdio>
27#include <Kokkos_Core.hpp>
28#include <limits>
29#include <numeric>
30#include <type_traits>
31#include <unordered_map>
32#include <vector>
33
34#include "peclet/core/solver/coloring.hpp" // greedyColoring
35#include "peclet/core/solver/graph_amg.hpp" // smoothed-aggregation AMG (O(N) CG preconditioner)
36#include "peclet/voro/energy/interface.hpp" // rung A3: interfacial energy on the published view
37#include "peclet/voro/ot_optimizer.hpp" // OtResult + detail::toHostVec/toHostVecT
38#include "peclet/voro/sdf.hpp"
40
41namespace peclet::voro {
42
43// CG preconditioner for the Gauss-Newton Hessian solve. Jacobi = O(N^4/3) (iterations grow with N);
44// ColoredGS = a stronger single-level smoother; GraphAMG = smoothed-aggregation multigrid, the only
45// one whose iteration count stays mesh-independent ⇒ an O(N) solve at large N (see
46// peclet::core::solver::GraphAMG).
48
64template <class Real, bool Weighted = false, class Sdf = NoSdf>
65OtResult meshVolumeOptimize(std::vector<Real>& pos, std::vector<Real>& weight,
66 const std::vector<Real>& vsetIn, const Real L[3], int N, int sw,
67 const Sdf& sdf, int maxNewton, Real tol,
68 int cgIters = kOptimizerCgIters, Precond prec = Precond::Jacobi,
69 bool verbose = false, Real muBarrier = 0,
70 Real muDecay = (Real)kBarrierDecay, bool freeEnergy = false) {
71 using peclet::core::Index;
72 using MemSpace = peclet::core::MemSpace;
73 const Real Larr[3] = {L[0], L[1], L[2]};
74 constexpr bool kHasSdf = !std::is_same_v<Sdf, peclet::voro::NoSdf>;
75 const int nD = Weighted ? 4 * N : 3 * N; // DOFs: [0,3N) positions 3i+d ; [3N,4N) weights 3N+i
76 auto wdof = [N](int i) { return 3 * N + i; };
77
78 Kokkos::View<long*, MemSpace> gd;
79 const double boxVol = (double)L[0] * L[1] * L[2];
80 double sumVset = 0;
81 for (int i = 0; i < N; ++i)
82 sumVset += vsetIn[i];
83 std::vector<double> vset(N);
84 for (int i = 0; i < N; ++i)
85 vset[i] = vsetIn[i] * (boxVol / sumVset);
86
87 // Per-cell objective pieces {energy, dE/dV, d²E/dV²}. Two modes (chosen by `freeEnergy`):
88 // • default: relative energy (V/V_ref−1)² (barCell = the collapse barrier, added with weight μ).
89 // • free-energy: e = −V_ref·log(V) (freeCell), pressure V_ref/V equalises to V∝V_ref and −log V
90 // resists collapse on its own. Both relax below a small V (matching quadratic) so a slightly
91 // infeasible start (V≤0 from tessellation flicker) stays finite and is driven feasible.
92 const double barEps = 0.1;
93 auto barCell = [&](double V, double vref, double mu) {
94 struct {
95 double e, d1, d2;
96 } r{0.0, 0.0, 0.0};
97 if (mu <= 0.0)
98 return r;
99 const double t = V / vref;
100 if (t > barEps) {
101 r.e = -mu * std::log(t);
102 r.d1 = -mu / V;
103 r.d2 = mu / (V * V);
104 } else {
105 const double dt = t - barEps, ie = 1.0 / barEps;
106 r.e = mu * (-std::log(barEps) - dt * ie + 0.5 * dt * dt * ie * ie);
107 r.d1 = mu * (-ie + dt * ie * ie) / vref;
108 r.d2 = mu * ie * ie / (vref * vref);
109 }
110 return r;
111 };
112 const double feMin = 0.1;
113 auto freeCell = [&](double V, double vref) {
114 struct {
115 double e, d1, d2;
116 } r{0.0, 0.0, 0.0};
117 const double Vmin = feMin * vref, iv = 1.0 / Vmin;
118 if (V > Vmin) {
119 r.e = -vref * std::log(V);
120 r.d1 = -vref / V;
121 r.d2 = vref / (V * V);
122 } else {
123 const double dV = V - Vmin;
124 r.e = -vref * (std::log(Vmin) + dV * iv - 0.5 * dV * dV * iv * iv);
125 r.d1 = -vref * (iv - dV * iv * iv);
126 r.d2 = vref * iv * iv;
127 }
128 return r;
129 };
130
131 struct Geo {
132 std::vector<double> vol, dvr, area, conn;
133 std::vector<int> off, cnt;
134 std::vector<peclet::voro::gid_t> nbr;
135 double E = 0, maxErr = 0, meanErr = 0;
136 long nBad = 0;
137 };
138 auto build = [&](const std::vector<Real>& x, const std::vector<Real>& w, Geo& G) {
139 Kokkos::View<Real*, MemSpace> dpos("mo.pos", 3 * N), dw;
140 Kokkos::deep_copy(dpos, Kokkos::View<const Real*, Kokkos::HostSpace>(x.data(), 3 * N));
141 if constexpr (Weighted) {
142 dw = Kokkos::View<Real*, MemSpace>("mo.w", N);
143 Kokkos::deep_copy(dw, Kokkos::View<const Real*, Kokkos::HostSpace>(w.data(), N));
144 }
145 auto res = buildTessellation<Real, Weighted, Sdf>(dpos, dw, N, Larr, sw, N, gd, sdf, true);
146 G.vol = detail::toHostVec<Real>(res.view.cellVolume);
147 G.dvr = detail::toHostVec<Real>(res.view.facetConnect);
148 G.area = detail::toHostVec<Real>(res.view.facetArea);
149 G.conn = detail::toHostVec<Real>(res.view.facetConnVec);
150 G.off = detail::toHostVecT<int>(res.view.cellFacetOffset);
151 G.cnt = detail::toHostVecT<int>(res.view.cellFacetCount);
152 G.nbr = detail::toHostVecT<peclet::voro::gid_t>(res.view.facetNeighbor);
153 G.E = G.maxErr = G.meanErr = 0;
154 G.nBad = 0;
155 for (int i = 0; i < N; ++i) {
156 const double e = G.vol[i] / vset[i] - 1.0; // relative deviation from target (diagnostic)
157 G.E += freeEnergy ? freeCell(G.vol[i], vset[i]).e : e * e;
158 G.maxErr = std::max(G.maxErr, std::fabs(e));
159 G.meanErr += std::fabs(e);
160 if (G.vol[i] <= 0.0)
161 ++G.nBad;
162 }
163 G.meanErr /= N;
164 };
165
166 // Per-cell sparse gradient ∇V_c over the DOFs (dof index, value). Positions from facetConnect,
167 // weights from A/(2d). `Rc` scales it into the objective gradient / Hessian rank-1 update.
168 auto stencil = [&](const Geo& G, int c, std::vector<std::pair<int, double>>& st) {
169 st.clear();
170 const int nF = (int)G.nbr.size();
171 double sx = 0, sy = 0, sz = 0, sw2 = 0;
172 for (int f = G.off[c]; f < G.off[c] + G.cnt[c] && f < nF; ++f) {
173 const long j = (long)G.nbr[f];
174 if (j < 0 || j >= N)
175 continue;
176 const double dx = G.dvr[3 * f], dy = G.dvr[3 * f + 1], dz = G.dvr[3 * f + 2];
177 st.emplace_back(3 * (int)j, dx);
178 st.emplace_back(3 * (int)j + 1, dy);
179 st.emplace_back(3 * (int)j + 2, dz);
180 sx -= dx;
181 sy -= dy;
182 sz -= dz;
183 if constexpr (Weighted) {
184 const double A =
185 std::sqrt(G.area[3 * f] * G.area[3 * f] + G.area[3 * f + 1] * G.area[3 * f + 1] +
186 G.area[3 * f + 2] * G.area[3 * f + 2]);
187 const double d =
188 std::sqrt(G.conn[3 * f] * G.conn[3 * f] + G.conn[3 * f + 1] * G.conn[3 * f + 1] +
189 G.conn[3 * f + 2] * G.conn[3 * f + 2]);
190 const double wgt = (d > 0) ? A / (2 * d) : 0;
191 st.emplace_back(wdof((int)j), -wgt);
192 sw2 += wgt;
193 }
194 }
195 // SDF WALL-FACET term of ∂V_c/∂x_c (pore-space meshing). dV/dn for a facet is its outward area
196 // vector, which the tessellator already publishes (facetArea) for the wall facets of its own
197 // EXACT clipped cell — no cell rebuild. Sum those into gw = Σ_wall A_k û_k, then chain through
198 // the seed-foot wall model n_wall(x) = −φ(x)·û, û = ∇φ/|∇φ|: dn = −∇φ (dx·∇φ)/|∇φ|² (+ a
199 // curvature term ∝ ∇²φ for a curved wall). This is exactly addSdfWallForce's Jacobian J_wallᵀ
200 // evaluated at the seed. Added into the own-seed force (sx,sy,sz) so the Gauss-Newton outer
201 // product stays consistent. Guarded to the SDF case (NoSdf has no eval / no wall facets).
202 if constexpr (kHasSdf) {
203 // gw = Σ_wall (dV/dn_k) = Σ of the published wall-facet outward area vectors (facetArea).
204 double gw[3] = {0, 0, 0};
205 for (int f = G.off[c]; f < G.off[c] + G.cnt[c] && f < nF; ++f)
206 if (G.nbr[f] == kBoundaryFacet) { // wall facet (SDF cut); box faces are −1, real nbrs ≥ 0
207 gw[0] += G.area[3 * f];
208 gw[1] += G.area[3 * f + 1];
209 gw[2] += G.area[3 * f + 2];
210 }
211 // Rung A3: the shared seed-foot chain (sdf.hpp::sdfWallChain — foot vector n = −φ∇φ/|∇φ|²,
212 // |∇φ|-independent leading term + the φ/|∇φ| curvature term); same arithmetic as before.
213 {
214 double fw[3] = {0.0, 0.0, 0.0};
215 sdfWallChain<Real>(sdf, pos[3 * c], pos[3 * c + 1], pos[3 * c + 2], gw, fw);
216 sx += fw[0];
217 sy += fw[1];
218 sz += fw[2];
219 }
220 }
221 st.emplace_back(3 * c, sx);
222 st.emplace_back(3 * c + 1, sy);
223 st.emplace_back(3 * c + 2, sz);
224 if constexpr (Weighted)
225 st.emplace_back(wdof(c), sw2);
226 };
227
228 Geo G;
229 build(pos, weight, G);
230 // Renormalise the targets to the ACTUAL total cell volume from the first build — the box volume
231 // for a pure/periodic Voronoi partition, but the FLUID volume when an SDF clips the cells against
232 // a solid (seeds meshing a pore space). Targeting the box volume there makes every target
233 // ~1/porosity too large, so the gradient is dominated by an unachievable uniform-growth mode and
234 // the line search rejects every step. For NoSdf the total is the box volume ⇒ this is an exact
235 // no-op (sc == 1).
236 {
237 double totVol = 0.0, sVset = 0.0;
238 for (int i = 0; i < N; ++i) {
239 totVol += G.vol[i];
240 sVset += vset[i];
241 }
242 const double sc = (sVset > 0.0) ? totVol / sVset : 1.0;
243 for (int i = 0; i < N; ++i)
244 vset[i] *= sc;
245 G.E = G.maxErr = G.meanErr = 0;
246 G.nBad = 0;
247 for (int i = 0; i < N; ++i) {
248 const double e = G.vol[i] / vset[i] - 1.0; // relative deviation from target (diagnostic)
249 G.E += freeEnergy ? freeCell(G.vol[i], vset[i]).e : e * e;
250 G.maxErr = std::max(G.maxErr, std::fabs(e));
251 G.meanErr += std::fabs(e);
252 if (G.vol[i] <= 0.0)
253 ++G.nBad;
254 }
255 G.meanErr /= N;
256 }
257 auto pack = [&](const std::vector<Real>& x, const std::vector<Real>& w, std::vector<Real>& q) {
258 q.assign(x.begin(), x.end());
259 if constexpr (Weighted)
260 q.insert(q.end(), w.begin(), w.end());
261 };
262 auto unpack = [&](const std::vector<Real>& q, std::vector<Real>& x, std::vector<Real>& w) {
263 x.assign(q.begin(), q.begin() + 3 * N);
264 if constexpr (Weighted)
265 w.assign(q.begin() + 3 * N, q.end());
266 };
267
268 auto barrierE = [&](const Geo& G, double mu) -> double {
269 if (freeEnergy || mu <= 0.0)
270 return 0.0; // free-energy mode carries its own collapse resistance
271 double b = 0.0;
272 for (int i = 0; i < N; ++i)
273 b += barCell(G.vol[i], vset[i], mu).e;
274 return b;
275 };
276
277 OtResult R;
278 std::vector<double> g(nD), dq(nD);
279 std::vector<std::pair<int, double>> st;
280 const bool useHessian = (prec != Precond::SteepestDescent);
281 for (int it = 0; it < maxNewton; ++it) {
282 // Barrier-continuation weight for this Newton step: μ_it = μ0·decay^it → 0 (free-energy mode
283 // carries its own collapse resistance, so no separate barrier).
284 const double muCur = freeEnergy ? 0.0 : (double)muBarrier * std::pow((double)muDecay, it);
285 // gradient + assembled Gauss-Newton Hessian (rows as maps → CSR).
286 std::fill(g.begin(), g.end(), 0.0);
287 std::vector<std::unordered_map<int, double>> row(nD);
288 for (int c = 0; c < N; ++c) {
289 // Relative energy E = Σ (V_c/V_ref − 1)²: residual r_c = V_c/V_ref − 1, so the objective
290 // gradient picks up ∇V_c with coefficient Rc = 2 r_c/V_ref and the Gauss-Newton rank-1 weight
291 // is Hw = 2/V_ref². (V_ref = vset, renormalised to the total cell volume above; per-cell so a
292 // graded V_ref is a drop-in.) The log-barrier adds ∂(−μ log V_c)/∂V_c = −μ/V_c to Rc and the
293 // GN term +μ/V_c² to Hw (both ∝ ∇V_c, so they ride the same stencil).
294 double Rc, Hw;
295 if (freeEnergy) { // e=−V_ref log V ⇒ dE/dV=−V_ref/V (the pressure), GN weight V_ref/V²
296 const auto fc = freeCell(G.vol[c], vset[c]);
297 Rc = fc.d1;
298 Hw = fc.d2;
299 } else {
300 Rc = 2.0 * (G.vol[c] / vset[c] - 1.0) / vset[c];
301 Hw = 2.0 / (vset[c] * vset[c]);
302 if (muCur > 0.0) {
303 const auto bc = barCell(G.vol[c], vset[c], muCur);
304 Rc += bc.d1; // add the (relaxed) barrier's dE/dV and GN Hessian weight d²E/dV²
305 Hw += bc.d2;
306 }
307 }
308 stencil(G, c, st);
309 for (auto& [a, va] : st) {
310 g[a] += Rc * va;
311 if (useHessian) {
312 auto& ra = row[a];
313 for (auto& [b, vb] : st)
314 ra[b] += Hw * va * vb;
315 }
316 }
317 }
318 double gnorm = 0;
319 for (int i = 0; i < nD; ++i)
320 gnorm = std::max(gnorm, std::fabs(g[i]));
321
322 if (verbose && it == 0) { // FD-validate the objective gradient (one DOF)
323 const int d = 3 * (N / 2);
324 const double h = 1e-7;
325 std::vector<Real> xp = pos, wp = weight;
326 Geo Gp, Gm;
327 xp[d] += (Real)h;
328 build(xp, wp, Gp);
329 xp[d] -= (Real)(2 * h);
330 build(xp, wp, Gm);
331 std::printf(" FD dE/dx_%d: analytic=%.4e fd=%.4e\n", d, g[d],
332 ((Gp.E + barrierE(Gp, muCur)) - (Gm.E + barrierE(Gm, muCur))) / (2 * h));
333 if constexpr (Weighted) { // also validate a weight-DOF gradient
334 const int dw = wdof(N / 3);
335 std::vector<Real> wq = weight;
336 wq[N / 3] += (Real)h;
337 Geo Gwp, Gwm;
338 build(pos, wq, Gwp);
339 wq[N / 3] -= (Real)(2 * h);
340 build(pos, wq, Gwm);
341 std::printf(" FD dE/dw_%d: analytic=%.4e fd=%.4e\n", N / 3, g[dw],
342 (Gwp.E - Gwm.E) / (2 * h));
343 }
344 }
345
346 R.iters = it;
347 R.maxVolErr = G.maxErr;
348 R.meanVolErr = G.meanErr;
349 R.nEmpty = G.nBad;
350 if (verbose)
351 std::printf(" [vmesh] iter %2d E=%.4e maxVolErr=%.3e gnorm=%.3e nBad=%ld mu=%.2e\n", it,
352 G.E, G.maxErr, gnorm, G.nBad, muCur);
353 if (gnorm < tol) {
354 R.converged = true;
355 break;
356 }
357
358 // Search direction. Steepest descent uses dq = −g directly (the "volume-pressure" force
359 // −Σ_c (∂E/∂V_c)∇V_c) — a guaranteed descent direction, no Hessian solve, robust where the
360 // rank-deficient Gauss-Newton Hessian sends Newton into infeasible territory. Otherwise solve
361 // H dq = −g by preconditioned CG.
362 double rz0 = 0, rz = 0; // (reported in the verbose line below; set on the CG path)
363 if (!useHessian) {
364 for (int i = 0; i < nD; ++i)
365 dq[i] = -g[i];
366 } else {
367 // flatten to CSR (Hcol/Hval incl. diagonal; Hdiag separate; off-diagonal CSR for colouring).
368 std::vector<Index> Hstart(nD + 1, 0), Hcol, ostart(nD + 1, 0), onbr;
369 std::vector<double> Hval, Hdiag(nD, 0.0);
370 for (int i = 0; i < nD; ++i) {
371 Hstart[i] = (Index)Hcol.size();
372 ostart[i] = (Index)onbr.size();
373 for (auto& [j, v] : row[i]) {
374 Hcol.push_back(j);
375 Hval.push_back(v);
376 if (j == i)
377 Hdiag[i] = v;
378 else
379 onbr.push_back(j);
380 }
381 }
382 Hstart[nD] = (Index)Hcol.size();
383 ostart[nD] = (Index)onbr.size();
384
385 auto matvec = [&](const std::vector<double>& v, std::vector<double>& out) {
386 for (int i = 0; i < nD; ++i) {
387 double s = 0;
388 for (Index k = Hstart[i]; k < Hstart[i + 1]; ++k)
389 s += Hval[k] * v[Hcol[k]];
390 out[i] = s;
391 }
392 };
393
394 // preconditioner z ≈ H⁻¹ r.
395 peclet::core::solver::Coloring col;
396 std::vector<Index> colIdxHost;
397 if (prec == Precond::ColoredGS) {
398 col = peclet::core::solver::greedyColoring(ostart, onbr, (Index)nD);
399 colIdxHost = detail::toHostVecT<Index>(col.idx);
400 }
401 // Smoothed-aggregation AMG: rebuilt each Newton step (H moves with the geometry) from the
402 // same assembled Hessian CSR. Nodal aggregation over the 3-DOF-per-seed position blocks (the
403 // weighted path segregates the N weight DOFs after the 3N positions, so it falls back to
404 // scalar aggregation, s=1 — correct, just not block-aware; a block-aware weighted variant is
405 // a later refinement). One V-cycle per CG iteration keeps the iteration count flat as N
406 // grows.
407 peclet::core::solver::GraphAMG amg;
408 if (prec == Precond::GraphAMG) {
409 peclet::core::solver::HostCsrOp Aop;
410 Aop.n = nD;
411 Aop.diag = Hdiag;
412 Aop.start.assign((std::size_t)nD + 1, 0);
413 for (int i = 0; i < nD; ++i) {
414 for (Index k = Hstart[i]; k < Hstart[i + 1]; ++k)
415 if (Hcol[k] != i) {
416 Aop.nbr.push_back(Hcol[k]);
417 Aop.coef.push_back(Hval[k]);
418 }
419 Aop.start[(std::size_t)i + 1] = (Index)Aop.nbr.size();
420 }
421 peclet::core::solver::AmgParams ap;
422 ap.ndofPerNode = Weighted ? 1 : 3;
423 amg.build(Aop, ap);
424 }
425 auto precond = [&](const std::vector<double>& r, std::vector<double>& z) {
426 if (prec == Precond::Jacobi) {
427 for (int i = 0; i < nD; ++i)
428 z[i] = std::fabs(Hdiag[i]) > 1e-30 ? r[i] / Hdiag[i] : r[i];
429 return;
430 }
431 if (prec == Precond::GraphAMG) {
432 amg.apply(r, z);
433 return;
434 }
435 // symmetric multicolour Gauss–Seidel (forward + backward sweep), z = 0 start.
436 std::fill(z.begin(), z.end(), 0.0);
437 auto sweep = [&](bool forward) {
438 for (int ci = 0; ci < col.nColors; ++ci) {
439 const int cc = forward ? ci : col.nColors - 1 - ci;
440 for (Index t = col.hStart[cc]; t < col.hStart[cc + 1]; ++t) {
441 const Index i = colIdxHost[t];
442 double s = r[i];
443 for (Index k = Hstart[i]; k < Hstart[i + 1]; ++k)
444 if (Hcol[k] != i)
445 s -= Hval[k] * z[Hcol[k]];
446 if (std::fabs(Hdiag[i]) > 1e-30)
447 z[i] = s / Hdiag[i];
448 }
449 }
450 };
451 sweep(true);
452 sweep(false);
453 };
454
455 // Jacobi-/GS-preconditioned CG for H dq = −g.
456 std::fill(dq.begin(), dq.end(), 0.0);
457 std::vector<double> rr(nD), z(nD), p(nD), Ap(nD);
458 for (int i = 0; i < nD; ++i)
459 rr[i] = -g[i];
460 precond(rr, z);
461 p = z;
462 rz = 0;
463 for (int i = 0; i < nD; ++i)
464 rz += rr[i] * z[i];
465 rz0 = rz;
466 for (int k = 0; k < cgIters && rz > 1e-18 * rz0; ++k) {
467 matvec(p, Ap);
468 double pAp = 0;
469 for (int i = 0; i < nD; ++i)
470 pAp += p[i] * Ap[i];
471 if (pAp <= 0)
472 break;
473 const double a = rz / pAp;
474 for (int i = 0; i < nD; ++i) {
475 dq[i] += a * p[i];
476 rr[i] -= a * Ap[i];
477 }
478 precond(rr, z);
479 double rzn = 0;
480 for (int i = 0; i < nD; ++i)
481 rzn += rr[i] * z[i];
482 const double beta = rzn / rz;
483 for (int i = 0; i < nD; ++i)
484 p[i] = z[i] + beta * p[i];
485 rz = rzn;
486 }
487 } // else (Gauss-Newton CG path)
488
489 // Armijo backtracking on the TOTAL energy E + barrier along dq.
490 double gdq = 0;
491 for (int i = 0; i < nD; ++i)
492 gdq += g[i] * dq[i];
493 const double Ecur = G.E + barrierE(G, muCur);
494 std::vector<Real> q, qtry, xt, wt;
495 pack(pos, weight, q);
496 double alpha = 1.0;
497 bool accepted = false;
498 Geo Gt;
499 for (int bt = 0; bt < 24; ++bt) {
500 qtry.resize(nD);
501 for (int i = 0; i < nD; ++i)
502 qtry[i] = q[i] + (Real)(alpha * dq[i]);
503 unpack(qtry, xt, wt);
504 build(xt, wt, Gt);
505 const double Etry = Gt.E + barrierE(Gt, muCur);
506 // Validity gate. With the barrier on (μ>0) a collapsed trial has Etry=+∞ ⇒ rejected by
507 // isfinite, so feasibility (all V>0) is enforced automatically. With no barrier: NoSdf
508 // requires nBad==0; the SDF-without-barrier case accepts on the energy decrease alone.
509 const bool cellsValid =
510 (muCur > 0.0) ? std::isfinite(Etry) : (kHasSdf ? std::isfinite(Gt.E) : (Gt.nBad == 0));
511 if (cellsValid && Etry <= Ecur + 1e-4 * alpha * gdq) {
512 pos = xt;
513 if constexpr (Weighted)
514 weight = wt;
515 G = Gt;
516 accepted = true;
517 break;
518 }
519 alpha *= 0.5;
520 }
521 if (verbose)
522 std::printf(" prec=%s cg rz %.2e->%.2e alpha=%.4f\n",
523 prec == Precond::Jacobi ? "jac" : "cgs", rz0, rz, alpha);
524 if (!accepted)
525 break;
526 }
527 return R;
528}
529
530// ================================================================================================
531// Interfacial-energy optimiser (Surface-Evolver-style) — move seed positions to minimise the total
532// area of the faces between cells of DIFFERENT type: E = Σ_{f: type_i≠type_j} σ A_f. A two-phase
533// interface-tension minimiser (foam/emulsion coarsening).
534//
535// The face-area gradient ∂A_f/∂x is not published, so each cell is RECONSTRUCTED as a ConvexCell
536// from its facet neighbours and the validated per-triangle area-Jacobian geomVolumeAreaGrad is
537// gathered into ∂A_k/∂n_l, then routed to positions by chainToDofs<Voronoi>. Steepest descent with
538// an Armijo line search on E. Host (the oracle per docs/DEVICE_RESIDENCY_PLAN.md); the device port
539// mirrors the volume path (assemble on device, reuse the same reconstruction kernel per cell).
540// ================================================================================================
541namespace detail {
546template <class Real, class Sdf>
547void interfaceGradReconstruct(const std::vector<Real>& x, const std::vector<int>& type,
548 double sigma, const Real L[3], int N, int sw, const Sdf& sdf,
549 std::vector<double>& g, double& E) {
550 using MemSpace = peclet::core::MemSpace;
552 const Real Larr[3] = {L[0], L[1], L[2]}, Lh[3] = {L[0] / 2, L[1] / 2, L[2] / 2};
553 Kokkos::View<Real*, MemSpace> dw;
554 Kokkos::View<long*, MemSpace> gd;
555 {
556 Kokkos::View<Real*, MemSpace> dpos("if.pos", 3 * N);
557 Kokkos::deep_copy(dpos, Kokkos::View<const Real*, Kokkos::HostSpace>(x.data(), 3 * N));
558 auto res = buildTessellation<Real, false, Sdf>(dpos, dw, N, Larr, sw, N, gd, sdf, true);
559 auto off = detail::toHostVecT<int>(res.view.cellFacetOffset);
560 auto cnt = detail::toHostVecT<int>(res.view.cellFacetCount);
561 auto nbr = detail::toHostVecT<peclet::voro::gid_t>(res.view.facetNeighbor);
562 const int nF = (int)nbr.size();
563 g.assign(3 * N, 0.0);
564 E = 0;
565 for (int c = 0; c < N; ++c) {
566 // gather + sort real neighbours by distance (min-image) → reconstruct the cell.
567 std::vector<std::array<Real, 4>> nb; // dist², rx, ry, rz
568 std::vector<int> ids;
569 for (int f = off[c]; f < off[c] + cnt[c] && f < nF; ++f) {
570 const long j = (long)nbr[f];
571 if (j < 0 || j >= N)
572 continue;
573 Real r[3];
574 for (int d = 0; d < 3; ++d) {
575 Real rr = x[3 * j + d] - x[3 * c + d];
576 rr = rr > Lh[d] ? rr - L[d] : (rr < -Lh[d] ? rr + L[d] : rr);
577 r[d] = rr;
578 }
579 nb.push_back({r[0] * r[0] + r[1] * r[1] + r[2] * r[2], r[0], r[1], r[2]});
580 ids.push_back((int)j);
581 }
582 const int M = (int)ids.size();
583 std::vector<int> ord(M);
584 for (int i = 0; i < M; ++i)
585 ord[i] = i;
586 std::sort(ord.begin(), ord.end(), [&](int a, int b) { return nb[a][0] < nb[b][0]; });
587 std::vector<Real> rx(M), ry(M), rz(M);
588 std::vector<int> id2(M);
589 for (int i = 0; i < M; ++i) {
590 rx[i] = nb[ord[i]][1];
591 ry[i] = nb[ord[i]][2];
592 rz[i] = nb[ord[i]][3];
593 id2[i] = ids[ord[i]];
594 }
595 RCell cell;
596 buildConvexCell(cell, Larr, rx.data(), ry.data(), rz.data(), id2.data(), M);
597 if (cell.empty() || cell.overflow)
598 continue;
599
600 // gather per-face area magnitudes Ag[k] and the area Jacobian dA[k][l][cc] = ∂A_k/∂n_l.
601 const int np = cell.np;
602 std::vector<double> Ag(np, 0.0), dA((size_t)np * np * 3, 0.0);
603 for (int t = 0; t < cell.nt; ++t) {
604 if (!cell.alive[t])
605 continue;
606 int pl[3];
607 double cb[3], gr[3][3][3];
608 cell.geomVolumeAreaGrad(t, pl, cb, gr);
609 for (int ii = 0; ii < 3; ++ii) {
610 Ag[pl[ii]] += cb[ii];
611 for (int jj = 0; jj < 3; ++jj)
612 for (int cc = 0; cc < 3; ++cc)
613 dA[((size_t)pl[ii] * np + pl[jj]) * 3 + cc] += gr[ii][jj][cc];
614 }
615 }
616 // interfacial energy of this cell (½ per face — each interface face is shared by two cells),
617 // and dGeom/dn_l accumulated over the cell's interface faces.
618 std::vector<double> gn(3 * np, 0.0);
619 for (int k = 0; k < np; ++k) {
620 const int j = cell.pnbr[k];
621 if (j < 0 || j >= N || type[j] == type[c])
622 continue; // only different-type faces
623 E += 0.5 * sigma * Ag[k];
624 for (int l = 0; l < np; ++l)
625 for (int cc = 0; cc < 3; ++cc)
626 gn[3 * l + cc] += 0.5 * sigma * dA[((size_t)k * np + l) * 3 + cc];
627 }
628 // route ∂E_c/∂n_l → positions (self + neighbours) and scatter into g.
630 for (int l = 0; l < np; ++l) {
631 gx[l] = gn[3 * l];
632 gy[l] = gn[3 * l + 1];
633 gz[l] = gn[3 * l + 2];
634 }
635 const double seed3[3] = {(double)x[3 * c], (double)x[3 * c + 1], (double)x[3 * c + 2]};
638 chainToDofs<Voronoi>(cell, seed3, (const double*)nullptr, 0.0, (const double*)nullptr,
639 (double)L[0], gx, gy, gz, fSelf, fwSelf, fnx, fny, fnz, fwn);
640 g[3 * c] += fSelf[0];
641 g[3 * c + 1] += fSelf[1];
642 g[3 * c + 2] += fSelf[2];
643 for (int l = 0; l < np; ++l) {
644 const int j = cell.pnbr[l];
645 if (j < 0 || j >= N)
646 continue;
647 g[3 * j] += fnx[l];
648 g[3 * j + 1] += fny[l];
649 g[3 * j + 2] += fnz[l];
650 }
651 }
652 }
653}
654} // namespace detail
655
660 int iters = 0;
661 double energy = 0.0;
662 double energyRatio = 1.0;
663 bool converged = false;
664};
665
666template <class Real, class Sdf = NoSdf>
667InterfaceResult interfaceMinimize(std::vector<Real>& pos, const std::vector<int>& type,
668 double sigma, const Real L[3], int N, int sw, const Sdf& sdf,
669 int maxIter, Real tol, bool verbose = false) {
670 using MemSpace = peclet::core::MemSpace;
671 const Real Larr[3] = {L[0], L[1], L[2]};
672 Kokkos::View<Real*, MemSpace> dw;
673 Kokkos::View<long*, MemSpace> gd;
674 // Rung A3: the energy + gradient come from the PUBLISHED facet-edge area Jacobians
675 // (buildTessellation with withAreaGrad) through energy::interfaceEnergyForce — one device
676 // kernel over the view, no per-cell reconstruction (the old path is detail::
677 // interfaceGradReconstruct, now the test oracle). Two-type tension table: σ off-diagonal.
678 int nTypes = 1;
679 for (int t : type)
680 nTypes = std::max(nTypes, t + 1);
681 Kokkos::View<int*, MemSpace> dtype("if.type", N);
682 Kokkos::deep_copy(dtype, Kokkos::View<const int*, Kokkos::HostSpace>(type.data(), N));
683 Kokkos::View<Real*, MemSpace> dten("if.tension", (size_t)nTypes * nTypes);
684 {
685 auto h = Kokkos::create_mirror_view(dten);
686 for (int a = 0; a < nTypes; ++a)
687 for (int b = 0; b < nTypes; ++b)
688 h(a * nTypes + b) = (a == b) ? Real(0) : (Real)sigma;
689 Kokkos::deep_copy(dten, h);
690 }
691 Kokkos::View<Real*, MemSpace> dpos("if.pos", 3 * N), dforce("if.force", 3 * N);
692 auto energyGrad = [&](const std::vector<Real>& x, std::vector<double>& g, double& E) {
693 Kokkos::deep_copy(dpos, Kokkos::View<const Real*, Kokkos::HostSpace>(x.data(), 3 * N));
694 auto res = buildTessellation<Real, false, Sdf>(dpos, dw, N, Larr, sw, N, gd, sdf, true, -1, {},
695 {}, {}, {}, {}, {}, 0, nullptr, {},
696 /*withAreaGrad=*/true);
697 Kokkos::deep_copy(dforce, Real(0));
698 E = (double)energy::interfaceEnergyForce<Real, Voronoi, Sdf>(res.view, dtype, dten, nTypes,
699 dpos, dw, L[0], dforce, {}, sdf);
700 auto gh = detail::toHostVec<Real>(dforce);
701 g.assign(gh.begin(), gh.end());
702 };
703
704 InterfaceResult R;
705 std::vector<double> g;
706 double E;
707 energyGrad(pos, g, E);
708 const double E0 = E;
709 if (verbose) { // FD-validate the interfacial-area gradient (one DOF)
710 const int d = 3 * (N / 2);
711 const double h = 1e-7;
712 std::vector<Real> xp = pos;
713 std::vector<double> gp, gm;
714 double Ep, Em;
715 xp[d] += (Real)h;
716 energyGrad(xp, gp, Ep);
717 xp[d] -= (Real)(2 * h);
718 energyGrad(xp, gm, Em);
719 std::printf(" FD dE/dx_%d: analytic=%.4e fd=%.4e\n", d, g[d], (Ep - Em) / (2 * h));
720 }
721 for (int it = 0; it < maxIter; ++it) {
722 double gnorm = 0;
723 for (double v : g)
724 gnorm = std::max(gnorm, std::fabs(v));
725 R.iters = it;
726 R.energy = E;
727 if (verbose)
728 std::printf(" [iface] iter %2d E=%.6e gnorm=%.3e\n", it, E, gnorm);
729 if (gnorm < tol) {
730 R.converged = true;
731 break;
732 }
733 // steepest descent with Armijo line search on E (dq = −g, g·dq = −|g|² < 0). Bound the initial
734 // step to a trust region (≈2% of the spacing) so it does not overshoot across the non-smooth
735 // topology-change kinks of the interfacial energy.
736 const double gdq = -std::inner_product(g.begin(), g.end(), g.begin(), 0.0);
737 const double spacing = std::cbrt((double)L[0] * L[1] * L[2] / N);
738 double alpha = std::min(1.0, 0.02 * spacing / std::max(gnorm, 1e-30));
739 bool accepted = false;
740 std::vector<Real> xtry(3 * N);
741 std::vector<double> gt;
742 double Et;
743 for (int bt = 0; bt < 30; ++bt) {
744 for (int i = 0; i < 3 * N; ++i)
745 xtry[i] = pos[i] - (Real)(alpha * g[i]);
746 energyGrad(xtry, gt, Et);
747 if (Et <= E + 1e-4 * alpha * gdq) {
748 pos = xtry;
749 g = gt;
750 E = Et;
751 accepted = true;
752 break;
753 }
754 alpha *= 0.5;
755 }
756 if (!accepted)
757 break;
758 }
759 R.energyRatio = E / std::max(E0, 1e-30);
760 return R;
761}
762
763// ================================================================================================
764// Device-resident volume optimiser (pure Voronoi positions) — every per-Newton computation runs on
765// the Kokkos device: gradient assembly, matrix-free Gauss-Newton matvec, Jacobi-preconditioned CG,
766// line search, and the DOF update. Only convergence scalars (energy, dot products) cross to host.
767// This is the device path per docs/DEVICE_RESIDENCY_PLAN.md; the host meshVolumeOptimize is its
768// oracle. (Weight DOFs, colored-GS-on-device, and MPI halo exchange are the recorded next
769// increments.)
770// ================================================================================================
771template <class Real, class Sdf = NoSdf>
772OtResult meshVolumeOptimizeDevice(std::vector<Real>& posHost, const std::vector<Real>& vsetIn,
773 const Real L[3], int N, int sw, const Sdf& sdf, int maxNewton,
774 Real tol, int cgIters = kOptimizerCgIters, bool verbose = false) {
775 using MemSpace = peclet::core::MemSpace;
776 using Exec = peclet::core::ExecSpace;
777 using DV = Kokkos::View<double*, MemSpace>;
778 const Real Larr[3] = {L[0], L[1], L[2]};
779 const double gamma = 1.0, boxVol = (double)L[0] * L[1] * L[2];
780 const int nD = 3 * N;
781
782 double sumVset = 0;
783 for (int i = 0; i < N; ++i)
784 sumVset += vsetIn[i];
785 DV vset("mo.vset", N);
786 {
787 auto h = Kokkos::create_mirror_view(vset);
788 for (int i = 0; i < N; ++i)
789 h(i) = vsetIn[i] * (boxVol / sumVset);
790 Kokkos::deep_copy(vset, h);
791 }
792 Kokkos::View<Real*, MemSpace> dpos("mo.pos", 3 * N), dw;
793 Kokkos::deep_copy(dpos, Kokkos::View<const Real*, Kokkos::HostSpace>(posHost.data(), 3 * N));
794 Kokkos::View<long*, MemSpace> gd;
795
796 constexpr bool kHasSdfD = !std::is_same_v<Sdf, peclet::voro::NoSdf>;
797 DV g("mo.g", nD), dq("mo.dq", nD), diag("mo.diag", nD);
798 DV rr("mo.r", nD), z("mo.z", nD), pp("mo.p", nD), Ap("mo.Ap", nD), yy("mo.y", N);
799 DV wallG("mo.wallG", nD); // per-cell SDF wall-facet own-seed term (zero without an SDF)
800
801 // Score a built tessellation: E + max/mean vol error + empty count (device reduce).
802 auto score = [&](TessellatorResult<Real>& res, double& E, double& maxErr, double& meanErr,
803 long& nBad) {
804 auto vol = res.view.cellVolume;
805 auto vs = vset;
806 double e = 0, mx = 0, mn = 0;
807 long nb = 0;
808 Kokkos::parallel_reduce(
809 "mo.E", Kokkos::RangePolicy<Exec>(0, N),
810 KOKKOS_LAMBDA(int i, double& le, double& lmx, double& lmn, long& lnb) {
811 const double d = vol(i) - vs(i);
812 le += gamma * d * d;
813 lmx = Kokkos::max(lmx, Kokkos::fabs(d));
814 lmn += Kokkos::fabs(d);
815 if (vol(i) <= 0.0)
816 ++lnb;
817 },
818 e, Kokkos::Max<double>(mx), mn, nb);
819 E = e;
820 maxErr = mx;
821 meanErr = mn / N;
822 nBad = nb;
823 };
824 // Build the tessellation at `x` and score it.
825 auto evaluate = [&](const Kokkos::View<Real*, MemSpace>& x, TessellatorResult<Real>& res,
826 double& E, double& maxErr, double& meanErr, long& nBad) {
827 res = buildTessellation<Real, false, Sdf>(x, dw, N, Larr, sw, N, gd, sdf, true);
828 score(res, E, maxErr, meanErr, nBad);
829 };
830
832 double E, maxErr, meanErr;
833 long nBad;
834 evaluate(dpos, res, E, maxErr, meanErr, nBad);
835 if constexpr (kHasSdfD) {
836 // Renormalize the targets to the ACTUAL tessellated volume (the pore volume under the SDF
837 // clip; == boxVol unclipped) — the host oracle rescales per build; the clip volume is fixed,
838 // so once suffices. Then re-score with the renormalized targets (no rebuild).
839 double totVol = 0;
840 auto vol0 = res.view.cellVolume;
841 Kokkos::parallel_reduce(
842 "mo.totvol", Kokkos::RangePolicy<Exec>(0, N),
843 KOKKOS_LAMBDA(int i, double& l) { l += vol0(i); }, totVol);
844 const double sc = (totVol > 0) ? totVol / boxVol : 1.0;
845 auto vsc = vset;
846 Kokkos::parallel_for(
847 "mo.vscale", Kokkos::RangePolicy<Exec>(0, N), KOKKOS_LAMBDA(int i) { vsc(i) *= sc; });
848 score(res, E, maxErr, meanErr, nBad);
849 }
850
851 OtResult R;
852 for (int it = 0; it < maxNewton; ++it) {
853 auto off = res.view.cellFacetOffset;
854 auto cnt = res.view.cellFacetCount;
855 auto nbr = res.view.facetNeighbor;
856 auto dvr = res.view.facetConnect; // ∂V_c/∂r per facet
857 auto vol = res.view.cellVolume;
858 auto vs = vset;
859
860 // SDF wall-facet own-seed term (device mirror of the host stencil's wall block): sum the
861 // published wall-facet outward area vectors gw = Σ_wall A_k, then chain through the seed-foot
862 // wall model n(x) = −φ ∇φ/|∇φ|²: leading term −(û·gw)û (|∇φ|-independent) + the φ/|∇φ|
863 // curvature term ∝ ∇²φ. Recomputed each Newton iteration from the CURRENT tessellation.
864 if constexpr (kHasSdfD) {
865 auto area = res.view.facetArea;
866 auto wG = wallG;
867 auto X = dpos;
868 auto sdfD = sdf;
869 Kokkos::parallel_for(
870 "mo.wall", Kokkos::RangePolicy<Exec>(0, N), KOKKOS_LAMBDA(int c) {
871 double gw0 = 0, gw1 = 0, gw2 = 0;
872 for (int f = off(c); f < off(c) + cnt(c); ++f)
873 if ((int)nbr(f) == kBoundaryFacet) {
874 gw0 += area(3 * f);
875 gw1 += area(3 * f + 1);
876 gw2 += area(3 * f + 2);
877 }
878 // Rung A3: the shared seed-foot chain (sdf.hpp::sdfWallChain), same arithmetic.
879 const double gwd[3] = {gw0, gw1, gw2};
880 double fw[3] = {0.0, 0.0, 0.0};
881 sdfWallChain<Real>(sdfD, X(3 * c), X(3 * c + 1), X(3 * c + 2), gwd, fw);
882 const double wx = fw[0], wy = fw[1], wz = fw[2];
883 wG(3 * c) = wx;
884 wG(3 * c + 1) = wy;
885 wG(3 * c + 2) = wz;
886 });
887 }
888
889 // gradient g and Gauss-Newton diagonal (device, atomic scatter).
890 Kokkos::deep_copy(g, 0.0);
891 Kokkos::deep_copy(diag, 0.0);
892 auto wGc = wallG;
893 Kokkos::parallel_for(
894 "mo.grad", Kokkos::RangePolicy<Exec>(0, N), KOKKOS_LAMBDA(int c) {
895 const double Rc = 2.0 * gamma * (vol(c) - vs(c));
896 double sx = wGc(3 * c), sy = wGc(3 * c + 1), sz = wGc(3 * c + 2);
897 for (int f = off(c); f < off(c) + cnt(c); ++f) {
898 const int j = (int)nbr(f);
899 if (j < 0 || j >= N)
900 continue;
901 const double dx = dvr(3 * f), dy = dvr(3 * f + 1), dz = dvr(3 * f + 2);
902 Kokkos::atomic_add(&g(3 * j), Rc * dx);
903 Kokkos::atomic_add(&g(3 * j + 1), Rc * dy);
904 Kokkos::atomic_add(&g(3 * j + 2), Rc * dz);
905 Kokkos::atomic_add(&diag(3 * j), 2.0 * gamma * dx * dx);
906 Kokkos::atomic_add(&diag(3 * j + 1), 2.0 * gamma * dy * dy);
907 Kokkos::atomic_add(&diag(3 * j + 2), 2.0 * gamma * dz * dz);
908 sx -= dx;
909 sy -= dy;
910 sz -= dz;
911 }
912 Kokkos::atomic_add(&g(3 * c), Rc * sx);
913 Kokkos::atomic_add(&g(3 * c + 1), Rc * sy);
914 Kokkos::atomic_add(&g(3 * c + 2), Rc * sz);
915 Kokkos::atomic_add(&diag(3 * c), 2.0 * gamma * sx * sx);
916 Kokkos::atomic_add(&diag(3 * c + 1), 2.0 * gamma * sy * sy);
917 Kokkos::atomic_add(&diag(3 * c + 2), 2.0 * gamma * sz * sz);
918 });
919 double gnorm = 0;
920 Kokkos::parallel_reduce(
921 "mo.gnorm", Kokkos::RangePolicy<Exec>(0, nD),
922 KOKKOS_LAMBDA(int i, double& m) { m = Kokkos::max(m, Kokkos::fabs(g(i))); },
923 Kokkos::Max<double>(gnorm));
924
925 R.iters = it;
926 R.maxVolErr = maxErr;
927 R.meanVolErr = meanErr;
928 R.nEmpty = nBad;
929 if (verbose)
930 std::printf(" [dvmesh] iter %2d E=%.4e maxVolErr=%.3e gnorm=%.3e nBad=%ld\n", it, E, maxErr,
931 gnorm, nBad);
932 if (gnorm < tol) {
933 R.converged = true;
934 break;
935 }
936
937 // matrix-free Gauss-Newton apply H v = 2γ Jᵀ(J v) (two device passes: J into yy, Jᵀ scatter).
938 auto Hmul = [&](const DV& v, DV& out) {
939 auto y = yy;
940 auto wGv = wallG;
941 Kokkos::parallel_for(
942 "mo.Jv", Kokkos::RangePolicy<Exec>(0, N), KOKKOS_LAMBDA(int c) {
943 double yc = 0;
944 for (int f = off(c); f < off(c) + cnt(c); ++f) {
945 const int j = (int)nbr(f);
946 if (j < 0 || j >= N)
947 continue;
948 for (int d = 0; d < 3; ++d)
949 yc += dvr(3 * f + d) * (v(3 * j + d) - v(3 * c + d));
950 }
951 for (int d = 0; d < 3; ++d)
952 yc += wGv(3 * c + d) * v(3 * c + d); // wall own-seed term
953 y(c) = yc;
954 });
955 Kokkos::deep_copy(out, 0.0);
956 Kokkos::parallel_for(
957 "mo.JTy", Kokkos::RangePolicy<Exec>(0, N), KOKKOS_LAMBDA(int c) {
958 const double s = 2.0 * gamma * y(c);
959 for (int f = off(c); f < off(c) + cnt(c); ++f) {
960 const int j = (int)nbr(f);
961 if (j < 0 || j >= N)
962 continue;
963 for (int d = 0; d < 3; ++d) {
964 Kokkos::atomic_add(&out(3 * j + d), s * dvr(3 * f + d));
965 Kokkos::atomic_add(&out(3 * c + d), -s * dvr(3 * f + d));
966 }
967 }
968 for (int d = 0; d < 3; ++d) // wall own-seed term
969 Kokkos::atomic_add(&out(3 * c + d), s * wGv(3 * c + d));
970 });
971 };
972 auto dot = [&](const DV& a, const DV& b) {
973 double s = 0;
974 Kokkos::parallel_reduce(
975 "mo.dot", Kokkos::RangePolicy<Exec>(0, nD),
976 KOKKOS_LAMBDA(int i, double& l) { l += a(i) * b(i); }, s);
977 return s;
978 };
979
980 // Jacobi-preconditioned CG for H dq = −g (device).
981 Kokkos::deep_copy(dq, 0.0);
982 {
983 auto G = g, D = diag;
984 Kokkos::parallel_for(
985 "mo.r0", Kokkos::RangePolicy<Exec>(0, nD), KOKKOS_LAMBDA(int i) {
986 rr(i) = -G(i);
987 z(i) = Kokkos::fabs(D(i)) > 1e-30 ? rr(i) / D(i) : rr(i);
988 pp(i) = z(i);
989 });
990 }
991 double rz = dot(rr, z), rz0 = rz;
992 for (int k = 0; k < cgIters && rz > 1e-18 * rz0; ++k) {
993 Hmul(pp, Ap);
994 const double pAp = dot(pp, Ap);
995 if (pAp <= 0)
996 break;
997 const double a = rz / pAp;
998 {
999 auto D = diag;
1000 Kokkos::parallel_for(
1001 "mo.cgupd", Kokkos::RangePolicy<Exec>(0, nD), KOKKOS_LAMBDA(int i) {
1002 dq(i) += a * pp(i);
1003 rr(i) -= a * Ap(i);
1004 z(i) = Kokkos::fabs(D(i)) > 1e-30 ? rr(i) / D(i) : rr(i);
1005 });
1006 }
1007 const double rzn = dot(rr, z);
1008 const double beta = rzn / rz;
1009 Kokkos::parallel_for(
1010 "mo.pupd", Kokkos::RangePolicy<Exec>(0, nD),
1011 KOKKOS_LAMBDA(int i) { pp(i) = z(i) + beta * pp(i); });
1012 rz = rzn;
1013 }
1014 const double gdq = dot(g, dq);
1015
1016 // Armijo line search on E: x_try = x + α dir (device), rebuild + reduce E; backtrack. If the
1017 // Gauss-Newton step is rejected (its quadratic model degrades near dead/clipped cells), retry
1018 // along steepest descent −g before giving up — same globalization the host oracle uses.
1019 Kokkos::View<Real*, MemSpace> xtry("mo.xtry", 3 * N);
1020 auto lineSearch = [&](const DV& dir, double gdir) {
1021 double alpha = 1.0;
1022 for (int bt = 0; bt < 24; ++bt) {
1023 const double al = alpha;
1024 auto X = dpos, DQ = dir, XT = xtry;
1025 Kokkos::parallel_for(
1026 "mo.trial", Kokkos::RangePolicy<Exec>(0, nD),
1027 KOKKOS_LAMBDA(int i) { XT(i) = X(i) + (Real)(al * DQ(i)); });
1029 double Et, mxT, mnT;
1030 long nbT;
1031 evaluate(xtry, resT, Et, mxT, mnT, nbT);
1032 const bool feasible = kHasSdfD ? std::isfinite(Et) : (nbT == 0);
1033 if (feasible && Et <= E + 1e-4 * alpha * gdir) {
1034 Kokkos::deep_copy(dpos, xtry);
1035 res = resT;
1036 E = Et;
1037 maxErr = mxT;
1038 meanErr = mnT;
1039 nBad = nbT;
1040 return true;
1041 }
1042 alpha *= 0.5;
1043 }
1044 return false;
1045 };
1046 bool accepted = lineSearch(dq, gdq);
1047 if (!accepted) {
1048 // steepest-descent fallback: dq := −g (reuse dq as scratch), unit natural scale.
1049 auto G = g, DQ = dq;
1050 Kokkos::parallel_for(
1051 "mo.sd", Kokkos::RangePolicy<Exec>(0, nD), KOKKOS_LAMBDA(int i) { DQ(i) = -G(i); });
1052 accepted = lineSearch(dq, -dot(g, g));
1053 }
1054 if (!accepted)
1055 break;
1056 }
1057 // download the optimised positions.
1058 auto h = Kokkos::create_mirror_view(dpos);
1059 Kokkos::deep_copy(h, dpos);
1060 for (int i = 0; i < 3 * N; ++i)
1061 posHost[i] = h(i);
1062 return R;
1063}
1064
1065} // namespace peclet::voro
1066
1067#endif // PECLET_VORO_MESH_OPTIMIZER_HPP
Rung A3: interfacial (surface-tension) energy between cells of different species on the published vie...
KOKKOS_INLINE_FUNCTION Dual< Real, K > dnum(Real c)
Definition convex_cell.hpp:53
void interfaceGradReconstruct(const std::vector< Real > &x, const std::vector< int > &type, double sigma, const Real L[3], int N, int sw, const Sdf &sdf, std::vector< double > &g, double &E)
Definition mesh_optimizer.hpp:547
Device-native Voronoi tessellation, geometry and finite-volume physics.
Definition convex_cell.hpp:41
constexpr int kOptimizerCgIters
Definition params.hpp:37
OtResult meshVolumeOptimize(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, int cgIters=kOptimizerCgIters, Precond prec=Precond::Jacobi, bool verbose=false, Real muBarrier=0, Real muDecay=(Real) kBarrierDecay, bool freeEnergy=false)
Definition mesh_optimizer.hpp:65
OtResult meshVolumeOptimizeDevice(std::vector< Real > &posHost, const std::vector< Real > &vsetIn, const Real L[3], int N, int sw, const Sdf &sdf, int maxNewton, Real tol, int cgIters=kOptimizerCgIters, bool verbose=false)
Definition mesh_optimizer.hpp:772
constexpr double kBarrierDecay
Definition params.hpp:38
InterfaceResult interfaceMinimize(std::vector< Real > &pos, const std::vector< int > &type, double sigma, const Real L[3], int N, int sw, const Sdf &sdf, int maxIter, Real tol, bool verbose=false)
Definition mesh_optimizer.hpp:667
Precond
Definition mesh_optimizer.hpp:47
KOKKOS_INLINE_FUNCTION void buildConvexCell(ConvexCell< Real, MAXP, MAXT, TrackAdj > &c, const Real L[3], const Real *relx, const Real *rely, const Real *relz, const int *ids, int nNbr, Real wSelf=Real(0), const Real *weights=nullptr)
Definition convex_cell.hpp:1843
constexpr int kBoundaryFacet
Definition sdf.hpp:36
constexpr int kPoreMaxPlanes
Definition params.hpp:23
Semi-discrete optimal-transport VOLUME control on the power tessellation — the first energy-minimisat...
Definition convex_cell.hpp:140
Definition mesh_optimizer.hpp:659
int iters
Definition mesh_optimizer.hpp:660
double energy
final interfacial energy
Definition mesh_optimizer.hpp:661
double energyRatio
energy / initial energy
Definition mesh_optimizer.hpp:662
bool converged
Definition mesh_optimizer.hpp:663
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
TessellationView< Real > view
Definition tessellator.hpp:70