Skip to content

Extended number to replace infinities on rationals - #1040

Merged
lukovdm merged 33 commits into
stormchecker:masterfrom
lukovdm:extended-number
Sep 7, 2026
Merged

Extended number to replace infinities on rationals#1040
lukovdm merged 33 commits into
stormchecker:masterfrom
lukovdm:extended-number

Conversation

@lukovdm

@lukovdm lukovdm commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

In storm infinities in rational numbers are currently encoded as 10^11 (the sentinel). To get rid of this we introduce ExtendedValue<ValueType> which contains a value and flags to denote positive and negative infinity.

We also introduce a ExtendedValueType<ValueType> which gives the ValueType if it already supports infinity (doubles) and returns ExtendedValue<ValueType> if it does not.

This ExtendedValueType is used in any location where it is neccesary to calculate with infinities or on outside facing API endpoints. This mean that for example CheckResults return a vector of ExtendedValueType. Several other ways to store infinities in storm have also been consolidated into ExtendedValueType, the most prominent being Extremum<ValueType>.

This PR also adds STORM_LOG_DEPRECATED which, in DEBUG mode, prints a deprecation warning once. In RELEASE mode, this macro does nothing. - Tim

Extremum

Extremum no longer has two ways to store the value but always goes through ExtendedValueType. This also solves the issue where Etremum<ValueType> behaved differently on Doubles then on RationalNumbers. On doubles if you assign infinity to a just constructed extremum it would still be considerd empty. On rational numbers if you assign "infinity" (think 10^11) to an extremum it would no longer be empty. The old double semantics now work for all value types extremum is instantiated with. I tried to test and review this but would appreciate an extra set of eyes on this.

Performance

After running experiments both on multiple areas of storm and more detailed using a reduced set of experiments from the revised practitioners guide no changes more then +1% where found and those were also within the precision of the tests.

Reviewing

I have reviewed all code myself already however I have less understanding of several parts of Storm most importantly:

  • storm-dft
  • storm-pars

These could use some extra attention during the review.

Still using the sentinel

Several locations have not been done yet. This PR is waiting for #1004 to be merged such that this work can also be applied there. Also I have net been able to figure out how to make ExtendedRational work in the sylvan storm c wrapper.

  • Decision diagram leaves (all engines: symbolic, hybrid, game-based). The sentinel is written into an ADD constant and compared out of one. DdManager.cpp:68 (getInfinity()), SymbolicDtmcPrctlHelper.cpp:195,204,240, SymbolicMdpPrctlHelper.cpp:320,358, HybridDtmcPrctlHelper.cpp:297,375,379, HybridMdpPrctlHelper.cpp:611,770,774, SymbolicMinMaxLinearEquationSolver.cpp:37, SymbolicGameSolver.cpp:26,28, AbstractAbstractionRefinementModelChecker.cpp:904. Blocked on Sylvan leaf payloads; CUDD is double-only and unaffected.
  • storm-pomdp belief exploration. Computes with the sentinel rather than only passing it through: BeliefExplorationPomdpModelChecker.cpp:1087,1208,1221,1237,1264,1354,1374,1414. ExtremePOMDPValueBound also carries a parallel isInfinite BitVector (BeliefExplorationBounds.h:56, threaded through BeliefManager.h:97,108). Left alone pending Revised POMDP belief exploration #1004.

Bridges that exist only until the above are gone

When all locations that still use the old sentinel way are gone these methods can be removed.

  • toSentinel — ExplicitQuantitativeCheckResult.cpp:189 (inside getSentinelValueVector), HybridMdpPrctlModelChecker.cpp:186.
  • fromSentinel — DD result reads in SymbolicQuantitativeCheckResult.cpp:135,150,177,182,192 and HybridQuantitativeCheckResult.cpp:161,163,181,195,234,237,245,248,256,258; POMDP in BeliefExplorationPomdpModelChecker.cpp:59,70,165,166 and BeliefMdpExplorer.cpp:1089.
  • getSentinelValueVector — 5 callers in PreprocessingPomdpValueBoundsModelChecker.cpp:104,140,182,203,337 and BeliefMdpExplorer.cpp:923. Returns by value; the calls it replaced could move.

