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