Skip to content

Add upper and lowerbounds to check results. - #1031

Open
lukovdm wants to merge 15 commits into
stormchecker:masterfrom
lukovdm:soundresults
Open

lukovdm wants to merge 15 commits into
stormchecker:masterfrom
lukovdm:soundresults

Conversation

@lukovdm

@lukovdm lukovdm commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

This PR adds upper and lower bounds to ExplicitQuantitativeCheckResults and implements this for OVI and II. Further algorithms are implemented it #1048.

Upper and lower bounds are stored in a class SolutionBounds. It has an optional vector of lower bounds and an optional vector of upper bounds. No bound mean the algorithm did not try to give any bounds, infinity as a value in the bound means the algorithm tried and that is the best it came up with.

The ExplicitQuantitativeCheckResults and ExplicitQualitativeCheckResults have had there map option removed and instead get an optional bitvector states holding the states for which the values and bounds hold. This was done such that the vectors in the SolutionBounds can be used easily in conjuction with any values stored in the check results. ExplicitQualitativeCheckResults was update to be consistent with ExplicitQuantitativeCheckResults.

Solution bounds are passed around from helper to PRCTLmodel checker using (MDP/DTMC)SparseModelCheckingHelperReturnType, the dtmc variant is new.

Solution bounds in this PR have been implemented for OVI and II.

Printing of bounds has been implemented where no bound is reported with a - if no bound is present. This was done to differentiate it from infinities.

So far I have the convention that any non-maybe states get bounds that exist from the algorithm. So if only lower bounds are produced, the maybe states get 1 [1, -] where - is no bound. This could be changed.

Comment thread src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.cpp Outdated
Comment thread src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.h Outdated
Comment thread src/storm/solver/SolutionBounds.h Outdated
@lukovdm

lukovdm commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

I will do a pass over the code and clean up any weird AI smells, this draft is mostly so I can have early feedback on the architecture.

Comment thread src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.h Outdated
@sjunges

sjunges commented Aug 24, 2026 via email

Copy link
Copy Markdown
Contributor

@volkm

volkm commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Is there a difference between infinity and undefined or can they be considered the same?
For example, a conditional probability should throw an exception if the condition is not reachable but one could also set the result to undefined. There could also be a difference between not calculating the upper bound (undefined) and calculating it but getting infinity (for example in reward queries)? But I am not sure whether there is actually a distinction to be made.

@lukovdm

lukovdm commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

I guess there is a difference between I tied to find an upperbound as this is the best I came up with and I did not even try to find one. That would be the difference between infinite and undefined. But not trying is usually for all states while infinite could be for some states.

@lukovdm

lukovdm commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

This PR now includes moving away from a map in the ExplicitQua(nt/l)itativeCheckResults. This allows for using the SolutionBounds as the upper and lower bounds in the CheckResults

lukovdm and others added 10 commits September 10, 2026 14:58
(cherry picked from commit 68f0d73)
… available states in vector.

(cherry picked from commit 5a6f464)
… sentinel

The check result stores its bounds in the extended value type so that a single
interface serves every algorithm, whether or not it can bound a value by
infinity. Bounds that arrive in the plain value type are therefore widened, and
they are taken to be finite throughout while doing so: nothing hands bounds
around by sentinel, and an algorithm that has an infinite bound to report says
so by handing over the extended type in the first place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oamBujHwK891QqnAYZ6Wr
(cherry picked from commit 5632fd1)
The out-parameter that the interval and optimistic value iteration helpers use to
hand back the bounds they maintain was a raw pointer defaulting to nullptr. Storm
has a type for exactly this, storm::OptionalRef, whose own documentation names
optional function arguments as its use case, and which is already used in this way
elsewhere in the core library. Using it here keeps the callers from spelling out
an address-of and gives the parameter a name that says it does not own anything.

The pointer form remains the prevailing idiom for other optional out-parameters
in this directory (Multiplier, GameSolver, LpMinMaxLinearEquationSolver); those
are left alone, as changing them has nothing to do with the bounds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfkAUfVqCKnSCRm3fAwJzL
Constructing a check result from a plain vector of values scanned every entry
for the sentinel that storm::utility::infinity yields, which for rationals is
the literal 100000000000. On an exact reachability probability that is a
comparison against a fake infinity for every state, and it made "P=? [F ...]
--exact" report two deprecation warnings about a sentinel that path never
produces.

These constructors now widen instead, applying to the values the rule that
already governs the bounds: what arrives in the plain type is finite, and a
computation that can produce an infinity says so by handing over the extended
type. Every sparse path that can produce one does -- the reachability reward,
total reward and expected visiting time helpers all return extended vectors --
so nothing is lost. Verified that an unreachable target still reports [inf, inf]
in both the exact and the floating point case.

The remaining callers of fromSentinel are the symbolic and hybrid results, whose
helpers do still fill ADDs with the sentinel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfkAUfVqCKnSCRm3fAwJzL
(cherry picked from commit 6b71f2f17394f2b0f6e6419debf459cb1f7f3ad0)
A side that was never proven was printed as the infinity that bounds anything, so
a probability computed by a procedure that certified only one side read as
[0.3828117384, inf] -- which says the value might be arbitrarily large about a
number that cannot exceed one. The reader cannot tell that apart from a genuinely
infinite bound, which is a statement the tool does make: a state that cannot reach
the target has an expected reward of exactly [inf, inf].

Both are now distinct. A missing side prints as "-", and "inf" is reserved for a
bound that really is infinite. The JSON export already omitted the key for a
missing side and is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfkAUfVqCKnSCRm3fAwJzL
…nclose the answer

Nothing asserted the one thing the bounds claim. This adds a suite that checks,
for each of the two configurations that produce them, that

    lower <= exact <= upper   and   lower <= reported value <= upper

on properties whose answer is known in closed form: the Knuth-Yao die and the two
process consensus protocol, as until probabilities on a DTMC and on an MDP.

The configurations are listed as ones that must report both bounds, so a path that
quietly stops reporting fails rather than passing vacuously.

The tolerance of 1e-12, far below the solver precision the configurations ask for,
is there because a bound computed in doubles can land an ulp on the far side of an
answer that doubles cannot represent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfkAUfVqCKnSCRm3fAwJzL
lukovdm and others added 2 commits September 10, 2026 16:22
Comments that restate the code or a member name are removed, as are notes about
work that is not done. What is left states a reason the code does not: why
copying the result vector supplies the values outside the maybe states, why the
two bounds are swapped when complementing, and why an unverified guess is not an
upper bound. The convention for the two optional sides is stated once, on
SolutionBounds, rather than repeated with examples at each use.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfkAUfVqCKnSCRm3fAwJzL
@lukovdm

lukovdm commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

I have split of most algorithms to an new PR #1048. I think this PR should be ready for a more thorough review now. I have nothing more I want to add here.

@lukovdm
lukovdm marked this pull request as ready for review September 10, 2026 14:27
@lukovdm
lukovdm requested a review from volkm September 14, 2026 08:40
@lukovdm
lukovdm requested a lite review from Copilot and removed request for volkm September 16, 2026 07:29

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Unresolved moderate findings remain in bounds propagation and result handling.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This pull request adds optional lower and upper solution bounds to explicit model-checking results and propagates bounds from OVI and interval iteration.

Changes:

  • Introduces SolutionBounds and solver bound lifecycle support.
  • Updates result APIs and bounds propagation across model-checking helpers.
  • Adds bounds tests and registers them.
File summaries
File Summary
src/test/storm/modelchecker/bounds/SolutionBoundsTest.cpp Adds DTMC and MDP bounds tests.
src/test/storm/CMakeLists.txt Registers bounds tests.
src/storm/solver/SolutionBounds.h Defines optional lower and upper bounds.
src/storm/solver/NativeLinearEquationSolver.cpp Integrates solver-bound handling.
src/storm/solver/MinMaxLinearEquationSolver.cpp Integrates solver-bound handling.
src/storm/solver/LinearEquationSolver.cpp Integrates solver-bound handling.
src/storm/solver/IterativeMinMaxLinearEquationSolver.cpp Integrates solver-bound handling.
src/storm/solver/helper/OptimisticValueIterationHelper.h Exposes OVI bound outputs.
src/storm/solver/helper/OptimisticValueIterationHelper.cpp Reused outputs can retain a stale upper bound before verification.
src/storm/solver/helper/IntervalterationHelper.h Convenience II overload cannot request or forward bounds.
src/storm/solver/helper/IntervalterationHelper.cpp Extracts interval-iteration bounds.
src/storm/solver/AbstractEquationSolver.h Stores solver-produced bounds.
src/storm/solver/AbstractEquationSolver.cpp Manages stored solver bounds.
src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.h Adds bound-aware result storage and access.
src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp Bounds can be omitted from compact output, retained stale, or mishandled at endpoints.
src/storm/modelchecker/results/ExplicitQualitativeCheckResult.h Refactors optional state subsets.
src/storm/modelchecker/results/ExplicitQualitativeCheckResult.cpp Updates qualitative result filtering and access.
src/storm/modelchecker/reachability/SparseDtmcEliminationModelChecker.cpp Updates explicit result construction.
src/storm/modelchecker/prctl/SparseMdpPrctlModelChecker.cpp Attaches helper bounds to MDP results.
src/storm/modelchecker/prctl/SparseDtmcPrctlModelChecker.cpp Attaches helper bounds to DTMC results.
src/storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.h Carries bounds through the MDP helper API.
src/storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.cpp Interval and end-component paths drop or fail to transform bounds.
src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.h Carries bounds through the DTMC helper API.
src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.cpp Robust interval-DTMC path does not populate returned bounds.
src/storm/modelchecker/prctl/helper/MDPModelCheckingHelperReturnType.h Adds bounds to the helper return type.
src/storm/modelchecker/prctl/helper/DTMCModelCheckingHelperReturnType.h Adds bounds to the helper return type.
src/storm/modelchecker/helper/ltl/SparseLTLHelper.cpp Product-DTMC path drops bounds before the final result.
src/storm/modelchecker/csl/SparseCtmcCslModelChecker.cpp Global CTMC path drops helper bounds.
src/storm/modelchecker/csl/helper/SparseCtmcCslHelper.cpp CTMC wrapper drops DTMC helper bounds.
src/storm-gamebased-ar/modelchecker/abstraction/GameBasedMdpModelChecker.cpp Updates qualitative state access.
src/storm-dft/modelchecker/DFTModelChecker.cpp Updates result filtering.
src/storm-counterexamples/counterexamples/SMTMinimalLabelSetGenerator.h Adapts helper result access.
src/storm-counterexamples/api/counterexamples.cpp Adapts helper result access.
.gitignore Adds ignored local metadata.
Review details

Suppressed comments (7)

src/storm/modelchecker/helper/ltl/SparseLTLHelper.cpp:320

  • The product DTMC result now carries solutionBounds, but this path moves only .values into prodNumericResult. Any OVI/II enclosure is discarded before LTL model-checking returns its quantitative values; project the lower/upper vectors alongside the values and propagate them to the final result.
                                          .values);

src/storm/modelchecker/prctl/helper/SparseDtmcPrctlHelper.cpp:298

  • For interval DTMCs, the robust solver path above fills result but never fills solutionBounds; this newly returned field is therefore always empty for that branch even when the min-max solver computed an enclosure. Propagate and embed the robust solver's lower/upper vectors before returning the result.
    returnValue.solutionBounds = std::move(solutionBounds);

src/storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.cpp:794

  • The interval-model branch reaches this assertion after computeValuesForMaybeStates has produced a result object, but the following assignment copies only its values. Any lower/upper vectors returned by the solver are dropped, so interval checks report no bounds despite the non-interval branch propagating them. Copy resultForMaybeStates.solutionBounds into resultBounds here.
                    STORM_LOG_ASSERT(resultForMaybeStates.getValues().size() == transitionMatrix.getColumnCount(), "Dimensions do not match.");

src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp:187

  • SolutionBounds uses an unset side to mean that no bound was produced, but this setter only updates sides that are present and leaves any previously stored side untouched. Reusing a result and calling setBounds with only a lower (or only an upper) bound therefore exposes a stale bound that is not guaranteed for the new values. Clear this->bounds before applying the incoming sides.
void ExplicitQuantitativeCheckResult<ValueType>::setBounds(storm::solver::SolutionBounds<ExtendedValueType> bounds) {
    if (bounds.hasLower()) {
        this->setLowerBounds(std::move(*bounds.lower));
    }
    if (bounds.hasUpper()) {
        this->setUpperBounds(std::move(*bounds.upper));

src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp:410

  • The strict interior test misses thresholds equal to either reported endpoint, even though those comparisons can still be undecided. For example, with an enclosure [0, 0.5], comparing an approximate value against value < 0.5 can return true while the exact value is 0.5, but this condition emits no warning. Account for endpoint cases (and the comparison operator) when deciding whether the result is sound.
            STORM_LOG_WARN_COND(!((*bounds.lower)[offset] < bound && bound < (*bounds.upper)[offset]),
                                "The bound " << bound << " lies between the lower bound " << (*bounds.lower)[offset] << " and the upper bound "
                                             << (*bounds.upper)[offset] << ", so the comparison against it is not decided at state " << offset << ".");

src/storm/solver/helper/IntervalterationHelper.h:43

  • The bounds output parameter was added to the overload that exposes numIterations, but the convenience overload immediately below still has no way for callers to request SolutionBounds and its implementation forwards no output reference. This makes the new bounds API unavailable through one of the two public II entry points (unlike the corresponding OVI overloads); add the optional reference to this overload and forward it.
                    storm::OptionalRef<SolutionBounds<ValueType>> solutionBounds = storm::NullRef) const;

src/storm/solver/helper/OptimisticValueIterationHelper.cpp:285

  • If a caller reuses the output SolutionBounds object, an OVI run that terminates before verification leaves the previous upper vector intact because this branch only assigns upper on convergence. That stale upper vector is then reported as if it were sound for the new run; clear the supplied bounds before writing the new lower/conditional upper result.
  • Files reviewed: 33/34 changed files
  • Comments generated: 4
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/storm/modelchecker/csl/SparseCtmcCslModelChecker.cpp
Comment thread src/storm/modelchecker/csl/helper/SparseCtmcCslHelper.cpp
Comment thread src/storm/modelchecker/prctl/helper/SparseMdpPrctlHelper.cpp
Comment thread src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp
lukovdm and others added 2 commits September 17, 2026 10:25
@lukovdm
lukovdm requested a review from volkm September 17, 2026 09:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants