Skip to content

Commit 11f4c00

Browse files
FilipovicLadotobre1kenyastyle
authored
fix: adaptive time stepping in advection (#145)
* Limit time step at material interfaces and test with python SquareEtch example * Update logger * Reverted material interface behavior and added removeLastMaterial functionality * format * Fix material interface stability with adaptive sub-stepping of thin layers (soft landing) * Added WENO integration scheme * Add option to enable adaptive time stepping during advection * Add python bindings * Bump version * Add option to set the adaptive time stepping threshold * Fix MultiSurfaceMesh ctors * Added Runge-Kutta 3 time integration * Added Runge Kutta time integration scheme * Fixed errors from previous commit and format * Clean up * Update Python stubs * Fixed time step differences between FE and RK3 * Format AirGapDeposition example * rename integration -> discretization where appropriate * fix formatting * Updated AirGapDeposition, Deposition, and SquareEtch examples to work in 3D. Added VoidEtching python example. Removed redundant prepareLS(); call in computeRates during advection. Updated RK3 substepping. * format * fixed SquareEtch example * prepareLS(); in computeRates was added again which made it redundant in advect. * Rename to SpatialScheme, add legacy IntegrationSchemeEnum * Fixed RK3 time integration scheme to only need velocities on the sparse field * Fixed RK3 (Strong-Stability preserving Runge-Kutta 3rd order) temporal scheme. The scheme uses a constant velocity during the entire time step, while the gradient is averaged according to SSP-RK3. * Added compile-time "IntegrationScheme" naming depreciation narning. * format * Refactor Advect to use TemporalSchemeEnum and consolidate time integration * Allow for calculating intermediate velocities during higher order time integration * Added python bindings to the velocity calculation callback function * Small fixes in FromMesh and MarkVoidPoints, add velocityUpdateCallback to support velocity calculation during stages in RK integration schemes. * format * remove last velocity calculation when in single step mode * fixed failing PatternedSubstrate and VolumeToLevelSets examples, added 3D functionality to CompareSparseField and fixed ConvexHull generation for 3D. * format * remove GIT_TAG for ViennaHRLE * Added 3D options and tests for comparing domains * format * use CompareVolume for 3D and CompareArea for 2D, fix minor errors, improve python tests, add changes to python wrapper and API * Removed CompareArea from top level Python API, now it is only in d2 * Update python stubs * Small fixes * Correct inconsistency in custom z-increments. * Small fixes and improvements to tests. * Minor fix * Enhance test for lsCompareCriticalDimensions * Fixed RK2/RK3 workflow * fix faulty multiMaterial averaging in combineLevelSets * format * add multi material etch test * Fix adaptive time stepping logic for FE * format * refactor: use emplace_back, reduce test gridDelta --------- Co-authored-by: Tobias Reiter <t.reiter1@live.de> Co-authored-by: Roman Kostal <roman.kstl@gmail.com>
1 parent 5418487 commit 11f4c00

5 files changed

Lines changed: 277 additions & 204 deletions

File tree

examples/AirGapDeposition/AirGapDeposition.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ double runSimulation(AdvectKernelType &kernel,
9090
int main() {
9191

9292
constexpr int D = 2;
93-
omp_set_num_threads(8);
93+
omp_set_num_threads(16);
9494

9595
NumericType extent = 30;
9696
NumericType gridDelta = 0.5;

include/viennals/lsAdvect.hpp

Lines changed: 53 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ template <class T, int D> class Advect {
7878
bool adaptiveTimeStepping = false;
7979
unsigned adaptiveTimeStepSubdivisions = 20;
8080
static constexpr double wrappingLayerEpsilon = 1e-4;
81-
SmartPointer<Domain<T, D>> originalLevelSet = nullptr;
81+
std::vector<SmartPointer<Domain<T, D>>> initialLevelSets;
8282
std::function<bool(SmartPointer<Domain<T, D>>)> velocityUpdateCallback =
8383
nullptr;
8484

@@ -196,41 +196,43 @@ template <class T, int D> class Advect {
196196

197197
// Helper function for linear combination:
198198
// target = wTarget * target + wSource * source
199-
void combineLevelSets(double wTarget, double wSource) {
199+
bool combineLevelSets(T wTarget, T wSource) {
200+
// Calculate required expansion width based on CFL and RK steps
201+
int steps = 1;
202+
if (temporalScheme == TemporalSchemeEnum::RUNGE_KUTTA_2ND_ORDER) {
203+
steps = 2;
204+
} else if (temporalScheme == TemporalSchemeEnum::RUNGE_KUTTA_3RD_ORDER) {
205+
steps = 3;
206+
}
200207

201-
auto &domainDest = levelSets.back()->getDomain();
202-
auto &grid = levelSets.back()->getGrid();
208+
// Expand both level sets to ensure sufficient overlap
209+
int expansionWidth = std::ceil(2.0 * steps * timeStepRatio + 1);
210+
viennals::Expand<T, D>(levelSets.back(), expansionWidth).apply();
211+
viennals::Expand<T, D>(initialLevelSets.back(), expansionWidth).apply();
212+
213+
bool movedDown = false;
214+
215+
viennals::BooleanOperation<T, D> op(levelSets.back(),
216+
initialLevelSets.back(),
217+
viennals::BooleanOperationEnum::CUSTOM);
218+
op.setBooleanOperationComparator(
219+
[wTarget, wSource, &movedDown](const T &a, const T &b) {
220+
if (a != Domain<T, D>::POS_VALUE && a != Domain<T, D>::NEG_VALUE &&
221+
b != Domain<T, D>::POS_VALUE && b != Domain<T, D>::NEG_VALUE) {
222+
T res = wSource * a + wTarget * b;
223+
if (res > b)
224+
movedDown = true;
225+
return std::make_pair(res, true);
226+
}
227+
if (a == Domain<T, D>::POS_VALUE || a == Domain<T, D>::NEG_VALUE)
228+
return std::make_pair(a, false);
229+
return std::make_pair(b, false);
230+
});
231+
op.apply();
203232

204-
#pragma omp parallel num_threads(domainDest.getNumberOfSegments())
205-
{
206-
int p = 0;
207-
#ifdef _OPENMP
208-
p = omp_get_thread_num();
209-
#endif
210-
auto &segDest = domainDest.getDomainSegment(p);
211-
212-
viennahrle::Index<D> start = (p == 0)
213-
? grid.getMinGridPoint()
214-
: domainDest.getSegmentation()[p - 1];
215-
viennahrle::Index<D> end =
216-
(p != static_cast<int>(domainDest.getNumberOfSegments()) - 1)
217-
? domainDest.getSegmentation()[p]
218-
: grid.incrementIndices(grid.getMaxGridPoint());
233+
rebuildLS();
219234

220-
ConstSparseIterator itDest(domainDest, start);
221-
ConstSparseIterator itTarget(originalLevelSet->getDomain(), start);
222-
223-
unsigned definedValueIndex = 0;
224-
for (; itDest.getStartIndices() < end; ++itDest) {
225-
if (itDest.isDefined()) {
226-
itTarget.goToIndicesSequential(itDest.getStartIndices());
227-
T valSource = itDest.getValue();
228-
T valTarget = itTarget.getValue();
229-
segDest.definedValues[definedValueIndex++] =
230-
wTarget * valTarget + wSource * valSource;
231-
}
232-
}
233-
}
235+
return movedDown;
234236
}
235237

236238
void rebuildLS() {
@@ -450,6 +452,7 @@ template <class T, int D> class Advect {
450452
}
451453
const bool ignoreVoidPoints = ignoreVoids;
452454
const bool useAdaptiveTimeStepping = adaptiveTimeStepping;
455+
const auto adaptiveFactor = 1.0 / adaptiveTimeStepSubdivisions;
453456

454457
if (!storedRates.empty()) {
455458
VIENNACORE_LOG_WARNING("Advect: Overwriting previously stored rates.");
@@ -473,8 +476,9 @@ template <class T, int D> class Advect {
473476
: grid.incrementIndices(grid.getMaxGridPoint());
474477

475478
double tempMaxTimeStep = maxTimeStep;
476-
auto &tempRates = storedRates[p];
477-
tempRates.reserve(
479+
// store the rates and value of underneath LS for this segment
480+
auto &rates = storedRates[p];
481+
rates.reserve(
478482
topDomain.getNumberOfPoints() /
479483
static_cast<double>((levelSets.back())->getNumberOfSegments()) +
480484
10);
@@ -527,15 +531,14 @@ template <class T, int D> class Advect {
527531
// Case 1: Growth / Deposition (Velocity > 0)
528532
// Limit the time step based on the standard CFL condition.
529533
maxStepTime += cfl / velocity;
530-
tempRates.push_back(std::make_pair(gradNDissipation,
531-
-std::numeric_limits<T>::max()));
534+
rates.emplace_back(gradNDissipation,
535+
-std::numeric_limits<T>::max());
532536
break;
533537
} else if (velocity == 0.) {
534538
// Case 2: Static (Velocity == 0)
535539
// No time step limit imposed by this point.
536540
maxStepTime = std::numeric_limits<T>::max();
537-
tempRates.push_back(std::make_pair(gradNDissipation,
538-
std::numeric_limits<T>::max()));
541+
rates.emplace_back(gradNDissipation, std::numeric_limits<T>::max());
539542
break;
540543
} else {
541544
// Case 3: Etching (Velocity < 0)
@@ -555,30 +558,30 @@ template <class T, int D> class Advect {
555558
// Sub-case 3a: Standard Advection
556559
// Far from interface: Use full CFL time step.
557560
maxStepTime -= cfl / velocity;
558-
tempRates.push_back(std::make_pair(
559-
gradNDissipation, std::numeric_limits<T>::max()));
561+
rates.emplace_back(gradNDissipation,
562+
std::numeric_limits<T>::max());
560563
break;
561-
562564
} else {
563565
// Sub-case 3b: Interface Interaction
564-
auto adaptiveFactor = 1.0 / adaptiveTimeStepSubdivisions;
565-
if (useAdaptiveTimeStepping && difference > 0.2 * cfl) {
566+
// Use adaptiveFactor as threshold.
567+
if (useAdaptiveTimeStepping &&
568+
difference > adaptiveFactor * cfl) {
566569
// Adaptive Sub-stepping:
567570
// Approaching boundary: Force small steps to gather
568571
// flux statistics and prevent numerical overshoot ("Soft
569572
// Landing").
570573
maxStepTime -= adaptiveFactor * cfl / velocity;
571-
tempRates.push_back(std::make_pair(
572-
gradNDissipation, std::numeric_limits<T>::min()));
574+
rates.emplace_back(gradNDissipation,
575+
std::numeric_limits<T>::max());
576+
break;
573577
} else {
574578
// Terminal Step:
575579
// Within tolerance: Snap to boundary, consume budget, and
576580
// switch material.
577-
tempRates.push_back(
578-
std::make_pair(gradNDissipation, valueBelow));
579581
cfl -= difference;
580582
value = valueBelow;
581583
maxStepTime -= difference / velocity;
584+
rates.emplace_back(gradNDissipation, valueBelow);
582585
}
583586
}
584587
}
@@ -732,7 +735,8 @@ template <class T, int D> class Advect {
732735
// set the LS value to the one below
733736
auto const [gradient, dissipation] = itRS->first;
734737
T velocity = gradient - dissipation;
735-
// check if dissipation is too high
738+
// check if dissipation is too high and would cause a change in
739+
// direction of the velocity
736740
if (checkDiss && (gradient < 0 && velocity > 0) ||
737741
(gradient > 0 && velocity < 0)) {
738742
velocity = 0;

include/viennals/lsAdvectIntegrationSchemes.hpp

Lines changed: 75 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -43,110 +43,128 @@ namespace lsInternal {
4343
template <class T, int D> struct AdvectTimeIntegration {
4444
using AdvectType = viennals::Advect<T, D>;
4545

46-
static double evolveForwardEuler(AdvectType &kernel, double maxTimeStep) {
46+
static double evolveForwardEuler(AdvectType &kernel, double maxTimeStep,
47+
bool updateLowerLayers = true) {
4748
if (kernel.currentTimeStep < 0. || kernel.storedRates.empty())
4849
kernel.computeRates(maxTimeStep);
4950

5051
kernel.updateLevelSet(kernel.currentTimeStep);
5152

5253
kernel.rebuildLS();
5354

54-
kernel.adjustLowerLayers();
55+
if (updateLowerLayers)
56+
kernel.adjustLowerLayers();
5557

5658
return kernel.currentTimeStep;
5759
}
5860

5961
static double evolveRungeKutta2(AdvectType &kernel, double maxTimeStep) {
6062
// TVD Runge-Kutta 2nd Order (Heun's Method)
61-
// 1. Determine time step
62-
kernel.computeRates(maxTimeStep);
63-
const double dt = kernel.getCurrentTimeStep();
64-
65-
// 2. Save u^n
66-
if (kernel.originalLevelSet == nullptr) {
67-
kernel.originalLevelSet =
68-
viennals::Domain<T, D>::New(kernel.levelSets.back()->getGrid());
69-
}
70-
kernel.originalLevelSet->deepCopy(kernel.levelSets.back());
7163

72-
if (dt <= 0)
73-
return 0.;
64+
// Save initial level sets
65+
if (kernel.initialLevelSets.size() != kernel.levelSets.size()) {
66+
kernel.initialLevelSets.resize(kernel.levelSets.size());
67+
for (auto &ls : kernel.initialLevelSets)
68+
ls = viennals::Domain<T, D>::New(kernel.levelSets[0]->getGrid());
69+
}
70+
kernel.initialLevelSets.back()->deepCopy(kernel.levelSets.back());
71+
72+
// Save initial lower level sets only if Stage 1 will modify them (via
73+
// callback)
74+
if (kernel.velocityUpdateCallback) {
75+
for (size_t i = 0; i < kernel.levelSets.size() - 1; ++i) {
76+
kernel.initialLevelSets[i]->deepCopy(kernel.levelSets[i]);
77+
}
78+
}
7479

7580
// Stage 1: u^(1) = u^n + dt * L(u^n)
76-
kernel.updateLevelSet(dt);
81+
// Update lower layers only if we have a callback
82+
double dt1 = evolveForwardEuler(kernel, maxTimeStep,
83+
kernel.velocityUpdateCallback != nullptr);
84+
85+
if (dt1 <= 0.)
86+
return 0.;
7787

7888
if (kernel.velocityUpdateCallback)
7989
kernel.velocityUpdateCallback(kernel.levelSets.back());
8090

8191
// Stage 2: u^(n+1) = 1/2 u^n + 1/2 (u^(1) + dt * L(u^(1)))
8292
// Current level set is u^(1). Compute L(u^(1)).
83-
kernel.computeRates(dt);
8493
// Update to u* = u^(1) + dt * L(u^(1))
85-
kernel.updateLevelSet(dt);
86-
// Combine: u^(n+1) = 0.5 * u^n + 0.5 * u*
87-
kernel.combineLevelSets(0.5, 0.5);
94+
double dt2 = evolveForwardEuler(kernel, dt1, false);
8895

89-
// If we are in single step mode, the level set will be rebuilt immediately
90-
// after this, invalidating the velocity field. Thus, we skip the update.
91-
if (kernel.velocityUpdateCallback && !kernel.performOnlySingleStep)
92-
kernel.velocityUpdateCallback(kernel.levelSets.back());
93-
94-
// Finalize
95-
kernel.rebuildLS();
96+
// Combine: u^(n+1) = 0.5 * u^n + 0.5 * u*
97+
bool etched = kernel.combineLevelSets(0.5, 0.5);
9698

99+
// Restore lower level sets if etched.
100+
if (etched && kernel.velocityUpdateCallback) {
101+
for (size_t i = 0; i < kernel.levelSets.size() - 1; ++i) {
102+
kernel.levelSets[i]->deepCopy(kernel.initialLevelSets[i]);
103+
}
104+
}
97105
kernel.adjustLowerLayers();
98106

99-
return dt;
107+
return 0.5 * dt1 + 0.5 * dt2;
100108
}
101109

102110
static double evolveRungeKutta3(AdvectType &kernel, double maxTimeStep) {
103-
// 1. Determine the single time step 'dt' for all stages.
104-
kernel.computeRates(maxTimeStep);
105-
const double dt = kernel.getCurrentTimeStep();
106-
107-
// 2. Save u^n (Deep copy to preserve topology)
108-
if (kernel.originalLevelSet == nullptr) {
109-
kernel.originalLevelSet =
110-
viennals::Domain<T, D>::New(kernel.levelSets.back()->getGrid());
111+
// Save initial level sets
112+
if (kernel.initialLevelSets.size() != kernel.levelSets.size()) {
113+
kernel.initialLevelSets.resize(kernel.levelSets.size());
114+
for (auto &ls : kernel.initialLevelSets)
115+
ls = viennals::Domain<T, D>::New(kernel.levelSets[0]->getGrid());
116+
}
117+
for (size_t i = 0; i < kernel.levelSets.size(); ++i) {
118+
kernel.initialLevelSets[i]->deepCopy(kernel.levelSets[i]);
111119
}
112-
kernel.originalLevelSet->deepCopy(kernel.levelSets.back());
113-
114-
// If dt is 0 or negative, no advection is possible or needed.
115-
if (dt <= 0)
116-
return 0.;
117120

118121
// Stage 1: u^(1) = u^n + dt * L(u^n)
119-
kernel.updateLevelSet(dt);
122+
// This calculates dt based on u^n and advances to u^1.
123+
double dt1 = evolveForwardEuler(kernel, maxTimeStep,
124+
kernel.velocityUpdateCallback != nullptr);
125+
126+
if (dt1 <= 0.)
127+
return 0.;
120128

121129
if (kernel.velocityUpdateCallback)
122130
kernel.velocityUpdateCallback(kernel.levelSets.back());
123131

124132
// Stage 2: u^(2) = 3/4 u^n + 1/4 (u^(1) + dt * L(u^(1)))
125-
kernel.computeRates(dt);
126-
kernel.updateLevelSet(dt);
133+
// u* = u^(1) + dt * L(u^(1))
134+
double dt2 = evolveForwardEuler(kernel, dt1, false);
135+
127136
// Combine to get u^(2) = 0.75 * u^n + 0.25 * u*.
128-
kernel.combineLevelSets(0.75, 0.25);
137+
bool etched1 = kernel.combineLevelSets(0.75, 0.25);
129138

130-
if (kernel.velocityUpdateCallback)
131-
kernel.velocityUpdateCallback(kernel.levelSets.back());
139+
// Restore lower level sets if etched
140+
if (etched1 && kernel.velocityUpdateCallback) {
141+
for (size_t i = 0; i < kernel.levelSets.size() - 1; ++i) {
142+
kernel.levelSets[i]->deepCopy(kernel.initialLevelSets[i]);
143+
}
144+
}
132145

133-
// Stage 3: u^(n+1) = 1/3 u^n + 2/3 (u^(2) + dt * L(u^(2)))
134-
kernel.computeRates(dt);
135-
kernel.updateLevelSet(dt);
136-
// Combine to get u^(n+1) = 1/3 * u^n + 2/3 * u**.
137-
kernel.combineLevelSets(1.0 / 3.0, 2.0 / 3.0);
146+
kernel.adjustLowerLayers();
138147

139-
// If we are in single step mode, the level set will be rebuilt immediately
140-
// after this, invalidating the velocity field. Thus, we skip the update.
141-
if (kernel.velocityUpdateCallback && !kernel.performOnlySingleStep)
148+
if (kernel.velocityUpdateCallback) {
142149
kernel.velocityUpdateCallback(kernel.levelSets.back());
150+
}
143151

144-
// Finalize: Re-segment and renormalize the final result.
145-
kernel.rebuildLS();
152+
// Stage 3: u^(n+1) = 1/3 u^n + 2/3 (u^(2) + dt * L(u^(2)))
153+
// u** = u^(2) + dt * L(u^(2))
154+
double dt3 = evolveForwardEuler(kernel, dt1, false);
146155

156+
// Combine to get u^(n+1) = 1/3 * u^n + 2/3 * u**.
157+
bool etched2 = kernel.combineLevelSets(1.0 / 3.0, 2.0 / 3.0);
158+
159+
// Restore lower level sets if etched.
160+
if (etched2) {
161+
for (size_t i = 0; i < kernel.levelSets.size() - 1; ++i) {
162+
kernel.levelSets[i]->deepCopy(kernel.initialLevelSets[i]);
163+
}
164+
}
147165
kernel.adjustLowerLayers();
148166

149-
return dt;
167+
return (dt1 + dt2 + 4.0 * dt3) / 6.0;
150168
}
151169
};
152170

include/viennals/lsBooleanOperation.hpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
#include <vcSmartPointer.hpp>
1212
#include <vcVectorType.hpp>
1313

14+
#include <functional>
15+
1416
namespace viennals {
1517

1618
using namespace viennacore;
@@ -42,7 +44,8 @@ enum struct BooleanOperationEnum : unsigned {
4244
/// ComparatorType.
4345
template <class T, int D> class BooleanOperation {
4446
public:
45-
using ComparatorType = std::pair<T, bool> (*)(const T &, const T &);
47+
using ComparatorType =
48+
std::function<std::pair<T, bool>(const T &, const T &)>;
4649

4750
private:
4851
typedef typename Domain<T, D>::DomainType hrleDomainType;

0 commit comments

Comments
 (0)