ESPResSo
Extensible Simulation Package for Research on Soft Matter Systems
Loading...
Searching...
No Matches
Correlator.cpp
Go to the documentation of this file.
1/*
2 * Copyright (C) 2010-2026 The ESPResSo project
3 *
4 * This file is part of ESPResSo.
5 *
6 * ESPResSo is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * ESPResSo is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19#include "Correlator.hpp"
20
21#include <utils/Vector.hpp>
22#include <utils/math/sqr.hpp>
24
25#include <boost/archive/binary_iarchive.hpp>
26#include <boost/archive/binary_oarchive.hpp>
27#include <boost/iostreams/device/array.hpp>
28#include <boost/iostreams/stream.hpp>
29#include <boost/serialization/string.hpp>
30#include <boost/serialization/vector.hpp>
31
32#include <algorithm>
33#include <array>
34#include <cassert>
35#include <cmath>
36#include <cstddef>
37#include <functional>
38#include <numeric>
39#include <sstream>
40#include <stdexcept>
41#include <string>
42#include <vector>
43
44namespace Accumulators {
45/** Compress computing arithmetic mean: A_compressed=(A1+A2)/2 */
46std::vector<double> compress_linear(std::vector<double> const &A1,
47 std::vector<double> const &A2) {
48 assert(A1.size() == A2.size());
49 std::vector<double> A_compressed(A1.size());
50
51 std::ranges::transform(A1, A2, A_compressed.begin(),
52 [](double a, double b) { return 0.5 * (a + b); });
53
54 return A_compressed;
55}
56
57/** Compress discarding the 1st argument and return the 2nd */
58std::vector<double>
59compress_discard1([[maybe_unused]] std::vector<double> const &A1,
60 [[maybe_unused]] std::vector<double> const &A2) {
61 assert(A1.size() == A2.size());
62 std::vector<double> A_compressed(A2);
63 return A_compressed;
64}
65
66/** Compress discarding the 2nd argument and return the 1st */
67std::vector<double>
68compress_discard2([[maybe_unused]] std::vector<double> const &A1,
69 [[maybe_unused]] std::vector<double> const &A2) {
70 assert(A1.size() == A2.size());
71 std::vector<double> A_compressed(A1);
72 return A_compressed;
73}
74
75std::vector<double> scalar_product(std::vector<double> const &A,
76 std::vector<double> const &B,
77 Utils::Vector3d const &) {
78 if (A.size() != B.size()) {
79 throw std::runtime_error(
80 "Error in scalar product: The vector sizes do not match");
81 }
82
83 auto const result = std::inner_product(A.begin(), A.end(), B.begin(), 0.0);
84 return {result};
85}
86
87std::vector<double> componentwise_product(std::vector<double> const &A,
88 std::vector<double> const &B,
89 Utils::Vector3d const &) {
90 std::vector<double> C(A.size());
91 if (A.size() != B.size()) {
92 throw std::runtime_error(
93 "Error in componentwise product: The vector sizes do not match");
94 }
95
96 std::ranges::transform(A, B, C.begin(), std::multiplies<>());
97
98 return C;
99}
100
101std::vector<double> tensor_product(std::vector<double> const &A,
102 std::vector<double> const &B,
103 Utils::Vector3d const &) {
104 std::vector<double> C(A.size() * B.size());
105 auto C_it = C.begin();
106
107 for (double a : A) {
108 for (double b : B) {
109 *(C_it++) = a * b;
110 }
111 }
112
113 return C;
114}
115
116std::vector<double> square_distance_componentwise(std::vector<double> const &A,
117 std::vector<double> const &B,
118 Utils::Vector3d const &) {
119 if (A.size() != B.size()) {
120 throw std::runtime_error(
121 "Error in square distance componentwise: The vector sizes do not "
122 "match.");
123 }
124
125 std::vector<double> C(A.size());
126
127 std::ranges::transform(A, B, C.begin(), [](double a, double b) -> double {
128 return Utils::sqr(a - b);
129 });
130
131 return C;
132}
133
134// note: the argument name wsquare denotes that its value is w^2 while the user
135// sets w
136std::vector<double> fcs_acf(std::vector<double> const &A,
137 std::vector<double> const &B,
138 Utils::Vector3d const &wsquare) {
139 if (A.size() != B.size()) {
140 throw std::runtime_error(
141 "Error in fcs_acf: The vector sizes do not match.");
142 }
143
144 auto const C_size = A.size() / 3u;
145 assert(3u * C_size == A.size());
146
147 std::vector<double> C{};
148 C.reserve(C_size);
149
150 for (std::size_t i = 0u; i < C_size; i++) {
151 auto acc = 0.;
152 for (std::size_t j = 0u; j < 3u; j++) {
153 auto const a = A[3u * i + j];
154 auto const b = B[3u * i + j];
155 acc -= Utils::sqr(a - b) / wsquare[j];
156 }
157 C.emplace_back(std::exp(acc));
158 }
159
160 return C;
161}
162
163void Correlator::initialize_operations() {
164 // Class members are assigned via the initializer list
165
166 if (m_tau_lin == 1) { // use the default
167 m_tau_lin = static_cast<int>(std::ceil(m_tau_max / m_dt));
168 m_tau_lin += m_tau_lin % 2;
169 }
170
171 if (m_tau_lin < 2) {
172 throw std::runtime_error("tau_lin must be >= 2");
173 }
174
175 if (m_tau_lin % 2) {
176 throw std::runtime_error("tau_lin must be divisible by 2");
177 }
178
179 if (m_tau_max <= m_dt) {
180 throw std::runtime_error("tau_max must be >= delta_t (delta_N too large)");
181 }
182 // set hierarchy depth which can accommodate at least m_tau_max
183 if ((m_tau_max / m_dt) < m_tau_lin) {
184 m_hierarchy_depth = 1;
185 } else {
186 auto const operand = (m_tau_max / m_dt) / double(m_tau_lin - 1);
187 assert(operand > 0.);
188 m_hierarchy_depth = static_cast<int>(std::ceil(1. + std::log2(operand)));
189 }
190
191 assert(A_obs);
192 assert(B_obs);
193 dim_A = A_obs->n_values();
194 dim_B = B_obs->n_values();
195
196 if (dim_A == 0u) {
197 throw std::runtime_error("dimension of first observable has to be >= 1");
198 }
199 if (dim_B == 0u) {
200 throw std::runtime_error("dimension of second observable has to be >= 1");
201 }
202
203 // choose the correlation operation
204 if (corr_operation_name == "componentwise_product") {
205 m_dim_corr = dim_A;
206 m_shape = A_obs->shape();
207 corr_operation = &componentwise_product;
208 m_correlation_args = Utils::Vector3d{0, 0, 0};
209 } else if (corr_operation_name == "tensor_product") {
210 m_dim_corr = dim_A * dim_B;
211 m_shape.clear();
212 m_shape.emplace_back(dim_A);
213 m_shape.emplace_back(dim_B);
214 corr_operation = &tensor_product;
215 m_correlation_args = Utils::Vector3d{0, 0, 0};
216 } else if (corr_operation_name == "square_distance_componentwise") {
217 m_dim_corr = dim_A;
218 m_shape = A_obs->shape();
219 corr_operation = &square_distance_componentwise;
220 m_correlation_args = Utils::Vector3d{0, 0, 0};
221 } else if (corr_operation_name == "fcs_acf") {
222 // note: user provides w=(wx,wy,wz) but we want to use
223 // wsquare=(wx^2,wy^2,wz^2)
224 if (not(m_correlation_args_input > Utils::Vector3d::broadcast(0.))) {
225 throw std::runtime_error("missing parameter for fcs_acf: w_x w_y w_z");
226 }
227 m_correlation_args = Utils::hadamard_product(m_correlation_args_input,
228 m_correlation_args_input);
229 if (dim_A % 3u)
230 throw std::runtime_error("dimA must be divisible by 3 for fcs_acf");
231 m_dim_corr = dim_A / 3u;
232 m_shape = A_obs->shape();
233 if (m_shape.back() != 3u)
234 throw std::runtime_error(
235 "the last dimension of dimA must be 3 for fcs_acf");
236 m_shape.pop_back();
237 corr_operation = &fcs_acf;
238 } else if (corr_operation_name == "scalar_product") {
239 m_dim_corr = 1u;
240 m_shape.clear();
241 m_shape.emplace_back(1u);
242 corr_operation = &scalar_product;
243 m_correlation_args = Utils::Vector3d{0, 0, 0};
244 } else {
245 throw std::invalid_argument("correlation operation '" +
246 corr_operation_name + "' not implemented");
247 }
248
249 // Choose the compression function
250 if (compressA_name == "discard2") {
251 compressA = &compress_discard2;
252 } else if (compressA_name == "discard1") {
253 compressA = &compress_discard1;
254 } else if (compressA_name == "linear") {
255 compressA = &compress_linear;
256 } else {
257 throw std::invalid_argument("unknown compression method '" +
258 compressA_name + "' for first observable");
259 }
260
261 if (compressB_name == "discard2") {
262 compressB = &compress_discard2;
263 } else if (compressB_name == "discard1") {
264 compressB = &compress_discard1;
265 } else if (compressB_name == "linear") {
266 compressB = &compress_linear;
267 } else {
268 throw std::invalid_argument("unknown compression method '" +
269 compressB_name + "' for second observable");
270 }
271}
272
273void Correlator::initialize_buffers() {
274 using index_type = decltype(result)::index;
275
276 A.resize(std::array<int, 2>{{m_hierarchy_depth, m_tau_lin + 1}});
277 std::fill_n(A.data(), A.num_elements(), std::vector<double>(dim_A, 0));
278 B.resize(std::array<int, 2>{{m_hierarchy_depth, m_tau_lin + 1}});
279 std::fill_n(B.data(), B.num_elements(), std::vector<double>(dim_B, 0));
280
281 n_data = 0;
282 A_accumulated_average = std::vector<double>(dim_A, 0);
283 B_accumulated_average = std::vector<double>(dim_B, 0);
284
285 auto const n_result = n_values();
286 n_sweeps = std::vector<std::size_t>(n_result, 0);
287 n_vals = std::vector<long>(m_hierarchy_depth, 0);
288
289 result.resize(std::array<std::size_t, 2>{{n_result, m_dim_corr}});
290 for (index_type i = 0; i < static_cast<index_type>(n_result); i++) {
291 for (index_type j = 0; j < static_cast<index_type>(m_dim_corr); j++) {
292 result[i][j] = 0.;
293 }
294 }
295
296 newest = std::vector<long>(m_hierarchy_depth, m_tau_lin);
297
298 tau.resize(n_result);
299 for (int i = 0; i < m_tau_lin + 1; i++) {
300 tau[i] = i;
301 }
302
303 for (int j = 1; j < m_hierarchy_depth; j++) {
304 for (int k = 0; k < m_tau_lin / 2; k++) {
305 tau[m_tau_lin + 1 + (j - 1) * m_tau_lin / 2 + k] =
306 (k + (m_tau_lin / 2) + 1) * (1 << j);
307 }
308 }
309}
310
311void Correlator::compress_kernel(long lowest_level, long highest_level) {
312 auto const tau = static_cast<long>(m_tau_lin);
313 for (long i = highest_level; i >= lowest_level; i--) {
314 // We increase the index indicating the newest on level i+1 by one (plus
315 // folding)
316 newest[i + 1l] = (newest[i + 1l] + 1l) % (tau + 1l);
317 n_vals[i + 1l] += 1l;
318
319 A[i + 1l][newest[i + 1l]] =
320 (*compressA)(A[i][(newest[i] + 1l) % (tau + 1l)],
321 A[i][(newest[i] + 2l) % (tau + 1l)]);
322 B[i + 1l][newest[i + 1l]] =
323 (*compressB)(B[i][(newest[i] + 1l) % (tau + 1l)],
324 B[i][(newest[i] + 2l) % (tau + 1l)]);
325 }
326 newest[lowest_level] = (newest[lowest_level] + 1l) % (tau + 1l);
327}
328
329void Correlator::correlate_kernel(long lowest_level, long highest_level) {
330 using index_type = decltype(result)::index;
331 auto const tau = static_cast<long>(m_tau_lin);
332 auto const half_tau = (tau + 1l) / 2l + 1l;
333 // We only need to update correlation estimates for the higher levels
334 for (long i = lowest_level + 1l; i < highest_level + 2l; i++) {
335 for (long j = half_tau; j < std::min(tau + 1l, n_vals[i]); j++) {
336 auto const index_new = newest[i];
337 auto const index_old = (newest[i] - j + tau + 1l) % (tau + 1l);
338 auto const index_res =
339 tau + (i - 1l) * tau / 2l + (j - tau / 2l + 1l) - 1l;
340 auto const temp = (corr_operation)(A[i][index_old], B[i][index_new],
341 m_correlation_args);
342 assert(temp.size() == m_dim_corr);
343 n_sweeps[index_res]++;
344 for (index_type k = 0; k < static_cast<index_type>(m_dim_corr); k++) {
345 result[index_res][k] += temp[k];
346 }
347 }
348 }
349}
350
351void Correlator::update(boost::mpi::communicator const &comm) {
352 if (finalized) {
353 throw std::runtime_error(
354 "No data can be added after finalize() was called.");
355 }
356
357 if (comm.rank() != 0) {
358 // worker nodes just need to update the observables and exit
359 A_obs->operator()(comm);
360 if (A_obs != B_obs) {
361 B_obs->operator()(comm);
362 }
363
364 return;
365 }
366
367 // We must now go through the hierarchy and make sure there is space for the
368 // new datapoint. For every hierarchy level we have to decide if it is
369 // necessary to move something
370 long highest_level_to_compress = -1l;
371
372 t++;
373
374 // Let's find out how far we have to go back in the hierarchy to make space
375 // for the new value
376 {
377 auto const max_depth = m_hierarchy_depth - 1;
378 int i = 0;
379 while (true) {
380 if (i >= max_depth or n_vals[i] <= m_tau_lin) {
381 break;
382 }
383 auto const modulo = 1 << (i + 1);
384 auto const remainder = (t - (m_tau_lin + 1) * (modulo - 1) - 1) % modulo;
385 if (remainder != 0) {
386 break;
387 }
388 highest_level_to_compress++;
389 i++;
390 }
391 }
392
393 // Now we know we must make space on the levels 0..highest_level_to_compress
394 // Now let's compress the data level by level.
395 compress_kernel(0l, highest_level_to_compress);
396
397 n_vals[0]++;
398
399 A[0][newest[0]] = A_obs->operator()(comm);
400 if (A_obs != B_obs) {
401 B[0][newest[0]] = B_obs->operator()(comm);
402 } else {
403 B[0][newest[0]] = A[0][newest[0]];
404 }
405
406 // Now we update the cumulated averages and variances of A and B
407 n_data++;
408 for (std::size_t k = 0; k < dim_A; k++) {
409 A_accumulated_average[k] += A[0][newest[0]][k];
410 }
411
412 for (std::size_t k = 0; k < dim_B; k++) {
413 B_accumulated_average[k] += B[0][newest[0]][k];
414 }
415
416 using index_type = decltype(result)::index;
417 auto const tau = static_cast<long>(m_tau_lin);
418 // Now update the lowest level correlation estimates
419 for (long j = 0l; j < std::min(tau + 1l, n_vals[0]); j++) {
420 auto const index_new = newest[0];
421 auto const index_old = (newest[0] - j + tau + 1l) % (tau + 1l);
422 auto const temp =
423 (corr_operation)(A[0][index_old], B[0][index_new], m_correlation_args);
424 assert(temp.size() == m_dim_corr);
425
426 n_sweeps[j]++;
427 for (index_type k = 0; k < static_cast<index_type>(m_dim_corr); k++) {
428 result[j][k] += temp[k];
429 }
430 }
431 // Now for the higher ones
432 correlate_kernel(0l, highest_level_to_compress);
433}
434
435int Correlator::finalize(boost::mpi::communicator const &comm) {
436 if (finalized) {
437 throw std::runtime_error("Correlator::finalize() can only be called once.");
438 }
439 // We must now go through the hierarchy and make sure there is space for the
440 // new datapoint. For every hierarchy level we have to decide if it is
441 // necessary to move something
442
443 // mark the correlation as finalized
444 finalized = true;
445
446 // worker nodes don't need to do anything
447 if (comm.rank() != 0) {
448 return 0;
449 }
450
451 auto const tau = static_cast<long>(m_tau_lin);
452 for (long ll = 0; ll < static_cast<long>(m_hierarchy_depth - 1); ll++) {
453 long vals_ll; // number of values remaining in the lowest level
454 if (n_vals[ll] > tau + 1l)
455 vals_ll = tau + n_vals[ll] % 2l;
456 else
457 vals_ll = n_vals[ll];
458
459 while (vals_ll) {
460 // Check, if we will want to push the value from the lowest level
461 auto highest_level_to_compress = (vals_ll % 2l) ? ll : -1l;
462
463 // Let's find out how far we have to go back in the hierarchy to make
464 // space for the new value
465 {
466 auto const max_depth = static_cast<long>(m_hierarchy_depth - 1);
467 long i = ll + 1l; // lowest level for which to check for compression
468 while (highest_level_to_compress > -1l) {
469 if (i >= max_depth or n_vals[i] % 2l == 0l or n_vals[i] <= tau) {
470 break;
471 }
472 highest_level_to_compress++;
473 i++;
474 }
475 }
476 vals_ll--;
477
478 // Now we know we must make space on the levels
479 // ll..highest_level_to_compress
480 // Now let's compress the data level by level.
481 compress_kernel(ll, highest_level_to_compress);
482 correlate_kernel(ll, highest_level_to_compress);
483 }
484 }
485 return 0;
486}
487
488std::vector<double> Correlator::get_correlation() {
489 using index_type = decltype(result)::index;
490 auto const n_result = n_values();
491 std::vector<double> res(n_result * m_dim_corr);
492
493 for (std::size_t i = 0; i < n_result; i++) {
494 auto const index = static_cast<index_type>(m_dim_corr * i);
495 for (index_type k = 0; k < static_cast<index_type>(m_dim_corr); k++) {
496 if (n_sweeps[i]) {
497 res[index + k] = result[static_cast<index_type>(i)][k] /
498 static_cast<double>(n_sweeps[i]);
499 }
500 }
501 }
502 return res;
503}
504
505std::vector<double> Correlator::get_lag_times() const {
506 std::vector<double> res(n_values());
507 std::ranges::transform(tau, res.begin(),
508 [dt = m_dt](auto const &a) { return a * dt; });
509 return res;
510}
511
513 std::stringstream ss;
514 boost::archive::binary_oarchive oa(ss);
515
516 oa << t;
517 oa << m_dt;
518 oa << m_shape;
519 oa << m_correlation_args_input;
520 oa << A;
521 oa << B;
522 oa << result;
523 oa << n_sweeps;
524 oa << n_vals;
525 oa << newest;
526 oa << A_accumulated_average;
527 oa << B_accumulated_average;
528 oa << n_data;
529
530 return ss.str();
531}
532
533void Correlator::set_internal_state(std::string const &state) {
534 namespace iostreams = boost::iostreams;
535 iostreams::array_source src(state.data(), state.size());
536 iostreams::stream<iostreams::array_source> ss(src);
537 boost::archive::binary_iarchive ia(ss);
538
539 ia >> t;
540 ia >> m_dt;
541 ia >> m_shape;
542 ia >> m_correlation_args_input;
543 ia >> A;
544 ia >> B;
545 ia >> result;
546 ia >> n_sweeps;
547 ia >> n_vals;
548 ia >> newest;
549 ia >> A_accumulated_average;
550 ia >> B_accumulated_average;
551 ia >> n_data;
552 initialize_operations();
553 m_system = nullptr;
554}
555
556} // namespace Accumulators
Vector implementation and trait types for boost qvm interoperability.
void const * m_system
for bookkeeping purposes
std::string get_internal_state() const final
void set_internal_state(std::string const &) final
std::vector< double > get_lag_times() const
int finalize(boost::mpi::communicator const &comm)
At the end of data collection, go through the whole hierarchy and correlate data left there.
std::vector< double > get_correlation()
Return correlation result.
void update(boost::mpi::communicator const &comm) override
The function to process a new datapoint of A and B.
static DEVICE_QUALIFIER constexpr Vector< T, N > broadcast(typename Base::value_type const &value) noexcept
Create a vector that has all entries set to the same value.
Definition Vector.hpp:131
std::vector< double > componentwise_product(std::vector< double > const &A, std::vector< double > const &B, Utils::Vector3d const &)
std::vector< double > tensor_product(std::vector< double > const &A, std::vector< double > const &B, Utils::Vector3d const &)
std::vector< double > compress_linear(std::vector< double > const &A1, std::vector< double > const &A2)
Compress computing arithmetic mean: A_compressed=(A1+A2)/2.
std::vector< double > scalar_product(std::vector< double > const &A, std::vector< double > const &B, Utils::Vector3d const &)
std::vector< double > compress_discard1(std::vector< double > const &A1, std::vector< double > const &A2)
Compress discarding the 1st argument and return the 2nd.
std::vector< double > compress_discard2(std::vector< double > const &A1, std::vector< double > const &A2)
Compress discarding the 2nd argument and return the 1st.
std::vector< double > fcs_acf(std::vector< double > const &A, std::vector< double > const &B, Utils::Vector3d const &wsquare)
std::vector< double > square_distance_componentwise(std::vector< double > const &A, std::vector< double > const &B, Utils::Vector3d const &)
DEVICE_QUALIFIER constexpr T sqr(T x)
Calculates the SQuaRe of x.
Definition sqr.hpp:28
auto hadamard_product(Vector< T, N > const &a, Vector< U, N > const &b)
Definition Vector.hpp:385