aGrUM 3.2.0
a C++ library for (probabilistic) graphical models
KTBNDatabaseGenerator_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 <algorithm>
52#include <cctype>
53#include <cmath>
54#include <filesystem>
55#include <fstream>
56#include <numeric>
57#include <optional>
58#include <queue>
59#include <sstream>
60#include <string>
61#include <vector>
62
65
67#include <unordered_map>
68
69namespace gum::learning {
70
71 template < GUM_Numeric GUM_SCALAR >
75
76 template < GUM_Numeric GUM_SCALAR >
78 _template_(kdbn.toBN()), _k_(kdbn.k()) {
79 GUM_CONSTRUCTOR(KTBNDatabaseGenerator)
80 _build_(kdbn);
81 }
82
83 template < GUM_Numeric GUM_SCALAR >
87
88 template < GUM_Numeric GUM_SCALAR >
89 std::pair< std::string, int > KTBNDatabaseGenerator< GUM_SCALAR >::_decode_(
90 const std::string& name,
91 const std::unordered_set< std::string >& temporalSet) {
92 if (!name.empty() && name.back() == ']') {
93 const auto p = name.rfind('[');
94 if (p != std::string::npos && p + 1 < name.size() - 1) {
95 const std::string base = name.substr(0, p);
96 const std::string digits = name.substr(p + 1, name.size() - p - 2);
97 if (temporalSet.contains(base)
98 && std::all_of(digits.begin(), digits.end(), [](unsigned char c) {
99 return std::isdigit(c) != 0;
100 }))
101 return {base, std::stoi(digits)};
102 }
103 }
104 return {name, KTBN< GUM_SCALAR >::ATEMPORAL};
105 }
106
107 template < GUM_Numeric GUM_SCALAR >
108 void KTBNDatabaseGenerator< GUM_SCALAR >::_build_(const KTBN< GUM_SCALAR >& kdbn) {
109 const std::unordered_set< std::string >& temporalSet = kdbn.temporalVarNames();
110
111 // 1) canonical column numbering: one column per base variable (temporal
112 // variables first, then atemporal ones).
113 std::unordered_map< std::string, Idx > colByName; // local: only needed during construction
114 _nbVars_ = 0;
115 for (const std::string& base: temporalSet) {
116 _baseCols_.push_back(base);
117 colByName.emplace(base, _nbVars_++);
118 }
119 for (const std::string& base: kdbn.atemporalVarNames()) {
120 _baseCols_.push_back(base);
121 colByName.emplace(base, _nbVars_++);
122 }
123
124 // 2) precompile every template node (and its parents) in topological order,
125 // so that drawSamples() never has to parse a name again. In the same pass we
126 // pick one representative variable per column (_vars_, for label rendering)
127 // and build the shared instantiation (_inst_, reused for every draw). All
128 // cached pointers refer to _template_ (our own copy), so they outlive the
129 // source k-DBN.
130 _vars_.resize(_nbVars_);
131 const int lastSlice = int(_k_) - 1;
132 for (const NodeId n: _template_.topologicalOrder()) {
133 const DiscreteVariable& v = _template_.variable(n);
134 const Tensor< GUM_SCALAR >& cpt = _template_.cpt(n);
135 const auto [base, slice] = _decode_(v.name(), temporalSet);
136
137 NodeRef nr;
138 nr.var = &v;
139 nr.cpt = &cpt;
140 nr.slice = slice;
141 nr.col = colByName[base];
142
143 for (Idx i = 1; i < cpt.nbrDim(); ++i) {
144 const DiscreteVariable& pv = cpt.variable(i);
145 const auto [pbase, pslice] = _decode_(pv.name(), temporalSet);
146 const bool pAtemporal = (pslice == KTBN< GUM_SCALAR >::ATEMPORAL);
147
148 ParentRef pr;
149 pr.var = &pv;
150 pr.col = colByName[pbase];
151 pr.isAtemporal = pAtemporal;
152 pr.lag = pAtemporal ? KTBN< GUM_SCALAR >::ATEMPORAL : slice - pslice;
153 nr.parents.push_back(std::move(pr));
154 }
155
156 _vars_[nr.col] = &v;
157 _inst_.add(v);
158 if (slice == lastSlice) _kernel_.push_back(_nodes_.size());
159 _nodes_.push_back(std::move(nr));
160 }
161 }
162
163 template < GUM_Numeric GUM_SCALAR >
165 const Tensor< GUM_SCALAR >& cpt,
166 double& log2likelihood) {
167 const double threshold = gum::randomProba();
168 double cumulProb = 0.0;
169 for (_inst_.setFirstVar(var); !_inst_.end(); _inst_.incVar(var)) {
170 cumulProb += cpt[_inst_];
171 if (cumulProb > threshold) break;
172 }
173 if (_inst_.end()) _inst_.setLastVar(var);
174 log2likelihood += std::log2(cpt[_inst_]);
175 return _inst_.val(var);
176 }
177
178 template < GUM_Numeric GUM_SCALAR >
179 std::vector< double >
181 Size nbTimeSlices,
182 std::string_view dirPath,
183 std::string_view csvBaseName,
184 VarOrderMode mode,
185 bool useLabels,
186 std::string csvSeparator) {
187 // fixed horizon: every trajectory shares nbTimeSlices (perTraj == nullptr)
188 return _drawSamples_(nbSamples,
189 nbTimeSlices,
190 nullptr,
191 dirPath,
192 csvBaseName,
193 mode,
194 useLabels,
195 csvSeparator);
196 }
197
198 template < GUM_Numeric GUM_SCALAR >
199 std::vector< double >
200 KTBNDatabaseGenerator< GUM_SCALAR >::drawSamples(const std::vector< Size >& nbTimeSlices,
201 std::string_view dirPath,
202 std::string_view csvBaseName,
203 VarOrderMode mode,
204 bool useLabels,
205 std::string csvSeparator) {
206 // per-trajectory horizons: nbTimeSlices[i] is trajectory i's length
207 return _drawSamples_(nbTimeSlices.size(),
208 0,
209 &nbTimeSlices,
210 dirPath,
211 csvBaseName,
212 mode,
213 useLabels,
214 csvSeparator);
215 }
216
227 template < GUM_Numeric GUM_SCALAR >
228 std::vector< double >
230 Size fixedLen,
231 const std::vector< Size >* perTraj,
232 std::string_view dirPath,
233 std::string_view csvBaseName,
234 VarOrderMode mode,
235 bool useLabels,
236 const std::string& csvSeparator) {
237 // horizon of trajectory i: from perTraj when given, else the shared fixedLen
238 const auto lengthAt = [&](Idx i) { return perTraj ? (*perTraj)[i] : fixedLen; };
239
240 // validate everything up front, before any file is created
241 if (csvSeparator.find('\n') != std::string::npos)
242 GUM_ERROR(InvalidArgument, "csvSeparator must not contain end-line characters")
243 for (Idx i = 0; i < nbSamples; ++i)
244 if (lengthAt(i) < _k_)
245 GUM_ERROR(OperationNotAllowed, "nbTimeSlices=" << lengthAt(i) << " must be >= k=" << _k_)
246
247 // decide the column order once (shared by every trajectory file):
248 // colOrder[i] is the canonical column written at output position i.
249 std::vector< Idx > colOrder;
250 switch (mode) {
251 case VarOrderMode::RANDOM : setVarOrderRandomized(colOrder); break;
254 default : GUM_ERROR(InvalidArgument, "unknown VarOrderMode")
255 }
256
257 const std::filesystem::path dir{dirPath};
258 const std::string stem{csvBaseName};
259 std::filesystem::create_directories(dir); // ensure the destination exists
260
261 std::vector< double > log2Ls;
262 log2Ls.reserve(nbSamples);
263
264 const bool hasListener = onProgress.hasListener();
265 std::optional< Timer > timer;
266 int progress = 0;
267 if (hasListener) {
268 timer.emplace();
269 GUM_EMIT2(onProgress, 0, 0.0);
270 }
271
272 // generate one trajectory at a time; each is sampled then written to its file
273 for (std::size_t i = 0; i < nbSamples; ++i) {
274 const Size T = lengthAt(i);
275
276 // flat trajectory buffer (row-major) in canonical column order: the value at
277 // time t, base column c is traj[t * _nbVars_ + c]. Sampling uses the cached
278 // integer columns (nr.col / pr.col) — no name lookup in the hot loop.
279 std::vector< Idx > traj(T * _nbVars_);
280 double log2L = 0;
281
282 // Phase 1: draw the first k time steps from scratch (no history available yet)
283 for (const NodeRef& nr: _nodes_) {
284 for (const ParentRef& pr: nr.parents) {
285 // an atemporal parent has the same value on every row, stored at row 0
286 const Size parentRow = pr.isAtemporal ? 0 : nr.slice - pr.lag;
287 _inst_.chgVal(*pr.var, traj[parentRow * _nbVars_ + pr.col]);
288 }
289 const Idx drawn = _drawVar_(*nr.var, *nr.cpt, log2L);
290 if (nr.slice == KTBN< GUM_SCALAR >::ATEMPORAL)
291 for (Size t = 0; t < T; ++t)
292 traj[t * _nbVars_ + nr.col] = drawn;
293 else traj[nr.slice * _nbVars_ + nr.col] = drawn;
294 }
295
296 // Phase 2: extend to T-1 by sliding the transition kernel forward one step at a time
297 for (Size t = _k_; t < T; ++t) {
298 for (const Idx kernelIdx: _kernel_) {
299 const NodeRef& nr = _nodes_[kernelIdx];
300 for (const ParentRef& pr: nr.parents) {
301 const Size parentRow = pr.isAtemporal ? 0 : t - pr.lag;
302 _inst_.chgVal(*pr.var, traj[parentRow * _nbVars_ + pr.col]);
303 }
304 traj[t * _nbVars_ + nr.col] = _drawVar_(*nr.var, *nr.cpt, log2L);
305 }
306 }
307
308 log2Ls.push_back(log2L);
309 const std::filesystem::path file = dir / (stem + std::to_string(i + 1) + ".csv");
310 _writeTrajectory_(file.string(), traj, T, useLabels, csvSeparator, colOrder);
311
312 if (hasListener) {
313 const int p = int((i * 100) / nbSamples);
314 if (p != progress) {
315 progress = p;
316 GUM_EMIT2(onProgress, progress, timer->step());
317 }
318 }
319 }
320
321 if (hasListener) {
322 std::stringstream ss;
323 ss << nbSamples << " trajectories generated in " << timer->step() << " s.";
324 GUM_EMIT1(onStop, ss.str());
325 }
326
327 return log2Ls;
328 }
329
330 template < GUM_Numeric GUM_SCALAR >
334
335 template < GUM_Numeric GUM_SCALAR >
339
340 template < GUM_Numeric GUM_SCALAR >
344
345 template < GUM_Numeric GUM_SCALAR >
347 const DiscreteVariable& v = *_vars_[col];
348 if (v.varType() == VarType::DISCRETIZED) {
349 switch (_discretizedLabelMode_) {
350 case DiscretizedLabelMode::MEDIAN : return std::to_string(v.numerical(idx));
352 return std::to_string(static_cast< const IDiscretizedVariable& >(v).draw(idx));
353 case DiscretizedLabelMode::INTERVAL : return v.label(idx);
354 default : GUM_ERROR(FatalError, "unknown DiscretizedLabelMode")
355 }
356 }
357 return v.label(idx);
358 }
359
360 template < GUM_Numeric GUM_SCALAR >
362 std::string_view csvFileURL,
363 const std::vector< Idx >& traj,
364 Size nbTimeSlices,
365 bool useLabels,
366 const std::string& csvSeparator,
367 const std::vector< Idx >& colOrder) const {
368 std::ofstream os(std::filesystem::path{csvFileURL}, std::ofstream::out);
369 if (!os) GUM_ERROR(IOError, "could not open '" << csvFileURL << "' for writing")
370
371 // header: one column per base variable, in the chosen output order
372 bool firstCol = true;
373 for (const Idx col: colOrder) {
374 if (!firstCol) os << csvSeparator;
375 os << _baseCols_[col];
376 firstCol = false;
377 }
378 os << "\n";
379
380 for (Size t = 0; t < nbTimeSlices; ++t) {
381 const Size base = t * _nbVars_;
382 firstCol = true;
383 for (const Idx col: colOrder) {
384 if (!firstCol) os << csvSeparator;
385 os << (useLabels ? _label_(col, traj[base + col]) : std::to_string(traj[base + col]));
386 firstCol = false;
387 }
388 os << "\n";
389 }
390 }
391
392 template < GUM_Numeric GUM_SCALAR >
394 std::vector< Idx >& colOrder) const {
395 colOrder.resize(_nbVars_);
396 std::iota(colOrder.begin(), colOrder.end(), 0);
397 std::shuffle(colOrder.begin(), colOrder.end(), gum::randomGenerator());
398 }
399
400 template < GUM_Numeric GUM_SCALAR >
402 std::vector< Idx >& colOrder) const {
403 // The column order is a topological sort of the "contemporaneous" DAG over
404 // base columns. Only two kinds of arc constrain the column order:
405 // * atemporal -> atemporal — orders the atemporal block
406 // * same-slice kernel arcs — lag-0 arcs at slice k-1; order the temporal block
407 // Lagged arcs are resolved at sampling time, not here; atemporal -> temporal
408 // arcs need no edge since every atemporal column already precedes every
409 // temporal one (the blocks are emitted in that order below).
410 std::vector< std::vector< Idx > > children(_nbVars_);
411 std::vector< int > indegree(_nbVars_, 0);
412 std::vector< bool > isAtemporal(_nbVars_, false);
413
414 auto addEdge = [&](Idx parent, Idx child) {
415 children[parent].push_back(child);
416 ++indegree[child];
417 };
418
419 // (1) build the contemporaneous DAG, one arc set per block.
420 const int lastSlice = int(_k_) - 1;
421 for (const NodeRef& nf: _nodes_) {
422 if (nf.slice == KTBN< GUM_SCALAR >::ATEMPORAL) {
423 isAtemporal[nf.col] = true;
424 for (const ParentRef& pr: nf.parents) {
425 if (!pr.isAtemporal)
427 "temporal variable '"
428 << _baseCols_[pr.col] << "' is a parent of atemporal variable '"
429 << _baseCols_[nf.col] << "' — violates the k-DBN invariant")
430 addEdge(pr.col, nf.col);
431 }
432 } else if (nf.slice == lastSlice) {
433 for (const ParentRef& pr: nf.parents)
434 if (!pr.isAtemporal && pr.lag == 0) addEdge(pr.col, nf.col);
435 }
436 }
437
438 // (2) Kahn's sort of one block, seeded in ascending col order so the result
439 // is deterministic (the seed is canonical and edges follow _nodes_).
440 auto topoSortBlock = [&](bool atempBlock) {
441 std::queue< Idx > q;
442 for (Idx c = 0; c < _nbVars_; ++c)
443 if (isAtemporal[c] == atempBlock && indegree[c] == 0) q.push(c);
444 while (!q.empty()) {
445 const Idx cur = q.front();
446 q.pop();
447 colOrder.push_back(cur);
448 for (const Idx ch: children[cur])
449 if (--indegree[ch] == 0) q.push(ch);
450 }
451 };
452
453 // (3) atemporal block first, then the temporal kernel order.
454 colOrder.clear();
455 topoSortBlock(true);
456 topoSortBlock(false);
457 }
458
459 template < GUM_Numeric GUM_SCALAR >
461 std::vector< Idx >& colOrder) const {
462 setVarOrderTopological(colOrder);
463 std::reverse(colOrder.begin(), colOrder.end());
464 }
465
466} // namespace gum::learning
A database generator for k-order dynamic Bayesian networks.
Base class for discrete random variable.
virtual double numerical(Idx indice) const =0
get a numerical representation of the indice-th value.
VarType varType() const override=0
returns the varType of variable
virtual std::string label(Idx i) const =0
get the indice-th label. This method is pure virtual.
Exception : fatal (unknown ?) error.
A base class for discretized variables, independent of the ticks type.
Exception : input/output problem.
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
Exception : operation not allowed.
Signaler< std::string_view > onStop
with a possible explanation for stopping
Signaler< Size, double > onProgress
Progression (percent) and time.
const std::string & name() const
returns the name of the variable
Idx _drawVar_(const DiscreteVariable &var, const Tensor< GUM_SCALAR > &cpt, double &log2likelihood)
inverse-CDF draw of var given the parents already set in inst; accumulates log2(P(drawn value)) into ...
Size _nbVars_
number of base variable columns
std::vector< const DiscreteVariable * > _vars_
one representative variable per base column (same order as baseCols), pointing into template so it ou...
void setVarOrderAntiTopological(std::vector< Idx > &colOrder) const
builds the reverse of setVarOrderTopological()
void setVarOrderTopological(std::vector< Idx > &colOrder) const
builds a topological column order (transition-kernel projection)
void setDiscretizedLabelModeInterval()
set discretized-label rendering to the interval label "[min,max["
std::vector< std::string > _baseCols_
col index -> base name (canonical column numbering)
static std::pair< std::string, int > _decode_(const std::string &name, const std::unordered_set< std::string > &temporalSet)
decodes a template node name into (base, slice): "B[t]" with B a known temporal process -> (B,...
void _writeTrajectory_(std::string_view csvFileURL, const std::vector< Idx > &traj, Size nbTimeSlices, bool useLabels, const std::string &csvSeparator, const std::vector< Idx > &colOrder) const
writes one trajectory CSV (header + T rows) to csvFileURL. traj is the flat row-major buffer (T x nbV...
void setDiscretizedLabelModeRandom()
set discretized-label rendering to a uniform random draw in the interval (this is the default; each l...
std::vector< Idx > _kernel_
indices, in nodes, of the slice-(k-1) nodes (the transition kernel, drives Phase 2); already in topol...
std::vector< NodeRef > _nodes_
all template nodes in topological order (drives Phase 1, the bootstrap)
void setDiscretizedLabelModeMedian()
set discretized-label rendering to the (deterministic) interval median
std::vector< double > _drawSamples_(Size nbSamples, Size fixedLen, const std::vector< Size > *perTraj, std::string_view dirPath, std::string_view csvBaseName, VarOrderMode mode, bool useLabels, const std::string &csvSeparator)
the single worker behind both drawSamples() overloads. Trajectory i's horizon is read from perTraj (w...
void _build_(const KTBN< GUM_SCALAR > &kdbn)
one-shot initialisation called by the constructor: fills the column index (baseCols,...
Instantiation _inst_
a shared instantiation over all template variables, so we don't have to rebuild it for every draw
KTBNDatabaseGenerator(const KTBN< GUM_SCALAR > &kdbn)
Constructor.
std::string _label_(Idx col, Idx idx) const
renders the label of modality idx of base column col (taking the discretized-label mode into account)
BayesNet< GUM_SCALAR > _template_
the -slice template (a small copy, independent of the horizon)
VarOrderMode
column order used for the exported CSV
void setVarOrderRandomized(std::vector< Idx > &colOrder) const
builds a uniformly random column order
Size nbVars() const
returns the number of base variable columns
DiscretizedLabelMode _discretizedLabelMode_
rendering of discretized variables when labels are requested
std::vector< double > drawSamples(Size nbSamples, Size nbTimeSlices, std::string_view dirPath, std::string_view csvBaseName, VarOrderMode mode=VarOrderMode::RANDOM, bool useLabels=true, std::string csvSeparator=",")
Generates nbSamples independent trajectories, writing one CSV file per trajectory into dirPath.
#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.
GUM_SHARED_PUBLIC std::mt19937 & randomGenerator()
define a random_engine with correct seed
GUM_SHARED_PUBLIC double randomProba()
Returns a random double between 0 and 1 included (i.e.
include the inlined functions if necessary
Definition CSVParser.h:55
#define GUM_EMIT2(signal, arg1, arg2)
Definition signaler.h:290
#define GUM_EMIT1(signal, arg1)
Definition signaler.h:289
a template node, precompiled for fast sampling
int slice
its template slice (ATEMPORAL if static)
const DiscreteVariable * var
the node variable (in template)
std::vector< ParentRef > parents
its parents (drives sampling and topology)
const Tensor< GUM_SCALAR > * cpt
its CPT (in template)
a parent of a template node, precompiled for fast sampling
bool isAtemporal
whether the parent is atemporal
const DiscreteVariable * var
the parent variable (in template)
int lag
time steps back: parentTime = nodeTime - lag
Contains useful methods for random stuff.