ViennaLS
Loading...
Searching...
No Matches
lsOxidationMask.hpp
Go to the documentation of this file.
1#pragma once
2
5
6#include <hrleSparseStarIterator.hpp>
7
8#include <algorithm>
9#include <array>
10#include <cmath>
11#include <omp.h>
12#include <stdexcept>
13#include <unordered_map>
14
15namespace viennals {
16
18 // Contact mode:
19 // 0 = kinematic: mask velocity at oxide-contact faces equals the solved
20 // oxide velocity (legacy Dirichlet BC). No traction computation.
21 // 1 = oneway: mask solved with oxide stress traction as Neumann BC
22 // (multigrid GMRES); oxide uses kinematic Dirichlet at mask faces.
23 // Stable; captures mask bending driven by oxide force. (Config
24 // aliases: "oneway", "traction", "1", "2".)
25 // 2 = elastic: multigrid-GMRES elastic solve with youngModulus and
26 // poissonRatio. Oxide contact prescribes elastic displacement
27 // (v_oxide × dt). The outer coupling loop in lsOxidation feeds the
28 // solved mask displacement back into the next oxide solve. (Config
29 // aliases: "elastic", "twoway" and variants, "3", "4".)
30 int contactMode = 1;
31 // Arrhenius creep viscosity law:
32 // eta(T) = eta_ref * exp(E/R * (1/T - 1/T_ref)).
33 // Viscosity is in Pa·hr, activation energy in J/mol, temperature in K.
34 double temperature = 1273.15;
35 double referenceTemperature = 1273.15;
36 double referenceViscosity = 5e8;
38 // Young's modulus for elastic contact mode (2), in Pa.
39 // Si₃N₄ at 1000 °C: ~250–270 GPa.
40 double youngModulus = 270e9;
41 // Current solve timestep in hours; set by lsOxidation before each apply().
42 // Elastic modes use it to scale oxide velocity to contact displacement
43 // (v * dt) and to convert the solved displacement back to advection velocity.
44 double stressTimeStep = 1;
45 double poissonRatio = 0.27;
46 // false = bonded mask/oxide interface (traction continuity in compression
47 // and tension); true = optional unilateral contact/release model.
48 bool unilateralContact = true;
49 // Outer Aitken relaxation factor applied to the mask/oxide coupling residual.
50 // Independent of the multigrid smoother below.
51 double relaxation = 1.;
52 // Under-relaxation for the unilateral contact load active set. A hard
53 // compressive/tensile switch makes the mask/oxide fixed point oscillate when
54 // contact faces release; relaxing the load keeps the complementarity limit
55 // but makes the iteration continuous.
56 double contactLoadRelaxation = 0.25;
57 // Relative pressure floor for releasing a relaxed contact face. Machine
58 // epsilon is far too small for Pa-scale contact stresses: a tensile face with
59 // a decaying old compressive load would otherwise remain "active" for many
60 // nonlinear iterations. The scale is the previous/current normal traction.
62 // SOR omega for the multigrid V-cycle smoother (both forward and backward
63 // sweeps). 1.0 is standard Gauss-Seidel; values in (1, 1.4] add over-
64 // relaxation. Do not share this with the Aitken relaxation above.
66 double tolerance = 1e-8;
67 // Minimum sub-grid boundary distance as a fraction of gridDelta. This is a
68 // mechanical derivative length scale, so keep it comparable to the oxide
69 // deformation solver; near-zero distances make elastic contact stresses blow
70 // up as E * u / d.
71 double minBoundaryDistance = 0.05;
72 unsigned maxIterations = 10000;
73 std::size_t maxGridPoints = 5000000;
74 int material = -1;
75 // Optional far-field clamp used to remove rigid-body mask drift and provide
76 // the support that makes mask thickness mechanically meaningful. A side of
77 // -1 clamps the lower index side, +1 clamps the upper side, and 0 disables
78 // the clamp. Direction defaults to x in a LOCOS cross-section.
82};
83
89template <class T, int D>
90class OxidationMaskBending final : public VelocityField<T>,
91 public OxidationSolverBase<T, D> {
92 using IndexType = viennahrle::Index<D>;
93 using ConstSparseIterator =
94 viennahrle::ConstSparseIterator<typename Domain<T, D>::DomainType>;
95
96private:
97 // bring base members into scope
114
115 struct Node {
116 IndexType index;
117 Vec3D<T> velocity{0., 0., 0.};
118 bool contact = false;
119 bool fixed = false;
120 };
121
122 struct SparseMatrix {
123 std::size_t nodeCount = 0;
124 std::vector<std::size_t> rowPtr;
125 std::vector<std::size_t> colIndex;
126 std::vector<T> values;
127 std::vector<T> invDiagonal;
128 };
129
130 struct MultigridLevel {
131 std::vector<IndexType> indices;
132 // For level l > 0, children maps each coarse node to level l-1 nodes.
133 std::vector<std::vector<std::size_t>> children;
134 std::vector<std::size_t> fineToCoarse;
135 SparseMatrix matrix;
136 };
137
138 SmartPointer<OxidationDeformation<T, D>> deformationField = nullptr;
139 SmartPointer<Domain<T, D>> maskInterface = nullptr;
140 SmartPointer<Domain<T, D>> ambientInterface = nullptr;
141 OxidationMaskParameters parameters;
142 int maskSign = 1;
143 int ambientSign = -1;
144
145 bool solved = false;
146 bool useRequestedBounds = false;
147 T residual = std::numeric_limits<T>::max();
148 unsigned iterations = 0;
149 std::array<T, D> maxVelocity_{};
150 std::size_t contactNodes = 0;
151 std::size_t fixedNodes = 0;
152 T lastApplyVelocityChange = std::numeric_limits<T>::max();
153 T lastApplyAbsoluteVelocityChange = std::numeric_limits<T>::max();
154 T aitkenOmega = 1.;
155 std::size_t candidateContactFaces_ = 0;
156 std::size_t activeContactFaces_ = 0;
157 std::size_t tensileContactFaces_ = 0;
158 T minContactNormalTraction_ = std::numeric_limits<T>::max();
159 T maxContactNormalTraction_ = std::numeric_limits<T>::lowest();
160 T contactReleaseThreshold_ = 0.;
161 std::unordered_map<std::size_t, Vec3D<T>> previousContactTraction_;
162 std::unordered_map<std::size_t, T> previousContactReleaseScale_;
163 std::vector<T> previousAitkenResidual;
164 // Elastic mode (contactMode==2): snapshot of u_new (elastic equilibrium
165 // displacement in µm, stored as µm/hr with implicit dt_ref=1 hr) taken by
166 // finalizeElasticAdvectionVelocity(). Used by writeFieldsToLevelSet() to
167 // write the "MaskVelocity" warm-start for the next substep's solver.
168 std::vector<Vec3D<T>> elasticU_;
169 IndexType requestedMinIndex{};
170 IndexType requestedMaxIndex{};
171 std::vector<Node> nodes;
172 // Face-major flat contact BC arrays: index = faceIdx * n + nodeId.
173 std::vector<uint8_t> contactFaceActive_; // 1 if contact BC applies
174 std::vector<Vec3D<T>> contactFaceVelocity_; // Dirichlet velocity at contact
175 std::vector<Vec3D<T>> contactFaceTraction_; // Oxide traction on mask face
176 std::vector<T> contactFaceDistance_; // Node-to-interface distance
177 std::unordered_map<IndexType, T, typename IndexType::hash> ambientPhiCache_;
178
179 // Cached multigrid hierarchy. The stiffness matrix depends on the node
180 // geometry and the contact face classification (active/inactive) but NOT on
181 // the traction magnitudes, which only affect the load vector b. When the
182 // contact pattern is unchanged from the previous apply() call, the hierarchy
183 // can be reused and the O(n) matrix build and Galerkin coarsening skipped.
184 std::vector<MultigridLevel> cachedMultigridLevels_;
185 std::vector<uint8_t> cachedContactFaceActive_; // snapshot at last build
186 std::size_t cachedNodeCount_ = 0;
187
188public:
190
192 SmartPointer<OxidationDeformation<T, D>> passedDeformation,
193 OxidationMaskParameters passedParameters = {})
194 : deformationField(passedDeformation), parameters(passedParameters) {}
195
197 SmartPointer<OxidationDeformation<T, D>> passedDeformation,
198 SmartPointer<Domain<T, D>> passedMaskInterface,
199 OxidationMaskParameters passedParameters = {}, int passedMaskSign = 1)
200 : deformationField(passedDeformation), maskInterface(passedMaskInterface),
201 parameters(passedParameters), maskSign((passedMaskSign < 0) ? -1 : 1) {}
202
204
205 static SmartPointer<OxidationMaskBending>
206 New(SmartPointer<OxidationDeformation<T, D>> passedDeformation,
207 OxidationMaskParameters passedParameters = {}) {
208 return SmartPointer<OxidationMaskBending>::New(passedDeformation,
209 passedParameters);
210 }
211
212 static SmartPointer<OxidationMaskBending>
213 New(SmartPointer<OxidationDeformation<T, D>> passedDeformation,
214 SmartPointer<Domain<T, D>> passedMaskInterface,
215 OxidationMaskParameters passedParameters = {}, int passedMaskSign = 1) {
216 return SmartPointer<OxidationMaskBending>::New(
217 passedDeformation, passedMaskInterface, passedParameters,
218 passedMaskSign);
219 }
220
221 void setMaskInterface(SmartPointer<Domain<T, D>> passedMaskInterface,
222 int passedMaskSign = 1) {
223 maskInterface = passedMaskInterface;
224 maskSign = (passedMaskSign < 0) ? -1 : 1;
225 solved = false;
226 }
227
232 void setAmbientInterface(SmartPointer<Domain<T, D>> passedAmbientInterface,
233 int passedAmbientSign = -1) {
234 ambientInterface = passedAmbientInterface;
235 ambientSign = (passedAmbientSign < 0) ? -1 : 1;
236 solved = false;
237 }
238
239 void setParameters(OxidationMaskParameters passedParameters) {
240 parameters = passedParameters;
241 solved = false;
242 }
243
244 void setSolveBounds(const IndexType &passedMinIndex,
245 const IndexType &passedMaxIndex) {
246 requestedMinIndex = passedMinIndex;
247 requestedMaxIndex = passedMaxIndex;
248 useRequestedBounds = true;
249 solved = false;
250 }
251
253 useRequestedBounds = false;
254 solved = false;
255 }
256
257 OxidationMaskParameters getParameters() const { return parameters; }
258 unsigned getIterations() const { return iterations; }
259 T getResidual() const { return residual; }
260 std::size_t getNumberOfSolutionNodes() const { return nodes.size(); }
261 std::size_t getNumberOfContactNodes() const { return contactNodes; }
262 std::size_t getNumberOfFixedNodes() const { return fixedNodes; }
263 T getLastApplyVelocityChange() const { return lastApplyVelocityChange; }
265 return lastApplyAbsoluteVelocityChange;
266 }
267
268 void apply() {
269 if (maskInterface == nullptr) {
270 solved = true;
271 return;
272 }
273 if (deformationField == nullptr) {
274 Logger::getInstance()
275 .addWarning("OxidationMaskBending: deformation field is null; "
276 "mask bending will produce zero velocities.")
277 .print();
278 solved = true;
279 return;
280 }
281
282 const auto previousVelocities = collectVelocitiesByGridPoint();
283 if (!initialiseGrid())
284 return; // base class already logged the error
285 elasticU_
286 .clear(); // reset before coupling so getVectorVelocity divides by dt
287 buildNodes();
288 seedFromLevelSet(); // warm-start velocity from previous substep's pointData
289 seedFromPrevious(previousVelocities);
290 if (nodes.empty()) {
291 Logger::getInstance()
292 .addWarning("OxidationMaskBending: no mask nodes found after "
293 "buildNodes(). Verify that the mask level set defines "
294 "a non-empty interior region within the solve bounds.")
295 .print();
296 solved = true;
297 return;
298 }
299 if (contactNodes == 0)
300 VIENNACORE_LOG_DEBUG(
301 "OxidationMaskBending: mask has no oxide-contact nodes; "
302 "no traction will be applied this step.");
303 solveElasticVelocity();
304 validateNodeVelocities("solveElasticVelocity");
305 smoothVelocityField();
306
307 const auto fixedPointResidual = contactResidualVector(previousVelocities);
308 const T omega = aitkenRelaxation(fixedPointResidual);
309 relaxVelocities(previousVelocities, omega);
310 validateNodeVelocities("Aitken relaxation");
311 lastApplyVelocityChange = maxVelocityChange(previousVelocities);
312 lastApplyAbsoluteVelocityChange =
313 absoluteVelocityChange(previousVelocities);
314
315 maxVelocity_.fill(T(0));
316 for (const auto &node : nodes) {
317 for (unsigned d = 0; d < D; ++d)
318 maxVelocity_[d] = std::max(maxVelocity_[d], std::abs(node.velocity[d]));
319 }
320 VIENNACORE_LOG_DEBUG(
321 "OxidationMaskBending: contact faces active=" +
322 std::to_string(activeContactFaces_) + "/" +
323 std::to_string(candidateContactFaces_) + ", tensileSkipped=" +
324 std::to_string(tensileContactFaces_) + ", normalTraction=[" +
325 std::to_string(
326 candidateContactFaces_ == 0 ? T(0) : minContactNormalTraction_) +
327 ", " +
328 std::to_string(
329 candidateContactFaces_ == 0 ? T(0) : maxContactNormalTraction_) +
330 "], aitkenOmega=" + std::to_string(omega) + ", contactLoadRelaxation=" +
331 std::to_string(
332 std::clamp(parameters.contactLoadRelaxation, T(0.02), T(1))) +
333 ", contactReleaseThreshold=" +
334 std::to_string(contactReleaseThreshold_) + ", residual=" +
335 std::to_string(lastApplyVelocityChange) + ", absVelocityChange=" +
336 std::to_string(lastApplyAbsoluteVelocityChange));
337
338 solved = true;
339 }
340
341 // Called from lsOxidation after writeFieldsToLevelSet() and before advect().
342 //
343 // Elastic mode (2): the contact faces use kinematic (Dirichlet) BC so the
344 // mask stays bonded to the oxide surface. The solver output u_new is the
345 // elastic bending displacement (in µm, stored as µm/hr with implicit
346 // dt_ref=1 hr). Convert to advection velocity u_new/dt and update
347 // maxVelocity_ for CFL subcycling. No-op for viscous modes.
349 if (!isElasticContactMode() || nodes.empty())
350 return;
351 const T dt = parameters.stressTimeStep;
352 if (dt <= T(0)) {
353 for (auto &node : nodes)
354 node.velocity = {T(0), T(0), T(0)};
355 return;
356 }
357
358 const std::size_t n = nodes.size();
359 elasticU_.resize(n);
360 for (std::size_t i = 0; i < n; ++i)
361 elasticU_[i] = nodes[i].velocity;
362
363 T maxUNew = T(0);
364 for (std::size_t i = 0; i < n; ++i)
365 for (unsigned d = 0; d < D; ++d)
366 maxUNew = std::max(maxUNew, std::abs(elasticU_[i][d]));
367
368 const T invDt = T(1) / dt;
369 for (std::size_t i = 0; i < n; ++i)
370 for (unsigned d = 0; d < D; ++d)
371 nodes[i].velocity[d] = elasticU_[i][d] * invDt;
372
373 VIENNACORE_LOG_INFO("ElasticFinalize: dt=" + std::to_string(dt) +
374 " max|u_new|=" + std::to_string(maxUNew) +
375 " max|u_new/dt|=" + std::to_string(maxUNew * invDt));
376
377 maxVelocity_.fill(T(0));
378 for (const auto &node : nodes)
379 for (unsigned d = 0; d < D; ++d)
380 maxVelocity_[d] = std::max(maxVelocity_[d], std::abs(node.velocity[d]));
381 }
382
383 Vec3D<T> getVectorVelocity(const Vec3D<T> &coordinate, int material,
384 const Vec3D<T> & /*normalVector*/,
385 unsigned long /*pointId*/) final {
386 if (deformationField == nullptr)
387 return {0., 0., 0.};
388
389 if (parameters.material >= 0 && material != parameters.material)
390 return {0., 0., 0.};
391
392 if (!solved)
393 apply();
394
395 if (maskInterface == nullptr || nodes.empty())
396 return {0., 0., 0.};
397
398 IndexType index;
399 for (unsigned i = 0; i < D; ++i)
400 index[i] = std::llround(coordinate[i] / gridDelta);
401
402 // Elastic mode: the solver stores u_new (displacement in µm) as if it were
403 // a velocity (µm/hr, with implicit dt_ref=1hr). The oxide needs the actual
404 // advection velocity u_new/dt. Before finalization elasticU_ is empty and
405 // nodes[i].velocity = u_new; we divide by dt here. After finalization
406 // nodes[i].velocity = u_new/dt already, so we return it directly.
407 if (isElasticContactMode() && elasticU_.empty()) {
408 const T dt = parameters.stressTimeStep;
409 if (dt <= T(0))
410 return {T(0), T(0), T(0)};
411 return getVelocity(index) / dt;
412 }
413
414 return getVelocity(index);
415 }
416
417 T getDissipationAlpha(int direction, int /*material*/,
418 const Vec3D<T> & /*centralDifferences*/) final {
419 return maxVelocity_[direction];
420 }
421
425 if (nodes.empty() || maskInterface == nullptr)
426 return;
427
428 using VD = typename PointData<T>::VectorDataType;
429
430 // Elastic mode: after finalization, elasticU_ holds u_new (the displacement
431 // in µm, stored as µm/hr). Use it as the "MaskVelocity" warm-start so the
432 // next substep's solver begins near its expected solution.
433 // Before finalization (elasticU_ empty), fall back to nodes[nId].velocity
434 // which still equals u_new from the current solve.
435 const bool useElasticU = isElasticContactMode() && !elasticU_.empty();
436
437 VD velocity;
438 ConstSparseIterator it(maskInterface->getDomain());
439 for (; !it.isFinished(); ++it) {
440 if (!it.isDefined())
441 continue;
442 const std::size_t nId = lookupNode(it.getStartIndices());
443 Vec3D<T> v{T(0), T(0), T(0)};
444 if (nId != noNode)
445 v = (useElasticU && nId < elasticU_.size()) ? elasticU_[nId]
446 : nodes[nId].velocity;
447 velocity.push_back(v);
448 }
449 maskInterface->getPointData().insertReplaceVectorData(std::move(velocity),
450 "MaskVelocity");
451
452 // Cauchy stress tensor in the mask: σ = λ(∇·u)I + 2μ·sym(∇u)
453 // In elastic mode use the raw displacement u_new (elasticU_); in viscous
454 // mode use the velocity field — both give the correct stress via the same
455 // Lamé constants (which already encode the physical units).
456 const T lambda = lameLambda();
457 const T mu = lameMu();
458
459 std::vector<Vec3D<T>> velForStress(nodes.size());
460 for (std::size_t i = 0; i < nodes.size(); ++i)
461 velForStress[i] = (useElasticU && i < elasticU_.size())
462 ? elasticU_[i]
463 : nodes[i].velocity;
464
465 VD stressR0, stressR1, stressR2;
466 for (ConstSparseIterator sit(maskInterface->getDomain()); !sit.isFinished();
467 ++sit) {
468 if (!sit.isDefined())
469 continue;
470 const std::size_t nId = lookupNode(sit.getStartIndices());
471 Vec3D<T> row0{T(0), T(0), T(0)};
472 Vec3D<T> row1{T(0), T(0), T(0)};
473 Vec3D<T> row2{T(0), T(0), T(0)};
474 if (nId != noNode) {
475 // grad[j][i] = ∂v_i/∂x_j via central differences
476 std::array<Vec3D<T>, 3> grad{};
477 for (unsigned j = 0; j < static_cast<unsigned>(D); ++j) {
478 const auto vp = simpleNeighborVelocity(velForStress, nId, j, +1);
479 const auto vm = simpleNeighborVelocity(velForStress, nId, j, -1);
480 for (unsigned i = 0; i < 3; ++i)
481 grad[j][i] = (vp[i] - vm[i]) / (T(2) * gridDelta);
482 }
483 T div = T(0);
484 for (unsigned i = 0; i < static_cast<unsigned>(D); ++i)
485 div += grad[i][i];
486 // σ_ij = λ·div·δ_ij + μ·(∂v_i/∂x_j + ∂v_j/∂x_i)
487 row0[0] = lambda * div + T(2) * mu * grad[0][0];
488 row0[1] = mu * (grad[1][0] + grad[0][1]);
489 row0[2] = (D > 2) ? mu * (grad[2][0] + grad[0][2]) : T(0);
490 row1[0] = row0[1];
491 row1[1] = lambda * div + T(2) * mu * grad[1][1];
492 row1[2] = (D > 2) ? mu * (grad[2][1] + grad[1][2]) : T(0);
493 row2[0] = row0[2];
494 row2[1] = row1[2];
495 row2[2] = (D > 2) ? lambda * div + T(2) * mu * grad[2][2] : T(0);
496 }
497 stressR0.push_back(row0);
498 stressR1.push_back(row1);
499 stressR2.push_back(row2);
500 }
501 maskInterface->getPointData().insertReplaceVectorData(std::move(stressR0),
502 "MaskStressR0");
503 maskInterface->getPointData().insertReplaceVectorData(std::move(stressR1),
504 "MaskStressR1");
505 maskInterface->getPointData().insertReplaceVectorData(std::move(stressR2),
506 "MaskStressR2");
507 }
508
509private:
510 bool initialiseGrid() {
512 maskInterface, useRequestedBounds, requestedMinIndex, requestedMaxIndex,
513 parameters.maxGridPoints, "OxidationMaskBending");
514 }
515
518 void seedFromLevelSet() {
519 if (maskInterface == nullptr || nodes.empty())
520 return;
521 const int vIdx =
522 maskInterface->getPointData().getVectorDataIndex("MaskVelocity");
523 if (vIdx == -1)
524 return;
525 const auto *vd = maskInterface->getPointData().getVectorData(vIdx);
526 if (vd == nullptr)
527 return;
528
529 ConstSparseIterator it(maskInterface->getDomain());
530 for (; !it.isFinished(); ++it) {
531 if (!it.isDefined())
532 continue;
533 const auto ptId = it.getPointId();
534 if (ptId >= static_cast<decltype(ptId)>(vd->size()))
535 continue;
536 const std::size_t nId = lookupNode(it.getStartIndices());
537 if (nId == noNode)
538 continue;
539 if (!isFinite((*vd)[ptId]))
540 throwNonFinite("stored mask velocity point data");
541 else
542 nodes[nId].velocity = (*vd)[ptId];
543 }
544 }
545
546 void
547 seedFromPrevious(const std::unordered_map<std::size_t, Vec3D<T>> &previous) {
548 if (previous.empty())
549 return;
550 for (auto &node : nodes) {
551 const auto found = previous.find(linearIndex(node.index));
552 if (found != previous.end() && isFinite(found->second))
553 node.velocity = found->second;
554 if (node.fixed)
555 node.velocity = {T(0), T(0), T(0)};
556 }
557 }
558
559 std::unordered_map<std::size_t, Vec3D<T>>
560 collectVelocitiesByGridPoint() const {
561 std::unordered_map<std::size_t, Vec3D<T>> result;
562 result.reserve(nodes.size());
563 for (const auto &node : nodes)
564 if (inBounds(node.index) && isFinite(node.velocity))
565 result.emplace(linearIndex(node.index), node.velocity);
566 return result;
567 }
568
569 static bool isFinite(const Vec3D<T> &v) {
570 for (unsigned i = 0; i < 3; ++i)
571 if (!std::isfinite(v[i]))
572 return false;
573 return true;
574 }
575
576 static bool isFiniteTensor(const std::array<T, 9> &tensor) {
577 for (T value : tensor)
578 if (!std::isfinite(value))
579 return false;
580 return true;
581 }
582
583 [[noreturn]] void throwNonFinite(const std::string &stage) const {
584 const std::string message =
585 "OxidationMaskBending: " + stage + " produced non-finite values.";
586 Logger::getInstance().addError(message).print();
587 throw std::runtime_error(message);
588 }
589
590 void validateNodeVelocities(const std::string &stage) const {
591 for (const auto &node : nodes)
592 for (unsigned i = 0; i < D; ++i)
593 if (!std::isfinite(node.velocity[i]))
594 throwNonFinite(stage);
595 }
596
597 T maxVelocityChange(
598 const std::unordered_map<std::size_t, Vec3D<T>> &previous) const {
599 if (previous.empty())
600 return std::numeric_limits<T>::max();
601
602 T changeSquaredSum = 0.;
603 T magnitudeSquaredSum = std::numeric_limits<T>::epsilon();
604 for (const auto &node : nodes) {
605 if (!node.contact)
606 continue;
607 const auto found = previous.find(linearIndex(node.index));
608 if (found == previous.end())
609 continue;
610
611 for (unsigned i = 0; i < D; ++i) {
612 const T delta = node.velocity[i] - found->second[i];
613 changeSquaredSum += delta * delta;
614 magnitudeSquaredSum += node.velocity[i] * node.velocity[i];
615 magnitudeSquaredSum += found->second[i] * found->second[i];
616 }
617 }
618
619 const T change = std::sqrt(changeSquaredSum / magnitudeSquaredSum);
620 if (!std::isfinite(change))
621 throwNonFinite("mask velocity coupling residual");
622 return change;
623 }
624
625 T absoluteVelocityChange(
626 const std::unordered_map<std::size_t, Vec3D<T>> &previous) const {
627 if (previous.empty())
628 return std::numeric_limits<T>::max();
629
630 T changeSquaredSum = 0.;
631 std::size_t components = 0;
632 for (const auto &node : nodes) {
633 if (!node.contact)
634 continue;
635 const auto found = previous.find(linearIndex(node.index));
636 if (found == previous.end())
637 continue;
638
639 for (unsigned i = 0; i < D; ++i) {
640 const T delta = node.velocity[i] - found->second[i];
641 if (!std::isfinite(delta))
642 throwNonFinite("mask absolute velocity coupling residual");
643 changeSquaredSum += delta * delta;
644 ++components;
645 }
646 }
647
648 if (components == 0)
649 return std::numeric_limits<T>::max();
650 const T change = std::sqrt(changeSquaredSum / static_cast<T>(components));
651 if (!std::isfinite(change))
652 throwNonFinite("mask absolute velocity coupling residual");
653 return change;
654 }
655
656 std::vector<T> contactResidualVector(
657 const std::unordered_map<std::size_t, Vec3D<T>> &previous) const {
658 std::vector<T> residualVector;
659 residualVector.reserve(contactNodes * D);
660 if (previous.empty())
661 return residualVector;
662
663 for (const auto &node : nodes) {
664 if (!node.contact)
665 continue;
666 const auto found = previous.find(linearIndex(node.index));
667 if (found == previous.end())
668 continue;
669 for (unsigned i = 0; i < D; ++i) {
670 const T residualComponent = node.velocity[i] - found->second[i];
671 if (!std::isfinite(residualComponent))
672 throwNonFinite("mask contact residual");
673 residualVector.push_back(residualComponent);
674 }
675 }
676 return residualVector;
677 }
678
679 T aitkenRelaxation(const std::vector<T> &residualVector) {
680 const T baseOmega = std::clamp(parameters.relaxation, T(0.01), T(1));
681 if (residualVector.empty()) {
682 previousAitkenResidual.clear();
683 aitkenOmega = baseOmega;
684 return 1.;
685 }
686
687 T omega = std::clamp(aitkenOmega, T(0.01), baseOmega);
688 if (previousAitkenResidual.size() != residualVector.size())
689 omega = baseOmega;
690 if (previousAitkenResidual.size() == residualVector.size()) {
691 T numerator = 0.;
692 T denominator = 0.;
693 T residualNorm2 = 0.;
694 T previousNorm2 = 0.;
695 for (std::size_t i = 0; i < residualVector.size(); ++i) {
696 if (!std::isfinite(residualVector[i]) ||
697 !std::isfinite(previousAitkenResidual[i]))
698 throwNonFinite("mask Aitken residual");
699 const T delta = residualVector[i] - previousAitkenResidual[i];
700 numerator += previousAitkenResidual[i] * delta;
701 denominator += delta * delta;
702 residualNorm2 += residualVector[i] * residualVector[i];
703 previousNorm2 += previousAitkenResidual[i] * previousAitkenResidual[i];
704 }
705
706 if (!std::isfinite(numerator) || !std::isfinite(denominator))
707 throwNonFinite("mask Aitken coefficient");
708
709 if (denominator > std::numeric_limits<T>::epsilon()) {
710 omega = -aitkenOmega * numerator / denominator;
711 if (std::isfinite(omega)) {
712 // Traction contact mode: cap at 1.0 (no extrapolation). The
713 // compressive/tensile contact state can alternate across coupling
714 // iterations, and omega > 1 extrapolates through the fixed point
715 // for those nodes, producing sign-alternating velocities that
716 // appear as zigzag kinks after level-set advection.
717 const T omegaMax = (parameters.contactMode > 0) ? baseOmega : T(1.5);
718 omega = std::clamp(omega, T(0.05), omegaMax);
719 } else {
720 throwNonFinite("mask Aitken coefficient");
721 }
722 }
723
724 if (std::isfinite(residualNorm2) && std::isfinite(previousNorm2) &&
725 residualNorm2 > previousNorm2 * T(1.05)) {
726 omega = std::min(omega, std::max(T(0.05), aitkenOmega * T(0.5)));
727 }
728 }
729
730 previousAitkenResidual = residualVector;
731 aitkenOmega = omega;
732 return omega;
733 }
734
735 // One pass of Laplacian (neighbour-average) smoothing on the mask velocity
736 // field. Damps grid-scale oscillations that arise near the contact-to-free
737 // boundary transition without materially changing the bulk velocity.
738 // Fixed (anchor) nodes are skipped — they must stay at zero.
739 void smoothVelocityField() {
740 const std::size_t n = nodes.size();
741 if (n == 0)
742 return;
743
744 std::vector<Vec3D<T>> smoothed(n);
745 for (std::size_t id = 0; id < n; ++id) {
746 if (nodes[id].fixed) {
747 smoothed[id] = {T(0), T(0), T(0)};
748 continue;
749 }
750 Vec3D<T> sum = nodes[id].velocity;
751 int count = 1;
752 for (unsigned dir = 0; dir < D; ++dir) {
753 for (int off : {-1, 1}) {
754 IndexType nb = nodes[id].index;
755 nb[dir] += off;
756 if (!inBounds(nb))
757 continue;
758 const std::size_t nbId = nodeLookupFlat[linearIndex(nb)];
759 if (nbId == noNode)
760 continue;
761 for (unsigned c = 0; c < D; ++c)
762 sum[c] += nodes[nbId].velocity[c];
763 ++count;
764 }
765 }
766 const T w = T(1) / static_cast<T>(count);
767 for (unsigned c = 0; c < D; ++c)
768 smoothed[id][c] = sum[c] * w;
769 }
770
771 for (std::size_t id = 0; id < n; ++id)
772 nodes[id].velocity = smoothed[id];
773 }
774
775 void
776 relaxVelocities(const std::unordered_map<std::size_t, Vec3D<T>> &previous,
777 T omega) {
778 if (!std::isfinite(omega))
779 throwNonFinite("mask Aitken coefficient");
780 if (previous.empty() || omega == T(1))
781 return;
782
783 for (auto &node : nodes) {
784 const auto found = previous.find(linearIndex(node.index));
785 if (found == previous.end())
786 continue;
787 for (unsigned i = 0; i < D; ++i) {
788 const T relaxed =
789 found->second[i] + omega * (node.velocity[i] - found->second[i]);
790 if (!std::isfinite(relaxed))
791 throwNonFinite("mask velocity relaxation");
792 node.velocity[i] = relaxed;
793 }
794 }
795 }
796
797 void buildAmbientPhiCache() {
798 ambientPhiCache_.clear();
799 if (ambientInterface == nullptr)
800 return;
801 for (ConstSparseIterator it(ambientInterface->getDomain());
802 !it.isFinished(); ++it) {
803 if (!it.isDefined())
804 continue;
805 ambientPhiCache_[it.getStartIndices()] =
806 static_cast<T>(ambientSign) * it.getValue();
807 }
808 }
809
810 void buildNodes() {
811 auto oldContactTraction = std::move(previousContactTraction_);
812 auto oldContactReleaseScale = std::move(previousContactReleaseScale_);
813 previousContactTraction_.clear();
814 previousContactReleaseScale_.clear();
815 contactReleaseThreshold_ = T(0);
816 nodes.clear();
818 buildAmbientPhiCache();
819
820 ConstSparseIterator maskIt(maskInterface->getDomain());
821 IndexType index = minIndex;
822 while (true) {
823 if (isInsideMask(maskIt, index)) {
824 const std::size_t id = nodes.size();
825 nodeLookupFlat[linearIndex(index)] = id;
826 nodes.push_back({index});
827 }
828
829 if (!increment(index))
830 break;
831 }
832
833 const std::size_t n = nodes.size();
834 contactFaceActive_.assign(2 * D * n, uint8_t(0));
835 contactFaceVelocity_.assign(2 * D * n, Vec3D<T>{T(0), T(0), T(0)});
836 contactFaceTraction_.assign(2 * D * n, Vec3D<T>{T(0), T(0), T(0)});
837 contactFaceDistance_.assign(2 * D * n, gridDelta);
838
839 contactNodes = 0;
840 candidateContactFaces_ = 0;
841 activeContactFaces_ = 0;
842 tensileContactFaces_ = 0;
843 minContactNormalTraction_ = std::numeric_limits<T>::max();
844 maxContactNormalTraction_ = std::numeric_limits<T>::lowest();
845 for (std::size_t id = 0; id < n; ++id) {
846 auto &node = nodes[id];
847 node.contact = touchesContactBoundary(maskIt, node.index);
848 if (node.contact)
849 ++contactNodes;
850
851 for (unsigned dir = 0; dir < D; ++dir) {
852 for (int offset : {-1, 1}) {
853 const unsigned faceIdx = dir * 2u + (offset == 1 ? 1u : 0u);
854 IndexType neighbor = node.index;
855 neighbor[dir] += offset;
856 const bool neighborIsNode =
857 inBounds(neighbor) && lookupNode(neighbor) != noNode;
858 if (neighborIsNode ||
859 !isContactBoundary(node.index, dir, offset, maskIt))
860 continue; // inactive already set by assign()
861
862 const T faceDistance = maskFaceDistance(maskIt, node.index, neighbor);
863 ++candidateContactFaces_;
864 Vec3D<T> faceNormal{0., 0., 0.};
865 faceNormal[dir] = static_cast<T>(offset);
866 Vec3D<T> oxidePt{0., 0., 0.};
867 for (unsigned i = 0; i < D; ++i)
868 oxidePt[i] = node.index[i] * gridDelta;
869 oxidePt[dir] += offset * gridDelta;
870
871 const auto sigma = deformationField->getStressTensor(oxidePt);
872 if (!isFiniteTensor(sigma))
873 throwNonFinite("oxide stress at mask contact");
874 Vec3D<T> t{0., 0., 0.};
875 for (unsigned i = 0; i < D; ++i)
876 for (unsigned j = 0; j < D; ++j)
877 t[i] += sigma[3 * i + j] * faceNormal[j];
878
879 T tn = 0.;
880 for (unsigned i = 0; i < D; ++i)
881 tn += t[i] * faceNormal[i];
882
883 if (!isFinite(t) || !std::isfinite(tn))
884 throwNonFinite("oxide traction at mask contact");
885 minContactNormalTraction_ = std::min(minContactNormalTraction_, tn);
886 maxContactNormalTraction_ = std::max(maxContactNormalTraction_, tn);
887
888 Vec3D<T> contactLoad = t;
889 if (parameters.unilateralContact && tn >= T(0)) {
890 ++tensileContactFaces_;
891 contactLoad = {T(0), T(0), T(0)};
892 }
893
894 if (parameters.contactMode > 0) {
895 const auto key = contactFaceKey(node.index, dir, offset);
896 const auto prevIt = oldContactTraction.find(key);
897 if (prevIt != oldContactTraction.end()) {
898 const T alpha =
899 std::clamp(parameters.contactLoadRelaxation, T(0.02), T(1));
900 for (unsigned c = 0; c < D; ++c)
901 contactLoad[c] = prevIt->second[c] +
902 alpha * (contactLoad[c] - prevIt->second[c]);
903 }
904 }
905
906 T loadNormal = T(0);
907 for (unsigned i = 0; i < D; ++i)
908 loadNormal += contactLoad[i] * faceNormal[i];
909
910 T releaseThreshold = std::numeric_limits<T>::epsilon();
911 T releaseScale = std::max(std::abs(tn), T(1));
912 if (parameters.contactMode > 0 && parameters.unilateralContact) {
913 const auto scaleIt = oldContactReleaseScale.find(
914 contactFaceKey(node.index, dir, offset));
915 if (scaleIt != oldContactReleaseScale.end())
916 releaseScale = std::max(releaseScale, scaleIt->second);
917 releaseThreshold =
918 std::clamp(parameters.contactReleaseFraction, T(0), T(0.25)) *
919 releaseScale;
920 contactReleaseThreshold_ =
921 std::max(contactReleaseThreshold_, releaseThreshold);
922 }
923
924 if (parameters.unilateralContact && loadNormal >= -releaseThreshold)
925 continue;
926
927 if (!isFinite(contactLoad))
928 throwNonFinite("relaxed oxide contact load");
929
930 if (parameters.contactMode > 0) {
931 const auto key = contactFaceKey(node.index, dir, offset);
932 previousContactTraction_[key] = contactLoad;
933 if (parameters.unilateralContact)
934 previousContactReleaseScale_[key] =
935 std::max(releaseScale, std::abs(loadNormal));
936 }
937
938 contactFaceActive_[faceIdx * n + id] = uint8_t(1);
939 ++activeContactFaces_;
940 contactFaceTraction_[faceIdx * n + id] = contactLoad;
941 contactFaceDistance_[faceIdx * n + id] = faceDistance;
942 if (usesKinematicContactBoundary()) {
943 auto oxVel = deformationField->getVectorVelocity(
944 oxidePt, parameters.material, faceNormal, 0);
945 if (isElasticContactMode()) {
946 // Scale by dt: elastic solver uses dt_ref=1hr so the Dirichlet BC
947 // must be the physical displacement (v_oxide × dt), not the
948 // velocity.
949 oxVel = oxVel * parameters.stressTimeStep;
950 }
951 contactFaceVelocity_[faceIdx * n + id] = oxVel;
952 }
953 }
954 }
955 }
956 markFixedNodes();
957 }
958
959 std::size_t contactFaceKey(const IndexType &index, unsigned direction,
960 int offset) const {
961 std::size_t seed = typename IndexType::hash{}(index);
962 seed ^= std::hash<unsigned>{}(direction) +
963 std::size_t(0x9e3779b97f4a7c15ULL) + (seed << 6) + (seed >> 2);
964 seed ^= std::hash<int>{}(offset) + std::size_t(0x9e3779b97f4a7c15ULL) +
965 (seed << 6) + (seed >> 2);
966 return seed;
967 }
968
969 // Returns neighbor velocity without ghost extrapolation.
970 // All arithmetic is in T; reads from the SolverT work vector are widened.
971 template <class SolverT>
972 Vec3D<T> simpleNeighborVelocity(const std::vector<Vec3D<SolverT>> &velocity,
973 std::size_t nodeId, unsigned direction,
974 int offset) const {
975 IndexType neighbor = nodes[nodeId].index;
976 neighbor[direction] += offset;
977 const auto toT = [](const Vec3D<SolverT> &v) -> Vec3D<T> {
978 return {static_cast<T>(v[0]), static_cast<T>(v[1]), static_cast<T>(v[2])};
979 };
980 if (!inBounds(neighbor))
981 return toT(velocity[nodeId]);
982 const std::size_t foundId = nodeLookupFlat[linearIndex(neighbor)];
983 return (foundId != noNode) ? toT(velocity[foundId]) : toT(velocity[nodeId]);
984 }
985
986 // Ghost velocity enforcing sigma_mask * n = traction at a mask boundary face.
987 template <class SolverT>
988 Vec3D<T> stressBoundaryGhost(const std::vector<Vec3D<SolverT>> &velocity,
989 std::size_t nodeId, unsigned normalDir,
990 int offset, T distance,
991 const Vec3D<T> &traction) const {
992 const T lambda = lameLambda();
993 const T mu = lameMu();
994 const T denom =
995 std::max(lambda + T(2) * mu, std::numeric_limits<T>::epsilon());
996 const T signedOffset = static_cast<T>(offset);
997 Vec3D<T> ghost{static_cast<T>(velocity[nodeId][0]),
998 static_cast<T>(velocity[nodeId][1]),
999 static_cast<T>(velocity[nodeId][2])};
1000 T tangentialDivergence = T(0);
1001 std::array<T, D> normalTangentialDerivative{};
1002 for (unsigned tanDir = 0; tanDir < D; ++tanDir) {
1003 if (tanDir == normalDir)
1004 continue;
1005 const Vec3D<T> vPlus =
1006 simpleNeighborVelocity(velocity, nodeId, tanDir, +1);
1007 const Vec3D<T> vMinus =
1008 simpleNeighborVelocity(velocity, nodeId, tanDir, -1);
1009 tangentialDivergence +=
1010 (vPlus[tanDir] - vMinus[tanDir]) / (T(2) * gridDelta);
1011 normalTangentialDerivative[tanDir] =
1012 (vPlus[normalDir] - vMinus[normalDir]) / (T(2) * gridDelta);
1013 }
1014
1015 const T normalTraction = traction[normalDir] * signedOffset;
1016 const T normalDerivative =
1017 (normalTraction - lambda * tangentialDivergence) / denom;
1018 ghost[normalDir] += signedOffset * distance * normalDerivative;
1019
1020 for (unsigned tanDir = 0; tanDir < D; ++tanDir) {
1021 if (tanDir == normalDir)
1022 continue;
1023 const T normalDerivativeTangential =
1024 signedOffset * traction[tanDir] /
1025 std::max(mu, std::numeric_limits<T>::epsilon()) -
1026 normalTangentialDerivative[tanDir];
1027 ghost[tanDir] += signedOffset * distance * normalDerivativeTangential;
1028 }
1029 return ghost;
1030 }
1031
1032 template <class SolverT>
1033 Vec3D<T> tractionFreeGhost(const std::vector<Vec3D<SolverT>> &velocity,
1034 std::size_t nodeId, unsigned normalDir,
1035 int offset) const {
1036 return stressBoundaryGhost(velocity, nodeId, normalDir, offset, gridDelta,
1037 Vec3D<T>{T(0), T(0), T(0)});
1038 }
1039
1040 template <class SolverT>
1041 Vec3D<T> neighborVelocity(const std::vector<Vec3D<SolverT>> &velocity,
1042 std::size_t nodeId, unsigned direction,
1043 int offset) const {
1044 IndexType neighbor = nodes[nodeId].index;
1045 neighbor[direction] += offset;
1046
1047 if (inBounds(neighbor)) {
1048 const std::size_t foundId = nodeLookupFlat[linearIndex(neighbor)];
1049 if (foundId != noNode)
1050 return {static_cast<T>(velocity[foundId][0]),
1051 static_cast<T>(velocity[foundId][1]),
1052 static_cast<T>(velocity[foundId][2])};
1053 }
1054
1055 const unsigned faceIdx = direction * 2u + (offset == 1 ? 1u : 0u);
1056 const std::size_t nn = nodes.size();
1057 if (contactFaceActive_[faceIdx * nn + nodeId]) {
1058 if (usesKinematicContactBoundary())
1059 return contactFaceVelocity_[faceIdx * nn + nodeId] * T(2) -
1060 Vec3D<T>{static_cast<T>(velocity[nodeId][0]),
1061 static_cast<T>(velocity[nodeId][1]),
1062 static_cast<T>(velocity[nodeId][2])};
1063
1064 return stressBoundaryGhost(velocity, nodeId, direction, offset,
1065 contactFaceDistance_[faceIdx * nn + nodeId],
1066 contactFaceTraction_[faceIdx * nn + nodeId]);
1067 }
1068
1069 return tractionFreeGhost(velocity, nodeId, direction, offset);
1070 }
1071
1072 template <class SolverT>
1073 Vec3D<T> divergenceGradient(const std::vector<Vec3D<SolverT>> &velocity,
1074 const IndexType &index) const {
1075 Vec3D<T> gradient{T(0), T(0), T(0)};
1076 for (unsigned component = 0; component < D; ++component) {
1077 IndexType plus = index;
1078 IndexType minus = index;
1079 plus[component] += 1;
1080 minus[component] -= 1;
1081 const T plusDiv = divergence(velocity, plus);
1082 const T minusDiv = divergence(velocity, minus);
1083 gradient[component] = (plusDiv - minusDiv) / (T(2) * gridDelta);
1084 }
1085 return gradient;
1086 }
1087
1088 template <class SolverT>
1089 T divergence(const std::vector<Vec3D<SolverT>> &velocity,
1090 const IndexType &index) const {
1091 if (!inBounds(index))
1092 return T(0);
1093 const std::size_t nodeId = nodeLookupFlat[linearIndex(index)];
1094 if (nodeId == noNode)
1095 return T(0);
1096
1097 T result = T(0);
1098 for (unsigned direction = 0; direction < D; ++direction) {
1099 const Vec3D<T> plus = neighborVelocity(velocity, nodeId, direction, 1);
1100 const Vec3D<T> minus = neighborVelocity(velocity, nodeId, direction, -1);
1101 result += (plus[direction] - minus[direction]) / (T(2) * gridDelta);
1102 }
1103 return result;
1104 }
1105
1106 // Evaluates the elastic stencil F(v)[i] = laplaceAverage + gradDivCorrection.
1107 // (A·v)[i] = v[i] - F(v)[i] + b[i], where b[i] = F(0)[i] (contact BC
1108 // constants).
1109 template <class SolverT>
1110 Vec3D<T> computeElasticStencilAt(std::size_t nodeId,
1111 const std::vector<Vec3D<SolverT>> &v,
1112 T gradDivWeight) const {
1113 Vec3D<T> laplaceAverage{T(0), T(0), T(0)};
1114 for (unsigned direction = 0; direction < D; ++direction)
1115 for (int offset : {-1, 1})
1116 laplaceAverage =
1117 laplaceAverage + neighborVelocity(v, nodeId, direction, offset);
1118
1119 const T count = static_cast<T>(2 * D);
1120 const Vec3D<T> lapAvg = laplaceAverage / count;
1121 const Vec3D<T> gradDivCorr =
1122 divergenceGradient(v, nodes[nodeId].index) *
1123 (gradDivWeight * gridDelta * gridDelta / (T(2) * static_cast<T>(D)));
1124
1125 return lapAvg + gradDivCorr;
1126 }
1127
1128 // (Av)[i] = v[i] - F(v)[i] + b[i], stored as SolverT.
1129 template <class SolverT>
1130 void elasticMatvec(const std::vector<Vec3D<SolverT>> &v,
1131 const std::vector<Vec3D<T>> &b, T gradDivWeight,
1132 std::vector<Vec3D<SolverT>> &Av) const {
1133#pragma omp parallel for schedule(static)
1134 for (std::size_t i = 0; i < nodes.size(); ++i) {
1135 if (nodes[i].fixed) {
1136 for (unsigned c = 0; c < D; ++c)
1137 Av[i][c] = v[i][c];
1138 continue;
1139 }
1140 const Vec3D<T> Fv = computeElasticStencilAt(i, v, gradDivWeight);
1141 for (unsigned c = 0; c < D; ++c)
1142 Av[i][c] =
1143 static_cast<SolverT>(static_cast<T>(v[i][c]) - Fv[c] + b[i][c]);
1144 }
1145 }
1146
1147 static constexpr std::size_t mgNoNode =
1148 std::numeric_limits<std::size_t>::max();
1149
1150 static Vec3D<T> zeroVec() { return Vec3D<T>{T(0), T(0), T(0)}; }
1151
1152 static IndexType coarsenIndex(const IndexType &index) {
1153 IndexType coarse{};
1154 for (unsigned d = 0; d < D; ++d) {
1155 const auto value = index[d];
1156 coarse[d] = (value >= 0) ? value / 2 : -((-value + 1) / 2);
1157 }
1158 return coarse;
1159 }
1160
1161 std::vector<MultigridLevel> buildMultigridHierarchy() const {
1162 std::vector<MultigridLevel> levels;
1163 levels.emplace_back();
1164 auto &fine = levels.back();
1165 fine.indices.reserve(nodes.size());
1166 for (const auto &node : nodes)
1167 fine.indices.push_back(node.index);
1168
1169 constexpr unsigned maxLevels = 12;
1170 constexpr std::size_t minCoarsenNodes = 48;
1171 while (levels.size() < maxLevels &&
1172 levels.back().indices.size() > minCoarsenNodes) {
1173 const auto &previous = levels.back();
1174 MultigridLevel coarse;
1175 coarse.indices.reserve(previous.indices.size() / 2 + 1);
1176 coarse.children.reserve(previous.indices.size() / 2 + 1);
1177 coarse.fineToCoarse.assign(previous.indices.size(), mgNoNode);
1178
1179 std::unordered_map<IndexType, std::size_t, typename IndexType::hash>
1180 coarseLookup;
1181 coarseLookup.reserve(previous.indices.size());
1182 for (std::size_t fineId = 0; fineId < previous.indices.size(); ++fineId) {
1183 const IndexType coarseIndex = coarsenIndex(previous.indices[fineId]);
1184 auto found = coarseLookup.find(coarseIndex);
1185 if (found == coarseLookup.end()) {
1186 const std::size_t coarseId = coarse.indices.size();
1187 coarseLookup.emplace(coarseIndex, coarseId);
1188 coarse.indices.push_back(coarseIndex);
1189 coarse.children.emplace_back();
1190 found = coarseLookup.find(coarseIndex);
1191 }
1192
1193 const std::size_t coarseId = found->second;
1194 coarse.children[coarseId].push_back(fineId);
1195 coarse.fineToCoarse[fineId] = coarseId;
1196 }
1197
1198 if (coarse.indices.empty() ||
1199 coarse.indices.size() >= previous.indices.size())
1200 break;
1201
1202 // Stop if any grid dimension collapses to a single cell on the coarse
1203 // level. For thin masks this happens quickly in the thickness direction,
1204 // and a 1-cell-thick level loses all stress-gradient information across
1205 // that dimension, making the smoother degenerate.
1206 {
1207 IndexType lo = coarse.indices.front(), hi = coarse.indices.front();
1208 for (const auto &idx : coarse.indices)
1209 for (unsigned d = 0; d < D; ++d) {
1210 lo[d] = std::min(lo[d], idx[d]);
1211 hi[d] = std::max(hi[d], idx[d]);
1212 }
1213 bool degenerate = false;
1214 for (unsigned d = 0; d < D; ++d)
1215 if (hi[d] == lo[d])
1216 degenerate = true;
1217 if (degenerate)
1218 break;
1219 }
1220
1221 levels.push_back(std::move(coarse));
1222 }
1223 return levels;
1224 }
1225
1226 static T vectorDot(const std::vector<Vec3D<T>> &a,
1227 const std::vector<Vec3D<T>> &b) {
1228 T sum = T(0);
1229 for (std::size_t i = 0; i < a.size(); ++i)
1230 for (unsigned c = 0; c < D; ++c)
1231 sum += a[i][c] * b[i][c];
1232 return sum;
1233 }
1234
1235 static T vectorNorm(const std::vector<Vec3D<T>> &v) {
1236 return std::sqrt(std::max(vectorDot(v, v), T(0)));
1237 }
1238
1239 static void vectorScale(std::vector<Vec3D<T>> &v, T scale) {
1240 for (auto &entry : v)
1241 for (unsigned c = 0; c < D; ++c)
1242 entry[c] *= scale;
1243 }
1244
1245 static void vectorAxpy(std::vector<Vec3D<T>> &y, T alpha,
1246 const std::vector<Vec3D<T>> &x) {
1247 for (std::size_t i = 0; i < y.size(); ++i)
1248 for (unsigned c = 0; c < D; ++c)
1249 y[i][c] += alpha * x[i][c];
1250 }
1251
1252 static void vectorSubtractInPlace(std::vector<Vec3D<T>> &y, T alpha,
1253 const std::vector<Vec3D<T>> &x) {
1254 for (std::size_t i = 0; i < y.size(); ++i)
1255 for (unsigned c = 0; c < D; ++c)
1256 y[i][c] -= alpha * x[i][c];
1257 }
1258
1259 static T flatValue(const std::vector<Vec3D<T>> &v, std::size_t row) {
1260 return v[row / D][row % D];
1261 }
1262
1263 static void flatSet(std::vector<Vec3D<T>> &v, std::size_t row, T value) {
1264 v[row / D][row % D] = value;
1265 }
1266
1267 void collectLocalCandidatesRecursive(unsigned dim, const IndexType &center,
1268 IndexType &candidate,
1269 std::vector<std::size_t> &out) const {
1270 if (dim == D) {
1271 if (!inBounds(candidate))
1272 return;
1273 const std::size_t nodeId = lookupNode(candidate);
1274 if (nodeId != noNode)
1275 out.push_back(nodeId);
1276 return;
1277 }
1278
1279 for (int offset = -2; offset <= 2; ++offset) {
1280 candidate[dim] = center[dim] + offset;
1281 collectLocalCandidatesRecursive(dim + 1, center, candidate, out);
1282 }
1283 }
1284
1285 void collectLocalCandidates(std::size_t rowNode,
1286 std::vector<std::size_t> &out) const {
1287 out.clear();
1288 IndexType candidate = nodes[rowNode].index;
1289 collectLocalCandidatesRecursive(0, nodes[rowNode].index, candidate, out);
1290 out.push_back(rowNode);
1291 std::sort(out.begin(), out.end());
1292 out.erase(std::unique(out.begin(), out.end()), out.end());
1293 }
1294
1295 SparseMatrix
1296 compressSparseRows(std::vector<std::unordered_map<std::size_t, T>> &rows,
1297 std::size_t nodeCount) const {
1298 SparseMatrix matrix;
1299 matrix.nodeCount = nodeCount;
1300 matrix.rowPtr.assign(rows.size() + 1, 0);
1301 matrix.invDiagonal.assign(rows.size(), T(1));
1302
1303 for (std::size_t row = 0; row < rows.size(); ++row) {
1304 std::vector<std::pair<std::size_t, T>> entries(rows[row].begin(),
1305 rows[row].end());
1306 std::sort(entries.begin(), entries.end(),
1307 [](const auto &a, const auto &b) { return a.first < b.first; });
1308
1309 matrix.rowPtr[row] = matrix.values.size();
1310 T diagonal = T(0);
1311 for (const auto &[col, value] : entries) {
1312 if (std::abs(value) <= std::numeric_limits<T>::epsilon() * T(100))
1313 continue;
1314 if (!std::isfinite(value))
1315 throwNonFinite("traction sparse matrix assembly");
1316 matrix.colIndex.push_back(col);
1317 matrix.values.push_back(value);
1318 if (col == row)
1319 diagonal += value;
1320 }
1321
1322 if (std::abs(diagonal) > std::numeric_limits<T>::epsilon())
1323 matrix.invDiagonal[row] = T(1) / diagonal;
1324 else
1325 matrix.invDiagonal[row] = T(1);
1326 }
1327 matrix.rowPtr[rows.size()] = matrix.values.size();
1328 return matrix;
1329 }
1330
1331 SparseMatrix buildFineElasticMatrix(const std::vector<Vec3D<T>> &b,
1332 T gradDivWeight) const {
1333 const std::size_t n = nodes.size();
1334 std::vector<std::unordered_map<std::size_t, T>> rows(n * D);
1335 std::vector<Vec3D<T>> basis(n, zeroVec());
1336 std::vector<std::size_t> candidates;
1337
1338 for (std::size_t rowNode = 0; rowNode < n; ++rowNode) {
1339 if (nodes[rowNode].fixed) {
1340 for (unsigned rowComponent = 0; rowComponent < D; ++rowComponent) {
1341 const std::size_t row = rowNode * D + rowComponent;
1342 rows[row][row] = T(1);
1343 }
1344 continue;
1345 }
1346
1347 // Build A column-by-column via probing. The operator is (A·v)[i][c] =
1348 // v[i][c] - F(v)[i][c], where F is affine: F(v) = F_linear(v) + F(0).
1349 // b[rowNode] = F(0)[rowNode] (the traction load vector). The matrix
1350 // entry is the linear part only:
1351 // A[row, col] = δ(row,col) - (F(e_col)[rowComp] - F(0)[rowComp])
1352 // = δ(row,col) - (Fbasis[rowComp] - b[rowNode][rowComp])
1353 // b is constant with respect to col so it cancels between probes; it
1354 // is included here to isolate the linear part of F from its affine
1355 // offset.
1356 collectLocalCandidates(rowNode, candidates);
1357 for (std::size_t colNode : candidates) {
1358 for (unsigned colComponent = 0; colComponent < D; ++colComponent) {
1359 basis[colNode][colComponent] = T(1);
1360 const Vec3D<T> Fbasis =
1361 computeElasticStencilAt(rowNode, basis, gradDivWeight);
1362 basis[colNode][colComponent] = T(0);
1363
1364 for (unsigned rowComponent = 0; rowComponent < D; ++rowComponent) {
1365 const std::size_t row = rowNode * D + rowComponent;
1366 const std::size_t col = colNode * D + colComponent;
1367 const T identity =
1368 (rowNode == colNode && rowComponent == colComponent) ? T(1)
1369 : T(0);
1370 const T value =
1371 identity - (Fbasis[rowComponent] - b[rowNode][rowComponent]);
1372 rows[row][col] += value;
1373 }
1374 }
1375 }
1376 }
1377
1378 return compressSparseRows(rows, n);
1379 }
1380
1381 SparseMatrix buildGalerkinMatrix(const SparseMatrix &fine,
1382 const MultigridLevel &coarseLevel) const {
1383 const std::size_t coarseNodes = coarseLevel.indices.size();
1384 std::vector<std::unordered_map<std::size_t, T>> rows(coarseNodes * D);
1385
1386 for (std::size_t fineRow = 0; fineRow < fine.nodeCount * D; ++fineRow) {
1387 const std::size_t fineRowNode = fineRow / D;
1388 if (fineRowNode >= coarseLevel.fineToCoarse.size())
1389 continue;
1390 const std::size_t coarseRowNode = coarseLevel.fineToCoarse[fineRowNode];
1391 if (coarseRowNode == mgNoNode)
1392 continue;
1393
1394 const T restrictionWeight =
1395 T(1) / static_cast<T>(std::max<std::size_t>(
1396 coarseLevel.children[coarseRowNode].size(), 1));
1397 const std::size_t coarseRow = coarseRowNode * D + fineRow % D;
1398
1399 for (std::size_t nz = fine.rowPtr[fineRow]; nz < fine.rowPtr[fineRow + 1];
1400 ++nz) {
1401 const std::size_t fineCol = fine.colIndex[nz];
1402 const std::size_t fineColNode = fineCol / D;
1403 if (fineColNode >= coarseLevel.fineToCoarse.size())
1404 continue;
1405 const std::size_t coarseColNode = coarseLevel.fineToCoarse[fineColNode];
1406 if (coarseColNode == mgNoNode)
1407 continue;
1408 const std::size_t coarseCol = coarseColNode * D + fineCol % D;
1409 rows[coarseRow][coarseCol] += restrictionWeight * fine.values[nz];
1410 }
1411 }
1412
1413 return compressSparseRows(rows, coarseNodes);
1414 }
1415
1416 void sparseMatvec(const SparseMatrix &matrix, const std::vector<Vec3D<T>> &x,
1417 std::vector<Vec3D<T>> &Ax) const {
1418 Ax.assign(matrix.nodeCount, zeroVec());
1419#pragma omp parallel for schedule(static)
1420 for (std::size_t row = 0; row < matrix.nodeCount * D; ++row) {
1421 T sum = T(0);
1422 for (std::size_t nz = matrix.rowPtr[row]; nz < matrix.rowPtr[row + 1];
1423 ++nz)
1424 sum += matrix.values[nz] * flatValue(x, matrix.colIndex[nz]);
1425 flatSet(Ax, row, sum);
1426 }
1427 }
1428
1429 void multigridProlong(const std::vector<MultigridLevel> &levels,
1430 std::size_t coarseLevelId,
1431 const std::vector<Vec3D<T>> &coarse,
1432 std::vector<Vec3D<T>> &fine) const {
1433 const auto &fineLevel = levels[coarseLevelId - 1];
1434 const auto &coarseLevel = levels[coarseLevelId];
1435 fine.assign(fineLevel.indices.size(), zeroVec());
1436 for (std::size_t coarseId = 0; coarseId < coarseLevel.children.size();
1437 ++coarseId)
1438 for (std::size_t fineId : coarseLevel.children[coarseId])
1439 for (unsigned c = 0; c < D; ++c)
1440 fine[fineId][c] = coarse[coarseId][c];
1441 }
1442
1443 void multigridSmooth(const SparseMatrix &matrix,
1444 const std::vector<Vec3D<T>> &rhs,
1445 std::vector<Vec3D<T>> &x, unsigned sweeps,
1446 T omega) const {
1447 const std::size_t rows = matrix.nodeCount * D;
1448 auto relaxRow = [&](std::size_t row) {
1449 T offDiagonal = T(0);
1450 for (std::size_t nz = matrix.rowPtr[row]; nz < matrix.rowPtr[row + 1];
1451 ++nz) {
1452 const std::size_t col = matrix.colIndex[nz];
1453 if (col == row)
1454 continue;
1455 offDiagonal += matrix.values[nz] * flatValue(x, col);
1456 }
1457 const T updated =
1458 (flatValue(rhs, row) - offDiagonal) * matrix.invDiagonal[row];
1459 flatSet(x, row,
1460 flatValue(x, row) + omega * (updated - flatValue(x, row)));
1461 };
1462
1463 for (unsigned sweep = 0; sweep < sweeps; ++sweep) {
1464 for (std::size_t row = 0; row < rows; ++row)
1465 relaxRow(row);
1466 for (std::size_t row = rows; row-- > 0;)
1467 relaxRow(row);
1468 }
1469 }
1470
1471 void multigridResidual(const SparseMatrix &matrix,
1472 const std::vector<Vec3D<T>> &rhs,
1473 const std::vector<Vec3D<T>> &x,
1474 std::vector<Vec3D<T>> &residualOut) const {
1475 sparseMatvec(matrix, x, residualOut);
1476 for (std::size_t i = 0; i < rhs.size(); ++i)
1477 for (unsigned c = 0; c < D; ++c)
1478 residualOut[i][c] = rhs[i][c] - residualOut[i][c];
1479 }
1480
1481 void multigridRestrict(const std::vector<Vec3D<T>> &fineResidual,
1482 const MultigridLevel &coarseLevel,
1483 std::vector<Vec3D<T>> &coarseRhs) const {
1484 coarseRhs.assign(coarseLevel.indices.size(), zeroVec());
1485 for (std::size_t coarseId = 0; coarseId < coarseLevel.children.size();
1486 ++coarseId) {
1487 const auto &children = coarseLevel.children[coarseId];
1488 if (children.empty())
1489 continue;
1490 for (std::size_t fineId : children)
1491 for (unsigned c = 0; c < D; ++c)
1492 coarseRhs[coarseId][c] += fineResidual[fineId][c];
1493 const T scale = T(1) / static_cast<T>(children.size());
1494 for (unsigned c = 0; c < D; ++c)
1495 coarseRhs[coarseId][c] *= scale;
1496 }
1497 }
1498
1499 void multigridProlongAdd(const std::vector<Vec3D<T>> &coarseCorrection,
1500 const MultigridLevel &coarseLevel,
1501 std::vector<Vec3D<T>> &fineCorrection) const {
1502 for (std::size_t coarseId = 0; coarseId < coarseLevel.children.size();
1503 ++coarseId) {
1504 for (std::size_t fineId : coarseLevel.children[coarseId])
1505 for (unsigned c = 0; c < D; ++c)
1506 fineCorrection[fineId][c] += coarseCorrection[coarseId][c];
1507 }
1508 }
1509
1510 void multigridVCycle(const std::vector<MultigridLevel> &levels,
1511 std::size_t levelId, const std::vector<Vec3D<T>> &rhs,
1512 std::vector<Vec3D<T>> &x, T smootherOmega) const {
1513 const auto &level = levels[levelId];
1514 if (levelId + 1 >= levels.size() || level.indices.size() <= 32) {
1515 multigridSmooth(level.matrix, rhs, x, 40, smootherOmega);
1516 return;
1517 }
1518
1519 multigridSmooth(level.matrix, rhs, x, 2, smootherOmega);
1520
1521 std::vector<Vec3D<T>> fineResidual;
1522 multigridResidual(level.matrix, rhs, x, fineResidual);
1523
1524 std::vector<Vec3D<T>> coarseRhs;
1525 const auto &coarseLevel = levels[levelId + 1];
1526 multigridRestrict(fineResidual, coarseLevel, coarseRhs);
1527
1528 std::vector<Vec3D<T>> coarseCorrection(coarseRhs.size(), zeroVec());
1529 multigridVCycle(levels, levelId + 1, coarseRhs, coarseCorrection,
1530 smootherOmega);
1531 multigridProlongAdd(coarseCorrection, coarseLevel, x);
1532
1533 multigridSmooth(level.matrix, rhs, x, 2, smootherOmega);
1534 }
1535
1536 std::vector<Vec3D<T>>
1537 multigridPrecondition(const std::vector<MultigridLevel> &levels,
1538 const std::vector<Vec3D<T>> &rhs,
1539 T smootherOmega) const {
1540 std::vector<Vec3D<T>> correction(rhs.size(), zeroVec());
1541 if (levels.empty())
1542 return correction;
1543 multigridVCycle(levels, 0, rhs, correction, smootherOmega);
1544 return correction;
1545 }
1546
1547 void exactElasticResidual(const SparseMatrix &matrix,
1548 const std::vector<Vec3D<T>> &x,
1549 const std::vector<Vec3D<T>> &b,
1550 std::vector<Vec3D<T>> &r) const {
1551 std::vector<Vec3D<T>> Ax(x.size(), zeroVec());
1552 sparseMatvec(matrix, x, Ax);
1553 r.assign(x.size(), zeroVec());
1554 for (std::size_t i = 0; i < x.size(); ++i)
1555 for (unsigned c = 0; c < D; ++c)
1556 r[i][c] = b[i][c] - Ax[i][c];
1557 }
1558
1559 static std::vector<T>
1560 solveUpperTriangular(const std::vector<std::vector<T>> &h,
1561 const std::vector<T> &g, unsigned usedColumns) {
1562 std::vector<T> y(usedColumns, T(0));
1563 for (int row = static_cast<int>(usedColumns) - 1; row >= 0; --row) {
1564 T sum = g[static_cast<std::size_t>(row)];
1565 for (unsigned col = static_cast<unsigned>(row) + 1; col < usedColumns;
1566 ++col)
1567 sum -= h[static_cast<std::size_t>(row)][col] * y[col];
1568 const T diag =
1569 h[static_cast<std::size_t>(row)][static_cast<std::size_t>(row)];
1570 if (std::abs(diag) > std::numeric_limits<T>::epsilon())
1571 y[static_cast<std::size_t>(row)] = sum / diag;
1572 }
1573 return y;
1574 }
1575
1576 void solveElasticVelocity() {
1577 if (parameters.contactMode > 0) {
1578 solveElasticVelocityMultigridGMRES();
1579 return;
1580 }
1581 solveElasticVelocityBiCGSTAB();
1582 }
1583
1584 void solveElasticVelocityMultigridGMRES() {
1585 iterations = 0;
1586 residual = 0.;
1587 if (nodes.empty())
1588 return;
1589
1590 const T lambda = lameLambda();
1591 const T mu = lameMu();
1592 const T gradDivWeight =
1593 (lambda + mu) /
1594 std::max(lambda + T(2) * mu, std::numeric_limits<T>::epsilon());
1595 const T smootherOmega =
1596 std::clamp(parameters.multigridSmootherOmega, T(0.2), T(1.4));
1597
1598 const std::size_t n = nodes.size();
1599 const std::vector<Vec3D<T>> zeros(n, zeroVec());
1600 std::vector<Vec3D<T>> b(n, zeroVec());
1601#pragma omp parallel for schedule(static)
1602 for (std::size_t i = 0; i < n; ++i) {
1603 if (nodes[i].fixed)
1604 b[i] = zeroVec();
1605 else
1606 b[i] = computeElasticStencilAt(i, zeros, gradDivWeight);
1607 }
1608
1609 std::vector<Vec3D<T>> x(n, zeroVec());
1610 for (std::size_t i = 0; i < n; ++i)
1611 x[i] = nodes[i].fixed ? zeroVec() : nodes[i].velocity;
1612
1613 // Rebuild the multigrid hierarchy only when the node count or the contact
1614 // face classification changes. The stiffness matrix depends only on node
1615 // geometry and which faces are active (compressive), not on the traction
1616 // magnitudes — those only enter the load vector b computed above. Within
1617 // a single time step the coupling iterations run without advection, so the
1618 // mask geometry and contact pattern are typically stable after the first
1619 // call and the matrix can be reused for all subsequent iterations.
1620 const bool hierarchyDirty =
1621 cachedNodeCount_ != n || cachedContactFaceActive_ != contactFaceActive_;
1622
1623 if (hierarchyDirty) {
1624 cachedMultigridLevels_ = buildMultigridHierarchy();
1625 if (cachedMultigridLevels_.empty())
1626 return;
1627 cachedMultigridLevels_[0].matrix =
1628 buildFineElasticMatrix(b, gradDivWeight);
1629 for (std::size_t level = 1; level < cachedMultigridLevels_.size();
1630 ++level)
1631 cachedMultigridLevels_[level].matrix =
1632 buildGalerkinMatrix(cachedMultigridLevels_[level - 1].matrix,
1633 cachedMultigridLevels_[level]);
1634 cachedNodeCount_ = n;
1635 cachedContactFaceActive_ = contactFaceActive_;
1636 }
1637
1638 if (cachedMultigridLevels_.empty())
1639 return;
1640 const auto &multigridLevels = cachedMultigridLevels_;
1641
1642 std::vector<Vec3D<T>> r;
1643 exactElasticResidual(multigridLevels[0].matrix, x, b, r);
1644
1645 const T rhsNorm = vectorNorm(b);
1646 const T absTolerance =
1647 std::max(parameters.tolerance * rhsNorm,
1648 std::numeric_limits<T>::epsilon() * T(100) *
1649 std::sqrt(static_cast<T>(
1650 std::max<std::size_t>(std::size_t(1), n * D))));
1651 const T residualNormDenom = std::max(rhsNorm, absTolerance);
1652 auto updateGmresResidual = [&](T absResidual) {
1653 residual = (absResidual <= absTolerance)
1654 ? T(0)
1655 : absResidual / residualNormDenom;
1656 };
1657
1658 T absResidual = vectorNorm(r);
1659 updateGmresResidual(absResidual);
1660 if (absResidual <= absTolerance || residual < parameters.tolerance) {
1661 for (std::size_t i = 0; i < n; ++i)
1662 nodes[i].velocity = x[i];
1663 return;
1664 }
1665
1666 constexpr unsigned restart = 32;
1667 while (iterations < parameters.maxIterations &&
1668 residual > parameters.tolerance) {
1669 const T beta = vectorNorm(r);
1670 if (!std::isfinite(beta))
1671 throwNonFinite("traction multigrid GMRES residual");
1672 if (beta <= absTolerance) {
1673 residual = T(0);
1674 break;
1675 }
1676
1677 const unsigned innerLimit =
1678 std::min<unsigned>(restart, parameters.maxIterations - iterations);
1679 std::vector<std::vector<Vec3D<T>>> v(innerLimit + 1,
1680 std::vector<Vec3D<T>>(n, zeroVec()));
1681 std::vector<std::vector<Vec3D<T>>> z(innerLimit,
1682 std::vector<Vec3D<T>>(n, zeroVec()));
1683 v[0] = r;
1684 vectorScale(v[0], T(1) / beta);
1685
1686 std::vector<std::vector<T>> h(innerLimit + 1,
1687 std::vector<T>(innerLimit, T(0)));
1688 std::vector<T> cs(innerLimit, T(0));
1689 std::vector<T> sn(innerLimit, T(0));
1690 std::vector<T> g(innerLimit + 1, T(0));
1691 g[0] = beta;
1692
1693 unsigned usedColumns = 0;
1694 for (unsigned j = 0; j < innerLimit; ++j) {
1695 z[j] = multigridPrecondition(multigridLevels, v[j], smootherOmega);
1696
1697 std::vector<Vec3D<T>> w(n, zeroVec());
1698 sparseMatvec(multigridLevels[0].matrix, z[j], w);
1699
1700 for (unsigned i = 0; i <= j; ++i) {
1701 h[i][j] = vectorDot(w, v[i]);
1702 vectorSubtractInPlace(w, h[i][j], v[i]);
1703 }
1704
1705 h[j + 1][j] = vectorNorm(w);
1706 if (h[j + 1][j] > std::numeric_limits<T>::epsilon()) {
1707 v[j + 1] = w;
1708 vectorScale(v[j + 1], T(1) / h[j + 1][j]);
1709 }
1710
1711 for (unsigned i = 0; i < j; ++i) {
1712 const T h0 = h[i][j];
1713 const T h1 = h[i + 1][j];
1714 h[i][j] = cs[i] * h0 + sn[i] * h1;
1715 h[i + 1][j] = -sn[i] * h0 + cs[i] * h1;
1716 }
1717
1718 const T h0 = h[j][j];
1719 const T h1 = h[j + 1][j];
1720 const T denom = std::hypot(h0, h1);
1721 if (denom <= std::numeric_limits<T>::epsilon()) {
1722 cs[j] = T(1);
1723 sn[j] = T(0);
1724 } else {
1725 cs[j] = h0 / denom;
1726 sn[j] = h1 / denom;
1727 }
1728 h[j][j] = cs[j] * h0 + sn[j] * h1;
1729 h[j + 1][j] = T(0);
1730
1731 const T g0 = g[j];
1732 g[j] = cs[j] * g0;
1733 g[j + 1] = -sn[j] * g0;
1734
1735 ++iterations;
1736 usedColumns = j + 1;
1737 const T projectedAbsResidual = std::abs(g[j + 1]);
1738 updateGmresResidual(projectedAbsResidual);
1739 if (!std::isfinite(residual))
1740 throwNonFinite("traction multigrid GMRES residual");
1741 if (projectedAbsResidual <= absTolerance ||
1742 residual < parameters.tolerance)
1743 break;
1744 }
1745
1746 if (usedColumns == 0)
1747 break;
1748
1749 const auto y = solveUpperTriangular(h, g, usedColumns);
1750 for (unsigned col = 0; col < usedColumns; ++col)
1751 vectorAxpy(x, y[col], z[col]);
1752
1753 exactElasticResidual(multigridLevels[0].matrix, x, b, r);
1754 absResidual = vectorNorm(r);
1755 updateGmresResidual(absResidual);
1756 if (!std::isfinite(residual))
1757 throwNonFinite("traction multigrid GMRES residual");
1758 }
1759
1760 for (std::size_t i = 0; i < n; ++i)
1761 nodes[i].velocity = x[i];
1762
1763 if (residual > parameters.tolerance)
1764 Logger::getInstance()
1765 .addWarning("solveElasticVelocity: traction multigrid GMRES did not "
1766 "converge after " +
1767 std::to_string(iterations) + "/" +
1768 std::to_string(parameters.maxIterations) +
1769 " iterations (residual=" + std::to_string(residual) +
1770 ", tolerance=" + std::to_string(parameters.tolerance) +
1771 ")")
1772 .print();
1773 }
1774
1775 void solveElasticVelocityRelaxation() {
1776 iterations = 0;
1777 residual = 0.;
1778 if (nodes.empty())
1779 return;
1780
1781 const T lambda = lameLambda();
1782 const T mu = lameMu();
1783 const T gradDivWeight =
1784 (lambda + mu) /
1785 std::max(lambda + T(2) * mu, std::numeric_limits<T>::epsilon());
1786 const T relaxation = std::clamp(parameters.relaxation, T(0.01), T(1));
1787
1788 std::vector<Vec3D<T>> current(nodes.size());
1789 std::vector<Vec3D<T>> next(nodes.size());
1790 for (std::size_t i = 0; i < nodes.size(); ++i)
1791 current[i] =
1792 nodes[i].fixed ? Vec3D<T>{T(0), T(0), T(0)} : nodes[i].velocity;
1793
1794 for (; iterations < parameters.maxIterations; ++iterations) {
1795 T maxDelta = T(0);
1796 T maxMagnitude = std::numeric_limits<T>::epsilon();
1797 int finiteFlag = 1;
1798
1799#pragma omp parallel for schedule(static) \
1800 reduction(max : maxDelta, maxMagnitude) reduction(min : finiteFlag)
1801 for (std::size_t i = 0; i < nodes.size(); ++i) {
1802 Vec3D<T> candidate =
1803 nodes[i].fixed ? Vec3D<T>{T(0), T(0), T(0)}
1804 : computeElasticStencilAt(i, current, gradDivWeight);
1805
1806 for (unsigned c = 0; c < D; ++c) {
1807 if (!std::isfinite(candidate[c]) || !std::isfinite(current[i][c])) {
1808 finiteFlag = 0;
1809 next[i][c] = current[i][c];
1810 continue;
1811 }
1812 next[i][c] =
1813 current[i][c] + relaxation * (candidate[c] - current[i][c]);
1814 maxDelta = std::max(maxDelta, std::abs(next[i][c] - current[i][c]));
1815 maxMagnitude = std::max(maxMagnitude, std::abs(next[i][c]));
1816 maxMagnitude = std::max(maxMagnitude, std::abs(current[i][c]));
1817 }
1818 }
1819
1820 if (!finiteFlag) {
1821 residual = std::numeric_limits<T>::infinity();
1822 throwNonFinite("traction mask solve");
1823 }
1824
1825 residual = maxDelta / maxMagnitude;
1826 current.swap(next);
1827 if (residual < parameters.tolerance) {
1828 ++iterations;
1829 break;
1830 }
1831 }
1832
1833 for (std::size_t i = 0; i < nodes.size(); ++i)
1834 nodes[i].velocity = current[i];
1835
1836 if (residual > parameters.tolerance)
1837 Logger::getInstance()
1838 .addWarning("solveElasticVelocity: traction relaxation did not "
1839 "converge after " +
1840 std::to_string(iterations) + "/" +
1841 std::to_string(parameters.maxIterations) +
1842 " iterations (residual=" + std::to_string(residual) +
1843 ", tolerance=" + std::to_string(parameters.tolerance) +
1844 ")")
1845 .print();
1846 }
1847
1848 void solveElasticVelocityBiCGSTAB() {
1849 iterations = 0;
1850 residual = 0.;
1851 if (nodes.empty())
1852 return;
1853
1854 using SolverT = float;
1855
1856 const T lambda = lameLambda();
1857 const T mu = lameMu();
1858 const T gradDivWeight =
1859 (lambda + mu) /
1860 std::max(lambda + T(2) * mu, std::numeric_limits<T>::epsilon());
1861
1862 const std::size_t n = nodes.size();
1863 const Vec3D<SolverT> zero3{SolverT(0), SolverT(0), SolverT(0)};
1864
1865 // b[i] = F(0)[i]: contact BC constants (reaction/contact velocities).
1866 // OOB and traction-free faces contribute v[i]=0 at zeros.
1867 std::vector<Vec3D<T>> b(n);
1868 {
1869 const std::vector<Vec3D<SolverT>> zeros(n, zero3);
1870#pragma omp parallel for schedule(static)
1871 for (std::size_t i = 0; i < n; ++i) {
1872 if (nodes[i].fixed)
1873 b[i] = Vec3D<T>{T(0), T(0), T(0)};
1874 else
1875 b[i] = computeElasticStencilAt(i, zeros, gradDivWeight);
1876 }
1877 }
1878
1879 // Cold start from zeros (matching original Jacobi behavior).
1880 std::vector<Vec3D<SolverT>> x(n, zero3);
1881
1882 // r = b - A*x. With x=0: A*0 = 0 - F(0) + b = 0, so r = b.
1883 std::vector<Vec3D<SolverT>> r(n), r_hat(n);
1884 for (std::size_t i = 0; i < n; ++i)
1885 for (unsigned c = 0; c < D; ++c) {
1886 r[i][c] = static_cast<SolverT>(b[i][c]);
1887 r_hat[i][c] = r[i][c];
1888 }
1889
1890 // BiCGSTAB with identity preconditioner.
1891 // For most interior nodes the diagonal of A is ≈ 1; the identity
1892 // preconditioner is exact there and a reasonable approximation at boundary
1893 // nodes.
1894 std::vector<Vec3D<SolverT>> pv(n, zero3), sv(n, zero3), y(n), z(n), s(n),
1895 t(n);
1896 T rho = T(1), alpha = T(1), omega = T(1);
1897
1898 auto vecDot = [&](const std::vector<Vec3D<SolverT>> &a,
1899 const std::vector<Vec3D<SolverT>> &bv) {
1900 T sum = T(0);
1901 for (std::size_t i = 0; i < n; ++i)
1902 for (unsigned c = 0; c < D; ++c) {
1903 const T av = static_cast<T>(a[i][c]);
1904 const T bvVal = static_cast<T>(bv[i][c]);
1905 if (!std::isfinite(av) || !std::isfinite(bvVal))
1906 return std::numeric_limits<T>::quiet_NaN();
1907 sum += av * bvVal;
1908 }
1909 return sum;
1910 };
1911
1912 auto vecMaxAbs = [&](const std::vector<Vec3D<SolverT>> &vin) {
1913 T m = T(0);
1914 for (std::size_t i = 0; i < n; ++i)
1915 for (unsigned c = 0; c < D; ++c) {
1916 const T value = static_cast<T>(vin[i][c]);
1917 if (!std::isfinite(value))
1918 return std::numeric_limits<T>::infinity();
1919 m = std::max(m, std::abs(value));
1920 }
1921 return m;
1922 };
1923
1924 const T b_norm = [&] {
1925 T m = T(0);
1926 for (std::size_t i = 0; i < n; ++i)
1927 for (unsigned c = 0; c < D; ++c)
1928 m = std::max(m, std::abs(b[i][c]));
1929 return (m < T(1e-100)) ? T(1) : m;
1930 }();
1931
1932 for (; iterations < parameters.maxIterations; ++iterations) {
1933 const T rho_new = vecDot(r_hat, r);
1934 if (!std::isfinite(rho_new) || std::abs(rho_new) < T(1e-100))
1935 break;
1936 if (!std::isfinite(rho) || !std::isfinite(alpha) ||
1937 !std::isfinite(omega) || std::abs(omega) < T(1e-100))
1938 break;
1939
1940 const T beta = (rho_new / rho) * (alpha / omega);
1941 if (!std::isfinite(beta))
1942 break;
1943 rho = rho_new;
1944
1945 for (std::size_t i = 0; i < n; ++i)
1946 for (unsigned c = 0; c < D; ++c)
1947 pv[i][c] = static_cast<SolverT>(r[i][c] +
1948 beta * (pv[i][c] - omega * sv[i][c]));
1949
1950 // Identity preconditioner: y = p
1951 y = pv;
1952 elasticMatvec(y, b, gradDivWeight, sv);
1953
1954 const T r_hat_v = vecDot(r_hat, sv);
1955 if (!std::isfinite(r_hat_v) || std::abs(r_hat_v) < T(1e-100))
1956 break;
1957
1958 alpha = rho_new / r_hat_v;
1959 if (!std::isfinite(alpha))
1960 break;
1961
1962 for (std::size_t i = 0; i < n; ++i)
1963 for (unsigned c = 0; c < D; ++c)
1964 s[i][c] = static_cast<SolverT>(r[i][c] - alpha * sv[i][c]);
1965
1966 residual = vecMaxAbs(s);
1967 if (!std::isfinite(residual))
1968 break;
1969 if (residual < parameters.tolerance * b_norm) {
1970 for (std::size_t i = 0; i < n; ++i)
1971 for (unsigned c = 0; c < D; ++c)
1972 x[i][c] = static_cast<SolverT>(x[i][c] + alpha * y[i][c]);
1973 ++iterations;
1974 break;
1975 }
1976
1977 // Identity preconditioner: z = s
1978 z = s;
1979 elasticMatvec(z, b, gradDivWeight, t);
1980
1981 const T t_s = vecDot(t, s);
1982 const T t_t = vecDot(t, t);
1983 if (!std::isfinite(t_s) || !std::isfinite(t_t) || t_t <= T(1e-100))
1984 break;
1985 omega = t_s / t_t;
1986 if (!std::isfinite(omega))
1987 break;
1988
1989 for (std::size_t i = 0; i < n; ++i)
1990 for (unsigned c = 0; c < D; ++c) {
1991 x[i][c] =
1992 static_cast<SolverT>(x[i][c] + alpha * y[i][c] + omega * z[i][c]);
1993 r[i][c] = static_cast<SolverT>(s[i][c] - omega * t[i][c]);
1994 }
1995
1996 residual = vecMaxAbs(r);
1997 if (!std::isfinite(residual))
1998 break;
1999 if (residual < parameters.tolerance * b_norm) {
2000 ++iterations;
2001 break;
2002 }
2003 }
2004
2005 bool finiteSolution = true;
2006 for (std::size_t i = 0; i < n; ++i)
2007 for (unsigned c = 0; c < D; ++c)
2008 if (!std::isfinite(static_cast<T>(x[i][c])))
2009 finiteSolution = false;
2010
2011 if (finiteSolution) {
2012 for (std::size_t i = 0; i < n; ++i)
2013 for (unsigned c = 0; c < D; ++c)
2014 nodes[i].velocity[c] = static_cast<T>(x[i][c]);
2015 } else {
2016 residual = std::numeric_limits<T>::infinity();
2017 throwNonFinite("legacy kinematic mask solve");
2018 }
2019 if (residual > parameters.tolerance * b_norm)
2020 Logger::getInstance()
2021 .addWarning(
2022 "solveElasticVelocity: BiCGSTAB did not converge after " +
2023 std::to_string(iterations) + "/" +
2024 std::to_string(parameters.maxIterations) +
2025 " iterations (residual=" + std::to_string(residual / b_norm) +
2026 ", tolerance=" + std::to_string(parameters.tolerance) + ")")
2027 .print();
2028 }
2029
2030 bool touchesContactBoundary(ConstSparseIterator &maskIt,
2031 const IndexType &index) const {
2032 for (unsigned direction = 0; direction < D; ++direction) {
2033 for (int offset : {-1, 1}) {
2034 IndexType neighbor = index;
2035 neighbor[direction] += offset;
2036 if (!inBounds(neighbor)) {
2037 // Solve region is clipped to the mask/oxide interface: an
2038 // out-of-bounds neighbor is by definition outside the mask.
2039 if (isContactBoundary(index, direction, offset, maskIt))
2040 return true;
2041 continue;
2042 }
2043 if (lookupNode(neighbor) != noNode)
2044 continue;
2045 if (!crosses(valueAt(maskIt, index), valueAt(maskIt, neighbor)))
2046 continue;
2047 if (isContactBoundary(index, direction, offset, maskIt))
2048 return true;
2049 }
2050 }
2051 return false;
2052 }
2053
2054 bool isContactBoundary(const IndexType &index, unsigned direction, int offset,
2055 ConstSparseIterator &maskIt) const {
2056 const T grad = maskGradientComponent(index, direction, maskIt);
2057
2058 // The face exits the selected bending domain only if it points from an
2059 // inside node toward the outside of that domain. Use the signed level-set
2060 // gradient instead of a normalized normal so HRLE far-field sentinels
2061 // cannot produce a spurious zero normal.
2062 if (static_cast<T>(maskSign) * static_cast<T>(offset) * grad >= T(0))
2063 return false;
2064
2065 if (ambientInterface == nullptr)
2066 return direction == D - 1; // no ambient LS: fall back to bottom-face only
2067
2068 IndexType ghostIndex = index;
2069 ghostIndex[direction] += offset;
2070 return isInsideOxide(ghostIndex);
2071 }
2072
2073 bool isInsideOxide(const IndexType &index) const {
2074 if (ambientInterface == nullptr)
2075 return false;
2076 const auto it = ambientPhiCache_.find(index);
2077 return it != ambientPhiCache_.end() && it->second >= T(0);
2078 }
2079
2080 T maskFaceDistance(ConstSparseIterator &maskIt, const IndexType &inside,
2081 const IndexType &outside) const {
2082 if (!inBounds(outside))
2083 return gridDelta;
2084 return crossingDistance(valueAt(maskIt, inside), valueAt(maskIt, outside));
2085 }
2086
2087 void markFixedNodes() {
2088 fixedNodes = 0;
2089 if (nodes.empty() || parameters.anchorBoundarySide == 0)
2090 return;
2091 if (parameters.anchorBoundaryDirection < 0 ||
2092 parameters.anchorBoundaryDirection >= D)
2093 return;
2094
2095 const unsigned dir =
2096 static_cast<unsigned>(parameters.anchorBoundaryDirection);
2097
2098 // Warn if the anchor is placed along the oxide-growth direction (D-1 in a
2099 // standard LOCOS cross-section is the vertical/y axis). Clamping the top
2100 // or bottom of the mask instead of a far lateral edge removes the degrees
2101 // of freedom the bending solve needs and will suppress the bird's-beak
2102 // deflection entirely.
2103 if (dir == static_cast<unsigned>(D - 1))
2104 Logger::getInstance()
2105 .addWarning("OxidationMaskBending: anchorBoundaryDirection=" +
2106 std::to_string(parameters.anchorBoundaryDirection) +
2107 " points along the oxide-growth axis (direction D-1=" +
2108 std::to_string(D - 1) +
2109 "). The anchor is normally "
2110 "placed at a lateral edge (direction 0) so bending "
2111 "degrees of freedom are preserved. Set "
2112 "anchorBoundaryDirection=0 for a LOCOS cross-section.")
2113 .print();
2114 IndexType nodeMin = nodes.front().index;
2115 IndexType nodeMax = nodes.front().index;
2116 for (const auto &node : nodes) {
2117 for (unsigned d = 0; d < D; ++d) {
2118 nodeMin[d] = std::min(nodeMin[d], node.index[d]);
2119 nodeMax[d] = std::max(nodeMax[d], node.index[d]);
2120 }
2121 }
2122
2123 const auto layers = static_cast<viennahrle::IndexType>(
2124 std::max(1u, parameters.anchorBoundaryLayers));
2125 for (auto &node : nodes) {
2126 const bool onLower = parameters.anchorBoundarySide < 0 &&
2127 node.index[dir] <= nodeMin[dir] + layers - 1;
2128 const bool onUpper = parameters.anchorBoundarySide > 0 &&
2129 node.index[dir] >= nodeMax[dir] - layers + 1;
2130 if (onLower || onUpper) {
2131 node.fixed = true;
2132 node.velocity = {T(0), T(0), T(0)};
2133 ++fixedNodes;
2134 }
2135 }
2136 }
2137
2138 T maskGradientComponent(const IndexType &index, unsigned direction,
2139 ConstSparseIterator &maskIt) const {
2140 IndexType pos = index;
2141 IndexType neg = index;
2142 pos[direction] += 1;
2143 neg[direction] -= 1;
2144 if (!inBounds(pos))
2145 pos = index;
2146 if (!inBounds(neg))
2147 neg = index;
2148 return detail::clampLevelSetPhi(valueAt(maskIt, pos)) -
2149 detail::clampLevelSetPhi(valueAt(maskIt, neg));
2150 }
2151
2152 T clampedPoissonRatio() const {
2153 return std::clamp(parameters.poissonRatio, T(-0.95), T(0.49));
2154 }
2155
2156 static constexpr T gasConstant = T(8.31446261815324);
2157
2158 bool isElasticContactMode() const { return parameters.contactMode == 2; }
2159
2160 bool usesKinematicContactBoundary() const {
2161 return parameters.contactMode == 0 || isElasticContactMode();
2162 }
2163
2164 T effectiveMaskViscosity() const {
2165 // Elastic mode: η_eff = E (Pa·hr, with implicit dt_ref = 1 hr).
2166 // lameMu/lameLambda equal the standard elastic Lamé constants G and λ.
2167 // Contact faces use kinematic (Dirichlet) BC: v_contact = v_oxide × dt,
2168 // so the solver gives u_new ≈ v_oxide × dt (displacement in µm stored as
2169 // µm/hr). finalizeElasticAdvectionVelocity() converts u_new → u_new/dt
2170 // (the actual advection velocity µm/hr). Displacement feedback into the
2171 // oxide comes through the outer coupling loop in lsOxidation.
2172 if (isElasticContactMode())
2173 return parameters.youngModulus; // Pa·hr with implicit dt_ref = 1 hr
2174 const T temperature = std::max(parameters.temperature, T(1.));
2175 const T referenceTemperature =
2176 std::max(parameters.referenceTemperature, T(1.));
2177 return parameters.referenceViscosity *
2178 std::exp(parameters.creepActivationEnergy / gasConstant *
2179 (T(1) / temperature - T(1) / referenceTemperature));
2180 }
2181
2182 // Lamé viscosity parameters: same structure as elastic Lamé constants but
2183 // with eta(T) in place of Young's modulus, making the governing equation a
2184 // viscous Stokes flow rather than elastic equilibrium.
2185 T lameMu() const {
2186 const T nu = clampedPoissonRatio();
2187 return effectiveMaskViscosity() / (T(2) * (T(1) + nu));
2188 }
2189
2190 T lameLambda() const {
2191 const T nu = clampedPoissonRatio();
2192 return effectiveMaskViscosity() * nu / ((T(1) + nu) * (T(1) - T(2) * nu));
2193 }
2194
2195 Vec3D<T> getVelocity(const IndexType &index) const {
2196 const std::size_t nodeId = lookupNode(index);
2197 if (nodeId != noNode)
2198 return nodes[nodeId].velocity;
2199
2200 const auto nearby = findNearbyNode(index);
2201 if (nearby == noNode)
2202 return {0., 0., 0.};
2203 return nodes[nearby].velocity;
2204 }
2205
2206 bool isInsideMask(ConstSparseIterator &maskIt, const IndexType &index) const {
2207 return maskSign * valueAt(maskIt, index) >= 0.;
2208 }
2209
2210 T crossingDistance(T insidePhi, T outsidePhi) const {
2212 insidePhi, outsidePhi, parameters.minBoundaryDistance, gridDelta);
2213 }
2214};
2215
2220template <class T, int D>
2222 using IndexType = viennahrle::Index<D>;
2223 using ConstSparseIterator =
2224 viennahrle::ConstSparseIterator<typename Domain<T, D>::DomainType>;
2225
2226 SmartPointer<OxidationDeformation<T, D>> deformationField = nullptr;
2227 SmartPointer<OxidationMaskBending<T, D>> maskVelocityField = nullptr;
2228 SmartPointer<Domain<T, D>> maskInterface = nullptr;
2229 SmartPointer<Domain<T, D>> ambientInterface = nullptr;
2230 int maskSign = 1;
2231 std::unordered_map<IndexType, T, typename IndexType::hash> maskPhiCache_;
2232 T maskGridDelta_ = 1.;
2233 std::array<T, D> maxVelocity_{};
2234
2235public:
2237
2239 SmartPointer<OxidationDeformation<T, D>> passedDeformation,
2240 SmartPointer<OxidationMaskBending<T, D>> passedMaskVelocity,
2241 SmartPointer<Domain<T, D>> passedMaskInterface, int passedMaskSign = 1)
2242 : deformationField(passedDeformation),
2243 maskVelocityField(passedMaskVelocity),
2244 maskInterface(passedMaskInterface),
2245 maskSign((passedMaskSign < 0) ? -1 : 1) {
2246 buildMaskPhiCache();
2247 buildEffectiveVelocityCache();
2248 }
2249
2251 SmartPointer<OxidationDeformation<T, D>> passedDeformation,
2252 SmartPointer<OxidationMaskBending<T, D>> passedMaskVelocity,
2253 SmartPointer<Domain<T, D>> passedMaskInterface,
2254 SmartPointer<Domain<T, D>> passedAmbientInterface, int passedMaskSign = 1)
2255 : deformationField(passedDeformation),
2256 maskVelocityField(passedMaskVelocity),
2257 maskInterface(passedMaskInterface),
2258 ambientInterface(passedAmbientInterface),
2259 maskSign((passedMaskSign < 0) ? -1 : 1) {
2260 buildMaskPhiCache();
2261 buildEffectiveVelocityCache();
2262 }
2263
2264 template <class... Args> static auto New(Args &&...args) {
2265 return SmartPointer<OxidationConstrainedAmbient>::New(
2266 std::forward<Args>(args)...);
2267 }
2268
2269 Vec3D<T> getVectorVelocity(const Vec3D<T> &coordinate, int material,
2270 const Vec3D<T> &normalVector,
2271 unsigned long pointId) final {
2272 const T signedPhi = getSignedMaskPhi(coordinate);
2273
2274 // At/inside mask surface: kinematic constraint — oxide tracks mask exactly.
2275 if (signedPhi >= T(0) && maskVelocityField != nullptr)
2276 return maskVelocityField->getVectorVelocity(coordinate, material,
2277 normalVector, pointId);
2278 if (deformationField == nullptr)
2279 return {0., 0., 0.};
2280
2281 auto v_def = deformationField->getVectorVelocity(coordinate, material,
2282 normalVector, pointId);
2283
2284 // Near-contact gap zone: smoothly approach the mask velocity instead of
2285 // letting stress spikes just outside the mask edge advect the ambient level
2286 // set with the full oxide deformation velocity.
2287 if (maskVelocityField != nullptr) {
2288 if (signedPhi > -T(3) * maskGridDelta_) {
2289 auto v_mask = maskVelocityField->getVectorVelocity(
2290 coordinate, material, normalVector, pointId);
2291 const T blend =
2292 std::max(T(0), std::min(T(1), (signedPhi + T(3) * maskGridDelta_) /
2293 (T(3) * maskGridDelta_)));
2294 for (int k = 0; k < D; ++k)
2295 v_def[k] = (T(1) - blend) * v_def[k] + blend * v_mask[k];
2296
2297 T def_n = T(0), mask_n = T(0);
2298 for (int k = 0; k < D; ++k) {
2299 def_n += v_def[k] * normalVector[k];
2300 mask_n += v_mask[k] * normalVector[k];
2301 }
2302 if (mask_n > def_n) {
2303 const T boost = mask_n - def_n;
2304 Vec3D<T> v_out = v_def;
2305 for (int k = 0; k < D; ++k)
2306 v_out[k] += boost * normalVector[k];
2307 return v_out;
2308 }
2309 }
2310 }
2311 return v_def;
2312 }
2313
2314 T getScalarVelocity(const Vec3D<T> &coordinate, int material,
2315 const Vec3D<T> &normalVector,
2316 unsigned long pointId) final {
2317 if (getSignedMaskPhi(coordinate) >= T(0))
2318 return 0.;
2319 if (deformationField == nullptr)
2320 return 0.;
2321 return deformationField->getScalarVelocity(coordinate, material,
2322 normalVector, pointId);
2323 }
2324
2325 T getDissipationAlpha(int direction, int material,
2326 const Vec3D<T> &centralDifferences) final {
2327 if (direction < 0 || direction >= static_cast<int>(D))
2328 return T(0);
2329 (void)material;
2330 (void)centralDifferences;
2331 return maxVelocity_[direction];
2332 }
2333
2334private:
2335 // Returns maskSign * maskPhi at the grid node nearest to coordinate.
2336 // Positive → inside mask (contact), negative → outside mask (gap or free).
2337 // Uses a pre-built cache so no HRLE iterator is constructed in the hot path.
2338 // Nodes outside the narrow band return lowest() (unambiguously outside mask).
2339 T getSignedMaskPhi(const Vec3D<T> &coordinate) const {
2340 if (maskInterface == nullptr)
2341 return std::numeric_limits<T>::lowest();
2342 IndexType index;
2343 for (unsigned i = 0; i < D; ++i)
2344 index[i] = std::llround(coordinate[i] / maskGridDelta_);
2345 const auto it = maskPhiCache_.find(index);
2346 return (it != maskPhiCache_.end()) ? it->second
2347 : std::numeric_limits<T>::lowest();
2348 }
2349
2350 void buildMaskPhiCache() {
2351 maskPhiCache_.clear();
2352 if (maskInterface == nullptr)
2353 return;
2354 maskGridDelta_ = maskInterface->getGrid().getGridDelta();
2355 for (ConstSparseIterator it(maskInterface->getDomain()); !it.isFinished();
2356 ++it) {
2357 if (!it.isDefined())
2358 continue;
2359 const auto key = it.getStartIndices();
2360 maskPhiCache_[key] = static_cast<T>(maskSign) * it.getValue();
2361 }
2362 }
2363
2364 void buildEffectiveVelocityCache() {
2365 maxVelocity_.fill(T(0));
2366 if (ambientInterface == nullptr) {
2367 for (unsigned d = 0; d < D; ++d) {
2368 if (deformationField != nullptr)
2369 maxVelocity_[d] =
2370 std::max(maxVelocity_[d],
2371 deformationField->getDissipationAlpha(d, -1, {}));
2372 if (maskVelocityField != nullptr)
2373 maxVelocity_[d] =
2374 std::max(maxVelocity_[d],
2375 maskVelocityField->getDissipationAlpha(d, -1, {}));
2376 }
2377 return;
2378 }
2379
2380 const T gridDelta = ambientInterface->getGrid().getGridDelta();
2381 bool foundInterfacePoint = false;
2382 viennahrle::ConstSparseStarIterator<typename Domain<T, D>::DomainType, 1>
2383 neighborIterator(ambientInterface->getDomain());
2384 for (ConstSparseIterator it(ambientInterface->getDomain());
2385 !it.isFinished(); ++it) {
2386 if (!it.isDefined() || std::abs(it.getValue()) > T(1))
2387 continue;
2388 foundInterfacePoint = true;
2389
2390 const auto index = it.getStartIndices();
2391 neighborIterator.goToIndicesSequential(index);
2392
2393 Vec3D<T> coordinate{0., 0., 0.};
2394 Vec3D<T> normal{0., 0., 0.};
2395 T normalNorm2 = T(0);
2396 for (unsigned d = 0; d < D; ++d) {
2397 coordinate[d] = static_cast<T>(index[d]) * gridDelta;
2398 normal[d] = neighborIterator.getNeighbor(d).getValue() -
2399 neighborIterator.getNeighbor(d + D).getValue();
2400 normalNorm2 += normal[d] * normal[d];
2401 }
2402 if (normalNorm2 > std::numeric_limits<T>::epsilon()) {
2403 const T invNorm = T(1) / std::sqrt(normalNorm2);
2404 for (unsigned d = 0; d < D; ++d)
2405 normal[d] *= invNorm;
2406 }
2407
2408 const auto vectorVelocity = getVectorVelocity(coordinate, -1, normal, 0);
2409 const T scalarVelocity = getScalarVelocity(coordinate, -1, normal, 0);
2410 for (unsigned d = 0; d < D; ++d) {
2411 maxVelocity_[d] =
2412 std::max(maxVelocity_[d],
2413 std::abs(vectorVelocity[d] + scalarVelocity * normal[d]));
2414 }
2415 }
2416
2417 if (!foundInterfacePoint) {
2418 for (unsigned d = 0; d < D; ++d) {
2419 if (deformationField != nullptr)
2420 maxVelocity_[d] =
2421 std::max(maxVelocity_[d],
2422 deformationField->getDissipationAlpha(d, -1, {}));
2423 if (maskVelocityField != nullptr)
2424 maxVelocity_[d] =
2425 std::max(maxVelocity_[d],
2426 maskVelocityField->getDissipationAlpha(d, -1, {}));
2427 }
2428 }
2429 }
2430};
2431
2432} // namespace viennals
constexpr int D
Definition Epitaxy.cpp:12
double T
Definition Epitaxy.cpp:13
Class containing all information about the level set, including the dimensions of the domain,...
Definition lsDomain.hpp:27
OxidationConstrainedAmbient(SmartPointer< OxidationDeformation< T, D > > passedDeformation, SmartPointer< OxidationMaskBending< T, D > > passedMaskVelocity, SmartPointer< Domain< T, D > > passedMaskInterface, SmartPointer< Domain< T, D > > passedAmbientInterface, int passedMaskSign=1)
Definition lsOxidationMask.hpp:2250
T getDissipationAlpha(int direction, int material, const Vec3D< T > &centralDifferences) final
If lsLocalLaxFriedrichsAnalytical is used as the spatial discretization scheme, this is called to pro...
Definition lsOxidationMask.hpp:2325
T getScalarVelocity(const Vec3D< T > &coordinate, int material, const Vec3D< T > &normalVector, unsigned long pointId) final
Should return a scalar value for the velocity at coordinate for a point of material with the given no...
Definition lsOxidationMask.hpp:2314
static auto New(Args &&...args)
Definition lsOxidationMask.hpp:2264
OxidationConstrainedAmbient(SmartPointer< OxidationDeformation< T, D > > passedDeformation, SmartPointer< OxidationMaskBending< T, D > > passedMaskVelocity, SmartPointer< Domain< T, D > > passedMaskInterface, int passedMaskSign=1)
Definition lsOxidationMask.hpp:2238
Vec3D< T > getVectorVelocity(const Vec3D< T > &coordinate, int material, const Vec3D< T > &normalVector, unsigned long pointId) final
Like getScalarVelocity, but returns a velocity value for each cartesian direction.
Definition lsOxidationMask.hpp:2269
Propagates the volume expansion generated at the Si/SiO2 interface through the oxide as a Cartesian-g...
Definition lsOxidationDeformation.hpp:60
Vector velocity field for a compliant oxidation mask driven by solved oxide traction....
Definition lsOxidationMask.hpp:91
void apply()
Definition lsOxidationMask.hpp:268
void setSolveBounds(const IndexType &passedMinIndex, const IndexType &passedMaxIndex)
Definition lsOxidationMask.hpp:244
void setMaskInterface(SmartPointer< Domain< T, D > > passedMaskInterface, int passedMaskSign=1)
Definition lsOxidationMask.hpp:221
std::size_t getNumberOfSolutionNodes() const
Definition lsOxidationMask.hpp:260
void setAmbientInterface(SmartPointer< Domain< T, D > > passedAmbientInterface, int passedAmbientSign=-1)
Provide the SiO₂/ambient interface so that contact faces can be detected on any mask face that border...
Definition lsOxidationMask.hpp:232
std::size_t getNumberOfFixedNodes() const
Definition lsOxidationMask.hpp:262
void finalizeElasticAdvectionVelocity()
Definition lsOxidationMask.hpp:348
OxidationMaskBending(SmartPointer< OxidationDeformation< T, D > > passedDeformation, OxidationMaskParameters passedParameters={})
Definition lsOxidationMask.hpp:191
static SmartPointer< OxidationMaskBending > New(SmartPointer< OxidationDeformation< T, D > > passedDeformation, OxidationMaskParameters passedParameters={})
Definition lsOxidationMask.hpp:206
T getDissipationAlpha(int direction, int, const Vec3D< T > &) final
If lsLocalLaxFriedrichsAnalytical is used as the spatial discretization scheme, this is called to pro...
Definition lsOxidationMask.hpp:417
T getLastApplyVelocityChange() const
Definition lsOxidationMask.hpp:263
static SmartPointer< OxidationMaskBending > New(SmartPointer< OxidationDeformation< T, D > > passedDeformation, SmartPointer< Domain< T, D > > passedMaskInterface, OxidationMaskParameters passedParameters={}, int passedMaskSign=1)
Definition lsOxidationMask.hpp:213
unsigned getIterations() const
Definition lsOxidationMask.hpp:258
std::size_t getNumberOfContactNodes() const
Definition lsOxidationMask.hpp:261
void writeFieldsToLevelSet()
Write mask bending velocity into maskInterface->getPointData() so that lsInterior + lsAdvect carry it...
Definition lsOxidationMask.hpp:424
OxidationMaskBending(SmartPointer< OxidationDeformation< T, D > > passedDeformation, SmartPointer< Domain< T, D > > passedMaskInterface, OxidationMaskParameters passedParameters={}, int passedMaskSign=1)
Definition lsOxidationMask.hpp:196
OxidationMaskParameters getParameters() const
Definition lsOxidationMask.hpp:257
void clearSolveBounds()
Definition lsOxidationMask.hpp:252
Vec3D< T > getVectorVelocity(const Vec3D< T > &coordinate, int material, const Vec3D< T > &, unsigned long) final
Like getScalarVelocity, but returns a velocity value for each cartesian direction.
Definition lsOxidationMask.hpp:383
void setParameters(OxidationMaskParameters passedParameters)
Definition lsOxidationMask.hpp:239
T getResidual() const
Definition lsOxidationMask.hpp:259
T getLastApplyAbsoluteVelocityChange() const
Definition lsOxidationMask.hpp:264
Common Cartesian-grid infrastructure shared by the three oxidation solver classes (diffusion,...
Definition lsOxidationSolverBase.hpp:43
static constexpr std::size_t noNode
Definition lsOxidationSolverBase.hpp:49
bool crosses(T a, T b) const
Definition lsOxidationSolverBase.hpp:63
std::size_t lookupNode(const IndexType &index) const
Definition lsOxidationSolverBase.hpp:90
std::size_t linearIndex(const IndexType &index) const
Definition lsOxidationSolverBase.hpp:96
void initNodeLookup()
Definition lsOxidationSolverBase.hpp:83
bool inBounds(const IndexType &index) const
Definition lsOxidationSolverBase.hpp:76
std::array< std::size_t, D > strides
Definition lsOxidationSolverBase.hpp:54
T gridDelta
Definition lsOxidationSolverBase.hpp:55
bool initializeGridFromMask(SmartPointer< Domain< T, D > > maskInterface, bool useRequestedBounds, const IndexType &requestedMinIndex, const IndexType &requestedMaxIndex, std::size_t maxGridPoints, const std::string &solverName)
Definition lsOxidationSolverBase.hpp:230
std::vector< std::size_t > nodeLookupFlat
Definition lsOxidationSolverBase.hpp:50
std::array< std::size_t, D > extents
Definition lsOxidationSolverBase.hpp:53
viennahrle::ConstSparseIterator< typename Domain< T, D >::DomainType > ConstSparseIterator
Definition lsOxidationSolverBase.hpp:46
T valueAt(ConstSparseIterator &it, const IndexType &index) const
Definition lsOxidationSolverBase.hpp:71
bool increment(IndexType &index) const
Definition lsOxidationSolverBase.hpp:105
IndexType minIndex
Definition lsOxidationSolverBase.hpp:51
std::size_t findNearbyNode(const IndexType &index) const
Definition lsOxidationSolverBase.hpp:119
IndexType maxIndex
Definition lsOxidationSolverBase.hpp:52
float gridDelta
Definition AirGapDeposition.py:61
relaxation
Definition LOCOSOxidation.py:111
dict v
Definition LOCOSOxidation.py:217
T levelSetCrossingDistance(T insidePhi, T outsidePhi, T minBoundaryFraction, T gridDelta)
Definition lsOxidationSolverBase.hpp:29
T clampLevelSetPhi(T v)
Clamp HRLE far-field sentinels (±DBL_MAX) to ±1 before differencing to prevent DBL_MAX² overflow that...
Definition lsOxidationSolverBase.hpp:24
Definition lsAdvect.hpp:41
Definition lsOxidationMask.hpp:17
double creepActivationEnergy
Definition lsOxidationMask.hpp:37
int anchorBoundarySide
Definition lsOxidationMask.hpp:80
int contactMode
Definition lsOxidationMask.hpp:30
std::size_t maxGridPoints
Definition lsOxidationMask.hpp:73
unsigned anchorBoundaryLayers
Definition lsOxidationMask.hpp:81
double minBoundaryDistance
Definition lsOxidationMask.hpp:71
double stressTimeStep
Definition lsOxidationMask.hpp:44
double referenceViscosity
Definition lsOxidationMask.hpp:36
double contactReleaseFraction
Definition lsOxidationMask.hpp:61
double multigridSmootherOmega
Definition lsOxidationMask.hpp:65
int material
Definition lsOxidationMask.hpp:74
double poissonRatio
Definition lsOxidationMask.hpp:45
double referenceTemperature
Definition lsOxidationMask.hpp:35
int anchorBoundaryDirection
Definition lsOxidationMask.hpp:79
double tolerance
Definition lsOxidationMask.hpp:66
unsigned maxIterations
Definition lsOxidationMask.hpp:72
double youngModulus
Definition lsOxidationMask.hpp:40
bool unilateralContact
Definition lsOxidationMask.hpp:48
double relaxation
Definition lsOxidationMask.hpp:51
double temperature
Definition lsOxidationMask.hpp:34
double contactLoadRelaxation
Definition lsOxidationMask.hpp:56