Wildmeshing Toolkit
Loading...
Searching...
No Matches
SmoothVertex.hpp
1#pragma once
2
3#include <wmtk/optimization/AMIPSEnergy.hpp>
4#include <wmtk/optimization/EnergySum.hpp>
5#include <wmtk/optimization/EnvelopeEnergy.hpp>
6#include <wmtk/optimization/solver.hpp>
7
8#include <wmtk/Types.hpp>
9#include <wmtk/envelope/Envelope.hpp>
10#include <wmtk/simplex/SimplexCollection.hpp>
11#include <wmtk/utils/Logger.hpp>
12#include <wmtk/utils/TetraQualityUtils.hpp>
13
14#include <array>
15#include <atomic>
16#include <limits>
17#include <memory>
18#include <vector>
19
20namespace wmtk::optimization {
21
31{
32 std::atomic<size_t> already_inverted{0};
33 std::atomic<size_t> envelope{0};
34 std::atomic<size_t> inverted{0};
35 std::atomic<size_t> quality{0};
36 std::atomic<size_t> accepted{0};
37
38 void reset()
39 {
41 envelope = 0;
42 inverted = 0;
43 quality = 0;
44 accepted = 0;
45 }
46
47 std::string to_string() const
48 {
49 return fmt::format(
50 "accepted {} | rejected: pre-inverted {}, envelope {}, inverted {}, quality {}",
51 accepted.load(),
52 already_inverted.load(),
53 envelope.load(),
54 inverted.load(),
55 quality.load());
56 }
57};
58
70{
71 double w_amips = 1e-4;
72 double w_envelope = 1.0 - 1e-4;
73 double s_amips = 1.0;
74 double s_envelope = 1.0;
75
78 bool two_stage = true;
79
108
123 enum class SmoothingMode { Projected, Exact };
124 SmoothingMode smoothing_mode = SmoothingMode::Projected;
125
129
146};
147
170template <class Mesh>
171bool smooth_vertex_3d(
172 Mesh& m,
173 const typename Mesh::Tuple& t,
174 const SmoothVertexOptions& opts,
175 std::unique_ptr<polysolve::nonlinear::Solver>& solver,
176 SmoothRejectCounters* counters = nullptr)
177{
178 using Tuple = typename Mesh::Tuple;
179
180 const size_t vid = t.vid(m);
181 auto& VA = m.m_vertex_attribute;
182 const auto locs = m.get_one_ring_tets_for_vertex(t);
183 assert(!locs.empty());
184
185 double max_quality = 0.;
186 for (const Tuple& tet : locs) {
187 max_quality = std::max(max_quality, m.cell_quality(tet.tid(m)));
188 if (m.is_inverted_f(tet)) {
189 // A neighbour that is not rounded can leave a tet inverted in floats even
190 // though it is fine in exact arithmetic; there is nothing to optimize from.
191 if (counters) ++counters->already_inverted;
192 return false;
193 }
194 }
195
196 // AMIPS wants each tet as 12 doubles with the moving vertex first, so that the solver
197 // can overwrite the leading xyz.
198 std::vector<std::array<double, 12>> assembles(locs.size());
199 for (size_t i = 0; i < locs.size(); ++i) {
200 std::array<size_t, 4> local_verts = m.oriented_tet_vids(locs[i].tid(m));
201 local_verts = wmtk::orient_preserve_tet_reorder(local_verts, vid);
202 for (int k = 0; k < 4; k++) {
203 for (int j = 0; j < 3; j++) {
204 assembles[i][k * 3 + j] = VA[local_verts[k]].m_posf[j];
205 }
206 }
207 }
208
209 if (!solver) {
210 solver = create_basic_solver();
211 }
212
213 const double amips_w = opts.w_amips > 0 ? opts.s_amips * opts.w_amips : 1.0;
214 auto amips_energy = std::make_shared<AMIPSEnergy3D>(assembles, amips_w);
215 std::shared_ptr<polysolve::nonlinear::Problem> total_energy = amips_energy;
216
217 auto solve = [&]() {
218 VectorXd x = VA[vid].m_posf;
219 try {
220 solver->minimize(*total_energy, x);
221 } catch (const std::exception&) {
222 // polysolve reports a failed line search by throwing; the position it reached
223 // is still the best it found, and the checks below decide whether to keep it.
224 }
225 VA[vid].m_posf = x;
226 };
227
228 const std::shared_ptr<SampleEnvelope> pull_env =
229 VA[vid].m_is_on_surface ? m.smoothing_energy_envelope(vid) : nullptr;
230
231 if (pull_env && opts.smoothing_mode == SmoothVertexOptions::SmoothingMode::Projected) {
232 // Smooth as if the vertex were interior, then walk back onto the input.
233 const Vector3d x_orig = VA[vid].m_posf;
234 total_energy = amips_energy;
235 solve();
236 const Vector3d x_new = VA[vid].m_posf;
237
238 // Place a candidate and report the worst incident quality, or infinity if it
239 // inverts. is_inverted is exact, so the rational position tracks every candidate.
240 const auto worst_at = [&](const Vector3d& p) {
241 VA[vid].m_posf = p;
242 VA[vid].m_pos = to_rational(p);
243 double mq = 0.;
244 for (const Tuple& loc : locs) {
245 if (m.is_inverted(loc)) {
246 return std::numeric_limits<double>::infinity();
247 }
248 mq = std::max(mq, m.get_quality(loc));
249 }
250 return mq;
251 };
252
253 bool accepted = false;
254 std::vector<Vector3d> interp, proj; // kept for the nested pass below
255 interp.reserve(opts.project_line_search_steps);
256 proj.reserve(opts.project_line_search_steps);
257 for (int k = 0; k < opts.project_line_search_steps; ++k) {
258 const Vector3d p = x_orig + std::pow(0.5, k) * (x_new - x_orig);
259 Vector3d q;
260 pull_env->nearest_point(p, q);
261 interp.push_back(p);
262 proj.push_back(q);
263 if (worst_at(q) < max_quality) {
264 accepted = true;
265 break;
266 }
267 }
268
269 // Nothing ON the input was acceptable anywhere, so settle for getting as close to
270 // it as the one-ring allows. For each candidate, bisect the segment from the
271 // interpolated point (s = 0) to its projection (s = 1) for the LARGEST acceptable s.
272 // s = 1 is already known to fail -- that is what the first pass just established --
273 // so this brackets the boundary and converges up to it from below. Halving s down
274 // from 1 instead would cap the result at the midpoint and leave the vertex needlessly
275 // far from the input.
276 if (!accepted && opts.project_line_search_nested_steps > 0) {
277 for (size_t k = 0; k < proj.size() && !accepted; ++k) {
278 double lo = 0.0, hi = 1.0; // lo: best acceptable so far, hi: known bad
279 Vector3d best;
280 bool found = false;
281 for (int j = 0; j < opts.project_line_search_nested_steps; ++j) {
282 const double mid = 0.5 * (lo + hi);
283 const Vector3d cand = interp[k] + mid * (proj[k] - interp[k]);
284 if (worst_at(cand) < max_quality) {
285 lo = mid;
286 best = cand;
287 found = true;
288 } else {
289 hi = mid;
290 }
291 }
292 if (found) {
293 // worst_at left the vertex at the last candidate tried, which is not
294 // necessarily the best one.
295 worst_at(best);
296 accepted = true;
297 }
298 }
299 }
300 if (!accepted) {
301 VA[vid].m_posf = x_orig;
302 VA[vid].m_pos = to_rational(x_orig);
303 if (counters) ++counters->quality;
304 return false;
305 }
306 } else if (pull_env) {
307 auto envelope_energy =
308 std::make_shared<ExactDistanceEnergy3D>(pull_env, opts.s_envelope * opts.w_envelope);
309
310 if (opts.two_stage) {
311 auto warmup = std::make_shared<EnergySum>();
312 if (opts.w_amips > 0) warmup->add_energy(amips_energy, 1. / opts.w_amips);
313 if (opts.w_envelope > 0) warmup->add_energy(envelope_energy, 1. / opts.w_envelope);
314 total_energy = warmup;
315 solve();
316 }
317
318 auto weighted = std::make_shared<EnergySum>();
319 if (opts.w_amips > 0) weighted->add_energy(amips_energy);
320 if (opts.w_envelope > 0) weighted->add_energy(envelope_energy);
321 total_energy = weighted;
322 solve();
323 } else {
324 solve();
325 }
326
327 // Containment: every surface triangle at this vertex must still be inside. Checked
328 // against the containment envelope, which is not necessarily the one it was pulled to.
329 const std::shared_ptr<SampleEnvelope> check_env =
330 VA[vid].m_is_on_surface ? m.smoothing_containment_envelope(vid) : nullptr;
331 if (check_env) {
332 const simplex::SimplexCollection surf = m.get_surface_faces_for_vertex(vid);
333 for (const simplex::Face& f : surf.faces()) {
334 const std::array<Eigen::Vector3d, 3> face = {
335 {VA[f.vertices()[0]].m_posf,
336 VA[f.vertices()[1]].m_posf,
337 VA[f.vertices()[2]].m_posf}};
338 if (check_env->is_outside(face)) {
339 if (counters) ++counters->envelope;
340 return false;
341 }
342 }
343 }
344
345 // The rational position must be current before the exact inversion test.
346 VA[vid].m_pos = to_rational(VA[vid].m_posf);
347
348 double max_after_quality = 0.;
349 for (const Tuple& loc : locs) {
350 if (m.is_inverted(loc)) {
351 if (counters) ++counters->inverted;
352 return false;
353 }
354 const size_t tid = loc.tid(m);
355 const double quality = m.get_quality(loc);
356 m.set_cell_quality(tid, quality);
357 max_after_quality = std::max(max_after_quality, quality);
358 }
359
360 if (!VA[vid].m_is_on_surface || opts.quality_veto_on_surface) {
361 if (max_after_quality > max_quality) {
362 if (counters) ++counters->quality;
363 return false;
364 }
365 }
366
367 if (counters) ++counters->accepted;
368 return true;
369}
370
371
389template <class Mesh>
390bool smooth_vertex_2d(
391 Mesh& m,
392 const typename Mesh::Tuple& t,
393 const SmoothVertexOptions& opts,
394 std::unique_ptr<polysolve::nonlinear::Solver>& solver,
395 SmoothRejectCounters* counters = nullptr)
396{
397 const size_t vid = t.vid(m);
398 auto& VA = m.m_vertex_attribute;
399 auto& FA = m.m_face_attribute;
400
401 const std::vector<size_t>& locs = m.get_one_ring_fids_for_vertex(t);
402 assert(!locs.empty());
403
404 double max_quality = 0.;
405 for (const size_t fid : locs) {
406 max_quality = std::max(max_quality, FA[fid].m_quality);
407 if (m.is_inverted_f(fid)) {
408 // Nothing to optimize from: a neighbour that is not rounded can leave a face
409 // inverted in floats even when it is fine exactly.
410 if (counters) ++counters->already_inverted;
411 return false;
412 }
413 }
414
415 // AMIPS wants each face as 6 doubles with the moving vertex first, keeping the winding.
416 std::vector<std::array<double, 6>> assembles;
417 assembles.reserve(locs.size());
418 for (const size_t fid : locs) {
419 std::array<size_t, 3> vs = m.oriented_tri_vids(fid);
420 size_t v_loc = 0;
421 for (size_t i = 0; i < 3; ++i) {
422 if (vs[i] == vid) {
423 v_loc = i;
424 break;
425 }
426 }
427 const std::array<size_t, 3> buf = vs;
428 vs[0] = buf[v_loc];
429 vs[1] = buf[(v_loc + 1) % 3];
430 vs[2] = buf[(v_loc + 2) % 3];
431
432 std::array<double, 6> T;
433 for (int i = 0; i < 3; i++) {
434 const Vector2d p = m.smoothing_position(vs[i]);
435 T[i * 2] = p[0];
436 T[i * 2 + 1] = p[1];
437 }
438 assembles.push_back(T);
439 }
440
441 if (!solver) {
442 solver = create_basic_solver();
443 }
444
445 const double amips_w = opts.w_amips > 0 ? opts.s_amips * opts.w_amips : 1.0;
446 auto amips_energy = std::make_shared<AMIPSEnergy2D>(assembles, amips_w);
447 std::shared_ptr<polysolve::nonlinear::Problem> total_energy = amips_energy;
448
449 auto solve = [&]() {
450 VectorXd x = m.smoothing_position(vid);
451 try {
452 solver->minimize(*total_energy, x);
453 } catch (const std::exception&) {
454 // A failed line search is reported by throwing; the position reached is still
455 // the best found, and the checks below decide whether to keep it.
456 }
457 m.set_smoothing_position(vid, Vector2d(x));
458 };
459
460 // Neighbours along the incident surface edges, captured before the solve. Only `vid`
461 // moves, so their positions are the same either way, but taking them first matches what
462 // both applications did and keeps the assert below meaningful.
463 std::vector<Vector2d> surf_neighbors;
464 if (VA[vid].m_is_on_surface) {
465 const simplex::SimplexCollection es = m.get_surface_edges_for_vertex(vid);
466 surf_neighbors.reserve(es.edges().size());
467 for (const simplex::Edge& e : es.edges()) {
468 const auto& evs = e.vertices();
469 surf_neighbors.push_back(m.smoothing_position(evs[0] != vid ? evs[0] : evs[1]));
470 }
471 assert(!surf_neighbors.empty());
472 }
473
474 const std::shared_ptr<SampleEnvelope> envelope =
475 VA[vid].m_is_on_surface ? m.m_envelope : nullptr;
476
477 if (envelope && opts.smoothing_mode == SmoothVertexOptions::SmoothingMode::Projected) {
478 const Vector2d x_orig = m.smoothing_position(vid);
479 total_energy = amips_energy;
480 solve();
481 const Vector2d x_new = m.smoothing_position(vid);
482
483 // set_smoothing_position keeps the rational position in step, which the exact
484 // is_inverted below depends on.
485 const auto worst_at = [&](const Vector2d& p) {
486 m.set_smoothing_position(vid, p);
487 double mq = 0.;
488 for (const size_t fid : locs) {
489 if (m.is_inverted(fid)) {
490 return std::numeric_limits<double>::infinity();
491 }
492 mq = std::max(mq, m.get_quality(fid));
493 }
494 return mq;
495 };
496
497 bool accepted = false;
498 std::vector<Vector2d> interp, proj; // kept for the nested pass below
499 interp.reserve(opts.project_line_search_steps);
500 proj.reserve(opts.project_line_search_steps);
501 for (int k = 0; k < opts.project_line_search_steps; ++k) {
502 const Vector2d p = x_orig + std::pow(0.5, k) * (x_new - x_orig);
503 Vector2d q;
504 envelope->nearest_point(p, q);
505 interp.push_back(p);
506 proj.push_back(q);
507 if (worst_at(q) < max_quality) {
508 accepted = true;
509 break;
510 }
511 }
512
513 // Nothing ON the input was acceptable anywhere, so settle for getting as close to
514 // it as the one-ring allows. For each candidate, bisect the segment from the
515 // interpolated point (s = 0) to its projection (s = 1) for the LARGEST acceptable s.
516 // s = 1 is already known to fail -- that is what the first pass just established --
517 // so this brackets the boundary and converges up to it from below. Halving s down
518 // from 1 instead would cap the result at the midpoint and leave the vertex needlessly
519 // far from the input.
520 if (!accepted && opts.project_line_search_nested_steps > 0) {
521 for (size_t k = 0; k < proj.size() && !accepted; ++k) {
522 double lo = 0.0, hi = 1.0; // lo: best acceptable so far, hi: known bad
523 Vector2d best;
524 bool found = false;
525 for (int j = 0; j < opts.project_line_search_nested_steps; ++j) {
526 const double mid = 0.5 * (lo + hi);
527 const Vector2d cand = interp[k] + mid * (proj[k] - interp[k]);
528 if (worst_at(cand) < max_quality) {
529 lo = mid;
530 best = cand;
531 found = true;
532 } else {
533 hi = mid;
534 }
535 }
536 if (found) {
537 // worst_at left the vertex at the last candidate tried, which is not
538 // necessarily the best one.
539 worst_at(best);
540 accepted = true;
541 }
542 }
543 }
544 if (!accepted) {
545 m.set_smoothing_position(vid, x_orig);
546 if (counters) ++counters->quality;
547 return false;
548 }
549 } else if (envelope) {
550 auto envelope_energy =
551 std::make_shared<ExactDistanceEnergy2D>(envelope, opts.s_envelope * opts.w_envelope);
552
553 if (opts.two_stage) {
554 auto warmup = std::make_shared<EnergySum>();
555 if (opts.w_amips > 0) warmup->add_energy(amips_energy, 1. / opts.w_amips);
556 if (opts.w_envelope > 0) warmup->add_energy(envelope_energy, 1. / opts.w_envelope);
557 total_energy = warmup;
558 solve();
559 }
560
561 auto weighted = std::make_shared<EnergySum>();
562 if (opts.w_amips > 0) weighted->add_energy(amips_energy);
563 if (opts.w_envelope > 0) weighted->add_energy(envelope_energy);
564 total_energy = weighted;
565 solve();
566 } else {
567 solve();
568 }
569
570 // Per-vertex positional constraint, on top of the envelope. A mesh uses this to pin a
571 // vertex to a 0-dimensional feature it stands for -- within a ball, so the vertex is
572 // still free to move and improve quality, it just cannot walk away from the feature.
573 // Meshes with no such features answer true unconditionally.
574 if (!m.smoothing_position_is_allowed(vid, m.smoothing_position(vid))) {
575 if (counters) ++counters->envelope;
576 return false;
577 }
578
579 // Containment, edge by edge rather than face by face as in 3D.
580 if (envelope) {
581 const Vector2d p = m.smoothing_position(vid);
582 for (const Vector2d& q : surf_neighbors) {
583 const std::array<Eigen::Vector2d, 2> edge = {{p, q}};
584 if (envelope->is_outside(edge)) {
585 if (counters) ++counters->envelope;
586 return false;
587 }
588 }
589 }
590
591 double max_after_quality = 0.;
592 for (const size_t fid : locs) {
593 if (m.is_inverted(fid)) {
594 if (counters) ++counters->inverted;
595 return false;
596 }
597 const double q = m.get_quality(fid);
598 FA[fid].m_quality = q;
599 max_after_quality = std::max(max_after_quality, q);
600 }
601
602 if (!VA[vid].m_is_on_surface || opts.quality_veto_on_surface) {
603 if (max_after_quality > max_quality) {
604 if (counters) ++counters->quality;
605 return false;
606 }
607 }
608
609 if (counters) ++counters->accepted;
610 return true;
611}
612
613} // namespace wmtk::optimization
Why a smoothing attempt was refused, counted per pass.
Definition SmoothVertex.hpp:31
std::atomic< size_t > envelope
a surface triangle left the envelope
Definition SmoothVertex.hpp:33
std::atomic< size_t > already_inverted
incident tet inverted in floats on entry
Definition SmoothVertex.hpp:32
std::atomic< size_t > quality
the move made the worst incident tet worse
Definition SmoothVertex.hpp:35
std::atomic< size_t > inverted
the move inverted a tet (exact predicate)
Definition SmoothVertex.hpp:34
Weights and policy for smooth_vertex_3d.
Definition SmoothVertex.hpp:70
bool quality_veto_on_surface
Definition SmoothVertex.hpp:107
int project_line_search_steps
Definition SmoothVertex.hpp:128
int project_line_search_nested_steps
Definition SmoothVertex.hpp:145
bool two_stage
Definition SmoothVertex.hpp:78
SmoothingMode
Definition SmoothVertex.hpp:123