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
147 bool quality_veto = true;
148
149
166};
167
193template <class Mesh>
194bool smooth_vertex_3d(
195 Mesh& m,
196 const typename Mesh::Tuple& t,
197 const SmoothVertexOptions& opts,
198 std::unique_ptr<polysolve::nonlinear::Solver>& solver,
199 SmoothRejectCounters* counters = nullptr)
200{
201 using Tuple = typename Mesh::Tuple;
202
203 const size_t vid = t.vid(m);
204 auto& VA = m.m_vertex_attribute;
205 const auto locs = m.get_one_ring_tets_for_vertex(t);
206 assert(!locs.empty());
207
208 double max_quality = 0.;
209 for (const Tuple& tet : locs) {
210 max_quality = std::max(max_quality, m.cell_quality(tet.tid(m)));
211 if (m.is_inverted_f(tet)) {
212 // A neighbour that is not rounded can leave a tet inverted in floats even
213 // though it is fine in exact arithmetic; there is nothing to optimize from.
214 if (counters) ++counters->already_inverted;
215 return false;
216 }
217 }
218
219 // AMIPS wants each tet as 12 doubles with the moving vertex first, so that the solver
220 // can overwrite the leading xyz.
221 std::vector<std::array<double, 12>> assembles(locs.size());
222 for (size_t i = 0; i < locs.size(); ++i) {
223 std::array<size_t, 4> local_verts = m.oriented_tet_vids(locs[i].tid(m));
224 local_verts = wmtk::orient_preserve_tet_reorder(local_verts, vid);
225 for (int k = 0; k < 4; k++) {
226 for (int j = 0; j < 3; j++) {
227 assembles[i][k * 3 + j] = VA[local_verts[k]].m_posf[j];
228 }
229 }
230 }
231
232 if (!solver) {
233 solver = create_basic_solver();
234 }
235
236 const double amips_w = opts.w_amips > 0 ? opts.s_amips * opts.w_amips : 1.0;
237 auto amips_energy = std::make_shared<AMIPSEnergy3D>(assembles, amips_w);
238
239 // An application-supplied term for this vertex. It carries its own weight, exactly as
240 // ExactDistanceEnergy3D below does, so it is summed here at 1. Null for TetWild and SimWild,
241 // which leaves `base_energy` as `amips_energy` and every expression below exactly what it
242 // was.
243 const std::shared_ptr<polysolve::nonlinear::Problem> extra_energy =
244 m.smoothing_extra_energy(vid);
245 std::shared_ptr<polysolve::nonlinear::Problem> base_energy = amips_energy;
246 if (extra_energy) {
247 auto sum = std::make_shared<EnergySum>();
248 if (opts.w_amips > 0) sum->add_energy(amips_energy);
249 sum->add_energy(extra_energy);
250 base_energy = sum;
251 }
252 std::shared_ptr<polysolve::nonlinear::Problem> total_energy = base_energy;
253
254 auto solve = [&]() {
255 VectorXd x = VA[vid].m_posf;
256 try {
257 solver->minimize(*total_energy, x);
258 } catch (const std::exception&) {
259 // polysolve reports a failed line search by throwing; the position it reached
260 // is still the best it found, and the checks below decide whether to keep it.
261 }
262 VA[vid].m_posf = x;
263 };
264
265 const std::shared_ptr<SampleEnvelope> pull_env =
266 VA[vid].m_is_on_surface ? m.smoothing_energy_envelope(vid) : nullptr;
267
268 if (pull_env && opts.smoothing_mode == SmoothVertexOptions::SmoothingMode::Projected) {
269 // Smooth as if the vertex were interior, then walk back onto the input.
270 const Vector3d x_orig = VA[vid].m_posf;
271 total_energy = base_energy;
272 solve();
273 const Vector3d x_new = VA[vid].m_posf;
274
275 // Place a candidate and report the worst incident quality, or infinity if it
276 // inverts. is_inverted is exact, so the rational position tracks every candidate.
277 const auto worst_at = [&](const Vector3d& p) {
278 VA[vid].m_posf = p;
279 VA[vid].m_pos = to_rational(p);
280 double mq = 0.;
281 for (const Tuple& loc : locs) {
282 if (m.is_inverted(loc)) {
283 return std::numeric_limits<double>::infinity();
284 }
285 mq = std::max(mq, m.get_quality(loc));
286 }
287 return mq;
288 };
289
290 bool accepted = false;
291 std::vector<Vector3d> interp, proj; // kept for the nested pass below
292 interp.reserve(opts.project_line_search_steps);
293 proj.reserve(opts.project_line_search_steps);
294 for (int k = 0; k < opts.project_line_search_steps; ++k) {
295 const Vector3d p = x_orig + std::pow(0.5, k) * (x_new - x_orig);
296 Vector3d q;
297 pull_env->nearest_point(p, q);
298 interp.push_back(p);
299 proj.push_back(q);
300 if (worst_at(q) < max_quality) {
301 accepted = true;
302 break;
303 }
304 }
305
306 // Nothing ON the input was acceptable anywhere, so settle for getting as close to
307 // it as the one-ring allows. For each candidate, bisect the segment from the
308 // interpolated point (s = 0) to its projection (s = 1) for the LARGEST acceptable s.
309 // s = 1 is already known to fail -- that is what the first pass just established --
310 // so this brackets the boundary and converges up to it from below. Halving s down
311 // from 1 instead would cap the result at the midpoint and leave the vertex needlessly
312 // far from the input.
313 if (!accepted && opts.project_line_search_nested_steps > 0) {
314 for (size_t k = 0; k < proj.size() && !accepted; ++k) {
315 double lo = 0.0, hi = 1.0; // lo: best acceptable so far, hi: known bad
316 Vector3d best;
317 bool found = false;
318 for (int j = 0; j < opts.project_line_search_nested_steps; ++j) {
319 const double mid = 0.5 * (lo + hi);
320 const Vector3d cand = interp[k] + mid * (proj[k] - interp[k]);
321 if (worst_at(cand) < max_quality) {
322 lo = mid;
323 best = cand;
324 found = true;
325 } else {
326 hi = mid;
327 }
328 }
329 if (found) {
330 // worst_at left the vertex at the last candidate tried, which is not
331 // necessarily the best one.
332 worst_at(best);
333 accepted = true;
334 }
335 }
336 }
337 if (!accepted) {
338 VA[vid].m_posf = x_orig;
339 VA[vid].m_pos = to_rational(x_orig);
340 if (counters) ++counters->quality;
341 return false;
342 }
343 } else if (pull_env) {
344 auto envelope_energy =
345 std::make_shared<ExactDistanceEnergy3D>(pull_env, opts.s_envelope * opts.w_envelope);
346
347 if (opts.two_stage) {
348 auto warmup = std::make_shared<EnergySum>();
349 if (opts.w_amips > 0) warmup->add_energy(amips_energy, 1. / opts.w_amips);
350 if (opts.w_envelope > 0) warmup->add_energy(envelope_energy, 1. / opts.w_envelope);
351 if (extra_energy) warmup->add_energy(extra_energy);
352 total_energy = warmup;
353 solve();
354 }
355
356 auto weighted = std::make_shared<EnergySum>();
357 if (opts.w_amips > 0) weighted->add_energy(amips_energy);
358 if (opts.w_envelope > 0) weighted->add_energy(envelope_energy);
359 if (extra_energy) weighted->add_energy(extra_energy);
360 total_energy = weighted;
361 solve();
362 } else {
363 solve();
364 }
365
366 // Containment: every surface triangle at this vertex must still be inside. Checked
367 // against the containment envelope, which is not necessarily the one it was pulled to.
368 const std::shared_ptr<SampleEnvelope> check_env =
369 VA[vid].m_is_on_surface ? m.smoothing_containment_envelope(vid) : nullptr;
370 if (check_env) {
371 const simplex::SimplexCollection surf = m.get_surface_faces_for_vertex(vid);
372 for (const simplex::Face& f : surf.faces()) {
373 const std::array<Eigen::Vector3d, 3> face = {
374 {VA[f.vertices()[0]].m_posf,
375 VA[f.vertices()[1]].m_posf,
376 VA[f.vertices()[2]].m_posf}};
377 if (check_env->is_outside(face)) {
378 if (counters) ++counters->envelope;
379 return false;
380 }
381 }
382 }
383
384 // The rational position must be current before the exact inversion test.
385 VA[vid].m_pos = to_rational(VA[vid].m_posf);
386
387 double max_after_quality = 0.;
388 for (const Tuple& loc : locs) {
389 if (m.is_inverted(loc)) {
390 if (counters) ++counters->inverted;
391 return false;
392 }
393 const size_t tid = loc.tid(m);
394 const double quality = m.get_quality(loc);
395 m.set_cell_quality(tid, quality);
396 max_after_quality = std::max(max_after_quality, quality);
397 }
398
399 if (opts.quality_veto && (!VA[vid].m_is_on_surface || opts.quality_veto_on_surface)) {
400 if (max_after_quality > max_quality) {
401 if (counters) ++counters->quality;
402 return false;
403 }
404 }
405
406 if (counters) ++counters->accepted;
407 return true;
408}
409
410
428template <class Mesh>
429bool smooth_vertex_2d(
430 Mesh& m,
431 const typename Mesh::Tuple& t,
432 const SmoothVertexOptions& opts,
433 std::unique_ptr<polysolve::nonlinear::Solver>& solver,
434 SmoothRejectCounters* counters = nullptr)
435{
436 const size_t vid = t.vid(m);
437 auto& VA = m.m_vertex_attribute;
438 auto& FA = m.m_face_attribute;
439
440 const std::vector<size_t>& locs = m.get_one_ring_fids_for_vertex(t);
441 assert(!locs.empty());
442
443 double max_quality = 0.;
444 for (const size_t fid : locs) {
445 max_quality = std::max(max_quality, FA[fid].m_quality);
446 if (m.is_inverted_f(fid)) {
447 // Nothing to optimize from: a neighbour that is not rounded can leave a face
448 // inverted in floats even when it is fine exactly.
449 if (counters) ++counters->already_inverted;
450 return false;
451 }
452 }
453
454 // AMIPS wants each face as 6 doubles with the moving vertex first, keeping the winding.
455 std::vector<std::array<double, 6>> assembles;
456 assembles.reserve(locs.size());
457 for (const size_t fid : locs) {
458 std::array<size_t, 3> vs = m.oriented_tri_vids(fid);
459 size_t v_loc = 0;
460 for (size_t i = 0; i < 3; ++i) {
461 if (vs[i] == vid) {
462 v_loc = i;
463 break;
464 }
465 }
466 const std::array<size_t, 3> buf = vs;
467 vs[0] = buf[v_loc];
468 vs[1] = buf[(v_loc + 1) % 3];
469 vs[2] = buf[(v_loc + 2) % 3];
470
471 std::array<double, 6> T;
472 for (int i = 0; i < 3; i++) {
473 const Vector2d p = m.smoothing_position(vs[i]);
474 T[i * 2] = p[0];
475 T[i * 2 + 1] = p[1];
476 }
477 assembles.push_back(T);
478 }
479
480 if (!solver) {
481 solver = create_basic_solver();
482 }
483
484 const double amips_w = opts.w_amips > 0 ? opts.s_amips * opts.w_amips : 1.0;
485 auto amips_energy = std::make_shared<AMIPSEnergy2D>(assembles, amips_w);
486
487 // An application-supplied term for this vertex. It carries its own weight, exactly as
488 // ExactDistanceEnergy2D below does, so it is summed here at 1. Null for TriWild and
489 // SimWild, which leaves `base_energy` as `amips_energy` and every expression below exactly
490 // what it was.
491 const std::shared_ptr<polysolve::nonlinear::Problem> extra_energy =
492 m.smoothing_extra_energy(vid);
493 std::shared_ptr<polysolve::nonlinear::Problem> base_energy = amips_energy;
494 if (extra_energy) {
495 auto sum = std::make_shared<EnergySum>();
496 if (opts.w_amips > 0) sum->add_energy(amips_energy);
497 sum->add_energy(extra_energy);
498 base_energy = sum;
499 }
500 std::shared_ptr<polysolve::nonlinear::Problem> total_energy = base_energy;
501
502 auto solve = [&]() {
503 VectorXd x = m.smoothing_position(vid);
504 try {
505 solver->minimize(*total_energy, x);
506 } catch (const std::exception&) {
507 // A failed line search is reported by throwing; the position reached is still
508 // the best found, and the checks below decide whether to keep it.
509 }
510 m.set_smoothing_position(vid, Vector2d(x));
511 };
512
513 // Neighbours along the incident surface edges, captured before the solve. Only `vid`
514 // moves, so their positions are the same either way, but taking them first matches what
515 // both applications did and keeps the assert below meaningful.
516 std::vector<Vector2d> surf_neighbors;
517 if (VA[vid].m_is_on_surface) {
518 const simplex::SimplexCollection es = m.get_surface_edges_for_vertex(vid);
519 surf_neighbors.reserve(es.edges().size());
520 for (const simplex::Edge& e : es.edges()) {
521 const auto& evs = e.vertices();
522 surf_neighbors.push_back(m.smoothing_position(evs[0] != vid ? evs[0] : evs[1]));
523 }
524 assert(!surf_neighbors.empty());
525 }
526
527 // Per-vertex rather than the mesh's single m_envelope, and split into the PULL and the
528 // CONTAINER exactly as the 3D path above: see TriOptimizerMesh::smoothing_energy_envelope.
529 // The defaults return the old expression, so TriWild and SimWild are unaffected.
530 const std::shared_ptr<SampleEnvelope> envelope = m.smoothing_energy_envelope(vid);
531
532 if (envelope && opts.smoothing_mode == SmoothVertexOptions::SmoothingMode::Projected) {
533 const Vector2d x_orig = m.smoothing_position(vid);
534 total_energy = base_energy;
535 solve();
536 const Vector2d x_new = m.smoothing_position(vid);
537
538 // set_smoothing_position keeps the rational position in step, which the exact
539 // is_inverted below depends on.
540 const auto worst_at = [&](const Vector2d& p) {
541 m.set_smoothing_position(vid, p);
542 double mq = 0.;
543 for (const size_t fid : locs) {
544 if (m.is_inverted(fid)) {
545 return std::numeric_limits<double>::infinity();
546 }
547 mq = std::max(mq, m.get_quality(fid));
548 }
549 return mq;
550 };
551
552 bool accepted = false;
553 std::vector<Vector2d> interp, proj; // kept for the nested pass below
554 interp.reserve(opts.project_line_search_steps);
555 proj.reserve(opts.project_line_search_steps);
556 for (int k = 0; k < opts.project_line_search_steps; ++k) {
557 const Vector2d p = x_orig + std::pow(0.5, k) * (x_new - x_orig);
558 Vector2d q;
559 envelope->nearest_point(p, q);
560 interp.push_back(p);
561 proj.push_back(q);
562 if (worst_at(q) < max_quality) {
563 accepted = true;
564 break;
565 }
566 }
567
568 // Nothing ON the input was acceptable anywhere, so settle for getting as close to
569 // it as the one-ring allows. For each candidate, bisect the segment from the
570 // interpolated point (s = 0) to its projection (s = 1) for the LARGEST acceptable s.
571 // s = 1 is already known to fail -- that is what the first pass just established --
572 // so this brackets the boundary and converges up to it from below. Halving s down
573 // from 1 instead would cap the result at the midpoint and leave the vertex needlessly
574 // far from the input.
575 if (!accepted && opts.project_line_search_nested_steps > 0) {
576 for (size_t k = 0; k < proj.size() && !accepted; ++k) {
577 double lo = 0.0, hi = 1.0; // lo: best acceptable so far, hi: known bad
578 Vector2d best;
579 bool found = false;
580 for (int j = 0; j < opts.project_line_search_nested_steps; ++j) {
581 const double mid = 0.5 * (lo + hi);
582 const Vector2d cand = interp[k] + mid * (proj[k] - interp[k]);
583 if (worst_at(cand) < max_quality) {
584 lo = mid;
585 best = cand;
586 found = true;
587 } else {
588 hi = mid;
589 }
590 }
591 if (found) {
592 // worst_at left the vertex at the last candidate tried, which is not
593 // necessarily the best one.
594 worst_at(best);
595 accepted = true;
596 }
597 }
598 }
599 if (!accepted) {
600 m.set_smoothing_position(vid, x_orig);
601 if (counters) ++counters->quality;
602 return false;
603 }
604 } else if (envelope) {
605 auto envelope_energy =
606 std::make_shared<ExactDistanceEnergy2D>(envelope, opts.s_envelope * opts.w_envelope);
607
608 if (opts.two_stage) {
609 auto warmup = std::make_shared<EnergySum>();
610 if (opts.w_amips > 0) warmup->add_energy(amips_energy, 1. / opts.w_amips);
611 if (opts.w_envelope > 0) warmup->add_energy(envelope_energy, 1. / opts.w_envelope);
612 if (extra_energy) warmup->add_energy(extra_energy);
613 total_energy = warmup;
614 solve();
615 }
616
617 auto weighted = std::make_shared<EnergySum>();
618 if (opts.w_amips > 0) weighted->add_energy(amips_energy);
619 if (opts.w_envelope > 0) weighted->add_energy(envelope_energy);
620 if (extra_energy) weighted->add_energy(extra_energy);
621 total_energy = weighted;
622 solve();
623 } else {
624 solve();
625 }
626
627 // Per-vertex positional constraint, on top of the envelope. A mesh uses this to pin a
628 // vertex to a 0-dimensional feature it stands for -- within a ball, so the vertex is
629 // still free to move and improve quality, it just cannot walk away from the feature.
630 // Meshes with no such features answer true unconditionally.
631 if (!m.smoothing_position_is_allowed(vid, m.smoothing_position(vid))) {
632 if (counters) ++counters->envelope;
633 return false;
634 }
635
636 // Containment, edge by edge rather than face by face as in 3D -- and against the
637 // CONTAINMENT envelope, which is not necessarily the one the vertex was pulled to.
638 if (const std::shared_ptr<SampleEnvelope> hold_env = m.smoothing_containment_envelope(vid)) {
639 const Vector2d p = m.smoothing_position(vid);
640 for (const Vector2d& q : surf_neighbors) {
641 const std::array<Eigen::Vector2d, 2> edge = {{p, q}};
642 if (hold_env->is_outside(edge)) {
643 if (counters) ++counters->envelope;
644 return false;
645 }
646 }
647 }
648
649 double max_after_quality = 0.;
650 for (const size_t fid : locs) {
651 if (m.is_inverted(fid)) {
652 if (counters) ++counters->inverted;
653 return false;
654 }
655 const double q = m.get_quality(fid);
656 FA[fid].m_quality = q;
657 max_after_quality = std::max(max_after_quality, q);
658 }
659
660 if (opts.quality_veto && (!VA[vid].m_is_on_surface || opts.quality_veto_on_surface)) {
661 if (max_after_quality > max_quality) {
662 if (counters) ++counters->quality;
663 return false;
664 }
665 }
666
667 if (counters) ++counters->accepted;
668 return true;
669}
670
671} // 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:165
bool quality_veto
Definition SmoothVertex.hpp:147
bool two_stage
Definition SmoothVertex.hpp:78
SmoothingMode
Definition SmoothVertex.hpp:123