Skip to content

Report the operative CNV tolerance, the convergence gate and the oscillation source - #7298

Open
hnil wants to merge 5 commits into
OPM:masterfrom
hnil:convergence-diagnostics
Open

Report the operative CNV tolerance, the convergence gate and the oscillation source#7298
hnil wants to merge 5 commits into
OPM:masterfrom
hnil:convergence-diagnostics

Conversation

@hnil

@hnil hnil commented Aug 9, 2026

Copy link
Copy Markdown
Member

Three INFOITER columns, no numerics touched: which condition granted the relaxed CNV tolerance and the tolerance actually applied (CnvRelax, CnvTolUsed), whether a physically converged iteration was blocked by the well-group/network gate (Gate), and what was unsatisfied when the Newton oscillation damping triggered (OscSource). The PRT trailer counts substeps accepted under a relaxed tolerance.

Motivation: on SPE1CASE2_RADIAL a single run now shows 1357 of 1402 iterations accepted at tolerance 1.0 via the pore-volume branch and only 45 at the nominal 1e-2 — previously invisible. INFOSTEP byte-identical on the decks tested.

🤖 Generated with Claude Code

@hnil hnil added the manual:enhancement This is an enhancement/improvent that needs to be documented in the manual label Aug 10, 2026
@hnil
hnil requested a review from bska August 10, 2026 09:01
@hnil
hnil marked this pull request as ready for review August 10, 2026 09:02
@bska

bska commented Aug 10, 2026

Copy link
Copy Markdown
Member

jenkins build this please

@hnil

hnil commented Aug 11, 2026

Copy link
Copy Markdown
Member Author

CI was red on runSimulator/tuning_trgmbe. Not a numerics change: ColumnData in tests/test_tuning_TRGMBE.cpp stored a Column* per header name while emplacing into the vector those pointers point into, with reserve(20). The INFOITER header was exactly 20 wide, so the dangling pointers only surfaced when this PR added a column. Fixed in a separate commit (index instead of pointer); the mass-balance numbers are unchanged (4.5e-8 / 2.0e-9 / 8.3e-11, all inside the thresholds).

@akva2

akva2 commented Aug 11, 2026

Copy link
Copy Markdown
Member

jenkins build this please

@bska bska left a comment

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.

Looks good for the most part. I just have a couple of remarks concerning the tabulated output format in the INFOITER file, and especially the column sizes.

Comment on lines +237 to +272
{
// Which condition granted the relaxed CNV tolerance, and the tolerance actually
// applied. Without this the accepted state's quality is invisible: the same run
// may accept some steps at tolerance-cnv and others at its relaxed twin.
os << std::right << std::setw(colSize) << to_string(report.cnvRelaxSource());

const auto tol = report.cnvToleranceApplied();
if (tol > 0.0) {
os << std::right << std::setw(colSize) << tol;
}
else {
os << std::right << std::setw(colSize) << '-';
}
}

void writeOscillationSource(std::ostream& os,
const std::string::size_type colSize,
const Opm::ConvergenceReport& report)
{
os << std::right << std::setw(colSize) << to_string(report.oscillationSource());
}

void writeConvergenceGate(std::ostream& os,
const std::string::size_type colSize,
const Opm::ConvergenceReport& report)
{
// Which secondary gate, if any, kept this iteration from counting as
// converged. These gates fire when the residual metrics themselves
// are already satisfied, so they identify iterations spent purely on
// well/group control changes or network balancing.
const auto gate = report.wellGroupTargetsViolated()
? (report.networkNeedsMoreBalancing() ? "GROUP+NETWORK" : "GROUP")
: (report.networkNeedsMoreBalancing() ? "NETWORK" : "NONE");

os << std::right << std::setw(colSize) << gate;
}

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.

As PR #7241 points out, std::setw() takes an int, not a string::size_type. While I would have liked to have this component defined in terms of string::size_type, I've (reluctantly) accepted that that would lead to too many explicit type conversions (i.e., static_cast<>s) in the calls to setw() so that it's better to use int instead of string::size_type here.

It's not a fault of this PR which was just following the established pattern, but I do think we should use int for column sizes instead of string::size_type going forward.

// are already satisfied, so they identify iterations spent purely on
// well/group control changes or network balancing.
const auto gate = report.wellGroupTargetsViolated()
? (report.networkNeedsMoreBalancing() ? "GROUP+NETWORK" : "GROUP")

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.

Does the string GROUP+NETWORK fit in the colSize? If not, we might consider including that string in the determination of the appropriate column size.

hnil and others added 4 commits August 11, 2026 14:05
An iteration can have all residual metrics converged and still not count
as converged, because a well/group control target changed or the network
needs further balancing. These secondary gates were invisible in the
convergence output, so iterations spent purely on control changes could
not be distinguished from ones spent on the physics.

Add const accessors for the two gate flags on ConvergenceReport and a
Gate column (NONE/GROUP/NETWORK/GROUP+NETWORK) to INFOITER, placed
before WellStatus since well failures append variable-length text that
must stay last on the line.
The CNV criterion has two tolerances, tolerance-cnv and tolerance-cnv-relaxed
(100x apart by default), and which one applies is decided per iteration by four
conditions.  Nothing in the output said which tolerance accepted a step, so the
local volumetric error of an accepted solution could vary by 100x within a run
with no way to tell.

ConvergenceReport now carries the granting condition and the tolerance actually
used; INFOITER gains CnvRelax (NONE|PVFRAC|DSOL|FINALIT|ITER) and CnvTolUsed
columns, and the PRT trailer counts substeps accepted only under a relaxed
tolerance.

On SPE1CASE2_RADIAL this immediately shows 1357 of 1402 iterations running at
1.0 via the pore-volume branch and only 45 at the nominal 1e-2 -- previously
that took a multi-deck study to establish.

INFOSTEP unchanged on SPE1CASE2_RADIAL, STONE2_MOD1, 3B_WSEGVALV_MODEL3,
GASLIFT-06 and 2_GCONINJE_NETV; convergencereport, gatherconvergencereport and
broadcast tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Newton oscillation detector reads reservoir residual history and its only
response is damping the reservoir update, but the oscillation may originate in
the well/group control layer, where damping dx only helps indirectly.  Nothing
in the output distinguished the two.

The convergence report now records, for iterations where damping triggered,
whether the reservoir residuals, the control layer (control equations or a
group/network gate), or both were unsatisfied; INFOITER gains an OscSource
column and the log line names the source.

Measured over the eight decks where the detector fires hardest (774 firings):
85% reservoir-only, 15% mixed, <1% control-only.  Read with the base rate in
mind -- a non-final iteration nearly always has unsatisfied reservoir residuals
-- so the discriminating signal is control participation: 0% on DISPERC_FINGERS,
STONE2_MOD1 and ACTIONX_MULT, versus 45% on WVFPEXP-01, 27% on 01-WGRUPCON and
~24% on GASLIFT-09/-10.  That separates the pure solution-oscillation decks from
those where the control layer takes part.

INFOSTEP unchanged on SPE1CASE2_RADIAL, STONE2_MOD1, 3B_WSEGVALV_MODEL3 and
GASLIFT-06.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ColumnData stored a Column* per header name while emplacing into the
vector those pointers point into.  Past the reserve of 20 the vector
reallocates and every stored pointer dangles; the INFOITER header was
exactly 20 wide, so this only surfaced once a column was added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"GROUP+NETWORK" is wider than the "Gate" heading it appears under, so the
column width has to consider the longest token each diagnostic column can
emit. Tabulate the headings with those tokens and take the width from both.
The width is unchanged in practice -- the fixed headers already force 17 --
so the INFOITER output is the same; the invariant is now explicit rather
than incidental.

Also take the column size as int in the three new writers, since that is
what std::setw() wants (cf. OPM#7241).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@hnil

hnil commented Aug 11, 2026

Copy link
Copy Markdown
Member Author

Both addressed: colSize is int in the three new writers, and the diagnostic columns are now tabulated together with the longest token each can emit, so GROUP+NETWORK is part of the width calculation rather than merely fitting by accident. The width itself does not change (the fixed headers already force 17), so INFOITER output is unaffected.

@bska

bska commented Aug 11, 2026

Copy link
Copy Markdown
Member

jenkins build this please

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

Labels

manual:enhancement This is an enhancement/improvent that needs to be documented in the manual

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants