Skip to content

Timestep regridding for 1D solver#2074

Open
wandadars wants to merge 12 commits intoCantera:mainfrom
wandadars:timestep_regridding
Open

Timestep regridding for 1D solver#2074
wandadars wants to merge 12 commits intoCantera:mainfrom
wandadars:timestep_regridding

Conversation

@wandadars
Copy link
Copy Markdown
Contributor

@wandadars wandadars commented Jan 11, 2026

During a discussion on the users group, Ray mentioned that Cantera doesn't perform any regridding if timestepping fails during a solve. This pull request is an attempt to add a capability to the 1D solver that would allow for a regrid to happen if there is a failure in the timestepping. This feature allows for a set number of regridding steps to be taken during a failure of the timestepping. The test case that we were looking at was that of a pressure field being stepped up incrementally on a counterflow diffusion flame domain. The highest pressure possible was increased when the timestep regridding is allowed. This happens when cases have a bad initial condition or a coarse grid and fails right at the start during the timestepping phase.

This is Ray's script that I tweaked a bit and used to test the regridding.

Details
import cantera as ct
import numpy as np
import matplotlib.pyplot as plt

mech_path = 'h2o2.yaml'


# Inlet parameters
fuel_X = "H2:1.0"
oxidizer_X = "O2:1.0"
fuel_T = 800.0
oxidizer_T = 711.0
mdot_fuel = 0.3  # kg/m^2/s
width = 30e-3
P_start = 10e6
P_end = 60e6
n_pressures = 100
pressure_levels = np.linspace(P_start, P_end, n_pressures)

oxidizer_mdot_factor = 3.0
n_points = 50

# Adaptive stepping with backtracking on failures
loglevel = 1

gas = ct.Solution(mech_path)
#gas = ct.Solution(mech_path, 'ohmech-RK')

def make_flame(gas):
    flame = ct.CounterflowDiffusionFlame(gas, grid=np.linspace(0, width, n_points))
    flame.max_time_step_count = 1000
    #flame.set_time_step(1.0e-5, [10,20,30,50])

    flame.set_refine_criteria(ratio=2.0, slope=0.06, curve=0.08, prune=0.02)
    flame.fuel_inlet.mdot = mdot_fuel
    flame.fuel_inlet.X = fuel_X
    flame.fuel_inlet.T = fuel_T
    flame.oxidizer_inlet.X = oxidizer_X
    flame.oxidizer_inlet.T = oxidizer_T
    return flame

def update_inlet_mdot(flame):
    rho_f = flame.fuel_inlet.phase.density
    rho_o = flame.oxidizer_inlet.phase.density
    flame.fuel_inlet.mdot = mdot_fuel
    flame.oxidizer_inlet.mdot = (mdot_fuel / rho_f) * rho_o * oxidizer_mdot_factor

flame = make_flame(gas)
flame.set_time_step_regrid(15)

def plot_diagnostics(flame, pressure, step_index, total_pressures):
    z = flame.grid
    temperature = flame.T
    velocity = flame.velocity
    spread_rate = flame.spread_rate
    heat_release = flame.heat_release_rate
    density = flame.density
    mixture_fraction = flame.mixture_fraction('Bilger')

    fig, axes = plt.subplots(3, 2, figsize=(14, 10), sharex=True)
    axes = axes.ravel()

    line_kwargs = dict(linewidth=2.0, marker='o', markersize=3,
                       markerfacecolor='none', markeredgewidth=0.6)
    axes[0].plot(z, temperature, **line_kwargs)
    axes[0].set_title('Temperature')
    axes[0].set_ylabel('T [K]')

    axes[1].plot(z, heat_release, **line_kwargs)
    axes[1].set_title('Heat Release Rate')
    axes[1].set_ylabel('HRR [W/m^3]')

    axes[2].plot(z, velocity, **line_kwargs)
    axes[2].set_title('Velocity')
    axes[2].set_ylabel('u [m/s]')

    axes[3].plot(z, spread_rate, **line_kwargs)
    axes[3].set_title('Spread Rate')
    axes[3].set_ylabel('V [1/s]')

    axes[4].plot(z, density, **line_kwargs)
    axes[4].set_title('Density')
    axes[4].set_ylabel('rho [kg/m^3]')

    axes[5].plot(z, mixture_fraction, **line_kwargs)
    axes[5].set_title('Bilger Mixture Fraction')
    axes[5].set_ylabel('Z [-]')

    for ax in axes:
        ax.grid(True, alpha=0.3)
    for ax in axes[-2:]:
        ax.set_xlabel('z [m]')

    fig.suptitle(f'Diagnostics at P = {pressure:.3e} Pa '
                 f'({step_index}/{total_pressures})')
    fig.tight_layout(rect=[0, 0.03, 1, 0.95])
    plt.show()
    plt.close(fig)

