Skip to content

fieldsplit_velocity_ksp_rtol and fieldsplit_pressure_ksp_rtol are silently un-settable — solve() clobbers them every call #477

Description

@lmoresi

What

SNES_Stokes_SaddlePt.solve() executes

self.tolerance = tolerance          # pyx:8811 (and 8799 on the Picard branch)
self.snes.atol = self.atol
self.petsc_options.setValue("snes_max_it", snes_max_it)
self.snes.setFromOptions()

on every solve, and the tolerance setter writes (pyx:6300-6301):

self.petsc_options["fieldsplit_pressure_ksp_rtol"] = self._tolerance * 0.1
self.petsc_options["fieldsplit_velocity_ksp_rtol"] = self._tolerance * 0.033

So a user's value for either option is overwritten between being set and being read by
setFromOptions(). Both options are documented (in the tolerance docstring itself)
and both are unreachable.

Reproducer

s.tolerance = 1e-8
s.petsc_options["fieldsplit_velocity_ksp_rtol"]  = 1.0e-4    # ignored
s.petsc_options["fieldsplit_velocity_ksp_max_it"] = 7        # honoured
s.solve()
pc = s.snes.getKSP().getPC(); pc.setUp()
k = pc.getFieldSplitSubKSP()[0]
print(k.getTolerances())     # rtol 3.300e-10 (= 1e-8 * 0.033), max_it 7

Measured, res-8 box, both orderings:

set BEFORE first solve -> vel_rtol=3.300e-10 (asked 1.000e-04)   vel_max_it=7 (asked 7)
set AFTER  first solve -> vel_rtol=3.300e-10 (asked 1.000e-04)   vel_max_it=7 (asked 7)

max_it is the control: same prefix (Solver_N_fieldsplit_velocity_), same dict, same
solve — it goes through, because the tolerance setter does not touch it. Only the two
keys the setter writes are affected. Re-asserting the option after the solve does not help
either; the next solve clobbers it again.

Why it matters more than a missing knob

The failure is invisible. A sweep over the velocity inner tolerance returns rows that
are identical to five significant figures, which reads as "this variable does not matter"
rather than "this variable was never set". It cost a 12-cell configuration sweep here
before the identical cells were followed up:

outer  vel_rtol  schur_mu | ok  nl   vel      wall     |F|/|F0|
gmres  tight     eta_eff  | F   37  36633   779.5s   4.9134e-01
gmres  1e-4      eta_eff  | F   37  36612   771.9s   4.9134e-01

Six decades apart, same trajectory.

It also means the Citcom inner-tolerance margins (0.033 velocity, 0.1 pressure) have
never been tunable
. Their existence is principled — inexact inner solves must still be
bounded well below the tolerance asked of the outer solve — but their size is inherited
convention with no derivation on record, and nobody could have measured an alternative
even if they had wanted to. That is a real gap for hard nonlinear Stokes, where the
velocity block is applied once per Schur action and the inner tolerance is a direct
multiplier on total cost.

Suggested fix

Have the tolerance setter establish these as defaults rather than re-assert them:
track whether the user has written either key explicitly and leave it alone if so;
and stop calling the setter from inside solve() unless the tolerance has actually
changed. The snes_rtol/ksp_atol writes in the same setter are fine to keep — those are
the tolerance's own business — but the two sub-block rtols are separately meaningful
quantities that a user has a legitimate reason to set.

Underworld development team with AI support from Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions