Belle II Software development
NeuroTrigger3DH.cc
1/**************************************************************************
2 * basf2 (Belle II Analysis Software Framework) *
3 * Author: The Belle II Collaboration *
4 * *
5 * See git log for contributors and copyright holders. *
6 * This file is licensed under LGPL-3.0, see LICENSE.md. *
7 **************************************************************************/
8
9#include "trg/cdc/NeuroTrigger3DH.h"
10
11#include <vector>
12#include <cmath>
13#include <algorithm>
14#include <cstdint>
15
16#include "trg/cdc/dataobjects/CDCTrigger3DHTrack.h"
17#include "framework/gearbox/Unit.h"
18#include "framework/datastore/StoreArray.h"
19#include "framework/logging/Logger.h"
20#include "cdc/geometry/CDCGeometryPar.h"
21
22using namespace Belle2;
23
24// Initialize CDC geometry
25void NeuroTrigger3DH::initialize()
26{
27 std::array<std::vector<int>, m_nSL> getWireLayerID;
28
29 int layer = 0;
30 // First superlayer (8 layers)
31 for (int i = 0; i < 8; ++i) {
32 getWireLayerID[0].push_back(layer++);
33 }
34 // Remaining superlayers (6 layers each)
35 for (unsigned short superLayerIdx = 1; superLayerIdx < m_nSL; ++superLayerIdx) {
36 for (int i = 0; i < 6; ++i) {
37 getWireLayerID[superLayerIdx].push_back(layer++);
38 }
39 }
40
41 const CDC::CDCGeometryPar& cdc = CDC::CDCGeometryPar::Instance();
42
43 for (unsigned short superLayerIdx = 0; superLayerIdx < m_nSL; ++superLayerIdx) {
44 if (superLayerIdx == 0) {
45 m_radiusWireLayer[superLayerIdx][0] = cdc.senseWireR(getWireLayerID[superLayerIdx][3]);
46 m_radiusWireLayer[superLayerIdx][1] = cdc.senseWireR(getWireLayerID[superLayerIdx][4]);
47 } else {
48 m_radiusWireLayer[superLayerIdx][0] = cdc.senseWireR(getWireLayerID[superLayerIdx][2]);
49 m_radiusWireLayer[superLayerIdx][1] = cdc.senseWireR(getWireLayerID[superLayerIdx][3]);
50 }
51 m_nWires[superLayerIdx] = cdc.nWiresInLayer(getWireLayerID[superLayerIdx][0]);
52 }
53
54 m_cumulativeWires[0] = 0;
55 for (unsigned short superLayerIdx = 1; superLayerIdx <= m_nSL; ++superLayerIdx) {
56 m_cumulativeWires[superLayerIdx] = m_cumulativeWires[superLayerIdx - 1] + m_nWires[superLayerIdx - 1];
57 }
58}
59
60// Initialize the track segment hit collections
61void NeuroTrigger3DH::initializeCollections(std::string hitCollectionName)
62{
63 m_segmentHits.isRequired(hitCollectionName);
64 m_hitCollectionName = hitCollectionName;
65}
66
67// Set the MLP (includes the neuro parameters)
68void NeuroTrigger3DH::setMLP(const CDCTrigger3DHMLP& mlp)
69{
70 m_MLP = mlp;
71 m_neuroParameters3DH = mlp.getNeuroParameters();
72}
73
74// Transform the weights and biases (range [-4, 4]) to precisionWeights bit integers
75void NeuroTrigger3DH::createIntWeights()
76{
77 const std::vector<float>& floatWeights = m_MLP.getFloatWeights();
78 float minFloatWeight = *std::min_element(floatWeights.begin(), floatWeights.end());
79 float maxFloatWeight = *std::max_element(floatWeights.begin(), floatWeights.end());
80 float maxAbsoluteWeight = std::max(std::abs(minFloatWeight), std::abs(maxFloatWeight));
81
82 // Calculate the necessary fractional bit size to not exceed precisionWeights for the weights
83 int shift = static_cast<int>(std::ceil(std::log2(maxAbsoluteWeight)));
84 int fractionalWeightBits = m_precisionWeights - shift;
85
86 // Transform the weights and return the fractional bit size for scaling
87 std::vector<int32_t> intWeights(floatWeights.size());
88 std::transform(floatWeights.begin(), floatWeights.end(), intWeights.begin(),
89 [fractionalWeightBits](float w) { return static_cast<int32_t>(std::round(w * (1 << fractionalWeightBits))); });
90
91 m_intWeights = intWeights;
92 m_fractionalWeightBits = fractionalWeightBits;
93}
94
95// Calculate the crossing angles (m_alpha) and reference IDs (m_referenceID) for each super layer
96void NeuroTrigger3DH::calculateTrackParameters(const CDCTrigger3DHTrack& track)
97{
98 const double omega = track.getOmega();
99 const double phi = track.getPhi0();
100 for (unsigned short superLayerIdx = 0; superLayerIdx < m_nSL; ++superLayerIdx) {
101 for (int priorityLayer = 0; priorityLayer < 2; ++priorityLayer) {
102 double radiusWireLayer = m_radiusWireLayer[superLayerIdx][priorityLayer];
103 double alpha = (radiusWireLayer * std::abs(omega) < 2.0)
104 ? std::asin(radiusWireLayer * omega / 2.0)
105 : M_PI_2;
106 double referenceID = (phi - alpha) * m_nWires[superLayerIdx] / (2.0 * M_PI);
107 m_alpha[superLayerIdx][priorityLayer] = alpha;
108 m_referenceID[superLayerIdx][priorityLayer] = std::remainder(referenceID, m_nWires[superLayerIdx]);
109 }
110 }
111}
112
113// Calculate the same track parameters but with fixed floating point accuracy
114void NeuroTrigger3DH::calculateTrackParametersFixedPrecision(const CDCTrigger3DHTrack& track)
115{
116 const long scalePhi = 1L << m_precisionPhi;
117 const long scaleAlpha = 1L << m_precisionAlpha;
118 const long scaleFactor = 1L << m_precisionScaleFactor;
119 const long scaleID = 1L << m_precisionReferenceID;
120
121 const double omega = track.getOmega();
122 const long phiFixed = std::round(track.getPhi0() * scalePhi);
123
124 for (unsigned short int superLayerIdx = 0; superLayerIdx < m_nSL; ++superLayerIdx) {
125 const long scaledWireFactor = std::round((m_nWires[superLayerIdx] / (2.0 * M_PI)) * scaleFactor);
126 for (int priorityLayer = 0; priorityLayer < 2; ++priorityLayer) {
127 const double radius = m_radiusWireLayer[superLayerIdx][priorityLayer];
128 long alphaFixed = (radius * std::abs(omega) < 2.0)
129 ? std::round(std::asin(radius * omega / 2.0) * scaleAlpha)
130 : std::round(M_PI_2 * scaleAlpha);
131 long deltaPhi = phiFixed - alphaFixed;
132 long refIDFixed = (deltaPhi * scaledWireFactor) / (1L << (m_precisionPhi + m_precisionScaleFactor - m_precisionReferenceID));
133 m_alpha[superLayerIdx][priorityLayer] = static_cast<double>(alphaFixed) / scaleAlpha;
134 m_referenceID[superLayerIdx][priorityLayer] = static_cast<double>(refIDFixed) / scaleID;
135 }
136 }
137}
138
139// Set the event time based on the 3DFinder track segment hits
140void NeuroTrigger3DH::setEventTime(const CDCTrigger3DHTrack& track)
141{
142 m_T0 = getLowestTime(track.getRelationsTo<CDCTriggerSegmentHit>(m_hitCollectionName));
143 if (m_T0 < 9999) {
144 m_hasT0 = true;
145 } else {
146 m_T0 = 0;
147 m_hasT0 = false;
148 }
149}
150
151// Get the lowest priority time from the track segment hits
152int NeuroTrigger3DH::getLowestTime(const RelationVector<CDCTriggerSegmentHit>& trackSegmentHits) const
153{
154 int lowestTime = 9999;
155 for (unsigned int hitIdx = 0; hitIdx < trackSegmentHits.size(); ++hitIdx) {
156 if (trackSegmentHits.weight(hitIdx) < 0) continue;
157 if (trackSegmentHits[hitIdx]->priorityTime() < lowestTime) {
158 lowestTime = trackSegmentHits[hitIdx]->priorityTime();
159 }
160 }
161 return lowestTime;
162}
163
164// Load the 3DFinder track segment hits
165std::vector<size_t> NeuroTrigger3DH::load3DHits(const CDCTrigger3DHTrack& track) const
166{
167 RelationVector<CDCTriggerSegmentHit> trackSegmentHits =
168 track.getRelationsTo<CDCTriggerSegmentHit>(m_hitCollectionName);
169 std::vector<size_t> allHitIDs;
170 for (size_t hitIdx = 0; hitIdx < trackSegmentHits.size(); ++hitIdx) {
171 if (trackSegmentHits.weight(hitIdx) >= 0) { // Ignore hits with negative relation weight
172 allHitIDs.push_back(trackSegmentHits[hitIdx]->getArrayIndex());
173 }
174 }
175 return allHitIDs;
176}
177
178// Calculate the input vector for the MLP
179std::vector<float> NeuroTrigger3DH::getInputVector(const std::vector<size_t>& hitIds) const
180{
181 const size_t nInput = m_neuroParameters3DH.nInput;
182
183 if (nInput != m_nStandardInputNodes && nInput != m_nExtendedInputNodes) {
184 B2FATAL("Invalid neural network input size");
185 }
186 // Maximal drift time (for scaling), hits with larger values are ignored.
187 constexpr unsigned short tMax = 256;
188
189 const bool hasExtendedInput = (nInput == m_nExtendedInputNodes);
190 const size_t featuresPerHit = nInput / m_nSL;
191 const size_t patternSize = 11;
192
193 std::vector<float> inputVector(nInput, 0.0f);
194 for (const size_t hitIdx : hitIds) {
195 const CDCTriggerSegmentHit* trackSegmentHit = m_segmentHits[hitIdx];
196 const unsigned short priority = trackSegmentHit->getPriorityPosition();
197 const unsigned short superLayerIdx = trackSegmentHit->getISuperLayer();
198 const size_t priorityLayer = (priority == 3) ? 0 : 1;
199 const size_t baseIdx = featuresPerHit * superLayerIdx;
200
201 const float relativeID = scaleRelativeID(getRelativeID(*trackSegmentHit), superLayerIdx);
202 const float alpha = m_alpha[superLayerIdx][priorityLayer];
203 const float scaledDriftTime = getScaledDriftTime(*trackSegmentHit, tMax);
204
205 inputVector[baseIdx] = relativeID;
206 inputVector[baseIdx + 1] = scaledDriftTime;
207 inputVector[baseIdx + 2] = alpha;
208
209 if (m_hasT0 && hasExtendedInput) {
210 unsigned int hitPattern = trackSegmentHit->gethitpattern();
211 for (size_t j = 0; j < patternSize; ++j) {
212 inputVector[baseIdx + 3 + j] = (hitPattern >> j) & 1;
213 }
214 }
215 }
216 return inputVector;
217}
218
219// Calculates the relative wire ID difference between the 3DFinder track and the priority hit
220double NeuroTrigger3DH::getRelativeID(const CDCTriggerSegmentHit& trackSegmentHit) const
221{
222 const int superLayerIdx = trackSegmentHit.getISuperLayer();
223 const unsigned int priority = trackSegmentHit.getPriorityPosition();
224 const unsigned int priorityLayer = (priority == 3) ? 0 : 1;
225 // 0 = no hit, 1 = 2nd right, 2 = 2nd left, 3 = fist priority
226 constexpr std::array<short, 4> priorityOffsetFactor = {0, -1, 1, 0};
227 double relativeID = trackSegmentHit.getSegmentID()
228 - m_cumulativeWires[superLayerIdx]
229 + 0.5 * priorityOffsetFactor[priority]
230 - m_referenceID[superLayerIdx][priorityLayer];
231 return std::remainder(relativeID, m_nWires[superLayerIdx]);
232}
233
234// Scale relative TS ID from relevant range to approximately [-1, 1]
235float NeuroTrigger3DH::scaleRelativeID(const double relativeID, const unsigned superLayerIdx) const
236{
237 // Relevant ID ranges for each super layer (2x9)
238 constexpr std::array<float, 18> relevantID =
239 {-1.5, 1.5, -7.5, -0.5, -1.5, 1.5, 0.5, 7.5, -1.5, 1.5, -8.5, 0.5, -2.5, 1.5, -0.5, 10.5, -3.5, 2.5};
240 const float idMin = relevantID[2 * superLayerIdx];
241 const float idMax = relevantID[2 * superLayerIdx + 1];
242 const float range = idMax - idMin;
243 float scale = 2.0f / range;
244 // Round down to nearest power of 2 for FPGA implementation
245 scale = std::pow(2.0f, std::floor(std::log2(scale)));
246 const float offset = (idMin + idMax) * 0.5f;
247 return scale * static_cast<float>(relativeID - offset);
248}
249
250// Calculate the scaled drift time of a track segment hit for the network input
251float NeuroTrigger3DH::getScaledDriftTime(const CDCTriggerSegmentHit& trackSegmentHit, const unsigned short maxTime) const
252{
253 const float scaleT = std::pow(2.0f, std::floor(std::log2(1.0f / maxTime)));
254 int driftTime = (m_hasT0) ? trackSegmentHit.priorityTime() - m_T0 : 0;
255 driftTime = std::clamp(driftTime, 0, static_cast<int>(maxTime));
256 // 0 = no hit, 1 = right, 2 = left, 3 = undetermined
257 constexpr std::array<short, 4> leftRightFactor = {0, -1, 1, 0};
258 float scaledDriftTime = leftRightFactor[trackSegmentHit.getLeftRight()] * driftTime * scaleT;
259 return scaledDriftTime;
260}
261
262// Run the neural network (MLP) with the input vector
263std::vector<float> NeuroTrigger3DH::runMLP(const std::vector<float>& input) const
264{
265 const CDCTrigger3DHMLP& network = m_MLP;
266 const std::vector<float>& weights = network.getFloatWeights();
267
268 std::vector<float> layerInput = input;
269 std::vector<float> layerOutput;
270 size_t weightIndex = 0;
271
272 const size_t nLayers = network.getNumberOfLayers();
273 for (size_t layerIdx = 1; layerIdx < nLayers; ++layerIdx) {
274 layerInput.push_back(1.0f); // add bias
275 const size_t nInputs = layerInput.size();
276 const size_t nOutputs = network.getNumberOfNodes(layerIdx);
277 layerOutput.assign(nOutputs, 0.0f); // create new empty output vector
278 for (size_t outIdx = 0; outIdx < nOutputs; ++outIdx) {
279 for (size_t inputIdx = 0; inputIdx < nInputs; ++inputIdx) {
280 layerOutput[outIdx] += layerInput[inputIdx] * weights[weightIndex++];
281 }
282 layerOutput[outIdx] = std::tanh(layerOutput[outIdx]);
283 }
284 layerInput = layerOutput;
285 }
286 return unscaleTarget(layerOutput);
287}
288
289// Run the MLP with fixed point arithmetic
290std::vector<float> NeuroTrigger3DH::runMLPFixedPrecision(const std::vector<float>& input) const
291{
292 const CDCTrigger3DHMLP& network = m_MLP;
293 const int fractionalInputBits = m_precisionInputs - 1;
294
295 // Transform the input floats (range [-2, 2]) to m_precisionInputs bit integers
296 std::vector<int32_t> layerInput(input.size());
297 std::transform(input.begin(), input.end(), layerInput.begin(),
298 [](float i) { return static_cast<int32_t>(std::round(i * (1 << fractionalInputBits))); });
299
300 std::vector<int32_t> layerOutput;
301 size_t weightIndex = 0;
302 const size_t nLayers = network.getNumberOfLayers();
303 for (size_t layerIdx = 1; layerIdx < nLayers; ++layerIdx) {
304 // Set bias input and scaling shift (different precisions for network inputs and tanh outputs possible)
305 unsigned int scalingShift;
306 if (layerIdx == 1) {
307 layerInput.push_back(1 << fractionalInputBits);
308 scalingShift = fractionalInputBits + m_fractionalWeightBits - TanhLUT::LUT_FRAC_BITS;
309 } else {
310 layerInput.push_back(1 << TanhLUT::LUT_FRAC_BITS);
311 scalingShift = TanhLUT::LUT_FRAC_BITS + m_fractionalWeightBits - TanhLUT::LUT_FRAC_BITS;
312 }
313 const size_t nOutputs = network.getNumberOfNodes(layerIdx);
314 const size_t nInputs = layerInput.size();
315 layerOutput.assign(nOutputs, 0);
316 for (size_t outIdx = 0; outIdx < nOutputs; ++outIdx) {
317 int64_t mulAccSum = 0;
318 for (size_t inputIdx = 0; inputIdx < nInputs; ++inputIdx) {
319 mulAccSum += static_cast<int64_t>(layerInput[inputIdx]) * static_cast<int64_t>(m_intWeights[weightIndex++]);
320 }
321 int mulAccSumSign = (mulAccSum < 0) ? -1 : 1;
322
323 // Cutting a LUT_FRAC_BITS + MAX_FLOAT_SHIFT + HEADROOM_BITS + 1 window from the DSP
324 // This is equivalent to uint32_t lutInput = static_cast<uint32_t>(std::abs(mulAccSum) >> scalingShift);
325 const int high = scalingShift + TanhLUT::LUT_FRAC_BITS + TanhLUT::MAX_FLOAT_SHIFT + TanhLUT::HEADROOM_BITS;
326 const int low = scalingShift;
327 uint32_t lutInput = static_cast<uint32_t>(std::abs(extractBits(mulAccSum, high, low)));
328
329 int32_t outFix;
330 if (lutInput >= TanhLUT::LUT_SATURATION) {
331 outFix = static_cast<int32_t>(mulAccSumSign * ((1 << TanhLUT::LUT_FRAC_BITS) - 1));
332 } else {
333 // This is just a scaling shift (equivalent to lutInput >> MAX_FLOAT_SHIFT if LUT_FRAC_BITS == LUT_INDEX_BITS)
334 size_t lutIdx = static_cast<size_t>((lutInput * TanhLUT::LUT_SIZE) / TanhLUT::LUT_SATURATION);
335 int32_t lutVal = TanhLUT::get(lutIdx);
336 outFix = static_cast<int32_t>(mulAccSumSign * lutVal);
337 }
338 layerOutput[outIdx] = outFix;
339 }
340 layerInput = layerOutput;
341 }
342 // Convert back to float and unscale
343 std::vector<float> output(layerOutput.size(), 0.0f);
344 std::transform(layerOutput.begin(), layerOutput.end(), output.begin(),
345 [](int32_t val) { return val / static_cast<float>(1 << TanhLUT::LUT_FRAC_BITS); });
346 return unscaleTarget(output);
347}
348
349// Scale target value from [-1, 1] to outputScale
350std::vector<float> NeuroTrigger3DH::unscaleTarget(const std::vector<float>& target) const
351{
352 std::vector<float> unscaled(target.size());
353 for (size_t i = 0; i < target.size(); ++i) {
354 float min = m_neuroParameters3DH.outputScale[2 * i];
355 float max = m_neuroParameters3DH.outputScale[2 * i + 1];
356 if (i == 1) {
357 min *= Unit::deg;
358 max *= Unit::deg;
359 }
360 float range = max - min;
361 unscaled[i] = (target[i] + 1.0f) * range * 0.5f + min;
362 }
363 return unscaled;
364}
365
366// Scale target value from outputScale to [-1, 1]
367std::vector<float> NeuroTrigger3DH::scaleTarget(const std::vector<float>& target) const
368{
369 std::vector<float> scaled(target.size());
370 for (size_t i = 0; i < target.size(); ++i) {
371 float min = m_neuroParameters3DH.outputScale[2 * i];
372 float max = m_neuroParameters3DH.outputScale[2 * i + 1];
373 if (i == 1) {
374 min *= Unit::deg;
375 max *= Unit::deg;
376 }
377 float range = max - min;
378 scaled[i] = 2.0f * (target[i] - min) / range - 1.0f;
379 }
380 return scaled;
381}
Combination of several CDCHits to a track segment hit for the trigger.
short priorityTime() const
get hit time of priority cell in trigger clocks
unsigned short getPriorityPosition() const
get position of the priority cell within the track segment (0: no hit, 3: 1st priority,...
unsigned int gethitpattern() const
get hit pattern in a segment hit
unsigned short getSegmentID() const
get continuous ID of the track segment [0, 2335]
unsigned short getISuperLayer() const
get super layer number.
unsigned short getLeftRight() const
get position of the priority cell relative to the track (0: no hit, 1: right, 2: left,...
static CDCGeometryPar & Instance(const CDCGeometry *=nullptr)
Static method to get a reference to the CDCGeometryPar instance.
Class for type safe access to objects that are referred to in relations.
size_t size() const
Get number of relations.
float weight(int index) const
Get weight with index.
static const double deg
degree to radians
Definition Unit.h:109
Abstract base class for different kinds of events.