lukovdm and others added 24 commits August 24, 2026 16:09
Several places used storm::utility::infinity<T>() -- which is the literal
100000000000 for the exact value types -- as a stand-in for something
structural rather than for a value: a fold over an empty set, a bound that
the formula does not have, or a marker on a row. None of them needs a
number, and all of them are wrong for exact types the moment a real value
grows past the sentinel.

  * The CSL time-bounded helpers and their model checkers take
    std::optional<ValueType> for the upper time bound instead of
    synthesizing an infinite one when the formula has no bound, following
    the precedent of LpSolver. computeBoundedUntilProbabilitiesImca now
    rejects an absent upper bound instead of computing a step count from
    the sentinel.

  * AcyclicSolverHelper stores std::optional factors and skips the
    multiplication for rows whose b entry must be zero. That row used to
    compute one / (one - one) and multiply the entry by the result, which
    is a division by zero for every exact type.

  * The min/max folds in RobustParameterLifter and
    GradientDescentInstantiationSearcher use Extremum and std::optional,
    which also avoids constructing a large rational per fold.

  * MenuGameRefiner's Dijkstra initialisation uses a finite distance that
    no reachable state can attain instead of an infinite one.

  * DftExplorationHeuristic and PreprocessingPomdpValueBoundsModelChecker
    express "not computed yet" as std::optional and a BitVector.

  * storm-pomdp's sizeThresholdInit used infinity<uint64_t>(), which is
    numeric_limits<uint64_t>::infinity() and therefore zero. Zero is the
    codebase's own "pick a heuristic default" value, so "no size limit"
    was only half implemented: the over-approximation reinterpreted the
    zero as max() and was accidentally right, while the under-
    approximation fell through to a heuristic threshold. It now says
    max().

With those gone, infinity<T>() is ill-formed for integral types and
isInfinity<T>() is false for them, so the class of bug above cannot
return. The two DdManager instantiations of getInfinity<uint_fast64_t>
had no callers and would have produced an ADD of zeros.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
storm::utility::infinity<T>() is the literal 100000000000 for every exact
type, and isInfinity is a comparison against it. That aliases a reward of
1e11 with infinity, is not absorbing under arithmetic, is not the maximum
of the order, and has no negative counterpart, so every place that needs
a signed infinity has invented its own encoding.

ExtendedNumber<T> is a tag {NegativeInfinity, Finite, PositiveInfinity}
next to a T. It has a total order in which the infinities really are the
extremes, absorbing arithmetic, and unary minus. The exact value types
have no NaN, so the forms that are undefined over the extended reals --
inf - inf, 0 * inf and inf / inf -- throw an InvalidOperationException
rather than producing a quiet junk value.

The operators are hidden friends, so a finite T converts implicitly on
either side of them and mixed expressions read as they did before.
NumberTraits and std::numeric_limits are specialized for the type, which
is what lets the generic storm::utility::infinity and isInfinity reach it
without a special case.

Nothing pays for this that does not use it. ExtendedValueType<T> is

    std::conditional_t<NumberTraits<T>::HasInfinity, T, ExtendedNumber<T>>

so ExtendedValueType<double> is double: the floating point path keeps its
IEEE infinity and is unchanged, and only the exact types get the tag,
where it sits next to a heap pointer that already dominates it.

NumberTraits gains HasInfinity, which is exactly the is_iec559 test that
Extremum was already making through a proxy, so Extremum's behaviour is
unchanged. Its new getExtendedValue() is the total counterpart of
getOptionalValue(): an empty extremum is the infinity it stands for,
+infinity when minimizing and -infinity when maximizing.

The sentinel specializations stay in place for now; they can only be
removed once the last caller is migrated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A reward or an expected time can be infinite, and until now the only way
a result vector could say so was the literal 100000000000 that
storm::utility::infinity yields for the exact value types. Every consumer
that wanted to know had to compare against that number, and any real
value that reached it was indistinguishable from infinity.

QuantitativeCheckResult now has an extended_value_type, which is
ExtendedValueType<ValueType>: unchanged for double, and ExtendedNumber
for the exact types. The explicit results hold their vector and their map
in it, and getMin, getMax, average and sum return it, as do the symbolic
and hybrid results. The result vector is therefore self-describing -- an
infinite entry is an infinite value, not a magic number the caller has to
recognise.

Nothing that computes changes type. The solvers, the matrices and the
solver hints keep operating on plain vectors of ValueType, which is where
the performance is, and infinity never enters an equation system: the
states that have it are excluded from the maybe states by construction.

Two bridges carry the parts that have not moved yet, and both are named
so that they can be removed with the sentinel:

  * storm::utility::fromSentinel recognises the 100000000000 that the
    reward and expected time helpers still write. It is applied where a
    plain vector becomes a check result and where a decision diagram leaf
    is read, so the boundary is honest even though the inside is not.
    Probabilities are unaffected -- they live in [0,1].
  * storm::utility::toSentinel goes the other way, for the interfaces
    that are deliberately staying on the plain value type. It rejects
    -infinity, which the sentinel cannot express at all.

For the consumers that have no representation for an infinite value there
is getFiniteValueVector, which asserts that there is none, and
getSentinelValueVector for the solver hints.

Where the type earns its keep, the hand-written case analysis goes away:
SparseParameterLiftingModelChecker used to carry two ternaries to move an
infinity across double -> RationalNumber -> CoefficientType, with a
comment conceding that the conversion would otherwise fail. Both are now
a plain convertNumber, and the region bounds -- which are already kept in
an Extremum -- hold the extended type.

JSON export writes an infinite value as "inf". It used to write the
sentinel for the exact types and null for double, neither of which is a
representation of infinity.

Tests that assert an infinite result now compare against a real infinity
rather than against the sentinel, and EXPECT_NEAR knows that an infinity
is within any distance of itself and of nothing else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The sparse helpers computed a reward or an expected visiting time of infinity
and wrote it as the sentinel that storm::utility::infinity yields, leaving the
check result to reinterpret it. They now return the solution type extended with
the infinities, so the value is infinite from the point where it is computed.

Widened are the reachability reward, total reward, reachability time and
conditional queries of the DTMC, MDP, CTMC and Markov automaton helpers, the
expected visiting times helper, and the reward query of the elimination model
checker. The equation systems are untouched: a solver still sees only finite
values, and the states that are solved for are exactly the ones that have one.

MDPSparseModelCheckingHelperReturnType gains a second type parameter so that
the values it carries can be extended while the scheduler it carries is not.
setVectorValues and SparseMdpEndComponentInformation::setValues accept a source
whose element type differs from the target's, which is what widening a result
while narrowing into a solver needs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A quantile in a dimension that needs no bound at all is infinite, and the
quantile helper wrote that as the sentinel. It now hands back the extended
value type, which means the Pareto curve that carries a multi-dimensional
quantile has to hold one too, so ParetoCurveCheckResult's points are extended
while its approximating polytopes stay in the plain value type: a polytope is
a geometric object and has no infinity.

The producers that cannot yield an infinite coordinate -- the multi-objective
queries -- keep handing over plain points through a widening constructor.

Adds storm::utility::widen, narrow and isFinite. narrow is what toSentinel
should have been: it keeps the value where the plain type has an infinity of
its own and throws where it has none, rather than inventing a number for it.
equalModuloPrecision compares infinite values for equality instead of taking
a difference that is not defined for them.

QuantileQueryTest now expects a real infinity where it accepted 100000000000.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bounds that belief exploration narrows start at the two infinities and stay
infinite for an objective that is not achievable, so Result holds them in the
extended value type and diff() treats an infinite gap as unbounded rather than
subtracting values it cannot subtract.

The exploration itself still computes in the plain value type, so the sentinel
is interpreted where a bound enters the result -- the trivial bounds and the two
update functions -- and nowhere else. That bridge goes away when the belief
value bounds stop using the sentinel.

BeliefExplorationPomdpModelCheckerTest now expects a real infinity where it
accepted 100000000000, and its bound comparisons force the GMP expression to
the value type before comparing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Building the extended result vector from a plain one cost two things that
profiling showed to be larger than the widening itself: every element was
deep-copied even where the caller had handed over a temporary, and each
element was compared against a freshly constructed sentinel, which for a
value type whose infinity is a number rather than a bit pattern allocates.

fromSentinel gains a vector form that builds the sentinel once and moves the
values over, and the check result gains the rvalue constructor that reaches it.

On an exact until-probability query over 205k states this takes the extra
allocations from 668k to 267k and removes an 11% model-checking regression.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The values that are handed to the equation solver were copied out of the
extended vector one by one. They are taken over instead, which is what
narrowFinite does: the counterpart of widen for a vector known to be finite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A hint for a reward computation says of some states that they cannot reach
the target at all, so it has to be able to hold an infinite value. It went
through the sentinel instead: the instantiation checkers narrowed a result
to the plain value type, wrote 100000000000 for the infinite entries, and
the next check read that number back as an infinity.

The hint now stores the extended value type, and the instantiation checkers
hand it the result vector directly. The solvers keep working on the plain
type, so the values are narrowed where they enter one. A hint may be stale
-- the computation it came from may have found a state unable to reach the
target where this one does not -- so an infinite entry falls back to the
default starting value rather than being narrowed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The expected time until failure of a DFT that cannot fail is genuinely
infinite. For the parametric value type that came out as 100000000000: the
check result held a real infinity and DFTModelChecker narrowed it back to
the sentinel on the way out, so a parametric MTTF query printed the literal
number instead of an infinity.

The DFT result type is now the extended value type, which the analysis API
and the CLI pick up for free -- both already spell their return type as
DFTModelChecker<ValueType>::dft_results, and printing goes through the
visitor. A parametric PAND now reports inf rather than 100000000000; the
double path is unchanged, since double had a real infinity all along.

Modularisation keeps combining module results in the plain value type. It
only handles probability formulas, so nothing infinite reaches it, and
wrapping the fold would defeat the expression templates of the parametric
types for no gain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both call sites bound the result of asExplicitQuantitativeCheckResult by
value, so every monotonicity check and every assumption check deep-copied
the entire value vector only to read it. Bind by const reference instead;
the only use is the const getFiniteValueVector.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Parameter lifting knows exactly which states cannot reach the target -- it
has an infinityStates bit vector -- and was writing 100000000000 into the
result for them purely because the vector could not hold an infinity. The
quantitative values are now the extended type, so the sentinel is gone from
the region checkers and from the value bound the region records.

The gradient descent seeds its search at the worst value there is, which was
the sentinel too. That had turned an ordinary guard into a divergence
between the value types: after two steps that both land outside the region,
the tiny-change test computes oldValue - currentValue. For double this is
inf - inf, so NaN, so the comparison fails and the search continues. For the
sentinel it was 1e11 - 1e11, so zero, which counts as a tiny change and ends
the search early. The test now requires both values to be finite, which is
what double did all along.

Order-based monotonicity keeps its plain vectors: it does arithmetic on
them, and it only accepts probability formulas, so the values it is handed
are finite by construction and are narrowed on the way in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sampling and the monotonicity check both read a result through
getFiniteValueVector, which throws from inside the check result once any
value is infinite -- naming neither the property nor the state. Both
analyses sample whatever property was asked for, rewards included, so this
is reachable.

They now check finiteness first and report the property and the state.

This also changes the double path, which getFiniteValueVector never guarded:
an infinite value used to flow into the derivative checker and produce a NaN
there. It now stops with the same message, so both value types behave alike.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Exporting a check result is the one user-visible behaviour that widening
changed, and nothing covered it. The test checks a reward property with two
unreachable states, exports it, and reads the file back: an infinite state
must be the string "inf", a finite one a number equal to its value. It runs
for double and for the exact type, since double had a real infinity all
along and would not catch a regression here.

Against the old behaviour the exact case exported 100000000000.0 and the
double case exported null, since JSON has no infinity of its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It assigns an extended vector straight into a plain one, which works only
because bounded until is instantiated for double alone and double is its own
extended type. Say so at both sites, so that whoever gives RationalNumber an
exponential is not surprised.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Upstream's header split (stormchecker#1034) replaced the umbrella includes in
DFTModelChecker.h at the same time as the extended value type added
ExtendedNumber.h to that block, which is the only conflict in the merge.
Resolved in favour of the fine-grained includes, keeping ours in place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
isFinite reported a NaN as finite, because a NaN is not an infinity. Every
guard built on this predicate is there to keep a value that is not a number
out of a computation, and at the double to exact seam a NaN does not merely
give a bad answer: carl::rationalize asserts on it.

The narrowing helpers only asserted that the value they were handed was
finite. An infinite value carries a zero as its payload, so a release build
turned an infinity into a zero and carried on. These are preconditions on
data rather than invariants of the code, so they now throw.

