Storm 1.14.0.1
A Modern Probabilistic Model Checker
Loading...
Searching...
No Matches
RegionConstraintCheckerTest.cpp
Go to the documentation of this file.
1#include "storm-config.h"
2#include "test/storm_gtest.h"
3
4#include <memory>
5#include <string>
6
19
20namespace {
21
26
27template<typename T>
28CoefficientType coefficient(T const& value) {
30}
31
32double asDouble(CoefficientType const& value) {
34}
35
39storm::RationalFunctionVariable variableByName(std::string const& name) {
40 carl::VariablePool& pool = carl::VariablePool::getInstance();
41 auto variable = pool.findVariableWithName(name);
42 if (variable == storm::RationalFunctionVariable()) {
43 variable = pool.getFreshPersistentVariable(name);
44 }
45 return variable;
46}
47
51Region buildRegion(std::vector<std::pair<std::string, std::pair<double, double>>> const& bounds) {
52 Region::Valuation lowerBoundaries;
53 Region::Valuation upperBoundaries;
54 for (auto const& [variable, boundsOfVariable] : bounds) {
55 lowerBoundaries[variableByName(variable)] = coefficient(boundsOfVariable.first);
56 upperBoundaries[variableByName(variable)] = coefficient(boundsOfVariable.second);
57 }
58 return Region(lowerBoundaries, upperBoundaries);
59}
60
64storm::expressions::Expression variable(std::shared_ptr<storm::expressions::ExpressionManager> const& manager, std::string const& name) {
65 manager->declareRationalVariable(name);
66 return manager->getVariableExpression(name);
67}
68
69ConstraintSet buildConstraintSet(storm::expressions::Expression const& constraint) {
70 return ConstraintSet{constraint};
71}
72
73} // namespace
74
75TEST(RegionConstraintCheckerTest, AffineConstraintHoldsThroughout) {
76 auto manager = std::make_shared<storm::expressions::ExpressionManager>();
77 auto x = variable(manager, "x");
78 auto y = variable(manager, "y");
79
80 // x + y - 3 <= 0 holds on the unit square, where the maximum of x + y is 2.
81 auto result = Checker::checkConstraints(buildConstraintSet(x + y - manager->integer(3) <= manager->integer(0)),
82 buildRegion({{"x", {0.0, 1.0}}, {"y", {0.0, 1.0}}}), "test");
83
84 EXPECT_EQ(storm::pars::analysis::ConstraintVerdict::Sat, result.verdict);
85 EXPECT_FALSE(result.violatedConstraint.has_value());
86 EXPECT_TRUE(result.undecidedConstraints.empty());
87 EXPECT_FALSE(result.witness.has_value());
88 EXPECT_TRUE(result.holdsThroughout());
89}
90
91TEST(RegionConstraintCheckerTest, AffineConstraintViolatedAtCorner) {
92 auto manager = std::make_shared<storm::expressions::ExpressionManager>();
93 auto x = variable(manager, "x");
94 auto y = variable(manager, "y");
95
96 // x + y - 1 <= 0 does not hold throughout the unit square; the corner (1, 1) is a witness.
97 auto result = Checker::checkConstraints(buildConstraintSet(x + y - manager->integer(1) <= manager->integer(0)),
98 buildRegion({{"x", {0.0, 1.0}}, {"y", {0.0, 1.0}}}), "test");
99
100 EXPECT_EQ(storm::pars::analysis::ConstraintVerdict::Unsat, result.verdict);
101 EXPECT_FALSE(result.holdsThroughout());
102 EXPECT_TRUE(result.violatedConstraint.has_value());
103 ASSERT_TRUE(result.witness.has_value());
104 EXPECT_NEAR(1.0, asDouble(result.witness->at(variableByName("x"))), 1e-9);
105 EXPECT_NEAR(1.0, asDouble(result.witness->at(variableByName("y"))), 1e-9);
106}
107
108TEST(RegionConstraintCheckerTest, StrictInequalityOnBoundaryIsViolated) {
109 auto manager = std::make_shared<storm::expressions::ExpressionManager>();
110 auto x = variable(manager, "x");
111 Region region = buildRegion({{"x", {0.0, 1.0}}});
112
113 // x < 1 does not hold throughout [0, 1], because the maximum 1 is attained at the closed boundary.
114 auto violated = Checker::checkConstraints(buildConstraintSet(x < manager->integer(1)), region, "test");
115 EXPECT_EQ(storm::pars::analysis::ConstraintVerdict::Unsat, violated.verdict);
116 ASSERT_TRUE(violated.witness.has_value());
117 EXPECT_NEAR(1.0, asDouble(violated.witness->at(variableByName("x"))), 1e-9);
118
119 // In contrast, x <= 1 does hold throughout.
120 EXPECT_EQ(storm::pars::analysis::ConstraintVerdict::Sat, Checker::checkConstraints(buildConstraintSet(x <= manager->integer(1)), region, "test").verdict);
121
122 // x < 2 holds throughout.
123 EXPECT_EQ(storm::pars::analysis::ConstraintVerdict::Sat, Checker::checkConstraints(buildConstraintSet(x < manager->integer(2)), region, "test").verdict);
124}
125
126TEST(RegionConstraintCheckerTest, NegativeCoefficientIsHandled) {
127 auto manager = std::make_shared<storm::expressions::ExpressionManager>();
128 auto x = variable(manager, "x");
129 Region region = buildRegion({{"x", {0.0, 1.0}}});
130
131 // 3 - 2 * x >= 0 holds on [0, 1], as the minimum of 3 - 2x is attained at x = 1 and equals 1.
132 EXPECT_EQ(storm::pars::analysis::ConstraintVerdict::Sat,
133 Checker::checkConstraints(buildConstraintSet(manager->integer(3) - manager->integer(2) * x >= manager->integer(0)), region, "test").verdict);
134
135 // In contrast, 1 - 2 * x >= 0 does not hold on [0, 1], because the form is -1 at x = 1.
136 EXPECT_EQ(storm::pars::analysis::ConstraintVerdict::Unsat,
137 Checker::checkConstraints(buildConstraintSet(manager->integer(1) - manager->integer(2) * x >= manager->integer(0)), region, "test").verdict);
138
139 // 1 - x >= 0 does not hold on [0, 2].
140 auto violated = Checker::checkConstraints(buildConstraintSet(manager->integer(1) - x >= manager->integer(0)), buildRegion({{"x", {0.0, 2.0}}}), "test");
141 EXPECT_EQ(storm::pars::analysis::ConstraintVerdict::Unsat, violated.verdict);
142 ASSERT_TRUE(violated.witness.has_value());
143 EXPECT_NEAR(2.0, asDouble(violated.witness->at(variableByName("x"))), 1e-9);
144}
145
146TEST(RegionConstraintCheckerTest, NotEqualOverPositiveRegion) {
147 auto manager = std::make_shared<storm::expressions::ExpressionManager>();
148 auto x = variable(manager, "x");
149
150 // x != 0 holds throughout a region that does not contain zero.
151 EXPECT_EQ(storm::pars::analysis::ConstraintVerdict::Sat,
152 Checker::checkConstraints(buildConstraintSet(x != manager->integer(0)), buildRegion({{"x", {1.0, 2.0}}}), "test").verdict);
153
154 // x != 0 does not hold on a region containing zero; the witness attains exactly zero.
155 auto violated = Checker::checkConstraints(buildConstraintSet(x != manager->integer(0)), buildRegion({{"x", {-1.0, 1.0}}}), "test");
156 EXPECT_EQ(storm::pars::analysis::ConstraintVerdict::Unsat, violated.verdict);
157 ASSERT_TRUE(violated.witness.has_value());
158 EXPECT_NEAR(0.0, asDouble(violated.witness->at(variableByName("x"))), 1e-9);
159}
160
161TEST(RegionConstraintCheckerTest, EqualityRequiresIdenticallyZero) {
162 auto manager = std::make_shared<storm::expressions::ExpressionManager>();
163 auto x = variable(manager, "x");
164 Region region = buildRegion({{"x", {0.0, 1.0}}});
165
166 // x == 0 does not hold throughout, although it holds at x = 0.
167 auto violated = Checker::checkConstraints(buildConstraintSet(x == manager->integer(0)), region, "test");
168 EXPECT_EQ(storm::pars::analysis::ConstraintVerdict::Unsat, violated.verdict);
169 ASSERT_TRUE(violated.witness.has_value());
170 EXPECT_NEAR(1.0, asDouble(violated.witness->at(variableByName("x"))), 1e-9);
171
172 // The identically vanishing form holds everywhere.
173 EXPECT_EQ(storm::pars::analysis::ConstraintVerdict::Sat,
174 Checker::checkConstraints(buildConstraintSet(x - x == manager->integer(0)), region, "test").verdict);
175}
176
177TEST(RegionConstraintCheckerTest, BooleanLiteralConstraintsAreDecided) {
178 auto manager = std::make_shared<storm::expressions::ExpressionManager>();
179 Region region = buildRegion({{"x", {0.0, 1.0}}});
180
181 // A constantly false constraint is violated at every point of the region and carries a witness.
182 auto violated = Checker::checkConstraints(buildConstraintSet(manager->boolean(false)), region, "test");
183 EXPECT_EQ(storm::pars::analysis::ConstraintVerdict::Unsat, violated.verdict);
184 EXPECT_TRUE(violated.violatedConstraint.has_value());
185 ASSERT_TRUE(violated.witness.has_value());
186 EXPECT_TRUE(violated.witness->count(variableByName("x")) > 0);
187
188 // A constantly true constraint holds throughout.
189 auto satisfied = Checker::checkConstraints(buildConstraintSet(manager->boolean(true)), region, "test");
190 EXPECT_EQ(storm::pars::analysis::ConstraintVerdict::Sat, satisfied.verdict);
191 EXPECT_FALSE(satisfied.violatedConstraint.has_value());
192}
193
194TEST(RegionConstraintCheckerTest, CancellingCoefficientDoesNotRequireVariableInRegion) {
195 auto manager = std::make_shared<storm::expressions::ExpressionManager>();
196 auto x = variable(manager, "x");
197
198 // x - x <= 0 is constantly zero: x cancels, so the region need not mention x.
199 auto result = Checker::checkConstraints(buildConstraintSet(x - x <= manager->integer(0)), buildRegion({{"y", {0.0, 1.0}}}), "test");
200 EXPECT_EQ(storm::pars::analysis::ConstraintVerdict::Sat, result.verdict);
201}
202
203TEST(RegionConstraintCheckerTest, NonLinearConstraintIsUndecided) {
204 auto manager = std::make_shared<storm::expressions::ExpressionManager>();
205 auto x = variable(manager, "x");
206 auto y = variable(manager, "y");
207 Region region = buildRegion({{"x", {0.0, 1.0}}, {"y", {0.0, 1.0}}});
208
209 // Non-linear constraints are not supported, even if they are violated (here at the corner (1, 1)).
210 auto result = Checker::checkConstraints(buildConstraintSet(x * y <= manager->rational(0.25)), region, "test");
211 EXPECT_EQ(storm::pars::analysis::ConstraintVerdict::Unknown, result.verdict);
212 EXPECT_FALSE(result.undecidedConstraints.empty());
213 EXPECT_FALSE(result.holdsThroughout());
214}
215
216TEST(RegionConstraintCheckerTest, IfThenElseIsUndecided) {
217 auto manager = std::make_shared<storm::expressions::ExpressionManager>();
218 auto x = variable(manager, "x");
219 Region region = buildRegion({{"x", {0.0, 1.0}}});
220
221 auto condition = x <= manager->rational(0.5);
222 auto constraint = storm::expressions::ite(condition, x, manager->integer(1) - x) <= manager->integer(1);
223
224 // An if-then-else constraint is piecewise and therefore not affine; we never claim that it holds.
225 auto result = Checker::checkConstraints(buildConstraintSet(constraint), region, "test");
226 EXPECT_EQ(storm::pars::analysis::ConstraintVerdict::Unknown, result.verdict);
227}
228
229TEST(RegionConstraintCheckerTest, MissingVariableIsRejected) {
230 auto manager = std::make_shared<storm::expressions::ExpressionManager>();
231 auto x = variable(manager, "x");
232 auto z = variable(manager, "z");
233
234 // The constraint constrains z, but the region does not contain z, so no decision is possible.
235 EXPECT_THROW(Checker::checkConstraints(buildConstraintSet(x + z <= manager->integer(1)), buildRegion({{"x", {0.0, 1.0}}}), "test"),
236 storm::exceptions::InvalidArgumentException);
237}
238
239TEST(RegionConstraintCheckerTest, RealModelConstraintsAreDecidedExactly) {
240#ifndef STORM_HAVE_Z3
241 GTEST_SKIP() << "Z3 not available.";
242#endif
244
245 storm::prism::Program program = storm::api::parseProgram(STORM_TEST_RESOURCES_DIR "/pdtmc/parametric_die_2.pm");
246 program.checkValidity();
248 EXPECT_EQ(1ul, formulas.size());
249 storm::generator::NextStateGeneratorOptions options(*formulas.front());
250 auto model =
252
253 storm::analysis::ConstraintCollector collector(*model);
254 EXPECT_FALSE(collector.getWellformedConstraints().empty());
255 EXPECT_FALSE(collector.getGraphPreservingConstraints().empty());
256 Checker checker(collector);
257
258 // The collected well-formedness constraints are p <= 1, q <= 1, p >= 0 and q >= 0, so the closed unit
259 // square satisfies all of them. All constraints are linear, hence they are decided rather than undecided.
260 auto unitSquare = buildRegion({{"p", {0.0, 1.0}}, {"q", {0.0, 1.0}}});
261 auto wellformed = checker.checkWellformedness(unitSquare);
262 EXPECT_EQ(storm::pars::analysis::ConstraintVerdict::Sat, wellformed.verdict);
263 EXPECT_TRUE(wellformed.undecidedConstraints.empty());
264 EXPECT_TRUE(wellformed.holdsThroughout());
265
266 // The graph-preserving constraints are p != 0, p != 1, q != 0 and q != 1. Because the region is closed and
267 // therefore contains its boundary, no rectangular region containing 0 or 1 can satisfy all of them. The
268 // checker reports the violation and provides a witness, rather than pretending the model is graph
269 // preserving.
270 auto graphPreserving = checker.checkGraphPreservation(unitSquare);
271 EXPECT_EQ(storm::pars::analysis::ConstraintVerdict::Unsat, graphPreserving.verdict);
272 EXPECT_FALSE(graphPreserving.holdsThroughout());
273 EXPECT_TRUE(graphPreserving.violatedConstraint.has_value());
274 EXPECT_TRUE(graphPreserving.witness.has_value());
275
276 // On a strictly interior region, both properties hold.
277 auto interior = buildRegion({{"p", {0.1, 0.9}}, {"q", {0.1, 0.9}}});
278 EXPECT_TRUE(checker.isWellDefined(interior));
279 EXPECT_TRUE(checker.isGraphPreserving(interior));
280
281 // A region outside [0, 1] satisfies p != 0, p != 1, q != 0 and q != 1, but is not well-formed. Hence it is not graph-preserving.
282 auto outside = buildRegion({{"p", {2.0, 3.0}}, {"q", {2.0, 3.0}}});
283 EXPECT_FALSE(checker.isWellDefined(outside));
284 EXPECT_TRUE(Checker::checkConstraints(collector.getGraphPreservingConstraints(), outside, "test").holdsThroughout());
285 auto outsideResult = checker.checkGraphPreservation(outside);
286 EXPECT_EQ(storm::pars::analysis::ConstraintVerdict::Unsat, outsideResult.verdict);
287 EXPECT_FALSE(checker.isGraphPreserving(outside));
288 EXPECT_TRUE(outsideResult.violatedConstraint.has_value());
289 EXPECT_TRUE(outsideResult.witness.has_value());
290}
291
292TEST(RegionConstraintCheckerTest, NonAffineModelConstraintsAreNeverClaimedToHold) {
293#ifndef STORM_HAVE_Z3
294 GTEST_SKIP() << "Z3 not available.";
295#endif
297
298 storm::prism::Program program = storm::api::parseProgram(STORM_TEST_RESOURCES_DIR "/pdtmc/only_rational_denominator.pm");
299 program.checkValidity();
301 if (formulas.size() != 1ul) {
302 GTEST_SKIP() << "Could not extract a formula from the model.";
303 }
304 storm::generator::NextStateGeneratorOptions options(*formulas.front());
305 auto model =
307
308 storm::analysis::ConstraintCollector collector(*model);
309 Checker checker(collector);
310
311 // This model yields piecewise, non-affine constraints. The checker must not claim that they hold.
312 auto result = checker.checkWellformedness(buildRegion({{"p", {0.0, 1.0}}, {"q", {0.0, 1.0}}}));
313 EXPECT_FALSE(result.undecidedConstraints.empty());
314 EXPECT_FALSE(result.holdsThroughout());
315}
TEST(RegionConstraintCheckerTest, AffineConstraintHoldsThroughout)
Class to collect constraints on parametric Markov chains.
std::vector< storm::expressions::Expression > ConstraintSet
The type of the collected constraints.
std::shared_ptr< storm::models::sparse::Model< ValueType, RewardModelType > > build()
Convert the program given at construction time to an abstract model.
This class represents a discrete-time Markov chain.
Definition Dtmc.h:16
Checks whether the constraints collected for a parametric model hold over an entire rectangular regio...
void checkValidity(Program::ValidityCheckLevel lvl=Program::ValidityCheckLevel::READYFORPROCESSING) const
Checks the validity of the program.
Definition Program.cpp:1252
std::vector< storm::jani::Property > parsePropertiesForPrismProgram(std::string const &inputString, storm::prism::Program const &program, boost::optional< std::set< std::string > > const &propertyFilter)
storm::prism::Program parseProgram(std::string const &filename, bool prismCompatibility, bool simplify)
std::vector< std::shared_ptr< storm::logic::Formula const > > extractFormulasFromProperties(std::vector< storm::jani::Property > const &properties)
Expression ite(Expression const &condition, Expression const &thenExpression, Expression const &elseExpression)
storm::builder::BuilderOptions NextStateGeneratorOptions
storm::storage::ParameterRegion< storm::RationalFunction > RationalFunctionRegion
RationalFunctionRegion::CoefficientType CoefficientType
SettingsManager const & manager()
Retrieves the settings manager.
TargetType convertNumber(SourceType const &number)
carl::Variable RationalFunctionVariable
void clearRFVariablePool()
Clears the global pool of rational-function variables.