pressure_outfile = 'pressure_solutions.h5'
total_pressures = len(pressure_levels)
for step_index, pressure in enumerate(pressure_levels, start=1):
    print(f'\n=== Solving at P = {pressure:.3e} Pa ({step_index}/{total_pressures}) ===')
    flame.P = pressure
    flame.fuel_inlet.X = fuel_X
    flame.fuel_inlet.T = fuel_T
    flame.oxidizer_inlet.X = oxidizer_X
    flame.oxidizer_inlet.T = oxidizer_T
    update_inlet_mdot(flame)
    flame.solve(loglevel=loglevel, auto=False)
    flame.save(pressure_outfile, overwrite=True)
    print(f'Saved solution to {pressure_outfile}')
    plot_diagnostics(flame, pressure, step_index, total_pressures)

print(f'Number of debug outputs = {i}')
flame.show_stats()

AI Statement (required)

-Extensive use of generative AI.
Significant portions of code or documentation were generated with AI, including
logic and implementation decisions. All generated code and documentation were
reviewed and understood by the contributor. Examples: Output from agentic coding
tools and/or substantial refactoring by LLMs (web-based or local).

Checklist

  • The pull request includes a clear description of this code change
  • Commit messages have short titles and reference relevant issues
  • Build passes (scons build & scons test) and unit tests address code coverage
  • Style & formatting of contributed code follows contributing guidelines
  • AI Statement is included
  • The pull request is ready for review

@wandadars
Copy link
Copy Markdown
Contributor Author

I don't know how to edit the PR header, but this isn't a WIP anymore.

@speth speth changed the title WIP: Timestep regridding for 1D solver Timestep regridding for 1D solver Feb 3, 2026
@speth
Copy link
Copy Markdown
Member

speth commented Feb 11, 2026

Based on the tests that are failing and timing out, it seems like this is affecting the solution of steady-state reactor networks, which also use the SteadyStateSystem solver, even though those systems don't use regridding.

@codecov
Copy link
Copy Markdown

codecov Bot commented Feb 12, 2026

Codecov Report

❌ Patch coverage is 70.00000% with 33 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.63%. Comparing base (05de7fe) to head (655642e).
⚠️ Report is 33 commits behind head on main.

Files with missing lines Patch % Lines
src/oneD/Sim1D.cpp 23.52% 12 Missing and 1 partial ⚠️
src/numerics/SteadyStateSystem.cpp 83.58% 10 Missing and 1 partial ⚠️
include/cantera/numerics/SteadyStateSystem.h 60.00% 4 Missing ⚠️
include/cantera/oneD/Sim1D.h 33.33% 3 Missing and 1 partial ⚠️
interfaces/cython/cantera/_onedim.pyx 83.33% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2074      +/-   ##
==========================================
+ Coverage   77.53%   77.63%   +0.09%     
==========================================
  Files         451      451              
  Lines       52810    53150     +340     
  Branches     8828     8868      +40     
==========================================
+ Hits        40947    41261     +314     
- Misses       8888     8909      +21     
- Partials     2975     2980       +5     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Member