Writing a result out only recognised the positive infinity, so a negative
one fell through to the finite payload -- exported as null for double and
as an assertion failure for the exact types. Add the generic counterpart of
isInfinity and use it here; print and writeToStream already had this right.

The narrow and widen helpers had no tests at all, which is where the
boundary lives. Cover them, along with the negative infinity arithmetic and
the conversion that carries an infinity out of a double computation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The extremal value a region check hands around can now really be infinite,
and asking for a relative guarantee multiplied the precision by it. With the
precision the command line passes by default that is 0 * inf, and with any
other precision the comparison that follows is inf - inf; both threw. There
is no relative precision around an infinite value, so compare against it
directly instead. The absolute case already coped.

Take the two callbacks by const reference while here: they are called inside
the refinement loop and each argument is a tag plus a GMP rational.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The values handed to the hint come out of getSentinelValueVector, so an
infinite one among them is the sentinel rather than an infinity. Widening
them took the sentinel at face value and produced a finite 10^11, which is
what widen is supposed to do; fromSentinel is the one that translates it.
The comment claimed the opposite of what the code did.

Nothing reads a wrong answer out of this today -- the hint is a starting
point for value iteration and the consumer that tests for an infinity is not
reached on this path -- but the two value types had silently diverged, with
double starting from zero where the exact types started from 10^11.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comparing a plain value with an extended one converted the plain operand
first, and that conversion copies it. For a GMP rational a copy is two heap
allocations, so every such comparison allocated -- in value iteration, once
per choice.

Add the comparisons that read the payload instead. They are an exact match
where the converting ones need a user defined conversion, so they win
wherever they apply, and they are not viable at all when both operands are
extended, so no existing comparison changes meaning. Measured over 3000
comparisons of a GMP rational against an extended value: 6000 allocations
before, none after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An extremum kept the value it was given plus a flag saying whether it had
been given one, except for a value type with its own infinity, which encoded
the empty case as the infinity no value improves upon. Widening that
condition from is_iec559 to NumberTraits::HasInfinity handed the second
encoding to ExtendedNumber as well, and there it does not work: for that
type an infinity is a value someone may legitimately contribute, so an
extremum holding one became indistinguishable from an empty one.

Rather than exclude ExtendedNumber, store every extremum in the type that
extends its value type with the two infinities. That type is the plain one
wherever it already has an infinity, so double keeps the exact layout it
had. The empty case then stops being a case at all: the minimum over an
empty set is +infinity and the maximum over one is -infinity, which is what
those extrema are.

getExtendedValue is total and is now the accessor to reach for. operator*
keeps handing out the plain value, so the value iteration helpers are
untouched, but its precondition is that the value is finite rather than that
the extremum is non-empty -- a value type without an infinity has nothing to
return for one. The region refinement loop reads its bounds through
getExtendedValue accordingly, which is what it needed: an infinite bound is
a bound, not a missing one.

Comparisons against the stored value go through the mixed operators, so
nothing is wrapped on the way in and the hot path allocates no more than it
did. AnnotatedRegion goes back to holding the plain coefficient type, and
the four ExtendedNumber instantiations of Extremum are no longer needed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The rational number type extended with the two infinities was spelled out at
every use, in two different ways -- as the wrapper and as the alias that
picks it -- which made the same type look like two. Name it once and use it.

The RationalFunction instantiations keep the long spelling: this is the one
name that was asked for, and adding a sibling is a separate call.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
#	src/storm-pars/derivative/GradientDescentInstantiationSearcher.cpp
The functions that only exist for as long as the sentinel does -- fromSentinel,
toSentinel, and the infinity specialisations that return the literal
100000000000 -- now say so when they are reached. Storm builds with -Werror, so
the [[deprecated]] attribute would turn every remaining use into a build failure
rather than a warning; STORM_LOG_DEPRECATED reports the same thing at runtime,
once per function, and only in a debug build.

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

@lukovdm lukovdm left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Partial review. Look at patterns in the review and apply them PR wide.

