aGrUM 3.2.0
a C++ library for (probabilistic) graphical models
KTBNAdaptiveLearner_tpl.h
Go to the documentation of this file.
1/****************************************************************************
2 * This file is part of the aGrUM/pyAgrum library. *
3 * *
4 * Copyright (c) 2005-2026 by *
5 * - Pierre-Henri WUILLEMIN(_at_LIP6) *
6 * - Christophe GONZALES(_at_AMU) *
7 * *
8 * The aGrUM/pyAgrum library is free software; you can redistribute it *
9 * and/or modify it under the terms of either : *
10 * *
11 * - the GNU Lesser General Public License as published by *
12 * the Free Software Foundation, either version 3 of the License, *
13 * or (at your option) any later version, *
14 * - the MIT license (MIT), *
15 * - or both in dual license, as here. *
16 * *
17 * (see https://agrum.gitlab.io/articles/dual-licenses-lgplv3mit.html) *
18 * *
19 * This aGrUM/pyAgrum library is distributed in the hope that it will be *
20 * useful, but WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, *
21 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES MERCHANTABILITY or FITNESS *
22 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE *
23 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER *
24 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, *
25 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR *
26 * OTHER DEALINGS IN THE SOFTWARE. *
27 * *
28 * See LICENCES for more details. *
29 * *
30 * SPDX-FileCopyrightText: Copyright 2005-2026 *
31 * - Pierre-Henri WUILLEMIN(_at_LIP6) *
32 * - Christophe GONZALES(_at_AMU) *
33 * SPDX-License-Identifier: LGPL-3.0-or-later OR MIT *
34 * *
35 * Contact : info_at_agrum_dot_org *
36 * homepage : http://agrum.gitlab.io *
37 * gitlab : https://gitlab.com/agrumery/agrum *
38 * *
39 ****************************************************************************/
40
41
42#pragma once
43
44
50
51#include <cmath>
52#include <filesystem>
53#include <format>
54#include <fstream>
55#include <memory>
56
68
69#include <unordered_map>
70
71namespace gum::learning {
72
73 // =========================================================================
74 // Constructors / Destructors
75 // =========================================================================
76
77 template < GUM_Numeric GUM_SCALAR >
79 std::string_view dirPath,
80 std::string_view csvBaseName,
81 Size nbSamples,
82 Size kMax,
83 const std::unordered_set< std::string >& atemporalVars,
84 const std::vector< std::string >& missingSymbols,
85 bool induceTypes) :
86 _dirPath_(dirPath), _csvBaseName_(csvBaseName), _nbSamples_(nbSamples), _kMax_(kMax),
87 _atemporalVars_(atemporalVars), _missingSymbols_(missingSymbols), _induceTypes_(induceTypes) {
88 // cheap argument checks first, so bad calls fail before touching the disk
90 if (nbSamples < 1) GUM_ERROR(InvalidArgument, "KTBNAdaptiveLearner requires nbSamples >= 1")
91
92 // Load the base-variable names (the CSV columns) so the constraint setters can
93 // reject unknown names eagerly. Only the header row is needed, so we parse a
94 // single line instead of loading and typing the whole file. The column order
95 // matches what every per-k KTBNLearner reads from the same header at
96 // learnKTBN() time.
97 namespace fs = std::filesystem;
98 const fs::path firstCSV = fs::path{dirPath} / (std::string{csvBaseName} + "1.csv");
99 std::ifstream is(firstCSV, std::ifstream::in);
100 if (!is.is_open()) GUM_ERROR(IOError, "Cannot open " << firstCSV.string())
101
102 CSVParser parser(is, firstCSV.string());
103 if (!parser.next())
104 GUM_ERROR(IOError, "Empty trajectory file (no header row): " << firstCSV.string())
105 const std::vector< std::string >& header = parser.current();
106 _baseNames_.insert(header.begin(), header.end());
107
108 // every declared atemporal variable must actually be one of those columns
109 for (const std::string& aname: _atemporalVars_)
110 if (!_baseNames_.contains(aname))
112 "atemporal variable '" << aname << "' not found in the CSV header")
113
114 GUM_CONSTRUCTOR(KTBNAdaptiveLearner)
115 }
116
117 template < GUM_Numeric GUM_SCALAR >
119 std::string_view dirPath,
120 std::string_view csvBaseName,
121 Size nbSamples,
122 Size kMax,
123 const std::vector< std::string >& missingSymbols,
124 bool induceTypes) :
125 // _inferAtemporalVars_ checks kMax>=2 and nbSamples>=1 itself, before
126 // opening anything (see its declaration) — this initialiser-list call
127 // runs before the delegated-to constructor's own body, so those checks
128 // cannot be left to that body the way the explicit constructor does.
129 // Delegates to the explicit-atemporalVars constructor for the rest
130 // (header read); _inferAtemporalVars_ needs nbSamples, unlike the
131 // header-only read above, since one trajectory alone cannot show that a
132 // value stays constant.
134 dirPath,
135 csvBaseName,
136 nbSamples,
137 kMax,
138 _inferAtemporalVars_(dirPath, csvBaseName, nbSamples, kMax, missingSymbols),
139 missingSymbols,
140 induceTypes) {}
141
142 template < GUM_Numeric GUM_SCALAR >
144 std::string_view dirPath,
145 std::string_view csvBaseName,
146 Size nbSamples,
147 Size kMax,
148 const std::vector< std::string >& missingSymbols) {
150 if (nbSamples < 1) GUM_ERROR(InvalidArgument, "KTBNAdaptiveLearner requires nbSamples >= 1")
152 csvBaseName,
153 nbSamples,
154 missingSymbols);
155 }
156
157 template < GUM_Numeric GUM_SCALAR >
159 std::string_view dirPath,
160 std::string_view csvBaseName,
161 Size nbSamples,
162 Size kMax,
163 const BayesNet< GUM_SCALAR >& bn,
164 const std::unordered_set< std::string >& atemporalVars,
165 const std::vector< std::string >& missingSymbols) :
166 _dirPath_(dirPath), _csvBaseName_(csvBaseName), _nbSamples_(nbSamples), _kMax_(kMax),
167 _atemporalVars_(atemporalVars), _missingSymbols_(missingSymbols), _induceTypes_(false),
168 _prior_bn_(std::make_unique< BayesNet< GUM_SCALAR > >(bn)) {
170 if (nbSamples < 1) GUM_ERROR(InvalidArgument, "KTBNAdaptiveLearner requires nbSamples >= 1")
171
172 // the schema BN is the authoritative variable universe here (not the CSV
173 // header): take the base names from it, so the constraint setters validate
174 // against the same variables every per-k KTBNLearner will use. The CSV is
175 // read only later, at learnKTBN() time.
176 for (const NodeId node: bn.nodes())
177 _baseNames_.insert(bn.variable(node).name());
178
179 // every declared atemporal variable must be a node of the schema BN
180 for (const std::string& aname: _atemporalVars_)
181 if (!_baseNames_.contains(aname))
183 "atemporal variable '" << aname << "' not found in the schema BN")
184
185 GUM_CONSTRUCTOR(KTBNAdaptiveLearner)
186 }
187
188 template < GUM_Numeric GUM_SCALAR >
192
193 // =========================================================================
194 // Main learning methods
195 // =========================================================================
196
197 template < GUM_Numeric GUM_SCALAR >
199 // Explore every candidate order _kMin_.._kMax_ and keep the k-TBN with the
200 // best cross-k model-selection score (BIC by default; see useOrderScore*).
201 // _kMin_ -- 2 unless raised by a constraint naming a concrete slice, see
202 // _recomputeKMin_ -- skips candidates that would only drop that constraint.
203 // Each candidate is a full fixed-k problem: a fresh KTBNLearner on the same
204 // trajectories, replaying the recorded configuration and constraints (those
205 // whose slices do not fit that k are dropped by _applyConstraints_).
206 // Reset first: a re-entry, or a throw below, must not leave a stale selection
207 // readable through bestK() / latentVariables().
208 _bestK_ = 0;
209 _bestLatentVariables_.clear();
210 _scorePerCandidateK_.clear();
211 KTBN< GUM_SCALAR > best;
212 Size bestK = 0; // local; committed to _bestK_ only if the whole sweep succeeds
213 double bestScore = 0.0; // unread until the first candidate sets it
214 double logN = 0.0; // log2 of the total number of observations
215 std::vector< std::pair< Size, double > > scores; // per-k scores, committed at the end
216 std::vector< std::pair< std::string, std::string > >
217 bestLatents; // winner's latent pairs, committed at the end
218
219 for (Size k = _kMin_; k <= _kMax_; ++k) {
220 // build this candidate's fixed-k learner: from the schema BN when one was
221 // supplied (domains fixed explicitly), else from the CSV (domains inferred).
222 // KTBNLearner is non-movable, so hold it through a unique_ptr.
223 std::unique_ptr< KTBNLearner< GUM_SCALAR > > learnerPtr
224 = _prior_bn_ ? std::make_unique< KTBNLearner< GUM_SCALAR > >(_dirPath_,
227 k,
228 *_prior_bn_,
232 : std::make_unique< KTBNLearner< GUM_SCALAR > >(_dirPath_,
235 k,
240 KTBNLearner< GUM_SCALAR >& learner = *learnerPtr;
241
242 // Refused here rather than deep inside structure learning: learnDag_ would
243 // otherwise raise its own generic message only after this candidate's
244 // databases had been built. Every candidate reads the same trajectories, so
245 // the verdict on the first holds for all.
246 if (!_ignoreMissingSymbols_ && learner.hasMissingValues())
248 "the trajectories contain missing values. Neither aGrUM's structure learning "
249 "nor the cross-k order score can evaluate an incomplete window. Call "
250 "ignoreMissingSymbols() to learn and score on the fully observed data only "
251 "(see its warning: dropping skews the selection towards larger k).")
252
253 // sample size for the BIC penalty: the total number of time-slice rows over
254 // all trajectories. Independent of k, so it is computed once on the first
255 // candidate and reused — it must stay fixed for the scores to compare.
256 if (logN == 0.0) {
257 Size nbObs = 0;
258 for (const Size len: learner.nbRows())
259 nbObs += len;
260 logN = std::log2(nbObs);
261 }
262
263 _applyConfig_(learner);
264 _applyConstraints_(learner, k);
265
266 KTBN< GUM_SCALAR > candidate = learner.learnKTBN();
267
268 // Cross-k model selection by the recorded order score (BIC by default):
269 // it must be comparable across k, unlike the per-k structure score, so it
270 // is computed here on the whole learned k-TBN rather than read off the
271 // internal learners. bestK == 0 means nothing is selected yet, so the
272 // first candidate is always taken; a strict '>' afterwards keeps the
273 // smallest k on ties (mild parsimony).
274 const double score = _orderSelectionScore_(candidate, logN);
275 scores.emplace_back(k, score);
276 if (bestK == 0 || score > bestScore) {
277 bestScore = score;
278 best = std::move(candidate);
279 bestK = k;
280 // capture the winner's latent-variable annotations while its learner is
281 // still alive (only MIIC produces them; other algos leave the set empty)
282 bestLatents = (_algo_ == IBNLearner::AlgoType::MIIC)
283 ? learner.latentVariables()
284 : std::vector< std::pair< std::string, std::string > >{};
285 }
286 }
287
288 // Commit only after every candidate has succeeded. An exception thrown above
289 // (e.g. a trajectory too short for some candidate k) leaves the members at
290 // their reset state, so bestK() / latentVariables() / scorePerCandidateK()
291 // keep reporting "not learned" — the all-or-nothing guarantee promised above.
292 _bestK_ = bestK;
293 _scorePerCandidateK_ = std::move(scores);
294 _bestLatentVariables_ = std::move(bestLatents);
295 return best;
296 }
297
298 template < GUM_Numeric GUM_SCALAR >
300 // _bestK_ stays at its sentinel 0 (never a valid order: candidates are >= 2)
301 // until learnKTBN() selects one, so 0 means no learning has run yet.
302 if (_bestK_ == 0)
304 "bestK() is undefined: call learnKTBN() before querying the selected order.")
305 return _bestK_;
306 }
307
308 template < GUM_Numeric GUM_SCALAR >
309 const std::vector< std::pair< std::string, std::string > >&
311 // gated on the same sentinel as bestK(): 0 means learnKTBN() has not run.
312 if (_bestK_ == 0)
313 GUM_ERROR(OperationNotAllowed, "latentVariables() is undefined: call learnKTBN() first.")
315 }
316
317 template < GUM_Numeric GUM_SCALAR >
318 const std::vector< std::pair< Size, double > >&
320 // same sentinel gate as bestK()/latentVariables(): 0 means learnKTBN() has not run.
321 if (_bestK_ == 0)
322 GUM_ERROR(
324 "scorePerCandidateK() is undefined: call learnKTBN() before querying the per-k scores.")
326 }
327
328 // =========================================================================
329 // Diagnostics
330 // =========================================================================
331
332 template < GUM_Numeric GUM_SCALAR >
336
337 template < GUM_Numeric GUM_SCALAR >
339 // MIIC is constraint-based: it consumes a correction, not a score, so no
340 // score/prior clash is possible. (Mirrors IBNLearner::isConstraintBased.)
341 if (_algo_ == IBNLearner::AlgoType::MIIC) return "";
342
343 // map the recorded prior to the internal PriorType. The adaptive learner only
344 // ever records NO_prior or SMOOTHING (its sole prior setter is useSmoothingPrior).
348
349 // delegate to the very same per-score static checks BNLearner uses
350 switch (_score_) {
359 default : return ""; // K2 is never recorded by this learner's setters
360 }
361 }
362
363 template < GUM_Numeric GUM_SCALAR >
364 std::vector< std::tuple< std::string, std::string, std::string > >
366 std::vector< std::tuple< std::string, std::string, std::string > > vals;
367 const auto add = [&](std::string k, std::string v, std::string c = "") {
368 vals.emplace_back(std::move(k), std::move(v), std::move(c));
369 };
370 // render a set of engine-name pairs as "{a->b, c->d}"
371 const auto arcs = [](const std::set< std::pair< std::string, std::string > >& s) {
372 std::string r = "{";
373 bool first = true;
374 for (const auto& [a, b]: s) {
375 if (!first) r += ", ";
376 first = false;
377 r += a + "->" + b;
378 }
379 return r + "}";
380 };
381 const auto names = [](const std::set< std::string >& s) {
382 std::string r = "{";
383 bool first = true;
384 for (const auto& n: s) {
385 if (!first) r += ", ";
386 first = false;
387 r += n;
388 }
389 return r + "}";
390 };
391 // render a set of (tailBase, headBase, lag) triples as "{a->b (lag 1), ...}"
392 const auto kernelArcs = [](const std::set< std::tuple< std::string, std::string, int > >& s) {
393 std::string r = "{";
394 bool first = true;
395 for (const auto& [a, b, lag]: s) {
396 if (!first) r += ", ";
397 first = false;
398 r += a + "->" + b + " (lag " + std::to_string(lag) + ")";
399 }
400 return r + "}";
401 };
402
403 add("Candidate orders", std::to_string(_kMin_) + ".." + std::to_string(_kMax_));
404 add("Selected k", _bestK_ == 0 ? "not learned yet" : std::to_string(_bestK_));
405 add("Base variables", std::to_string(_baseNames_.size()));
406 add("Atemporal variables", names({_atemporalVars_.begin(), _atemporalVars_.end()}));
407
408 switch (_algo_) {
409 case IBNLearner::AlgoType::MIIC : add("Algorithm", "MIIC"); break;
411 add("Algorithm", "Greedy Hill Climbing");
412 break;
414 add("Algorithm", "Extended Greedy Hill Climbing");
415 break;
417 add("Algorithm", "Local Search with Tabu List");
418 add("Tabu list size", std::to_string(_tabuSize_));
419 add("Tabu nb decrease", std::to_string(_nbDecrease_));
420 break;
421 default : add("Algorithm", "(other)"); break;
422 }
423
425 switch (_correction_) {
426 case CorrectedMutualInformation::KModeTypes::MDL : add("Correction", "MDL"); break;
427 case CorrectedMutualInformation::KModeTypes::NML : add("Correction", "NML"); break;
429 add("Correction", "No correction");
430 break;
431 }
432 } else {
433 switch (_score_) {
434 case IBNLearner::ScoreType::AIC : add("Structure score", "AIC"); break;
435 case IBNLearner::ScoreType::BD : add("Structure score", "BD"); break;
436 case IBNLearner::ScoreType::BDeu : add("Structure score", "BDeu"); break;
437 case IBNLearner::ScoreType::BIC : add("Structure score", "BIC"); break;
438 case IBNLearner::ScoreType::fNML : add("Structure score", "fNML"); break;
440 add("Structure score", "Log2Likelihood");
441 break;
442 case IBNLearner::ScoreType::MDL : add("Structure score", "MDL"); break;
443 default : add("Structure score", "(other)"); break;
444 }
445 }
446
447 // the outer, cross-k criterion — distinct from the per-k structure score above
448 switch (_orderScore_) {
449 case OrderScoreType::BIC : add("Order selection score", "BIC"); break;
450 case OrderScoreType::AIC : add("Order selection score", "AIC"); break;
451 case OrderScoreType::fNML : add("Order selection score", "fNML"); break;
452 }
453
455 add("Prior", "Smoothing", checkScorePriorCompatibility());
456 add("Prior weight", std::to_string(_priorWeight_));
457 } else {
458 add("Prior", "no prior is set", checkScorePriorCompatibility());
459 }
460
461 if (!_forbiddenArcs_.empty()) add("Forbidden arcs", arcs(_forbiddenArcs_));
462 if (!_mandatoryArcs_.empty()) add("Mandatory arcs", arcs(_mandatoryArcs_));
463 if (!_forbiddenKernelArcs_.empty())
464 add("Forbidden kernel arcs", kernelArcs(_forbiddenKernelArcs_));
465 if (!_mandatoryKernelArcs_.empty())
466 add("Mandatory kernel arcs", kernelArcs(_mandatoryKernelArcs_));
467 if (!_possibleEdges_.empty()) add("Possible edges", arcs(_possibleEdges_));
468 if (!_forbiddenIntraSliceArcs_.empty())
469 add("Forbidden intra-slice arcs", arcs(_forbiddenIntraSliceArcs_));
470 if (!_forbiddenArcsAllSlices_.empty())
471 add("Forbidden all-slices arcs", arcs(_forbiddenArcsAllSlices_));
472 if (!_noParentNodes_.empty()) add("No-parent nodes", names(_noParentNodes_));
473 if (!_noChildrenNodes_.empty()) add("No-children nodes", names(_noChildrenNodes_));
474 if (_maxIndegree_ != std::numeric_limits< Size >::max())
475 add("Max in-degree", std::to_string(_maxIndegree_));
476 if (!_allowAdditions_) add("Arc additions", "forbidden");
477 if (!_allowDeletions_) add("Arc deletions", "forbidden");
478 if (!_allowReversals_) add("Arc reversals", "forbidden");
479
480 return vals;
481 }
482
483 template < GUM_Numeric GUM_SCALAR >
485 // aligned "key : value (comment)" listing, same layout as BNLearner::toString
486 const auto st = state();
487 std::size_t maxkey = 0;
488 for (const auto& t: st)
489 maxkey = std::max(maxkey, std::get< 0 >(t).length());
490
491 std::string s;
492 for (const auto& t: st) {
493 s += std::format("{:<{}} : {}", std::get< 0 >(t), maxkey, std::get< 1 >(t));
494 if (!std::get< 2 >(t).empty()) s += std::format(" ({})", std::get< 2 >(t));
495 s += '\n';
496 }
497 return s;
498 }
499
500 // =========================================================================
501 // Score selection
502 // =========================================================================
503
504 template < GUM_Numeric GUM_SCALAR >
509
510 template < GUM_Numeric GUM_SCALAR >
515
516 template < GUM_Numeric GUM_SCALAR >
521
522 template < GUM_Numeric GUM_SCALAR >
527
528 template < GUM_Numeric GUM_SCALAR >
533
534 template < GUM_Numeric GUM_SCALAR >
539
540 template < GUM_Numeric GUM_SCALAR >
544
545 // =========================================================================
546 // Order selection (cross-k model selection)
547 // =========================================================================
548
549 template < GUM_Numeric GUM_SCALAR >
554
555 template < GUM_Numeric GUM_SCALAR >
560
561 template < GUM_Numeric GUM_SCALAR >
566
567 // =========================================================================
568 // Missing values
569 // =========================================================================
570
571 template < GUM_Numeric GUM_SCALAR >
574 _ignoreMissingSymbols_ = ignore;
575 return *this;
576 }
577
578 template < GUM_Numeric GUM_SCALAR >
582
583 // =========================================================================
584 // Algorithm selection
585 // =========================================================================
586
587 template < GUM_Numeric GUM_SCALAR >
592
593 template < GUM_Numeric GUM_SCALAR >
599
600 template < GUM_Numeric GUM_SCALAR >
603 Size nb_decrease) {
605 _tabuSize_ = tabu_size;
606 _nbDecrease_ = nb_decrease;
607 return *this;
608 }
609
610 template < GUM_Numeric GUM_SCALAR >
615
616 // =========================================================================
617 // MIIC correction
618 // =========================================================================
619
620 template < GUM_Numeric GUM_SCALAR >
625
626 template < GUM_Numeric GUM_SCALAR >
631
632 template < GUM_Numeric GUM_SCALAR >
637
638 // =========================================================================
639 // Prior selection
640 // =========================================================================
641
642 template < GUM_Numeric GUM_SCALAR >
649
650 // =========================================================================
651 // Structural constraints
652 // =========================================================================
653
654 template < GUM_Numeric GUM_SCALAR >
657 std::string_view headNode) {
658 const auto [tailBase, tailSlice] = _determineNode_(std::string{tailNode});
659 const auto [headBase, headSlice] = _determineNode_(std::string{headNode});
660 _verifyBase_(tailBase, tailSlice);
661 _verifyBase_(headBase, headSlice);
662 _forbiddenArcs_.emplace(std::string{tailNode}, std::string{headNode});
663 // no _checkArcTemporallyFeasible_ call on this path (forbidding an
664 // already-impossible backward arc is a harmless no-op), so head is not
665 // guaranteed >= tail here: both slices must be checked.
666 _raiseKMinForSlice_(tailSlice);
667 _raiseKMinForSlice_(headSlice);
668 return *this;
669 }
670
671 template < GUM_Numeric GUM_SCALAR >
674 int tailSlice,
675 std::string_view headBase,
676 int headSlice) {
677 // validate before _encode_ flattens the slice (a negative one would
678 // silently read back as atemporal, losing the real error)
679 _verifyBase_(tailBase, tailSlice);
680 _verifyBase_(headBase, headSlice);
681 return addForbiddenArc(_encode_(tailBase, tailSlice), _encode_(headBase, headSlice));
682 }
683
684 template < GUM_Numeric GUM_SCALAR >
687 std::string_view headNode) {
688 const auto [tailBase, tailSlice] = _determineNode_(std::string{tailNode});
689 const auto [headBase, headSlice] = _determineNode_(std::string{headNode});
690 _verifyBase_(tailBase, tailSlice);
691 _verifyBase_(headBase, headSlice);
692 this->_checkArcTemporallyFeasible_(tailNode, headNode, "un-forbid the arc");
693 _forbiddenArcs_.erase({std::string{tailNode}, std::string{headNode}});
695 return *this;
696 }
697
698 template < GUM_Numeric GUM_SCALAR >
701 int tailSlice,
702 std::string_view headBase,
703 int headSlice) {
704 // validate before _encode_ flattens the slice (a negative one would
705 // silently read back as atemporal, losing the real error)
706 _verifyBase_(tailBase, tailSlice);
707 _verifyBase_(headBase, headSlice);
708 return eraseForbiddenArc(_encode_(tailBase, tailSlice), _encode_(headBase, headSlice));
709 }
710
711 template < GUM_Numeric GUM_SCALAR >
714 std::string_view headNode) {
715 const auto [tailBase, tailSlice] = _determineNode_(std::string{tailNode});
716 const auto [headBase, headSlice] = _determineNode_(std::string{headNode});
717 _verifyBase_(tailBase, tailSlice);
718 _verifyBase_(headBase, headSlice);
719 // a mandatory arc must be feasible: reject up front the arcs the k-TBN can
720 // never contain (same check KTBNLearner::addMandatoryArc runs)
721 this->_checkArcTemporallyFeasible_(tailNode, headNode, "force the mandatory arc");
722 _mandatoryArcs_.emplace(std::string{tailNode}, std::string{headNode});
723 // the feasibility check above just rejected headSlice < tailSlice, so
724 // headSlice >= tailSlice is guaranteed here: it alone determines _kMin_.
725 _raiseKMinForSlice_(headSlice);
726 return *this;
727 }
728
729 template < GUM_Numeric GUM_SCALAR >
732 int tailSlice,
733 std::string_view headBase,
734 int headSlice) {
735 // validate before _encode_ flattens the slice (a negative one would
736 // silently read back as atemporal, losing the real error)
737 _verifyBase_(tailBase, tailSlice);
738 _verifyBase_(headBase, headSlice);
739 return addMandatoryArc(_encode_(tailBase, tailSlice), _encode_(headBase, headSlice));
740 }
741
742 template < GUM_Numeric GUM_SCALAR >
745 std::string_view headNode) {
746 const auto [tailBase, tailSlice] = _determineNode_(std::string{tailNode});
747 const auto [headBase, headSlice] = _determineNode_(std::string{headNode});
748 _verifyBase_(tailBase, tailSlice);
749 _verifyBase_(headBase, headSlice);
750 // no feasibility check: erasing a never-forced arc is a harmless no-op
751 // (mirrors KTBNLearner::eraseMandatoryArc)
752 _mandatoryArcs_.erase({std::string{tailNode}, std::string{headNode}});
754 return *this;
755 }
756
757 template < GUM_Numeric GUM_SCALAR >
760 int tailSlice,
761 std::string_view headBase,
762 int headSlice) {
763 // validate before _encode_ flattens the slice (a negative one would
764 // silently read back as atemporal, losing the real error)
765 _verifyBase_(tailBase, tailSlice);
766 _verifyBase_(headBase, headSlice);
767 return eraseMandatoryArc(_encode_(tailBase, tailSlice), _encode_(headBase, headSlice));
768 }
769
770 // =========================================================================
771 // Kernel-relative arcs
772 // =========================================================================
773
774 template < GUM_Numeric GUM_SCALAR >
777 int lag,
778 std::string_view headBase) {
779 _verifyKernelArc_(tailBase, headBase, lag);
780 _forbiddenKernelArcs_.emplace(std::string{tailBase}, std::string{headBase}, lag);
782 return *this;
783 }
784
785 template < GUM_Numeric GUM_SCALAR >
788 int lag,
789 std::string_view headBase) {
790 _verifyKernelArc_(tailBase, headBase, lag);
791 _forbiddenKernelArcs_.erase({std::string{tailBase}, std::string{headBase}, lag});
793 return *this;
794 }
795
796 template < GUM_Numeric GUM_SCALAR >
799 int lag,
800 std::string_view headBase) {
801 _verifyKernelArc_(tailBase, headBase, lag);
802 // no separate feasibility check: lag >= 0 (just verified) already guarantees
803 // headSlice = k-1 >= k-1-lag = tailSlice for every candidate, so this can
804 // never be a backward-in-time arc (see addMandatoryKernelArc's doc comment)
805 _mandatoryKernelArcs_.emplace(std::string{tailBase}, std::string{headBase}, lag);
807 return *this;
808 }
809
810 template < GUM_Numeric GUM_SCALAR >
813 int lag,
814 std::string_view headBase) {
815 _verifyKernelArc_(tailBase, headBase, lag);
816 _mandatoryKernelArcs_.erase({std::string{tailBase}, std::string{headBase}, lag});
818 return *this;
819 }
820
821 template < GUM_Numeric GUM_SCALAR >
824 std::string_view headBase) {
825 // these constraints carry no single slice, so no slice range is checked;
826 // an atemporal base has no intra-slice position and is rejected eagerly
827 // here, where KTBNLearner would throw at its own setter (expanding to "C[t]")
828 _checkBaseIsTemporal_(tailBase, "an intra-slice constraint");
829 _checkBaseIsTemporal_(headBase, "an intra-slice constraint");
830 // record the base pair; unlike KTBNLearner it cannot be expanded per-slice
831 // now (k is not fixed) — learnKTBN() expands it for each candidate k
832 _forbiddenIntraSliceArcs_.emplace(std::string{tailBase}, std::string{headBase});
833 return *this;
834 }
835
836 template < GUM_Numeric GUM_SCALAR >
839 std::string_view headBase) {
840 // same eager rejection as addForbiddenIntraSliceArc: such a constraint can
841 // never have been recorded
842 _checkBaseIsTemporal_(tailBase, "an intra-slice constraint");
843 _checkBaseIsTemporal_(headBase, "an intra-slice constraint");
844 _forbiddenIntraSliceArcs_.erase({std::string{tailBase}, std::string{headBase}});
845 return *this;
846 }
847
848 template < GUM_Numeric GUM_SCALAR >
851 std::string_view headBase) {
854 // record the base pair; learnKTBN() expands it over every causally-possible
855 // slice pair for each candidate k (k is not fixed here)
856 _forbiddenArcsAllSlices_.emplace(std::string{tailBase}, std::string{headBase});
857 return *this;
858 }
859
860 template < GUM_Numeric GUM_SCALAR >
863 std::string_view headBase) {
866 _forbiddenArcsAllSlices_.erase({std::string{tailBase}, std::string{headBase}});
867 return *this;
868 }
869
870 template < GUM_Numeric GUM_SCALAR >
872 KTBNAdaptiveLearner< GUM_SCALAR >::addNoParentNode(std::string_view base, int slice) {
873 // validate before _encode_ flattens the slice (see the arc overloads above)
874 _verifyBase_(base, slice);
875 return addNoParentNode(_encode_(base, slice));
876 }
877
878 template < GUM_Numeric GUM_SCALAR >
881 const auto [base, slice] = _determineNode_(std::string{name});
882 _verifyBase_(base, slice);
883 _noParentNodes_.insert(std::string{name});
884 _raiseKMinForSlice_(slice);
885 return *this;
886 }
887
888 template < GUM_Numeric GUM_SCALAR >
891 // validate before _encode_ flattens the slice (see the arc overloads above)
892 _verifyBase_(base, slice);
893 return eraseNoParentNode(_encode_(base, slice));
894 }
895
896 template < GUM_Numeric GUM_SCALAR >
899 const auto [base, slice] = _determineNode_(std::string{name});
900 _verifyBase_(base, slice);
901 _noParentNodes_.erase(std::string{name});
903 return *this;
904 }
905
906 template < GUM_Numeric GUM_SCALAR >
909 // validate before _encode_ flattens the slice (see the arc overloads above)
910 _verifyBase_(base, slice);
911 return addNoChildrenNode(_encode_(base, slice));
912 }
913
914 template < GUM_Numeric GUM_SCALAR >
917 const auto [base, slice] = _determineNode_(std::string{name});
918 _verifyBase_(base, slice);
919 _noChildrenNodes_.insert(std::string{name});
920 _raiseKMinForSlice_(slice);
921 return *this;
922 }
923
924 template < GUM_Numeric GUM_SCALAR >
927 // validate before _encode_ flattens the slice (see the arc overloads above)
928 _verifyBase_(base, slice);
929 return eraseNoChildrenNode(_encode_(base, slice));
930 }
931
932 template < GUM_Numeric GUM_SCALAR >
935 const auto [base, slice] = _determineNode_(std::string{name});
936 _verifyBase_(base, slice);
937 _noChildrenNodes_.erase(std::string{name});
939 return *this;
940 }
941
942 template < GUM_Numeric GUM_SCALAR >
945 int tailSlice,
946 std::string_view headBase,
947 int headSlice) {
948 // validate before _encode_ flattens the slice (a negative one would
949 // silently read back as atemporal, losing the real error)
950 _verifyBase_(tailBase, tailSlice);
951 _verifyBase_(headBase, headSlice);
952 return addPossibleEdge(_encode_(tailBase, tailSlice), _encode_(headBase, headSlice));
953 }
954
955 template < GUM_Numeric GUM_SCALAR >
958 std::string_view head) {
959 const auto [tailBase, tailSlice] = _determineNode_(std::string{tail});
960 const auto [headBase, headSlice] = _determineNode_(std::string{head});
961 _verifyBase_(tailBase, tailSlice);
962 _verifyBase_(headBase, headSlice);
963 // an edge is undirected (BNLearner stores it as a gum::Edge), so the pair is
964 // normalized alphabetically: (A,B) and (B,A) are one and the same record
965 std::string first{tail}, second{head};
966 if (second < first) std::swap(first, second);
967 _possibleEdges_.emplace(std::move(first), std::move(second));
968 // an edge is undirected and unchecked, so neither slice dominates: both
969 // must be checked (tailSlice/headSlice, decoded above the swap, are
970 // unaffected by it).
971 _raiseKMinForSlice_(tailSlice);
972 _raiseKMinForSlice_(headSlice);
973 return *this;
974 }
975
976 template < GUM_Numeric GUM_SCALAR >
979 int tailSlice,
980 std::string_view headBase,
981 int headSlice) {
982 // validate before _encode_ flattens the slice (a negative one would
983 // silently read back as atemporal, losing the real error)
984 _verifyBase_(tailBase, tailSlice);
985 _verifyBase_(headBase, headSlice);
986 return erasePossibleEdge(_encode_(tailBase, tailSlice), _encode_(headBase, headSlice));
987 }
988
989 template < GUM_Numeric GUM_SCALAR >
992 std::string_view head) {
993 const auto [tailBase, tailSlice] = _determineNode_(std::string{tail});
994 const auto [headBase, headSlice] = _determineNode_(std::string{head});
995 _verifyBase_(tailBase, tailSlice);
996 _verifyBase_(headBase, headSlice);
997 // same alphabetical normalization as addPossibleEdge, so erasing (B,A)
998 // removes the edge recorded as (A,B)
999 std::string first{tail}, second{head};
1000 if (second < first) std::swap(first, second);
1001 _possibleEdges_.erase({std::move(first), std::move(second)});
1003 return *this;
1004 }
1005
1006 template < GUM_Numeric GUM_SCALAR >
1009 _allowAdditions_ = allow;
1010 return *this;
1011 }
1012
1013 template < GUM_Numeric GUM_SCALAR >
1016 _allowDeletions_ = allow;
1017 return *this;
1018 }
1019
1020 template < GUM_Numeric GUM_SCALAR >
1023 _allowReversals_ = allow;
1024 return *this;
1025 }
1026
1027 template < GUM_Numeric GUM_SCALAR >
1030 _maxIndegree_ = max_indegree;
1031 return *this;
1032 }
1033
1034 // =========================================================================
1035 // Name encoding
1036 // =========================================================================
1037
1038 template < GUM_Numeric GUM_SCALAR >
1039 const std::unordered_set< std::string >&
1043
1044 template < GUM_Numeric GUM_SCALAR >
1045 bool KTBNAdaptiveLearner< GUM_SCALAR >::_isKnownBase_(std::string_view base) const {
1046 return _baseNames_.contains(std::string{base});
1047 }
1048
1049 // =========================================================================
1050 // Validation
1051 // =========================================================================
1052
1053 template < GUM_Numeric GUM_SCALAR >
1054 void KTBNAdaptiveLearner< GUM_SCALAR >::_verifyBase_(std::string_view base, int slice) const {
1055 if (!_baseNames_.contains(std::string{base}))
1057 "unknown base variable '" << base << "': it is not one of the data columns")
1058 // an atemporal variable's only valid slice is ATEMPORAL; reject eagerly
1059 // rather than recording e.g. "C[1]" and failing later at learnKTBN() time
1060 if (_atemporalVars_.contains(std::string{base})) {
1061 if (slice != KTBN< GUM_SCALAR >::ATEMPORAL)
1063 "atemporal variable '" << base << "' cannot be given a time slice (got " << slice
1064 << "): use KTBN::ATEMPORAL")
1065 return;
1066 }
1067 // temporal: valid slices are [0, kMax). A negative value other than
1068 // ATEMPORAL must be caught here -- _encode_ would turn it into "X[-5]",
1069 // which reads back as atemporal and slips past the slice-fit rule.
1070 if (slice != KTBN< GUM_SCALAR >::ATEMPORAL && slice < 0)
1072 "negative time slice " << slice << " for '" << base
1073 << "': slices start at 0 (use KTBN::ATEMPORAL to address an "
1074 "atemporal variable)")
1075 if (slice >= static_cast< int >(_kMax_))
1077 "time slice " << slice << " for '" << base
1078 << "' is out of range: it must be < kMax (" << _kMax_ << ")")
1079 }
1080
1081 template < GUM_Numeric GUM_SCALAR >
1083 std::string_view headBase,
1084 int lag) const {
1085 // an atemporal variable has a single instance, not a per-slice one, so it
1086 // has no kernel slice to anchor a lag against: same rule as the intra-slice
1087 // setters, hence the same shared check
1088 for (const std::string_view base: {tailBase, headBase})
1089 _checkBaseIsTemporal_(base, "a kernel-relative arc");
1090 // lag < 0 would place the tail AFTER the kernel slice (the last slice by
1091 // definition), which can never happen
1092 if (lag < 0) GUM_ERROR(InvalidArgument, "negative kernel lag " << lag << ": lag must be >= 0")
1093 // some candidate k in [2,kMax] must be able to place the tail at slice
1094 // k-1-lag >= 0, i.e. lag <= k-1 <= kMax-1
1095 if (lag >= static_cast< int >(_kMax_))
1097 "kernel lag " << lag << " is out of range: it must be < kMax (" << _kMax_
1098 << "), so some candidate k can place the tail at slice k-1-lag >= 0")
1099 }
1100
1101 // =========================================================================
1102 // Candidate-range bookkeeping
1103 // =========================================================================
1104
1105 template < GUM_Numeric GUM_SCALAR >
1107 if (slice != KTBN< GUM_SCALAR >::ATEMPORAL)
1108 _kMin_ = std::max(_kMin_, static_cast< Size >(slice) + 1);
1109 }
1110
1111 template < GUM_Numeric GUM_SCALAR >
1113 // kMin = 1 + the largest recorded slice (fits()'s "slice < k" rule means a
1114 // smaller k would drop that constraint). An erase can lower kMin, unlike a
1115 // single _raiseKMinForSlice_ call, so this rebuilds from scratch rather
1116 // than adjusting in place.
1117 _kMin_ = 2;
1118 for (const auto& [tail, head]: _forbiddenArcs_) {
1121 }
1122 for (const auto& [tail, head]: _mandatoryArcs_) {
1125 }
1126 for (const auto& [tail, head]: _possibleEdges_) {
1129 }
1130 for (const auto& node: _noParentNodes_)
1132 for (const auto& node: _noChildrenNodes_)
1134 // kernel-relative arcs: the lag itself is what must fit (the head's slice is
1135 // always the kernel k-1, which trivially fits any k), so raise on the lag
1136 // directly rather than decoding an engine name that doesn't exist yet.
1137 for (const auto& t: _forbiddenKernelArcs_)
1138 _raiseKMinForSlice_(std::get< 2 >(t));
1139 for (const auto& t: _mandatoryKernelArcs_)
1140 _raiseKMinForSlice_(std::get< 2 >(t));
1141 // _forbiddenIntraSliceArcs_ / _forbiddenArcsAllSlices_ carry no slice (they
1142 // are base-name pairs, expanded per-k by the fixed-k learner) and so are
1143 // left out: they never make a candidate drop anything, whatever k is.
1144 }
1145
1146 // =========================================================================
1147 // Configuration replay (learnKTBN helpers)
1148 // =========================================================================
1149
1150 template < GUM_Numeric GUM_SCALAR >
1152 // A fresh KTBNLearner already starts at the mirrored defaults (MIIC / BDeu /
1153 // MDL / NO_prior), so we only touch a knob when it differs from its default,
1154 // and only the knobs the chosen algorithm actually consumes.
1155 bool scoreBased = false;
1156 switch (_algo_) {
1158 // MIIC is the default algo — nothing to set on a fresh learner. It uses a
1159 // correction (not a score); apply it only when it is not the default MDL.
1160 switch (_correction_) {
1163 case CorrectedMutualInformation::KModeTypes::MDL : break; // default
1164 }
1165 break;
1167 learner.useGreedyHillClimbing();
1168 scoreBased = true;
1169 break;
1172 scoreBased = true;
1173 break;
1176 scoreBased = true;
1177 break;
1178 default : break; // K2 / PC are never recorded by our setters
1179 }
1180
1181 // score-based algos consume a score; apply it only when it is not the default BDeu
1182 if (scoreBased && _score_ != IBNLearner::ScoreType::BDeu) {
1183 switch (_score_) {
1184 case IBNLearner::ScoreType::AIC : learner.useScoreAIC(); break;
1185 case IBNLearner::ScoreType::BD : learner.useScoreBD(); break;
1186 case IBNLearner::ScoreType::BIC : learner.useScoreBIC(); break;
1188 case IBNLearner::ScoreType::MDL : learner.useScoreMDL(); break;
1189 case IBNLearner::ScoreType::fNML : learner.useScorefNML(); break;
1190 default : break; // BDeu (guarded out) and K2 (never recorded)
1191 }
1192 }
1193
1194 // the prior feeds parameter estimation whatever the algorithm, so it is applied
1195 // independently of the score/correction split, again only when non-default
1198 }
1199
1200 template < GUM_Numeric GUM_SCALAR >
1202 Size k) const {
1203 const int ik = static_cast< int >(k);
1204 // an engine-name endpoint fits candidate k iff its slice is < k (ATEMPORAL == -1
1205 // always fits); a constraint touching a slice this candidate does not have is
1206 // skipped for this k only.
1207 auto fits = [&](const std::string& node) { return _determineNode_(node).second < ik; };
1208
1209 for (const auto& [tail, head]: _forbiddenArcs_)
1210 if (fits(tail) && fits(head)) learner.addForbiddenArc(tail, head);
1211 for (const auto& [tail, head]: _mandatoryArcs_)
1212 if (fits(tail) && fits(head)) learner.addMandatoryArc(tail, head);
1213 for (const auto& [tail, head]: _possibleEdges_)
1214 if (fits(tail) && fits(head)) learner.addPossibleEdge(tail, head);
1215 for (const auto& node: _noParentNodes_)
1216 if (fits(node)) learner.addNoParentNode(node);
1217 for (const auto& node: _noChildrenNodes_)
1218 if (fits(node)) learner.addNoChildrenNode(node);
1219
1220 // kernel-relative arcs: resolve the lag against this candidate's kernel
1221 // slice (k-1) into an explicit (base, slice) arc. Always fits once k >=
1222 // _kMin_ (guaranteed by _raiseKMinForSlice_/_recomputeKMin_ above), but the
1223 // guard mirrors fits()'s defensive style above rather than assuming it.
1224 for (const auto& [tailBase, headBase, lag]: _forbiddenKernelArcs_) {
1225 const int tailSlice = ik - 1 - lag;
1226 if (tailSlice >= 0) learner.addForbiddenArc(tailBase, tailSlice, headBase, ik - 1);
1227 }
1228 for (const auto& [tailBase, headBase, lag]: _mandatoryKernelArcs_) {
1229 const int tailSlice = ik - 1 - lag;
1230 if (tailSlice >= 0) learner.addMandatoryArc(tailBase, tailSlice, headBase, ik - 1);
1231 }
1232
1233 // base-only constraints: hand them to the fixed-k learner, which expands them
1234 // over its own slices (no manual expansion / no slice filtering needed here).
1235 for (const auto& [tailBase, headBase]: _forbiddenIntraSliceArcs_)
1236 learner.addForbiddenIntraSliceArc(tailBase, headBase);
1237 for (const auto& [tailBase, headBase]: _forbiddenArcsAllSlices_)
1238 learner.addForbiddenArcAllSlices(tailBase, headBase);
1239
1240 // structural-search knobs, applied only when the user changed them from default
1241 if (!_allowAdditions_) learner.allowArcAdditions(false);
1242 if (!_allowDeletions_) learner.allowArcDeletions(false);
1243 if (!_allowReversals_) learner.allowArcReversals(false);
1244 if (_maxIndegree_ != std::numeric_limits< Size >::max()) learner.setMaxIndegree(_maxIndegree_);
1245 }
1246
1247 // =========================================================================
1248 // Model selection
1249 // =========================================================================
1250
1251 template < GUM_Numeric GUM_SCALAR >
1252 template < typename PerInstance, typename PerNodeFinal >
1254 PerInstance perInstance,
1255 PerNodeFinal perNodeFinal) const {
1256 namespace fs = std::filesystem;
1257 const int k = static_cast< int >(net.k());
1258
1259 // Precomputed scorer for one template node, reused across every row of
1260 // every trajectory so the inner loop never rebuilds an Instantiation
1261 // (~3.7x the cost of one chgVal) or re-decodes a variable name. selfDom /
1262 // counts carry the accumulator a reduction criterion (fNML) closes over
1263 // in perNodeFinal; likelihood-style reductions ignore them.
1264 struct NodeEval {
1265 const Tensor< GUM_SCALAR >* cpt;
1266 Instantiation inst; // slaved to *cpt
1267 std::vector< std::string > dimName; // cpt.variable(d).name()
1268 std::vector< std::pair< std::string, int > > dimNode; // _determineNode_(name)
1269 Size selfDom; // |node| (CPT dim 0 is the node)
1270 std::unordered_map< Size, double > counts; // parent-config index -> #obs
1271 };
1272
1273 auto makeEval = [&](const Tensor< GUM_SCALAR >& cpt) {
1274 NodeEval e;
1275 e.cpt = &cpt;
1276 e.inst = Instantiation(cpt);
1277 e.selfDom = cpt.variable(0).domainSize(); // the node varies fastest (dim 0)
1278 e.dimName.reserve(cpt.nbrDim());
1279 e.dimNode.reserve(cpt.nbrDim());
1280 for (Idx d = 0; d < cpt.nbrDim(); ++d) {
1281 std::string name = cpt.variable(d).name();
1282 e.dimNode.push_back(_determineNode_(name));
1283 e.dimName.push_back(std::move(name));
1284 }
1285 return e;
1286 };
1287
1288 // one evaluator per (temporal base, slice), indexed by slice in 0..k-1: the
1289 // node scored at absolute time t uses slice min(t, k-1) (see childSlice below)
1290 const auto& temporalBases = net.temporalVarNames();
1291 std::vector< std::vector< NodeEval > > temporalEvals(k);
1292 for (int slice = 0; slice < k; ++slice) {
1293 temporalEvals[slice].reserve(temporalBases.size());
1294 for (const auto& base: temporalBases)
1295 temporalEvals[slice].push_back(makeEval(net.cpt(base, slice)));
1296 }
1297
1298 // one evaluator per atemporal node (scored once per trajectory)
1299 std::vector< NodeEval > atemporalEvals;
1300 atemporalEvals.reserve(net.atemporalVarNames().size());
1301 for (const auto& base: net.atemporalVarNames())
1302 atemporalEvals.push_back(makeEval(net.cpt(base, KTBN< GUM_SCALAR >::ATEMPORAL)));
1303
1304 // one circular buffer of k rows, reused across trajectories: the row for
1305 // absolute time t lives at slot (t % k), so the k live rows never move —
1306 // reading a new row overwrites the stalest slot (which held time t-k).
1307 std::vector< std::vector< std::string > > window(k);
1308
1309 // missing-value markers, as a set for O(1) tests in the inner loop
1310 const std::unordered_set< std::string > missing(_missingSymbols_.begin(),
1311 _missingSymbols_.end());
1312
1313 for (Size s = 0; s < _nbSamples_; ++s) {
1314 const fs::path file = fs::path{_dirPath_} / (_csvBaseName_ + std::to_string(s + 1) + ".csv");
1315
1316 // ---- pre-pass: column index, and the atemporal values of this trajectory ----
1317 // An atemporal variable is constant down the file, but the row that
1318 // happens to carry a missing marker teaches nothing, so its value is the
1319 // first *informative* one rather than row 0's. The scan stops as soon as
1320 // every atemporal column is resolved (row 0 in the usual case), so it
1321 // costs one row unless the data actually has holes. It cannot be folded
1322 // into the main pass: a value resolved at row 12 must already be known
1323 // when row 0 is scored, and buffering the rows instead would defeat the
1324 // width-k window that bounds this function's memory.
1325 std::unordered_map< std::string, std::size_t > colOf;
1326 std::unordered_map< std::string, std::string > atempVal;
1327 {
1328 std::ifstream pis(file, std::ifstream::in);
1329 if (!pis.is_open()) GUM_ERROR(IOError, "Cannot open " << file.string())
1330 CSVParser pre(pis, file.string());
1331 if (!pre.next()) GUM_ERROR(IOError, "empty trajectory file '" << file.string() << "'") {
1332 const auto& header = pre.current();
1333 for (std::size_t c = 0; c < header.size(); ++c)
1334 colOf[header[c]] = c;
1335 }
1336 const std::size_t nbAtemp = net.atemporalVarNames().size();
1337 while (atempVal.size() < nbAtemp && pre.next()) {
1338 const auto& row = pre.current();
1339 for (const auto& c: net.atemporalVarNames()) {
1340 if (atempVal.contains(c)) continue;
1341 const std::string& tok = row[colOf.at(c)];
1342 if (!missing.contains(tok)) atempVal[c] = tok;
1343 }
1344 }
1345 }
1346
1347 std::ifstream is(file, std::ifstream::in);
1348 if (!is.is_open()) GUM_ERROR(IOError, "Cannot open " << file.string())
1349 CSVParser parser(is, file.string());
1350 parser.next(); // skip the header, already consumed by the pre-pass
1351
1352 // Label observed in the data for one CPT dimension (varBase, varSlice) —
1353 // the scored node itself or one of its parents — while scoring the node
1354 // whose template slice is scoredSlice at absolute time scoredTime.
1355 // Returns nullptr when the value is missing (or, for an atemporal
1356 // variable, never observed in this trajectory); the caller then drops the
1357 // whole instance. A pointer rather than a value: both sources are stable
1358 // strings, and this is the innermost loop of the order selection.
1359 auto labelOf = [&](const std::string& varBase,
1360 int varSlice,
1361 int scoredTime,
1362 int scoredSlice) -> const std::string* {
1363 // atemporal variable: constant down the trajectory, read the captured value
1364 if (varSlice == KTBN< GUM_SCALAR >::ATEMPORAL) {
1365 const auto it = atempVal.find(varBase);
1366 return (it == atempVal.end()) ? nullptr : &it->second;
1367 }
1368 // temporal variable: it lags (scoredSlice - varSlice) steps behind the
1369 // scored node, so its value was observed at that earlier absolute time
1370 const int varTime = scoredTime - (scoredSlice - varSlice);
1371 // fetch that row from the circular buffer (slot = time modulo k)
1372 const std::string& tok = window[varTime % k][colOf.at(varBase)];
1373 return missing.contains(tok) ? nullptr : &tok;
1374 };
1375
1376 int t = 0; // absolute time == data-row index
1377 while (parser.next()) {
1378 const auto& cur = parser.current();
1379 window[t % k].assign(cur.begin(), cur.end());
1380
1381 // first k-1 slices use their own initial CPT; from slice k-1 on the
1382 // transition kernel (template slice k-1) is reused for every step
1383 const int childSlice = (t < k - 1) ? t : (k - 1);
1384 for (NodeEval& e: temporalEvals[childSlice]) {
1385 // an instance counts only if its WHOLE family is observed: the node
1386 // and every parent, each read at its own lag. One missing value drops
1387 // this instance alone — the other nodes of the same row are unaffected.
1388 bool complete = true;
1389 for (std::size_t d = 0; d < e.dimName.size(); ++d) {
1390 const auto& [vbase, vslice] = e.dimNode[d];
1391 const std::string* lbl = labelOf(vbase, vslice, t, childSlice);
1392 if (lbl == nullptr) {
1393 complete = false;
1394 break;
1395 }
1396 e.inst.chgVal(e.dimName[d], *lbl);
1397 }
1398 if (complete) perInstance(e);
1399 }
1400 ++t;
1401 }
1402
1403 // atemporal nodes: scored once per trajectory (parents are atemporal too).
1404 // An atemporal column left unresolved by the pre-pass drops every instance
1405 // that reads it, here and in the temporal loop above — for THIS trajectory
1406 // only, the others still scoring normally.
1407 if (t > 0)
1408 for (NodeEval& e: atemporalEvals) {
1409 bool complete = true;
1410 for (std::size_t d = 0; d < e.dimName.size(); ++d) {
1411 const auto it = atempVal.find(e.dimNode[d].first);
1412 if (it == atempVal.end()) {
1413 complete = false;
1414 break;
1415 }
1416 e.inst.chgVal(e.dimName[d], it->second);
1417 }
1418 if (complete) perInstance(e);
1419 }
1420 }
1421
1422 // per-node closing pass over every evaluator (all slices + atemporal), for
1423 // criteria that reduce the accumulated per-node state after the whole walk
1424 for (auto& slice: temporalEvals)
1425 for (NodeEval& e: slice)
1426 perNodeFinal(e);
1427 for (NodeEval& e: atemporalEvals)
1428 perNodeFinal(e);
1429 }
1430
1431 template < GUM_Numeric GUM_SCALAR >
1432 double KTBNAdaptiveLearner< GUM_SCALAR >::_log2Likelihood_(const KTBN< GUM_SCALAR >& net) const {
1433 // sum log2 of the CPT entry over every visited node instance; no per-node
1434 // closing reduction is needed (the likelihood is fully additive per instance)
1435 double logL = 0.0;
1437 net,
1438 [&logL](auto& e) { logL += std::log2((*e.cpt)[e.inst]); },
1439 [](auto&) {});
1440 return logL;
1441 }
1442
1443 template < GUM_Numeric GUM_SCALAR >
1444 double KTBNAdaptiveLearner< GUM_SCALAR >::_fNMLScore_(const KTBN< GUM_SCALAR >& net) const {
1445 // one data walk: per instance, accumulate log2L AND bucket the observation by
1446 // its parent configuration; per node, close over the buckets with the
1447 // multinomial parametric complexity log2Cnr, exactly the per-node regret term
1448 // aGrUM's ScorefNML subtracts. Fused (rather than a separate _log2Likelihood_
1449 // call plus a separate penalty pass) so fNML order-selection reads every
1450 // trajectory once per candidate k, not twice.
1451 double logL = 0.0, penalty = 0.0;
1453 net,
1454 [&logL](auto& e) {
1455 logL += std::log2((*e.cpt)[e.inst]);
1456 // linear index over the parent dims (1..nbrDim-1); the node is dim 0
1457 Size idx = 0;
1458 for (Idx d = 1; d < e.cpt->nbrDim(); ++d)
1459 idx = idx * e.cpt->variable(d).domainSize() + e.inst.val(e.cpt->variable(d));
1460 e.counts[idx] += 1.0;
1461 },
1462 [this, &penalty](auto& e) {
1463 for (const auto& [cfg, n]: e.counts)
1464 penalty += _ctable_.log2Cnr(e.selfDom, n);
1465 });
1466 return logL - penalty;
1467 }
1468
1469 template < GUM_Numeric GUM_SCALAR >
1470 double KTBNAdaptiveLearner< GUM_SCALAR >::_countParameters_(const KTBN< GUM_SCALAR >& net) const {
1471 // Free parameters of the template: for each node, (|node| - 1) independent
1472 // entries per joint parent configuration. Summed over every template node
1473 // (initial slices, kernel slice, atemporal nodes), this is the k-TBN's total
1474 // parameter count — it rises with k, which is what the BIC penalty needs.
1475 double df = 0.0;
1476 for (const auto& [base, slice]: net.nodes()) {
1477 double cell = net.variable(base, slice).domainSize() - 1.0;
1478 for (const auto& [pbase, pslice]: net.parents(base, slice))
1479 cell *= net.variable(pbase, pslice).domainSize();
1480 df += cell;
1481 }
1482 return df;
1483 }
1484
1485 template < GUM_Numeric GUM_SCALAR >
1487 double logN) const {
1488 // BIC/AIC share a log2-likelihood term plus a data-free parameter-count
1489 // penalty, so both read the trajectories once via _log2Likelihood_. fNML
1490 // needs the likelihood too, but paired with a data-dependent per-node regret
1491 // over the very same walk, so it is fused into a single _forEachScoredNode_
1492 // pass (_fNMLScore_) instead of calling _log2Likelihood_ a second time.
1493 switch (_orderScore_) {
1494 case OrderScoreType::BIC :
1495 // reward fit, penalise complexity: log2L − ½·df·log2(N)
1496 return _log2Likelihood_(net) - 0.5 * _countParameters_(net) * logN;
1497 case OrderScoreType::AIC :
1498 // lighter, sample-size-independent penalty: log2L − df
1499 return _log2Likelihood_(net) - _countParameters_(net);
1501 // per-node multinomial parametric complexity (regret): log2L − Σ log2Cnr
1502 return _fNMLScore_(net);
1503 }
1504 // every criterion returns above; unreachable while OrderScoreType is exhausted
1505 return _log2Likelihood_(net) - 0.5 * _countParameters_(net) * logN;
1506 }
1507
1508} // namespace gum::learning
Class for fast parsing of CSV file (never more than one line in application memory).
A k-TBN learner that also selects the order k from the data.
Class representing a Bayesian network.
Definition BayesNet.h:99
Exception : input/output problem.
Class for assigning/browsing values to tuples of discrete variables.
Instantiation()
Default constructor: creates an empty tuple.
Exception: at least one argument passed to a function is not what was expected.
static constexpr int ATEMPORAL
Conventional time-slice value denoting an atemporal (static) variable.
Definition KTBN.h:200
Error: The database contains some missing values.
Exception : operation not allowed.
Class for fast parsing of CSV file (never more than one line in application memory).
Definition CSVParser.h:78
bool next()
gets the next line of the csv stream and parses it
const std::vector< std::string > & current() const
returns the current parsed line
void _checkBaseIsTemporal_(std::string_view base, std::string_view context) const
Throw InvalidArgument unless base is a known temporal base. context completes "cannot appear in <cont...
void _checkArcTemporallyFeasible_(std::string_view tail, std::string_view head, std::string_view action) const
Reject an arc the k-TBN definition can never contain, so eraseForbiddenArc and addMandatoryArc both f...
static std::unordered_set< std::string > _scanConstantColumns_(std::string_view dirPath, std::string_view csvBaseName, Size nbSamples, const std::vector< std::string > &missingSymbols)
Scans every trajectory and returns the base names classified atemporal: those whose value never chang...
static void _checkMinimalOrder_(Size order, std::string_view label)
Throw InvalidArgument unless order is at least 2, label naming the offending parameter ("k" for the f...
std::string _encode_(std::string_view base, int slice) const
(base, slice) -> engine name ("A[1]" / atemporal engine name). Pure function, shared by every learner...
std::pair< std::string, int > _determineNode_(const std::string &name) const
engine name -> (base, slice); atemporal names map to KTBN::ATEMPORAL. Shared by every learner; only t...
Learns a k-TBN (order k + structure + parameters) from trajectory CSVs.
std::string toString() const
Human-readable summary of the recorded configuration (candidate order range, algorithm / score / corr...
KTBNAdaptiveLearner< GUM_SCALAR > & addForbiddenArcAllSlices(std::string_view tailBase, std::string_view headBase) override
Forbid an arc from tailBase, lag slices before the kernel, to headBase in the kernel: tailBase at sli...
bool _induceTypes_
whether numeric columns are retyped (see KTBNLearner); unused (and forced false) when a schema BN is ...
std::set< std::string > _noParentNodes_
root nodes (no parents), as engine names
KTBNAdaptiveLearner< GUM_SCALAR > & allowArcAdditions(bool allow=true) override
Forbid an arc from tailBase, lag slices before the kernel, to headBase in the kernel: tailBase at sli...
std::vector< std::string > _missingSymbols_
symbols in the CSVs to interpret as missing values
std::unordered_set< std::string > _atemporalVars_
base names of the atemporal (static) variables
bool _allowAdditions_
recorded graph-change permissions / indegree cap
void _verifyKernelArc_(std::string_view tailBase, std::string_view headBase, int lag) const
Throw InvalidArgument unless tailBase and headBase are known, temporal base variables (a kernel-relat...
KTBNAdaptiveLearner< GUM_SCALAR > & useOrderScoreBIC()
Select k by BIC (the default): keep the k whose learned model maximises .
std::unique_ptr< BayesNet< GUM_SCALAR > > _prior_bn_
optional variable-schema BN (set by the BN constructor): when present, each per-k KTBNLearner is buil...
Size _bestK_
k selected by the last learnKTBN() call, or 0 as a sentinel while no learning has happened yet....
std::string _dirPath_
directory holding the trajectory CSV files
Size _kMax_
largest order to explore (candidates are kMin..kMax)
KTBNAdaptiveLearner< GUM_SCALAR > & addMandatoryArc(std::string_view tailNode, std::string_view headNode) override
Forbid an arc from tailBase, lag slices before the kernel, to headBase in the kernel: tailBase at sli...
bool _isKnownBase_(std::string_view base) const override
whether base is one of this learner's variables; the base names read at construction,...
KTBNAdaptiveLearner< GUM_SCALAR > & eraseForbiddenArcAllSlices(std::string_view tailBase, std::string_view headBase) override
Forbid an arc from tailBase, lag slices before the kernel, to headBase in the kernel: tailBase at sli...
KTBNAdaptiveLearner< GUM_SCALAR > & useNMLCorrection() override
void _applyConstraints_(KTBNLearner< GUM_SCALAR > &learner, Size k) const
Replay the recorded structural constraints onto learner (built for order k). Engine-name constraints ...
std::vector< std::pair< std::string, std::string > > _bestLatentVariables_
latent-variable arcs (engine-name pairs) reported by the winning candidate's MIIC run,...
IBNLearner::BNLearnerPriorType _prior_
recorded prior and its weight
double _orderSelectionScore_(const KTBN< GUM_SCALAR > &net, double logN) const
The cross-k order-selection score of net under the recorded orderScore criterion, given logN = log2 o...
const std::vector< std::pair< std::string, std::string > > & latentVariables() const
Engine-name (tail, head) pairs of arcs the selected model's MIIC run flagged as hiding a latent varia...
std::set< std::string > _noChildrenNodes_
leaf nodes (no children), as engine names
KTBNAdaptiveLearner< GUM_SCALAR > & useGreedyHillClimbing() override
double _fNMLScore_(const KTBN< GUM_SCALAR > &net) const
fNML order score of net over the recorded trajectories: , with the node's domain size and the count...
KTBNAdaptiveLearner< GUM_SCALAR > & eraseForbiddenIntraSliceArc(std::string_view tailBase, std::string_view headBase) override
Forbid an arc from tailBase, lag slices before the kernel, to headBase in the kernel: tailBase at sli...
void _forEachScoredNode_(const KTBN< GUM_SCALAR > &net, PerInstance perInstance, PerNodeFinal perNodeFinal) const
Stream every scored template-node instance of net over the recorded trajectories, driving both the li...
~KTBNAdaptiveLearner()
Constructor — the candidate orders are kMin..kMax.
KTBNAdaptiveLearner< GUM_SCALAR > & eraseMandatoryArc(std::string_view tailNode, std::string_view headNode) override
Forbid an arc from tailBase, lag slices before the kernel, to headBase in the kernel: tailBase at sli...
IBNLearner::AlgoType _algo_
recorded structure-learning algorithm
KTBNAdaptiveLearner< GUM_SCALAR > & addMandatoryKernelArc(std::string_view tailBase, int lag, std::string_view headBase)
Force an arc from tailBase, lag slices before the kernel, to headBase in the kernel....
OrderScoreType _orderScore_
recorded cross-k order-selection criterion (the outer score used by learnKTBN() to pick the best k; i...
Size kMax() const
Largest order explored (the kMax argument of the constructor).
KTBNAdaptiveLearner< GUM_SCALAR > & allowArcDeletions(bool allow=true) override
Forbid an arc from tailBase, lag slices before the kernel, to headBase in the kernel: tailBase at sli...
KTBNAdaptiveLearner< GUM_SCALAR > & erasePossibleEdge(std::string_view tailBase, int tailSlice, std::string_view headBase, int headSlice) override
Forbid an arc from tailBase, lag slices before the kernel, to headBase in the kernel: tailBase at sli...
KTBNAdaptiveLearner(std::string_view dirPath, std::string_view csvBaseName, Size nbSamples, Size kMax, const std::unordered_set< std::string > &atemporalVars, const std::vector< std::string > &missingSymbols={"?"}, bool induceTypes=true)
Constructor — the candidate orders are kMin..kMax.
KTBNAdaptiveLearner< GUM_SCALAR > & useSmoothingPrior(double weight=1.0) override
bool _ignoreMissingSymbols_
whether incomplete rows/instances are dropped (see ignoreMissingSymbols())
KTBNAdaptiveLearner< GUM_SCALAR > & addNoChildrenNode(std::string_view base, int slice) override
Forbid an arc from tailBase, lag slices before the kernel, to headBase in the kernel: tailBase at sli...
KTBNAdaptiveLearner< GUM_SCALAR > & addPossibleEdge(std::string_view tailBase, int tailSlice, std::string_view headBase, int headSlice) override
Forbid an arc from tailBase, lag slices before the kernel, to headBase in the kernel: tailBase at sli...
std::set< std::pair< std::string, std::string > > _forbiddenArcsAllSlices_
forbidden all-slices arcs, as (tailBase, headBase) pairs
KTBNAdaptiveLearner< GUM_SCALAR > & addForbiddenIntraSliceArc(std::string_view tailBase, std::string_view headBase) override
Forbid an arc from tailBase, lag slices before the kernel, to headBase in the kernel: tailBase at sli...
std::set< std::pair< std::string, std::string > > _mandatoryArcs_
mandatory arcs, as (tail, head) engine-name pairs
std::set< std::pair< std::string, std::string > > _possibleEdges_
MIIC candidate edges, as (tail, head) engine-name pairs.
const std::vector< std::pair< Size, double > > & scorePerCandidateK() const
Per-candidate cross-k scores from the last learnKTBN() call, as (k, score) pairs for k = kMin....
const std::unordered_set< std::string > & _atemporalVarNames_() const override
atemporal base names for IKTBNLearner's shared encode/_determineNode_; the set recorded at constructi...
IBNLearner::ScoreType _score_
recorded per-k structure score (the inner score, replayed on each candidate)
KTBNAdaptiveLearner< GUM_SCALAR > & useScoreBDeu() override
std::set< std::pair< std::string, std::string > > _forbiddenArcs_
forbidden arcs, as (tail, head) engine-name pairs
std::set< std::pair< std::string, std::string > > _forbiddenIntraSliceArcs_
forbidden intra-slice arcs, as (tailBase, headBase) pairs
KTBNAdaptiveLearner< GUM_SCALAR > & addForbiddenArc(std::string_view tailNode, std::string_view headNode) override
Forbid an arc from tailBase, lag slices before the kernel, to headBase in the kernel: tailBase at sli...
Size bestK() const
Order k selected by the last learnKTBN() call.
bool isIgnoringMissingSymbols() const
Whether incomplete rows and instances are dropped. False by default.
Size _nbSamples_
number of CSV files to read
KTBNAdaptiveLearner< GUM_SCALAR > & useScoreMDL() override
double _log2Likelihood_(const KTBN< GUM_SCALAR > &net) const
Factorized log2-likelihood of net over the recorded trajectories, comparable across candidate k (the ...
KTBNAdaptiveLearner< GUM_SCALAR > & eraseForbiddenKernelArc(std::string_view tailBase, int lag, std::string_view headBase)
Undo a previous addForbiddenKernelArc (same (tailBase, lag, headBase) triple).
KTBNAdaptiveLearner< GUM_SCALAR > & useScoreAIC() override
void _applyConfig_(KTBNLearner< GUM_SCALAR > &learner) const
Apply the recorded score / algorithm / correction / prior onto a freshly-built learner....
KTBNAdaptiveLearner< GUM_SCALAR > & eraseMandatoryKernelArc(std::string_view tailBase, int lag, std::string_view headBase)
Undo a previous addMandatoryKernelArc (same (tailBase, lag, headBase) triple).
KTBNAdaptiveLearner< GUM_SCALAR > & useExtendedGreedyHillClimbing() override
Size _kMin_
smallest order worth exploring (candidates are kMin..kMax). Starts at 2 and is recomputed by recomput...
std::set< std::tuple< std::string, std::string, int > > _mandatoryKernelArcs_
mandatory kernel-relative arcs, same shape as forbiddenKernelArcs
log_2 where the penalty replaces BIC s KTBNAdaptiveLearner< GUM_SCALAR > & useOrderScorefNML()
by a sum of per-node, per-parent-configuration multinomial parametric complexities (regret) – data-de...
KTBNAdaptiveLearner< GUM_SCALAR > & eraseForbiddenArc(std::string_view tailNode, std::string_view headNode) override
Forbid an arc from tailBase, lag slices before the kernel, to headBase in the kernel: tailBase at sli...
void _recomputeKMin_()
Recompute kMin from scratch: max(2, 1 + the largest concrete slice named by any recorded forbidden/ma...
KTBNAdaptiveLearner< GUM_SCALAR > & useMDLCorrection() override
std::vector< std::pair< Size, double > > _scorePerCandidateK_
per-candidate (k, order-score) pairs from the last learnKTBN() run, in ascending k....
VariableLog2ParamComplexity _ctable_
cache of log2 of the multinomial parametric complexity C^r_n, used by the fNML order penalty....
double _countParameters_(const KTBN< GUM_SCALAR > &net) const
Number of free parameters of net's template: summed over every template node (all initial slices,...
KTBNAdaptiveLearner< GUM_SCALAR > & allowArcReversals(bool allow=true) override
Forbid an arc from tailBase, lag slices before the kernel, to headBase in the kernel: tailBase at sli...
std::vector< std::tuple< std::string, std::string, std::string > > state() const
The recorded configuration as (key, value, comment) tuples (mirrors KTBNLearner::state()); toString()...
KTBN< GUM_SCALAR > learnKTBN() override
Learns the best k in [kMin, kMax] together with the structure and the CPTs: one KTBNLearner is built ...
KTBNAdaptiveLearner< GUM_SCALAR > & useScoreLog2Likelihood() override
std::unordered_set< std::string > _baseNames_
all base variable names (temporal + atemporal), read from the first trajectory CSV header at construc...
KTBNAdaptiveLearner< GUM_SCALAR > & eraseNoParentNode(std::string_view base, int slice) override
Forbid an arc from tailBase, lag slices before the kernel, to headBase in the kernel: tailBase at sli...
KTBNAdaptiveLearner< GUM_SCALAR > & ignoreMissingSymbols(bool ignore=true)
Learn and score on the fully observed data only, dropping every row and every scoring instance that c...
void _raiseKMinForSlice_(int slice)
Raise kMin, if needed, so that kMin > slice: a smaller candidate would silently drop a constraint nam...
KTBNAdaptiveLearner< GUM_SCALAR > & useOrderScoreAIC()
Select k by AIC: keep the k whose learned model maximises (a lighter, sample-size-independent comple...
void _verifyBase_(std::string_view base, int slice) const
Throw InvalidArgument unless base is a known base variable and slice is valid for it: base must be in...
std::string _csvBaseName_
stem of each trajectory file name
Size _tabuSize_
tabu-list parameters (meaningful when algo is LOCAL_SEARCH_WITH_TABU_LIST)
KTBNAdaptiveLearner< GUM_SCALAR > & useLocalSearchWithTabuList(Size tabu_size=100, Size nb_decrease=2) override
static std::unordered_set< std::string > _inferAtemporalVars_(std::string_view dirPath, std::string_view csvBaseName, Size nbSamples, Size kMax, const std::vector< std::string > &missingSymbols)
checks kMax >= 2 and nbSamples >= 1, then delegates to the shared IKTBNLearner::scanConstantColumns()...
KTBNAdaptiveLearner< GUM_SCALAR > & setMaxIndegree(Size max_indegree) override
Forbid an arc from tailBase, lag slices before the kernel, to headBase in the kernel: tailBase at sli...
KTBNAdaptiveLearner< GUM_SCALAR > & useScoreBD() override
KTBNAdaptiveLearner< GUM_SCALAR > & addNoParentNode(std::string_view base, int slice) override
Forbid an arc from tailBase, lag slices before the kernel, to headBase in the kernel: tailBase at sli...
KTBNAdaptiveLearner< GUM_SCALAR > & useScoreBIC() override
CorrectedMutualInformation::KModeTypes _correction_
recorded MIIC correction
KTBNAdaptiveLearner< GUM_SCALAR > & addForbiddenKernelArc(std::string_view tailBase, int lag, std::string_view headBase)
Forbid an arc from tailBase, lag slices before the kernel, to headBase in the kernel: tailBase at sli...
KTBNAdaptiveLearner< GUM_SCALAR > & useNoCorrection() override
KTBNAdaptiveLearner< GUM_SCALAR > & useMIIC() override
KTBNAdaptiveLearner< GUM_SCALAR > & eraseNoChildrenNode(std::string_view base, int slice) override
Forbid an arc from tailBase, lag slices before the kernel, to headBase in the kernel: tailBase at sli...
std::set< std::tuple< std::string, std::string, int > > _forbiddenKernelArcs_
forbidden kernel-relative arcs, as (tailBase, headBase, lag) triples: tailBase at slice k-1-lag -> he...
std::string checkScorePriorCompatibility() const
Warning string if the recorded score and prior are incompatible, empty otherwise. Data-free: it evalu...
Learns a k-TBN (structure and/or parameters) from trajectory CSVs.
KTBNLearner< GUM_SCALAR > & useNMLCorrection() override
Engine-name pairs (tail, head) of arcs flagged as hiding a latent variable by MIIC (merged from all i...
KTBNLearner< GUM_SCALAR > & addForbiddenArc(std::string_view tailNode, std::string_view headNode) override
Forbid tailNode from ever parenting headNode (engine names, e.g. "X[1]", "C").
KTBNLearner< GUM_SCALAR > & useScoreAIC() override
Returns a warning string if the current score and prior are incompatible, empty string otherwise.
KTBNLearner< GUM_SCALAR > & useGreedyHillClimbing() override
KTBNLearner< GUM_SCALAR > & useScoreLog2Likelihood() override
Returns a warning string if the current score and prior are incompatible, empty string otherwise.
KTBNLearner< GUM_SCALAR > & addForbiddenArcAllSlices(std::string_view tailBase, std::string_view headBase) override
Forbid tailBase -> headBase at every causally-possible slice pair (every lag, not just matching slice...
KTBNLearner< GUM_SCALAR > & useScoreBD() override
Returns a warning string if the current score and prior are incompatible, empty string otherwise.
KTBNLearner< GUM_SCALAR > & allowArcReversals(bool allow=true) override
Allow or forbid arc reversals during structure search.
KTBNLearner< GUM_SCALAR > & addNoChildrenNode(std::string_view base, int slice) override
Declare a single (base, slice) node as a leaf (no children).
KTBNLearner< GUM_SCALAR > & allowArcDeletions(bool allow=true) override
Allow or forbid arc deletions during structure search.
KTBNLearner< GUM_SCALAR > & useExtendedGreedyHillClimbing() override
KTBNLearner< GUM_SCALAR > & useNoCorrection() override
Engine-name pairs (tail, head) of arcs flagged as hiding a latent variable by MIIC (merged from all i...
KTBNLearner< GUM_SCALAR > & allowArcAdditions(bool allow=true) override
Allow or forbid arc additions during structure search.
KTBNLearner< GUM_SCALAR > & useScoreBIC() override
Returns a warning string if the current score and prior are incompatible, empty string otherwise.
void useScorefNML() override
Returns a warning string if the current score and prior are incompatible, empty string otherwise.
KTBNLearner< GUM_SCALAR > & useScoreMDL() override
Returns a warning string if the current score and prior are incompatible, empty string otherwise.
KTBNLearner< GUM_SCALAR > & addMandatoryArc(std::string_view tailNode, std::string_view headNode) override
Force tailNode to be a parent of headNode (engine names, e.g. "X[1]", "C").
KTBNLearner< GUM_SCALAR > & setMaxIndegree(Size max_indegree) override
Cap the number of parents of any single node.
KTBNLearner< GUM_SCALAR > & addForbiddenIntraSliceArc(std::string_view tailBase, std::string_view headBase) override
Forbid tailBase -> headBase at every intra-slice position (i.e. tailBase[t] -> headBase[t] for all t ...
KTBNLearner< GUM_SCALAR > & addPossibleEdge(std::string_view tailBase, int tailSlice, std::string_view headBase, int headSlice) override
Add a candidate edge for MIIC (only edges explicitly listed are explored).
KTBNLearner< GUM_SCALAR > & useSmoothingPrior(double weight=1.0) override
KTBNLearner< GUM_SCALAR > & addNoParentNode(std::string_view base, int slice) override
Declare a single (base, slice) node as a root (no parents).
KTBNLearner< GUM_SCALAR > & useLocalSearchWithTabuList(Size tabu_size=100, Size nb_decrease=2) override
std::string isPriorCompatible() const final
indicates whether the prior is compatible (meaningful) with the score
std::string isPriorCompatible() const final
indicates whether the prior is compatible (meaningful) with the score
std::string isPriorCompatible() const final
indicates whether the prior is compatible (meaningful) with the score
std::string isPriorCompatible() const final
indicates whether the prior is compatible (meaningful) with the score
std::string isPriorCompatible() const final
indicates whether the prior is compatible (meaningful) with the score
std::string isPriorCompatible() const final
indicates whether the prior is compatible (meaningful) with the score
#define GUM_ERROR(type, msg)
Definition exceptions.h:76
std::size_t Size
In aGrUM, hashed values are unsigned long int.
Definition types.h:74
Size Idx
Type for indexes.
Definition types.h:79
Size NodeId
Type for node ids.
Header files of gum::Instantiation.
include the inlined functions if necessary
Definition CSVParser.h:55
STL namespace.
the base class for all a priori
the class for computing AIC scores
the class for computing Bayesian Dirichlet (BD) log2 scores
the class for computing BDeu scores
the class for computing BIC scores
the class for computing Log2-likelihood scores
the class for computing MDL scores
the class for computing fNML scores