LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
mdd.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_API_MDD_MDD_H
6#define LINE_API_MDD_MDD_H
7
8/**
9 * @file
10 * @ingroup api_mdd
11 * Quasi-reduced ordered Multi-valued Decision Diagram.
12 *
13 * Port of matlab/src/api/mdd/MDD.m, jline.api.mdd.MDD and
14 * python/line_solver/api/mdd/mdd.py, after A.S. Miner, G. Ciardo, "Efficient
15 * Reachability Set Generation and Storage Using Decision Diagrams", ICATPN
16 * 1999, LNCS 1639, pp.6-25.
17 *
18 * A global state is a K-tuple of LOCAL state values, one per level/submodel,
19 * state[k] in {0,...,domain[k]-1}. The set is stored as a directed acyclic
20 * graph with K variable levels plus a terminal level: level 0 is the top
21 * (root), a node at level k has domain[k] outgoing arcs to level k+1 nodes, and
22 * a state belongs to the set iff its path of arcs reaches the TRUE terminal.
23 * Canonicity is enforced by a per-level unique table (no duplicate nodes) and
24 * by collapsing the all-FALSE node to the FALSE terminal. Storage is
25 * `O(#nodes)`, typically `O(K * #local-states)`, instead of `O(|S|)`.
26 *
27 * Node ids are 1-based in every codebase so that 0 can serve as TERM_FALSE, and
28 * the unique table is keyed by the arc row itself: the encodings differ across
29 * codebases (a byte-packed string in MATLAB, a tuple key in python) but the
30 * canonical form does not.
31 *
32 * The diagram is pure combinatorics, so unlike the rest of the api layer it is
33 * NOT templated on the numeric type: no rate ever enters it.
34 */
35
36#include <algorithm>
37#include <cstddef>
38#include <map>
39#include <string>
40#include <vector>
41
42#include "line/util/error.h"
43
44namespace line {
45namespace mdd {
46
47/** Terminal node "1": a completed path is accepted. */
48const int TERM_TRUE = -1;
49/** Terminal node "0": empty subgraph. */
50const int TERM_FALSE = 0;
51
52/**
53 * Plain-array export of an MDD, the input contract of `mdd_mcd`.
54 *
55 * Mirrors MDD.toStruct in MATLAB/JAR and MDD.to_struct in python.
56 */
57struct MddStruct {
58 /** Number of variable levels. */
59 std::size_t K = 0;
60 /** domain[k] is the number of local states at level k. */
61 std::vector<int> domain;
62 /** Id of the top (level-0) node; TERM_FALSE for the empty set. */
64 /** nnodes[k] is the live node count at level k. */
65 std::vector<int> nnodes;
66 /**
67 * node[k][p][v] is the child of arc v of level-k node id p+1: a level-(k+1)
68 * node id when k < K-1, or a terminal when k == K-1.
69 */
70 std::vector<std::vector<std::vector<int>>> node;
71};
72
73/** Storage description of the set held in an MDD. */
74struct MddStats {
75 std::size_t levels = 0;
76 /** Reachable node count per level. */
77 std::vector<int> nodes_per_level;
78 /** Reachable non-terminal nodes. */
79 int num_nodes = 0;
80 /** Nodes physically held in the tables, dead ones included. */
81 int table_nodes = 0;
82 /** |S|. */
83 long long num_states = 0;
84 /** Integers in the reachable arc arrays, the diagram footprint. */
85 long long mdd_ints = 0;
86 /** Integers an explicit state list would need, |S| * K. */
87 long long explicit_ints = 0;
88
89 /** Explicit footprint divided by the diagram footprint. */
90 double compression() const {
91 const long long den = mdd_ints > 1 ? mdd_ints : 1;
92 return static_cast<double>(explicit_ints) / static_cast<double>(den);
93 }
94};
95
96/** The diagram: insert / member / index / enumerate / cardinality. */
97class MDD {
98public:
99 /** An empty set over the given per-level domains. */
100 explicit MDD(const std::vector<int>& domain)
101 : domain_(domain), K_(domain.size()), root_(TERM_FALSE), dirty_(true) {
102 for (std::size_t k = 0; k < K_; ++k) {
103 if (domain[k] <= 0) throw InputError("MDD: a level domain must be positive");
104 }
105 node_.assign(K_, std::vector<std::vector<int>>());
106 uniq_.assign(K_, std::map<std::vector<int>, int>());
107 cnt_.assign(K_, std::vector<long long>());
108 }
109
110 /** Build from a set of 0-based state tuples. */
111 static MDD from_states(const std::vector<int>& domain,
112 const std::vector<std::vector<int>>& states) {
113 MDD obj(domain);
114 for (std::size_t i = 0; i < states.size(); ++i) obj.insert(states[i]);
115 return obj;
116 }
117
118 std::size_t K() const { return K_; }
119 const std::vector<int>& domain() const { return domain_; }
120 /** Id of the root node, or TERM_FALSE for the empty set. */
121 int root() const { return root_; }
122 /** Number of live nodes at level k. */
123 int node_count(std::size_t k) const { return static_cast<int>(node_[k].size()); }
124 /** Arc row of level-k node id (1-based id). */
125 const std::vector<int>& arcs(std::size_t k, int id) const { return node_[k][id - 1]; }
126
127 /** Add a K-tuple of 0-based local values to the set. */
128 void insert(const std::vector<int>& state) {
129 if (state.size() != K_) throw InputError("MDD::insert: state has the wrong length");
130 root_ = add_state(0, root_, state);
131 dirty_ = true;
132 }
133
134 /** True iff state is in the set; O(K). */
135 bool member(const std::vector<int>& state) const {
136 int id = root_;
137 for (std::size_t k = 0; k < K_; ++k) {
138 if (id == TERM_FALSE) return false;
139 id = node_[k][id - 1][state[k]];
140 }
141 return id == TERM_TRUE;
142 }
143
144 /** |S|, the number of stored states. */
145 long long cardinality() const {
146 ensure_counts();
147 return child_count(0, root_);
148 }
149
150 /**
151 * 0-based lexicographic rank of state among the stored set, level 0 most
152 * significant, or -1 when the state is not stored.
153 *
154 * This is a bijection S -> {0,...,|S|-1}, so a generator matrix can be
155 * assembled without an explicit state list.
156 */
157 long long index(const std::vector<int>& state) const {
158 ensure_counts();
159 long long idx = 0;
160 int id = root_;
161 for (std::size_t k = 0; k < K_; ++k) {
162 if (id == TERM_FALSE) return -1;
163 const std::vector<int>& a = node_[k][id - 1];
164 const int v = state[k];
165 for (int vv = 0; vv < v; ++vv) idx += child_count(k + 1, a[vv]);
166 id = a[v];
167 }
168 return id == TERM_TRUE ? idx : -1;
169 }
170
171 /** All stored states as rows, in index() order. */
172 std::vector<std::vector<int>> enumerate() const {
173 std::vector<std::vector<int>> out;
174 if (root_ == TERM_FALSE) return out;
175 std::vector<int> prefix(K_, 0);
176 enum_below(0, root_, prefix, out);
177 return out;
178 }
179
180 /** Export the diagram as plain arrays for downstream algorithms. */
182 MddStruct s;
183 s.K = K_;
184 s.domain = domain_;
185 s.root = root_;
186 s.nnodes.assign(K_, 0);
187 s.node.assign(K_, std::vector<std::vector<int>>());
188 for (std::size_t k = 0; k < K_; ++k) {
189 s.nnodes[k] = static_cast<int>(node_[k].size());
190 s.node[k] = node_[k];
191 }
192 return s;
193 }
194
195 /**
196 * Reclaim dead nodes left by the append-only build.
197 *
198 * Membership, index and enumerate are unchanged. A production MDD would
199 * reference-count instead and never accumulate dead nodes; this is the
200 * basic sweep.
201 */
202 void compact() {
203 const std::vector<std::vector<bool>> vis = reachable_ids();
204 std::vector<std::vector<std::vector<int>>> newnode(K_);
205 std::vector<std::vector<int>> remap(K_);
206 for (std::size_t k = 0; k < K_; ++k) {
207 remap[k].assign(node_[k].size() + 1, 0);
208 for (std::size_t p = 0; p < node_[k].size(); ++p) {
209 if (vis[k][p]) {
210 newnode[k].push_back(node_[k][p]);
211 remap[k][p + 1] = static_cast<int>(newnode[k].size());
212 }
213 }
214 }
215 for (std::size_t k = 0; k + 1 < K_; ++k) {
216 for (std::size_t p = 0; p < newnode[k].size(); ++p) {
217 for (int v = 0; v < domain_[k]; ++v) {
218 if (newnode[k][p][v] > 0) newnode[k][p][v] = remap[k + 1][newnode[k][p][v]];
219 }
220 }
221 }
222 node_ = newnode;
223 if (root_ != TERM_FALSE) root_ = remap[0][root_];
224 for (std::size_t k = 0; k < K_; ++k) {
225 uniq_[k].clear();
226 for (std::size_t p = 0; p < node_[k].size(); ++p)
227 uniq_[k][node_[k][p]] = static_cast<int>(p + 1);
228 }
229 dirty_ = true;
230 }
231
232 /** Storage description of the current set; only reachable nodes are counted. */
233 MddStats stats() const {
234 const std::vector<std::vector<bool>> vis = reachable_ids();
235 MddStats s;
236 s.levels = K_;
237 s.nodes_per_level.assign(K_, 0);
238 for (std::size_t k = 0; k < K_; ++k) {
239 int c = 0;
240 for (std::size_t p = 0; p < vis[k].size(); ++p)
241 if (vis[k][p]) ++c;
242 s.nodes_per_level[k] = c;
243 s.num_nodes += c;
244 s.mdd_ints += static_cast<long long>(c) * domain_[k];
245 s.table_nodes += static_cast<int>(node_[k].size());
246 }
248 s.explicit_ints = s.num_states * static_cast<long long>(K_);
249 return s;
250 }
251
252private:
253 /** Canonical node creation through the per-level unique table. */
254 int make_node(std::size_t k, const std::vector<int>& arc_row) {
255 bool all_false = true;
256 for (std::size_t v = 0; v < arc_row.size(); ++v)
257 if (arc_row[v] != TERM_FALSE) {
258 all_false = false;
259 break;
260 }
261 if (all_false) return TERM_FALSE; // collapse the empty node
262 const std::map<std::vector<int>, int>::const_iterator it = uniq_[k].find(arc_row);
263 if (it != uniq_[k].end()) return it->second;
264 node_[k].push_back(arc_row);
265 const int id = static_cast<int>(node_[k].size());
266 uniq_[k][arc_row] = id;
267 return id;
268 }
269
270 /**
271 * Recursively add one state below node id at level k.
272 *
273 * Nodes are immutable and shared, so this rebuilds the path bottom-up
274 * rather than mutating in place.
275 */
276 int add_state(std::size_t k, int id, const std::vector<int>& state) {
277 if (k >= K_) return TERM_TRUE;
278 std::vector<int> arc_row;
279 if (id == TERM_FALSE)
280 arc_row.assign(domain_[k], TERM_FALSE);
281 else
282 arc_row = node_[k][id - 1];
283 const int v = state[k];
284 if (v < 0 || v >= domain_[k])
285 throw InputError("MDD::insert: a local value is outside its level domain");
286 arc_row[v] = add_state(k + 1, arc_row[v], state);
287 return make_node(k, arc_row);
288 }
289
290 long long child_count(std::size_t k, int child_id) const {
291 if (k >= K_) return child_id == TERM_TRUE ? 1 : 0;
292 if (child_id == TERM_FALSE) return 0;
293 return count_node(k, child_id);
294 }
295
296 long long count_node(std::size_t k, int id) const {
297 long long c = cnt_[k][id - 1];
298 if (c >= 0) return c;
299 const std::vector<int>& a = node_[k][id - 1];
300 c = 0;
301 for (int v = 0; v < domain_[k]; ++v) c += child_count(k + 1, a[v]);
302 cnt_[k][id - 1] = c;
303 return c;
304 }
305
306 void ensure_counts() const {
307 bool sized = !dirty_;
308 if (sized) {
309 for (std::size_t k = 0; k < K_; ++k)
310 if (cnt_[k].size() != node_[k].size()) {
311 sized = false;
312 break;
313 }
314 }
315 if (sized) return;
316 for (std::size_t k = 0; k < K_; ++k) cnt_[k].assign(node_[k].size(), -1);
317 dirty_ = false;
318 }
319
320 /** Per-level masks of nodes reachable from the root. */
321 std::vector<std::vector<bool>> reachable_ids() const {
322 std::vector<std::vector<bool>> vis(K_);
323 for (std::size_t k = 0; k < K_; ++k) vis[k].assign(node_[k].size(), false);
324 if (root_ == TERM_FALSE) return vis;
325 vis[0][root_ - 1] = true;
326 std::vector<std::pair<std::size_t, int>> stack;
327 stack.push_back(std::make_pair(static_cast<std::size_t>(0), root_));
328 while (!stack.empty()) {
329 const std::pair<std::size_t, int> top = stack.back();
330 stack.pop_back();
331 const std::size_t k = top.first;
332 if (k + 1 == K_) continue; // children are terminals
333 const std::vector<int>& a = node_[k][top.second - 1];
334 for (int v = 0; v < domain_[k]; ++v) {
335 const int ch = a[v];
336 if (ch > 0 && !vis[k + 1][ch - 1]) {
337 vis[k + 1][ch - 1] = true;
338 stack.push_back(std::make_pair(k + 1, ch));
339 }
340 }
341 }
342 return vis;
343 }
344
345 void enum_below(std::size_t k, int id, std::vector<int>& prefix,
346 std::vector<std::vector<int>>& out) const {
347 const std::vector<int>& a = node_[k][id - 1];
348 if (k + 1 == K_) {
349 for (int v = 0; v < domain_[k]; ++v)
350 if (a[v] == TERM_TRUE) {
351 prefix[k] = v;
352 out.push_back(prefix);
353 }
354 return;
355 }
356 for (int v = 0; v < domain_[k]; ++v) {
357 if (a[v] != TERM_FALSE) {
358 prefix[k] = v;
359 enum_below(k + 1, a[v], prefix, out);
360 }
361 }
362 }
363
364 std::vector<int> domain_;
365 std::size_t K_;
366 /** node_[k][id-1] holds the arc row of level-k node id. */
367 std::vector<std::vector<std::vector<int>>> node_;
368 /** Per-level unique table, arc row -> node id. */
369 std::vector<std::map<std::vector<int>, int>> uniq_;
370 int root_;
371 /** Memoized per-node state counts; -1 marks "not yet computed". */
372 mutable std::vector<std::vector<long long>> cnt_;
373 mutable bool dirty_;
374};
375
376/** Human-readable storage summary, the twin of MDD.toString. */
377inline std::string mdd_to_string(const MDD& m) {
378 const MddStats s = m.stats();
379 std::string out = " MDD " + std::to_string(m.K()) + " levels, " +
380 std::to_string(s.num_states) + " states in " +
381 std::to_string(s.num_nodes) + " nodes, footprint " +
382 std::to_string(s.mdd_ints) + " ints vs " +
383 std::to_string(s.explicit_ints) + " explicit";
384 if (s.table_nodes > s.num_nodes)
385 out += " (" + std::to_string(s.table_nodes - s.num_nodes) +
386 " dead nodes in tables; call compact() to reclaim)";
387 return out;
388}
389
390} // namespace mdd
391} // namespace line
392
393#endif // LINE_API_MDD_MDD_H
InputError(const std::string &what)
Definition error.h:39
The diagram: insert / member / index / enumerate / cardinality.
Definition mdd.h:97
static MDD from_states(const std::vector< int > &domain, const std::vector< std::vector< int > > &states)
Build from a set of 0-based state tuples.
Definition mdd.h:111
long long cardinality() const
|S|, the number of stored states.
Definition mdd.h:145
MddStruct to_struct() const
Export the diagram as plain arrays for downstream algorithms.
Definition mdd.h:181
void insert(const std::vector< int > &state)
Add a K-tuple of 0-based local values to the set.
Definition mdd.h:128
MDD(const std::vector< int > &domain)
An empty set over the given per-level domains.
Definition mdd.h:100
const std::vector< int > & arcs(std::size_t k, int id) const
Arc row of level-k node id (1-based id).
Definition mdd.h:125
void compact()
Reclaim dead nodes left by the append-only build.
Definition mdd.h:202
bool member(const std::vector< int > &state) const
True iff state is in the set; O(K).
Definition mdd.h:135
int node_count(std::size_t k) const
Number of live nodes at level k.
Definition mdd.h:123
std::vector< std::vector< int > > enumerate() const
All stored states as rows, in index() order.
Definition mdd.h:172
MddStats stats() const
Storage description of the current set; only reachable nodes are counted.
Definition mdd.h:233
long long index(const std::vector< int > &state) const
0-based lexicographic rank of state among the stored set, level 0 most significant,...
Definition mdd.h:157
int root() const
Id of the root node, or TERM_FALSE for the empty set.
Definition mdd.h:121
std::size_t K() const
Definition mdd.h:118
const std::vector< int > & domain() const
Definition mdd.h:119
The exception types the port throws.
const int TERM_TRUE
Terminal node "1": a completed path is accepted.
Definition mdd.h:48
std::string mdd_to_string(const MDD &m)
Human-readable storage summary, the twin of MDD.toString.
Definition mdd.h:377
const int TERM_FALSE
Terminal node "0": empty subgraph.
Definition mdd.h:50
Storage description of the set held in an MDD.
Definition mdd.h:74
int num_nodes
Reachable non-terminal nodes.
Definition mdd.h:79
long long explicit_ints
Integers an explicit state list would need, |S| * K.
Definition mdd.h:87
long long mdd_ints
Integers in the reachable arc arrays, the diagram footprint.
Definition mdd.h:85
double compression() const
Explicit footprint divided by the diagram footprint.
Definition mdd.h:90
int table_nodes
Nodes physically held in the tables, dead ones included.
Definition mdd.h:81
std::vector< int > nodes_per_level
Reachable node count per level.
Definition mdd.h:77
long long num_states
|S|.
Definition mdd.h:83
std::size_t levels
Definition mdd.h:75
Plain-array export of an MDD, the input contract of mdd_mcd.
Definition mdd.h:57
int root
Id of the top (level-0) node; TERM_FALSE for the empty set.
Definition mdd.h:63
std::vector< std::vector< std::vector< int > > > node
node[k][p][v] is the child of arc v of level-k node id p+1: a level-(k+1) node id when k < K-1,...
Definition mdd.h:70
std::vector< int > domain
domain[k] is the number of local states at level k.
Definition mdd.h:61
std::vector< int > nnodes
nnodes[k] is the live node count at level k.
Definition mdd.h:65
std::size_t K
Number of variable levels.
Definition mdd.h:59