Comment thread src/storm/modelchecker/csl/helper/HybridCtmcCslHelper.cpp Outdated
Comment thread src/storm/modelchecker/csl/SparseCtmcCslModelChecker.cpp Outdated
Comment thread src/storm/modelchecker/csl/SparseCtmcCslModelChecker.cpp Outdated
Comment thread src/storm/modelchecker/results/ExplicitQuantitativeCheckResult.cpp Outdated
Comment thread src/storm/modelchecker/results/HybridQuantitativeCheckResult.cpp Outdated
Comment thread src/storm/modelchecker/results/QuantitativeCheckResult.h Outdated
Comment thread src/storm/modelchecker/results/SymbolicQuantitativeCheckResult.cpp Outdated
Comment thread src/storm/models/sparse/StandardRewardModel.cpp
lukovdm and others added 2 commits August 28, 2026 16:06
Name every alias for a value type extended with the infinities after the plain
alias it is built from -- ExtendedValueType, ExtendedSolutionType,
ExtendedCoefficientType, ExtendedConstantType, ExtendedPointType -- rather than
the four spellings that were there before, and give the check results a class
alias so that storm::utility::ExtendedValueType<ValueType> is not spelled out
inside them.

Write the type of a local instead of auto, and revert the files whose only
change was that rewrite. SparseDtmcPrctlModelChecker and SparseCtmcCslModelChecker
are back to upstream apart from the queries whose return type actually changed.

Add narrowFinite overloads that take a const reference and that fall back to a
given value, and use them where a check result or a stale solver hint was
narrowed by hand.

Let print and printRange deduce the type they are handed, which removes the
explicit template arguments at their call sites, and move
SparseMdpEndComponentInformation::setValues back into the .cpp with a single
template parameter, widening the maybe state values at the one call site that
needs it.

Drop the comments that justify the change rather than describe the code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AzQDTvuiaJqSGEVdpM4uYy
These were missed in the first pass over the review. The DFT model checker held
its results in a result_value_type; it is now called ExtendedValueType like every
other alias for a value type extended with the infinities, while
approximation_result, dft_results and property_vector keep the names they had.

The comments that justify where a value is held rather than describe the code are
dropped here too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AzQDTvuiaJqSGEVdpM4uYy
Comment thread src/storm/utility/Extremum.cpp Outdated
Comment thread src/storm/utility/Extremum.h Outdated
Comment thread src/storm/utility/Extremum.h Outdated
Comment thread src/storm/utility/Extremum.h Outdated
Comment thread src/storm/utility/Extremum.h
Comment thread src/storm/utility/Extremum.h Outdated
Comment thread src/storm/utility/Extremum.h Outdated
Comment thread src/storm/utility/vector.h
Comment thread src/storm-cli/model-handling-main-cli.h Outdated
Comment thread src/storm-pars-cli/finiteValues.h Outdated
lukovdm and others added 2 commits August 28, 2026 16:47
The extremum stores its value directly rather than in a one member struct, and
better gained the overload for an extended value, so betterThanStored and the
StorageType alias are gone. The documentation says what a thing is rather than
what it is not.

narrowFinite for a vector with a fallback value takes its values over rather than
copying them, which lets the two solver hints that narrowed state by state say so
in one statement. The scalar form had no caller left and is dropped.

getFiniteValues in storm-pars-cli only wrapped
ExplicitQuantitativeCheckResult::getFiniteValueVector in a longer message, so its
two callers use that directly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AzQDTvuiaJqSGEVdpM4uYy
The check result export test built its DTMC from a PRISM program that
pins the initial state down. Storm enumerates the initial states of such
a program with an SMT solver, so the test failed rather than skipped in
the CI configuration that is built without Z3. Make every state initial
instead, which takes the enumeration off the SMT path. That leaves what
the test checks alone, since the check result and its export cover all
states either way.

The scalar fromSentinel takes a forwarding reference, which its
constraint narrows to an rvalue. clang-tidy cannot see that and flags
the std::move as a move of a forwarding reference; forward instead,
which the constraint makes equivalent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NYYrpxGxxDTLqJo1TAYNML
A check result hands its values to print as the extended value type, but
the test that decides whether to add the decimal approximation still
recognised only the plain rational number. Every exact result therefore
lost its "(approx. ...)" suffix, which a benchmark run over the QVBS
exact set caught on all 17 of its rational instances. printRange had
already been updated; print had not.

Test both directions, since nothing covered the printed form of a check
result before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NYYrpxGxxDTLqJo1TAYNML
@lukovdm
lukovdm requested review from volkm and a balanced review from Copilot September 2, 2026 08:11

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@lukovdm
lukovdm marked this pull request as ready for review September 2, 2026 08:13
@lukovdm

