← Documentation
ADS-IDAC Engine 5.0.0
factor.cpp
Go to the documentation of this file.
3
4namespace bncore {
5
6Factor::Factor(const std::vector<NodeId> &scope,
7 const std::vector<std::size_t> &state_sizes)
8 : scope_(scope), tensor_(state_sizes) {
9 if (scope.size() != state_sizes.size() &&
10 scope.size() + 1 != state_sizes.size())
11 throw std::invalid_argument("state_sizes must either match scope size or "
12 "include a batch dimension.");
13}
14
15Factor::Factor(const std::vector<NodeId> &scope,
16 const std::vector<std::size_t> &state_sizes,
17 double *preallocated_ptr)
18 : scope_(scope), tensor_(state_sizes, preallocated_ptr) {
19 if (scope.size() != state_sizes.size() &&
20 scope.size() + 1 != state_sizes.size())
21 throw std::invalid_argument("state_sizes must either match scope size or "
22 "include a batch dimension.");
23}
24
25void Factor::bind_data(double *ptr) { tensor_.bind_data(ptr); }
26
27Factor Factor::multiply(const Factor &other, BumpAllocator *allocator) const {
28 std::vector<NodeId> new_scope = scope_;
29 std::vector<std::size_t> new_sizes;
30 for (std::size_t i = 0; i < scope_.size(); ++i)
31 new_sizes.push_back(tensor_.shape()[i]);
32
33 std::vector<int> b_in_c(other.scope_.size(), -1);
34 for (std::size_t j = 0; j < other.scope_.size(); ++j) {
35 auto it = std::find(new_scope.begin(), new_scope.end(), other.scope_[j]);
36 if (it == new_scope.end()) {
37 new_scope.push_back(other.scope_[j]);
38 new_sizes.push_back(other.tensor_.shape()[j]);
39 b_in_c[j] = new_scope.size() - 1;
40 } else {
41 b_in_c[j] = std::distance(new_scope.begin(), it);
42 }
43 }
44
45 bool a_batched = (scope_.size() + 1 == tensor_.shape().size());
46 bool b_batched = (other.scope_.size() + 1 == other.tensor_.shape().size());
47 std::size_t batch_size = a_batched
48 ? tensor_.shape().back()
49 : (b_batched ? other.tensor_.shape().back() : 1);
50
51 if (a_batched || b_batched) {
52 new_sizes.push_back(batch_size);
53 }
54
55 std::size_t num_elements = std::accumulate(new_sizes.begin(), new_sizes.end(),
56 1ULL, std::multiplies<>());
57 Factor result = allocator ? Factor(new_scope, new_sizes,
58 allocator->allocate(num_elements))
59 : Factor(new_scope, new_sizes);
60
61 std::size_t num_states_C = num_elements / batch_size;
62
63 // Precompute strides for C, A, B
64 const std::size_t c_ndim = new_scope.size();
65 const std::size_t a_ndim = scope_.size();
66 const std::size_t b_ndim = other.scope_.size();
67
68 std::vector<std::size_t> c_strides(c_ndim);
69 if (c_ndim > 0) {
70 c_strides[c_ndim - 1] = 1;
71 for (int i = static_cast<int>(c_ndim) - 2; i >= 0; --i)
72 c_strides[i] = c_strides[i + 1] * new_sizes[i + 1];
73 }
74
75 std::vector<std::size_t> a_strides(a_ndim);
76 if (a_ndim > 0) {
77 a_strides[a_ndim - 1] = 1;
78 for (int i = static_cast<int>(a_ndim) - 2; i >= 0; --i)
79 a_strides[i] = a_strides[i + 1] * tensor_.shape()[i + 1];
80 }
81
82 std::vector<std::size_t> b_strides(b_ndim);
83 if (b_ndim > 0) {
84 b_strides[b_ndim - 1] = 1;
85 for (int i = static_cast<int>(b_ndim) - 2; i >= 0; --i)
86 b_strides[i] = b_strides[i + 1] * other.tensor_.shape()[i + 1];
87 }
88
89 // Precompute index mapping tables: for each c_idx → a_idx, b_idx
90 std::vector<std::size_t> map_a(num_states_C);
91 std::vector<std::size_t> map_b(num_states_C);
92
93 for (std::size_t c_idx = 0; c_idx < num_states_C; ++c_idx) {
94 // Unravel c_idx using precomputed strides
95 std::size_t a_idx = 0;
96 std::size_t b_idx = 0;
97 std::size_t remainder = c_idx;
98
99 for (std::size_t d = 0; d < c_ndim; ++d) {
100 std::size_t coord = remainder / c_strides[d];
101 remainder -= coord * c_strides[d];
102
103 // A is prefix: dims 0..a_ndim-1 map directly
104 if (d < a_ndim) {
105 a_idx += coord * a_strides[d];
106 }
107
108 // B maps via b_in_c
109 for (std::size_t j = 0; j < b_ndim; ++j) {
110 if (b_in_c[j] == static_cast<int>(d)) {
111 b_idx += coord * b_strides[j];
112 break;
113 }
114 }
115 }
116 map_a[c_idx] = a_idx;
117 map_b[c_idx] = b_idx;
118 }
119
120 // Execute multiply using precomputed maps
121 double *c_data = result.tensor().data();
122 const double *a_data = tensor_.data();
123 const double *b_data = other.tensor().data();
124
125 if (batch_size == 1) {
126 // Scalar fast path — no batch loop overhead
127 for (std::size_t c_idx = 0; c_idx < num_states_C; ++c_idx) {
128 double val_a = a_data[map_a[c_idx]];
129 double val_b = b_data[map_b[c_idx]];
130 c_data[c_idx] = val_a * val_b;
131 }
132 } else {
133 for (std::size_t c_idx = 0; c_idx < num_states_C; ++c_idx) {
134 double *c_ptr = c_data + c_idx * batch_size;
135 const double *a_ptr =
136 a_data + map_a[c_idx] * (a_batched ? batch_size : 1);
137 const double *b_ptr =
138 b_data + map_b[c_idx] * (b_batched ? batch_size : 1);
139
140#pragma omp simd
141 for (std::size_t b = 0; b < batch_size; ++b) {
142 double val_a = a_batched ? a_ptr[b] : a_ptr[0];
143 double val_b = b_batched ? b_ptr[b] : b_ptr[0];
144 c_ptr[b] = val_a * val_b;
145 }
146 }
147 }
148 return result;
149}
150
151Factor Factor::marginalize(const std::vector<NodeId> &marg_vars,
152 BumpAllocator *allocator) const {
153 std::vector<NodeId> new_scope;
154 std::vector<std::size_t> new_sizes;
155 std::vector<int> c_in_a;
156 for (std::size_t i = 0; i < scope_.size(); ++i) {
157 if (std::find(marg_vars.begin(), marg_vars.end(), scope_[i]) ==
158 marg_vars.end()) {
159 new_scope.push_back(scope_[i]);
160 new_sizes.push_back(tensor_.shape()[i]);
161 c_in_a.push_back(i);
162 }
163 }
164
165 bool is_batched = (scope_.size() + 1 == tensor_.shape().size());
166 std::size_t batch_size = is_batched ? tensor_.shape().back() : 1;
167
168 if (is_batched) {
169 new_sizes.push_back(batch_size);
170 }
171
172 std::size_t num_elements = std::accumulate(new_sizes.begin(), new_sizes.end(),
173 1ULL, std::multiplies<>());
174 Factor result = allocator ? Factor(new_scope, new_sizes,
175 allocator->allocate(num_elements))
176 : Factor(new_scope, new_sizes);
177
178 if (allocator) {
179 result.tensor().fill(0.0);
180 }
181
182 std::size_t num_states_A = tensor_.size() / batch_size;
183
184 // Precompute strides for A and C
185 const std::size_t a_ndim = scope_.size();
186 const std::size_t c_ndim = new_scope.size();
187
188 std::vector<std::size_t> a_strides(a_ndim);
189 if (a_ndim > 0) {
190 a_strides[a_ndim - 1] = 1;
191 for (int i = static_cast<int>(a_ndim) - 2; i >= 0; --i)
192 a_strides[i] = a_strides[i + 1] * tensor_.shape()[i + 1];
193 }
194
195 std::vector<std::size_t> c_strides(c_ndim);
196 if (c_ndim > 0) {
197 c_strides[c_ndim - 1] = 1;
198 for (int i = static_cast<int>(c_ndim) - 2; i >= 0; --i)
199 c_strides[i] = c_strides[i + 1] * new_sizes[i + 1];
200 }
201
202 // Precompute a_idx → c_idx mapping
203 std::vector<std::size_t> map_c(num_states_A);
204 for (std::size_t a_idx = 0; a_idx < num_states_A; ++a_idx) {
205 std::size_t c_idx = 0;
206 std::size_t remainder = a_idx;
207
208 for (std::size_t d = 0; d < a_ndim; ++d) {
209 std::size_t coord = remainder / a_strides[d];
210 remainder -= coord * a_strides[d];
211
212 // Map only retained dimensions
213 for (std::size_t k = 0; k < c_ndim; ++k) {
214 if (c_in_a[k] == static_cast<int>(d)) {
215 c_idx += coord * c_strides[k];
216 break;
217 }
218 }
219 }
220 map_c[a_idx] = c_idx;
221 }
222
223 // Execute marginalize using precomputed maps
224 double *c_data = result.tensor().data();
225 const double *a_data = tensor_.data();
226
227 if (batch_size == 1) {
228 // Scalar fast path
229 for (std::size_t a_idx = 0; a_idx < num_states_A; ++a_idx) {
230 c_data[map_c[a_idx]] += a_data[a_idx];
231 }
232 } else {
233 for (std::size_t a_idx = 0; a_idx < num_states_A; ++a_idx) {
234 double *c_ptr = c_data + map_c[a_idx] * batch_size;
235 const double *a_ptr = a_data + a_idx * batch_size;
236
237#pragma omp simd
238 for (std::size_t b = 0; b < batch_size; ++b) {
239 c_ptr[b] += a_ptr[b];
240 }
241 }
242 }
243 return result;
244}
245
246} // namespace bncore
bncore::BumpAllocator, a fixed-capacity arena of doubles for temporary factor storage.
Fixed-capacity scratch buffer of doubles with pointer-bump allocation.
double * allocate(std::size_t num_doubles)
Reserves the next num_doubles doubles of the buffer.
double * data()
Pointer to the first element: the borrowed buffer when one is bound, otherwise the owned storage.
const std::vector< std::size_t > & shape() const
Extent of each dimension, as given at construction.
void bind_data(double *ptr)
Redirects the tensor to a caller buffer.
void fill(double value)
Sets every element to value.
std::size_t size() const
Total number of elements, the product of the shape.
A potential over an ordered scope of variables.
Definition: factor.hpp:22
Factor multiply(const Factor &other, class BumpAllocator *allocator=nullptr) const
Product of two factors.
Definition: factor.cpp:27
DenseTensor tensor_
Definition: factor.hpp:96
Factor(const std::vector< NodeId > &scope, const std::vector< std::size_t > &state_sizes)
Creates a factor that owns zero-initialized storage.
Definition: factor.cpp:6
void bind_data(double *ptr)
Redirects the value table to a caller buffer. See DenseTensor::bind_data().
Definition: factor.cpp:25
std::vector< NodeId > scope_
Definition: factor.hpp:95
const std::vector< NodeId > & scope() const
Variables of the factor, in tensor dimension order.
Definition: factor.hpp:45
Factor marginalize(const std::vector< NodeId > &marg_vars, class BumpAllocator *allocator=nullptr) const
Sums variables out of the factor.
Definition: factor.cpp:151
DenseTensor & tensor()
The value table.
Definition: factor.hpp:83
bncore::Factor, a table of doubles over a list of variables, with the product and sum-out operations ...