Wildmeshing Toolkit
Loading...
Searching...
No Matches
Parameters.h
1#pragma once
2#include <wmtk/OptimizerParameters.h>
3#include <nlohmann/json.hpp>
4#include <wmtk/Types.hpp>
5#include <wmtk/components/simwild/expression_parser/Expression.hpp>
6
7using ExpressionPtr = wmtk::components::simwild::expression_parser::ExpressionPtr;
8
9namespace wmtk::components::topological_offset {
10
24{
25 ExpressionPtr offset_selection;
26 std::set<std::string> offset_output_tag;
27 std::set<std::string> protected_tags;
28 bool offset_in;
29 bool offset_out;
30 double target_distance;
31 double target_distance_rel;
32 // Turn a non-converged run into a hard error instead of a warning. Off by default: a run that
33 // misses the target is still a usable offset, and the warnings name the criterion that failed.
34 // Integration tests set it true so a convergence regression fails rather than warns.
35 bool throw_on_nonconvergence;
39 // Half-width of the envelope that contains every tag-region boundary during optimization.
40 // Absolute; if < 0, computed from envelope_size_rel (relative to the bbox diagonal).
41 double envelope_size;
42 double envelope_size_rel;
43
44 // ---- the smooth offset potential ----
45 // Support radius of the potential, as a multiple of target_distance. Must be > 1: the offset
46 // level set has to lie strictly inside the support, or the vertices on it get no gradient. A
47 // band vertex that travels past the support is a hard error, not a silently frozen vertex.
48 double offset_dhat_factor;
54 std::string offset_field;
55 // ---- the two convergence epsilons (was the single front_conv_rel) ----
56 // Both are ABSOLUTE LENGTHS, resolved in init(); if < 0 each is computed from its _rel twin,
57 // which is a fraction of the BOUNDING BOX DIAGONAL -- the same absolute/relative pair as
58 // envelope_size / envelope_size_rel, and deliberately NOT a fraction of target_distance any
59 // more, so changing the offset distance no longer silently changes the accuracy. init()
60 // refuses either one above target_distance: an epsilon coarser than the offset it measures
61 // cannot decide anything.
62 //
63 // THE ONE BAR. 3D measures a single quantity everywhere -- over a face's stencil, the RMS of
64 // the distance to the level set along the field (OffsetPotential::relative_residual(), which
65 // for the euclidean field is the relative error (Phi - c)/c) -- and compares it against
66 // this. A vertex is placed when that same measure at the vertex alone is within it, which is
67 // the order-0 stencil, so the vertex measure and the face measure are one measure at two
68 // sample counts. Replaces vertex_conv / sag_conv, which split the two apart 2026-09-23. The
69 // loop exits on the face measure alone (every face within the bar, nothing unmeasurable);
70 // the vertex measure is a diagnostic since 2026-09-25. 2D exits on its chord measure, the
71 // midpoint sag, which unlike the 3D stencil does not sample the chord's two ends.
72 double front_conv;
73 double front_conv_rel;
74
81 double front_conv_frac() const { return front_conv / std::max(target_distance, 1e-16); }
82 // 2D ONLY since the 3D criteria were unified. Which convergence test gates a 2D run's vertex
83 // placement. F is the vertex's front objective, g its gradient, H its Gauss-Newton Hessian,
84 // n its move direction; all four compare against front_conv. See front_vertex_conv_ratio().
85 // "step_size_rel" (the default): the remaining 1-D Newton step, |n.g| / (n^T H n), against
86 // front_conv.
87 // "decrement": the Newton decrement, half of (n.g)^2 / (n^T H n), against
88 // front_conv_frac() x F.
89 // "gradient_norm_rel": |n.g| against front_conv_frac() x the reference gradient, measured
90 // once on the band as constructed.
91 // "residual_error": not a stationarity measure at all -- the field's own residual at the
92 // vertex as a length (OffsetPotential::residual_length(), so |d - target_distance| for
93 // the euclidean field and the distance to the level set along the field for the smooth
94 // one), against front_conv.
95 // No objective is built and n does not enter.
96 //
97 // 3D DOES NOT READ THIS. Its one measure is the stencil RMS of the relative error, which is
98 // not a stationarity test and has no variants -- see TopoOffsetTetMesh::face_conv_ratio().
101 // The front is placed by a one-dimensional solve along its field normal
102 // n = grad Phi / |grad Phi| -- same objective, solver and accept test, restricted to the line
103 // x0 + s n -- instead of a free solve. Where a vertex sits along the front carries no offset
104 // information, and in the free solve that tangential motion made fronts slide and fold where
105 // two of them meet.
106 bool front_normal_projection = true;
114 bool deform_others = true;
117 int max_rounds = 40;
118 // Sampling density of THE measure -- in 3D both the criterion's and the energy's -- and of
119 // the residual diagnostics that share the lattice. Order k puts these points on a face:
120 // 0 -> 3, the CORNERS alone, so the measure is exactly the three vertices' placement error;
121 // k >= 1 -> the vertices of the triangle subdivided k-1 times by 4-way midpoint refinement
122 // plus the centroid of each of its 4^(k-1) sub-triangles, i.e. 4, 10, 31, 109, ...
123 // The corners are IN the stencil, unlike the strictly interior lattice this replaces, because
124 // the quantity measured is a distance to the level set rather than an interpolation error and
125 // so is not identically zero there. Raising it costs a Phi value and gradient per sample, in
126 // the ops guard's hot path as well as in energy_criterion() and every smoothing solve.
127 // 2D reads this key for its diagnostics only; its chord test is still the MIDPOINT.
128 // See TopoOffsetTetMesh::for_each_face_sample, TopoOffsetTriMesh::offset_edge_samples.
129 int stencil_order;
140 std::string front_measure;
141 bool sorted_marching;
185 std::string output_path; // no extension
186 bool save_vtu;
187
188 // Samples per side of the grid the smooth offset potential is written on, beside the result,
189 // for the viewer. 0 disables it. The whole domain in 2D, one plane through the box in 3D.
190 int phi_grid_resolution;
191
192 int num_threads; // number of threads for parallel execution (smoothing, collapse). 0 = serial
196 // The operation passes' offset envelope half-width: the leash the front is kept inside while
197 // the operation passes run, the same tube every turn, rebuilt after every smoothing pass; see
198 // rebuild_offset_envelope(). Absolute-or-relative exactly as envelope_size / envelope_size_rel
199 // and against the same reference, the BOUNDING BOX DIAGONAL -- it is a distance in space, and
200 // tying it to target_distance made every change of the offset distance a silent change of the
201 // leash as well. Also feeds the derived sizing floor (min_edge_length_rel < 0).
203 double offset_envelope_rel;
204
205 // l_min from the paper: the shortest edge the sizing field may ask for, given as a multiple of
206 // target_distance rather than of the bounding box because that is the scale the offset has;
207 // min_edge_length is derived from min_edge_length_rel in init() when negative. A floor on
208 // refinement, so raising it makes the result coarser. When not given, it falls back to the
209 // offset envelope eps, following TetWild: a surface pinned only to within eps cannot buy
210 // fidelity from shorter edges, so this is a runaway rail, not a resolution setting.
211 double min_edge_length;
212 double min_edge_length_rel;
213
214 // ---- sizing field ----
215 // bounds for VertexAttributes::m_sizing_scalar
216 double min_sizing_scalar;
217 double max_sizing_scalar;
218 // gradation cap: neighboring vertices' sizing scalars may differ by at most this factor,
219 // enforced by propagating the refinement outward (monotone, only ever lowers a
220 // neighbor's scalar). <= 1 disables gradation entirely. Only used by "ring" mode.
221 double sizing_gradation;
237
238 VectorXd box_min;
239 VectorXd box_max;
240
241 Parameters() = default;
242
243 Parameters(const nlohmann::json& json_params)
244 {
245 for (const std::string& tag : json_params["offset_output_tags"]) {
246 if (tag == "ambient") {
247 logger().warn(
248 "'ambient' tag cannot be given explicitly to offset_output_tags, ignoring. To "
249 "set offset to 'ambient', pass offset_output_tags=[].");
250 continue;
251 }
252 offset_output_tag.insert(tag);
253 }
254 for (const std::string& tag : json_params["protected_tags"]) {
255 if (tag == "ambient") {
256 logger().warn("'ambient' tag cannot be protected, ignoring.");
257 continue;
258 }
259 protected_tags.insert(tag);
260 }
261 offset_in = json_params["offset_in"];
262 offset_out = json_params["offset_out"];
263 target_distance = json_params["target_distance"];
264 target_distance_rel = json_params["target_distance_rel"];
265 throw_on_nonconvergence = json_params["throw_on_nonconvergence"];
266 optimize_offset = json_params["optimize_offset"];
267 envelope_size = json_params["envelope_size"];
268 envelope_size_rel = json_params["envelope_size_rel"];
269 offset_dhat_factor = json_params["offset_dhat_factor"];
270 debug_manual_dhat = json_params["DEBUG_manual_dhat"];
271 offset_field = json_params["offset_field"];
272 front_conv = json_params["front_conv"];
273 front_conv_rel = json_params["front_conv_rel"];
274 front_conv_criterion = json_params["front_conv_criterion"];
275 stencil_order = json_params["stencil_order"];
276 front_measure = json_params["front_measure"];
277
278 sorted_marching = json_params["sorted_marching"];
279 sphere_trace_initialization = json_params["sphere_trace_initialization"];
280 sphere_trace_target_rel_tol = json_params["sphere_trace_target_rel_tol"];
282 json_params["EXPERIMENTAL_consistent_construction_split"];
283 experimental_aggresive_refine = json_params["EXPERIMENTAL_aggresive_refine"];
284 experimental_nonoverlapping_gates = json_params["EXPERIMENTAL_nonoverlapping_gates"];
285 output_path = json_params["output"];
286 save_vtu = json_params["save_vtu"];
287 phi_grid_resolution = json_params["phi_grid_resolution"];
288
289 num_threads = json_params["num_threads"];
290 max_iterations = json_params["max_iterations"];
291 offset_envelope = json_params["offset_envelope"];
292 offset_envelope_rel = json_params["offset_envelope_rel"];
293
294 min_edge_length = json_params["min_edge_length"];
295 min_edge_length_rel = json_params["min_edge_length_rel"];
296
297 min_sizing_scalar = json_params["min_sizing_scalar"];
298 max_sizing_scalar = json_params["max_sizing_scalar"];
299 sizing_gradation = json_params["sizing_gradation"];
300 sizing_gradation_mode = json_params["sizing_gradation_mode"];
301 adaptive_smoothing = json_params["adaptive_smoothing"];
302 adaptive_smoothing_max_passes = json_params["adaptive_smoothing_max_passes"];
303 adaptive_smoothing_stall_rel = json_params["adaptive_smoothing_stall_rel"];
304 adaptive_smoothing_step_rel = json_params["adaptive_smoothing_step_rel"];
305 pre_smooth = json_params["pre_smooth"];
306
307 // ---- inherited from wmtk::OptimizerParameters ----
308 debug_output = json_params["DEBUG_output"];
309 lr = json_params["length_rel"];
310 l = json_params["length"];
311 stop_energy = json_params["stop_energy"];
312 num_smoothing_passes = json_params["num_smoothing_passes"];
313 interleaved_smoothing = json_params["interleaved_smoothing"];
314 interleaved_smoothing_passes = json_params["interleaved_smoothing_passes"];
315 split_high_valence_threshold = json_params["split_high_valence_threshold"];
316 // skip_good_regions is deliberately not exposed: it would restrict a smoothing pass to
317 // cells still far from stop_energy, but the smoother is what places the offset boundary,
318 // so a well-shaped yet badly-placed patch is exactly what must not be skipped.
319 // Every key of the coarsening group is copied here, not just the on/off switch: declaring
320 // a key in the spec only makes jse inject its default into the json, so a key nothing
321 // copies into this struct silently keeps whatever OptimizerParameters holds.
322 coarsen_pass = json_params["coarsen_pass"];
323 coarsen_unbounded = json_params["coarsen_unbounded"];
324 coarsen_local_smoothing_passes = json_params["coarsen_local_smoothing_passes"];
325 coarsen_smooth_ring = json_params["coarsen_smooth_ring"];
326 coarsen_global_smoothing_passes = json_params["coarsen_global_smoothing_passes"];
327 coarsen_max_rounds = json_params["coarsen_max_rounds"];
328 stuck_refine_stall_eps = json_params["stuck_refine_stall_eps"];
329 stuck_refine_cooldown = json_params["stuck_refine_cooldown"];
330 stuck_refine_num_worst = json_params["stuck_refine_num_worst"];
331 stuck_refine_rings = json_params["stuck_refine_rings"];
332 stuck_refine_factor = json_params["stuck_refine_factor"];
333 stuck_refine_min_scalar = json_params["stuck_refine_min_scalar"];
334 stuck_refine_gradation = json_params["stuck_refine_gradation"];
335 stuck_refine_force_split = json_params["stuck_refine_force_split"];
336 front_normal_projection = json_params["front_normal_projection"];
337 front_alignment_energy = json_params["front_alignment_energy"];
338 sizing_collapse_min = json_params["sizing_collapse_min"];
339 deform_others = json_params["deform_others"];
340 max_rounds = json_params["max_rounds"];
341 w_amips = json_params["w_amips"];
342 smoothing_mode = json_params["smoothing_mode"];
343 project_line_search_steps = json_params["project_line_search_steps"];
344 project_line_search_nested_steps = json_params["project_line_search_nested_steps"];
345 smooth_quality_veto = json_params["smooth_quality_veto"];
346 w_envelope = 1. - w_amips;
347 perform_sanity_checks = json_params["perform_sanity_checks"];
348 }
349
350 void init(const VectorXd& min_, const VectorXd& max_)
351 {
352 box_min = min_;
353 box_max = max_;
354
355 // Not a user knob: a topological offset preserves the topology of the region it wraps, so
356 // the shared collapse must always apply the substructure link condition, or a collapse
357 // across a thin band pinches the two sides together and the region stops being manifold.
358 // Set here because tetwild and simwild leave the flag off.
359 preserve_topology = true;
360
361 // Fills diag_l, l/lr and splitting_l2 / collapsing_l2. It also derives eps from epsr,
362 // which the offset never reads: its envelope tolerance is m_envelope_eps, set on the mesh.
363 init_lengths_from_diagonal((max_ - min_).norm());
364
365 // The split gate moved out to twice the collapse gate, so no half a split produces is a
366 // collapse candidate: splitting_l2 = (8/5 l)^2 against collapsing_l2 = (4/5 l)^2, which
367 // is left where it is. The overlapping TetWild gates (4/3 and 4/5), still the default, let
368 // the passes trade the same edges every turn -- see the declaration for the measurement.
370 splitting_l2 = l * l * (64 / 25.);
371 }
372
373 if (target_distance > 0) {
374 target_distance_rel = target_distance / diag_l;
375 } else {
376 target_distance = target_distance_rel * diag_l;
377 }
378
379 // An ordinary relative length: it bounds how far a region boundary may drift in space, so
380 // the bounding box is the right reference.
381 if (envelope_size > 0) {
382 envelope_size_rel = envelope_size / diag_l;
383 } else {
384 envelope_size = envelope_size_rel * diag_l;
385 }
386
387 // The convergence epsilon, the same absolute-or-relative pair as the envelope and
388 // against the same reference. It is a length in space, so the bounding box diagonal is
389 // the reference, not target_distance: tying the accuracy to the offset distance made
390 // every change of target_distance a silent change of accuracy as well.
391 if (front_conv > 0) {
392 front_conv_rel = front_conv / diag_l;
393 } else {
394 front_conv = front_conv_rel * diag_l;
395 }
396
397 // An epsilon coarser than the offset it measures decides nothing: every front vertex is
398 // "placed" and every face "resolved" from the first turn, whatever the offset looks like.
399 // Checked on the resolved ABSOLUTE values, so it catches the mistake whichever of the two
400 // forms the config used to state it.
401 if (front_conv > target_distance) {
402 log_and_throw_error(
403 "front_conv {} must be <= target_distance {}: the convergence epsilon cannot be "
404 "coarser than the offset distance it measures, or every front face reads as "
405 "resolved from the first turn",
406 front_conv,
407 target_distance);
408 }
409
410 // The operation leash, the same absolute-or-relative pair as the envelope and the
411 // convergence epsilon, against the same reference.
412 if (offset_envelope > 0) {
413 offset_envelope_rel = offset_envelope / diag_l;
414 } else {
415 offset_envelope = offset_envelope_rel * diag_l;
416 }
417
418 // l_min is relative to the offset distance rather than the bounding box: it is the offset
419 // that has to be resolved. See the declaration.
420 if (min_edge_length_rel < 0) {
421 // The envelope eps expressed as a multiple of target_distance, which is what this
422 // wants. offset_envelope_rel is a fraction of the BBOX DIAGONAL since 2026-09-24, so
423 // the conversion goes through the resolved absolute rather than being the identity it
424 // used to be -- the derived floor is unchanged in model units either way.
425 min_edge_length_rel =
426 std::max(offset_envelope / std::max(target_distance, 1e-16), 1e-12);
427 }
428 if (min_edge_length < 0) {
429 min_edge_length = min_edge_length_rel * target_distance;
430 } else {
431 min_edge_length_rel = min_edge_length / std::max(target_distance, 1e-16);
432 }
433 }
434};
435} // namespace wmtk::components::topological_offset
The parameters tetwild, triwild and simwild all share.
Definition OptimizerParameters.h:29
int project_line_search_nested_steps
Partial-projection bisections tried after it gives up; 0 disables that pass.
Definition OptimizerParameters.h:177
double w_amips
Definition OptimizerParameters.h:171
std::string smoothing_mode
"projected" or "exact"; see SmoothVertexOptions::SmoothingMode.
Definition OptimizerParameters.h:173
bool coarsen_pass
Definition OptimizerParameters.h:213
int coarsen_global_smoothing_passes
Definition OptimizerParameters.h:278
int coarsen_max_rounds
Definition OptimizerParameters.h:302
void init_lengths_from_diagonal(const double diag)
Derive the edge-length and envelope quantities from the bounding-box diagonal.
Definition OptimizerParameters.h:332
bool coarsen_unbounded
Definition OptimizerParameters.h:237
int split_high_valence_threshold
Definition OptimizerParameters.h:71
int coarsen_local_smoothing_passes
Definition OptimizerParameters.h:255
int project_line_search_steps
Bisections tried before the projected search gives up. See SmoothVertexOptions.
Definition OptimizerParameters.h:175
int coarsen_smooth_ring
Definition OptimizerParameters.h:271
bool smooth_quality_veto
Definition OptimizerParameters.h:183
int num_smoothing_passes
Number and placement of smoothing passes in the shared Wild optimization driver.
Definition OptimizerParameters.h:187
What the offset needs on top of the parameters every wmtk optimizer shares.
Definition Parameters.h:24
bool front_alignment_energy
Definition Parameters.h:107
bool sphere_trace_initialization
Definition Parameters.h:144
std::string front_conv_criterion
gradient_norm_rel | step_size_rel | decrement | residual_error [2D ONLY]
Definition Parameters.h:100
bool pre_smooth
Definition Parameters.h:236
bool experimental_aggresive_refine
Definition Parameters.h:170
std::string offset_field
"smooth" (Phi level set) or "euclidean" (exact distance)
Definition Parameters.h:54
double front_conv_frac() const
Definition Parameters.h:81
int max_iterations
Definition Parameters.h:195
bool sizing_collapse_min
Definition Parameters.h:111
bool experimental_nonoverlapping_gates
Definition Parameters.h:184
double adaptive_smoothing_stall_rel
front stalled: max ratio dropped by less than this
Definition Parameters.h:232
int max_rounds
Definition Parameters.h:117
bool optimize_offset
Definition Parameters.h:38
bool deform_others
Definition Parameters.h:114
double offset_envelope
absolute; < 0 means use offset_envelope_rel
Definition Parameters.h:202
bool adaptive_smoothing
Definition Parameters.h:230
int adaptive_smoothing_max_passes
cap on the passes per group
Definition Parameters.h:231
std::string front_measure
Definition Parameters.h:140
std::string sizing_gradation_mode
Definition Parameters.h:226
double debug_manual_dhat
Definition Parameters.h:53
double adaptive_smoothing_step_rel
Definition Parameters.h:233
double sphere_trace_target_rel_tol
Definition Parameters.h:145
bool experimental_consistent_construction_split
Definition Parameters.h:152