lukovdm commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

@volkm I cannot request a copilot review, but I heard you still can. Would you want to do one for this PR?

@volkm

volkm commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Sure, I have triggered it. I will also do some manual review sometime this week.

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

Infinite aggregation and export paths plus deprecation logging still contain correctness or concurrency defects.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (2)

src/storm-pars-cli/solutionFunctions.cpp:70

  • The CTMC export path also unconditionally narrows an extended result. Infinite expected rewards therefore abort export with InvalidOperationException; pass the extended value through an infinity-aware exporter instead.
                std::optional<ValueType> rationalFunction =
                    storm::utility::getFinite(result->asExplicitQuantitativeCheckResult<ValueType>()[*model->getInitialStates().begin()]);

src/storm-pars-cli/solutionFunctions.cpp:94

  • For symbolic parametric results, sum() can now return an extended infinity, but this call immediately requires it to be finite. Exporting an infinite symbolic solution therefore fails; the exporter needs to accept and serialize the extended result.
                std::optional<ValueType> rationalFunction = storm::utility::getFinite(result->asSymbolicQuantitativeCheckResult<DdType, ValueType>().sum());
  • Files reviewed: 106/106 changed files
  • Comments generated: 4
  • Review effort level: Balanced

Comment thread src/storm/modelchecker/results/HybridQuantitativeCheckResult.cpp
Comment thread src/storm/modelchecker/results/SymbolicQuantitativeCheckResult.cpp
Comment thread src/storm/utility/macros.h Outdated
Comment thread src/storm-pars-cli/solutionFunctions.cpp Outdated

@tquatmann tquatmann left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Great work on this decade old issue!

I made a manual review and things look good to me besides my minor comments.

Many Thanks!

Comment thread doc/developers.md Outdated
Comment thread src/storm/utility/ExtendedNumber.h Outdated
Comment thread src/storm/utility/Extremum.h Outdated
Comment thread src/storm/utility/Extremum.h Outdated
Comment thread src/storm/utility/Extremum.cpp
Comment thread src/storm/utility/Extremum.cpp
Comment thread src/storm/modelchecker/results/SymbolicQuantitativeCheckResult.cpp
Comment on lines +24 to +31
#define STORM_LOG_DEPRECATED(message) \
do { \
static bool const storm_deprecation_reported = [&] { \
STORM_LOG_WARN("Deprecated: " << message); \
return true; \
}(); \
(void)storm_deprecation_reported; \
} while (false)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This PR also adds STORM_LOG_DEPRECATED which, in DEBUG mode, prints a deprecation warning once. In RELEASE mode, this macro does nothing.

(This looks good to me, I just found this change was a bit hidden and wanted to comment on this for visibility :)

Comment thread src/storm/utility/Extremum.cpp Outdated
@tquatmann

Copy link
Copy Markdown
Contributor

Besides that minor comment, I believe we should merge this soon.

lukovdm and others added 2 commits September 4, 2026 20:42
Two conflicts, both between a rename on this branch and an unrelated change
upstream, resolved by taking both sides:

- GradientDescentInstantiationSearcher: oldValue stays ExtendedConstantType,
  the precision now comes from env.modelTolerance().
- RegionRefinementChecker: computeExtremalValueHelper keeps its extended
  signature, PartitioningProgress gets the show-progress delay.

The getFinite and Extremum::operator* preconditions became assertions in
71b0931, so the tests that expected an exception from them are death tests
now, following the pattern in BitVectorTest.

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

lukovdm commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Then this should be done.

@lukovdm
lukovdm merged commit 3e2db41 into stormchecker:master Sep 7, 2026
23 checks passed
lukovdm added a commit to lukovdm/storm that referenced this pull request Sep 9, 2026
Resolve the conflict in SparseDtmcPrctlHelper::computeConditionalRewards:
keep this branch's up-front reachability check on the condition (which
replaces the `if (!conditionStates.empty())` guard), and take master's
ExtendedSolutionType for the result and the reachability rewards vector.

Adapt the new conditional tests to the extended-number representation
introduced in stormchecker#1040: assert against positiveInfinity rather than
infinity, which for rationals is now only a legacy placeholder value.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01218nYyRvf8TAGynDRXrtwa
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.

4 participants