ViennaLS
Loading...
Searching...
No Matches
lsOxidationDiffusion.hpp
Go to the documentation of this file.
1#pragma once
2
4#include <lsVelocityField.hpp>
5
6#include <algorithm>
7#include <array>
8#include <cmath>
9#include <cstdio>
10#include <optional>
11#include <stdexcept>
12#include <string>
13#include <unordered_map>
14#include <utility>
15#include <vector>
16
17#include <vcTimer.hpp>
18
19// AMGCL's solver parameters as plain structs (no Boost property trees).
20#ifndef AMGCL_NO_BOOST
21#define AMGCL_NO_BOOST
22#endif
23#include <amgcl/adapter/crs_tuple.hpp>
24#include <amgcl/amg.hpp>
25#include <amgcl/backend/builtin.hpp>
26#include <amgcl/coarsening/smoothed_aggregation.hpp>
27#include <amgcl/make_solver.hpp>
28#include <amgcl/relaxation/ilu0.hpp>
29#include <amgcl/solver/bicgstab.hpp>
30
31#include <omp.h>
32
33#ifdef VIENNALS_GPU_BICGSTAB
35#endif
36
37namespace viennals {
38
43enum class GpuMode {
52};
53
58 double reactionRate = 1.;
63 double velocitySign = 1.;
64
65 // Stress coupling for reaction rate (Kao et al., IEEE TED 35, 25 (1988);
66 // the "Vr" of SUPREM-IV and TCAD): the new oxide expands against the
67 // stress NORMAL to the Si/SiO2 interface,
68 // k_eff = k * exp((sigma_nn + p_ref) * V_k / (k_B * T)),
69 // with sigma_nn = n.sigma.n the oxide's Cauchy stress (tension positive) at
70 // the interface, so normal compression suppresses the rate. This is not
71 // the pressure: at a convex surface the interface is in normal compression
72 // while the oxide pressure there can be tensile.
73 // Activation volumes are in m^3, stresses in Pa, temperature in K.
74 double temperature = 1273.15;
76 double referencePressure = 0.;
77
78 // Stress coupling for diffusion coefficient:
79 // D_eff = D * exp(-(p - p_ref) * V_D / (k_B * T)).
81
82 // Crystal orientation factor on reaction rate.
83 // k(n) = k * [1 + (reactionRateRatio111 - 1) * (1 - (n . crystalAxis)^2)]
84 // reactionRateRatio111 = 1 disables the correction (isotropic).
85 // crystalAxis is the wafer normal: the default is the 2-D convention (y
86 // up); a 3-D domain with z up needs (0, 0, 1) (ViennaPS sets the last
87 // axis).
89 Vec3D<double> crystalAxis = {0., 1., 0.};
90
92 double maskConcentration = 0.;
101 double minBoundaryDistance = 1e-6;
102 unsigned maxIterations = 10000;
103 double tolerance = 1e-8;
104 double relaxation = 1.;
105 std::size_t maxGridPoints = 5000000;
106 int material = -1;
107};
108
124template <class T, int D>
125class OxidationDiffusion final : public VelocityField<T>,
126 public OxidationSolverBase<T, D> {
127 using IndexType = viennahrle::Index<D>;
128 using ConstSparseIterator =
129 viennahrle::ConstSparseIterator<typename Domain<T, D>::DomainType>;
130
131private:
132 static constexpr T boltzmannConstant = T(1.380649e-23);
133 static constexpr T minStressFactor = T(1.e-6);
134 static constexpr T maxStressFactor = T(1.e6);
135
136 // bring base members into scope
154
155 enum class Boundary { NONE, REACTION, AMBIENT, MASK };
156
157 struct Node {
158 IndexType index;
159 T concentration = 0.;
160 Vec3D<T> siNormal = {0., 0.,
161 0.}; // unit outward normal of Si surface (into oxide)
162 // Node lies inside the mask body (meshed mask diffusion): its diffusion
163 // coefficient is D / maskPermeabilityRatio instead of D.
164 bool inMask = false;
165 };
166
167 struct StencilSide {
168 T distance = 1.;
169 T nodeCoefficient = 1.;
170 T constant = 0.;
171 };
172
173 SmartPointer<Domain<T, D>> reactionInterface = nullptr;
174 SmartPointer<Domain<T, D>> ambientInterface = nullptr;
175 SmartPointer<Domain<T, D>> maskInterface = nullptr;
176 OxidationParameters parameters;
177 int reactionSign = 1;
178 int ambientSign = -1;
179 int maskSign = 1;
180 IndexType requestedMinIndex{};
181 IndexType requestedMaxIndex{};
182 unsigned iterations = 0;
183 T residual = std::numeric_limits<T>::max();
184 T normalizedResidual_ = std::numeric_limits<T>::max();
185 bool lastSolveConverged_ = false;
186 T maxScalarVelocity_ = 0.;
187 bool solved = false;
188 bool nodesDirty_ = true; // true → rebuild grid/nodes on next apply()
189 mutable std::string
190 lastLoggedBackend_; // suppresses repeated "using X" messages
191 bool useRequestedBounds = false;
192 bool warmStartable_ =
193 false; // true when nodes[i].concentration holds a prior solution
194 std::unordered_map<std::size_t, T> pressureLookup;
195 // Oxide Cauchy stress per node (row-major 3x3), fed back by OxidationModel
196 // for the normal-stress reaction law.
197 std::unordered_map<std::size_t, std::array<T, 9>> stressLookup;
198 // Interface normal stress per node for the reaction law (NaN = none);
199 // see updateInterfaceNormalStress.
200 std::vector<T> interfaceNormalStress_;
201 std::vector<T> lawPressure_;
202 std::unordered_map<std::size_t, T> concentrationCache_;
203 std::vector<Node> nodes;
204 // Face-major flat BC arrays: index = fi * n + nodeId, where fi in [0, 2*D).
205 std::vector<Boundary> faceBCTypes_;
206 std::vector<T> faceBCDists_;
207 // |n_axis| of the Si interface normal on REACTION faces (see
208 // reactionBoundarySide); 1 elsewhere.
209 std::vector<T> faceReactionNormal_;
210 // The Si front's interface points are the crossings of the REACTION faces
211 // (node + faceBCDists_ along the face's axis). Everything the front does
212 // is evaluated there, never per node: the normal (the level-set gradient
213 // interpolated to the crossing), the reaction rate k (stress and
214 // orientation laws), and the front speed k C_i / (N alpha) with the
215 // crossing's own interface concentration C_i. The oxidant sink, the
216 // advection speed of every level-set point and the expansion velocity of
217 // every mechanics ghost interpolate these along the front. Per-node values
218 // copied to nearby points (the earlier form) made the speed depend on where
219 // the front sat in its cell, and the stress feedback grew that stair-step
220 // pattern into a corrugated front.
221 std::vector<Vec3D<T>> faceCrossingNormal_;
222 std::vector<T> faceReactionRate_;
223 std::vector<T> faceFrontSpeed_;
224 // The REACTION crossings node by node (faces in order) with what
225 // frontVelocityAt sums over them, rebuilt with the front speeds: a query
226 // reads one short contiguous list per nearby node instead of 2D
227 // face-major arrays at stride n (every mechanics ghost asks for it).
228 struct FrontCrossing {
229 Vec3D<T> point; // index units
230 T areaWeight; // faceReactionNormal_
231 T speed; // faceFrontSpeed_
232 Vec3D<T> normal;
233 };
234 std::vector<std::size_t> frontStart_; // per node, n + 1 entries
235 std::vector<FrontCrossing> frontCrossings_;
236 // Point on the front (index units) a reaction node's law stress is sampled
237 // at: the node moved back along its normal by its front distance.
238 std::vector<Vec3D<T>> interfaceAnchor_;
239 // Neighbor node IDs for every face (geometry-only, rebuilt with nodes).
240 // Entry == noNode for boundary / out-of-bounds faces.
241 std::vector<std::size_t> neighborIds_;
242
243 // The solve runs on the GPU from this many nodes (with the GPU mode). Per
244 // solve on 3-D LOCOS corners, CPU (10 threads) / GPU: 7.3k nodes 11.5 /
245 // 12.7 ms, 25k 22.6 / 19.0 ms, 69k 66 / 46 ms (the AMG setup stays on the
246 // host).
247 static constexpr std::size_t gpuMinNodes = 20000;
248 GpuMode gpuMode_ = GpuMode::Cpu;
249 bool gpuOff_ = false; // Auto mode after the GPU was unavailable
250
251public:
257 bool found = false;
258 IndexType nodeIndex{};
261 unsigned crossingAxis = 0; // Cartesian axis of the crossing edge
262 int crossingOffset = 0; // +1 or -1: which neighbour the crossing faces
263 };
264
266
267 OxidationDiffusion(SmartPointer<Domain<T, D>> passedReactionInterface,
268 SmartPointer<Domain<T, D>> passedAmbientInterface,
269 OxidationParameters passedParameters = {})
270 : reactionInterface(passedReactionInterface),
271 ambientInterface(passedAmbientInterface), parameters(passedParameters) {
272 }
273
274 template <class... Args> static auto New(Args &&...args) {
275 return SmartPointer<OxidationDiffusion>::New(std::forward<Args>(args)...);
276 }
277
281 nodesDirty_ = true;
282 solved = false;
283 }
284
285 void setReactionInterface(SmartPointer<Domain<T, D>> passedInterface) {
286 reactionInterface = passedInterface;
287 nodesDirty_ = true;
288 solved = false;
289 }
290
291 void setAmbientInterface(SmartPointer<Domain<T, D>> passedInterface) {
292 ambientInterface = passedInterface;
293 nodesDirty_ = true;
294 solved = false;
295 }
296
297 void setMaskInterface(SmartPointer<Domain<T, D>> passedInterface,
298 int passedMaskSign = 1) {
299 maskInterface = passedInterface;
300 maskSign = (passedMaskSign < 0) ? -1 : 1;
301 nodesDirty_ = true;
302 solved = false;
303 }
304
306 maskInterface = nullptr;
307 nodesDirty_ = true;
308 solved = false;
309 }
310
311 void setParameters(OxidationParameters passedParameters) {
312 parameters = passedParameters;
313 solved = false;
314 }
315
316 OxidationParameters getParameters() const { return parameters; }
317
318 T getEffectiveReactionRate(const Vec3D<T> &coordinate) const {
319 IndexType index;
320 for (unsigned i = 0; i < D; ++i)
321 index[i] = std::llround(coordinate[i] / gridDelta);
322 return getEffectiveReactionRate(index);
323 }
324
326 pressureLookup.clear();
327 stressLookup.clear();
328 solved = false;
329 }
330
333 void setStress(const IndexType &index, const std::array<T, 9> &stress) {
334 const auto key = detail::gridIndexHash<D>(index);
335 if (std::all_of(stress.begin(), stress.end(),
336 [](T value) { return std::isfinite(value); }))
337 stressLookup[key] = stress;
338 else
339 stressLookup.erase(key);
340 solved = false;
341 }
342
345 void getFeedback(const IndexType &index, T &pressure,
346 std::array<T, 9> &stress) const {
347 const auto key = detail::gridIndexHash<D>(index);
348 const auto p = pressureLookup.find(key);
349 pressure =
350 p != pressureLookup.end() ? p->second : T(parameters.referencePressure);
351 const auto s = stressLookup.find(key);
352 if (s != stressLookup.end())
353 stress = s->second;
354 else
355 stress.fill(T(0));
356 }
357
358 using StressLookup = std::unordered_map<std::size_t, std::array<T, 9>>;
359
360 using PressureLookup = std::unordered_map<std::size_t, T>;
361
367 const PressureLookup &getPressureCache() const { return pressureLookup; }
368 const StressLookup &getStressCache() const { return stressLookup; }
369 void setFeedbackCache(PressureLookup pressures, StressLookup stresses) {
370 pressureLookup = std::move(pressures);
371 stressLookup = std::move(stresses);
372 solved = false;
373 }
374
377 const std::vector<T> &getInterfaceNormalStress() const {
378 return interfaceNormalStress_;
379 }
380
385 std::vector<T> interfaceNormalStressFor(const StressLookup &lookup) const {
386 const std::size_t n = nodes.size();
387 constexpr T nan = std::numeric_limits<T>::quiet_NaN();
388 std::vector<T> value(n, nan);
389 if (faceBCTypes_.size() != 2 * D * n)
390 return value;
391 for (std::size_t i = 0; i < n; ++i) {
392 if (nodes[i].inMask || !reactionNode(i))
393 continue;
394 const auto &normal = nodes[i].siNormal;
395 Vec3D<T> into = normal; // into the oxide
396 for (unsigned d = 0; d < D; ++d)
397 into[d] *= static_cast<T>(reactionSign);
398 const auto normalStress = [&](std::size_t j) {
399 const auto found =
400 lookup.find(detail::gridIndexHash<D>(nodes[j].index));
401 if (found == lookup.end())
402 return std::numeric_limits<T>::quiet_NaN();
403 T v = T(0);
404 for (unsigned r = 0; r < D; ++r)
405 for (unsigned c = 0; c < D; ++c)
406 v += normal[r] * found->second[3 * r + c] * normal[c];
407 return v;
408 };
409 T sample;
410 if (frontStressAt(i, into, frontDistance(nodes[i].index), T(0),
411 normalStress, sample) ||
412 medianNormalStress(nodes[i].index, lookup, sample))
413 value[i] = sample;
414 }
415 std::vector<T> next(n);
416 for (unsigned pass = 0; pass < 6; ++pass) {
417 for (std::size_t i = 0; i < n; ++i) {
418 next[i] = value[i];
419 if (!std::isfinite(value[i]))
420 continue;
421 T sum = value[i];
422 unsigned count = 1;
423 IndexType offset;
424 offset.fill(-1);
425 while (true) {
426 bool centre = true;
427 IndexType neighbor = nodes[i].index;
428 for (unsigned d = 0; d < D; ++d) {
429 neighbor[d] += offset[d];
430 centre = centre && offset[d] == 0;
431 }
432 if (!centre && inBounds(neighbor)) {
433 const std::size_t j = lookupNode(neighbor);
434 if (j != noNode && std::isfinite(value[j])) {
435 sum += value[j];
436 ++count;
437 }
438 }
439 unsigned d = 0;
440 while (d < D && offset[d] == 1)
441 offset[d++] = -1;
442 if (d == D)
443 break;
444 ++offset[d];
445 }
446 next[i] = sum / static_cast<T>(count);
447 }
448 value.swap(next);
449 }
450 return value;
451 }
452
453 void setStress(const Vec3D<T> &coordinate, const std::array<T, 9> &stress) {
454 IndexType index;
455 for (unsigned i = 0; i < D; ++i)
456 index[i] = std::llround(coordinate[i] / gridDelta);
457 setStress(index, stress);
458 }
459
460 void setPressure(const IndexType &index, T pressure) {
461 const auto key = detail::gridIndexHash<D>(index);
462 if (std::isfinite(pressure))
463 pressureLookup[key] = pressure;
464 else
465 pressureLookup.erase(key);
466 solved = false;
467 }
468
469 void setPressure(const Vec3D<T> &coordinate, T pressure) {
470 IndexType index;
471 for (unsigned i = 0; i < D; ++i)
472 index[i] = std::llround(coordinate[i] / gridDelta);
473 setPressure(index, pressure);
474 }
475
478 void setOxideSigns(int passedReactionSign, int passedAmbientSign) {
479 reactionSign = (passedReactionSign < 0) ? -1 : 1;
480 ambientSign = (passedAmbientSign < 0) ? -1 : 1;
481 solved = false;
482 }
483
486 void setSolveBounds(const IndexType &passedMinIndex,
487 const IndexType &passedMaxIndex) {
488 requestedMinIndex = passedMinIndex;
489 requestedMaxIndex = passedMaxIndex;
490 useRequestedBounds = true;
491 nodesDirty_ = true;
492 solved = false;
493 }
494
496 useRequestedBounds = false;
497 nodesDirty_ = true;
498 solved = false;
499 }
500
501 void apply() {
502 if (reactionInterface == nullptr || ambientInterface == nullptr) {
503 Logger::getInstance()
504 .addError("OxidationDiffusion: Missing level-set "
505 "interface.")
506 .print();
507 return;
508 }
509
510 if (nodesDirty_) {
511 if (!initialiseGrid())
512 return; // base class already logged the error
513 buildNodes();
514 nodesDirty_ = false;
515 if (nodes.empty())
516 Logger::getInstance()
517 .addWarning("OxidationDiffusion: no oxide nodes found after "
518 "buildNodes(). Verify that the reaction and ambient "
519 "level sets enclose a non-empty oxide band.")
520 .print();
521 }
522 if (parameters.reactionActivationVolume != T(0))
523 updateInterfaceNormalStress();
524 if (parameters.diffusionActivationVolume != T(0))
525 updateLawPressure();
526 updateFaceRates();
527 solveDiffusion();
528
529 concentrationCache_.clear();
530 for (const auto &node : nodes)
531 concentrationCache_[detail::gridIndexHash<D>(node.index)] =
532 node.concentration;
533
534 updateFaceSpeeds();
535 maxScalarVelocity_ = 0.;
536 for (const T speed : faceFrontSpeed_)
537 maxScalarVelocity_ = std::max(maxScalarVelocity_, speed);
538
539 solved = true;
540 }
541
542 T getScalarVelocity(const Vec3D<T> &coordinate, int material,
543 const Vec3D<T> &normalVector,
544 unsigned long /*pointId*/) final {
545 if (!solved)
546 apply();
547
548 if (parameters.material >= 0 && material != parameters.material)
549 return 0.;
550
551 IndexType index;
552 for (unsigned i = 0; i < D; ++i)
553 index[i] = std::llround(coordinate[i] / gridDelta);
554
555 // Meshed mask: mask nodes carry oxidant concentration but DO NOT react.
556 // A reaction speed queried at or nearest to a mask point is zero — never
557 // k*C from the film's near-C_eq concentration. Without this, velocity
558 // fallbacks that sample the reaction speed at mask coordinates invent a
559 // full oxidation rate inside the mask body.
560 {
561 std::size_t nodeId = lookupNode(index);
562 if (nodeId == noNode)
563 nodeId = findNearbyNode(index);
564 if (nodeId != noNode && nodes[nodeId].inMask)
565 return 0.;
566 }
567
568 // The front speed at the point of the front closest to this one,
569 // x - phi grad(phi) / |grad(phi)|.
570 {
571 ConstSparseIterator reactionIt(reactionInterface->getDomain());
572 const T phi = valueAt(reactionIt, index);
573 if (std::isfinite(phi) && std::abs(phi) <= T(2)) {
574 const auto g = computeSiNormal(index, reactionIt);
575 Vec3D<T> p{T(0), T(0), T(0)};
576 for (unsigned d = 0; d < D; ++d)
577 p[d] = (static_cast<T>(index[d]) - phi * g[d]) * gridDelta;
578 const auto front = frontVelocityAt(p);
579 if (front.found)
580 return parameters.velocitySign * front.speed;
581 }
582 }
583
584 const auto boundarySample = reactionBoundarySample(index);
585 const IndexType rateIndex =
586 boundarySample.found ? boundarySample.nodeIndex : index;
587 // (getReactionBoundaryConcentration would repeat the search that failed)
588 const T concentration = boundarySample.found ? boundarySample.concentration
589 : getConcentration(index);
590 return parameters.velocitySign * getEffectiveReactionRate(rateIndex) *
591 concentration /
592 (parameters.oxidantMoleculeDensity *
593 parameters.expansionCoefficient);
594 }
595
601 bool found = false;
602 T speed = 0.;
603 Vec3D<T> normal{0., 0., 0.};
604 };
605
606 FrontVelocity frontVelocityAt(const Vec3D<T> &coordinate) const {
607 FrontVelocity result;
608 const std::size_t n = nodes.size();
609 if (frontStart_.size() != n + 1)
610 return result;
611 Vec3D<T> p{T(0), T(0), T(0)};
612 for (unsigned d = 0; d < D; ++d)
613 p[d] = coordinate[d] / gridDelta;
614 constexpr T width = T(0.75);
615 T weightSum = T(0), speed = T(0);
616 Vec3D<T> normal{T(0), T(0), T(0)};
617 forEachNodeNear(p, 2, [&](std::size_t j) {
618 for (std::size_t k = frontStart_[j]; k < frontStart_[j + 1]; ++k) {
619 const auto &crossing = frontCrossings_[k];
620 T r2 = T(0);
621 for (unsigned d = 0; d < D; ++d)
622 r2 += (crossing.point[d] - p[d]) * (crossing.point[d] - p[d]);
623 // weighted by the crossing's share of the front area, |n_axis|
624 // (the factor its oxidant sink carries): an area average, so the
625 // extra crossings at stair steps do not count twice, and grazing
626 // arms -- the least accurate interface concentration -- hardly count
627 const T w =
628 std::exp(-r2 / (T(2) * width * width)) * crossing.areaWeight;
629 weightSum += w;
630 speed += w * crossing.speed;
631 for (unsigned d = 0; d < D; ++d)
632 normal[d] += w * crossing.normal[d];
633 }
634 });
635 if (weightSum <= T(1e-12))
636 return result;
637 T len = T(0);
638 for (unsigned d = 0; d < D; ++d)
639 len += normal[d] * normal[d];
640 len = std::sqrt(len);
641 if (len <= std::numeric_limits<T>::epsilon())
642 return result;
643 result.found = true;
644 result.speed = speed / weightSum;
645 for (unsigned d = 0; d < D; ++d)
646 result.normal[d] = normal[d] / len;
647 return result;
648 }
649
650 T getDissipationAlpha(int /*direction*/, int material,
651 const Vec3D<T> & /*centralDifferences*/) final {
652 if (parameters.material >= 0 && material != parameters.material)
653 return 0.;
654
655 T bulkVel =
656 std::abs(parameters.velocitySign) * parameters.reactionRate *
657 parameters.equilibriumConcentration /
658 (parameters.oxidantMoleculeDensity * parameters.expansionCoefficient);
659
660 return std::max(maxScalarVelocity_, bulkVel);
661 }
662
663 T getConcentration(const Vec3D<T> &coordinate) const {
664 IndexType index;
665 for (unsigned i = 0; i < D; ++i)
666 index[i] = std::llround(coordinate[i] / gridDelta);
667 return getConcentration(index);
668 }
669
670 T getConcentration(const IndexType &index) const {
671 const std::size_t nodeId = lookupNode(index);
672 if (nodeId == noNode) {
673 const auto nearby = findNearbyNode(index);
674 if (nearby == noNode)
675 return 0.;
676 return nodes[nearby].concentration;
677 }
678 return nodes[nodeId].concentration;
679 }
680
681 T getReactionBoundaryConcentration(const Vec3D<T> &coordinate) const {
682 IndexType index;
683 for (unsigned i = 0; i < D; ++i)
684 index[i] = std::llround(coordinate[i] / gridDelta);
686 }
687
688 T getReactionBoundaryConcentration(const IndexType &index) const {
689 const auto sample = reactionBoundarySample(index);
690 return sample.found ? sample.concentration : getConcentration(index);
691 }
692
693 unsigned getIterations() const { return iterations; }
694 T getResidual() const { return residual; }
695 T getNormalizedResidual() const { return normalizedResidual_; }
696 bool lastSolveConverged() const { return lastSolveConverged_; }
697 std::size_t getNumberOfSolutionNodes() const { return nodes.size(); }
699 for (const auto &node : nodes)
700 if (!std::isfinite(node.concentration))
701 return false;
702 return true;
703 }
704
705 const std::unordered_map<std::size_t, T> &getConcentrationCache() const {
706 return concentrationCache_;
707 }
708
709 void setConcentrationCache(std::unordered_map<std::size_t, T> cache) {
710 concentrationCache_ = std::move(cache);
711 }
712
715 void setGpuMode(GpuMode mode) { gpuMode_ = mode; }
716
720 void markSolved() { solved = true; }
721
726 if (nodes.empty() || ambientInterface == nullptr)
727 return;
728
729 std::vector<T> concentrations;
730 ConstSparseIterator it(ambientInterface->getDomain());
731 for (; !it.isFinished(); ++it) {
732 if (!it.isDefined())
733 continue;
734 const IndexType idx = it.getStartIndices();
735 const std::size_t nodeId = lookupNode(idx);
736 // Non-solve-grid narrow-band points (mask interior, gas-phase boundary)
737 // get concentration 0. Using equilibriumConcentration here contaminates
738 // the output: mask-interior points are impermeable (C≈0), and newly
739 // appeared bird's-beak points inherit C=1 spuriously after advection.
740 // The in-memory concentrationCache_ is the warm-start source for the
741 // next substep; the level-set value is only the fallback when that cache
742 // is cold, and C=0 converges just as quickly as C=1 from a cold start.
743 const T value = nodeId != noNode ? nodes[nodeId].concentration : T(0);
744 concentrations.push_back(std::isfinite(value) ? value : T(0));
745 }
746 ambientInterface->getPointData().insertReplaceScalarData(
747 std::move(concentrations), "OxConcentration");
748 }
749
753 if (pressureLookup.empty() || ambientInterface == nullptr)
754 return;
755
756 std::vector<T> pressures;
757 ConstSparseIterator it(ambientInterface->getDomain());
758 for (; !it.isFinished(); ++it) {
759 if (!it.isDefined())
760 continue;
761 const IndexType idx = it.getStartIndices();
762 const auto pIt = pressureLookup.find(detail::gridIndexHash<D>(idx));
763 const T value = pIt != pressureLookup.end() ? pIt->second : T(0);
764 pressures.push_back(std::isfinite(value) ? value : T(0));
765 }
766 ambientInterface->getPointData().insertReplaceScalarData(
767 std::move(pressures), "OxPressure");
768 }
769
775
779 ReactionBoundarySample
780 getReactionBoundarySample(const Vec3D<T> &coordinate) const {
781 IndexType index;
782 for (unsigned i = 0; i < D; ++i)
783 index[i] = std::llround(coordinate[i] / gridDelta);
784 return reactionBoundarySample(index);
785 }
786
790 if (!sample.found)
791 return T(0);
792 return std::abs(
794 (parameters.oxidantMoleculeDensity * parameters.expansionCoefficient));
795 }
796
797private:
798 bool initialiseGrid() {
800 reactionInterface, ambientInterface, maskInterface, useRequestedBounds,
801 requestedMinIndex, requestedMaxIndex, parameters.maxGridPoints,
802 "OxidationDiffusion");
803 }
804
805 void buildNodes() {
806 nodes.clear();
807 frontStart_.clear(); // rebuilt with the front speeds
809
810 // On the first substep after an outer-step boundary the in-memory cache is
811 // empty. Fall back to the concentration stored in the level set's pointData
812 // (written by writeConcentrationToLevelSet() before the previous advection
813 // and remapped by lsAdvect + lsInterior).
814 warmStartable_ = !concentrationCache_.empty();
815 if (!warmStartable_) {
816 // Single HRLE pass restores both concentration and pressure from the
817 // pointData written by writePersistentFields() before the last advection.
818 const int cIdx = ambientInterface->getPointData().getScalarDataIndex(
819 "OxConcentration");
820 const int pIdx =
821 ambientInterface->getPointData().getScalarDataIndex("OxPressure");
822 const auto *cd =
823 (cIdx != -1) ? ambientInterface->getPointData().getScalarData(cIdx)
824 : nullptr;
825 const auto *pd =
826 (pIdx != -1) ? ambientInterface->getPointData().getScalarData(pIdx)
827 : nullptr;
828 // The normal-stress reaction law starts from the previous step's oxide
829 // stress: the deviatoric end-of-step stress the deformation solver
830 // carries with the flow (OxStressR0..2) minus the pressure.
831 const auto &pointData = ambientInterface->getPointData();
832 const std::array<int, 3> sIdx = {
833 pointData.getVectorDataIndex("OxStressR0"),
834 pointData.getVectorDataIndex("OxStressR1"),
835 pointData.getVectorDataIndex("OxStressR2")};
836 const bool stressWarmStart = parameters.reactionActivationVolume != 0. &&
837 pd != nullptr && sIdx[0] != -1 &&
838 sIdx[1] != -1 && sIdx[2] != -1;
839 if (cd != nullptr || pd != nullptr) {
840 ConstSparseIterator it(ambientInterface->getDomain());
841 for (; !it.isFinished(); ++it) {
842 if (!it.isDefined())
843 continue;
844 const auto ptId = it.getPointId();
845 const std::size_t key =
846 detail::gridIndexHash<D>(it.getStartIndices());
847 if (cd != nullptr && ptId < static_cast<decltype(ptId)>(cd->size()) &&
848 std::isfinite((*cd)[ptId]))
849 concentrationCache_[key] = (*cd)[ptId];
850 if (pd != nullptr && ptId < static_cast<decltype(ptId)>(pd->size()) &&
851 std::isfinite((*pd)[ptId]))
852 pressureLookup[key] = (*pd)[ptId];
853 if (stressWarmStart &&
854 ptId < static_cast<decltype(ptId)>(pd->size())) {
855 std::array<T, 9> stress{};
856 bool valid = std::isfinite((*pd)[ptId]);
857 for (unsigned r = 0; r < 3 && valid; ++r) {
858 const auto *rows = pointData.getVectorData(sIdx[r]);
859 valid = rows != nullptr &&
860 ptId < static_cast<decltype(ptId)>(rows->size());
861 for (unsigned c = 0; c < 3 && valid; ++c)
862 stress[3 * r + c] = T((*rows)[ptId][c]);
863 }
864 if (valid) {
865 for (unsigned d = 0; d < 3; ++d)
866 stress[4 * d] -= T((*pd)[ptId]);
867 stressLookup[key] = stress;
868 }
869 }
870 }
871 warmStartable_ = !concentrationCache_.empty();
872 }
873 }
874
875 ConstSparseIterator reactionIt(reactionInterface->getDomain());
876 ConstSparseIterator ambientIt(ambientInterface->getDomain());
877 auto maskIt = makeMaskIterator();
878
879 // Meshed mask diffusion: with gamma > 0 the mask body joins the diffusion
880 // domain as nodes of their own (D/gamma), so the oxidant concentration is
881 // solved THROUGH the film instead of being lumped into a boundary
882 // coefficient.
883 const bool meshMask =
884 maskInterface != nullptr && parameters.maskPermeabilityRatio > 0.;
885
886 IndexType index = minIndex;
887 while (true) {
888 const T reactionPhi = valueAt(reactionIt, index);
889 const T ambientPhi = valueAt(ambientIt, index);
890 const bool insideMask = isInsideMask(maskIt, index);
891 constexpr T eps = T(1e-9);
892 const bool oxideNode =
893 isInsideOxide(reactionPhi, ambientPhi) && !insideMask;
894 const bool maskNode =
895 meshMask && insideMask && reactionSign * reactionPhi >= -eps;
896 if (oxideNode || maskNode) {
897 const std::size_t id = nodes.size();
898 nodeLookupFlat[linearIndex(index)] = id;
899 T seedConc = parameters.equilibriumConcentration;
900 auto cacheIt =
901 concentrationCache_.find(detail::gridIndexHash<D>(index));
902 if (cacheIt != concentrationCache_.end())
903 seedConc = cacheIt->second;
904 if (!std::isfinite(seedConc))
905 seedConc = parameters.equilibriumConcentration;
906 Node newNode{index, seedConc};
907 newNode.inMask = maskNode;
908 // The orientation factor and the normal-stress reaction law both
909 // need the interface normal.
910 if (!maskNode && (parameters.reactionRateRatio111 != T(1) ||
911 parameters.reactionActivationVolume != T(0)))
912 newNode.siNormal = computeSiNormal(index, reactionIt);
913 nodes.push_back(newNode);
914 }
915
916 if (!increment(index))
917 break;
918 }
919
920 // Precompute per-face boundary intersections into flat face-major arrays.
921 // Level-set positions are fixed during the inner solve; one pass per
922 // apply().
923 const std::size_t n = nodes.size();
924 faceBCTypes_.assign(2 * D * n, Boundary::NONE);
925 faceBCDists_.assign(2 * D * n, T(1));
926 faceReactionNormal_.assign(2 * D * n, T(1));
927 faceCrossingNormal_.assign(2 * D * n, Vec3D<T>{T(0), T(0), T(0)});
928 ConstSparseIterator faceReactionIt(reactionInterface->getDomain());
929 ConstSparseIterator faceAmbientIt(ambientInterface->getDomain());
930 auto faceMaskIt = makeMaskIterator();
931 for (std::size_t id = 0; id < n; ++id) {
932 const auto &node = nodes[id];
933 for (unsigned dir = 0; dir < D; ++dir) {
934 for (int off : {-1, 1}) {
935 const unsigned fi = dir * 2u + (off == 1 ? 1u : 0u);
936 IndexType nb = node.index;
937 nb[dir] += off;
938 if (!inBounds(nb) || lookupNode(nb) != noNode)
939 continue; // NONE/1.0 already set by assign()
940 auto bc = classifyBoundary(faceReactionIt, faceAmbientIt, faceMaskIt,
941 node.index, nb);
942 if (node.inMask) {
943 // Faces of a meshed mask node. Crossing the mask level set here
944 // is the film's OUTER surface — the gas inlet — so it takes the
945 // ambient Robin BC. Crossing the Si level set is mask resting on
946 // silicon: the mask does not react with Si, so zero flux.
947 if (bc.first == Boundary::MASK)
948 bc.first = Boundary::AMBIENT;
949 else if (bc.first == Boundary::REACTION)
950 bc.first = Boundary::NONE;
951 }
952 faceBCTypes_[fi * n + id] = bc.first;
953 faceBCDists_[fi * n + id] = bc.second;
954 if (bc.first == Boundary::REACTION) {
955 // the normal at the crossing: the node's and the neighbour's
956 // level-set gradients, interpolated to the crossing point
957 const auto a = computeSiNormal(node.index, faceReactionIt);
958 const auto b = computeSiNormal(nb, faceReactionIt);
959 const T t = std::clamp(bc.second / gridDelta, T(0), T(1));
960 Vec3D<T> m{T(0), T(0), T(0)};
961 T len = T(0);
962 for (unsigned e = 0; e < D; ++e) {
963 m[e] = (T(1) - t) * a[e] + t * b[e];
964 len += m[e] * m[e];
965 }
966 len = std::sqrt(len);
967 if (len > std::numeric_limits<T>::epsilon())
968 for (unsigned e = 0; e < D; ++e)
969 m[e] /= len;
970 else
971 m = a;
972 faceCrossingNormal_[fi * n + id] = m;
973 faceReactionNormal_[fi * n + id] = std::abs(m[dir]);
974 }
975 }
976 }
977 }
978
979 interfaceAnchor_.assign(n, Vec3D<T>{T(0), T(0), T(0)});
980 if (parameters.reactionActivationVolume != T(0))
981 for (std::size_t id = 0; id < n; ++id) {
982 if (nodes[id].inMask || !reactionNode(id))
983 continue;
984 const T phi = frontDistance(nodes[id].index);
985 for (unsigned d = 0; d < D; ++d)
986 interfaceAnchor_[id][d] =
987 static_cast<T>(nodes[id].index[d]) -
988 static_cast<T>(reactionSign) * nodes[id].siNormal[d] * phi;
989 }
990
991 // Precompute per-face neighbor node IDs (geometry-only).
992 // Used by computeFaceCoeffs().
993 neighborIds_.assign(2 * D * n, noNode);
994 for (std::size_t id = 0; id < n; ++id) {
995 for (unsigned dir = 0; dir < D; ++dir) {
996 for (int off : {-1, 1}) {
997 const unsigned fi = dir * 2u + (off == 1 ? 1u : 0u);
998 IndexType nb = nodes[id].index;
999 nb[dir] += off;
1000 if (inBounds(nb))
1001 neighborIds_[fi * n + id] = lookupNode(nb);
1002 }
1003 }
1004 }
1005
1006#ifdef VIENNALS_GPU_BICGSTAB
1007 if (gpuMode_ == GpuMode::Cpu)
1008 logDiffusionBackend("CPU AMG-BiCGSTAB", "GPU mode not selected");
1009 else if (n < gpuMinNodes)
1010 logDiffusionBackend("CPU AMG-BiCGSTAB",
1011 "below " + std::to_string(gpuMinNodes) + " nodes");
1012 else if (gpuOff_ || !gpu::gpuRuntimeAvailable())
1013 logDiffusionBackend("CPU AMG-BiCGSTAB", "GPU unavailable");
1014 else
1015 logDiffusionBackend("GPU AMG-BiCGSTAB", "AMGCL CUDA backend");
1016#else
1017 logDiffusionBackend("CPU AMG-BiCGSTAB",
1018 "ViennaLS was built without GPU support");
1019#endif
1020 }
1021
1022 // Per-face coupling coefficients: the off-diagonal entries of the
1023 // diffusion matrix.
1024 //
1025 // faceCoeffs[fi * n + id] = 2*D_eff / (dist_fi * distSum_axis)
1026 //
1027 // Only interior faces (neighbor != noNode) get a nonzero entry;
1028 // boundary and out-of-bounds faces contribute to diag/b but not to
1029 // the off-diagonal coupling stored here. Must be called after diag/b
1030 // are computed (or at least after D_eff is known), because D_eff
1031 // depends on pressure when diffusionActivationVolume != 0.
1032 void computeFaceCoeffs(std::vector<T> &faceCoeffs) const {
1033 const std::size_t n = nodes.size();
1034 faceCoeffs.assign(2 * D * n, T(0));
1035 const T eps = std::numeric_limits<T>::epsilon();
1036 const std::vector<T> zeros(n, T(0));
1037
1038 for (std::size_t id = 0; id < n; ++id) {
1039 const T D_eff = nodeDiffusionCoefficient(id);
1040 for (unsigned dir = 0; dir < D; ++dir) {
1041 const unsigned fiNeg = dir * 2u;
1042 const unsigned fiPos = dir * 2u + 1u;
1043
1044 // Use the exact side construction used by the CPU stencil so the GPU
1045 // SpMV cannot drift from computeStencilAt() at cut-cell boundaries.
1046 const auto neg = makeStencilSide(id, zeros, dir, -1, D_eff);
1047 const auto pos = makeStencilSide(id, zeros, dir, 1, D_eff);
1048 const T distNeg = neg.distance;
1049 const T distPos = pos.distance;
1050 const T distSum = distNeg + distPos;
1051 if (distSum <= eps)
1052 continue;
1053
1054 // Per-face diffusion, matching computeStencilAt: the harmonic mean
1055 // of the two nodes' D. A mirror side couples to the opposite
1056 // neighbour: its coefficient goes there.
1057 const unsigned fiNegEff =
1058 stencilOffset(nodes[id].index, dir, -1) == 1 ? fiPos : fiNeg;
1059 const unsigned fiPosEff =
1060 stencilOffset(nodes[id].index, dir, 1) == -1 ? fiNeg : fiPos;
1061 if (neighborIds_[fiNegEff * n + id] != noNode && distNeg > eps)
1062 faceCoeffs[fiNegEff * n + id] +=
1063 T(2) * faceDiffusionCoefficient(id, dir, -1, D_eff) /
1064 (distNeg * distSum);
1065 if (neighborIds_[fiPosEff * n + id] != noNode && distPos > eps)
1066 faceCoeffs[fiPosEff * n + id] +=
1067 T(2) * faceDiffusionCoefficient(id, dir, 1, D_eff) /
1068 (distPos * distSum);
1069 }
1070 }
1071 }
1072
1073 // Evaluates the stencil at one node: fills diag = A[i,i] and
1074 // rhs = sum_j(A_off[i,j] * x[j]) + bc_constants[i].
1075 // Called with x = zeros to precompute the geometry-fixed diagonal and b.
1076 template <class SolverT>
1077 void computeStencilAt(std::size_t nodeId, const std::vector<SolverT> &x,
1078 T &diag, T &rhs) const {
1079 diag = T(0);
1080 rhs = T(0);
1081 const T D_eff = nodeDiffusionCoefficient(nodeId);
1082 for (unsigned direction = 0; direction < D; ++direction) {
1083 const auto neg = makeStencilSide(nodeId, x, direction, -1, D_eff);
1084 const auto pos = makeStencilSide(nodeId, x, direction, 1, D_eff);
1085 // Each side carries its own face diffusion, the harmonic mean of the
1086 // two nodes' D.
1087 const T distSum = neg.distance + pos.distance;
1088 if (distSum <= std::numeric_limits<T>::epsilon())
1089 continue;
1090 addSideContribution(
1091 rhs, diag, neg, distSum,
1092 faceDiffusionCoefficient(nodeId, direction, -1, D_eff));
1093 addSideContribution(
1094 rhs, diag, pos, distSum,
1095 faceDiffusionCoefficient(nodeId, direction, 1, D_eff));
1096 }
1097 }
1098
1099 void solveDiffusion() {
1100 iterations = 0;
1101 residual = 0.;
1102 normalizedResidual_ = 0.;
1103 lastSolveConverged_ = false;
1104 if (nodes.empty()) {
1105 lastSolveConverged_ = true;
1106 return;
1107 }
1108
1109 const std::size_t n = nodes.size();
1110 const T eps = std::numeric_limits<T>::epsilon();
1111
1112 // Geometry-fixed diagonal and BC source vector (kept in T for full
1113 // precision).
1114 Timer<> tDiag;
1115 tDiag.start();
1116 std::vector<T> diag(n), b(n);
1117 {
1118 const std::vector<T> zeros(n, T(0));
1119#pragma omp parallel for schedule(static)
1120 for (std::size_t i = 0; i < n; ++i)
1121 computeStencilAt(i, zeros, diag[i], b[i]);
1122 }
1123 tDiag.finish();
1124
1125 bool finiteSystem = true;
1126 for (std::size_t i = 0; i < n; ++i)
1127 finiteSystem =
1128 finiteSystem && std::isfinite(diag[i]) && std::isfinite(b[i]);
1129 if (!finiteSystem) {
1130 residual = std::numeric_limits<T>::infinity();
1131 normalizedResidual_ = residual;
1132 Logger::getInstance()
1133 .addWarning("solveDiffusion: assembled non-finite matrix/RHS; "
1134 "rejecting this coupled trial.")
1135 .print();
1136 return;
1137 }
1138
1139 // Scale of the residual test: a typical row (the median diagonal) times
1140 // the concentration scale (max |b_i / diag_i|). Not max|b|: a node on
1141 // the ambient surface with the ambient on both sides along one axis has
1142 // a row of 1 / distance (2e10 for a node exactly on it), and the solve
1143 // then accepted its initial guess, C = 0 in the oxide (a dry Kao pillar
1144 // whose surface passed through grid points did not oxidise).
1145 T b_norm = T(0);
1146 {
1147 std::vector<T> sorted(diag);
1148 const auto median = sorted.begin() + n / 2;
1149 std::nth_element(sorted.begin(), median, sorted.end());
1150 T concentrationScale = T(0);
1151 for (std::size_t i = 0; i < n; ++i) {
1152 if (diag[i] > eps)
1153 concentrationScale =
1154 std::max(concentrationScale, std::abs(b[i] / diag[i]));
1155 }
1156 b_norm = *median * concentrationScale;
1157 }
1158 if (b_norm < T(1e-100))
1159 b_norm = T(1);
1160
1161 // Initial guess: warm-start or diagonal-preconditioned b. Sanitize the
1162 // warm start so a previous failed solve cannot seed NaNs into the next one.
1163 auto fallbackInitialGuess = [&](std::size_t i) -> T {
1164 if (std::isfinite(diag[i]) && std::isfinite(b[i]) && diag[i] > eps) {
1165 const T guess = b[i] / diag[i];
1166 if (std::isfinite(guess))
1167 return guess;
1168 }
1169 return T(0);
1170 };
1171
1172 std::vector<T> x(n);
1173 for (std::size_t i = 0; i < n; ++i) {
1174 x[i] = warmStartable_ ? nodes[i].concentration : fallbackInitialGuess(i);
1175 if (!std::isfinite(x[i]))
1176 x[i] = fallbackInitialGuess(i);
1177 }
1178 const auto initialGuess = x;
1179
1180 // BiCGSTAB preconditioned by smoothed-aggregation AMG with an ILU0
1181 // smoother (AMGCL) on the system as CSR (diag plus computeFaceCoeffs): on
1182 // the CPU or, with the GPU mode and at least gpuMinNodes nodes, the same
1183 // solver on AMGCL's CUDA backend, with the same stopping test and the
1184 // same acceptance below. A Jacobi-preconditioned BiCGSTAB (the former
1185 // solver on both) stagnated for thousands of iterations and broke down
1186 // (omega -> 0) on LOCOS bird's beaks, where the pressure-dependent D spans
1187 // five decades and the cut-cell rows make the matrix strongly
1188 // non-symmetric; the breakdown repeated on every CFL retry (the
1189 // quasi-steady system has no dt in it) until the run aborted. On those
1190 // systems this solver takes 3-4 iterations.
1191 Timer<> tSolve;
1192 tSolve.start();
1193 std::vector<T> faceCoeffs;
1194 computeFaceCoeffs(faceCoeffs);
1195 std::vector<std::ptrdiff_t> rowPtr(n + 1, 0), cols;
1196 std::vector<T> values;
1197 cols.reserve((2 * D + 1) * n);
1198 values.reserve((2 * D + 1) * n);
1199 std::vector<std::pair<std::ptrdiff_t, T>> row;
1200 for (std::size_t i = 0; i < n; ++i) {
1201 row.assign(1, {static_cast<std::ptrdiff_t>(i), diag[i]});
1202 for (unsigned f = 0; f < 2 * D; ++f) {
1203 const std::size_t j = neighborIds_[f * n + i];
1204 if (j != noNode && faceCoeffs[f * n + i] != T(0))
1205 row.emplace_back(static_cast<std::ptrdiff_t>(j),
1206 -faceCoeffs[f * n + i]);
1207 }
1208 std::sort(row.begin(), row.end());
1209 for (const auto &entry : row) {
1210 cols.push_back(entry.first);
1211 values.push_back(entry.second);
1212 }
1213 rowPtr[i + 1] = static_cast<std::ptrdiff_t>(cols.size());
1214 }
1215
1216 // ||r||_2 <= tolerance b_norm bounds the max-norm residual tested below
1217 const T absTolerance = parameters.tolerance * b_norm;
1218 bool solverFailed = false;
1219 bool onCpu = true;
1220 std::string path = " [CPU]";
1221#ifdef VIENNALS_GPU_BICGSTAB
1222 if (gpuMode_ != GpuMode::Cpu && !gpuOff_ && n >= gpuMinNodes) {
1223 static_assert(sizeof(std::ptrdiff_t) == sizeof(std::int64_t));
1224 std::vector<double> valuesGpu(values.begin(), values.end()),
1225 bGpu(b.begin(), b.end()), xGpu(x.begin(), x.end());
1226 unsigned gpuIterations = 0;
1227 const int status = gpu::gpuDiffusionSolve(
1228 static_cast<std::int64_t>(n),
1229 reinterpret_cast<const std::int64_t *>(rowPtr.data()),
1230 reinterpret_cast<const std::int64_t *>(cols.data()), valuesGpu.data(),
1231 bGpu.data(), xGpu.data(), parameters.maxIterations,
1232 static_cast<double>(absTolerance), gpuIterations);
1233 if (status > 0) {
1234 for (std::size_t i = 0; i < n; ++i)
1235 x[i] = static_cast<T>(xGpu[i]);
1236 iterations = gpuIterations;
1237 onCpu = false;
1238 path = " [GPU]";
1239 } else if (status == 0) {
1240 // Gpu mode stops here; Auto warns once and stays on the CPU
1241 reportGpuUnavailable("OxidationDiffusion: GPU mode was selected, but "
1242 "the GPU is unavailable.");
1243 gpuOff_ = true;
1244 } else {
1245 // a GPU that is present and fails is a malfunction, not an absence
1246 VIENNACORE_LOG_ERROR(
1247 "OxidationDiffusion: the GPU diffusion solve failed." +
1248 gpuErrorDetail());
1249 solverFailed = true;
1250 onCpu = false;
1251 path = " [GPU]";
1252 }
1253 }
1254#else
1255 if (gpuMode_ == GpuMode::Gpu) {
1256 VIENNACORE_LOG_ERROR("OxidationDiffusion: explicit GPU mode was "
1257 "requested, but ViennaLS was built without "
1258 "VIENNALS_GPU_BICGSTAB.");
1259 } else if (gpuMode_ == GpuMode::Auto && !gpuOff_) {
1260 VIENNACORE_LOG_WARNING("OxidationDiffusion: GPU mode Auto was requested, "
1261 "but ViennaLS was built without "
1262 "VIENNALS_GPU_BICGSTAB. Using the CPU solver.");
1263 gpuOff_ = true;
1264 }
1265#endif
1266 if (onCpu) {
1267 typedef amgcl::backend::builtin<T> Backend;
1268 typedef amgcl::make_solver<
1269 amgcl::amg<Backend, amgcl::coarsening::smoothed_aggregation,
1270 amgcl::relaxation::ilu0>,
1271 amgcl::solver::bicgstab<Backend>>
1272 Solver;
1273 typename Solver::params prm;
1274 prm.solver.maxiter = parameters.maxIterations;
1275 prm.solver.tol = T(0);
1276 prm.solver.abstol = absTolerance;
1277 const auto rows = static_cast<std::ptrdiff_t>(n);
1278 auto A = std::tie(rows, rowPtr, cols, values);
1279 try {
1280 Solver solve(A, prm);
1281 std::size_t count = 0;
1282 T estimate = T(0);
1283 std::tie(count, estimate) = solve(A, b, x);
1284 iterations = static_cast<unsigned>(count);
1285 } catch (const std::exception &e) {
1286 solverFailed = true;
1287 VIENNACORE_LOG_WARNING(std::string("solveDiffusion: AMGCL failed: ") +
1288 e.what());
1289 }
1290 }
1291
1292 // the max-norm residual of the result
1293 residual = T(0);
1294 bool finiteSolution = !solverFailed;
1295 for (std::size_t i = 0; i < n && finiteSolution; ++i) {
1296 T ax = T(0);
1297 for (auto k = rowPtr[i]; k < rowPtr[i + 1]; ++k)
1298 ax += values[k] * x[cols[k]];
1299 finiteSolution = std::isfinite(x[i]) && std::isfinite(ax);
1300 residual = std::max(residual, std::abs(b[i] - ax));
1301 }
1302 if (finiteSolution) {
1303 for (std::size_t i = 0; i < n; ++i)
1304 nodes[i].concentration = x[i];
1305 } else {
1306 for (std::size_t i = 0; i < n; ++i)
1307 nodes[i].concentration = initialGuess[i];
1308 residual = std::numeric_limits<T>::infinity();
1309 Logger::getInstance()
1310 .addWarning("solveDiffusion: the solve failed or produced "
1311 "non-finite concentrations; keeping the sanitized "
1312 "initial guess.")
1313 .print();
1314 }
1315 normalizedResidual_ = residual / b_norm;
1316 lastSolveConverged_ = finiteSolution &&
1317 std::isfinite(normalizedResidual_) &&
1318 normalizedResidual_ <= parameters.tolerance;
1319
1320 tSolve.finish();
1321 if (Logger::hasTiming()) {
1322 const std::string tag = "diffusion n=" + std::to_string(n) +
1323 " iters=" + std::to_string(iterations) +
1324 " res=" + std::to_string(residual) + path;
1325 Logger::getInstance().addTiming(tag + " AMG-BiCGSTAB", tSolve).print();
1326 }
1327 if (Logger::hasDebug()) {
1328 Logger::getInstance()
1329 .addTiming("diffusion n=" + std::to_string(n) + path +
1330 " diag/b precompute",
1331 tDiag)
1332 .print();
1333 }
1334 if (!lastSolveConverged_) {
1335 char numbers[96];
1336 std::snprintf(numbers, sizeof(numbers), "residual=%.3e, tolerance=%.1e",
1337 double(normalizedResidual_), double(parameters.tolerance));
1338 VIENNACORE_LOG_WARNING(
1339 "solveDiffusion: AMG-BiCGSTAB did not converge after " +
1340 std::to_string(iterations) + "/" +
1341 std::to_string(parameters.maxIterations) + " iterations (" + numbers +
1342 ")");
1343 }
1344 }
1345
1346#ifdef VIENNALS_GPU_BICGSTAB
1347 static std::string gpuErrorDetail() {
1348 const char *detail = gpu::gpuGetLastErrorMessage();
1349 if (detail && detail[0] != '\0')
1350 return std::string(" Detail: ") + detail;
1351 return {};
1352 }
1353
1358 void reportGpuUnavailable(const std::string &message) const {
1359 if (gpuMode_ == GpuMode::Auto) {
1360 VIENNACORE_LOG_WARNING(message + gpuErrorDetail() +
1361 " Falling back to the CPU solver.");
1362 } else {
1363 VIENNACORE_LOG_ERROR(message + gpuErrorDetail());
1364 }
1365 }
1366#endif
1367
1368 void logDiffusionBackend(const std::string &backend,
1369 const std::string &detail) const {
1370 if (!Logger::hasInfo())
1371 return;
1372 const std::string msg =
1373 "OxidationDiffusion: using " + backend +
1374 " for diffusion solve (nodes=" + std::to_string(nodes.size()) +
1375 (detail.empty() ? std::string() : ", " + detail) + ").";
1376 if (msg == lastLoggedBackend_)
1377 return;
1378 lastLoggedBackend_ = msg;
1379 Logger::getInstance().addInfo(msg).print();
1380 }
1381
1382 // Uses precomputed flat faceBC arrays — no HRLE access, safe for parallel
1383 // execution.
1384 template <class SolverT>
1385 StencilSide
1386 makeStencilSide(std::size_t nodeId, const std::vector<SolverT> &previous,
1387 unsigned direction, int offset, T diffusion) const {
1388 const IndexType &nodeIndex = nodes[nodeId].index;
1389 offset = stencilOffset(nodeIndex, direction, offset); // mirror sides
1390 IndexType neighbor = nodeIndex;
1391 neighbor[direction] += offset;
1392
1393 if (!inBounds(neighbor))
1394 return zeroFluxSide();
1395
1396 const std::size_t neighborId = lookupNode(neighbor);
1397 if (neighborId != noNode)
1398 return {gridDelta, 0., previous[neighborId]};
1399
1400 const unsigned fi = direction * 2u + (offset == 1 ? 1u : 0u);
1401 const std::size_t n = nodes.size();
1402 const Boundary faceType = faceBCTypes_[fi * n + nodeId];
1403 const T faceDist = faceBCDists_[fi * n + nodeId];
1404 if (faceType == Boundary::REACTION)
1405 return reactionBoundarySide(faceRate(nodeId, fi), faceDist, diffusion,
1406 faceReactionNormal_[fi * n + nodeId]);
1407 if (faceType == Boundary::AMBIENT)
1408 return ambientBoundarySide(faceDist, diffusion);
1409 if (faceType == Boundary::MASK)
1410 return maskBoundarySide(faceDist, diffusion);
1411 return zeroFluxSide();
1412 }
1413
1414 void addAxisContribution(T &rightHandSide, T &diagonal,
1415 const StencilSide &negativeSide,
1416 const StencilSide &positiveSide, T diffusion) const {
1417 const T distanceSum = negativeSide.distance + positiveSide.distance;
1418 if (distanceSum <= std::numeric_limits<T>::epsilon())
1419 return;
1420
1421 addSideContribution(rightHandSide, diagonal, negativeSide, distanceSum,
1422 diffusion);
1423 addSideContribution(rightHandSide, diagonal, positiveSide, distanceSum,
1424 diffusion);
1425 }
1426
1427 void addSideContribution(T &rightHandSide, T &diagonal,
1428 const StencilSide &side, T distanceSum,
1429 T diffusion) const {
1430 if (side.distance <= std::numeric_limits<T>::epsilon())
1431 return;
1432
1433 const T coefficient = T(2) * diffusion / (side.distance * distanceSum);
1434 rightHandSide += coefficient * side.constant;
1435 diagonal += coefficient * (T(1) - side.nodeCoefficient);
1436 }
1437
1438 StencilSide zeroFluxSide() const { return {gridDelta, 1., 0.}; }
1439
1440 // Robin condition D dC/dn = k C on one axis arm that crosses the Si
1441 // interface (distance = the crossing along that axis). Along the arm only
1442 // the axis component of the normal flux is available, D dC/dx_a =
1443 // |n_a| k C, so the rate is scaled by |n_a|. Imposing the full k on every
1444 // crossing arm sums to a normal sink of k (|n_x| + |n_y| (+ |n_z|)): exact
1445 // on an axis-aligned front, sqrt(2) too strong at 45 degrees, 4/pi on
1446 // average around a cylinder — measured as 3-7% too little Si consumed by
1447 // the Kao cylinders with no stress coupling. With the
1448 // scaling the reconstructed interface concentration is also the same on
1449 // every arm, C_node (D/d) / (D/d + k) for the normal distance d.
1450 StencilSide reactionBoundarySide(T rate, T distance, T diffusion,
1451 T normalComponent) const {
1452 const T conductance = diffusion / distance;
1453 const T reactionRate = rate * normalComponent;
1454 const T denominator = conductance + reactionRate;
1455 if (denominator <= std::numeric_limits<T>::epsilon())
1456 return zeroFluxSide();
1457
1458 return {distance, conductance / denominator, 0.};
1459 }
1460
1461 ReactionBoundarySample reactionBoundarySample(const IndexType &index) const {
1462 ConstSparseIterator reactionIt(reactionInterface->getDomain());
1463
1464 const std::size_t directId = lookupNode(index);
1465 if (directId != noNode) {
1466 const auto sample =
1467 reactionBoundarySampleFromNode(reactionIt, nodes[directId]);
1468 if (sample.found)
1469 return sample;
1470 }
1471
1472 // The reaction velocity is a local interface quantity. A global nearest
1473 // search can smear open-window concentrations deep under a mask if the
1474 // local oxide band is temporarily missing or clipped near contact. Keep
1475 // the fallback strictly local, consistent with the velocity-extension
1476 // radius used elsewhere in the oxidation fields.
1477 std::size_t bestNode = std::numeric_limits<std::size_t>::max();
1478 T bestDistance2 = std::numeric_limits<T>::max();
1479
1480 for (int radius = 1; radius <= 4; ++radius) {
1481 IndexType offset{};
1482 offset.fill(-radius);
1483 while (true) {
1484 IndexType candidate = index;
1485 T distance2 = 0.;
1486 for (unsigned d = 0; d < D; ++d) {
1487 candidate[d] += offset[d];
1488 distance2 += static_cast<T>(offset[d] * offset[d]);
1489 }
1490
1491 if (distance2 > T(0) && inBounds(candidate)) {
1492 const std::size_t foundId = nodeLookupFlat[linearIndex(candidate)];
1493 if (foundId != noNode && distance2 < bestDistance2) {
1494 const auto sample =
1495 reactionBoundarySampleFromNode(reactionIt, nodes[foundId]);
1496 if (sample.found) {
1497 bestDistance2 = distance2;
1498 bestNode = foundId;
1499 }
1500 }
1501 }
1502
1503 unsigned dim = 0;
1504 for (; dim < D; ++dim) {
1505 if (offset[dim] < radius) {
1506 ++offset[dim];
1507 break;
1508 }
1509 offset[dim] = -radius;
1510 }
1511 if (dim == D)
1512 break;
1513 }
1514
1515 if (bestNode != std::numeric_limits<std::size_t>::max())
1516 break;
1517 }
1518
1519 if (bestNode == std::numeric_limits<std::size_t>::max())
1520 return {};
1521 return reactionBoundarySampleFromNode(reactionIt, nodes[bestNode]);
1522 }
1523
1525 reactionBoundarySampleFromNode(ConstSparseIterator &reactionIt,
1526 const Node &node) const {
1528 // A meshed mask node never reacts with silicon: its reaction faces are
1529 // zero-flux, so it must not supply a reaction-boundary sample either.
1530 if (node.inMask)
1531 return best;
1532 best.nodeIndex = node.index;
1533 T bestDistance = std::numeric_limits<T>::max();
1534 const T insidePhi = valueAt(reactionIt, node.index);
1535 const std::size_t nodeId = lookupNode(node.index);
1536 const std::size_t n = nodes.size();
1537 std::optional<Vec3D<T>> siNormal;
1538
1539 for (unsigned direction = 0; direction < D; ++direction) {
1540 for (const int offset : {-1, 1}) {
1541 IndexType neighbor = node.index;
1542 neighbor[direction] += offset;
1543 if (!inBounds(neighbor))
1544 continue;
1545
1546 const T outsidePhi = valueAt(reactionIt, neighbor);
1547 if (!crosses(insidePhi, outsidePhi))
1548 continue;
1549
1550 const T distance = crossingDistance(insidePhi, outsidePhi);
1551 if (distance >= bestDistance)
1552 continue;
1553
1554 const T diffusion = getEffectiveDiffusionCoefficient(node.index);
1555 const unsigned fi = direction * 2u + (offset == 1 ? 1u : 0u);
1556 T normalComponent, rate;
1557 if (nodeId != noNode && faceReactionNormal_.size() == 2 * D * n &&
1558 faceBCTypes_[fi * n + nodeId] == Boundary::REACTION) {
1559 normalComponent = faceReactionNormal_[fi * n + nodeId];
1560 rate = faceRate(nodeId, fi);
1561 } else {
1562 if (!siNormal)
1563 siNormal = computeSiNormal(node.index, reactionIt);
1564 normalComponent = std::abs((*siNormal)[direction]);
1565 rate = getEffectiveReactionRate(node.index);
1566 }
1567 const auto side =
1568 reactionBoundarySide(rate, distance, diffusion, normalComponent);
1569 best.found = true;
1570 best.distance = distance;
1571 best.concentration =
1572 side.nodeCoefficient * node.concentration + side.constant;
1573 best.crossingAxis = direction;
1574 best.crossingOffset = offset;
1575 bestDistance = distance;
1576 }
1577 }
1578
1579 return best;
1580 }
1581
1582 StencilSide ambientBoundarySide(T distance, T diffusion) const {
1583 const T conductance = diffusion / distance;
1584 const T denominator = conductance + parameters.transferCoefficient;
1585 if (denominator <= std::numeric_limits<T>::epsilon())
1586 return zeroFluxSide();
1587
1588 return {distance, conductance / denominator,
1589 parameters.transferCoefficient *
1590 parameters.equilibriumConcentration / denominator};
1591 }
1592
1593 StencilSide maskBoundarySide(T distance, T diffusion) const {
1594 if (parameters.maskTransferCoefficient <= std::numeric_limits<T>::epsilon())
1595 return zeroFluxSide();
1596
1597 const T conductance = diffusion / distance;
1598 const T denominator = conductance + parameters.maskTransferCoefficient;
1599 if (denominator <= std::numeric_limits<T>::epsilon())
1600 return zeroFluxSide();
1601
1602 return {distance, conductance / denominator,
1603 parameters.maskTransferCoefficient * parameters.maskConcentration /
1604 denominator};
1605 }
1606
1607 // Crossing of REACTION face fi of node i, in index units.
1608 Vec3D<T> crossingPoint(std::size_t i, unsigned fi) const {
1609 const std::size_t n = nodes.size();
1610 Vec3D<T> c{T(0), T(0), T(0)};
1611 for (unsigned d = 0; d < D; ++d)
1612 c[d] = static_cast<T>(nodes[i].index[d]);
1613 c[fi / 2] +=
1614 (fi % 2 == 1 ? T(1) : T(-1)) * faceBCDists_[fi * n + i] / gridDelta;
1615 return c;
1616 }
1617
1618 // Calls f(j) for every node j in the box of half-width `radius` cells
1619 // around p (index units).
1620 template <class F>
1621 void forEachNodeNear(const Vec3D<T> &p, int radius, F f) const {
1622 IndexType centre, offset;
1623 for (unsigned d = 0; d < D; ++d)
1624 centre[d] = static_cast<viennahrle::IndexType>(std::llround(p[d]));
1625 offset.fill(-radius);
1626 while (true) {
1627 IndexType q = centre;
1628 for (unsigned d = 0; d < D; ++d)
1629 q[d] += offset[d];
1630 if (inBounds(q))
1631 if (const std::size_t j = lookupNode(q); j != noNode)
1632 f(j);
1633 unsigned d = 0;
1634 while (d < D && offset[d] == radius)
1635 offset[d++] = -radius;
1636 if (d == D)
1637 break;
1638 ++offset[d];
1639 }
1640 }
1641
1642 // The law's normal stress at a front point p (index units): the reaction
1643 // nodes' smoothed samples (interfaceNormalStress_, each taken at its anchor
1644 // on the front), interpolated with Gaussian weights one cell wide. NaN
1645 // where no reaction node within two cells carries one.
1646 T interfaceNormalStressAt(const Vec3D<T> &p) const {
1647 if (interfaceNormalStress_.size() != nodes.size() ||
1648 interfaceAnchor_.size() != nodes.size())
1649 return std::numeric_limits<T>::quiet_NaN();
1650 T weightSum = T(0), sum = T(0);
1651 forEachNodeNear(p, 2, [&](std::size_t j) {
1652 const T v = interfaceNormalStress_[j];
1653 if (!std::isfinite(v))
1654 return;
1655 T r2 = T(0);
1656 for (unsigned d = 0; d < D; ++d)
1657 r2 += (interfaceAnchor_[j][d] - p[d]) * (interfaceAnchor_[j][d] - p[d]);
1658 const T w = std::exp(-r2 / T(2));
1659 weightSum += w;
1660 sum += w * v;
1661 });
1662 return weightSum > T(1e-12) ? sum / weightSum
1663 : std::numeric_limits<T>::quiet_NaN();
1664 }
1665
1666 // Reaction rate at the crossing of REACTION face fi of node i: the
1667 // normal-stress law with the stress at the crossing, the orientation factor
1668 // with the crossing's normal.
1669 T interfaceReactionRate(std::size_t i, unsigned fi) const {
1670 const std::size_t n = nodes.size();
1671 T rate = parameters.reactionRate;
1672 if (parameters.reactionActivationVolume != T(0)) {
1673 T normalStress = interfaceNormalStressAt(crossingPoint(i, fi));
1674 if (!std::isfinite(normalStress)) {
1675 normalStress = -parameters.referencePressure;
1676 medianNormalStress(nodes[i].index, normalStress);
1677 }
1678 const T exponent =
1679 stressExponent(-normalStress, parameters.reactionActivationVolume);
1680 // floor 0.1 and ceiling 1: see getEffectiveReactionRate
1681 rate *= std::clamp(stressFactor(exponent), T(0.1), T(1.));
1682 }
1683 if (parameters.reactionRateRatio111 != T(1)) {
1684 const auto &normal = faceCrossingNormal_[fi * n + i];
1685 T dot = T(0);
1686 for (unsigned d = 0; d < D; ++d)
1687 dot += normal[d] * parameters.crystalAxis[d];
1688 rate *=
1689 T(1) + (parameters.reactionRateRatio111 - T(1)) * (T(1) - dot * dot);
1690 }
1691 return rate;
1692 }
1693
1694 void updateFaceRates() {
1695 const std::size_t n = nodes.size();
1696 faceReactionRate_.assign(2 * D * n, T(0));
1697#pragma omp parallel for schedule(static)
1698 for (std::size_t i = 0; i < n; ++i)
1699 for (unsigned fi = 0; fi < 2 * D; ++fi)
1700 if (faceBCTypes_[fi * n + i] == Boundary::REACTION)
1701 faceReactionRate_[fi * n + i] = interfaceReactionRate(i, fi);
1702 }
1703
1704 T faceRate(std::size_t i, unsigned fi) const {
1705 const std::size_t n = nodes.size();
1706 return faceReactionRate_.size() == 2 * D * n
1707 ? faceReactionRate_[fi * n + i]
1708 : getEffectiveReactionRate(nodes[i].index);
1709 }
1710
1711 // Front speed k C_i / (N alpha) at every REACTION face crossing, C_i the
1712 // crossing's interface concentration from the solved node value (the Robin
1713 // arm's own reconstruction).
1714 void updateFaceSpeeds() {
1715 const std::size_t n = nodes.size();
1716 faceFrontSpeed_.assign(2 * D * n, T(0));
1717#pragma omp parallel for schedule(static)
1718 for (std::size_t i = 0; i < n; ++i)
1719 for (unsigned fi = 0; fi < 2 * D; ++fi) {
1720 if (faceBCTypes_[fi * n + i] != Boundary::REACTION)
1721 continue;
1722 const T rate = faceRate(i, fi);
1723 const auto side = reactionBoundarySide(rate, faceBCDists_[fi * n + i],
1724 nodeDiffusionCoefficient(i),
1725 faceReactionNormal_[fi * n + i]);
1726 const T concentration =
1727 side.nodeCoefficient * nodes[i].concentration + side.constant;
1728 faceFrontSpeed_[fi * n + i] = std::abs(parameters.velocitySign) * rate *
1729 concentration /
1730 (parameters.oxidantMoleculeDensity *
1731 parameters.expansionCoefficient);
1732 }
1733 frontStart_.assign(n + 1, 0);
1734 for (std::size_t i = 0; i < n; ++i) {
1735 std::size_t count = 0;
1736 for (unsigned fi = 0; fi < 2 * D; ++fi)
1737 count += faceBCTypes_[fi * n + i] == Boundary::REACTION;
1738 frontStart_[i + 1] = frontStart_[i] + count;
1739 }
1740 frontCrossings_.resize(frontStart_[n]);
1741 for (std::size_t i = 0; i < n; ++i) {
1742 std::size_t k = frontStart_[i];
1743 for (unsigned fi = 0; fi < 2 * D; ++fi)
1744 if (faceBCTypes_[fi * n + i] == Boundary::REACTION)
1745 frontCrossings_[k++] = {
1746 crossingPoint(i, fi), faceReactionNormal_[fi * n + i],
1747 faceFrontSpeed_[fi * n + i], faceCrossingNormal_[fi * n + i]};
1748 }
1749 }
1750
1751 T getEffectiveReactionRate(const IndexType &index) const {
1752 T rate = parameters.reactionRate;
1753
1754 if (parameters.reactionActivationVolume != T(0)) {
1755 T normalStress = -parameters.referencePressure;
1756 const std::size_t nodeId = lookupNode(index);
1757 if (nodeId != noNode && nodeId < interfaceNormalStress_.size() &&
1758 std::isfinite(interfaceNormalStress_[nodeId]))
1759 normalStress = interfaceNormalStress_[nodeId];
1760 else
1761 medianNormalStress(index, normalStress);
1762 // -sigma_nn is the normal compression, measured like a pressure.
1763 const T exponent =
1764 stressExponent(-normalStress, parameters.reactionActivationVolume);
1765 // Reaction floor 0.1 (vs the generic 1e-6 clamp): the source term
1766 // scales with this factor, and a single-cell stress spike (e.g.
1767 // -15 GPa next to -2 GPa a cell away) would otherwise make the volume
1768 // source orders of magnitude rough on the grid scale.
1769 // Ceiling 1: interface stress only retards the reaction.
1770 // In Kao's cylinders the interface normal stress is compressive for
1771 // convex and concave Si alike, and there is no data for the reaction
1772 // under interface tension. Locally, tension appears where the Si
1773 // front dents: a dent in a convex front under the oxide's tensile hoop
1774 // stress is pulled open (membrane effect), so a tension-ENHANCED rate
1775 // deepens it — measured: Kao pillar (r = 0.45 um, Vr = 15 A^3) dented
1776 // 12.7 nm in 0.1 hr with ceiling 10, round within 0.6 nm with ceiling 1.
1777 rate *= std::clamp(stressFactor(exponent), T(0.1), T(1.));
1778 }
1779
1780 if (parameters.reactionRateRatio111 != T(1)) {
1781 const std::size_t nodeId = lookupNode(index);
1782 if (nodeId != noNode) {
1783 const auto &normal = nodes[nodeId].siNormal;
1784 T dot = T(0);
1785 for (unsigned d = 0; d < D; ++d)
1786 dot += normal[d] * parameters.crystalAxis[d];
1787 rate *= T(1) +
1788 (parameters.reactionRateRatio111 - T(1)) * (T(1) - dot * dot);
1789 }
1790 }
1791
1792 return rate;
1793 }
1794
1795 // Median of n.sigma.n over the node and its oxide face neighbours, all
1796 // projected on the node's own interface normal: cut
1797 // cells carry the least trustworthy stress in the field (single-cell spikes
1798 // of -15 GPa next to -2 GPa), and with up to 2D+1 samples a lone spike
1799 // cannot move the median, while a genuine gradient passes unchanged.
1800 bool medianNormalStress(const IndexType &index, T &result) const {
1801 return medianNormalStress(index, stressLookup, result);
1802 }
1803
1804 // Oxide stress at the Si front for the stress laws, extrapolated along the
1805 // front normal n (into the oxide) from the points 2 and 3 cells into the
1806 // oxide, each D-linear over the nodes around it. The first node layer
1807 // (the nodes with a reaction face) is left out: its MAC stress is O(1)
1808 // wrong next to the front -- on a Kao cylinder the pressure 0.2-0.4 GPa too
1809 // high and sigma_rr 0.1-0.2 GPa too compressive, at any grid spacing --
1810 // while the second layer is good to ~0.05 GPa. Fixed distances along the
1811 // normal keep the estimate independent of the front's orientation on the
1812 // grid (a least-squares fit over the nodes behind the front was not).
1813 // phi: the node's distance from the front in cells; returns the value at
1814 // the distance sAt. sample(j) gives the quantity at node j, NaN where
1815 // missing. False where a sample point lacks oxide nodes (a film thinner
1816 // than ~4 cells); the caller then falls back to the node layer.
1817 template <class Sample>
1818 bool frontStressAt(std::size_t i, const Vec3D<T> &n, T phi, T sAt,
1819 Sample sample, T &out) const {
1820 const std::size_t count = nodes.size();
1821 T q[2];
1822 for (unsigned k = 0; k < 2; ++k) {
1823 const T s = T(2 + k);
1824 std::array<T, D> base{}, frac{};
1825 for (unsigned d = 0; d < D; ++d) {
1826 const T y = static_cast<T>(nodes[i].index[d]) + n[d] * (s - phi);
1827 base[d] = std::floor(y);
1828 frac[d] = y - base[d];
1829 }
1830 T sum = T(0), weightSum = T(0);
1831 for (unsigned corner = 0; corner < (1u << D); ++corner) {
1832 IndexType idx;
1833 T w = T(1);
1834 for (unsigned d = 0; d < D; ++d) {
1835 const bool up = (corner >> d) & 1u;
1836 idx[d] = static_cast<viennahrle::IndexType>(base[d]) + (up ? 1 : 0);
1837 w *= up ? frac[d] : T(1) - frac[d];
1838 }
1839 if (w <= T(0) || !inBounds(idx))
1840 continue;
1841 const std::size_t j = lookupNode(idx);
1842 if (j == noNode || nodes[j].inMask)
1843 continue;
1844 bool firstLayer = false;
1845 if (faceBCTypes_.size() == 2 * D * count)
1846 for (unsigned f = 0; f < 2 * D && !firstLayer; ++f)
1847 firstLayer = faceBCTypes_[f * count + j] == Boundary::REACTION;
1848 if (firstLayer)
1849 continue;
1850 const T v = sample(j);
1851 if (!std::isfinite(v))
1852 continue;
1853 sum += w * v;
1854 weightSum += w;
1855 }
1856 if (weightSum < T(0.5))
1857 return false;
1858 q[k] = sum / weightSum;
1859 }
1860 out = q[0] + (q[1] - q[0]) * (sAt - T(2));
1861 return std::isfinite(out);
1862 }
1863
1864 // Distance (cells) from the Si front into the oxide at a node next to it.
1865 T frontDistance(const IndexType &index) const {
1866 ConstSparseIterator it(reactionInterface->getDomain());
1867 it.goToIndices(index);
1868 const T v =
1869 it.isDefined() ? static_cast<T>(reactionSign) * it.getValue() : T(0);
1870 return std::isfinite(v) ? std::clamp(v, T(0), T(1.5)) : T(0);
1871 }
1872
1873 bool reactionNode(std::size_t i) const {
1874 const std::size_t n = nodes.size();
1875 if (faceBCTypes_.size() != 2 * D * n)
1876 return false;
1877 for (unsigned f = 0; f < 2 * D; ++f)
1878 if (faceBCTypes_[f * n + i] == Boundary::REACTION)
1879 return true;
1880 return false;
1881 }
1882
1883 bool medianNormalStress(const IndexType &index, const StressLookup &lookup,
1884 T &result) const {
1885 Vec3D<T> normal{T(0), T(0), T(0)};
1886 normal[D - 1] = T(1);
1887 if (const std::size_t centerNode = lookupNode(index); centerNode != noNode)
1888 normal = nodes[centerNode].siNormal;
1889 T samples[2 * D + 1];
1890 unsigned count = 0;
1891 const auto sampleAt = [&](const IndexType &sampleIndex) {
1892 // Only genuine oxide nodes carry an oxide stress (not the gas-side
1893 // band, not the mask body).
1894 const std::size_t sampleNode = lookupNode(sampleIndex);
1895 if (sampleNode == noNode || nodes[sampleNode].inMask)
1896 return;
1897 const auto found = lookup.find(detail::gridIndexHash<D>(sampleIndex));
1898 if (found == lookup.end())
1899 return;
1900 T value = T(0);
1901 for (unsigned r = 0; r < D; ++r)
1902 for (unsigned c = 0; c < D; ++c)
1903 value += normal[r] * found->second[3 * r + c] * normal[c];
1904 if (std::isfinite(value))
1905 samples[count++] = value;
1906 };
1907 sampleAt(index);
1908 for (unsigned d = 0; d < D; ++d) {
1909 IndexType neighbor = index;
1910 neighbor[d] -= 1;
1911 sampleAt(neighbor);
1912 neighbor[d] += 2;
1913 sampleAt(neighbor);
1914 }
1915 if (count == 0)
1916 return false;
1917 std::nth_element(samples, samples + count / 2, samples + count);
1918 result = samples[count / 2];
1919 return true;
1920 }
1921
1922 // Interface normal stress for the reaction law: the median sample at every
1923 // node with a REACTION face, then smoothed along the interface.
1924 //
1925 // Why smoothed: the reaction law k = k0 exp(sigma_nn V/kT)
1926 // feeds back through the mechanics, and that loop is stabilising but STIFF.
1927 // A rate ripple of wavenumber q makes a normal stress ~ 2 eta_impl q v
1928 // (eta_impl = (1 - e^(-dt/tau)) eta ~ G dt), so the loop gain is
1929 // g(q) ~ (V/kT) 2 G dt q v ~ (V/kT) G q h at the CFL step: ~80 for the
1930 // grid ripple at V = 15 A^3, G = 30 GPa, independent of h. The partitioned
1931 // coupling cannot converge gains much above 10 (measured on a flat film:
1932 // undamped iterates alternate in sign and grow 4-5x per iteration; with the
1933 // loop exiting after one iteration the ripple doubled every step until the
1934 // mechanics failed). Six passes of neighbour averaging along the interface
1935 // (a ~2h Gaussian) cap the gain at ~8 for every q. It only removes stress
1936 // structure shorter than ~2 cells, which the converged solution suppresses
1937 // anyway (the stiffness is the physics damping such ripples), and it
1938 // vanishes with grid refinement.
1939 void updateInterfaceNormalStress() {
1940 interfaceNormalStress_ = interfaceNormalStressFor(stressLookup);
1941 }
1942
1943 // Pressure the diffusivity law uses at the first node layer (the nodes
1944 // with a reaction face), extrapolated from behind it (frontStressAt); NaN
1945 // elsewhere, where the node's own pressure is used.
1946 void updateLawPressure() {
1947 const std::size_t n = nodes.size();
1948 lawPressure_.assign(n, std::numeric_limits<T>::quiet_NaN());
1949 ConstSparseIterator reactionIt(reactionInterface->getDomain());
1950 for (std::size_t i = 0; i < n; ++i) {
1951 if (nodes[i].inMask || !reactionNode(i))
1952 continue;
1953 // the node normal exists where a reaction law or the orientation
1954 // factor needs it
1955 Vec3D<T> into = (parameters.reactionRateRatio111 != T(1) ||
1956 parameters.reactionActivationVolume != T(0))
1957 ? nodes[i].siNormal
1958 : computeSiNormal(nodes[i].index, reactionIt);
1959 for (unsigned d = 0; d < D; ++d)
1960 into[d] *= static_cast<T>(reactionSign);
1961 const T phi = frontDistance(nodes[i].index);
1962 const auto pressureAt = [&](std::size_t j) {
1963 const auto found =
1964 pressureLookup.find(detail::gridIndexHash<D>(nodes[j].index));
1965 return found == pressureLookup.end()
1966 ? std::numeric_limits<T>::quiet_NaN()
1967 : found->second;
1968 };
1969 T value;
1970 if (frontStressAt(i, into, phi, phi, pressureAt, value))
1971 lawPressure_[i] = value;
1972 }
1973 }
1974
1977 T nodeDiffusionCoefficient(std::size_t nodeId) const {
1978 T value = getEffectiveDiffusionCoefficient(nodes[nodeId].index);
1979 if (nodes[nodeId].inMask)
1980 value /= static_cast<T>(parameters.maskPermeabilityRatio);
1981 return value;
1982 }
1983
1991 T faceDiffusionCoefficient(std::size_t nodeId, unsigned direction, int offset,
1992 T ownDiffusion) const {
1993 offset = stencilOffset(nodes[nodeId].index, direction, offset);
1994 IndexType neighbor = nodes[nodeId].index;
1995 neighbor[direction] += offset;
1996 if (!inBounds(neighbor))
1997 return ownDiffusion;
1998 const std::size_t neighborId = lookupNode(neighbor);
1999 if (neighborId == noNode)
2000 return ownDiffusion;
2001 const T neighborDiffusion = nodeDiffusionCoefficient(neighborId);
2002 const T sum = ownDiffusion + neighborDiffusion;
2003 if (sum <= std::numeric_limits<T>::epsilon())
2004 return T(0);
2005 return T(2) * ownDiffusion * neighborDiffusion / sum;
2006 }
2007
2008 T getEffectiveDiffusionCoefficient(const IndexType &index) const {
2009 if (parameters.diffusionActivationVolume == T(0))
2010 return parameters.diffusionCoefficient;
2011
2012 T pressure = parameters.referencePressure;
2013 const auto found = pressureLookup.find(detail::gridIndexHash<D>(index));
2014 if (found != pressureLookup.end())
2015 pressure = found->second;
2016 if (const std::size_t id = lookupNode(index);
2017 id != noNode && id < lawPressure_.size() &&
2018 std::isfinite(lawPressure_[id]))
2019 pressure = lawPressure_[id];
2020 if (!std::isfinite(pressure))
2021 pressure = parameters.referencePressure;
2022
2023 const T exponent =
2024 stressExponent(pressure, parameters.diffusionActivationVolume);
2025 return parameters.diffusionCoefficient * stressFactor(exponent);
2026 }
2027
2028 T stressExponent(T pressure, T activationVolume) const {
2029 const T thermalEnergy =
2030 boltzmannConstant * std::max(parameters.temperature, T(1.));
2031 // Compression (p > 0) suppresses, tension enhances.
2032 return -(pressure - parameters.referencePressure) * activationVolume /
2033 thermalEnergy;
2034 }
2035
2036 static T stressFactor(T exponent) {
2037 if (!std::isfinite(exponent))
2038 return T(1);
2039 if (exponent <= std::log(minStressFactor))
2040 return minStressFactor;
2041 if (exponent >= std::log(maxStressFactor))
2042 return maxStressFactor;
2043 return std::exp(exponent);
2044 }
2045
2046 Vec3D<T> computeSiNormal(const IndexType &index,
2047 ConstSparseIterator &reactionIt) const {
2048 // Reflect neighbor indices that fall outside the HRLE grid. This handles
2049 // REFLECTIVE boundary conditions correctly: phi(b-k) = phi(b+k), so the
2050 // centered difference at b gives zero lateral gradient as expected.
2051 auto reflectToGrid = [&](IndexType idx) {
2052 auto &g = reactionInterface->getGrid();
2053 for (unsigned d2 = 0; d2 < D; ++d2) {
2054 const auto lo = g.getMinGridPoint(d2);
2055 const auto hi = g.getMaxGridPoint(d2);
2056 if (idx[d2] < lo)
2057 idx[d2] = 2 * lo - idx[d2];
2058 if (idx[d2] > hi)
2059 idx[d2] = 2 * hi - idx[d2];
2060 }
2061 return idx;
2062 };
2063 Vec3D<T> gradient = this->definedLevelSetGradient(
2064 reactionIt, index, [&](const IndexType &q, bool &ok) {
2065 ok = true;
2066 return reflectToGrid(q);
2067 });
2068 T len = T(0);
2069 for (unsigned d = 0; d < D; ++d)
2070 len += gradient[d] * gradient[d];
2071 len = std::sqrt(len);
2072 if (len > std::numeric_limits<T>::epsilon()) {
2073 for (unsigned d = 0; d < D; ++d)
2074 gradient[d] /= len;
2075 } else {
2076 gradient = Vec3D<T>{0., 0., 0.};
2077 gradient[D - 1] = T(1);
2078 }
2079 return gradient;
2080 }
2081
2082 std::pair<Boundary, T> classifyBoundary(ConstSparseIterator &reactionIt,
2083 ConstSparseIterator &ambientIt,
2084 ConstSparseIterator &maskIt,
2085 const IndexType &inside,
2086 const IndexType &outside) const {
2087 const T reactionInside = valueAt(reactionIt, inside);
2088 const T reactionOutside = valueAt(reactionIt, outside);
2089 const T ambientInside = valueAt(ambientIt, inside);
2090 const T ambientOutside = valueAt(ambientIt, outside);
2091 const T maskInside = valueAtMask(maskIt, inside);
2092 const T maskOutside = valueAtMask(maskIt, outside);
2093
2094 const T reactionDistance =
2095 crosses(reactionInside, reactionOutside)
2096 ? crossingDistance(reactionInside, reactionOutside)
2097 : std::numeric_limits<T>::max();
2098 const T ambientDistance =
2099 crosses(ambientInside, ambientOutside)
2100 ? crossingDistance(ambientInside, ambientOutside)
2101 : std::numeric_limits<T>::max();
2102 const T maskDistance =
2103 maskInterface != nullptr && crosses(maskInside, maskOutside)
2104 ? crossingDistance(maskInside, maskOutside)
2105 : std::numeric_limits<T>::max();
2106
2107 if (reactionDistance == std::numeric_limits<T>::max() &&
2108 ambientDistance == std::numeric_limits<T>::max() &&
2109 maskDistance == std::numeric_limits<T>::max())
2110 return {Boundary::NONE, gridDelta};
2111
2112 if (reactionDistance <= ambientDistance && reactionDistance <= maskDistance)
2113 return {Boundary::REACTION, reactionDistance};
2114
2115 // Ambient crossing heads into mask-occupied space: the oxide/gas surface
2116 // has drifted under the nitride. Classify as MASK regardless of whether
2117 // the crossings are coincident (isMaskAtCrossing) or the outer node is
2118 // wholly inside the mask body (valueAtMask check). This is equivalent to
2119 // sealing the diffusion domain with UNION(ambientInterface, maskInterface)
2120 // without mutating any level set.
2121 if (ambientDistance != std::numeric_limits<T>::max() &&
2122 (isMaskAtCrossing(maskInside, maskOutside, ambientDistance) ||
2123 (maskInterface != nullptr &&
2124 static_cast<T>(maskSign) * valueAtMask(maskIt, outside) >= T(0))))
2125 return {Boundary::MASK, ambientDistance};
2126
2127 if (maskDistance <= ambientDistance)
2128 return {Boundary::MASK, maskDistance};
2129 return {Boundary::AMBIENT, ambientDistance};
2130 }
2131
2132 bool isInsideOxide(T reactionPhi, T ambientPhi) const {
2133 // GeometricAdvect can leave a tiny positive residual (~4*epsilon) when the
2134 // interface lands exactly on a grid point at non-zero coordinates, because
2135 // k*gridDelta is not exactly representable in floating point. Allow a
2136 // tolerance of 1e-9 grid units so that grid points on the surface (phi≈0)
2137 // are correctly classified as inside the oxide.
2138 constexpr T eps = T(1e-9);
2139 return reactionSign * reactionPhi >= -eps &&
2140 ambientSign * ambientPhi >= -eps;
2141 }
2142
2143 ConstSparseIterator makeMaskIterator() const {
2144 if (maskInterface == nullptr)
2145 return ConstSparseIterator(reactionInterface->getDomain());
2146 return ConstSparseIterator(maskInterface->getDomain());
2147 }
2148
2149 bool isInsideMask(ConstSparseIterator &maskIt, const IndexType &index) const {
2150 if (maskInterface == nullptr)
2151 return false;
2152 return maskSign * valueAt(maskIt, index) >= 0.;
2153 }
2154
2155 T valueAtMask(ConstSparseIterator &maskIt, const IndexType &index) const {
2156 if (maskInterface == nullptr)
2157 return std::numeric_limits<T>::max();
2158 return valueAt(maskIt, index);
2159 }
2160
2161 bool isMaskAtCrossing(T maskInside, T maskOutside, T distance) const {
2162 if (maskInterface == nullptr)
2163 return false;
2164 const T fraction = std::clamp(distance / gridDelta, T(0), T(1));
2165 const T insidePhi = detail::clampLevelSetPhi(maskInside);
2166 const T outsidePhi = detail::clampLevelSetPhi(maskOutside);
2167 const T maskPhi = insidePhi + fraction * (outsidePhi - insidePhi);
2168 return static_cast<T>(maskSign) * maskPhi >= T(0);
2169 }
2170
2171 T crossingDistance(T insidePhi, T outsidePhi) const {
2173 insidePhi, outsidePhi, parameters.minBoundaryDistance, gridDelta);
2174 }
2175};
2176
2177} // 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
void markGeometryChanged()
Call after any in-place modification of the level sets (e.g. after ls::Advect) so that the next apply...
Definition lsOxidationDiffusion.hpp:280
bool hasFiniteConcentrationField() const
Definition lsOxidationDiffusion.hpp:698
T getDissipationAlpha(int, int material, const Vec3D< T > &) final
If lsLocalLaxFriedrichsAnalytical is used as the spatial discretization scheme, this is called to pro...
Definition lsOxidationDiffusion.hpp:650
void setConcentrationCache(std::unordered_map< std::size_t, T > cache)
Definition lsOxidationDiffusion.hpp:709
void setMaskInterface(SmartPointer< Domain< T, D > > passedInterface, int passedMaskSign=1)
Definition lsOxidationDiffusion.hpp:297
T getEffectiveReactionRate(const Vec3D< T > &coordinate) const
Definition lsOxidationDiffusion.hpp:318
void markSolved()
Mark the current solution as valid without re-solving. Call this before any parallel advection (lsAdv...
Definition lsOxidationDiffusion.hpp:720
const std::unordered_map< std::size_t, T > & getConcentrationCache() const
Definition lsOxidationDiffusion.hpp:705
std::unordered_map< std::size_t, T > PressureLookup
Definition lsOxidationDiffusion.hpp:360
OxidationDiffusion(SmartPointer< Domain< T, D > > passedReactionInterface, SmartPointer< Domain< T, D > > passedAmbientInterface, OxidationParameters passedParameters={})
Definition lsOxidationDiffusion.hpp:267
FrontVelocity frontVelocityAt(const Vec3D< T > &coordinate) const
Definition lsOxidationDiffusion.hpp:606
T getResidual() const
Definition lsOxidationDiffusion.hpp:694
T getNormalizedResidual() const
Definition lsOxidationDiffusion.hpp:695
void apply()
Definition lsOxidationDiffusion.hpp:501
std::size_t getNumberOfSolutionNodes() const
Definition lsOxidationDiffusion.hpp:697
void setParameters(OxidationParameters passedParameters)
Definition lsOxidationDiffusion.hpp:311
T getConcentration(const IndexType &index) const
Definition lsOxidationDiffusion.hpp:670
void setPressure(const Vec3D< T > &coordinate, T pressure)
Definition lsOxidationDiffusion.hpp:469
void clearSolveBounds()
Definition lsOxidationDiffusion.hpp:495
const StressLookup & getStressCache() const
Definition lsOxidationDiffusion.hpp:368
void clearPressureField()
Definition lsOxidationDiffusion.hpp:325
static auto New(Args &&...args)
Definition lsOxidationDiffusion.hpp:274
const std::vector< T > & getInterfaceNormalStress() const
The interface normal stress the reaction law used in the last solve (per node; NaN where the law fall...
Definition lsOxidationDiffusion.hpp:377
void writeConcentrationToLevelSet()
Write per-node concentration into ambientInterface->getPointData() so that lsInterior + lsAdvect can ...
Definition lsOxidationDiffusion.hpp:725
void writePressureToLevelSet()
Write per-node pressure into ambientInterface->getPointData() so that it survives advection and can w...
Definition lsOxidationDiffusion.hpp:752
void setSolveBounds(const IndexType &passedMinIndex, const IndexType &passedMaxIndex)
Restrict the dense Cartesian diffusion solve to a finite index box. This is useful for level sets wit...
Definition lsOxidationDiffusion.hpp:486
T getScalarVelocityFromSample(const ReactionBoundarySample &sample) const
Absolute scalar velocity derived from an already-computed boundary sample, avoiding a second call to ...
Definition lsOxidationDiffusion.hpp:789
void setPressure(const IndexType &index, T pressure)
Definition lsOxidationDiffusion.hpp:460
T getReactionBoundaryConcentration(const IndexType &index) const
Definition lsOxidationDiffusion.hpp:688
unsigned getIterations() const
Definition lsOxidationDiffusion.hpp:693
void setAmbientInterface(SmartPointer< Domain< T, D > > passedInterface)
Definition lsOxidationDiffusion.hpp:291
T getScalarVelocity(const Vec3D< T > &coordinate, int material, const Vec3D< T > &normalVector, unsigned long) final
Should return a scalar value for the velocity at coordinate for a point of material with the given no...
Definition lsOxidationDiffusion.hpp:542
void getFeedback(const IndexType &index, T &pressure, std::array< T, 9 > &stress) const
The pressure and stress the last solve used at a node (reference pressure / zero stress where nothing...
Definition lsOxidationDiffusion.hpp:345
T getReactionBoundaryConcentration(const Vec3D< T > &coordinate) const
Definition lsOxidationDiffusion.hpp:681
OxidationParameters getParameters() const
Definition lsOxidationDiffusion.hpp:316
void setReactionInterface(SmartPointer< Domain< T, D > > passedInterface)
Definition lsOxidationDiffusion.hpp:285
void setOxideSigns(int passedReactionSign, int passedAmbientSign)
Set signs defining the oxide band. A node is inside oxide if reactionSign * reactionPhi >= 0 and ambi...
Definition lsOxidationDiffusion.hpp:478
void setGpuMode(GpuMode mode)
Set the GPU solver selection mode (see GpuMode). On CPU-only builds (VIENNALS_GPU_BICGSTAB not define...
Definition lsOxidationDiffusion.hpp:715
void setFeedbackCache(PressureLookup pressures, StressLookup stresses)
Definition lsOxidationDiffusion.hpp:369
std::unordered_map< std::size_t, std::array< T, 9 > > StressLookup
Definition lsOxidationDiffusion.hpp:358
bool lastSolveConverged() const
Definition lsOxidationDiffusion.hpp:696
void setStress(const Vec3D< T > &coordinate, const std::array< T, 9 > &stress)
Definition lsOxidationDiffusion.hpp:453
const PressureLookup & getPressureCache() const
Pressure and stress feedback carried from one solve to the next, like the concentration cache: lsOxid...
Definition lsOxidationDiffusion.hpp:367
ReactionBoundarySample getReactionBoundarySample(const Vec3D< T > &coordinate) const
Return the reaction boundary sample for the grid node nearest to coordinate. Used by the deformation ...
Definition lsOxidationDiffusion.hpp:780
void clearMaskInterface()
Definition lsOxidationDiffusion.hpp:305
T getConcentration(const Vec3D< T > &coordinate) const
Definition lsOxidationDiffusion.hpp:663
std::vector< T > interfaceNormalStressFor(const StressLookup &lookup) const
The interface normal stress the reaction law would use with lookup: the median sample at every node w...
Definition lsOxidationDiffusion.hpp:385
void setStress(const IndexType &index, const std::array< T, 9 > &stress)
Oxide Cauchy stress (Pa, tension positive, row-major 3x3) at a node, for the normal-stress reaction l...
Definition lsOxidationDiffusion.hpp:333
void writePersistentFields()
Convenience wrapper: persist both concentration and pressure in one call.
Definition lsOxidationDiffusion.hpp:771
Common Cartesian-grid infrastructure shared by the oxidation solver classes (diffusion,...
Definition lsOxidationSolverBase.hpp:100
static constexpr std::size_t noNode
Definition lsOxidationSolverBase.hpp:106
bool crosses(T a, T b) const
Definition lsOxidationSolverBase.hpp:147
std::size_t lookupNode(const IndexType &index) const
Definition lsOxidationSolverBase.hpp:218
std::size_t linearIndex(const IndexType &index) const
Definition lsOxidationSolverBase.hpp:224
void initNodeLookup()
Definition lsOxidationSolverBase.hpp:211
bool inBounds(const IndexType &index) const
Definition lsOxidationSolverBase.hpp:204
std::array< std::size_t, D > strides
Definition lsOxidationSolverBase.hpp:111
T gridDelta
Definition lsOxidationSolverBase.hpp:112
std::vector< std::size_t > nodeLookupFlat
Definition lsOxidationSolverBase.hpp:107
bool initializeGridFromInterfaces(SmartPointer< Domain< T, D > > reactionInterface, SmartPointer< Domain< T, D > > ambientInterface, SmartPointer< Domain< T, D > > maskInterface, bool useRequestedBounds, const IndexType &requestedMinIndex, const IndexType &requestedMaxIndex, std::size_t maxGridPoints, const std::string &solverName)
Definition lsOxidationSolverBase.hpp:334
std::array< std::size_t, D > extents
Definition lsOxidationSolverBase.hpp:110
T valueAt(ConstSparseIterator &it, const IndexType &index) const
Definition lsOxidationSolverBase.hpp:155
bool increment(IndexType &index) const
Definition lsOxidationSolverBase.hpp:233
IndexType minIndex
Definition lsOxidationSolverBase.hpp:108
int stencilOffset(const IndexType &index, unsigned direction, int offset) const
Definition lsOxidationSolverBase.hpp:129
std::size_t findNearbyNode(const IndexType &index) const
Definition lsOxidationSolverBase.hpp:247
Vec3D< T > definedLevelSetGradient(ConstSparseIterator &it, const IndexType &index, OnGrid onGrid) const
Definition lsOxidationSolverBase.hpp:170
IndexType maxIndex
Definition lsOxidationSolverBase.hpp:109
dict v
Definition LOCOSOxidation.py:203
diffusion
Definition LOCOSOxidation.py:139
d2
Definition __init__.py:36
T levelSetCrossingDistance(T insidePhi, T outsidePhi, T minBoundaryFraction, T gridDelta)
Definition lsOxidationSolverBase.hpp:86
T clampLevelSetPhi(T v)
Clamp HRLE far-field sentinels (±DBL_MAX) to ±1 before differencing to prevent DBL_MAX² overflow that...
Definition lsOxidationSolverBase.hpp:81
std::size_t gridIndexHash(const viennahrle::Index< D > &index)
Definition lsOxidationSolverBase.hpp:30
Definition lsAdvect.hpp:41
GpuMode
Where the large linear solves of the oxidation solvers run: the diffusion solve and the 3-D mechanics...
Definition lsOxidationDiffusion.hpp:43
@ Auto
Definition lsOxidationDiffusion.hpp:51
@ Gpu
Always use GPU; fail if unavailable or unsuccessful Use the GPU when it is usable,...
Definition lsOxidationDiffusion.hpp:45
@ Cpu
Always use CPU (default).
Definition lsOxidationDiffusion.hpp:44
Si front speed (k C_i / (N alpha), >= 0) and normal (level-set gradient direction) at a point on the ...
Definition lsOxidationDiffusion.hpp:600
bool found
Definition lsOxidationDiffusion.hpp:601
T speed
Definition lsOxidationDiffusion.hpp:602
Vec3D< T > normal
Definition lsOxidationDiffusion.hpp:603
Sub-grid accurate sample of the reaction boundary crossing closest to a given grid node....
Definition lsOxidationDiffusion.hpp:256
T concentration
Definition lsOxidationDiffusion.hpp:260
bool found
Definition lsOxidationDiffusion.hpp:257
unsigned crossingAxis
Definition lsOxidationDiffusion.hpp:261
IndexType nodeIndex
Definition lsOxidationDiffusion.hpp:258
T distance
Definition lsOxidationDiffusion.hpp:259
int crossingOffset
Definition lsOxidationDiffusion.hpp:262
Parameters for the steady oxidant diffusion model used by OxidationDiffusion.
Definition lsOxidationDiffusion.hpp:56
double maskConcentration
Definition lsOxidationDiffusion.hpp:92
double maskTransferCoefficient
Definition lsOxidationDiffusion.hpp:91
double transferCoefficient
Definition lsOxidationDiffusion.hpp:59
double temperature
Definition lsOxidationDiffusion.hpp:74
double minBoundaryDistance
Definition lsOxidationDiffusion.hpp:101
double relaxation
Definition lsOxidationDiffusion.hpp:104
Vec3D< double > crystalAxis
Definition lsOxidationDiffusion.hpp:89
double reactionActivationVolume
Definition lsOxidationDiffusion.hpp:75
double diffusionActivationVolume
Definition lsOxidationDiffusion.hpp:80
double reactionRateRatio111
Definition lsOxidationDiffusion.hpp:88
double diffusionCoefficient
Definition lsOxidationDiffusion.hpp:57
double maskPermeabilityRatio
Meshed mask diffusion: gamma = D_SiO2 / D_mask. When > 0 the mask BODY becomes part of the diffusion ...
Definition lsOxidationDiffusion.hpp:100
int material
Definition lsOxidationDiffusion.hpp:106
double velocitySign
Definition lsOxidationDiffusion.hpp:63
double reactionRate
Definition lsOxidationDiffusion.hpp:58
double oxidantMoleculeDensity
Definition lsOxidationDiffusion.hpp:61
double equilibriumConcentration
Definition lsOxidationDiffusion.hpp:60
double referencePressure
Definition lsOxidationDiffusion.hpp:76
double tolerance
Definition lsOxidationDiffusion.hpp:103
std::size_t maxGridPoints
Definition lsOxidationDiffusion.hpp:105
double expansionCoefficient
Definition lsOxidationDiffusion.hpp:62
unsigned maxIterations
Definition lsOxidationDiffusion.hpp:102