@speth speth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for putting this together, @wandadars. I think there are some useful ideas here. What we need now is something to show that. If there are any very simple cases where this can be exercised, it would be great to add them to the test suite. Besides that, we should add a new example demonstrating these options and cases where they have a beneficial impact on solver convergence (and perhaps when they don't).

Comment thread include/cantera/numerics/SteadyStateSystem.h
Comment on lines +231 to +240
//! Enable or disable adaptive time-step growth heuristics.
//!
//! Adaptive heuristics are only applied after successful timesteps that reuse the
//! Jacobian. If disabled, the fixed growth factor from setTimeStepGrowthFactor()
//! is used directly. Disabled by default.
//!
//! @param enabled Enable (`true`) or disable (`false`) adaptive growth heuristics.
void setAdaptiveTimeStepGrowth(bool enabled) {
m_adaptive_tstep_growth = enabled;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likewise for this option.

heuristic);
}
m_tstep_growth_heuristic = heuristic;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I think the heuristics should be specified by a name rather than an integer. So, something like steady-norm, transient-residual, etc.
  2. Besides an example case where each of these is effective, these definitely need at least some non-trivial tests, like an actual solve where they are active.

Comment on lines +334 to +338
//! Determine the timestep growth factor after a successful step.
//!
//! Called only when a successful step reuses the current Jacobian.
double timeStepIncreaseFactor(span<const double> x_before,
span<const double> x_after);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This name invites confusion with the property timeStepGrowthFactor. Perhaps calculateTimeStepGrowthFactor?

Comment on lines +358 to +361
//! If `true`, use heuristic gating for successful-step growth; otherwise use the
//! fixed growth factor.
bool m_adaptive_tstep_growth = false;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could define this as another growth factor case, "fixed-growth" as opposed to having a separate variable.

Comment thread src/numerics/SteadyStateSystem.cpp Outdated
Comment on lines +127 to +179
if (loglevel == 1) { // At level 1, output concise information
double ss = ssnorm(x, r);
writelog("\n{:<5d} {:<6.4e} {:>7.4f}", n, dt, log10(ss));
} else if (loglevel > 1) {
double ss = ssnorm(x, r);
writelog("\nTimestep ({}) dt= {:<11.4e} log(ss)= {:<7.4f}", n, dt, log10(ss));
if (loglevel >= 1) {
double ss_before = ssnorm(x, r);
if (loglevel == 1) { // At level 1, output concise information
writelog("\n{:<5d} {:<6.4e} {:>7.4f}", n, dt, log10(ss_before));
} else {
writelog("\nTimestep ({}) dt= {:<11.4e} log(ss)= {:<7.4f}", n, dt,
log10(ss_before));
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Despite the separate calls to ssnorm in each branch, the previous version of this was more concise.

Comment thread src/numerics/SteadyStateSystem.cpp Outdated
Comment on lines +199 to +204
double grow_factor = 1.0;
if (m_jac->nEvals() == j0) {
dt *= 1.5;
grow_factor = timeStepIncreaseFactor(x, r);
}
copy(r.begin(), r.end(), x.begin());
dt *= grow_factor;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simpler, and with a more minimal diff:

Suggested change
double grow_factor = 1.0;
if (m_jac->nEvals() == j0) {
dt *= 1.5;
grow_factor = timeStepIncreaseFactor(x, r);
}
copy(r.begin(), r.end(), x.begin());
dt *= grow_factor;
if (m_jac->nEvals() == j0) {
dt *= timeStepIncreaseFactor(x, r);
}
copy(r.begin(), r.end(), x.begin());

Comment thread src/oneD/Sim1D.cpp Outdated
Comment thread src/oneD/Sim1D.cpp Outdated
Comment thread test/python/test_onedim.py Outdated
@speth
Copy link
Copy Markdown
Member

speth commented Mar 25, 2026

Thanks for the updates, @wandadars. I just wanted to check, is this ready for another review?

@wandadars
Copy link
Copy Markdown
Contributor Author

Yes @speth I think so. Sorry for the delay.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants