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 <cmath>
8#include <stdexcept>
9#include <string>
10#include <unordered_map>
11#include <utility>
12#include <vector>
13
14#include <vcTimer.hpp>
15
16#include <omp.h>
17
18#ifdef VIENNALS_GPU_BICGSTAB
20#endif
21
22namespace viennals {
23
26enum class GpuMode {
35};
36
42
47 double reactionRate = 1.;
52 double velocitySign = 1.;
53
54 // Stress coupling for reaction rate:
55 // k_eff = k * exp(-(p - p_ref) * V_k / (k_B * T)).
56 // Activation volumes are in m^3, pressure is in Pa, temperature is in K.
57 double temperature = 1273.15;
59 double referencePressure = 0.;
60
61 // Stress coupling for diffusion coefficient:
62 // D_eff = D * exp(-(p - p_ref) * V_D / (k_B * T)).
64
65 // Crystal orientation factor on reaction rate.
66 // k(n) = k * [1 + (reactionRateRatio111 - 1) * (1 - (n . crystalAxis)^2)]
67 // reactionRateRatio111 = 1 disables the correction (isotropic).
69 Vec3D<double> crystalAxis = {0., 1., 0.};
70
72 double maskConcentration = 0.;
73 double minBoundaryDistance = 1e-6;
74 unsigned maxIterations = 10000;
75 double tolerance = 1e-8;
76 double relaxation = 1.;
77 std::size_t maxGridPoints = 5000000;
78 int material = -1;
79};
80
96template <class T, int D>
97class OxidationDiffusion final : public VelocityField<T>,
98 public OxidationSolverBase<T, D> {
99 using IndexType = viennahrle::Index<D>;
100 using ConstSparseIterator =
101 viennahrle::ConstSparseIterator<typename Domain<T, D>::DomainType>;
102
103private:
104 static constexpr T boltzmannConstant = T(1.380649e-23);
105 static constexpr T minStressFactor = T(1.e-6);
106 static constexpr T maxStressFactor = T(1.e6);
107
108 // bring base members into scope
125
126 enum class Boundary { NONE, REACTION, AMBIENT, MASK };
127
128 struct Node {
129 IndexType index;
130 T concentration = 0.;
131 Vec3D<T> siNormal = {0., 0.,
132 0.}; // unit outward normal of Si surface (into oxide)
133 };
134
135 struct StencilSide {
136 T distance = 1.;
137 T nodeCoefficient = 1.;
138 T constant = 0.;
139 };
140
141 SmartPointer<Domain<T, D>> reactionInterface = nullptr;
142 SmartPointer<Domain<T, D>> ambientInterface = nullptr;
143 SmartPointer<Domain<T, D>> maskInterface = nullptr;
144 OxidationParameters parameters;
145 int reactionSign = 1;
146 int ambientSign = -1;
147 int maskSign = 1;
148 IndexType requestedMinIndex{};
149 IndexType requestedMaxIndex{};
150 unsigned iterations = 0;
151 T residual = std::numeric_limits<T>::max();
152 T normalizedResidual_ = std::numeric_limits<T>::max();
153 bool lastSolveConverged_ = false;
154 T maxScalarVelocity_ = 0.;
155 bool solved = false;
156 bool nodesDirty_ = true; // true → rebuild grid/nodes on next apply()
157 mutable std::string
158 lastLoggedBackend_; // suppresses repeated "using X" messages
159 bool useRequestedBounds = false;
160 bool warmStartable_ =
161 false; // true when nodes[i].concentration holds a prior solution
162 std::unordered_map<std::size_t, T> pressureLookup;
163 std::unordered_map<std::size_t, T> concentrationCache_;
164 std::vector<Node> nodes;
165 // Face-major flat BC arrays: index = fi * n + nodeId, where fi in [0, 2*D).
166 // Face-major layout gives coalesced GPU reads when all warp threads access
167 // the same face of consecutive nodes.
168 std::vector<Boundary> faceBCTypes_;
169 std::vector<T> faceBCDists_;
170 // Neighbor node IDs for every face (geometry-only, rebuilt with nodes).
171 // Entry == noNode for boundary / out-of-bounds faces.
172 std::vector<std::size_t> neighborIds_;
173
174 GpuMode gpuMode_ = GpuMode::Cpu;
175 GpuPreconditioner gpuPreconditioner_ = GpuPreconditioner::Jacobi;
176
177#ifdef VIENNALS_GPU_BICGSTAB
178 // Opaque pointer to device-side buffers (GpuBiCGSTABBuffers is defined only
179 // in the CUDA translation unit; we hold a raw pointer here so g++ never sees
180 // the CUDA internals).
181 gpu::GpuBiCGSTABBuffers *gpuBufs_ = nullptr;
182#endif
183
184public:
190 bool found = false;
191 IndexType nodeIndex{};
194 unsigned crossingAxis = 0; // Cartesian axis of the crossing edge
195 int crossingOffset = 0; // +1 or -1: which neighbour the crossing faces
196 };
197
199
200 OxidationDiffusion(SmartPointer<Domain<T, D>> passedReactionInterface,
201 SmartPointer<Domain<T, D>> passedAmbientInterface,
202 OxidationParameters passedParameters = {})
203 : reactionInterface(passedReactionInterface),
204 ambientInterface(passedAmbientInterface), parameters(passedParameters) {
205 }
206
208#ifdef VIENNALS_GPU_BICGSTAB
209 gpu::freeGpuBuffers(gpuBufs_);
210 gpuBufs_ = nullptr;
211#endif
212 }
213
214 template <class... Args> static auto New(Args &&...args) {
215 return SmartPointer<OxidationDiffusion>::New(std::forward<Args>(args)...);
216 }
217
221 nodesDirty_ = true;
222 solved = false;
223 }
224
225 void setReactionInterface(SmartPointer<Domain<T, D>> passedInterface) {
226 reactionInterface = passedInterface;
227 nodesDirty_ = true;
228 solved = false;
229 }
230
231 void setAmbientInterface(SmartPointer<Domain<T, D>> passedInterface) {
232 ambientInterface = passedInterface;
233 nodesDirty_ = true;
234 solved = false;
235 }
236
237 void setMaskInterface(SmartPointer<Domain<T, D>> passedInterface,
238 int passedMaskSign = 1) {
239 maskInterface = passedInterface;
240 maskSign = (passedMaskSign < 0) ? -1 : 1;
241 nodesDirty_ = true;
242 solved = false;
243 }
244
246 maskInterface = nullptr;
247 nodesDirty_ = true;
248 solved = false;
249 }
250
251 void setParameters(OxidationParameters passedParameters) {
252 parameters = passedParameters;
253 solved = false;
254 }
255
256 OxidationParameters getParameters() const { return parameters; }
257
258 T getEffectiveReactionRate(const Vec3D<T> &coordinate) const {
259 IndexType index;
260 for (unsigned i = 0; i < D; ++i)
261 index[i] = std::llround(coordinate[i] / gridDelta);
262 return getEffectiveReactionRate(index);
263 }
264
266 pressureLookup.clear();
267 solved = false;
268 }
269
270 void setPressure(const IndexType &index, T pressure) {
271 const auto key = detail::gridIndexHash<D>(index);
272 if (std::isfinite(pressure))
273 pressureLookup[key] = pressure;
274 else
275 pressureLookup.erase(key);
276 solved = false;
277 }
278
279 void setPressure(const Vec3D<T> &coordinate, T pressure) {
280 IndexType index;
281 for (unsigned i = 0; i < D; ++i)
282 index[i] = std::llround(coordinate[i] / gridDelta);
283 setPressure(index, pressure);
284 }
285
288 void setOxideSigns(int passedReactionSign, int passedAmbientSign) {
289 reactionSign = (passedReactionSign < 0) ? -1 : 1;
290 ambientSign = (passedAmbientSign < 0) ? -1 : 1;
291 solved = false;
292 }
293
296 void setSolveBounds(const IndexType &passedMinIndex,
297 const IndexType &passedMaxIndex) {
298 requestedMinIndex = passedMinIndex;
299 requestedMaxIndex = passedMaxIndex;
300 useRequestedBounds = true;
301 nodesDirty_ = true;
302 solved = false;
303 }
304
306 useRequestedBounds = false;
307 nodesDirty_ = true;
308 solved = false;
309 }
310
311 void apply() {
312 if (reactionInterface == nullptr || ambientInterface == nullptr) {
313 Logger::getInstance()
314 .addError("OxidationDiffusion: Missing level-set "
315 "interface.")
316 .print();
317 return;
318 }
319
320 if (nodesDirty_) {
321 if (!initialiseGrid())
322 return; // base class already logged the error
323 buildNodes();
324 nodesDirty_ = false;
325 if (nodes.empty())
326 Logger::getInstance()
327 .addWarning("OxidationDiffusion: no oxide nodes found after "
328 "buildNodes(). Verify that the reaction and ambient "
329 "level sets enclose a non-empty oxide band.")
330 .print();
331 }
332 solveDiffusion();
333
334 concentrationCache_.clear();
335 for (const auto &node : nodes)
336 concentrationCache_[detail::gridIndexHash<D>(node.index)] =
337 node.concentration;
338
339 maxScalarVelocity_ = 0.;
340 ConstSparseIterator reactionIt(reactionInterface->getDomain());
341 for (const auto &node : nodes) {
342 const auto sample = reactionBoundarySampleFromNode(reactionIt, node);
343 if (!sample.found)
344 continue;
345 const T rate = getEffectiveReactionRate(node.index);
346 const T vel =
347 std::abs(parameters.velocitySign) * rate * sample.concentration /
348 (parameters.oxidantMoleculeDensity * parameters.expansionCoefficient);
349 maxScalarVelocity_ = std::max(maxScalarVelocity_, vel);
350 }
351
352 solved = true;
353 }
354
355 T getScalarVelocity(const Vec3D<T> &coordinate, int material,
356 const Vec3D<T> &normalVector,
357 unsigned long /*pointId*/) final {
358 if (!solved)
359 apply();
360
361 if (parameters.material >= 0 && material != parameters.material)
362 return 0.;
363
364 IndexType index;
365 for (unsigned i = 0; i < D; ++i)
366 index[i] = std::llround(coordinate[i] / gridDelta);
367
368 const auto boundarySample = reactionBoundarySample(index);
369 const IndexType rateIndex =
370 boundarySample.found ? boundarySample.nodeIndex : index;
371 const T concentration = boundarySample.found
372 ? boundarySample.concentration
374 return parameters.velocitySign * getEffectiveReactionRate(rateIndex) *
375 concentration /
376 (parameters.oxidantMoleculeDensity *
377 parameters.expansionCoefficient);
378 }
379
380 T getDissipationAlpha(int /*direction*/, int material,
381 const Vec3D<T> & /*centralDifferences*/) final {
382 if (parameters.material >= 0 && material != parameters.material)
383 return 0.;
384
385 T bulkVel =
386 std::abs(parameters.velocitySign) * parameters.reactionRate *
387 parameters.equilibriumConcentration /
388 (parameters.oxidantMoleculeDensity * parameters.expansionCoefficient);
389
390 return std::max(maxScalarVelocity_, bulkVel);
391 }
392
393 T getConcentration(const Vec3D<T> &coordinate) const {
394 IndexType index;
395 for (unsigned i = 0; i < D; ++i)
396 index[i] = std::llround(coordinate[i] / gridDelta);
397 return getConcentration(index);
398 }
399
400 T getConcentration(const IndexType &index) const {
401 const std::size_t nodeId = lookupNode(index);
402 if (nodeId == noNode) {
403 const auto nearby = findNearbyNode(index);
404 if (nearby == noNode)
405 return 0.;
406 return nodes[nearby].concentration;
407 }
408 return nodes[nodeId].concentration;
409 }
410
411 T getReactionBoundaryConcentration(const Vec3D<T> &coordinate) const {
412 IndexType index;
413 for (unsigned i = 0; i < D; ++i)
414 index[i] = std::llround(coordinate[i] / gridDelta);
416 }
417
418 T getReactionBoundaryConcentration(const IndexType &index) const {
419 const auto sample = reactionBoundarySample(index);
420 return sample.found ? sample.concentration : getConcentration(index);
421 }
422
423 unsigned getIterations() const { return iterations; }
424 T getResidual() const { return residual; }
425 T getNormalizedResidual() const { return normalizedResidual_; }
426 bool lastSolveConverged() const { return lastSolveConverged_; }
427 std::size_t getNumberOfSolutionNodes() const { return nodes.size(); }
429 for (const auto &node : nodes)
430 if (!std::isfinite(node.concentration))
431 return false;
432 return true;
433 }
434
435 const std::unordered_map<std::size_t, T> &getConcentrationCache() const {
436 return concentrationCache_;
437 }
438
439 void setConcentrationCache(std::unordered_map<std::size_t, T> cache) {
440 concentrationCache_ = std::move(cache);
441 }
442
445 void setGpuMode(GpuMode mode) { gpuMode_ = mode; }
448 gpuPreconditioner_ = preconditioner;
449 }
450
454 void markSolved() { solved = true; }
455
460 if (nodes.empty() || ambientInterface == nullptr)
461 return;
462
463 std::vector<T> concentrations;
464 ConstSparseIterator it(ambientInterface->getDomain());
465 for (; !it.isFinished(); ++it) {
466 if (!it.isDefined())
467 continue;
468 const IndexType idx = it.getStartIndices();
469 const std::size_t nodeId = lookupNode(idx);
470 // Non-solve-grid narrow-band points (mask interior, gas-phase boundary)
471 // get concentration 0. Using equilibriumConcentration here contaminates
472 // the output: mask-interior points are impermeable (C≈0), and newly
473 // appeared bird's-beak points inherit C=1 spuriously after advection.
474 // The in-memory concentrationCache_ is the warm-start source for the
475 // next substep; the level-set value is only the fallback when that cache
476 // is cold, and C=0 converges just as quickly as C=1 from a cold start.
477 const T value = nodeId != noNode ? nodes[nodeId].concentration : T(0);
478 concentrations.push_back(std::isfinite(value) ? value : T(0));
479 }
480 ambientInterface->getPointData().insertReplaceScalarData(
481 std::move(concentrations), "OxConcentration");
482 }
483
487 if (pressureLookup.empty() || ambientInterface == nullptr)
488 return;
489
490 std::vector<T> pressures;
491 ConstSparseIterator it(ambientInterface->getDomain());
492 for (; !it.isFinished(); ++it) {
493 if (!it.isDefined())
494 continue;
495 const IndexType idx = it.getStartIndices();
496 const auto pIt = pressureLookup.find(detail::gridIndexHash<D>(idx));
497 const T value = pIt != pressureLookup.end() ? pIt->second : T(0);
498 pressures.push_back(std::isfinite(value) ? value : T(0));
499 }
500 ambientInterface->getPointData().insertReplaceScalarData(
501 std::move(pressures), "OxPressure");
502 }
503
509
513 ReactionBoundarySample
514 getReactionBoundarySample(const Vec3D<T> &coordinate) const {
515 IndexType index;
516 for (unsigned i = 0; i < D; ++i)
517 index[i] = std::llround(coordinate[i] / gridDelta);
518 return reactionBoundarySample(index);
519 }
520
524 if (!sample.found)
525 return T(0);
526 return std::abs(
528 (parameters.oxidantMoleculeDensity * parameters.expansionCoefficient));
529 }
530
531private:
532 bool initialiseGrid() {
534 reactionInterface, ambientInterface, maskInterface, useRequestedBounds,
535 requestedMinIndex, requestedMaxIndex, parameters.maxGridPoints,
536 "OxidationDiffusion");
537 }
538
539 void buildNodes() {
540 nodes.clear();
542
543 // On the first substep after an outer-step boundary the in-memory cache is
544 // empty. Fall back to the concentration stored in the level set's pointData
545 // (written by writeConcentrationToLevelSet() before the previous advection
546 // and remapped by lsAdvect + lsInterior).
547 warmStartable_ = !concentrationCache_.empty();
548 if (!warmStartable_) {
549 // Single HRLE pass restores both concentration and pressure from the
550 // pointData written by writePersistentFields() before the last advection.
551 const int cIdx = ambientInterface->getPointData().getScalarDataIndex(
552 "OxConcentration");
553 const int pIdx =
554 ambientInterface->getPointData().getScalarDataIndex("OxPressure");
555 const auto *cd =
556 (cIdx != -1) ? ambientInterface->getPointData().getScalarData(cIdx)
557 : nullptr;
558 const auto *pd =
559 (pIdx != -1) ? ambientInterface->getPointData().getScalarData(pIdx)
560 : nullptr;
561 if (cd != nullptr || pd != nullptr) {
562 ConstSparseIterator it(ambientInterface->getDomain());
563 for (; !it.isFinished(); ++it) {
564 if (!it.isDefined())
565 continue;
566 const auto ptId = it.getPointId();
567 const std::size_t key =
568 detail::gridIndexHash<D>(it.getStartIndices());
569 if (cd != nullptr && ptId < static_cast<decltype(ptId)>(cd->size()) &&
570 std::isfinite((*cd)[ptId]))
571 concentrationCache_[key] = (*cd)[ptId];
572 if (pd != nullptr && ptId < static_cast<decltype(ptId)>(pd->size()) &&
573 std::isfinite((*pd)[ptId]))
574 pressureLookup[key] = (*pd)[ptId];
575 }
576 warmStartable_ = !concentrationCache_.empty();
577 }
578 }
579
580 ConstSparseIterator reactionIt(reactionInterface->getDomain());
581 ConstSparseIterator ambientIt(ambientInterface->getDomain());
582 auto maskIt = makeMaskIterator();
583
584 IndexType index = minIndex;
585 while (true) {
586 const T reactionPhi = valueAt(reactionIt, index);
587 const T ambientPhi = valueAt(ambientIt, index);
588 if (isInsideOxide(reactionPhi, ambientPhi) &&
589 !isInsideMask(maskIt, index)) {
590 const std::size_t id = nodes.size();
591 nodeLookupFlat[linearIndex(index)] = id;
592 T seedConc = parameters.equilibriumConcentration;
593 auto cacheIt =
594 concentrationCache_.find(detail::gridIndexHash<D>(index));
595 if (cacheIt != concentrationCache_.end())
596 seedConc = cacheIt->second;
597 if (!std::isfinite(seedConc))
598 seedConc = parameters.equilibriumConcentration;
599 Node newNode{index, seedConc};
600 if (parameters.reactionRateRatio111 != T(1))
601 newNode.siNormal = computeSiNormal(index, reactionIt);
602 nodes.push_back(newNode);
603 }
604
605 if (!increment(index))
606 break;
607 }
608
609 // Precompute per-face boundary intersections into flat face-major arrays.
610 // Level-set positions are fixed during the inner solve; one pass per
611 // apply().
612 const std::size_t n = nodes.size();
613 faceBCTypes_.assign(2 * D * n, Boundary::NONE);
614 faceBCDists_.assign(2 * D * n, T(1));
615 ConstSparseIterator faceReactionIt(reactionInterface->getDomain());
616 ConstSparseIterator faceAmbientIt(ambientInterface->getDomain());
617 auto faceMaskIt = makeMaskIterator();
618 for (std::size_t id = 0; id < n; ++id) {
619 const auto &node = nodes[id];
620 for (unsigned dir = 0; dir < D; ++dir) {
621 for (int off : {-1, 1}) {
622 const unsigned fi = dir * 2u + (off == 1 ? 1u : 0u);
623 IndexType nb = node.index;
624 nb[dir] += off;
625 if (!inBounds(nb) || lookupNode(nb) != noNode)
626 continue; // NONE/1.0 already set by assign()
627 const auto bc = classifyBoundary(faceReactionIt, faceAmbientIt,
628 faceMaskIt, node.index, nb);
629 faceBCTypes_[fi * n + id] = bc.first;
630 faceBCDists_[fi * n + id] = bc.second;
631 }
632 }
633 }
634
635 // Precompute per-face neighbor node IDs (geometry-only).
636 // Used by computeFaceCoeffs() and uploaded once to GPU.
637 neighborIds_.assign(2 * D * n, noNode);
638 for (std::size_t id = 0; id < n; ++id) {
639 for (unsigned dir = 0; dir < D; ++dir) {
640 for (int off : {-1, 1}) {
641 const unsigned fi = dir * 2u + (off == 1 ? 1u : 0u);
642 IndexType nb = nodes[id].index;
643 nb[dir] += off;
644 if (inBounds(nb))
645 neighborIds_[fi * n + id] = lookupNode(nb);
646 }
647 }
648 }
649
650 bool loggedBackend = false;
651#ifdef VIENNALS_GPU_BICGSTAB
652 // Free any stale allocation from a previous buildNodes() call.
653 gpu::freeGpuBuffers(gpuBufs_);
654 gpuBufs_ = nullptr;
655
656 const bool tryGpu = (gpuMode_ == GpuMode::Gpu || gpuMode_ == GpuMode::Auto);
657 if (tryGpu) {
658 const bool useIlu0 = gpuPreconditioner_ == GpuPreconditioner::ILU0;
659 gpuBufs_ = gpu::allocGpuBuffers(static_cast<uint32_t>(n), 2 * D, useIlu0);
660
661 // Each setup step is chained with else-if: once one fails the handle is
662 // released and set to null, and every later step must be skipped rather
663 // than called with a null handle. (In GpuMode::Gpu reportGpuUnavailable
664 // throws, but in GpuMode::Auto it returns and execution continues here.)
665 if (!gpuBufs_) {
666 reportGpuUnavailable("OxidationDiffusion: GPU mode was selected, but "
667 "CUDA buffers could not be allocated or the CUDA "
668 "context could not be initialized.");
669 } else {
670 // Convert std::size_t neighbor IDs to uint32_t for the device
671 const std::size_t nf = 2u * D * n;
672 std::vector<uint32_t> nb32(nf);
673 for (std::size_t k = 0; k < nf; ++k)
674 nb32[k] = (neighborIds_[k] == noNode)
675 ? gpu::kNoNode
676 : static_cast<uint32_t>(neighborIds_[k]);
677 if (!gpu::gpuUploadNeighborIds(gpuBufs_, nb32.data(), nf)) {
678 gpu::freeGpuBuffers(gpuBufs_);
679 gpuBufs_ = nullptr;
680 reportGpuUnavailable("OxidationDiffusion: GPU mode was selected, but "
681 "uploading GPU neighbor IDs failed.");
682 } else if (useIlu0 &&
683 !gpu::gpuSetupCSR(gpuBufs_, nb32.data(),
684 static_cast<uint32_t>(n), 2 * D)) {
685 gpu::freeGpuBuffers(gpuBufs_);
686 gpuBufs_ = nullptr;
687 reportGpuUnavailable("OxidationDiffusion: GPU mode was selected, but "
688 "CUSPARSE setup for the GPU BiCGSTAB solver "
689 "failed.");
690 } else {
691 // Only claim the GPU backend once every setup step has succeeded.
692 logDiffusionBackend("GPU BiCGSTAB",
693 "preconditioner=" +
694 std::string(useIlu0 ? "ILU0" : "Jacobi"));
695 loggedBackend = true;
696 }
697 }
698 }
699#endif
700 if (!loggedBackend) {
701#ifdef VIENNALS_GPU_BICGSTAB
702 logDiffusionBackend("CPU BiCGSTAB",
703 gpuMode_ == GpuMode::Cpu
704 ? "GPU mode not selected"
705 : "GPU requested but unavailable");
706#else
707 logDiffusionBackend("CPU BiCGSTAB",
708 "ViennaLS was built without GPU BiCGSTAB support");
709#endif
710 }
711 }
712
713 // Precompute per-face coupling coefficients for the GPU SpMV.
714 //
715 // faceCoeffs[fi * n + id] = 2*D_eff / (dist_fi * distSum_axis)
716 //
717 // Only interior faces (neighbor != noNode) get a nonzero entry;
718 // boundary and out-of-bounds faces contribute to diag/b but not to
719 // the off-diagonal coupling stored here. Must be called after diag/b
720 // are computed (or at least after D_eff is known), because D_eff
721 // depends on pressure when diffusionActivationVolume != 0.
722 void computeFaceCoeffs(std::vector<T> &faceCoeffs) const {
723 const std::size_t n = nodes.size();
724 faceCoeffs.assign(2 * D * n, T(0));
725 const T eps = std::numeric_limits<T>::epsilon();
726 const std::vector<T> zeros(n, T(0));
727
728 for (std::size_t id = 0; id < n; ++id) {
729 const T D_eff = getEffectiveDiffusionCoefficient(nodes[id].index);
730 for (unsigned dir = 0; dir < D; ++dir) {
731 const unsigned fiNeg = dir * 2u;
732 const unsigned fiPos = dir * 2u + 1u;
733
734 // Use the exact side construction used by the CPU stencil so the GPU
735 // SpMV cannot drift from computeStencilAt() at cut-cell boundaries.
736 const auto neg = makeStencilSide(id, zeros, dir, -1, D_eff);
737 const auto pos = makeStencilSide(id, zeros, dir, 1, D_eff);
738 const T distNeg = neg.distance;
739 const T distPos = pos.distance;
740 const T distSum = distNeg + distPos;
741 if (distSum <= eps)
742 continue;
743
744 if (neighborIds_[fiNeg * n + id] != noNode && distNeg > eps)
745 faceCoeffs[fiNeg * n + id] = T(2) * D_eff / (distNeg * distSum);
746 if (neighborIds_[fiPos * n + id] != noNode && distPos > eps)
747 faceCoeffs[fiPos * n + id] = T(2) * D_eff / (distPos * distSum);
748 }
749 }
750 }
751
752 // Evaluates the stencil at one node: fills diag = A[i,i] and
753 // rhs = sum_j(A_off[i,j] * x[j]) + bc_constants[i].
754 // Called with x = zeros to precompute the geometry-fixed diagonal and b.
755 template <class SolverT>
756 void computeStencilAt(std::size_t nodeId, const std::vector<SolverT> &x,
757 T &diag, T &rhs) const {
758 diag = T(0);
759 rhs = T(0);
760 const T D_eff = getEffectiveDiffusionCoefficient(nodes[nodeId].index);
761 for (unsigned direction = 0; direction < D; ++direction) {
762 const auto neg = makeStencilSide(nodeId, x, direction, -1, D_eff);
763 const auto pos = makeStencilSide(nodeId, x, direction, 1, D_eff);
764 addAxisContribution(rhs, diag, neg, pos, D_eff);
765 }
766 }
767
768 // Computes Av = A * v using precomputed diagonal and b (RHS constants).
769 // (Av)[i] = precomputedDiag[i]*v[i] - rhs_at_v[i] + b[i]
770 // Stencil arithmetic stays in T; only storage uses SolverT.
771 template <class SolverT>
772 void matvec(const std::vector<SolverT> &v,
773 const std::vector<T> &precomputedDiag, const std::vector<T> &b,
774 std::vector<SolverT> &Av) const {
775#pragma omp parallel for schedule(static)
776 for (std::size_t i = 0; i < nodes.size(); ++i) {
777 T diag, rhs;
778 computeStencilAt(i, v, diag, rhs);
779 Av[i] = static_cast<SolverT>(precomputedDiag[i] * v[i] - rhs + b[i]);
780 }
781 }
782
783 void solveDiffusion() {
784 iterations = 0;
785 residual = 0.;
786 normalizedResidual_ = 0.;
787 lastSolveConverged_ = false;
788 if (nodes.empty()) {
789 lastSolveConverged_ = true;
790 return;
791 }
792
793 // Work vectors use float to halve memory bandwidth in the SpMV hot path.
794 // Stencil arithmetic and dot-product accumulation remain in T (double).
795 using SolverT = float;
796
797 const std::size_t n = nodes.size();
798 const T eps = std::numeric_limits<T>::epsilon();
799
800 // Geometry-fixed diagonal and BC source vector (kept in T for full
801 // precision).
802 Timer<> tDiag;
803 tDiag.start();
804 std::vector<T> diag(n), b(n);
805 {
806 const std::vector<SolverT> zeros(n, SolverT(0));
807#pragma omp parallel for schedule(static)
808 for (std::size_t i = 0; i < n; ++i)
809 computeStencilAt(i, zeros, diag[i], b[i]);
810 }
811 tDiag.finish();
812
813 T b_norm = T(0);
814 bool finiteSystem = true;
815 for (std::size_t i = 0; i < n; ++i) {
816 finiteSystem =
817 finiteSystem && std::isfinite(diag[i]) && std::isfinite(b[i]);
818 b_norm = std::max(b_norm, std::abs(b[i]));
819 }
820 if (b_norm < T(1e-100))
821 b_norm = T(1);
822 if (!finiteSystem) {
823 residual = std::numeric_limits<T>::infinity();
824 normalizedResidual_ = residual;
825 Logger::getInstance()
826 .addWarning("solveDiffusion: assembled non-finite matrix/RHS; "
827 "rejecting this coupled trial.")
828 .print();
829 return;
830 }
831
832 // Initial guess: warm-start or diagonal-preconditioned b. Sanitize the
833 // warm start so a previous failed solve cannot seed NaNs into the next one.
834 auto fallbackInitialGuess = [&](std::size_t i) -> T {
835 if (std::isfinite(diag[i]) && std::isfinite(b[i]) && diag[i] > eps) {
836 const T guess = b[i] / diag[i];
837 if (std::isfinite(guess))
838 return guess;
839 }
840 return T(0);
841 };
842
843 std::vector<SolverT> x(n);
844 for (std::size_t i = 0; i < n; ++i) {
845 T guess =
846 warmStartable_ ? nodes[i].concentration : fallbackInitialGuess(i);
847 if (!std::isfinite(guess))
848 guess = fallbackInitialGuess(i);
849 x[i] = static_cast<SolverT>(guess);
850 if (!std::isfinite(x[i]))
851 x[i] = SolverT(0);
852 }
853 const auto initialGuess = x;
854
855#ifdef VIENNALS_GPU_BICGSTAB
856 // ── GPU BiCGSTAB path ──────────────────────────────────────────────
857 // Engaged when GpuMode::Gpu is set and buildNodes() allocated GPU buffers.
858 // Uploads diag, b, and face coefficients fresh each call (they are
859 // pressure-dependent via D_eff), then runs the full BiCGSTAB loop on
860 // the device. Only dot-product scalars and the max-abs residual are
861 // downloaded; work vectors stay on the GPU between iterations.
862 if (gpu::gpuIsValid(gpuBufs_)) {
863 Timer<> tPrep, tUpload, tSolve;
864 tPrep.start();
865 std::vector<double> diagGpu(n), bGpu(n);
866 for (std::size_t i = 0; i < n; ++i) {
867 diagGpu[i] = static_cast<double>(diag[i]);
868 bGpu[i] = static_cast<double>(b[i]);
869 }
870
871 // Face coupling coefficients (pressure-dependent; recomputed each call)
872 std::vector<T> faceCoeffs;
873 computeFaceCoeffs(faceCoeffs);
874 std::vector<double> coeffGpu(faceCoeffs.size());
875 for (std::size_t k = 0; k < faceCoeffs.size(); ++k)
876 coeffGpu[k] = static_cast<double>(faceCoeffs[k]);
877
878 std::vector<double> xGpu(n);
879 for (std::size_t i = 0; i < n; ++i)
880 xGpu[i] = static_cast<double>(x[i]);
881 tPrep.finish();
882
883 tUpload.start();
884 const bool gpuUploadOk = gpu::gpuUploadSolverArrays(
885 gpuBufs_, diagGpu.data(), bGpu.data(), coeffGpu.data(),
886 static_cast<uint32_t>(n), faceCoeffs.size());
887 tUpload.finish();
888 if (!gpuUploadOk) {
889 if (Logger::hasDebug()) {
890 const std::string tag =
891 "diffusion n=" + std::to_string(n) + " [GPU upload failed]";
892 Logger::getInstance()
893 .addTiming(tag + " diag/b precompute", tDiag)
894 .addTiming(tag + " GPU prep+faceCoeffs", tPrep)
895 .addTiming(tag + " GPU upload", tUpload)
896 .print();
897 }
898 VIENNACORE_LOG_ERROR("OxidationDiffusion: GPU mode was selected, but "
899 "uploading GPU solver arrays or factorizing ILU "
900 "failed." +
901 gpuErrorDetail());
902 }
903
904 // Solve on GPU; xGpu holds the initial guess and receives the solution.
905 tSolve.start();
906 const bool gpuConverged = gpu::gpuSolveBiCGSTAB(
907 gpuBufs_, xGpu.data(), static_cast<double>(eps),
908 parameters.maxIterations, static_cast<double>(parameters.tolerance),
909 iterations, residual);
910 tSolve.finish();
911
912 const bool finiteGpuSolution =
913 gpuConverged &&
914 std::all_of(xGpu.begin(), xGpu.end(),
915 [](double value) { return std::isfinite(value); });
916 const unsigned gpuIterations = iterations;
917 const T gpuResidual = residual;
918
919 if (finiteGpuSolution) {
920 // Write solution back to nodes only after convergence and finite
921 // checks.
922 for (std::size_t i = 0; i < n; ++i)
923 nodes[i].concentration = static_cast<T>(xGpu[i]);
924 normalizedResidual_ = gpuResidual / b_norm;
925 lastSolveConverged_ = std::isfinite(normalizedResidual_) &&
926 normalizedResidual_ <= parameters.tolerance;
927
928 if (Logger::hasTiming()) {
929 const std::string tag = "diffusion n=" + std::to_string(n) +
930 " iters=" + std::to_string(iterations) +
931 " res=" + std::to_string(residual) + " [GPU]";
932 Logger::getInstance()
933 .addTiming(tag + " GPU BiCGSTAB", tSolve)
934 .print();
935 }
936 if (Logger::hasDebug()) {
937 const std::string tag = "diffusion n=" + std::to_string(n) + " [GPU]";
938 Logger::getInstance()
939 .addTiming(tag + " diag/b precompute", tDiag)
940 .addTiming(tag + " GPU prep+faceCoeffs", tPrep)
941 .addTiming(tag + " GPU upload", tUpload)
942 .print();
943 }
944 return;
945 }
946
947 if (Logger::hasDebug()) {
948 const std::string tag = "diffusion n=" + std::to_string(n) +
949 " iters=" + std::to_string(gpuIterations) +
950 " res=" + std::to_string(gpuResidual) +
951 " [GPU failed]";
952 Logger::getInstance()
953 .addTiming(tag + " diag/b precompute", tDiag)
954 .addTiming(tag + " GPU prep+faceCoeffs", tPrep)
955 .addTiming(tag + " GPU upload", tUpload)
956 .addTiming(tag + " GPU BiCGSTAB", tSolve)
957 .print();
958 }
959 // Under GpuMode::Auto this warns and execution continues into the CPU
960 // BiCGSTAB below, which recomputes the solution from the initial guess.
961 reportGpuUnavailable(
962 "OxidationDiffusion: GPU mode was selected, but GPU BiCGSTAB "
963 "failed, did not converge, or produced non-finite concentrations "
964 "(iters=" +
965 std::to_string(gpuIterations) +
966 ", residual=" + std::to_string(gpuResidual) + ").");
967 }
968#else
969 if (gpuMode_ == GpuMode::Gpu) {
970 VIENNACORE_LOG_ERROR("OxidationDiffusion: explicit GPU mode was "
971 "requested, but ViennaLS was built without "
972 "VIENNALS_GPU_BICGSTAB.");
973 } else if (gpuMode_ == GpuMode::Auto) {
974 VIENNACORE_LOG_WARNING("OxidationDiffusion: GPU mode Auto was requested, "
975 "but ViennaLS was built without "
976 "VIENNALS_GPU_BICGSTAB. Using the CPU solver.");
977 }
978#endif
979
980 // r = b - A*x
981 std::vector<SolverT> Ax(n);
982 matvec(x, diag, b, Ax);
983 std::vector<SolverT> r(n), r_hat(n);
984 for (std::size_t i = 0; i < n; ++i) {
985 r[i] = static_cast<SolverT>(b[i] - Ax[i]);
986 r_hat[i] = r[i];
987 }
988
989 // BiCGSTAB with diagonal (Jacobi) preconditioner.
990 // Scalars (rho, alpha, omega, beta) and dot products stay in T for
991 // stability.
992 Timer<> tCpuSolve;
993 tCpuSolve.start();
994 std::vector<SolverT> p(n, SolverT(0)), v(n, SolverT(0)), y(n), z(n), s(n),
995 t(n);
996 T rho = T(1), alpha = T(1), omega = T(1);
997 bool bicgstabBreakdown = false;
998 for (std::size_t i = 0; i < n; ++i) {
999 const T ri = static_cast<T>(r[i]);
1000 if (!std::isfinite(ri)) {
1001 bicgstabBreakdown = true;
1002 break;
1003 }
1004 residual = std::max(residual, std::abs(ri));
1005 }
1006
1007 for (; !bicgstabBreakdown && iterations < parameters.maxIterations;
1008 ++iterations) {
1009 T rho_new = T(0);
1010 for (std::size_t i = 0; i < n; ++i)
1011 rho_new += static_cast<T>(r_hat[i]) * static_cast<T>(r[i]);
1012
1013 if (!std::isfinite(rho_new)) {
1014 bicgstabBreakdown = true;
1015 break;
1016 }
1017 if (std::abs(rho_new) < T(1e-100))
1018 break;
1019 if (!std::isfinite(rho) || !std::isfinite(alpha) ||
1020 !std::isfinite(omega) || std::abs(omega) < T(1e-100)) {
1021 bicgstabBreakdown = true;
1022 break;
1023 }
1024
1025 const T beta = (rho_new / rho) * (alpha / omega);
1026 if (!std::isfinite(beta)) {
1027 bicgstabBreakdown = true;
1028 break;
1029 }
1030 rho = rho_new;
1031
1032 for (std::size_t i = 0; i < n; ++i)
1033 p[i] = static_cast<SolverT>(r[i] + beta * (p[i] - omega * v[i]));
1034
1035 for (std::size_t i = 0; i < n; ++i) {
1036 const T pi = p[i];
1037 y[i] = static_cast<SolverT>((diag[i] > eps) ? pi / diag[i] : pi);
1038 }
1039
1040 matvec(y, diag, b, v);
1041
1042 T r_hat_v = T(0);
1043 for (std::size_t i = 0; i < n; ++i)
1044 r_hat_v += static_cast<T>(r_hat[i]) * static_cast<T>(v[i]);
1045 if (!std::isfinite(r_hat_v)) {
1046 bicgstabBreakdown = true;
1047 break;
1048 }
1049 if (std::abs(r_hat_v) < T(1e-100))
1050 break;
1051
1052 alpha = rho_new / r_hat_v;
1053 if (!std::isfinite(alpha)) {
1054 bicgstabBreakdown = true;
1055 break;
1056 }
1057
1058 for (std::size_t i = 0; i < n; ++i)
1059 s[i] = static_cast<SolverT>(r[i] - alpha * v[i]);
1060
1061 residual = T(0);
1062 for (std::size_t i = 0; i < n; ++i)
1063 residual = std::max(residual, std::abs(static_cast<T>(s[i])));
1064 if (!std::isfinite(residual)) {
1065 bicgstabBreakdown = true;
1066 break;
1067 }
1068 if (residual < parameters.tolerance * b_norm) {
1069 for (std::size_t i = 0; i < n; ++i)
1070 x[i] = static_cast<SolverT>(x[i] + alpha * y[i]);
1071 ++iterations;
1072 break;
1073 }
1074
1075 for (std::size_t i = 0; i < n; ++i) {
1076 const T si = s[i];
1077 z[i] = static_cast<SolverT>((diag[i] > eps) ? si / diag[i] : si);
1078 }
1079
1080 matvec(z, diag, b, t);
1081
1082 T t_s = T(0), t_t = T(0);
1083 for (std::size_t i = 0; i < n; ++i) {
1084 t_s += static_cast<T>(t[i]) * static_cast<T>(s[i]);
1085 t_t += static_cast<T>(t[i]) * static_cast<T>(t[i]);
1086 }
1087 if (!std::isfinite(t_s) || !std::isfinite(t_t)) {
1088 bicgstabBreakdown = true;
1089 break;
1090 }
1091 omega = (t_t > T(1e-100)) ? t_s / t_t : T(0);
1092 if (!std::isfinite(omega)) {
1093 bicgstabBreakdown = true;
1094 break;
1095 }
1096
1097 for (std::size_t i = 0; i < n; ++i) {
1098 x[i] = static_cast<SolverT>(x[i] + alpha * y[i] + omega * z[i]);
1099 r[i] = static_cast<SolverT>(s[i] - omega * t[i]);
1100 }
1101
1102 residual = T(0);
1103 for (std::size_t i = 0; i < n; ++i)
1104 residual = std::max(residual, std::abs(static_cast<T>(r[i])));
1105 if (!std::isfinite(residual)) {
1106 bicgstabBreakdown = true;
1107 break;
1108 }
1109 if (residual < parameters.tolerance * b_norm) {
1110 ++iterations;
1111 break;
1112 }
1113 }
1114
1115 if (bicgstabBreakdown)
1116 residual = std::numeric_limits<T>::infinity();
1117
1118 const bool finiteCpuSolution =
1119 !bicgstabBreakdown &&
1120 std::all_of(x.begin(), x.end(),
1121 [](SolverT value) { return std::isfinite(value); });
1122 if (finiteCpuSolution) {
1123 for (std::size_t i = 0; i < n; ++i)
1124 nodes[i].concentration = x[i];
1125 } else {
1126 for (std::size_t i = 0; i < n; ++i)
1127 nodes[i].concentration = initialGuess[i];
1128 residual = std::numeric_limits<T>::infinity();
1129 Logger::getInstance()
1130 .addWarning("solveDiffusion: CPU BiCGSTAB produced non-finite "
1131 "concentrations; keeping the sanitized initial guess.")
1132 .print();
1133 }
1134 normalizedResidual_ = residual / b_norm;
1135 lastSolveConverged_ = finiteCpuSolution &&
1136 std::isfinite(normalizedResidual_) &&
1137 normalizedResidual_ <= parameters.tolerance;
1138
1139 tCpuSolve.finish();
1140 if (Logger::hasTiming()) {
1141 const std::string path = " [CPU]";
1142 const std::string tag = "diffusion n=" + std::to_string(n) +
1143 " iters=" + std::to_string(iterations) +
1144 " res=" + std::to_string(residual) + path;
1145 Logger::getInstance().addTiming(tag + " CPU BiCGSTAB", tCpuSolve).print();
1146 }
1147 if (Logger::hasDebug()) {
1148 const std::string path = " [CPU]";
1149 Logger::getInstance()
1150 .addTiming("diffusion n=" + std::to_string(n) + path +
1151 " diag/b precompute",
1152 tDiag)
1153 .print();
1154 }
1155 if (residual > parameters.tolerance * b_norm)
1156 VIENNACORE_LOG_WARNING(
1157 "solveDiffusion: BiCGSTAB did not converge after " +
1158 std::to_string(iterations) + "/" +
1159 std::to_string(parameters.maxIterations) +
1160 " iterations (residual=" + std::to_string(residual / b_norm) +
1161 ", tolerance=" + std::to_string(parameters.tolerance) + ")");
1162 }
1163
1164#ifdef VIENNALS_GPU_BICGSTAB
1165 static std::string gpuErrorDetail() {
1166 const char *detail = gpu::gpuGetLastErrorMessage();
1167 if (detail && detail[0] != '\0')
1168 return std::string(" Detail: ") + detail;
1169 return {};
1170 }
1171
1176 void reportGpuUnavailable(const std::string &message) const {
1177 if (gpuMode_ == GpuMode::Auto) {
1178 VIENNACORE_LOG_WARNING(message + gpuErrorDetail() +
1179 " Falling back to the CPU solver.");
1180 } else {
1181 VIENNACORE_LOG_ERROR(message + gpuErrorDetail());
1182 }
1183 }
1184#endif
1185
1186 void logDiffusionBackend(const std::string &backend,
1187 const std::string &detail) const {
1188 if (!Logger::hasInfo())
1189 return;
1190 const std::string msg =
1191 "OxidationDiffusion: using " + backend +
1192 " for diffusion solve (nodes=" + std::to_string(nodes.size()) +
1193 (detail.empty() ? std::string() : ", " + detail) + ").";
1194 if (msg == lastLoggedBackend_)
1195 return;
1196 lastLoggedBackend_ = msg;
1197 Logger::getInstance().addInfo(msg).print();
1198 }
1199
1200 // Uses precomputed flat faceBC arrays — no HRLE access, safe for parallel
1201 // execution.
1202 template <class SolverT>
1203 StencilSide
1204 makeStencilSide(std::size_t nodeId, const std::vector<SolverT> &previous,
1205 unsigned direction, int offset, T diffusion) const {
1206 const IndexType &nodeIndex = nodes[nodeId].index;
1207 IndexType neighbor = nodeIndex;
1208 neighbor[direction] += offset;
1209
1210 if (!inBounds(neighbor))
1211 return zeroFluxSide();
1212
1213 const std::size_t neighborId = lookupNode(neighbor);
1214 if (neighborId != noNode)
1215 return {gridDelta, 0., previous[neighborId]};
1216
1217 const unsigned fi = direction * 2u + (offset == 1 ? 1u : 0u);
1218 const std::size_t n = nodes.size();
1219 const Boundary faceType = faceBCTypes_[fi * n + nodeId];
1220 const T faceDist = faceBCDists_[fi * n + nodeId];
1221 if (faceType == Boundary::REACTION)
1222 return reactionBoundarySide(nodeIndex, faceDist, diffusion);
1223 if (faceType == Boundary::AMBIENT)
1224 return ambientBoundarySide(faceDist, diffusion);
1225 if (faceType == Boundary::MASK)
1226 return maskBoundarySide(faceDist, diffusion);
1227 return zeroFluxSide();
1228 }
1229
1230 void addAxisContribution(T &rightHandSide, T &diagonal,
1231 const StencilSide &negativeSide,
1232 const StencilSide &positiveSide, T diffusion) const {
1233 const T distanceSum = negativeSide.distance + positiveSide.distance;
1234 if (distanceSum <= std::numeric_limits<T>::epsilon())
1235 return;
1236
1237 addSideContribution(rightHandSide, diagonal, negativeSide, distanceSum,
1238 diffusion);
1239 addSideContribution(rightHandSide, diagonal, positiveSide, distanceSum,
1240 diffusion);
1241 }
1242
1243 void addSideContribution(T &rightHandSide, T &diagonal,
1244 const StencilSide &side, T distanceSum,
1245 T diffusion) const {
1246 if (side.distance <= std::numeric_limits<T>::epsilon())
1247 return;
1248
1249 const T coefficient = T(2) * diffusion / (side.distance * distanceSum);
1250 rightHandSide += coefficient * side.constant;
1251 diagonal += coefficient * (T(1) - side.nodeCoefficient);
1252 }
1253
1254 StencilSide zeroFluxSide() const { return {gridDelta, 1., 0.}; }
1255
1256 StencilSide reactionBoundarySide(const IndexType &nodeIndex, T distance,
1257 T diffusion) const {
1258 const T conductance = diffusion / distance;
1259 const T reactionRate = getEffectiveReactionRate(nodeIndex);
1260 const T denominator = conductance + reactionRate;
1261 if (denominator <= std::numeric_limits<T>::epsilon())
1262 return zeroFluxSide();
1263
1264 return {distance, conductance / denominator, 0.};
1265 }
1266
1267 ReactionBoundarySample reactionBoundarySample(const IndexType &index) const {
1268 ConstSparseIterator reactionIt(reactionInterface->getDomain());
1269
1270 const std::size_t directId = lookupNode(index);
1271 if (directId != noNode) {
1272 const auto sample =
1273 reactionBoundarySampleFromNode(reactionIt, nodes[directId]);
1274 if (sample.found)
1275 return sample;
1276 }
1277
1278 // The reaction velocity is a local interface quantity. A global nearest
1279 // search can smear open-window concentrations deep under a mask if the
1280 // local oxide band is temporarily missing or clipped near contact. Keep
1281 // the fallback strictly local, consistent with the velocity-extension
1282 // radius used elsewhere in the oxidation fields.
1283 std::size_t bestNode = std::numeric_limits<std::size_t>::max();
1284 T bestDistance2 = std::numeric_limits<T>::max();
1285
1286 for (int radius = 1; radius <= 4; ++radius) {
1287 IndexType offset{};
1288 offset.fill(-radius);
1289 while (true) {
1290 IndexType candidate = index;
1291 T distance2 = 0.;
1292 for (unsigned d = 0; d < D; ++d) {
1293 candidate[d] += offset[d];
1294 distance2 += static_cast<T>(offset[d] * offset[d]);
1295 }
1296
1297 if (distance2 > T(0) && inBounds(candidate)) {
1298 const std::size_t foundId = nodeLookupFlat[linearIndex(candidate)];
1299 if (foundId != noNode && distance2 < bestDistance2) {
1300 const auto sample =
1301 reactionBoundarySampleFromNode(reactionIt, nodes[foundId]);
1302 if (sample.found) {
1303 bestDistance2 = distance2;
1304 bestNode = foundId;
1305 }
1306 }
1307 }
1308
1309 unsigned dim = 0;
1310 for (; dim < D; ++dim) {
1311 if (offset[dim] < radius) {
1312 ++offset[dim];
1313 break;
1314 }
1315 offset[dim] = -radius;
1316 }
1317 if (dim == D)
1318 break;
1319 }
1320
1321 if (bestNode != std::numeric_limits<std::size_t>::max())
1322 break;
1323 }
1324
1325 if (bestNode == std::numeric_limits<std::size_t>::max())
1326 return {};
1327 return reactionBoundarySampleFromNode(reactionIt, nodes[bestNode]);
1328 }
1329
1331 reactionBoundarySampleFromNode(ConstSparseIterator &reactionIt,
1332 const Node &node) const {
1334 best.nodeIndex = node.index;
1335 T bestDistance = std::numeric_limits<T>::max();
1336 const T insidePhi = valueAt(reactionIt, node.index);
1337
1338 for (unsigned direction = 0; direction < D; ++direction) {
1339 for (const int offset : {-1, 1}) {
1340 IndexType neighbor = node.index;
1341 neighbor[direction] += offset;
1342 if (!inBounds(neighbor))
1343 continue;
1344
1345 const T outsidePhi = valueAt(reactionIt, neighbor);
1346 if (!crosses(insidePhi, outsidePhi))
1347 continue;
1348
1349 const T distance = crossingDistance(insidePhi, outsidePhi);
1350 if (distance >= bestDistance)
1351 continue;
1352
1353 const T diffusion = getEffectiveDiffusionCoefficient(node.index);
1354 const auto side = reactionBoundarySide(node.index, distance, diffusion);
1355 best.found = true;
1356 best.distance = distance;
1357 best.concentration =
1358 side.nodeCoefficient * node.concentration + side.constant;
1359 best.crossingAxis = direction;
1360 best.crossingOffset = offset;
1361 bestDistance = distance;
1362 }
1363 }
1364
1365 return best;
1366 }
1367
1368 StencilSide ambientBoundarySide(T distance, T diffusion) const {
1369 const T conductance = diffusion / distance;
1370 const T denominator = conductance + parameters.transferCoefficient;
1371 if (denominator <= std::numeric_limits<T>::epsilon())
1372 return zeroFluxSide();
1373
1374 return {distance, conductance / denominator,
1375 parameters.transferCoefficient *
1376 parameters.equilibriumConcentration / denominator};
1377 }
1378
1379 StencilSide maskBoundarySide(T distance, T diffusion) const {
1380 if (parameters.maskTransferCoefficient <= std::numeric_limits<T>::epsilon())
1381 return zeroFluxSide();
1382
1383 const T conductance = diffusion / distance;
1384 const T denominator = conductance + parameters.maskTransferCoefficient;
1385 if (denominator <= std::numeric_limits<T>::epsilon())
1386 return zeroFluxSide();
1387
1388 return {distance, conductance / denominator,
1389 parameters.maskTransferCoefficient * parameters.maskConcentration /
1390 denominator};
1391 }
1392
1393 T getEffectiveReactionRate(const IndexType &index) const {
1394 T rate = parameters.reactionRate;
1395
1396 if (parameters.reactionActivationVolume != T(0)) {
1397 T pressure = parameters.referencePressure;
1398 const auto foundPressure =
1399 pressureLookup.find(detail::gridIndexHash<D>(index));
1400 if (foundPressure != pressureLookup.end())
1401 pressure = foundPressure->second;
1402 if (!std::isfinite(pressure))
1403 pressure = parameters.referencePressure;
1404 const T exponent =
1405 stressExponent(pressure, parameters.reactionActivationVolume);
1406 rate *= stressFactor(exponent);
1407 }
1408
1409 if (parameters.reactionRateRatio111 != T(1)) {
1410 const std::size_t nodeId = lookupNode(index);
1411 if (nodeId != noNode) {
1412 const auto &normal = nodes[nodeId].siNormal;
1413 T dot = T(0);
1414 for (unsigned d = 0; d < D; ++d)
1415 dot += normal[d] * parameters.crystalAxis[d];
1416 rate *= T(1) +
1417 (parameters.reactionRateRatio111 - T(1)) * (T(1) - dot * dot);
1418 }
1419 }
1420
1421 return rate;
1422 }
1423
1424 T getEffectiveDiffusionCoefficient(const IndexType &index) const {
1425 if (parameters.diffusionActivationVolume == T(0))
1426 return parameters.diffusionCoefficient;
1427
1428 T pressure = parameters.referencePressure;
1429 const auto found = pressureLookup.find(detail::gridIndexHash<D>(index));
1430 if (found != pressureLookup.end())
1431 pressure = found->second;
1432 if (!std::isfinite(pressure))
1433 pressure = parameters.referencePressure;
1434
1435 const T exponent =
1436 stressExponent(pressure, parameters.diffusionActivationVolume);
1437 return parameters.diffusionCoefficient * stressFactor(exponent);
1438 }
1439
1440 T stressExponent(T pressure, T activationVolume) const {
1441 const T thermalEnergy =
1442 boltzmannConstant * std::max(parameters.temperature, T(1.));
1443 return -(pressure - parameters.referencePressure) * activationVolume /
1444 thermalEnergy;
1445 }
1446
1447 static T stressFactor(T exponent) {
1448 if (!std::isfinite(exponent))
1449 return T(1);
1450 if (exponent <= std::log(minStressFactor))
1451 return minStressFactor;
1452 if (exponent >= std::log(maxStressFactor))
1453 return maxStressFactor;
1454 return std::exp(exponent);
1455 }
1456
1457 Vec3D<T> computeSiNormal(const IndexType &index,
1458 ConstSparseIterator &reactionIt) const {
1459 // Reflect neighbor indices that fall outside the HRLE grid. This handles
1460 // REFLECTIVE boundary conditions correctly: phi(b-k) = phi(b+k), so the
1461 // centered difference at b gives zero lateral gradient as expected.
1462 auto reflectToGrid = [&](IndexType idx) {
1463 auto &g = reactionInterface->getGrid();
1464 for (unsigned d2 = 0; d2 < D; ++d2) {
1465 const auto lo = g.getMinGridPoint(d2);
1466 const auto hi = g.getMaxGridPoint(d2);
1467 if (idx[d2] < lo)
1468 idx[d2] = 2 * lo - idx[d2];
1469 if (idx[d2] > hi)
1470 idx[d2] = 2 * hi - idx[d2];
1471 }
1472 return idx;
1473 };
1474 Vec3D<T> gradient{0., 0., 0.};
1475 for (unsigned d = 0; d < D; ++d) {
1476 IndexType plus = index, minus = index;
1477 plus[d] += 1;
1478 minus[d] -= 1;
1479 gradient[d] =
1480 (detail::clampLevelSetPhi(valueAt(reactionIt, reflectToGrid(plus))) -
1482 valueAt(reactionIt, reflectToGrid(minus)))) /
1483 (T(2) * gridDelta);
1484 }
1485 T len = T(0);
1486 for (unsigned d = 0; d < D; ++d)
1487 len += gradient[d] * gradient[d];
1488 len = std::sqrt(len);
1489 if (len > std::numeric_limits<T>::epsilon()) {
1490 for (unsigned d = 0; d < D; ++d)
1491 gradient[d] /= len;
1492 } else {
1493 gradient = Vec3D<T>{0., 0., 0.};
1494 gradient[D - 1] = T(1);
1495 }
1496 return gradient;
1497 }
1498
1499 std::pair<Boundary, T> classifyBoundary(ConstSparseIterator &reactionIt,
1500 ConstSparseIterator &ambientIt,
1501 ConstSparseIterator &maskIt,
1502 const IndexType &inside,
1503 const IndexType &outside) const {
1504 const T reactionInside = valueAt(reactionIt, inside);
1505 const T reactionOutside = valueAt(reactionIt, outside);
1506 const T ambientInside = valueAt(ambientIt, inside);
1507 const T ambientOutside = valueAt(ambientIt, outside);
1508 const T maskInside = valueAtMask(maskIt, inside);
1509 const T maskOutside = valueAtMask(maskIt, outside);
1510
1511 const T reactionDistance =
1512 crosses(reactionInside, reactionOutside)
1513 ? crossingDistance(reactionInside, reactionOutside)
1514 : std::numeric_limits<T>::max();
1515 const T ambientDistance =
1516 crosses(ambientInside, ambientOutside)
1517 ? crossingDistance(ambientInside, ambientOutside)
1518 : std::numeric_limits<T>::max();
1519 const T maskDistance =
1520 maskInterface != nullptr && crosses(maskInside, maskOutside)
1521 ? crossingDistance(maskInside, maskOutside)
1522 : std::numeric_limits<T>::max();
1523
1524 if (reactionDistance == std::numeric_limits<T>::max() &&
1525 ambientDistance == std::numeric_limits<T>::max() &&
1526 maskDistance == std::numeric_limits<T>::max())
1527 return {Boundary::NONE, gridDelta};
1528
1529 if (reactionDistance <= ambientDistance && reactionDistance <= maskDistance)
1530 return {Boundary::REACTION, reactionDistance};
1531
1532 // Ambient crossing heads into mask-occupied space: the oxide/gas surface
1533 // has drifted under the nitride. Classify as MASK regardless of whether
1534 // the crossings are coincident (isMaskAtCrossing) or the outer node is
1535 // wholly inside the mask body (valueAtMask check). This is equivalent to
1536 // sealing the diffusion domain with UNION(ambientInterface, maskInterface)
1537 // without mutating any level set.
1538 if (ambientDistance != std::numeric_limits<T>::max() &&
1539 (isMaskAtCrossing(maskInside, maskOutside, ambientDistance) ||
1540 (maskInterface != nullptr &&
1541 static_cast<T>(maskSign) * valueAtMask(maskIt, outside) >= T(0))))
1542 return {Boundary::MASK, ambientDistance};
1543
1544 if (maskDistance <= ambientDistance)
1545 return {Boundary::MASK, maskDistance};
1546 return {Boundary::AMBIENT, ambientDistance};
1547 }
1548
1549 bool isInsideOxide(T reactionPhi, T ambientPhi) const {
1550 // GeometricAdvect can leave a tiny positive residual (~4*epsilon) when the
1551 // interface lands exactly on a grid point at non-zero coordinates, because
1552 // k*gridDelta is not exactly representable in floating point. Allow a
1553 // tolerance of 1e-9 grid units so that grid points on the surface (phi≈0)
1554 // are correctly classified as inside the oxide.
1555 constexpr T eps = T(1e-9);
1556 return reactionSign * reactionPhi >= -eps &&
1557 ambientSign * ambientPhi >= -eps;
1558 }
1559
1560 ConstSparseIterator makeMaskIterator() const {
1561 if (maskInterface == nullptr)
1562 return ConstSparseIterator(reactionInterface->getDomain());
1563 return ConstSparseIterator(maskInterface->getDomain());
1564 }
1565
1566 bool isInsideMask(ConstSparseIterator &maskIt, const IndexType &index) const {
1567 if (maskInterface == nullptr)
1568 return false;
1569 return maskSign * valueAt(maskIt, index) >= 0.;
1570 }
1571
1572 T valueAtMask(ConstSparseIterator &maskIt, const IndexType &index) const {
1573 if (maskInterface == nullptr)
1574 return std::numeric_limits<T>::max();
1575 return valueAt(maskIt, index);
1576 }
1577
1578 bool isMaskAtCrossing(T maskInside, T maskOutside, T distance) const {
1579 if (maskInterface == nullptr)
1580 return false;
1581 const T fraction = std::clamp(distance / gridDelta, T(0), T(1));
1582 const T insidePhi = detail::clampLevelSetPhi(maskInside);
1583 const T outsidePhi = detail::clampLevelSetPhi(maskOutside);
1584 const T maskPhi = insidePhi + fraction * (outsidePhi - insidePhi);
1585 return static_cast<T>(maskSign) * maskPhi >= T(0);
1586 }
1587
1588 T crossingDistance(T insidePhi, T outsidePhi) const {
1590 insidePhi, outsidePhi, parameters.minBoundaryDistance, gridDelta);
1591 }
1592};
1593
1594} // 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:220
bool hasFiniteConcentrationField() const
Definition lsOxidationDiffusion.hpp:428
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:380
void setConcentrationCache(std::unordered_map< std::size_t, T > cache)
Definition lsOxidationDiffusion.hpp:439
void setMaskInterface(SmartPointer< Domain< T, D > > passedInterface, int passedMaskSign=1)
Definition lsOxidationDiffusion.hpp:237
T getEffectiveReactionRate(const Vec3D< T > &coordinate) const
Definition lsOxidationDiffusion.hpp:258
~OxidationDiffusion()
Definition lsOxidationDiffusion.hpp:207
void markSolved()
Mark the current solution as valid without re-solving. Call this before any parallel advection (lsAdv...
Definition lsOxidationDiffusion.hpp:454
const std::unordered_map< std::size_t, T > & getConcentrationCache() const
Definition lsOxidationDiffusion.hpp:435
OxidationDiffusion(SmartPointer< Domain< T, D > > passedReactionInterface, SmartPointer< Domain< T, D > > passedAmbientInterface, OxidationParameters passedParameters={})
Definition lsOxidationDiffusion.hpp:200
T getResidual() const
Definition lsOxidationDiffusion.hpp:424
T getNormalizedResidual() const
Definition lsOxidationDiffusion.hpp:425
void apply()
Definition lsOxidationDiffusion.hpp:311
std::size_t getNumberOfSolutionNodes() const
Definition lsOxidationDiffusion.hpp:427
void setParameters(OxidationParameters passedParameters)
Definition lsOxidationDiffusion.hpp:251
T getConcentration(const IndexType &index) const
Definition lsOxidationDiffusion.hpp:400
void setPressure(const Vec3D< T > &coordinate, T pressure)
Definition lsOxidationDiffusion.hpp:279
void clearSolveBounds()
Definition lsOxidationDiffusion.hpp:305
void clearPressureField()
Definition lsOxidationDiffusion.hpp:265
static auto New(Args &&...args)
Definition lsOxidationDiffusion.hpp:214
void writeConcentrationToLevelSet()
Write per-node concentration into ambientInterface->getPointData() so that lsInterior + lsAdvect can ...
Definition lsOxidationDiffusion.hpp:459
void writePressureToLevelSet()
Write per-node pressure into ambientInterface->getPointData() so that it survives advection and can w...
Definition lsOxidationDiffusion.hpp:486
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:296
T getScalarVelocityFromSample(const ReactionBoundarySample &sample) const
Absolute scalar velocity derived from an already-computed boundary sample, avoiding a second call to ...
Definition lsOxidationDiffusion.hpp:523
void setPressure(const IndexType &index, T pressure)
Definition lsOxidationDiffusion.hpp:270
void setGpuPreconditioner(GpuPreconditioner preconditioner)
Set the GPU BiCGSTAB preconditioner. Jacobi matches the CPU solver.
Definition lsOxidationDiffusion.hpp:447
T getReactionBoundaryConcentration(const IndexType &index) const
Definition lsOxidationDiffusion.hpp:418
unsigned getIterations() const
Definition lsOxidationDiffusion.hpp:423
void setAmbientInterface(SmartPointer< Domain< T, D > > passedInterface)
Definition lsOxidationDiffusion.hpp:231
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:355
T getReactionBoundaryConcentration(const Vec3D< T > &coordinate) const
Definition lsOxidationDiffusion.hpp:411
OxidationParameters getParameters() const
Definition lsOxidationDiffusion.hpp:256
void setReactionInterface(SmartPointer< Domain< T, D > > passedInterface)
Definition lsOxidationDiffusion.hpp:225
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:288
void setGpuMode(GpuMode mode)
Set the GPU solver selection mode. See GpuMode for the two options. On CPU-only builds (VIENNALS_GPU_...
Definition lsOxidationDiffusion.hpp:445
bool lastSolveConverged() const
Definition lsOxidationDiffusion.hpp:426
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:514
void clearMaskInterface()
Definition lsOxidationDiffusion.hpp:245
T getConcentration(const Vec3D< T > &coordinate) const
Definition lsOxidationDiffusion.hpp:393
void writePersistentFields()
Convenience wrapper: persist both concentration and pressure in one call.
Definition lsOxidationDiffusion.hpp:505
Common Cartesian-grid infrastructure shared by the three oxidation solver classes (diffusion,...
Definition lsOxidationSolverBase.hpp:90
static constexpr std::size_t noNode
Definition lsOxidationSolverBase.hpp:96
bool crosses(T a, T b) const
Definition lsOxidationSolverBase.hpp:110
std::size_t lookupNode(const IndexType &index) const
Definition lsOxidationSolverBase.hpp:137
std::size_t linearIndex(const IndexType &index) const
Definition lsOxidationSolverBase.hpp:143
void initNodeLookup()
Definition lsOxidationSolverBase.hpp:130
bool inBounds(const IndexType &index) const
Definition lsOxidationSolverBase.hpp:123
std::array< std::size_t, D > strides
Definition lsOxidationSolverBase.hpp:101
T gridDelta
Definition lsOxidationSolverBase.hpp:102
std::vector< std::size_t > nodeLookupFlat
Definition lsOxidationSolverBase.hpp:97
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:207
std::array< std::size_t, D > extents
Definition lsOxidationSolverBase.hpp:100
viennahrle::ConstSparseIterator< typename Domain< T, D >::DomainType > ConstSparseIterator
Definition lsOxidationSolverBase.hpp:93
T valueAt(ConstSparseIterator &it, const IndexType &index) const
Definition lsOxidationSolverBase.hpp:118
bool increment(IndexType &index) const
Definition lsOxidationSolverBase.hpp:152
IndexType minIndex
Definition lsOxidationSolverBase.hpp:98
std::size_t findNearbyNode(const IndexType &index) const
Definition lsOxidationSolverBase.hpp:166
IndexType maxIndex
Definition lsOxidationSolverBase.hpp:99
dict v
Definition LOCOSOxidation.py:217
diffusion
Definition LOCOSOxidation.py:144
d2
Definition __init__.py:36
T levelSetCrossingDistance(T insidePhi, T outsidePhi, T minBoundaryFraction, T gridDelta)
Definition lsOxidationSolverBase.hpp:76
T clampLevelSetPhi(T v)
Clamp HRLE far-field sentinels (±DBL_MAX) to ±1 before differencing to prevent DBL_MAX² overflow that...
Definition lsOxidationSolverBase.hpp:71
std::size_t gridIndexHash(const viennahrle::Index< D > &index)
Definition lsOxidationSolverBase.hpp:23
Definition lsAdvect.hpp:41
GpuMode
Selects the BiCGSTAB back-end for the diffusion solve. GPU failures are reported and not silently fal...
Definition lsOxidationDiffusion.hpp:26
@ Auto
Definition lsOxidationDiffusion.hpp:34
@ Gpu
Always use GPU; fail if unavailable or unsuccessful Use the GPU when it is usable,...
Definition lsOxidationDiffusion.hpp:28
@ Cpu
Always use CPU (default).
Definition lsOxidationDiffusion.hpp:27
GpuPreconditioner
Selects the preconditioner used by the GPU BiCGSTAB solver. Jacobi matches the CPU solver's precondit...
Definition lsOxidationDiffusion.hpp:41
@ ILU0
Definition lsOxidationDiffusion.hpp:41
@ Jacobi
Definition lsOxidationDiffusion.hpp:41
Sub-grid accurate sample of the reaction boundary crossing closest to a given grid node....
Definition lsOxidationDiffusion.hpp:189
T concentration
Definition lsOxidationDiffusion.hpp:193
bool found
Definition lsOxidationDiffusion.hpp:190
unsigned crossingAxis
Definition lsOxidationDiffusion.hpp:194
IndexType nodeIndex
Definition lsOxidationDiffusion.hpp:191
T distance
Definition lsOxidationDiffusion.hpp:192
int crossingOffset
Definition lsOxidationDiffusion.hpp:195
Parameters for the steady oxidant diffusion model used by OxidationDiffusion.
Definition lsOxidationDiffusion.hpp:45
double maskConcentration
Definition lsOxidationDiffusion.hpp:72
double maskTransferCoefficient
Definition lsOxidationDiffusion.hpp:71
double transferCoefficient
Definition lsOxidationDiffusion.hpp:48
double temperature
Definition lsOxidationDiffusion.hpp:57
double minBoundaryDistance
Definition lsOxidationDiffusion.hpp:73
double relaxation
Definition lsOxidationDiffusion.hpp:76
Vec3D< double > crystalAxis
Definition lsOxidationDiffusion.hpp:69
double reactionActivationVolume
Definition lsOxidationDiffusion.hpp:58
double diffusionActivationVolume
Definition lsOxidationDiffusion.hpp:63
double reactionRateRatio111
Definition lsOxidationDiffusion.hpp:68
double diffusionCoefficient
Definition lsOxidationDiffusion.hpp:46
int material
Definition lsOxidationDiffusion.hpp:78
double velocitySign
Definition lsOxidationDiffusion.hpp:52
double reactionRate
Definition lsOxidationDiffusion.hpp:47
double oxidantMoleculeDensity
Definition lsOxidationDiffusion.hpp:50
double equilibriumConcentration
Definition lsOxidationDiffusion.hpp:49
double referencePressure
Definition lsOxidationDiffusion.hpp:59
double tolerance
Definition lsOxidationDiffusion.hpp:75
std::size_t maxGridPoints
Definition lsOxidationDiffusion.hpp:77
double expansionCoefficient
Definition lsOxidationDiffusion.hpp:51
unsigned maxIterations
Definition lsOxidationDiffusion.hpp:74