← Documentation
ADS-IDAC Engine 5.0.0
workspace.cpp
Go to the documentation of this file.
2#include <algorithm>
3#include <cassert>
4#include <cstring>
5#include <numeric>
6#include <stdexcept>
7
8namespace bncore {
9
10// ---------------------------------------------------------------------------
11// Static helpers — map builders
12// ---------------------------------------------------------------------------
13static std::vector<std::size_t> make_strides(const std::vector<std::size_t> &shape) {
14 std::vector<std::size_t> st(shape.size());
15 if (!shape.empty()) {
16 st.back() = 1;
17 for (int i = (int)shape.size() - 2; i >= 0; --i)
18 st[i] = st[i + 1] * shape[i + 1];
19 }
20 return st;
21}
22
23static std::vector<NodeId> combine_scopes(const std::vector<NodeId> &a,
24 const std::vector<NodeId> &b,
25 std::vector<int> &b_in_c) {
26 std::vector<NodeId> c = a;
27 b_in_c.resize(b.size(), -1);
28 for (std::size_t j = 0; j < b.size(); ++j) {
29 auto it = std::find(c.begin(), c.end(), b[j]);
30 if (it == c.end()) { b_in_c[j] = (int)c.size(); c.push_back(b[j]); }
31 else b_in_c[j] = (int)(it - c.begin());
32 }
33 return c;
34}
35
36// Build flat index map: product scope → source scope
37// Returns product_states count.
38static std::size_t build_pot_map_u32(const std::vector<NodeId> &prod_scope,
39 const std::vector<std::size_t> &prod_shape,
40 const std::vector<NodeId> &src_scope,
41 const std::vector<std::size_t> &src_shape,
42 std::vector<uint32_t> &pot_map) {
43 std::size_t prod_states = 1;
44 for (auto s : prod_shape) prod_states *= s;
45 auto prod_st = make_strides(prod_shape);
46 auto src_st = make_strides(src_shape);
47 pot_map.resize(prod_states);
48
49 for (std::size_t pi = 0; pi < prod_states; ++pi) {
50 std::size_t si = 0, rem = pi;
51 for (std::size_t d = 0; d < prod_scope.size(); ++d) {
52 std::size_t coord = rem / prod_st[d];
53 rem -= coord * prod_st[d];
54 for (std::size_t sd = 0; sd < src_scope.size(); ++sd)
55 if (src_scope[sd] == prod_scope[d]) { si += coord * src_st[sd]; break; }
56 }
57 pot_map[pi] = (uint32_t)si;
58 }
59 return prod_states;
60}
61
62// Build marginalisation map: src scope → dst scope (dst is a subset of src)
63static void build_marg_map_u32(const std::vector<NodeId> &src_scope,
64 const std::vector<std::size_t> &src_shape,
65 const std::vector<NodeId> &dst_scope,
66 const std::vector<std::size_t> &dst_shape,
67 std::vector<uint32_t> &marg_map) {
68 std::size_t src_st_tot = 1;
69 for (auto s : src_shape) src_st_tot *= s;
70 auto src_st = make_strides(src_shape);
71 auto dst_st = make_strides(dst_shape);
72 marg_map.resize(src_st_tot);
73
74 for (std::size_t si = 0; si < src_st_tot; ++si) {
75 std::size_t di = 0, rem = si;
76 for (std::size_t d = 0; d < src_scope.size(); ++d) {
77 std::size_t coord = rem / src_st[d];
78 rem -= coord * src_st[d];
79 for (std::size_t dd = 0; dd < dst_scope.size(); ++dd)
80 if (dst_scope[dd] == src_scope[d]) { di += coord * dst_st[dd]; break; }
81 }
82 marg_map[si] = (uint32_t)di;
83 }
84}
85
86// ---------------------------------------------------------------------------
87// Constructor
88// ---------------------------------------------------------------------------
89BatchWorkspace::BatchWorkspace(const JunctionTree &jt, std::size_t batch_size)
90 : jt_(jt), batch_size_(batch_size) {
91
92 std::size_t n = jt_.cliques().size();
93 root_clique_ = ~std::size_t(0);
94 parent_of_.assign(n, ~std::size_t(0));
95 children_of_.assign(n, {});
96
97 // BFS to orient the tree
98 std::vector<bool> visited(n, false);
99 std::vector<std::size_t> stack;
100 for (std::size_t start = 0; start < n; ++start) {
101 if (visited[start]) continue;
102 if (root_clique_ == ~std::size_t(0)) root_clique_ = start;
103 stack.push_back(start);
104 visited[start] = true;
105 while (!stack.empty()) {
106 std::size_t u = stack.back(); stack.pop_back();
107 distribute_order_.push_back(u);
108 for (std::size_t v : jt_.cliques()[u].neighbor_cliques)
109 if (!visited[v]) {
110 visited[v] = true;
111 parent_of_[v] = u;
112 children_of_[u].push_back(v);
113 stack.push_back(v);
114 }
115 }
116 }
118 std::reverse(collect_order_.begin(), collect_order_.end());
119
123}
124
125// ---------------------------------------------------------------------------
126// build_node_to_clique_map
127// ---------------------------------------------------------------------------
129 node_to_clique_.clear();
130 node_in_cliques_.clear();
131 for (std::size_t i = 0; i < jt_.cliques().size(); ++i)
132 for (NodeId nd : jt_.cliques()[i].scope) {
133 node_to_clique_.emplace(nd, i); // first occurrence = query clique
134 node_in_cliques_[nd].push_back(i);
135 }
136}
137
138// ---------------------------------------------------------------------------
139// build_message_schedule — pre-compute all ops + query marg maps
140// ---------------------------------------------------------------------------
142 std::size_t n = jt_.cliques().size();
143
144 // Clique scope shapes
145 std::vector<std::vector<std::size_t>> cshapes(n);
146 clique_states_.resize(n);
147 for (std::size_t i = 0; i < n; ++i) {
148 for (NodeId nd : jt_.cliques()[i].scope)
149 cshapes[i].push_back(jt_.graph()->get_variable(nd).states.size());
150 clique_states_[i] = 1;
151 for (auto s : cshapes[i]) clique_states_[i] *= s;
152 }
153
154 // Sepset sizes
155 sepset_size_.assign(n, 1);
156 auto sepset_scope_shape = [&](std::size_t u, std::size_t v)
157 -> std::pair<std::vector<NodeId>, std::vector<std::size_t>> {
158 const auto &us = jt_.cliques()[u].scope, &vs = jt_.cliques()[v].scope;
159 std::vector<NodeId> sep;
160 std::set_intersection(us.begin(), us.end(), vs.begin(), vs.end(), std::back_inserter(sep));
161 std::vector<std::size_t> sh;
162 for (NodeId nd : sep) sh.push_back(jt_.graph()->get_variable(nd).states.size());
163 return {sep, sh};
164 };
165
166 for (std::size_t u = 0; u < n; ++u) {
167 if (parent_of_[u] == ~std::size_t(0)) continue;
168 auto [sep, sh] = sepset_scope_shape(u, parent_of_[u]);
169 sepset_size_[u] = 1; for (auto s : sh) sepset_size_[u] *= s;
170 }
171
172 // Message buffers
173 up_msg_buf_.resize(n); down_msg_buf_.resize(n);
174 up_msg_size_.resize(n, 0); down_msg_size_.resize(n, 0);
175 base_up_buf_.resize(n); base_down_buf_.resize(n);
176
177 // Calibrated potential buffers
178 cal_pot_buf_.resize(n); cal_pot_size_.resize(n); base_cal_buf_.resize(n);
179
180 // P3: evidence scratch (per-clique, lazy resize)
181 ev_scratch_.resize(n);
182 ev_scratch_active_.assign(n, false);
183
184 // ── CollectOps ──────────────────────────────────────────────────────────
185 collect_ops_.resize(n);
186 for (std::size_t u : collect_order_) {
187 if (parent_of_[u] == ~std::size_t(0)) continue;
188 CollectOp &op = collect_ops_[u];
190
191 std::vector<NodeId> prod_scope = jt_.cliques()[u].scope;
192 std::vector<std::size_t> prod_shape = cshapes[u];
193
194 op.child_maps.resize(children_of_[u].size());
195 for (std::size_t ci = 0; ci < children_of_[u].size(); ++ci) {
196 auto [sep, sh] = sepset_scope_shape(children_of_[u][ci], u);
197 std::vector<int> b_in_c;
198 auto new_scope = combine_scopes(prod_scope, sep, b_in_c);
199 std::vector<std::size_t> new_shape;
200 for (auto nd : new_scope) {
201 auto it = std::find(prod_scope.begin(), prod_scope.end(), nd);
202 new_shape.push_back(it != prod_scope.end()
203 ? prod_shape[it - prod_scope.begin()]
204 : jt_.graph()->get_variable(nd).states.size());
205 }
206 prod_scope = new_scope; prod_shape = new_shape;
207 }
208
209 op.product_states = build_pot_map_u32(prod_scope, prod_shape,
210 jt_.cliques()[u].scope, cshapes[u],
211 op.pot_map);
212 for (std::size_t ci = 0; ci < children_of_[u].size(); ++ci) {
213 auto [sep, sh] = sepset_scope_shape(children_of_[u][ci], u);
214 build_pot_map_u32(prod_scope, prod_shape, sep, sh, op.child_maps[ci]);
215 }
216 auto [sep_scope, sep_shape] = sepset_scope_shape(u, parent_of_[u]);
217 build_marg_map_u32(prod_scope, prod_shape, sep_scope, sep_shape, op.marg_map);
218
220 }
221
222 // ── DistributeOps ────────────────────────────────────────────────────────
223 distribute_ops_.resize(n);
224 for (std::size_t u : distribute_order_) {
225 if (parent_of_[u] == ~std::size_t(0)) continue;
227 std::size_t p = parent_of_[u];
228 op.parent_clique = p;
229
230 std::vector<NodeId> prod_scope = jt_.cliques()[p].scope;
231 std::vector<std::size_t> prod_shape = cshapes[p];
232 op.has_parent_down = (parent_of_[p] != ~std::size_t(0));
233
234 op.sibling_cliques.clear();
235 for (std::size_t s : children_of_[p]) if (s != u) op.sibling_cliques.push_back(s);
236
237 for (std::size_t s : op.sibling_cliques) {
238 auto [sep, sh] = sepset_scope_shape(s, p);
239 std::vector<int> b_in_c;
240 auto new_scope = combine_scopes(prod_scope, sep, b_in_c);
241 std::vector<std::size_t> new_shape;
242 for (auto nd : new_scope) {
243 auto it = std::find(prod_scope.begin(), prod_scope.end(), nd);
244 new_shape.push_back(it != prod_scope.end()
245 ? prod_shape[it - prod_scope.begin()]
246 : jt_.graph()->get_variable(nd).states.size());
247 }
248 prod_scope = new_scope; prod_shape = new_shape;
249 }
250
251 op.product_states = build_pot_map_u32(prod_scope, prod_shape,
252 jt_.cliques()[p].scope, cshapes[p],
253 op.pot_map);
254 if (op.has_parent_down) {
255 auto [sep, sh] = sepset_scope_shape(p, parent_of_[p]);
256 build_pot_map_u32(prod_scope, prod_shape, sep, sh, op.down_map);
257 }
258 op.sibling_maps.resize(op.sibling_cliques.size());
259 for (std::size_t si = 0; si < op.sibling_cliques.size(); ++si) {
260 auto [sep, sh] = sepset_scope_shape(op.sibling_cliques[si], p);
261 build_pot_map_u32(prod_scope, prod_shape, sep, sh, op.sibling_maps[si]);
262 }
263 auto [sep_scope, sep_shape] = sepset_scope_shape(u, p);
264 build_marg_map_u32(prod_scope, prod_shape, sep_scope, sep_shape, op.marg_map);
265
267 }
268
269 // ── AssembleOps ──────────────────────────────────────────────────────────
270 assemble_ops_.resize(n);
271 for (std::size_t i = 0; i < n; ++i) {
272 AssembleOp &op = assemble_ops_[i];
274 op.has_down = (parent_of_[i] != ~std::size_t(0));
276
277 if (op.has_down) {
278 auto [sep, sh] = sepset_scope_shape(i, parent_of_[i]);
279 build_pot_map_u32(jt_.cliques()[i].scope, cshapes[i], sep, sh, op.down_map);
280 }
281 op.child_maps.resize(children_of_[i].size());
282 for (std::size_t ci = 0; ci < children_of_[i].size(); ++ci) {
283 auto [sep, sh] = sepset_scope_shape(children_of_[i][ci], i);
284 build_pot_map_u32(jt_.cliques()[i].scope, cshapes[i], sep, sh,
285 op.child_maps[ci]);
286 }
287 }
288
289 // ── P1: Persistent scratch buffer sized to worst-case product ────────────
290 max_product_states_ = std::max(max_product_states_, std::size_t(1));
292
293 // ── P2: Pre-baked per-clique per-variable marginalisation maps ────────────
294 clique_marg_info_.resize(n);
295 for (std::size_t ci = 0; ci < n; ++ci) {
297 const auto &scope = jt_.cliques()[ci].scope;
298 std::size_t K = scope.size();
300 mi.state_of_dim.assign(K, std::vector<uint8_t>(mi.n_states_total));
301
302 auto strides = make_strides(cshapes[ci]);
303 for (std::size_t flat = 0; flat < mi.n_states_total; ++flat) {
304 std::size_t rem = flat;
305 for (std::size_t d = 0; d < K; ++d) {
306 uint8_t coord = (uint8_t)(rem / strides[d]);
307 rem -= coord * strides[d];
308 mi.state_of_dim[d][flat] = coord;
309 }
310 }
311 }
312
313 // ── Phase-A: size scratch flag vectors (pre-sized = zero per-call alloc) ──
314 ev_clique_.assign(n, 0);
315 up_changed_.assign(n, 0);
316 down_changed_.assign(n, 0);
317 cal_changed_.assign(n, 0);
318
320}
321
322void BatchWorkspace::resize_runtime_buffers(std::size_t target_batch_size) {
323 const std::size_t effective_batch_size = std::max<std::size_t>(1, target_batch_size);
324 const std::size_t n = jt_.cliques().size();
325
326 for (std::size_t u = 0; u < n; ++u) {
327 if (parent_of_[u] == ~std::size_t(0)) {
328 up_msg_buf_[u].clear();
329 down_msg_buf_[u].clear();
330 base_up_buf_[u].clear();
331 base_down_buf_[u].clear();
332 up_msg_size_[u] = 0;
333 down_msg_size_[u] = 0;
334 continue;
335 }
336
337 const std::size_t msg_size = sepset_size_[u] * effective_batch_size;
338 up_msg_buf_[u].assign(msg_size, 0.0);
339 down_msg_buf_[u].assign(msg_size, 0.0);
340 base_up_buf_[u].assign(msg_size, 0.0);
341 base_down_buf_[u].assign(msg_size, 0.0);
342 up_msg_size_[u] = msg_size;
343 down_msg_size_[u] = msg_size;
344 }
345
346 for (std::size_t i = 0; i < n; ++i) {
347 const std::size_t pot_size = clique_states_[i] * effective_batch_size;
348 cal_pot_buf_[i].assign(pot_size, 0.0);
349 base_cal_buf_[i].assign(pot_size, 0.0);
350 cal_pot_size_[i] = pot_size;
351 }
352
353 scratch_buf_.assign(max_product_states_ * effective_batch_size, 0.0);
354}
355
356
357// ---------------------------------------------------------------------------
358// rebuild_clique_potentials
359// ---------------------------------------------------------------------------
363 for (const Factor &pot : clique_potentials_) {
364 Factor owned(pot.scope(), pot.tensor().shape());
365 std::copy(pot.tensor().data(), pot.tensor().data() + pot.tensor().size(),
366 owned.tensor().data());
367 base_clique_potentials_.push_back(std::move(owned));
368 }
369}
370
371void BatchWorkspace::rebuild_clique_potentials(std::size_t target_batch_size) {
372 if (target_batch_size == 0) {
373 throw std::invalid_argument("BatchWorkspace batch_size must be > 0.");
374 }
375
376 resize_runtime_buffers(target_batch_size);
377
378 // This is construction-time only — ok to use large temporary allocator.
379 // We use a local temporary std::vector-backed buffer so this is safe for
380 // any target_batch_size.
381 clique_potentials_.clear();
382 clique_potentials_.reserve(jt_.cliques().size());
383 bool uses_batch_offset = false;
384
385 for (const auto &clique : jt_.cliques()) {
386 const Factor &unbatched = clique.base_potential;
387 std::vector<std::size_t> new_sizes;
388 for (std::size_t i = 0; i < unbatched.scope().size(); ++i)
389 new_sizes.push_back(unbatched.tensor().shape()[i]);
390 new_sizes.push_back(target_batch_size);
391
392 Factor batched(unbatched.scope(), new_sizes); // heap-owned
393 batched.tensor().fill(1.0);
394
395 for (NodeId n_id : clique.assigned_cpts) {
396 std::vector<NodeId> family;
397 for (NodeId p : jt_.graph()->get_parents(n_id)) family.push_back(p);
398 family.push_back(n_id);
399 std::vector<std::size_t> fam_sizes;
400 for (NodeId p : family) fam_sizes.push_back(jt_.graph()->get_variable(p).states.size());
401 std::size_t fam_states = 1; for (auto s : fam_sizes) fam_states *= s;
402
403 std::vector<std::size_t> new_fam = fam_sizes;
404 new_fam.push_back(target_batch_size);
405 Factor cpt(family, new_fam);
406
407 const auto &probs = jt_.graph()->get_variable(n_id).cpt;
408 if (probs.empty()) {
409 throw std::invalid_argument("CPT for variable '" +
410 jt_.graph()->get_variable(n_id).name +
411 "' is empty.");
412 }
413 if (probs.size() % fam_states != 0) {
414 throw std::invalid_argument(
415 "CPT geometry mismatch for variable '" +
416 jt_.graph()->get_variable(n_id).name + "'. Expected scalar size " +
417 std::to_string(fam_states) + " or batched size " +
418 std::to_string(fam_states) + " * B, but got " +
419 std::to_string(probs.size()) + ".");
420 }
421 const std::size_t cpt_batch_size = probs.size() / fam_states;
422 if (cpt_batch_size == 0) {
423 throw std::invalid_argument("CPT for variable '" +
424 jt_.graph()->get_variable(n_id).name +
425 "' has invalid batch size 0.");
426 }
427 if (cpt_batch_size != 1 &&
428 !(cpt_batch_size == target_batch_size && cpt_batch_offset_ == 0) &&
429 !(cpt_batch_size > target_batch_size &&
430 cpt_batch_offset_ + target_batch_size <= cpt_batch_size)) {
431 throw std::invalid_argument(
432 "Batched CPT batch size mismatch for variable '" +
433 jt_.graph()->get_variable(n_id).name + "': CPT batch size is " +
434 std::to_string(cpt_batch_size) + ", workspace batch size is " +
435 std::to_string(target_batch_size) + ", batch offset is " +
436 std::to_string(cpt_batch_offset_) + ".");
437 }
438
439 double *d = cpt.tensor().data();
440 if (cpt_batch_size == 1) {
441 for (std::size_t s = 0; s < fam_states; ++s)
442 for (std::size_t b = 0; b < target_batch_size; ++b)
443 d[s * target_batch_size + b] = probs[s];
444 } else if (cpt_batch_size == target_batch_size) {
445 std::copy(probs.begin(), probs.end(), d);
446 } else {
447 uses_batch_offset = true;
448 for (std::size_t s = 0; s < fam_states; ++s) {
449 const double *src = probs.data() + s * cpt_batch_size + cpt_batch_offset_;
450 double *dst = d + s * target_batch_size;
451 std::copy(src, src + target_batch_size, dst);
452 }
453 }
454 batched = batched.multiply(cpt);
455 }
456 Factor owned(unbatched.scope(), new_sizes);
457 std::copy(batched.tensor().data(),
458 batched.tensor().data() + batched.tensor().size(),
459 owned.tensor().data());
460 clique_potentials_.push_back(std::move(owned));
461 }
462
464 base_batch_size_ = target_batch_size;
466 base_depends_on_offset_ = uses_batch_offset;
469 calibrate();
470}
471
472// ---------------------------------------------------------------------------
473// reset
474// ---------------------------------------------------------------------------
475void BatchWorkspace::reset(std::size_t new_batch_size,
476 std::size_t cpt_batch_offset) {
477 if (new_batch_size == 0) {
478 throw std::invalid_argument("BatchWorkspace batch_size must be > 0.");
479 }
480
481 batch_size_ = new_batch_size;
482 cpt_batch_offset_ = cpt_batch_offset;
484
485 // Restore evidence scratch tracking
486 std::fill(ev_scratch_active_.begin(), ev_scratch_active_.end(), false);
487
490 clique_potentials_.clear();
492 for (const Factor &bp : base_clique_potentials_)
493 clique_potentials_.push_back(
494 Factor(bp.scope(), bp.tensor().shape(),
495 const_cast<double*>(bp.tensor().data())));
496 return;
497 }
499}
500
501// ---------------------------------------------------------------------------
502// Hard evidence (unchanged API)
503// ---------------------------------------------------------------------------
504void BatchWorkspace::set_evidence_matrix(const int *ev, std::size_t num_vars) {
505 evidence_matrix_ = ev; evidence_num_vars_ = num_vars;
506}
509}
510
511// ---------------------------------------------------------------------------
512// Soft / virtual evidence
513// ---------------------------------------------------------------------------
514void BatchWorkspace::set_soft_evidence(NodeId var, const double *likelihoods,
515 std::size_t n_states) {
516 // Validate variable exists in graph.
517 const std::size_t card = jt_.graph()->get_variable(var).states.size();
518 if (n_states != card)
519 throw std::invalid_argument(
520 "set_soft_evidence: n_states (" + std::to_string(n_states) +
521 ") does not match variable cardinality (" + std::to_string(card) + ").");
522 for (std::size_t s = 0; s < n_states; ++s)
523 if (likelihoods[s] < 0.0)
524 throw std::invalid_argument(
525 "set_soft_evidence: likelihood[" + std::to_string(s) +
526 "] is negative (" + std::to_string(likelihoods[s]) + ").");
527 soft_evidence_scalar_[var].assign(likelihoods, likelihoods + n_states);
528 // Remove any batched soft evidence for this variable — scalar takes priority.
529 soft_evidence_matrix_.erase(var);
530}
531
533 const double *likelihoods_matrix,
534 std::size_t n_states) {
535 const std::size_t card = jt_.graph()->get_variable(var).states.size();
536 if (n_states != card)
537 throw std::invalid_argument(
538 "set_soft_evidence_matrix: n_states (" + std::to_string(n_states) +
539 ") does not match variable cardinality (" + std::to_string(card) + ").");
540 const std::size_t B = batch_size_;
541 for (std::size_t i = 0; i < B * n_states; ++i)
542 if (likelihoods_matrix[i] < 0.0)
543 throw std::invalid_argument(
544 "set_soft_evidence_matrix: negative likelihood at flat index " +
545 std::to_string(i) + ".");
546 soft_evidence_matrix_[var].assign(likelihoods_matrix,
547 likelihoods_matrix + B * n_states);
548 // Remove scalar soft evidence for this variable — matrix takes priority.
549 soft_evidence_scalar_.erase(var);
550}
551
553 soft_evidence_scalar_.clear();
554 soft_evidence_matrix_.clear();
555}
556
557// ---------------------------------------------------------------------------
558// apply_soft_evidence_to_clique
559//
560// Applies soft evidence (or hard evidence converted to lambda vectors) to the
561// potential of clique ci. Returns true if any likelihood was applied.
562//
563// Design: both hard and soft evidence are handled by multiplying the potential
564// entry at flat index f and batch row b by lambda[b][state_of_dim[d][f]].
565// This is the unified path — hard evidence is a lambda=[0..1..0] special case.
566//
567// Key invariants maintained:
568// - ev_scratch_[ci] is used as a working copy (lazily sized).
569// - clique_potentials_[ci] is replaced with a Factor wrapping the scratch.
570// - ev_clique_[ci] is set to 1 when any change is made.
571// - If all entries sum to 0 (B==1), throws "Inconsistent evidence".
572// ---------------------------------------------------------------------------
574 const Factor &pot = clique_potentials_[ci];
575 const auto &scope = pot.scope();
576 const auto &shape = pot.tensor().shape();
577 const std::size_t K = scope.size();
578 const std::size_t B = batch_size_;
579
580 // Collect which dimensions (variable positions in the clique scope) have
581 // either hard or soft evidence.
582 struct EvidenceDim {
583 std::size_t pos; // dimension in clique scope
584 NodeId nid;
585 bool is_scalar; // true → same likelihood for all batch rows
586 };
587 std::vector<EvidenceDim> ev_dims;
588 ev_dims.reserve(K);
589
590 for (std::size_t d = 0; d < K; ++d) {
591 NodeId nid = scope[d];
592 bool has_hard = (evidence_matrix_ && nid < evidence_num_vars_);
593 bool has_scalar_soft = (soft_evidence_scalar_.count(nid) > 0);
594 bool has_matrix_soft = (soft_evidence_matrix_.count(nid) > 0);
595
596 if (!has_hard && !has_scalar_soft && !has_matrix_soft) continue;
597
598 // If we have both hard evidence and soft evidence for the same variable,
599 // soft evidence takes precedence (it subsumes hard as a special case).
600 if (has_scalar_soft || has_matrix_soft) {
601 ev_dims.push_back({d, nid, has_scalar_soft});
602 } else {
603 // Hard evidence: check at least one batch row has a non-negative obs.
604 bool any_obs = false;
605 for (std::size_t b = 0; b < B && !any_obs; ++b)
606 if (evidence_matrix_[b * evidence_num_vars_ + nid] >= 0) any_obs = true;
607 if (any_obs) ev_dims.push_back({d, nid, /*is_scalar=*/true});
608 }
609 }
610 if (ev_dims.empty()) return false;
611
612 // Copy potential into scratch buffer.
613 const std::size_t total = clique_states_[ci];
614 std::vector<double> &scratch = ev_scratch_[ci];
615 scratch.resize(total * B);
616 const double *src = pot.tensor().data();
617 std::copy(src, src + total * B, scratch.data());
618
619 const CliqueMargInfo &mi = clique_marg_info_[ci];
620
621 // Apply each evidence dimension.
622 for (const EvidenceDim &ed : ev_dims) {
623 const std::size_t d = ed.pos;
624 const NodeId nid = ed.nid;
625 const std::size_t card = shape[d];
626
627 bool has_scalar_soft = soft_evidence_scalar_.count(nid) > 0;
628 bool has_matrix_soft = soft_evidence_matrix_.count(nid) > 0;
629
630 for (std::size_t flat = 0; flat < total; ++flat) {
631 const std::size_t state = mi.state_of_dim[d][flat];
632 double *dst = scratch.data() + flat * B;
633
634 if (has_scalar_soft) {
635 // Same lambda for all batch rows.
636 const double *lam = soft_evidence_scalar_.at(nid).data();
637 const double factor = lam[state];
638 if (factor == 1.0) continue; // hot-path skip for hard evidence matching state
639#pragma omp simd
640 for (std::size_t b = 0; b < B; ++b) dst[b] *= factor;
641
642 } else if (has_matrix_soft) {
643 // Per-row lambda. lam[b * card + state]
644 const double *lam = soft_evidence_matrix_.at(nid).data();
645 for (std::size_t b = 0; b < B; ++b) {
646 dst[b] *= lam[b * card + state];
647 }
648
649 } else {
650 // Hard evidence: convert to lambda on the fly.
651 for (std::size_t b = 0; b < B; ++b) {
652 const int obs = evidence_matrix_[b * evidence_num_vars_ + nid];
653 if (obs < 0) continue; // unobserved in this row
654 if (static_cast<std::size_t>(obs) >= card)
655 throw std::invalid_argument(
656 "Hard evidence state index out of range for variable '" +
657 jt_.graph()->get_variable(nid).name + "'.");
658 if (static_cast<std::size_t>(obs) != state) dst[b] = 0.0;
659 }
660 }
661 }
662 }
663
664 // Inconsistency check for the B==1 case — throw immediately.
665 if (B == 1) {
666 double sum = 0.0;
667 for (std::size_t f = 0; f < total; ++f) sum += scratch[f];
668 if (sum == 0.0)
669 throw std::invalid_argument("Inconsistent evidence: zero-sum potential in clique.");
670 }
671
672 clique_potentials_[ci] = Factor(scope, shape, scratch.data());
673 ev_scratch_active_[ci] = true;
674 ev_clique_[ci] = 1;
675 return true;
676}
677
678
679// ---------------------------------------------------------------------------
680// calibrate — ZERO heap allocation in the hot path
681// ---------------------------------------------------------------------------
683 const std::size_t n = jt_.cliques().size();
684 const std::size_t B = batch_size_;
685
686 // Phase-A: clear workspace-owned flag arrays — memset, zero heap allocation.
687 std::memset(ev_clique_.data(), 0, n);
688 std::memset(up_changed_.data(), 0, n);
689 std::memset(down_changed_.data(), 0, n);
690 std::memset(cal_changed_.data(), 0, n);
691
692 // ── Evidence application ─────────────────────────────────────────────────
693 // Unified path: handles hard evidence, soft evidence, and combinations.
694 // apply_soft_evidence_to_clique() sets ev_clique_[ci] and updates
695 // clique_potentials_[ci] in place. We call it for every clique (it returns
696 // immediately if no evidence touches that clique).
698 for (std::size_t ci = 0; ci < n; ++ci) {
699 if (ev_clique_[ci]) continue; // already processed (guard for multi-pass)
701 }
702 }
703
704
705 // ── Collect: leaves → root ───────────────────────────────────────────────
706 double *scratch = scratch_buf_.data();
707 if (B == 1) {
708 for (std::size_t u : collect_order_) {
709 if (parent_of_[u] == ~std::size_t(0)) continue;
710 bool needs = ev_clique_[u];
711 for (std::size_t c : children_of_[u]) if (up_changed_[c]) { needs = true; break; }
712 if (!needs && !is_first_calibration_) {
713 std::copy(base_up_buf_[u].begin(), base_up_buf_[u].end(), up_msg_buf_[u].begin());
714 continue;
715 }
716 const CollectOp &op = collect_ops_[u];
717 const double *pot = clique_potentials_[u].tensor().data();
718 const std::size_t PS = op.product_states;
719 for (std::size_t pi = 0; pi < PS; ++pi) scratch[pi] = pot[op.pot_map[pi]];
720 for (std::size_t ci = 0; ci < op.child_cliques.size(); ++ci) {
721 const double *cm = up_msg_buf_[op.child_cliques[ci]].data();
722 const uint32_t *cmap = op.child_maps[ci].data();
723 for (std::size_t pi = 0; pi < PS; ++pi) scratch[pi] *= cm[cmap[pi]];
724 }
725 double *umsg = up_msg_buf_[u].data();
726 const std::size_t US = up_msg_size_[u];
727 std::fill(umsg, umsg + US, 0.0);
728 const uint32_t *mm = op.marg_map.data();
729 for (std::size_t pi = 0; pi < PS; ++pi) umsg[mm[pi]] += scratch[pi];
730 up_changed_[u] = 1;
731 }
732
733 // ── Distribute: root → leaves ──────────────────────────────────────────
734 for (std::size_t u : distribute_order_) {
735 if (parent_of_[u] == ~std::size_t(0)) {
736 bool root_needs = ev_clique_[u];
737 for (std::size_t c : children_of_[u]) if (up_changed_[c]) { root_needs = true; break; }
738 cal_changed_[u] = root_needs ? 1 : 0;
739 continue;
740 }
741 std::size_t p = parent_of_[u];
742 bool needs = ev_clique_[p] || down_changed_[p];
743 for (std::size_t s : children_of_[p]) if (s != u && up_changed_[s]) { needs = true; break; }
744 if (!needs && !is_first_calibration_) {
745 std::copy(base_down_buf_[u].begin(), base_down_buf_[u].end(), down_msg_buf_[u].begin());
746 } else {
747 const DistributeOp &op = distribute_ops_[u];
748 const double *ppot = clique_potentials_[p].tensor().data();
749 const std::size_t PS = op.product_states;
750 for (std::size_t pi = 0; pi < PS; ++pi) scratch[pi] = ppot[op.pot_map[pi]];
751 if (op.has_parent_down) {
752 const double *pm = down_msg_buf_[p].data();
753 const uint32_t *dm = op.down_map.data();
754 for (std::size_t pi = 0; pi < PS; ++pi) scratch[pi] *= pm[dm[pi]];
755 }
756 for (std::size_t si = 0; si < op.sibling_cliques.size(); ++si) {
757 const double *sm = up_msg_buf_[op.sibling_cliques[si]].data();
758 const uint32_t *smap = op.sibling_maps[si].data();
759 for (std::size_t pi = 0; pi < PS; ++pi) scratch[pi] *= sm[smap[pi]];
760 }
761 double *dmsg = down_msg_buf_[u].data();
762 const std::size_t DS = down_msg_size_[u];
763 std::fill(dmsg, dmsg + DS, 0.0);
764 const uint32_t *mm = op.marg_map.data();
765 for (std::size_t pi = 0; pi < PS; ++pi) dmsg[mm[pi]] += scratch[pi];
766 down_changed_[u] = 1;
767 }
768 cal_changed_[u] = (ev_clique_[u] || down_changed_[u]) ? 1 : 0;
769 for (std::size_t c : children_of_[u]) if (up_changed_[c]) { cal_changed_[u] = 1; break; }
770 }
771
772 // ── Assemble calibrated potentials ─────────────────────────────────────
773 for (std::size_t i = 0; i < n; ++i) {
775 std::copy(base_cal_buf_[i].begin(), base_cal_buf_[i].end(), cal_pot_buf_[i].begin());
776 continue;
777 }
778 const AssembleOp &op = assemble_ops_[i];
779 const double *pot = clique_potentials_[i].tensor().data();
780 double *out = cal_pot_buf_[i].data();
781 const std::size_t NS = op.product_states;
782 std::copy(pot, pot + NS, out);
783 if (op.has_down) {
784 const double *dm = down_msg_buf_[i].data();
785 const uint32_t *dmap = op.down_map.data();
786 for (std::size_t pi = 0; pi < NS; ++pi) out[pi] *= dm[dmap[pi]];
787 }
788 for (std::size_t ci = 0; ci < op.child_cliques.size(); ++ci) {
789 const double *cm = up_msg_buf_[op.child_cliques[ci]].data();
790 const uint32_t *cmap = op.child_maps[ci].data();
791 for (std::size_t pi = 0; pi < NS; ++pi) out[pi] *= cm[cmap[pi]];
792 }
793 }
794 } else {
795 // ── Collect: leaves → root (batched) ───────────────────────────────────
796 for (std::size_t u : collect_order_) {
797 if (parent_of_[u] == ~std::size_t(0)) continue;
798 bool needs = ev_clique_[u];
799 for (std::size_t c : children_of_[u]) if (up_changed_[c]) { needs = true; break; }
800 if (!needs && !is_first_calibration_) {
801 std::copy(base_up_buf_[u].begin(), base_up_buf_[u].end(), up_msg_buf_[u].begin());
802 continue;
803 }
804
805 const CollectOp &op = collect_ops_[u];
806 const double *pot = clique_potentials_[u].tensor().data();
807 const std::size_t PS = op.product_states;
808
809 for (std::size_t pi = 0; pi < PS; ++pi) {
810 double *dst = scratch + pi * B;
811 const double *src = pot + static_cast<std::size_t>(op.pot_map[pi]) * B;
812#pragma omp simd
813 for (std::size_t b = 0; b < B; ++b) {
814 dst[b] = src[b];
815 }
816 }
817
818 for (std::size_t ci = 0; ci < op.child_cliques.size(); ++ci) {
819 const double *cm = up_msg_buf_[op.child_cliques[ci]].data();
820 const uint32_t *cmap = op.child_maps[ci].data();
821 for (std::size_t pi = 0; pi < PS; ++pi) {
822 double *dst = scratch + pi * B;
823 const double *src = cm + static_cast<std::size_t>(cmap[pi]) * B;
824#pragma omp simd
825 for (std::size_t b = 0; b < B; ++b) {
826 dst[b] *= src[b];
827 }
828 }
829 }
830
831 double *umsg = up_msg_buf_[u].data();
832 std::fill(umsg, umsg + up_msg_size_[u], 0.0);
833 const uint32_t *mm = op.marg_map.data();
834 for (std::size_t pi = 0; pi < PS; ++pi) {
835 double *dst = umsg + static_cast<std::size_t>(mm[pi]) * B;
836 const double *src = scratch + pi * B;
837#pragma omp simd
838 for (std::size_t b = 0; b < B; ++b) {
839 dst[b] += src[b];
840 }
841 }
842 up_changed_[u] = 1;
843 }
844
845 // ── Distribute: root → leaves (batched) ────────────────────────────────
846 for (std::size_t u : distribute_order_) {
847 if (parent_of_[u] == ~std::size_t(0)) {
848 bool root_needs = ev_clique_[u];
849 for (std::size_t c : children_of_[u]) if (up_changed_[c]) { root_needs = true; break; }
850 cal_changed_[u] = root_needs ? 1 : 0;
851 continue;
852 }
853
854 std::size_t p = parent_of_[u];
855 bool needs = ev_clique_[p] || down_changed_[p];
856 for (std::size_t s : children_of_[p]) if (s != u && up_changed_[s]) { needs = true; break; }
857 if (!needs && !is_first_calibration_) {
858 std::copy(base_down_buf_[u].begin(), base_down_buf_[u].end(), down_msg_buf_[u].begin());
859 } else {
860 const DistributeOp &op = distribute_ops_[u];
861 const double *ppot = clique_potentials_[p].tensor().data();
862 const std::size_t PS = op.product_states;
863
864 for (std::size_t pi = 0; pi < PS; ++pi) {
865 double *dst = scratch + pi * B;
866 const double *src =
867 ppot + static_cast<std::size_t>(op.pot_map[pi]) * B;
868#pragma omp simd
869 for (std::size_t b = 0; b < B; ++b) {
870 dst[b] = src[b];
871 }
872 }
873
874 if (op.has_parent_down) {
875 const double *pm = down_msg_buf_[p].data();
876 const uint32_t *dm = op.down_map.data();
877 for (std::size_t pi = 0; pi < PS; ++pi) {
878 double *dst = scratch + pi * B;
879 const double *src = pm + static_cast<std::size_t>(dm[pi]) * B;
880#pragma omp simd
881 for (std::size_t b = 0; b < B; ++b) {
882 dst[b] *= src[b];
883 }
884 }
885 }
886
887 for (std::size_t si = 0; si < op.sibling_cliques.size(); ++si) {
888 const double *sm = up_msg_buf_[op.sibling_cliques[si]].data();
889 const uint32_t *smap = op.sibling_maps[si].data();
890 for (std::size_t pi = 0; pi < PS; ++pi) {
891 double *dst = scratch + pi * B;
892 const double *src = sm + static_cast<std::size_t>(smap[pi]) * B;
893#pragma omp simd
894 for (std::size_t b = 0; b < B; ++b) {
895 dst[b] *= src[b];
896 }
897 }
898 }
899
900 double *dmsg = down_msg_buf_[u].data();
901 std::fill(dmsg, dmsg + down_msg_size_[u], 0.0);
902 const uint32_t *mm = op.marg_map.data();
903 for (std::size_t pi = 0; pi < PS; ++pi) {
904 double *dst = dmsg + static_cast<std::size_t>(mm[pi]) * B;
905 const double *src = scratch + pi * B;
906#pragma omp simd
907 for (std::size_t b = 0; b < B; ++b) {
908 dst[b] += src[b];
909 }
910 }
911 down_changed_[u] = 1;
912 }
913
914 cal_changed_[u] = (ev_clique_[u] || down_changed_[u]) ? 1 : 0;
915 for (std::size_t c : children_of_[u]) if (up_changed_[c]) { cal_changed_[u] = 1; break; }
916 }
917
918 // ── Assemble calibrated potentials (batched) ───────────────────────────
919 for (std::size_t i = 0; i < n; ++i) {
921 std::copy(base_cal_buf_[i].begin(), base_cal_buf_[i].end(), cal_pot_buf_[i].begin());
922 continue;
923 }
924
925 const AssembleOp &op = assemble_ops_[i];
926 const std::size_t NS = op.product_states;
927 const double *pot = clique_potentials_[i].tensor().data();
928 double *out = cal_pot_buf_[i].data();
929 std::copy(pot, pot + (NS * B), out);
930
931 if (op.has_down) {
932 const double *dm = down_msg_buf_[i].data();
933 const uint32_t *dmap = op.down_map.data();
934 for (std::size_t pi = 0; pi < NS; ++pi) {
935 double *dst = out + pi * B;
936 const double *src = dm + static_cast<std::size_t>(dmap[pi]) * B;
937#pragma omp simd
938 for (std::size_t b = 0; b < B; ++b) {
939 dst[b] *= src[b];
940 }
941 }
942 }
943
944 for (std::size_t ci = 0; ci < op.child_cliques.size(); ++ci) {
945 const double *cm = up_msg_buf_[op.child_cliques[ci]].data();
946 const uint32_t *cmap = op.child_maps[ci].data();
947 for (std::size_t pi = 0; pi < NS; ++pi) {
948 double *dst = out + pi * B;
949 const double *src = cm + static_cast<std::size_t>(cmap[pi]) * B;
950#pragma omp simd
951 for (std::size_t b = 0; b < B; ++b) {
952 dst[b] *= src[b];
953 }
954 }
955 }
956 }
957 }
958
959 // ── Global inconsistency check ────────────────────────────────────────────
960 if (evidence_matrix_ && B == 1 && root_clique_ != ~std::size_t(0)) {
961 double total = 0.0;
962 for (double v : cal_pot_buf_[root_clique_]) total += v;
963 if (total <= 0.0) throw std::invalid_argument("Inconsistent evidence");
964 }
965
966 // ── Base snapshot on first calibration ───────────────────────────────────
968 for (std::size_t u = 0; u < n; ++u) {
969 if (parent_of_[u] == ~std::size_t(0)) continue;
972 }
973 for (std::size_t i = 0; i < n; ++i) base_cal_buf_[i] = cal_pot_buf_[i];
974 is_first_calibration_ = false;
975 }
976}
977
978
979// ---------------------------------------------------------------------------
980// query_marginals_multi — per-query extraction using pre-baked state_of_dim.
981//
982// batch_size == 1 (HCL point mode): queries are grouped by clique so each
983// unique clique is scanned exactly once for all of its query variables.
984// Zero heap allocation; uses uint8 state_of_dim and workspace-owned flags.
985// batch_size > 1: direct extraction from calibrated batched beliefs.
986// ---------------------------------------------------------------------------
988 std::size_t num_queries,
989 const std::size_t *offsets,
990 double *out) const {
991 if (num_queries == 0) {
992 return;
993 }
994
995 struct QEntry {
996 std::size_t qi;
997 std::size_t node_dim;
998 std::size_t n_states;
999 };
1000
1001 constexpr std::size_t kStackQueryLimit = 64;
1002 constexpr std::size_t kStackCliqueLimit = 200;
1003
1004 std::size_t clique_of_q_stack[kStackQueryLimit];
1005 QEntry entries_stack[kStackQueryLimit];
1006 uint8_t visited_stack[kStackCliqueLimit];
1007
1008 std::vector<std::size_t> clique_of_q_heap;
1009 std::vector<QEntry> entries_heap;
1010
1011 std::size_t *clique_of_q = nullptr;
1012 QEntry *entries = nullptr;
1013 uint8_t *visited = nullptr;
1014
1015 if (num_queries <= kStackQueryLimit) {
1016 clique_of_q = clique_of_q_stack;
1017 entries = entries_stack;
1018 } else {
1019 clique_of_q_heap.resize(num_queries);
1020 entries_heap.resize(num_queries);
1021 clique_of_q = clique_of_q_heap.data();
1022 entries = entries_heap.data();
1023 }
1024
1025 const std::size_t n_cliques = jt_.cliques().size();
1026 if (n_cliques <= kStackCliqueLimit) {
1027 std::memset(visited_stack, 0, n_cliques);
1028 visited = visited_stack;
1029 } else {
1030 if (query_visited_scratch_.size() < n_cliques) {
1031 query_visited_scratch_.resize(n_cliques);
1032 }
1033 std::memset(query_visited_scratch_.data(), 0, n_cliques);
1034 visited = query_visited_scratch_.data();
1035 }
1036
1037 const std::size_t B = batch_size_;
1038 const std::size_t total_states = offsets[num_queries];
1039
1040 // First pass: resolve cliques and node dimensions + zero-fill output slices.
1041 for (std::size_t qi = 0; qi < num_queries; ++qi) {
1042 NodeId nid = query_vars[qi];
1043 auto it = node_to_clique_.find(nid);
1044 if (it == node_to_clique_.end())
1045 throw std::invalid_argument("query node not in JT");
1046 std::size_t ci = it->second;
1047 clique_of_q[qi] = ci;
1048 const auto &scope = jt_.cliques()[ci].scope;
1049 std::size_t node_dim = 0;
1050 for (std::size_t d = 0; d < scope.size(); ++d) {
1051 if (scope[d] == nid) {
1052 node_dim = d;
1053 break;
1054 }
1055 }
1056
1057 entries[qi] = {qi, node_dim, jt_.graph()->get_variable(nid).states.size()};
1058
1059 if (B == 1) {
1060 double *qout = out + offsets[qi];
1061 std::fill(qout, qout + entries[qi].n_states, 0.0);
1062 } else {
1063 for (std::size_t b = 0; b < B; ++b) {
1064 double *qout = out + b * total_states + offsets[qi];
1065 std::fill(qout, qout + entries[qi].n_states, 0.0);
1066 }
1067 }
1068 }
1069
1070 // Second pass: process each clique exactly once.
1071 for (std::size_t qi = 0; qi < num_queries; ++qi) {
1072 std::size_t ci = clique_of_q[qi];
1073 if (visited[ci]) continue;
1074 visited[ci] = 1;
1075
1076 const CliqueMargInfo &mi = clique_marg_info_[ci];
1077 const double *cal = cal_pot_buf_[ci].data();
1078 const std::size_t total = mi.n_states_total;
1079
1080 if (B == 1) {
1081 for (std::size_t flat = 0; flat < total; ++flat) {
1082 const double v = cal[flat];
1083 for (std::size_t qj = qi; qj < num_queries; ++qj) {
1084 if (clique_of_q[qj] != ci) continue;
1085 const QEntry &e = entries[qj];
1086 out[offsets[e.qi] + mi.state_of_dim[e.node_dim][flat]] += v;
1087 }
1088 }
1089
1090 for (std::size_t qj = qi; qj < num_queries; ++qj) {
1091 if (clique_of_q[qj] != ci) continue;
1092 const QEntry &e = entries[qj];
1093 double *qout = out + offsets[e.qi];
1094 double sum = 0.0;
1095 for (std::size_t s = 0; s < e.n_states; ++s) sum += qout[s];
1096 if (sum > 0.0) {
1097 for (std::size_t s = 0; s < e.n_states; ++s) qout[s] /= sum;
1098 }
1099 }
1100 } else {
1101 for (std::size_t flat = 0; flat < total; ++flat) {
1102 const double *cal_row = cal + flat * B;
1103 for (std::size_t qj = qi; qj < num_queries; ++qj) {
1104 if (clique_of_q[qj] != ci) continue;
1105 const QEntry &e = entries[qj];
1106 const std::size_t state = mi.state_of_dim[e.node_dim][flat];
1107 for (std::size_t b = 0; b < B; ++b) {
1108 out[b * total_states + offsets[e.qi] + state] += cal_row[b];
1109 }
1110 }
1111 }
1112
1113 for (std::size_t qj = qi; qj < num_queries; ++qj) {
1114 if (clique_of_q[qj] != ci) continue;
1115 const QEntry &e = entries[qj];
1116 for (std::size_t b = 0; b < B; ++b) {
1117 double *qout = out + b * total_states + offsets[e.qi];
1118 double sum = 0.0;
1119 for (std::size_t s = 0; s < e.n_states; ++s) sum += qout[s];
1120 if (sum > 0.0) {
1121 for (std::size_t s = 0; s < e.n_states; ++s) qout[s] /= sum;
1122 } else {
1123 std::fill(qout, qout + e.n_states, 0.0);
1124 }
1125 }
1126 }
1127 }
1128 }
1129}
1130
1131
1132// ---------------------------------------------------------------------------
1133// query_marginal — backward-compat wrapper (calls query_marginals_multi)
1134// ---------------------------------------------------------------------------
1136 std::size_t n_states = jt_.graph()->get_variable(node).states.size();
1137 std::vector<std::size_t> out_shape = {batch_size_, n_states};
1138 DenseTensor result(out_shape);
1139 result.fill(0.0);
1140
1141 std::size_t offsets[2] = {0, n_states};
1142 query_marginals_multi(&node, 1, offsets, result.data());
1143 return result;
1144}
1145
1146
1147// ---------------------------------------------------------------------------
1148// max_calibrate — max-product belief propagation for MAP / MPE inference
1149//
1150// Semantics: replaces the sum-marginalization in the collect pass with max.
1151// After max_calibrate(), call query_map() to decode the joint MAP assignment.
1152//
1153// Evidence is applied exactly as in calibrate() — the same
1154// apply_soft_evidence_to_clique() helper is used, followed by a max-product
1155// collect + assemble pass.
1156//
1157// Result is stored in map_cal_pot_buf_, which is separate from cal_pot_buf_
1158// so that sum-product and max-product results can coexist.
1159// ---------------------------------------------------------------------------
1161 const std::size_t n = jt_.cliques().size();
1162 if (batch_size_ != 1) {
1163 throw std::invalid_argument(
1164 "BatchWorkspace::max_calibrate currently supports batch_size == 1. "
1165 "Use BatchExecutionEngine::evaluate_map for batched MAP.");
1166 }
1167
1168 // Allocate max-product buffers on first use (same layout as sum-product).
1169 if (map_up_msg_buf_.size() != n) {
1170 map_up_msg_buf_.resize(n);
1171 map_cal_pot_buf_.resize(n);
1172 map_traceback_.resize(n);
1173 for (std::size_t u = 0; u < n; ++u) {
1174 if (parent_of_[u] == ~std::size_t(0)) continue;
1175 map_up_msg_buf_[u].assign(sepset_size_[u], 0.0);
1176 map_traceback_[u].assign(collect_ops_[u].product_states, 0);
1177 }
1178 for (std::size_t i = 0; i < n; ++i)
1179 map_cal_pot_buf_[i].assign(clique_states_[i], 0.0);
1180 }
1181
1182 // Restore base clique potentials (start from prior).
1183 if (!base_clique_potentials_.empty()) {
1184 clique_potentials_.clear();
1185 for (const Factor &bp : base_clique_potentials_)
1186 clique_potentials_.push_back(
1187 Factor(bp.scope(), bp.tensor().shape(),
1188 const_cast<double *>(bp.tensor().data())));
1189 }
1190 std::fill(ev_scratch_active_.begin(), ev_scratch_active_.end(), false);
1191 std::memset(ev_clique_.data(), 0, n);
1192
1193 // Apply evidence.
1194 if (evidence_matrix_ || !soft_evidence_scalar_.empty() ||
1195 !soft_evidence_matrix_.empty()) {
1196 for (std::size_t ci = 0; ci < n; ++ci)
1198 }
1199
1200 double *scratch = scratch_buf_.data();
1201
1202 // ── Max-product collect pass (B=1 only — MAP is always per-row) ─────────
1203 for (std::size_t u : collect_order_) {
1204 if (parent_of_[u] == ~std::size_t(0)) continue;
1205 const CollectOp &op = collect_ops_[u];
1206 const double *pot = clique_potentials_[u].tensor().data();
1207 const std::size_t PS = op.product_states;
1208
1209 // Gather clique potential into scratch.
1210 for (std::size_t pi = 0; pi < PS; ++pi)
1211 scratch[pi] = pot[op.pot_map[pi]];
1212
1213 // Multiply incoming child up-messages.
1214 for (std::size_t ci = 0; ci < op.child_cliques.size(); ++ci) {
1215 const double *cm = map_up_msg_buf_[op.child_cliques[ci]].data();
1216 const uint32_t *cmap = op.child_maps[ci].data();
1217 for (std::size_t pi = 0; pi < PS; ++pi) scratch[pi] *= cm[cmap[pi]];
1218 }
1219
1220 // Max-marginalize into up-message (replace scatter-add with scatter-max).
1221 double *umsg = map_up_msg_buf_[u].data();
1222 const std::size_t US = sepset_size_[u];
1223 std::fill(umsg, umsg + US, 0.0);
1224 const uint32_t *mm = op.marg_map.data();
1225 for (std::size_t pi = 0; pi < PS; ++pi) {
1226 if (scratch[pi] > umsg[mm[pi]]) umsg[mm[pi]] = scratch[pi];
1227 }
1228 }
1229
1230 // ── Assemble max-product calibrated beliefs at root ──────────────────────
1231 // For MAP we only need the root clique to extract the joint argmax.
1232 // For MPE over all variables, we assemble every clique.
1233 for (std::size_t i = 0; i < n; ++i) {
1234 const AssembleOp &op = assemble_ops_[i];
1235 const double *pot = clique_potentials_[i].tensor().data();
1236 double *out = map_cal_pot_buf_[i].data();
1237 const std::size_t NS = op.product_states;
1238 std::copy(pot, pot + NS, out);
1239 // Multiply incoming child up-messages from the max-product pass.
1240 for (std::size_t ci = 0; ci < op.child_cliques.size(); ++ci) {
1241 const double *cm = map_up_msg_buf_[op.child_cliques[ci]].data();
1242 const uint32_t *cmap = op.child_maps[ci].data();
1243 for (std::size_t pi = 0; pi < NS; ++pi) out[pi] *= cm[cmap[pi]];
1244 }
1245 // Note: in max-product, the root assembler ignores down-messages (no parent).
1246 // Non-root cliques do not receive down-messages in this simplified implementation;
1247 // the root clique's calibrated beliefs are used for argmax.
1248 }
1249}
1250
1251// ---------------------------------------------------------------------------
1252// query_map — decode MAP assignment from max-product calibrated beliefs
1253//
1254// out_states[v] = argmax state for variable v, or -1 if v is not in the JT.
1255// Caller must allocate out_states[n_vars].
1256// ---------------------------------------------------------------------------
1257void BatchWorkspace::query_map(int *out_states, std::size_t n_vars) const {
1258 // Initialise all states to -1 (not in JT / not processed).
1259 std::fill(out_states, out_states + n_vars, -1);
1260
1261 const std::size_t n = jt_.cliques().size();
1262
1263 // For each variable, find the clique that contains it and extract its
1264 // marginal argmax from the max-product calibrated beliefs.
1265 for (const auto &[nid, ci] : node_to_clique_) {
1266 if (nid >= static_cast<NodeId>(n_vars)) continue;
1267
1268 const CliqueMargInfo &mi = clique_marg_info_[ci];
1269 const double *cal = map_cal_pot_buf_[ci].data();
1270 const std::size_t tot = mi.n_states_total;
1271
1272 // Find the dimension index of this variable in the clique scope.
1273 const auto &scope = jt_.cliques()[ci].scope;
1274 std::size_t node_dim = 0;
1275 for (std::size_t d = 0; d < scope.size(); ++d)
1276 if (scope[d] == nid) { node_dim = d; break; }
1277
1278 const std::size_t n_states = jt_.graph()->get_variable(nid).states.size();
1279
1280 // Accumulate the max-product beliefs for each state (marginal max).
1281 double best_val = -1.0;
1282 int best_s = 0;
1283 for (std::size_t s = 0; s < n_states; ++s) {
1284 double v = 0.0;
1285 for (std::size_t flat = 0; flat < tot; ++flat)
1286 if (mi.state_of_dim[node_dim][flat] == static_cast<uint8_t>(s))
1287 v = std::max(v, cal[flat]);
1288 if (v > best_val) { best_val = v; best_s = static_cast<int>(s); }
1289 }
1290 out_states[nid] = best_s;
1291 }
1292}
1293
1294} // namespace bncore
document e
std::vector< std::size_t > down_msg_size_
Definition: workspace.hpp:339
std::size_t batch_size_
Definition: workspace.hpp:311
std::unordered_map< NodeId, std::vector< double > > soft_evidence_matrix_
Definition: workspace.hpp:327
void build_message_schedule()
Fills collect_ops_, distribute_ops_, assemble_ops_ and clique_marg_info_.
Definition: workspace.cpp:141
std::vector< std::size_t > collect_order_
Definition: workspace.hpp:330
std::vector< std::size_t > sepset_size_
Definition: workspace.hpp:340
std::vector< std::vector< uint32_t > > map_traceback_
Definition: workspace.hpp:370
std::vector< std::size_t > cal_pot_size_
Definition: workspace.hpp:363
std::vector< uint8_t > cal_changed_
Definition: workspace.hpp:359
std::unordered_map< NodeId, std::vector< double > > soft_evidence_scalar_
Definition: workspace.hpp:326
const int * evidence_matrix_
Definition: workspace.hpp:319
void clear_soft_evidence()
Removes all soft evidence, scalar and matrix.
Definition: workspace.cpp:552
std::size_t root_clique_
Definition: workspace.hpp:329
std::vector< std::vector< double > > map_cal_pot_buf_
Definition: workspace.hpp:372
void set_soft_evidence_matrix(NodeId var, const double *likelihoods_matrix, std::size_t n_states)
Sets a separate likelihood vector per batch row for a variable.
Definition: workspace.cpp:532
DenseTensor query_marginal(NodeId node) const
Marginal of one variable from the last calibrate().
Definition: workspace.cpp:1135
void calibrate()
Runs sum-product message passing with the current evidence.
Definition: workspace.cpp:682
std::unordered_map< NodeId, std::size_t > node_to_clique_
Definition: workspace.hpp:390
std::vector< std::vector< double > > base_up_buf_
Definition: workspace.hpp:375
std::vector< std::vector< double > > ev_scratch_
Definition: workspace.hpp:385
std::vector< uint8_t > query_visited_scratch_
Definition: workspace.hpp:394
std::vector< Factor > clique_potentials_
Definition: workspace.hpp:314
bool apply_soft_evidence_to_clique(std::size_t ci)
Definition: workspace.cpp:573
void clear_evidence()
Forgets the hard evidence matrix. Soft evidence is not affected.
Definition: workspace.cpp:507
std::vector< std::vector< double > > map_up_msg_buf_
Definition: workspace.hpp:367
std::vector< std::vector< double > > up_msg_buf_
Definition: workspace.hpp:336
std::size_t base_batch_size_
Definition: workspace.hpp:316
std::vector< bool > ev_scratch_active_
Definition: workspace.hpp:387
std::vector< std::vector< double > > base_cal_buf_
Definition: workspace.hpp:377
const JunctionTree & jt_
Definition: workspace.hpp:310
std::vector< std::vector< double > > cal_pot_buf_
Definition: workspace.hpp:362
std::vector< CollectOp > collect_ops_
Definition: workspace.hpp:343
std::unordered_map< NodeId, std::vector< std::size_t > > node_in_cliques_
Definition: workspace.hpp:391
BatchWorkspace(const JunctionTree &jt, std::size_t batch_size=1)
Builds the workspace and calibrates it without evidence.
Definition: workspace.cpp:89
std::vector< std::vector< double > > base_down_buf_
Definition: workspace.hpp:376
void resize_runtime_buffers(std::size_t target_batch_size)
Sizes message, belief and scratch buffers for a batch size.
Definition: workspace.cpp:322
std::vector< std::size_t > distribute_order_
Definition: workspace.hpp:331
std::size_t evidence_num_vars_
Definition: workspace.hpp:320
std::vector< AssembleOp > assemble_ops_
Definition: workspace.hpp:345
std::vector< std::vector< std::size_t > > children_of_
Definition: workspace.hpp:333
void max_calibrate()
Runs max-product propagation with the current evidence, for query_map().
Definition: workspace.cpp:1160
void set_soft_evidence(NodeId var, const double *likelihoods, std::size_t n_states)
Sets one likelihood vector for a variable, shared by all batch rows.
Definition: workspace.cpp:514
std::vector< std::size_t > clique_states_
Definition: workspace.hpp:341
std::vector< DistributeOp > distribute_ops_
Definition: workspace.hpp:344
void snapshot_base_potentials()
Copies clique_potentials_ into owning base_clique_potentials_.
Definition: workspace.cpp:360
std::vector< uint8_t > up_changed_
Definition: workspace.hpp:357
std::size_t base_batch_offset_
Definition: workspace.hpp:317
void set_evidence_matrix(const int *evidence_matrix, std::size_t num_vars)
Sets the hard evidence for the next calibration.
Definition: workspace.cpp:504
std::vector< double > scratch_buf_
Definition: workspace.hpp:350
void query_marginals_multi(const NodeId *query_vars, std::size_t num_queries, const std::size_t *offsets, double *out) const
Marginals of several variables from the last calibrate(), with one scan of each clique involved.
Definition: workspace.cpp:987
std::vector< std::vector< double > > down_msg_buf_
Definition: workspace.hpp:337
std::vector< CliqueMargInfo > clique_marg_info_
Definition: workspace.hpp:381
std::size_t max_product_states_
Definition: workspace.hpp:351
std::vector< std::size_t > up_msg_size_
Definition: workspace.hpp:338
std::vector< std::size_t > parent_of_
Definition: workspace.hpp:332
std::vector< uint8_t > ev_clique_
Definition: workspace.hpp:356
void reset(std::size_t new_batch_size, std::size_t cpt_batch_offset=0)
Prepares the workspace for a new evidence case.
Definition: workspace.cpp:475
std::size_t cpt_batch_offset_
Definition: workspace.hpp:312
std::vector< Factor > base_clique_potentials_
Definition: workspace.hpp:315
std::vector< uint8_t > down_changed_
Definition: workspace.hpp:358
void rebuild_clique_potentials(std::size_t target_batch_size)
Definition: workspace.cpp:371
void query_map(int *out_states, std::size_t n_vars) const
Decodes a state for each variable from the max-product beliefs.
Definition: workspace.cpp:1257
Dense row-major array of doubles with a fixed shape.
double * data()
Pointer to the first element: the borrowed buffer when one is bound, otherwise the owned storage.
const std::vector< std::size_t > & shape() const
Extent of each dimension, as given at construction.
void fill(double value)
Sets every element to value.
std::size_t size() const
Total number of elements, the product of the shape.
A potential over an ordered scope of variables.
Definition: factor.hpp:22
Factor multiply(const Factor &other, class BumpAllocator *allocator=nullptr) const
Product of two factors.
Definition: factor.cpp:27
const std::vector< NodeId > & scope() const
Variables of the factor, in tensor dimension order.
Definition: factor.hpp:45
DenseTensor & tensor()
The value table.
Definition: factor.hpp:83
const std::vector< NodeId > & get_parents(NodeId id) const
Parents of a variable, in the order their edges were added.
Definition: graph.cpp:105
const VariableMetadata & get_variable(NodeId id) const
Metadata of a variable.
Definition: graph.cpp:92
Cliques and separators compiled from one Graph.
const std::vector< Clique > & cliques() const
All cliques, indexed by Clique::id.
const Graph * graph() const
The graph given at construction.
int size
Total number of MPI processes.
static std::vector< NodeId > combine_scopes(const std::vector< NodeId > &a, const std::vector< NodeId > &b, std::vector< int > &b_in_c)
Definition: workspace.cpp:23
static void build_marg_map_u32(const std::vector< NodeId > &src_scope, const std::vector< std::size_t > &src_shape, const std::vector< NodeId > &dst_scope, const std::vector< std::size_t > &dst_shape, std::vector< uint32_t > &marg_map)
Definition: workspace.cpp:63
static std::size_t build_pot_map_u32(const std::vector< NodeId > &prod_scope, const std::vector< std::size_t > &prod_shape, const std::vector< NodeId > &src_scope, const std::vector< std::size_t > &src_shape, std::vector< uint32_t > &pot_map)
Definition: workspace.cpp:38
static std::vector< std::size_t > make_strides(const std::vector< std::size_t > &shape)
Definition: workspace.cpp:13
std::uint32_t NodeId
Handle of a variable within one Graph.
Definition: node.hpp:20
Index tables that assemble the calibrated belief of one clique from its potential,...
Definition: workspace.hpp:62
std::vector< uint32_t > down_map
[clique_idx] to index in the clique's down message; empty unless has_down
Definition: workspace.hpp:65
std::size_t product_states
number of states of the clique
Definition: workspace.hpp:63
std::vector< std::vector< uint32_t > > child_maps
[child_i][clique_idx] to index in that child's up message
Definition: workspace.hpp:66
std::vector< std::size_t > child_cliques
clique indices of the children, parallel to child_maps
Definition: workspace.hpp:67
bool has_down
true when the clique has a parent
Definition: workspace.hpp:64
Per-clique lookup from a flat clique index to the state of each scope variable.
Definition: workspace.hpp:79
std::vector< std::vector< uint8_t > > state_of_dim
Definition: workspace.hpp:84
std::size_t n_states_total
number of joint states of the clique
Definition: workspace.hpp:85
Index tables for the upward (collect) message of one non-root clique u to its parent.
Definition: workspace.hpp:32
std::vector< std::vector< uint32_t > > child_maps
[child_i][prod_idx] to index in that child's up message
Definition: workspace.hpp:35
std::vector< uint32_t > pot_map
[prod_idx] to flat index in clique u's potential
Definition: workspace.hpp:34
std::size_t product_states
number of states of the product space
Definition: workspace.hpp:33
std::vector< uint32_t > marg_map
[prod_idx] to index in u's up message (its separator states)
Definition: workspace.hpp:37
std::vector< std::size_t > child_cliques
clique indices of the children, parallel to child_maps
Definition: workspace.hpp:36
Index tables for the downward (distribute) message from a parent clique p to one child clique u.
Definition: workspace.hpp:47
std::size_t product_states
number of states of the product space
Definition: workspace.hpp:48
std::vector< uint32_t > marg_map
[prod_idx] to index in u's down message
Definition: workspace.hpp:54
std::size_t parent_clique
clique index of p
Definition: workspace.hpp:55
bool has_parent_down
true when p has a parent of its own
Definition: workspace.hpp:50
std::vector< std::size_t > sibling_cliques
other children of p, parallel to sibling_maps
Definition: workspace.hpp:53
std::vector< uint32_t > down_map
[prod_idx] to index in p's down message; empty unless has_parent_down
Definition: workspace.hpp:51
std::vector< std::vector< uint32_t > > sibling_maps
[sibling_i][prod_idx] to index in that sibling's up message
Definition: workspace.hpp:52
std::vector< uint32_t > pot_map
[prod_idx] to flat index in parent clique p's potential
Definition: workspace.hpp:49
std::vector< double > cpt
Definition: node.hpp:31
std::string name
Definition: node.hpp:25
std::vector< std::string > states
Definition: node.hpp:28
bncore::BatchWorkspace, the message passing state of junction tree inference, and the precomputed ind...