ATLAS Offline Software
KLGaussianMixtureReduction.cxx
Go to the documentation of this file.
1 /*
2  Copyright (C) 2002-2024 CERN for the benefit of the ATLAS collaboration
3 */
4 
8 //
10 //
11 #include "CxxUtils/restrict.h"
12 #include "CxxUtils/vec.h"
13 //
14 #include <cmath>
15 #include <memory>
16 #include <limits>
17 #include <numeric>
18 #include <stdexcept>
19 #include <vector>
20 
29 namespace {
30 
35 using namespace GSFUtils;
36 
49 inline float
50 symmetricKL(const Component1D& ATH_RESTRICT componentI,
51  const Component1D& ATH_RESTRICT componentJ)
52 {
53  const double meanDifference = componentI.mean - componentJ.mean;
54  const double inverCovSum = componentI.invCov + componentJ.invCov;
55  const double term1 = componentI.invCov * componentJ.cov;
56  const double term2 = componentJ.invCov * componentI.cov;
57  const double term3 = meanDifference * inverCovSum * meanDifference;
58  return term1 + term2 + term3;
59 }
67 inline void
70 {
71 
72  const double sumWeight = updated.weight + removed.weight;
73 
74  const double invSumWeight = 1. / sumWeight;
75  const double weightI_IJ = updated.weight * invSumWeight;
76  const double weightJ_IJ = removed.weight * invSumWeight;
77  const double meanDiff = (updated.mean - removed.mean);
78 
79  const double sumMean = weightI_IJ * updated.mean + weightJ_IJ * removed.mean;
80 
81  const double sumVariance = weightI_IJ * updated.cov +
82  weightJ_IJ * removed.cov +
83  weightI_IJ * weightJ_IJ * meanDiff * meanDiff;
84  updated.mean = sumMean;
85  updated.cov = sumVariance;
86  updated.invCov = 1. / sumVariance;
87  updated.weight = sumWeight;
88 }
89 
95 constexpr inline int32_t
96 numPadded(const int32_t n)
97 {
98  //This always return a padded number dividable
99  //with 16
100  //e.g ((33+15)&~15) = 48
101  return ((n+15)&~15);
102 }
103 
104 inline int32_t
105 numDistances(const int32_t n, float* distancesIn)
106 {
107  const int32_t npadded = numPadded(n);
108  // Make sure the padded elements are set to max
109  std::fill(
110  distancesIn + n, distancesIn + npadded, std::numeric_limits<float>::max());
111  return npadded;
112 }
113 
114 /*
115  * This is a O(N^3) time N^2 space algorithm.
116  * It can be seen as tailored implementation for
117  * our problem of the basic algorithm
118  * for Hierarchical Clustering.
119  *
120  * We rely on the fast findMinimunIndex above
121  * and a triangular array representation to
122  * reduce the actual time for our problem
123  * (max N=72)
124  *
125  * We opt for fixed size arrays of max N(=72) elements,
126  * but dynamic arrays of ~ N*(N-1)/2 elements.
127  *
128  * Existing alternatives in the literature:
129  * - 1. O(N^3) worst-case time , O(n^2) best
130  * - 2. O(N^2 log(N)) worst-time
131  * See :
132  * "Modern hierarchical, agglomerative clustering algorithms"
133  * https://arxiv.org/abs/1109.2378
134  * or
135  * "Efficient algorithms for agglomerative hierarchical clustering methods"
136  *
137  * What we found in the past:
138  * - We seem to hit the O(N^3)/worst case of Alg 1
139  * quite often.
140  * - Alg 2 had significant overhead
141  * from the required data-structures.
142  * We could revisit these in the future.
143  */
144 
145 /*
146  * Pairwise distances implementation:
147  * Assuming N total elements 0... N-1,
148  * the pairwise distance matrix
149  * can be represented in a triangular array: <br>
150  * [ (1,0) ] <br>
151  * [ (2,0), (2,1) ] <br>
152  * [ (3,0), (3,1), (3,2)] <br>
153  * [ (4,0), (4,1), (4,2) , (4,3) <br>
154  * [.............................] <br>
155  * [(N-1,0),(N-1,1),(N-1,2),(N-1,3) ... (N-1,N-2)]<br>
156  * With size 1+2+3+ .... (N-1) = N*(N-1)/2
157  *
158  * The lexicographical storage allocation function is
159  * Loc(i,j) = i*(i-1)/2 + j <br>
160  * e.g : <br>
161  * (1,0) => 1 *(1-1)/2 + 0 => 0 <br>
162  * (2,0) => 2 *(2-1)/2 + 0 => 1 <br>
163  * (2,1) => 2 *(2-1)/2 + 1 => 2 <br>
164  * (3,0) => 3 * (3-1)/2 +0 => 3 <br>
165  * Leading to <br>
166  * [(1,0),(2,0),(2,1),(3,0),(3,1),(3,2).... (N-1,N-2)]
167  *
168  * The N-1 Rows map to the value K of the 1st element in the pair
169  * 1,2,3,..,N-1. <br>
170  * Each Row has size K and starts at array positions K*(K-1)/2 <br>
171  * e.g <br>
172  * The row for element 1 starts at array position 0. <br>
173  * The row for element 2 starts at array position 1. <br>
174  * The row for element N-1 starts at array positon (N-1)*(N-2)/2 <br>
175  *
176  * The N-1 Columns map to the value K of the second element in the pair <br>
177  * K= 0,1,2 .., N-2 <br>
178  * The array positions follows (i-1)*i/2+K <br>
179  * where i : K+1 .... N-1 [for(i=K+1;i<N;++i) <br>
180  * e.g <br>
181  * 0 appears as 2nd element in the pair at array positions [0,1,3,6...] <br>
182  * 1 appears as 2nd element in the pair at array positions [2,4,7...] <br>
183  * 2 appears as 2nd element in the pair at array positions [5,8,12....] <br>
184  * N-2 appears as 2nd element once at position [N(N-1)/2-1] <br>
185  */
186 
190 constexpr std::array<int32_t, GSFConstants::maxComponentsAfterConvolution>
191  offset = []() {
193  std::array<int32_t, n> tmp = {};
194  for (int32_t i = 0; i < n; ++i) {
195  tmp[i] = (i - 1) * i / 2;
196  }
197  return tmp;
198  }();
199 
204 struct triangularToIJ
205 {
206  int8_t I = -1;
207  int8_t J = -1;
208 };
214 inline triangularToIJ
215 convert(int32_t idx)
216 {
217  // We prefer to preMap the maximum 2556 elements.
218  // Alternatively one can use the following
219  // if pre-mapping becomes an issue
220  // (see https://hal.archives-ouvertes.fr/hal-02047514/document)
221  // int8_t i = std::floor((std::sqrt(1 + 8 * idx) + 1) / 2);
222  // int8_t j = idx - (i - 1) * i / 2;
223  static const std::vector<triangularToIJ> preMap = []() {
225  constexpr size_t nn = n * (n - 1) / 2;
226  std::vector<triangularToIJ> indexMap(nn);
227  for (int8_t i = 1; i < n; ++i) {
228  const int32_t indexConst = offset[i];
229  for (int8_t j = 0; j < i; ++j) {
230  indexMap[indexConst + j] = { i, j };
231  }
232  }
233  return indexMap;
234  }();
235  return preMap[idx];
236 }
237 
244 inline void
245 calculateAllDistances(const Component1D* componentsIn,
246  float* distancesIn,
247  const int32_t n)
248 {
249  const Component1D* components =
250  std::assume_aligned<GSFConstants::alignment>(componentsIn);
251  float* distances =
252  std::assume_aligned<GSFConstants::alignment>(distancesIn);
253  for (int32_t i = 1; i < n; ++i) {
254  const int32_t indexConst = offset[i];
255  const Component1D componentI = components[i];
256  for (int32_t j = 0; j < i; ++j) {
257  const Component1D componentJ = components[j];
258  distances[indexConst + j] = symmetricKL(componentI, componentJ);
259  }
260  }
261 }
262 
272 inline int32_t
273 updateDistances(
274  Component1D* ATH_RESTRICT componentsIn,
275  std::array<int8_t, GSFConstants::maxComponentsAfterConvolution>& mergingIndex,
276  float* ATH_RESTRICT distancesIn,
277  int32_t minFrom,
278  int32_t minTo,
279  int32_t n)
280 {
281  float* distances =
282  std::assume_aligned<GSFConstants::alignment>(distancesIn);
283  Component1D* components =
284  std::assume_aligned<GSFConstants::alignment>(componentsIn);
285  // We swap the last elements with the ones indexed by minFrom.
286  // After this the remaining components we care about
287  // are n-1 which we return
288  const int32_t last = (n - 1);
289  const int32_t indexOffsetJ = offset[minFrom];
290  const int32_t indexOffsetLast = offset[last];
291  // we do no need to swap the last with itself
292  if (minFrom != last) {
293  // Rows in distance matrix
294  for (int32_t i = 0; i < minFrom; ++i) {
295  std::swap(distances[indexOffsetJ + i], distances[indexOffsetLast + i]);
296  }
297  // Columns in distance matrix
298  for (int32_t i = minFrom + 1; i < last; ++i) {
299  const int32_t index = offset[i] + minFrom;
300  std::swap(distances[index], distances[indexOffsetLast + i]);
301  }
302  // swap the components
303  std::swap(components[minFrom], components[last]);
304  std::swap(mergingIndex[minFrom], mergingIndex[last]);
305  }
306  // In case minTo was indexing the last now it should
307  // be indexing minFrom due to the above swapping
308  if (minTo == last) {
309  minTo = minFrom;
310  }
311  const int32_t indexConst = offset[minTo];
312  // This is the component that has been updated
313  const Component1D componentJ = components[minTo];
314  // Rows in distance matrix
315  for (int32_t i = 0; i < minTo; ++i) {
316  const Component1D componentI = components[i];
317  const int32_t index = indexConst + i;
318  distances[index] = symmetricKL(componentI, componentJ);
319  }
320  // Columns in distance matrix
321  for (int32_t i = minTo + 1; i < last; ++i) {
322  const Component1D componentI = components[i];
323  const int32_t index = offset[i] + minTo;
324  distances[index] = symmetricKL(componentI, componentJ);
325  }
326  return last;
327 }
328 
333 findMergesImpl(const Component1DArray& componentsIn,
334  const int32_t n,
335  const int8_t reducedSize)
336 {
337  // copy the array for internal use
338  Component1DArray copyComponents(componentsIn);
339  Component1D* components = std::assume_aligned<GSFConstants::alignment>(
340  copyComponents.components.data());
341  // Based on the inputSize n allocate enough space for the pairwise distances
342  int32_t nn = n * (n - 1) / 2;
343  int32_t nnpadded = numPadded(nn);
345  nnpadded, std::numeric_limits<float>::max());
346  // initial distance calculation
347  calculateAllDistances(components, distances.buffer(), n);
348  // As we merge keep track where things moved
349  std::array<int8_t, GSFConstants::maxComponentsAfterConvolution>
350  mergingIndex{};
351  std::iota(mergingIndex.begin(), mergingIndex.end(), 0);
352  // Result to be returned
353  MergeArray result{};
354  int32_t numberOfComponentsLeft = n;
355  // merge loop
356  while (numberOfComponentsLeft > reducedSize) {
357  // find pair with minimum distance
358  const int32_t minIndex =
359  findIdxOfMinimum::impl<findIdxOfMinimum::VecMinThenIdx>(
360  distances.buffer(), nnpadded);
361  const triangularToIJ conversion = convert(minIndex);
362  int8_t minTo = conversion.I;
363  int8_t minFrom = conversion.J;
364  // This is the convention we had so retained.
365  if (mergingIndex[minTo] < mergingIndex[minFrom]) {
366  std::swap(minTo, minFrom);
367  }
368  // prepare what to return
369  const int8_t miniToreturn = mergingIndex[minTo];
370  const int8_t minjToreturn = mergingIndex[minFrom];
371  result.merges[result.numMerges] = { miniToreturn, minjToreturn };
372  ++result.numMerges;
373  // Combine
374  combine(components[minTo], components[minFrom]);
375  // update distances
376  numberOfComponentsLeft = updateDistances(components,
377  mergingIndex,
378  distances.buffer(),
379  minFrom,
380  minTo,
381  numberOfComponentsLeft);
382 
383  // number of remaining distances padded
384  nn = offset[numberOfComponentsLeft];
385  nnpadded = numDistances(nn, distances.buffer());
386  } // end of merge while
387  return result;
388 }
389 
390 } // anonymous namespace with implementation
391 
392 namespace GSFUtils {
394 findMerges(const Component1DArray& componentsIn, const int8_t reducedSize)
395 {
396  const int32_t n = componentsIn.numComponents;
398  reducedSize > n) {
399  throw std::runtime_error("findMerges :Invalid InputSize or reducedSize");
400  }
401  return findMergesImpl(componentsIn, n, reducedSize);
402 }
403 } // end namespace GSFUtils
get_generator_info.result
result
Definition: get_generator_info.py:21
GSFUtils::MergeArray
struct representing an array or the merges.
Definition: KLGaussianMixtureReduction.h:54
max
#define max(a, b)
Definition: cfImp.cxx:41
index
Definition: index.py:1
ATH_RESTRICT
#define ATH_RESTRICT
Definition: restrict.h:31
FlavorTagDiscriminants::combine
size_t combine(size_t lhs, size_t rhs)
Definition: hash.h:21
lumiFormat.i
int i
Definition: lumiFormat.py:92
beamspotman.n
n
Definition: beamspotman.py:731
GSFConstants::maxComponentsAfterConvolution
constexpr int8_t maxComponentsAfterConvolution
The maximum size State x Bethe-Heitler components The typical number we use is the max 6x12 = 72 i....
Definition: GsfConstants.h:61
DeMoUpdate.tmp
string tmp
Definition: DeMoUpdate.py:1167
AlignedDynArray.h
WriteCalibToCool.swap
swap
Definition: WriteCalibToCool.py:94
restrict.h
Macro wrapping the nonstandard restrict keyword.
TMVAToMVAUtils::convert
std::unique_ptr< MVAUtils::BDT > convert(TMVA::MethodBDT *bdt, bool isRegression=true, bool useYesNoLeaf=false)
Definition: TMVAToMVAUtils.h:114
KLGaussianMixtureReduction.h
Utilities to facilitate the calculation of the KL divergence/distance between components of the mixtu...
DeMoScan.index
string index
Definition: DeMoScan.py:362
vec.h
Vectorization helpers.
GSFUtils::Component1DArray::numComponents
int32_t numComponents
Definition: KLGaussianMixtureReduction.h:46
lumiFormat.fill
fill
Definition: lumiFormat.py:111
GSFFindIndexOfMinimum.h
convertTimingResiduals.offset
offset
Definition: convertTimingResiduals.py:71
LArNewCalib_DelayDump_OFC_Cali.idx
idx
Definition: LArNewCalib_DelayDump_OFC_Cali.py:69
GSFUtils
Dynamic array fullfilling alignment requirements.
Definition: KLGaussianMixtureReduction.cxx:392
GsfConstants.h
I
#define I(x, y, z)
Definition: MD5.cxx:116
GSFUtils::Component1D
struct representing 1D component
Definition: KLGaussianMixtureReduction.h:30
GSFUtils::findMerges
MergeArray findMerges(const Component1DArray &componentsIn, const int8_t reducedSize)
Find the order in which the components need to be merged.
Definition: KLGaussianMixtureReduction.cxx:394
GSFUtils::Component1DArray
struct representing an array of 1D component.
Definition: KLGaussianMixtureReduction.h:42
GSFUtils::AlignedDynArray
A wrapper around std::aligned_alloc.
Definition: AlignedDynArray.h:30