Storm 1.11.1.1
A Modern Probabilistic Model Checker
Loading...
Searching...
No Matches
shortestPaths.cpp
Go to the documentation of this file.
2
3#include <ostream>
4#include <queue>
5#include <set>
6#include <string>
7
11#include "storm/utility/graph.h"
13
14// FIXME: I've accidentally used k=0 *twice* now without realizing that k>=1 is required!
15// (Also, did I document this? I think so, somewhere. I went with k>=1 because
16// that's what the KSP paper used, but in retrospect k>=0 seems more intuitive ...)
17
18namespace storm {
19namespace utility {
20namespace ksp {
21template<typename T>
22ShortestPathsGenerator<T>::ShortestPathsGenerator(storage::SparseMatrix<T> const& transitionMatrix, std::unordered_map<state_t, T> const& targetProbMap,
23 BitVector const& initialStates, MatrixFormat matrixFormat)
24 : transitionMatrix(transitionMatrix),
25 numStates(transitionMatrix.getColumnCount() + 1), // one more for meta-target
26 metaTarget(transitionMatrix.getColumnCount()), // first unused state index
27 initialStates(initialStates),
28 targetProbMap(targetProbMap),
29 matrixFormat(matrixFormat) {
30 computePredecessors();
31
32 // gives us SP-predecessors, SP-distances
33 performDijkstra();
34
35 computeSPSuccessors();
36
37 // constructs the recursive shortest path representations
38 initializeShortestPaths();
39
40 candidatePaths.resize(numStates);
41}
42
43template<typename T>
44ShortestPathsGenerator<T>::ShortestPathsGenerator(storage::SparseMatrix<T> const& transitionMatrix, std::vector<T> const& targetProbVector,
45 BitVector const& initialStates, MatrixFormat matrixFormat)
46 : ShortestPathsGenerator<T>(transitionMatrix, vectorToMap(targetProbVector), initialStates, matrixFormat) {}
47
48// extracts the relevant info from the model and delegates to ctor above
49template<typename T>
51 : ShortestPathsGenerator<T>(model.getTransitionMatrix(), allProbOneMap(targetBV), model.getInitialStates(), MatrixFormat::straight) {}
52
53// several alternative ways to specify the targets are provided,
54// each converts the targets and delegates to the ctor above
55// I admit it's kind of ugly, but actually pretty convenient (and I've wanted to try C++11 delegation)
56template<typename T>
58 : ShortestPathsGenerator<T>(model, std::vector<state_t>{singleTarget}) {}
59
60template<typename T>
61ShortestPathsGenerator<T>::ShortestPathsGenerator(Model const& model, std::vector<state_t> const& targetList)
62 : ShortestPathsGenerator<T>(model, BitVector(model.getNumberOfStates(), targetList)) {}
63
64template<typename T>
65ShortestPathsGenerator<T>::ShortestPathsGenerator(Model const& model, std::string const& targetLabel)
66 : ShortestPathsGenerator<T>(model, model.getStates(targetLabel)) {}
67
68template<typename T>
70 computeKSP(k);
71 return kShortestPaths[metaTarget][k - 1].distance;
72}
73
74template<typename T>
76 computeKSP(k);
77 BitVector stateSet(numStates - 1, false); // no meta-target
78
79 Path<T> currentPath = kShortestPaths[metaTarget][k - 1];
80 boost::optional<state_t> maybePredecessor = currentPath.predecessorNode;
81 // this omits the first node, which is actually convenient since that's the meta-target
82
83 while (maybePredecessor) {
84 state_t predecessor = maybePredecessor.get();
85 stateSet.set(predecessor, true);
86
87 currentPath = kShortestPaths[predecessor][currentPath.predecessorK - 1]; // god damn you, index
88 maybePredecessor = currentPath.predecessorNode;
89 }
90
91 return stateSet;
92}
93
94template<typename T>
95std::vector<state_t> ShortestPathsGenerator<T>::getPathAsList(unsigned long k) {
96 computeKSP(k);
97
98 std::vector<state_t> backToFrontList;
99
100 Path<T> currentPath = kShortestPaths[metaTarget][k - 1];
101 boost::optional<state_t> maybePredecessor = currentPath.predecessorNode;
102 // this omits the first node, which is actually convenient since that's the meta-target
103
104 while (maybePredecessor) {
105 state_t predecessor = maybePredecessor.get();
106 backToFrontList.push_back(predecessor);
107
108 currentPath = kShortestPaths[predecessor][currentPath.predecessorK - 1];
109 maybePredecessor = currentPath.predecessorNode;
110 }
111
112 return backToFrontList;
113}
114
115template<typename T>
117 assert(transitionMatrix.hasTrivialRowGrouping());
118
119 // one more for meta-target
120 graphPredecessors.resize(numStates);
121
122 for (state_t i = 0; i < numStates - 1; i++) {
123 for (auto const& transition : transitionMatrix.getRowGroup(i)) {
124 // to avoid non-minimal paths, the meta-target-predecessors are
125 // *not* predecessors of any state but the meta-target
126 if (!isMetaTargetPredecessor(i)) {
127 graphPredecessors[transition.getColumn()].push_back(i);
128 }
129 }
130 }
131
132 // meta-target has exactly the meta-target-predecessors as predecessors
133 // (duh. note that the meta-target-predecessors used to be called target,
134 // but that's not necessarily true in the matrix/value invocation case)
135 for (auto targetProbPair : targetProbMap) {
136 graphPredecessors[metaTarget].push_back(targetProbPair.first);
137 }
138}
139
140template<typename T>
141void ShortestPathsGenerator<T>::performDijkstra() {
142 // the existing Dijkstra isn't working anyway AND
143 // doesn't fully meet our requirements, so let's roll our own
144
145 T inftyDistance = zero<T>();
146 T zeroDistance = one<T>();
147 shortestPathDistances.resize(numStates, inftyDistance);
148 shortestPathPredecessors.resize(numStates, boost::optional<state_t>());
149
150 // set serves as priority queue with unique membership
151 // default comparison on pair actually works fine if distance is the first entry
152 std::set<std::pair<T, state_t>, std::greater<std::pair<T, state_t>>> dijkstraQueue;
153
154 for (state_t initialState : initialStates) {
155 shortestPathDistances[initialState] = zeroDistance;
156 dijkstraQueue.emplace(zeroDistance, initialState);
157 }
158
159 while (!dijkstraQueue.empty()) {
160 state_t currentNode = (*dijkstraQueue.begin()).second;
161 dijkstraQueue.erase(dijkstraQueue.begin());
162
163 if (!isMetaTargetPredecessor(currentNode)) {
164 // non-target node, treated normally
165 for (auto const& transition : transitionMatrix.getRowGroup(currentNode)) {
166 state_t otherNode = transition.getColumn();
167
168 // note that distances are probabilities, thus they are multiplied and larger is better
169 T alternateDistance = shortestPathDistances[currentNode] * convertDistance(currentNode, otherNode, transition.getValue());
170 assert((zero<T>() <= alternateDistance) && (alternateDistance <= one<T>()));
171 if (alternateDistance > shortestPathDistances[otherNode]) {
172 shortestPathDistances[otherNode] = alternateDistance;
173 shortestPathPredecessors[otherNode] = boost::optional<state_t>(currentNode);
174 dijkstraQueue.emplace(alternateDistance, otherNode);
175 }
176 }
177 } else {
178 // node only has one "virtual edge" (with prob as per targetProbMap) to meta-target
179 T alternateDistance = shortestPathDistances[currentNode] * targetProbMap[currentNode];
180 if (alternateDistance > shortestPathDistances[metaTarget]) {
181 shortestPathDistances[metaTarget] = alternateDistance;
182 shortestPathPredecessors[metaTarget] = boost::optional<state_t>(currentNode);
183 }
184 // no need to enqueue meta-target
185 }
186 }
187}
188
189template<typename T>
190void ShortestPathsGenerator<T>::computeSPSuccessors() {
191 shortestPathSuccessors.resize(numStates);
192
193 for (state_t i = 0; i < numStates; i++) {
194 if (shortestPathPredecessors[i]) {
195 state_t predecessor = shortestPathPredecessors[i].get();
196 shortestPathSuccessors[predecessor].push_back(i);
197 }
198 }
199}
200
201template<typename T>
202void ShortestPathsGenerator<T>::initializeShortestPaths() {
203 kShortestPaths.resize(numStates);
204
205 // BFS in Dijkstra-SP order
206 std::queue<state_t> bfsQueue;
207 for (state_t initialState : initialStates) {
208 bfsQueue.push(initialState);
209 }
210
211 while (!bfsQueue.empty()) {
212 state_t currentNode = bfsQueue.front();
213 bfsQueue.pop();
214
215 if (!kShortestPaths[currentNode].empty()) {
216 continue; // already visited
217 }
218
219 for (state_t successorNode : shortestPathSuccessors[currentNode]) {
220 bfsQueue.push(successorNode);
221 }
222
223 // note that `shortestPathPredecessor` may not be present
224 // if current node is an initial state
225 // in this case, the boost::optional copy of an uninitialized optional is hopefully also uninitialized
226 kShortestPaths[currentNode].push_back(Path<T>{shortestPathPredecessors[currentNode], 1, shortestPathDistances[currentNode]});
227 }
228}
229
230template<typename T>
231T ShortestPathsGenerator<T>::getEdgeDistance(state_t tailNode, state_t headNode) const {
232 // just to be clear, head is where the arrow points (obviously)
233 if (headNode != metaTarget) {
234 // edge is "normal", not to meta-target
235
236 for (auto const& transition : transitionMatrix.getRowGroup(tailNode)) {
237 if (transition.getColumn() == headNode) {
238 return convertDistance(tailNode, headNode, transition.getValue());
239 }
240 }
241
242 // there is no such edge
243 // let's disallow that for now, because I'm not expecting it to happen
244 STORM_LOG_THROW(false, storm::exceptions::UnexpectedException, "Should not happen.");
245 } else {
246 // edge must be "virtual edge" to meta-target
247 assert(isMetaTargetPredecessor(tailNode));
248 return targetProbMap.at(tailNode);
249 }
250}
251
252template<typename T>
253void ShortestPathsGenerator<T>::computeNextPath(state_t node, unsigned long k) {
254 assert(k >= 2); // Dijkstra is used for k=1
255 assert(kShortestPaths[node].size() == k - 1); // if not, the previous SP must not exist
256
257 // TODO: I could extract the candidate generation to make this function more succinct
258 if (k == 2) {
259 // Step B.1 in J&M paper
260
261 Path<T> shortestPathToNode = kShortestPaths[node][1 - 1]; // never forget index shift :-|
262
263 for (state_t predecessor : graphPredecessors[node]) {
264 // add shortest paths to predecessors plus edge to current node
265 Path<T> pathToPredecessorPlusEdge = {boost::optional<state_t>(predecessor), 1,
266 shortestPathDistances[predecessor] * getEdgeDistance(predecessor, node)};
267 candidatePaths[node].insert(pathToPredecessorPlusEdge);
268
269 // ... but not the actual shortest path
270 auto it = std::find(candidatePaths[node].begin(), candidatePaths[node].end(), shortestPathToNode);
271 if (it != candidatePaths[node].end()) {
272 candidatePaths[node].erase(it);
273 }
274 }
275 }
276
277 if (not(k == 2 && isInitialState(node))) {
278 // Steps B.2-5 in J&M paper
279
280 // the (k-1)th shortest path (i.e., one better than the one we want to compute)
281 Path<T> previousShortestPath = kShortestPaths[node][k - 1 - 1]; // oh god, I forgot index shift AGAIN
282
283 // the predecessor node on that path
284 state_t predecessor = previousShortestPath.predecessorNode.get();
285 // the path to that predecessor was the `tailK`-shortest
286 unsigned long tailK = previousShortestPath.predecessorK;
287
288 // i.e. source ~~tailK-shortest path~~> predecessor --> node
289
290 // compute one-worse-shortest path to the predecessor (if it hasn't yet been computed)
291 if (kShortestPaths[predecessor].size() < tailK + 1) {
292 // TODO: investigate recursion depth and possible iterative alternative
293 computeNextPath(predecessor, tailK + 1);
294 }
295
296 if (kShortestPaths[predecessor].size() >= tailK + 1) {
297 // take that path, add an edge to the current node; that's a candidate
298 Path<T> pathToPredecessorPlusEdge = {boost::optional<state_t>(predecessor), tailK + 1,
299 kShortestPaths[predecessor][tailK + 1 - 1].distance * getEdgeDistance(predecessor, node)};
300 candidatePaths[node].insert(pathToPredecessorPlusEdge);
301 }
302 // else there was no path; TODO: does this need handling? -- yes, but not here (because the step B.1 may have added candidates)
303 }
304
305 // Step B.6 in J&M paper
306 if (!candidatePaths[node].empty()) {
307 Path<T> minDistanceCandidate = *(candidatePaths[node].begin());
308 for (auto path : candidatePaths[node]) {
309 if (path.distance > minDistanceCandidate.distance) {
310 minDistanceCandidate = path;
311 }
312 }
313
314 candidatePaths[node].erase(std::find(candidatePaths[node].begin(), candidatePaths[node].end(), minDistanceCandidate));
315 kShortestPaths[node].push_back(minDistanceCandidate);
316 } else {
317 // TODO: kSP does not exist. this is handled later, but it would be nice to catch it as early as possble, wouldn't it?
318 STORM_LOG_TRACE("KSP: no candidates, this will trigger nonexisting ksp after exiting these recursions. TODO: handle here");
319 }
320}
321
322template<typename T>
323void ShortestPathsGenerator<T>::computeKSP(unsigned long k) {
324 if (k == 0) {
325 throw std::invalid_argument("Index 0 is invalid, since we use 1-based indices (sorry)!");
326 }
327
328 unsigned long alreadyComputedK = kShortestPaths[metaTarget].size();
329
330 for (unsigned long nextK = alreadyComputedK + 1; nextK <= k; nextK++) {
331 computeNextPath(metaTarget, nextK);
332 if (kShortestPaths[metaTarget].size() < nextK) {
333 unsigned long lastExistingK = nextK - 1;
334 STORM_LOG_DEBUG("KSP throws (as expected) due to nonexistence -- maybe this is unhandled and causes the Python interface to segfault?");
335 STORM_LOG_DEBUG("last existing k-SP has k=" + std::to_string(lastExistingK));
336 STORM_LOG_DEBUG("maybe this is unhandled and causes the Python interface to segfault?");
337 throw std::invalid_argument("k-SP does not exist for k=" + std::to_string(k));
338 }
339 }
340}
341
342template<typename T>
343void ShortestPathsGenerator<T>::printKShortestPath(state_t targetNode, unsigned long k, bool head) const {
344 // note the index shift! risk of off-by-one
345 Path<T> p = kShortestPaths[targetNode][k - 1];
346
347 if (head) {
348 std::cout << "Path (reversed";
349 if (targetNode == metaTarget) {
350 std::cout << ", w/ meta-target";
351 }
352 std::cout << "), dist (prob)=" << p.distance << ": [";
353 }
354
355 std::cout << " " << targetNode;
356
357 if (p.predecessorNode) {
358 printKShortestPath(p.predecessorNode.get(), p.predecessorK, false);
359 } else {
360 std::cout << " ]\n";
361 }
362}
363
364// only prints the info stored in the Path struct;
365// does not traverse the actual path (see printKShortestPath for that)
366template<typename T>
367std::ostream& operator<<(std::ostream& out, Path<T> const& p) {
368 out << "Path with predecessorNode: " << ((p.predecessorNode) ? std::to_string(p.predecessorNode.get()) : "None");
369 out << " predecessorK: " << p.predecessorK << " distance: " << p.distance;
370 return out;
371}
372template std::ostream& operator<<(std::ostream& out, Path<double> const& p);
373
374template class ShortestPathsGenerator<double>;
376} // namespace ksp
377} // namespace utility
378} // namespace storm
Base class for all sparse models.
Definition Model.h:32
A bit vector that is internally represented as a vector of 64-bit values.
Definition BitVector.h:16
void set(uint64_t index, bool value=true)
Sets the given truth value at the given index.
A class that holds a possibly non-square matrix in the compressed row storage format.
index_type getColumnCount() const
Returns the number of columns of the matrix.
bool hasTrivialRowGrouping() const
Retrieves whether the matrix has a trivial row grouping.
ShortestPathsGenerator(Model const &model, BitVector const &targetBV)
Performs precomputations (including meta-target insertion and Dijkstra).
storage::BitVector getStates(unsigned long k)
Returns the states that occur in the KSP.
T getDistance(unsigned long k)
Returns distance (i.e., probability) of the KSP.
OrderedStateList getPathAsList(unsigned long k)
Returns the states of the KSP as back-to-front traversal.
#define STORM_LOG_DEBUG(message)
Definition logging.h:18
#define STORM_LOG_TRACE(message)
Definition logging.h:12
#define STORM_LOG_THROW(cond, exception, message)
Definition macros.h:30
storage::sparse::state_type state_t
boost::optional< state_t > predecessorNode