Project
Loading...
Searching...
No Matches
testMaterialPhysics.cxx
Go to the documentation of this file.
1// Copyright 2019-2020 CERN and copyright holders of ALICE O2.
2// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders.
3// All rights not expressly granted are reserved.
4//
5// This software is distributed under the terms of the GNU General Public
6// License v3 (GPL Version 3), copied verbatim in the file "COPYING".
7//
8// In applying this license CERN does not waive the privileges and immunities
9// granted to it by virtue of its status as an Intergovernmental Organization
10// or submit itself to any jurisdiction.
11
12#define BOOST_TEST_MODULE ITSMFTMaterialPhysics
13#define BOOST_TEST_MAIN
14#define BOOST_TEST_DYN_LINK
15#include <boost/test/unit_test.hpp>
16
17#include <cmath>
18#include <cstdint>
19#include <bit>
20#include <limits>
21#include <vector>
22
28
29namespace
30{
31using namespace o2::itsmft::tracking::material;
32using o2::track::PID;
33
34constexpr float AbsTol = 1.e-5f;
35constexpr float RelTol = 5.e-4f;
36
37bool closeTo(float a, float b, float absTol = AbsTol, float relTol = RelTol)
38{
39 const float diff = std::fabs(a - b);
40 return diff <= absTol || diff <= relTol * std::fabs(b);
41}
42
43// Reference copies of the production-private Highland/straggling constants,
44// used only to build the double-precision oracle below. Retained here as
45// characterization/reference evidence; not production arithmetic.
46constexpr double kHighlandConst2 = 0.0136 * 0.0136;
47constexpr double kStragglingConst = 0.0007;
48constexpr float kMinMomentumGeV = 0.01f;
49
50// Higher-precision (double) replica of the accepted capped-substep
51// algorithm. This independently re-derives, at double precision, the exact
52// sequence of operations the float production kernel performs, and serves
53// only as test-side characterization/reference evidence -- it is never
54// linked into or used by production code.
55struct Oracle {
56 double momentumAfterGeV{};
57 double signedEnergyChangeGeV{};
58 double highlandTheta2Rad2{};
59 double relativeInverseMomentumVariance{};
60 uint8_t substeps{0};
61 bool requestedAboveCap{false};
62 bool stopped{false};
63 bool nonFinite{false};
64};
65
66Oracle referenceCharged(double p0, double mass, double absCharge, double xOverX0, double arealDensity,
67 bool alongMomentum)
68{
69 Oracle oracle{};
70 const double q2 = absCharge * absCharge;
71 const double e0 = std::sqrt(p0 * p0 + mass * mass);
72 const double beta2 = (p0 * p0) / (e0 * e0);
73
74 double e = e0;
75 double p = p0;
76
77 if (arealDensity > 0.) {
78 const double ekin = e0 - mass;
79 const double bg0 = p0 / mass;
80 const double dedx0 = o2::track::BetheBlochSolidOpt<double>(bg0) * q2;
81 const double fullStepLoss = dedx0 * arealDensity;
82 const double ratio = std::fabs(fullStepLoss) / ekin * o2::track::ELoss2EKinThreshInv;
83 if (!std::isfinite(ratio) || ratio >= static_cast<double>(o2::track::MaxELossIter)) {
84 oracle.substeps = static_cast<uint8_t>(o2::track::MaxELossIter);
85 oracle.requestedAboveCap = true;
86 } else {
87 oracle.substeps = static_cast<uint8_t>(1 + static_cast<int>(ratio));
88 }
89 const double arealDensityStep = arealDensity / static_cast<double>(oracle.substeps);
90 for (uint8_t i = 0; i < oracle.substeps; ++i) {
91 const double bg = p / mass;
92 const double dedx = o2::track::BetheBlochSolidOpt<double>(bg) * q2;
93 const double dE = dedx * arealDensityStep;
94 e = alongMomentum ? (e - dE) : (e + dE);
95 if (!std::isfinite(e)) {
96 oracle.nonFinite = true;
97 break;
98 }
99 if (e <= mass) {
100 oracle.stopped = true;
101 break;
102 }
103 p = std::sqrt(e * e - mass * mass);
104 if (!std::isfinite(p)) {
105 oracle.nonFinite = true;
106 break;
107 }
108 }
109 }
110
111 oracle.momentumAfterGeV = p;
112 oracle.signedEnergyChangeGeV = e - e0;
113 oracle.highlandTheta2Rad2 = (xOverX0 > 0.) ? (kHighlandConst2 / (beta2 * p0 * p0) * xOverX0 * q2) : 0.;
114 oracle.relativeInverseMomentumVariance = (oracle.signedEnergyChangeGeV != 0.)
115 ? (kStragglingConst * kStragglingConst * std::fabs(oracle.signedEnergyChangeGeV) * e0 * e0 / (p0 * p0 * p0 * p0))
116 : 0.;
117 return oracle;
118}
119
120float energyChange(float before, float after, PID pid)
121{
122 const double mass = pid.getMass();
123 return std::sqrt(static_cast<double>(after) * after + mass * mass) -
124 std::sqrt(static_cast<double>(before) * before + mass * mass);
125}
126
127} // namespace
128
129BOOST_AUTO_TEST_CASE(EveryValidMassivePidIdChargedSucceeds)
130{
131 IntegratedMaterialBudget material{0.01f, 0.05f};
132 for (uint8_t id = 0; id < PID::NIDsTot; ++id) {
133 PID pid(static_cast<PID::ID>(id));
134 if (pid.getMass() == 0.f) {
135 continue; // massless PIDs are covered by ChargedMasslessRejection below
136 }
137
138 float resultMomentum = 0.f;
139 float resultTheta2 = 0.f;
140 float resultVariance = 0.f;
141 const bool result = calculateMaterialPhysics(2.f, pid, 1, MaterialTraversalDirection::AlongMomentum, material, resultMomentum, resultTheta2, resultVariance);
142 BOOST_CHECK_MESSAGE(result, "PID id " << static_cast<int>(id) << " failed");
143 }
144}
145
146BOOST_AUTO_TEST_CASE(InvalidPidIdsRejectedBeforeMassLookup)
147{
148 IntegratedMaterialBudget material{0.f, 0.f};
149 for (uint8_t id : {static_cast<uint8_t>(PID::NIDsTot), static_cast<uint8_t>(255)}) {
150 PID pid(static_cast<PID::ID>(id));
151
152 float chargedMomentum = 0.f;
153 float chargedTheta2 = 0.f;
154 float chargedVariance = 0.f;
155 const bool charged = calculateMaterialPhysics(1.f, pid, 1, MaterialTraversalDirection::AlongMomentum, material, chargedMomentum, chargedTheta2, chargedVariance);
156 BOOST_CHECK(!charged);
157 BOOST_CHECK_EQUAL(chargedMomentum, 0.f);
158 BOOST_CHECK_EQUAL(chargedTheta2, 0.f);
159 BOOST_CHECK_EQUAL(chargedVariance, 0.f);
160 }
161}
162
163BOOST_AUTO_TEST_CASE(PidAndChargeAreIndependent)
164{
165 // PID::Electron has a nominal charge of 1 in the PID table, but absCharge
166 // is supplied independently and must be the only source of q^2 scaling.
167 IntegratedMaterialBudget material{0.05f, 0.f};
168
169 float q1Momentum = 0.f;
170 float q1Theta2 = 0.f;
171 float q1Variance = 0.f;
172 const bool q1 = calculateMaterialPhysics(2.f, PID::Electron, 1, MaterialTraversalDirection::AlongMomentum, material, q1Momentum, q1Theta2, q1Variance);
173
174 float q2resultMomentum = 0.f;
175 float q2resultTheta2 = 0.f;
176 float q2resultVariance = 0.f;
177 const bool q2result = calculateMaterialPhysics(2.f, PID::Electron, 2, MaterialTraversalDirection::AlongMomentum, material, q2resultMomentum, q2resultTheta2, q2resultVariance);
178 BOOST_REQUIRE(q1);
179 BOOST_REQUIRE(q2result);
180 // Highland variance scales with absCharge^2, independent of PID::getCharge().
181 BOOST_CHECK(closeTo(q2resultTheta2, 4.f * q1Theta2));
182}
183
184BOOST_AUTO_TEST_CASE(ChargedMasslessRejected)
185{
186 IntegratedMaterialBudget material{0.f, 0.f};
187 for (uint8_t absCharge : {1, 2, 3, 255}) {
188
189 float resultMomentum = 0.f;
190 float resultTheta2 = 0.f;
191 float resultVariance = 0.f;
192 const bool result = calculateMaterialPhysics(1.f, PID::Photon, absCharge, MaterialTraversalDirection::AlongMomentum, material, resultMomentum, resultTheta2, resultVariance);
194 BOOST_CHECK_EQUAL(resultMomentum, 0.f);
195 BOOST_CHECK_EQUAL(resultTheta2, 0.f);
196 BOOST_CHECK_EQUAL(resultVariance, 0.f);
197 }
198}
199
200BOOST_AUTO_TEST_CASE(AbsChargeVariantsScaleHighlandQuadratically)
201{
202 IntegratedMaterialBudget material{0.03f, 0.f}; // MCS-only: isolates the charge scaling.
203
204 float baseMomentum = 0.f;
205 float baseTheta2 = 0.f;
206 float baseVariance = 0.f;
207 const bool base = calculateMaterialPhysics(1.5f, PID::Pion, 1, MaterialTraversalDirection::AlongMomentum, material, baseMomentum, baseTheta2, baseVariance);
208 BOOST_REQUIRE(base);
209 for (uint8_t absCharge : {2, 3, 200}) {
210
211 float resultMomentum = 0.f;
212 float resultTheta2 = 0.f;
213 float resultVariance = 0.f;
214 const bool result = calculateMaterialPhysics(1.5f, PID::Pion, absCharge, MaterialTraversalDirection::AlongMomentum, material, resultMomentum, resultTheta2, resultVariance);
215 BOOST_REQUIRE(result);
216 const float expectedRatio = static_cast<float>(absCharge) * static_cast<float>(absCharge);
217 BOOST_CHECK(closeTo(resultTheta2, expectedRatio * baseTheta2));
218 }
219}
220
221BOOST_AUTO_TEST_CASE(DirectionInvalidCastRejected)
222{
223 IntegratedMaterialBudget material{0.f, 0.f};
224 for (uint8_t raw : {2, 255}) {
225 auto direction = static_cast<MaterialTraversalDirection>(raw);
226
227 float resultMomentum = 0.f;
228 float resultTheta2 = 0.f;
229 float resultVariance = 0.f;
230 const bool result = calculateMaterialPhysics(1.f, PID::Pion, 1, direction, material, resultMomentum, resultTheta2, resultVariance);
232 BOOST_CHECK_EQUAL(resultMomentum, 0.f);
233 BOOST_CHECK_EQUAL(resultTheta2, 0.f);
234 BOOST_CHECK_EQUAL(resultVariance, 0.f);
235 }
236}
237
238BOOST_AUTO_TEST_CASE(MaterialFieldsMustBeNonNegative)
239{
240 const std::vector<IntegratedMaterialBudget> invalidMaterials = {
241 {-1.f, 0.1f}, {0.1f, -1.f}, {-1.f, -1.f}};
242 for (auto material : invalidMaterials) {
243
244 float resultMomentum = 0.f;
245 float resultTheta2 = 0.f;
246 float resultVariance = 0.f;
247 const bool result = calculateMaterialPhysics(1.f, PID::Pion, 1, MaterialTraversalDirection::AlongMomentum, material, resultMomentum, resultTheta2, resultVariance);
249 BOOST_CHECK_EQUAL(resultMomentum, 0.f);
250 BOOST_CHECK_EQUAL(resultTheta2, 0.f);
251 BOOST_CHECK_EQUAL(resultVariance, 0.f);
252 }
253}
254
255BOOST_AUTO_TEST_CASE(MomentumMustBePositive)
256{
257 IntegratedMaterialBudget material{0.f, 0.f};
258 for (float momentum : {0.f, -1.f}) {
259
260 float resultMomentum = 0.f;
261 float resultTheta2 = 0.f;
262 float resultVariance = 0.f;
263 const bool result = calculateMaterialPhysics(momentum, PID::Pion, 1, MaterialTraversalDirection::AlongMomentum, material, resultMomentum, resultTheta2, resultVariance);
265 BOOST_CHECK_EQUAL(resultMomentum, 0.f);
266 BOOST_CHECK_EQUAL(resultTheta2, 0.f);
267 BOOST_CHECK_EQUAL(resultVariance, 0.f);
268 }
269}
270
271BOOST_AUTO_TEST_CASE(ZeroMaterialIsAPassThrough)
272{
273 IntegratedMaterialBudget material{0.f, 0.f};
274
275 float resultMomentum = 0.f;
276 float resultTheta2 = 0.f;
277 float resultVariance = 0.f;
278 const bool result = calculateMaterialPhysics(1.f, PID::Pion, 1, MaterialTraversalDirection::AlongMomentum, material, resultMomentum, resultTheta2, resultVariance);
279 BOOST_REQUIRE(result);
280 BOOST_CHECK_EQUAL(resultMomentum, 1.f);
281 BOOST_CHECK_EQUAL(energyChange(1.f, resultMomentum, PID::Pion), 0.f);
282 BOOST_CHECK_EQUAL(resultTheta2, 0.f);
283 BOOST_CHECK_EQUAL(resultVariance, 0.f);
284}
285
286BOOST_AUTO_TEST_CASE(McsOnlyMaterialMatchesAnalyticHighland)
287{
288 const float p0 = 2.f;
289 const float mass = PID(PID::Pion).getMass();
290 IntegratedMaterialBudget material{0.05f, 0.f};
291
292 float resultMomentum = 0.f;
293 float resultTheta2 = 0.f;
294 float resultVariance = 0.f;
295 const bool result = calculateMaterialPhysics(p0, PID::Pion, 1, MaterialTraversalDirection::AlongMomentum, material, resultMomentum, resultTheta2, resultVariance);
296 BOOST_REQUIRE(result);
297 BOOST_CHECK_EQUAL(resultMomentum, p0);
298 BOOST_CHECK_EQUAL(energyChange(p0, resultMomentum, PID::Pion), 0.f);
299
300 BOOST_CHECK_EQUAL(resultVariance, 0.f);
301
302 const double e0 = std::sqrt(static_cast<double>(p0) * p0 + static_cast<double>(mass) * mass);
303 const double beta2 = (static_cast<double>(p0) * p0) / (e0 * e0);
304 const double expectedTheta2 = kHighlandConst2 / (beta2 * p0 * p0) * material.xOverX0;
305 BOOST_CHECK(closeTo(resultTheta2, static_cast<float>(expectedTheta2)));
306}
307
308BOOST_AUTO_TEST_CASE(EnergyLossOnlyMaterialProducesNoScattering)
309{
310 IntegratedMaterialBudget material{0.f, 0.02f};
311
312 float resultMomentum = 0.f;
313 float resultTheta2 = 0.f;
314 float resultVariance = 0.f;
315 const bool result = calculateMaterialPhysics(2.f, PID::Pion, 1, MaterialTraversalDirection::AlongMomentum, material, resultMomentum, resultTheta2, resultVariance);
316 BOOST_REQUIRE(result);
317 BOOST_CHECK_EQUAL(resultTheta2, 0.f);
318 BOOST_CHECK_LT(resultMomentum, 2.f);
319 BOOST_CHECK_LT(energyChange(2.f, resultMomentum, PID::Pion), 0.f);
320
321 BOOST_CHECK_GT(resultVariance, 0.f);
322}
323
324BOOST_AUTO_TEST_CASE(CombinedMaterialMatchesOracle)
325{
326 const float p0 = 1.2f;
327 const PID pid = PID::Kaon;
328 const uint8_t absCharge = 1;
329 IntegratedMaterialBudget material{0.04f, 0.03f};
330
331 float resultMomentum = 0.f;
332 float resultTheta2 = 0.f;
333 float resultVariance = 0.f;
334 const bool result = calculateMaterialPhysics(p0, pid, absCharge, MaterialTraversalDirection::AlongMomentum, material, resultMomentum, resultTheta2, resultVariance);
335 BOOST_REQUIRE(result);
336
337 auto oracle = referenceCharged(p0, pid.getMass(), absCharge, material.xOverX0, material.arealDensityGPerCm2, true);
338 BOOST_CHECK(!oracle.stopped && !oracle.nonFinite);
339
340 BOOST_CHECK(closeTo(resultMomentum, static_cast<float>(oracle.momentumAfterGeV)));
341 BOOST_CHECK(closeTo(energyChange(p0, resultMomentum, pid), static_cast<float>(oracle.signedEnergyChangeGeV)));
342 BOOST_CHECK(closeTo(resultTheta2, static_cast<float>(oracle.highlandTheta2Rad2)));
343 BOOST_CHECK(closeTo(resultVariance, static_cast<float>(oracle.relativeInverseMomentumVariance)));
344}
345
346BOOST_AUTO_TEST_CASE(LossAndGainHaveOppositeSignedEnergyChange)
347{
348 const float p0 = 1.5f;
349 IntegratedMaterialBudget material{0.f, 0.005f}; // small enough to stay single-substep
350
351 float lossMomentum = 0.f;
352 float lossTheta2 = 0.f;
353 float lossVariance = 0.f;
354 const bool loss = calculateMaterialPhysics(p0, PID::Proton, 1, MaterialTraversalDirection::AlongMomentum, material, lossMomentum, lossTheta2, lossVariance);
355
356 float gainMomentum = 0.f;
357 float gainTheta2 = 0.f;
358 float gainVariance = 0.f;
359 const bool gain = calculateMaterialPhysics(p0, PID::Proton, 1, MaterialTraversalDirection::OppositeMomentum, material, gainMomentum, gainTheta2, gainVariance);
360 BOOST_REQUIRE(loss);
361 BOOST_REQUIRE(gain);
362
363 BOOST_CHECK_LT(energyChange(p0, lossMomentum, PID::Proton), 0.f);
364 BOOST_CHECK_GT(energyChange(p0, gainMomentum, PID::Proton), 0.f);
365 BOOST_CHECK(closeTo(energyChange(p0, lossMomentum, PID::Proton), -energyChange(p0, gainMomentum, PID::Proton), AbsTol, 1.e-2f));
366 BOOST_CHECK_LT(lossMomentum, p0);
367 BOOST_CHECK_GT(gainMomentum, p0);
368}
369
370BOOST_AUTO_TEST_CASE(MaterialAcrossSubstepRangeMatchesOracle)
371{
372 const float p0 = 1.f;
373 const PID pid = PID::Proton;
374 const double mass = pid.getMass();
375 const double e0 = std::sqrt(static_cast<double>(p0) * p0 + mass * mass);
376 const double ekin = e0 - mass;
377 const double bg0 = p0 / mass;
378 const double dedx0 = o2::track::BetheBlochSolidOpt<double>(bg0);
379
380 auto arealDensityForRatio = [&](double ratio) {
381 return ratio * ekin / (o2::track::ELoss2EKinThreshInv * dedx0);
382 };
383
384 // OppositeMomentum (energy gain) is used deliberately: it isolates the
385 // substep-count bookkeeping from the (physically legitimate) risk that a
386 // large requested ratio also represents more energy loss than the
387 // particle's kinetic energy can absorb, which is covered separately by
388 // the StoppingIsDetected test.
389 for (const double ratio : {0.3, 5.5, 48.9, 49.5, 60.0, 1.e6}) {
390 IntegratedMaterialBudget material{0.f, static_cast<float>(arealDensityForRatio(ratio))};
391
392 float resultMomentum = 0.f;
393 float resultTheta2 = 0.f;
394 float resultVariance = 0.f;
395 const bool result = calculateMaterialPhysics(p0, pid, 1, MaterialTraversalDirection::OppositeMomentum, material, resultMomentum, resultTheta2, resultVariance);
396 BOOST_REQUIRE_MESSAGE(result, "unexpected failure for ratio " << ratio);
397
398 auto oracle = referenceCharged(p0, mass, 1., 0., material.arealDensityGPerCm2, false);
399 BOOST_REQUIRE(!oracle.stopped && !oracle.nonFinite);
400 BOOST_CHECK(closeTo(resultMomentum, static_cast<float>(oracle.momentumAfterGeV)));
401 }
402}
403
404BOOST_AUTO_TEST_CASE(ClampedSubstepsStillProcessCompleteArealDensity)
405{
406 // Use OppositeMomentum (energy gain) so a very large ratio clamps the
407 // substep count without stopping the particle, letting us verify the
408 // full arealDensityGPerCm2 was processed across exactly 50 substeps.
409 const float p0 = 1.f;
410 const PID pid = PID::Proton;
411 IntegratedMaterialBudget material{0.f, 500.f};
412
413 float resultMomentum = 0.f;
414 float resultTheta2 = 0.f;
415 float resultVariance = 0.f;
416 const bool result = calculateMaterialPhysics(p0, pid, 1, MaterialTraversalDirection::OppositeMomentum, material, resultMomentum, resultTheta2, resultVariance);
417 BOOST_REQUIRE(result);
418
419 auto oracle = referenceCharged(p0, pid.getMass(), 1., 0., material.arealDensityGPerCm2, false);
420 BOOST_REQUIRE(!oracle.stopped && !oracle.nonFinite);
421 BOOST_CHECK_EQUAL(oracle.substeps, o2::track::MaxELossIter);
422 BOOST_CHECK(closeTo(energyChange(p0, resultMomentum, pid), static_cast<float>(oracle.signedEnergyChangeGeV), AbsTol, 2.e-3f));
423}
424
425BOOST_AUTO_TEST_CASE(BetheBlochIsRecomputedPerSubstep)
426{
427 // A naive fixed-dedx-at-entry integration must differ measurably from the
428 // recompute-per-substep result once the momentum changes appreciably
429 // across the traversal.
430 const float p0 = 0.3f;
431 const PID pid = PID::Proton;
432 const double mass = pid.getMass();
433 IntegratedMaterialBudget material{0.f, 1.f};
434
435 float resultMomentum = 0.f;
436 float resultTheta2 = 0.f;
437 float resultVariance = 0.f;
438 const bool result = calculateMaterialPhysics(p0, pid, 1, MaterialTraversalDirection::AlongMomentum, material, resultMomentum, resultTheta2, resultVariance);
439 BOOST_REQUIRE(result);
440
441 const double e0 = std::sqrt(static_cast<double>(p0) * p0 + mass * mass);
442 const double bg0 = p0 / mass;
443 const double dedx0 = o2::track::BetheBlochSolidOpt<double>(bg0);
444 const double naiveEnergyAfter = e0 - dedx0 * material.arealDensityGPerCm2;
445
446 auto oracle = referenceCharged(p0, mass, 1., 0., material.arealDensityGPerCm2, true);
447 BOOST_REQUIRE(!oracle.stopped && !oracle.nonFinite);
448 const double recomputedEnergyAfter = e0 + oracle.signedEnergyChangeGeV;
449
450 BOOST_CHECK(closeTo(energyChange(p0, resultMomentum, pid), static_cast<float>(oracle.signedEnergyChangeGeV)));
451 BOOST_CHECK_GT(std::fabs(recomputedEnergyAfter - naiveEnergyAfter), 1.e-4);
452}
453
454BOOST_AUTO_TEST_CASE(StoppingIsDetected)
455{
456 IntegratedMaterialBudget material{0.f, 50.f}; // grossly exceeds a 0.5 GeV/c proton's kinetic energy
457
458 float resultMomentum = 0.f;
459 float resultTheta2 = 0.f;
460 float resultVariance = 0.f;
461 const bool result = calculateMaterialPhysics(0.5f, PID::Proton, 1, MaterialTraversalDirection::AlongMomentum, material, resultMomentum, resultTheta2, resultVariance);
463 BOOST_CHECK_EQUAL(resultMomentum, 0.f);
464 BOOST_CHECK_EQUAL(resultTheta2, 0.f);
465 BOOST_CHECK_EQUAL(resultVariance, 0.f);
466}
467
468BOOST_AUTO_TEST_CASE(FinalMomentumBoundary)
469{
470 IntegratedMaterialBudget material{0.f, 0.f}; // zero material: momentumAfter == momentumBefore exactly
471
472 float atThresholdMomentum = 0.f;
473 float atThresholdTheta2 = 0.f;
474 float atThresholdVariance = 0.f;
475 const bool atThreshold = calculateMaterialPhysics(kMinMomentumGeV, PID::Pion, 1, MaterialTraversalDirection::AlongMomentum, material, atThresholdMomentum, atThresholdTheta2, atThresholdVariance);
476 BOOST_REQUIRE(atThreshold);
477 BOOST_CHECK_EQUAL(atThresholdMomentum, kMinMomentumGeV);
478
479 float belowThresholdMomentum = 0.f;
480 float belowThresholdTheta2 = 0.f;
481 float belowThresholdVariance = 0.f;
482 const bool belowThreshold = calculateMaterialPhysics(std::nextafter(kMinMomentumGeV, 0.f), PID::Pion, 1,
483 MaterialTraversalDirection::AlongMomentum, material, belowThresholdMomentum, belowThresholdTheta2, belowThresholdVariance);
484 BOOST_CHECK(!belowThreshold);
485 BOOST_CHECK_EQUAL(belowThresholdMomentum, 0.f);
486 BOOST_CHECK_EQUAL(belowThresholdTheta2, 0.f);
487 BOOST_CHECK_EQUAL(belowThresholdVariance, 0.f);
488}
489
490BOOST_AUTO_TEST_CASE(ExcessiveScatteringIsRejected)
491{
492 IntegratedMaterialBudget material{500.f, 0.f}; // absurdly thick, drives theta^2 past pi^2
493
494 float resultMomentum = 0.f;
495 float resultTheta2 = 0.f;
496 float resultVariance = 0.f;
497 const bool result = calculateMaterialPhysics(0.1f, PID::Pion, 1, MaterialTraversalDirection::AlongMomentum, material, resultMomentum, resultTheta2, resultVariance);
499 BOOST_CHECK_EQUAL(resultMomentum, 0.f);
500 BOOST_CHECK_EQUAL(resultTheta2, 0.f);
501 BOOST_CHECK_EQUAL(resultVariance, 0.f);
502}
503
504BOOST_AUTO_TEST_CASE(HugeFiniteArealDensityDeterministicallyStops)
505{
506 // 1e30 g/cm^2 is many orders of magnitude beyond what a 1 GeV/c proton's
507 // kinetic energy can absorb: even after the substep count clamps to 50
508 // (since the requested count vastly exceeds it), the very first substep's
509 // energy loss drives the particle's energy far below its rest mass. This
510 // must terminate deterministically without any float-to-int UB in the
511 // substep-count calculation.
512 const float p0 = 1.f;
513 IntegratedMaterialBudget material{0.f, 1.e30f};
514
515 float resultMomentum = 0.f;
516 float resultTheta2 = 0.f;
517 float resultVariance = 0.f;
518 const bool result = calculateMaterialPhysics(p0, PID::Proton, 1, MaterialTraversalDirection::AlongMomentum, material, resultMomentum, resultTheta2, resultVariance);
520 BOOST_CHECK_EQUAL(resultMomentum, 0.f);
521 BOOST_CHECK_EQUAL(resultTheta2, 0.f);
522 BOOST_CHECK_EQUAL(resultVariance, 0.f);
523
524 float repeatMomentum = 0.f;
525 float repeatTheta2 = 0.f;
526 float repeatVariance = 0.f;
527 const bool repeat = calculateMaterialPhysics(p0, PID::Proton, 1, MaterialTraversalDirection::AlongMomentum, material, repeatMomentum, repeatTheta2, repeatVariance);
528 BOOST_CHECK_EQUAL(result, repeat);
529 BOOST_CHECK_EQUAL(std::bit_cast<uint32_t>(resultMomentum), std::bit_cast<uint32_t>(repeatMomentum));
530 BOOST_CHECK_EQUAL(std::bit_cast<uint32_t>(resultTheta2), std::bit_cast<uint32_t>(repeatTheta2));
531 BOOST_CHECK_EQUAL(std::bit_cast<uint32_t>(resultVariance), std::bit_cast<uint32_t>(repeatVariance));
532}
533
534BOOST_AUTO_TEST_CASE(DirectBetheBlochReferenceValue)
535{
536 const float p0 = 1.f;
537 const PID pid = PID::Proton;
538 const double mass = pid.getMass();
539 IntegratedMaterialBudget material{0.f, 0.001f}; // small enough to guarantee a single substep
540
541 float resultMomentum = 0.f;
542 float resultTheta2 = 0.f;
543 float resultVariance = 0.f;
544 const bool result = calculateMaterialPhysics(p0, pid, 1, MaterialTraversalDirection::AlongMomentum, material, resultMomentum, resultTheta2, resultVariance);
545 BOOST_REQUIRE(result);
546
547 const double e0 = std::sqrt(static_cast<double>(p0) * p0 + mass * mass);
548 const double bg0 = p0 / mass;
549 const double dedx = o2::track::BetheBlochSolidOpt<double>(bg0);
550 const double expectedEnergyAfter = e0 - dedx * material.arealDensityGPerCm2;
551 const double expectedSignedChange = expectedEnergyAfter - e0;
552 BOOST_CHECK(closeTo(energyChange(p0, resultMomentum, pid), static_cast<float>(expectedSignedChange)));
553}
554
555BOOST_AUTO_TEST_CASE(ChargeSquaredScalesSingleSubstepEnergyLoss)
556{
557 // Material thin enough that absCharge up to 3 (q^2 up to 9) still resolves
558 // to a single substep for every case below. PID::Electron's nominal
559 // PID::getCharge() is fixed at 1 regardless of absCharge, so any observed
560 // scaling with absCharge (not with PID::getCharge()) demonstrates that
561 // getCharge() is never consulted.
562 const float p0 = 1.f;
563 const PID pid = PID::Electron;
564 const double mass = pid.getMass();
565 const IntegratedMaterialBudget material{0.f, 0.0001f};
566
567 const double e0 = std::sqrt(static_cast<double>(p0) * p0 + mass * mass);
568 const double bg0 = p0 / mass;
569 const double dedxUnit = o2::track::BetheBlochSolidOpt<double>(bg0); // reference dE/dx at q^2 = 1
570
571 float baseSignedChange = 0.f;
572 float baseVariance = 0.f;
573 for (uint8_t absCharge : {1, 2, 3}) {
574
575 float resultMomentum = 0.f;
576 float resultTheta2 = 0.f;
577 float resultVariance = 0.f;
578 const bool result = calculateMaterialPhysics(p0, pid, absCharge, MaterialTraversalDirection::AlongMomentum, material, resultMomentum, resultTheta2, resultVariance);
579 BOOST_REQUIRE(result);
580
581 const double q2 = static_cast<double>(absCharge) * absCharge;
582 const double expectedDE = dedxUnit * q2 * material.arealDensityGPerCm2;
583 const double expectedEnergyAfter = e0 - expectedDE;
584 const double expectedSignedChange = expectedEnergyAfter - e0;
585 const double expectedMomentumAfter = std::sqrt(expectedEnergyAfter * expectedEnergyAfter - mass * mass);
586 const double expectedVariance = kStragglingConst * kStragglingConst * std::fabs(expectedSignedChange) * e0 * e0 /
587 (static_cast<double>(p0) * p0 * p0 * p0);
588
589 BOOST_CHECK(closeTo(energyChange(p0, resultMomentum, pid), static_cast<float>(expectedSignedChange)));
590 BOOST_CHECK(closeTo(resultMomentum, static_cast<float>(expectedMomentumAfter)));
591 BOOST_CHECK(closeTo(resultVariance, static_cast<float>(expectedVariance)));
592
593 if (absCharge == 1) {
594 baseSignedChange = energyChange(p0, resultMomentum, pid);
595 baseVariance = resultVariance;
596 } else {
597 const float q2f = static_cast<float>(absCharge) * static_cast<float>(absCharge);
598 BOOST_CHECK(closeTo(energyChange(p0, resultMomentum, pid), q2f * baseSignedChange));
599 BOOST_CHECK(closeTo(resultVariance, q2f * baseVariance));
600 }
601 }
602}
603
604BOOST_AUTO_TEST_CASE(RepeatedCallsHaveIdenticalPhysicsOutputs)
605{
606 IntegratedMaterialBudget material{0.03f, 0.02f};
607
608 float aMomentum = 0.f;
609 float aTheta2 = 0.f;
610 float aVariance = 0.f;
611 const bool a = calculateMaterialPhysics(1.3f, PID::Kaon, 1, MaterialTraversalDirection::AlongMomentum, material, aMomentum, aTheta2, aVariance);
612
613 float bMomentum = 0.f;
614 float bTheta2 = 0.f;
615 float bVariance = 0.f;
616 const bool b = calculateMaterialPhysics(1.3f, PID::Kaon, 1, MaterialTraversalDirection::AlongMomentum, material, bMomentum, bTheta2, bVariance);
618 BOOST_CHECK_EQUAL(std::bit_cast<uint32_t>(aMomentum), std::bit_cast<uint32_t>(bMomentum));
619 BOOST_CHECK_EQUAL(std::bit_cast<uint32_t>(aTheta2), std::bit_cast<uint32_t>(bTheta2));
620 BOOST_CHECK_EQUAL(std::bit_cast<uint32_t>(aVariance), std::bit_cast<uint32_t>(bVariance));
621}
particle ids, masses, names class definition
int32_t i
o2::raw::RawFileWriter * raw
useful math constants
uint16_t pid
Definition RawData.h:2
o2::track::PID PID
Definition SVertexer.cxx:36
pid_constants::ID ID
Definition PID.h:92
GLuint64EXT * result
Definition glcorearb.h:5662
GLboolean GLboolean GLboolean b
Definition glcorearb.h:1233
GLboolean GLboolean GLboolean GLboolean a
Definition glcorearb.h:1233
GLuint id
Definition glcorearb.h:650
bool calculateMaterialPhysics(float momentumGeV, o2::track::PID pid, uint8_t absCharge, MaterialTraversalDirection direction, IntegratedMaterialBudget material, float &momentumAfterGeV, float &highlandTheta2Rad2, float &relativeInverseMomentumVariance) noexcept
BOOST_AUTO_TEST_CASE(EveryValidMassivePidIdChargedSucceeds)
BOOST_CHECK(tree)
BOOST_CHECK_EQUAL(triggersD.size(), triggers.size())