Storm 1.14.0.1
A Modern Probabilistic Model Checker
Loading...
Searching...
No Matches
PolynomialToExpressionTest.cpp
Go to the documentation of this file.
1#include "storm-config.h"
2#include "test/storm_gtest.h"
3
4#include <cstdint>
5#include <memory>
6#include <string>
7#include <utility>
8#include <vector>
9
14
15namespace {
16
17using Coeff = storm::RationalFunctionCoefficient;
18using Variable = storm::RationalFunctionVariable;
19
20// A single term of a polynomial: a rational coefficient given as a numerator and a denominator, multiplied by a
21// product of variables, where a repeated variable stands for a power.
22struct Term {
23 std::int64_t numerator;
24 std::int64_t denominator;
25 std::vector<Variable> factors;
26};
27
28using Terms = std::vector<Term>;
29
30// Creating a rational function variable always yields a new one, even for a name that is already in use. The tests
31// need the same variable everywhere, so these are created only once.
32Variable p() {
33 static Variable const variable = storm::createRFVariable("p");
34 return variable;
35}
36
37Variable q() {
38 static Variable const variable = storm::createRFVariable("q");
39 return variable;
40}
41
42// An integer as an exact rational number. The cast keeps the constructor call unambiguous.
43storm::RationalNumber rational(std::int64_t value) {
44 return storm::RationalNumber{static_cast<long>(value)};
45}
46
47// The coefficient of the given term as carl uses it.
48Coeff coefficientOf(Term const& term) {
49 return Coeff(term.numerator) / Coeff(term.denominator);
50}
51
52// Builds the polynomial described by the given terms. This constructs exactly the polynomial type that
53// ConstraintCollector hands to polynomialToExpression, so no part of the value is lost on the way in.
54storm::RawPolynomial buildPolynomial(Terms const& terms) {
56 for (auto const& term : terms) {
57 if (term.numerator == 0) {
58 continue;
59 }
60 storm::RawPolynomial partial{coefficientOf(term)};
61 for (auto const& factor : term.factors) {
62 partial *= factor;
63 }
64 result += partial;
65 }
66 return result;
67}
68
69// Converts the given polynomial to an expression and evaluates that expression at the given values.
70storm::RationalNumber evaluateConverted(storm::RawPolynomial const& polynomial, storm::RationalNumber const& pValue, storm::RationalNumber const& qValue) {
71 auto manager = std::make_shared<storm::expressions::ExpressionManager>();
72 auto expression = storm::expressions::polynomialToExpression(polynomial, manager);
73
75 if (manager->hasVariable("p")) {
76 evaluator.setRationalValue(manager->getVariable("p"), pValue);
77 }
78 if (manager->hasVariable("q")) {
79 evaluator.setRationalValue(manager->getVariable("q"), qValue);
80 }
81
82 return evaluator.asRational(expression);
83}
84
85// Evaluates the terms described by the given list directly, without any conversion, which yields the value that the
86// conversion has to reproduce.
87storm::RationalNumber evaluateDirectly(Terms const& terms, storm::RationalNumber const& pValue, storm::RationalNumber const& qValue) {
88 storm::RationalNumber result{0};
89 for (auto const& term : terms) {
90 if (term.numerator == 0) {
91 continue;
92 }
93 storm::RationalNumber value = rational(term.numerator);
94 value /= rational(term.denominator);
95 for (auto const& factor : term.factors) {
96 value *= factor == p() ? pValue : qValue;
97 }
98 result += value;
99 }
100 return result;
101}
102
103// The values at which the converted expressions are checked. Since the arithmetic is rational, every comparison is
104// exact.
105std::vector<std::pair<storm::RationalNumber, storm::RationalNumber>> const& sampleValues() {
106 static std::vector<std::pair<storm::RationalNumber, storm::RationalNumber>> const values = {
107 {storm::RationalNumber(0), storm::RationalNumber(0)},
108 {storm::RationalNumber(1), storm::RationalNumber(1)},
109 {storm::RationalNumber(-1), storm::RationalNumber(2)},
110 {storm::RationalNumber(2), storm::RationalNumber(-3)},
111 {storm::RationalNumber(1) / storm::RationalNumber(3), storm::RationalNumber(-5) / storm::RationalNumber(7)},
112 {storm::RationalNumber(-4), storm::RationalNumber(1) / storm::RationalNumber(5)},
113 {storm::RationalNumber(7) / storm::RationalNumber(2), storm::RationalNumber(-3) / storm::RationalNumber(4)},
114 };
115 return values;
116}
117
118// Checks that converting the polynomial described by the given terms to an expression preserves its value.
119void expectValuesPreserved(Terms const& terms) {
120 auto polynomial = buildPolynomial(terms);
121
122 for (auto const& values : sampleValues()) {
123 auto expected = evaluateDirectly(terms, values.first, values.second);
124 auto actual = evaluateConverted(polynomial, values.first, values.second);
125
126 EXPECT_EQ(expected, actual) << "at p = " << values.first << " and q = " << values.second;
127 }
128}
129
130} // namespace
131
132TEST(PolynomialToExpressionTest, ConvertsPowers) {
133 // Higher powers produce exponentiation nodes, which the affine models used by the graph condition tests never
134 // produce.
135 expectValuesPreserved({{1, 1, {p(), p()}}});
136 expectValuesPreserved({{1, 1, {p(), p(), p()}}});
137 expectValuesPreserved({{1, 1, {q(), q()}}});
138 expectValuesPreserved({{1, 1, {p(), p(), q()}}});
139 expectValuesPreserved({{2, 1, {p(), p()}}, {1, 1, {}}});
140 expectValuesPreserved({{1, 1, {q(), q(), q(), q()}}});
141}
142
143TEST(PolynomialToExpressionTest, ConvertsMixedVariableProducts) {
144 // A product of distinct variables must keep both factors.
145 expectValuesPreserved({{1, 1, {p(), q()}}});
146 expectValuesPreserved({{1, 1, {p(), q()}}, {1, 1, {p()}}, {1, 1, {q()}}});
147 expectValuesPreserved({{2, 1, {p(), q()}}});
148 expectValuesPreserved({{3, 1, {p(), q()}}, {2, 1, {p()}}, {1, 1, {q()}}});
149 expectValuesPreserved({{1, 1, {p(), q()}}, {1, 1, {p(), p(), q()}}});
150}
151
152TEST(PolynomialToExpressionTest, ConvertsNonUnitRationalCoefficients) {
153 // Coefficients other than 1 and -1 are emitted as rational literals.
154 expectValuesPreserved({{1, 2, {p()}}});
155 expectValuesPreserved({{1, 3, {p()}}, {5, 8, {}}});
156 expectValuesPreserved({{1, 3, {p()}}, {5, 8, {p(), q()}}});
157 expectValuesPreserved({{7, 2, {p(), p()}}});
158 expectValuesPreserved({{-1, 2, {q()}}, {1, 4, {p()}}});
159 expectValuesPreserved({{3, 4, {p()}}, {-7, 2, {q()}}});
160 expectValuesPreserved({{1, 1, {p()}}, {1, 1, {q()}}, {-11, 6, {p(), q()}}});
161}
162
163TEST(PolynomialToExpressionTest, ConvertsZeroAndConstants) {
164 // A polynomial with no terms at all is zero, which the conversion has to handle separately.
165 EXPECT_TRUE(buildPolynomial(Terms{}).isZero());
166 expectValuesPreserved(Terms{});
167
168 // Terms that cancel out or vanish leave an empty polynomial behind as well.
169 expectValuesPreserved({{1, 1, {p(), q()}}, {-1, 1, {p(), q()}}});
170 expectValuesPreserved({{0, 1, {p()}}});
171 expectValuesPreserved({{0, 1, {p(), p()}}, {0, 1, {q()}}});
172
173 // A nonzero constant is a plain rational literal.
174 expectValuesPreserved({{1, 1, {}}});
175 expectValuesPreserved({{-1, 1, {}}});
176 expectValuesPreserved({{5, 3, {}}});
177 expectValuesPreserved({{5, 3, {}}, {1, 1, {p()}}, {-1, 2, {p(), q()}}});
178}
179
180TEST(PolynomialToExpressionTest, ConvertsNegativeCoefficients) {
181 // Negative coefficients must keep their sign. The converter emits them as a subtraction from the accumulated
182 // result, but that is not observable from the value alone, so only the sign is checked here.
183 expectValuesPreserved({{1, 1, {p()}}, {-1, 1, {}}});
184 expectValuesPreserved({{-1, 1, {p(), p()}}});
185 expectValuesPreserved({{1, 2, {p()}}, {-3, 4, {q()}}});
186 expectValuesPreserved({{-1, 1, {p(), q()}}, {1, 1, {p()}}, {1, 1, {q()}}});
187 expectValuesPreserved({{-1, 1, {p(), p(), q()}}, {1, 3, {q()}}});
188}
189
190TEST(PolynomialToExpressionTest, ConvertsTermsIndependentlyOfTheirOrder) {
191 // The converter accumulates terms, so a different order must yield the same value.
192 expectValuesPreserved({{2, 1, {p()}}, {3, 1, {q()}}, {4, 1, {p(), q()}}});
193 expectValuesPreserved({{4, 1, {p(), q()}}, {3, 1, {q()}}, {2, 1, {p()}}});
194}
195
196TEST(PolynomialToExpressionTest, DeclaresTheVariablesItNeeds) {
197 auto manager = std::make_shared<storm::expressions::ExpressionManager>();
198 auto polynomial = buildPolynomial({{1, 1, {p(), p(), q()}}, {1, 2, {p()}}});
199
200 // Every variable occurring in the polynomial is declared in the given manager, by name.
201 EXPECT_FALSE(manager->hasVariable("p"));
202 EXPECT_FALSE(manager->hasVariable("q"));
203
204 auto expression = storm::expressions::polynomialToExpression(polynomial, manager);
205
206 EXPECT_TRUE(manager->hasVariable("p"));
207 EXPECT_TRUE(manager->hasVariable("q"));
208 EXPECT_EQ(expression.getVariables().size(), 2ul);
209}
210
211TEST(PolynomialToExpressionTest, DoesNotDeclareUnusedVariables) {
212 auto manager = std::make_shared<storm::expressions::ExpressionManager>();
213 auto polynomial = buildPolynomial({{1, 1, {p()}}});
214
215 auto expression = storm::expressions::polynomialToExpression(polynomial, manager);
216
217 EXPECT_TRUE(manager->hasVariable("p"));
218 EXPECT_FALSE(manager->hasVariable("q"));
219 EXPECT_EQ(expression.getVariables().size(), 1ul);
220}
221
222TEST(PolynomialToExpressionTest, ReusesPreviouslyDeclaredVariables) {
223 // Converting into a manager that already knows the variables reuses them rather than redeclaring them.
224 auto manager = std::make_shared<storm::expressions::ExpressionManager>();
225 auto declaredP = manager->declareRationalVariable("p");
226 auto declaredQ = manager->declareRationalVariable("q");
227
228 auto polynomial = buildPolynomial({{1, 1, {p(), p(), q()}}});
229 auto expression = storm::expressions::polynomialToExpression(polynomial, manager);
230
231 // The conversion resolves the variables through the manager, so these are the ones the expression refers to.
232 EXPECT_TRUE(expression.containsVariable({declaredP, declaredQ}));
233 EXPECT_EQ(expression.getVariables(), (std::set<storm::expressions::Variable>{declaredP, declaredQ}));
234}
235
236TEST(PolynomialToExpressionTest, ConvertsAPolynomialWithoutVariablesToAConstant) {
237 auto manager = std::make_shared<storm::expressions::ExpressionManager>();
238 auto polynomial = buildPolynomial({{5, 3, {}}});
239
240 auto expression = storm::expressions::polynomialToExpression(polynomial, manager);
241
242 // Such a polynomial must not declare any variable and must convert to a constant literal.
243 EXPECT_FALSE(manager->hasVariable("p"));
244 EXPECT_FALSE(manager->hasVariable("q"));
245 EXPECT_FALSE(expression.containsVariables());
246 EXPECT_TRUE(expression.isLiteral());
247}
TEST(PolynomialToExpressionTest, ConvertsPowers)
Expression polynomialToExpression(PolynomialType const &polynomial, std::shared_ptr< ExpressionManager > const &manager)
Converts the given polynomial into a Storm expression.
SettingsManager const & manager()
Retrieves the settings manager.
RationalFunctionVariable createRFVariable(std::string const &name)
carl::Variable RationalFunctionVariable
carl::MultivariatePolynomial< RationalFunctionCoefficient > RawPolynomial