Skip to content

Commit 96754fd

Browse files
authored
Add energy and range straggeling to energy loss models (ATTPC#256)
* Add interface for straggling (range and energy) * Add and test range straggling to Eloss models (CATIMA and SRIM) * Fix density scaling to SRIM (dE/dx was scaled, energy loss was not) * Remove scaling parameter in AtELossModel. Enforce scaling in SetDensity function. * Add range test for CATIMA at different density * Set environment variables before running tests
1 parent 1dffcc6 commit 96754fd

12 files changed

Lines changed: 581 additions & 51 deletions

File tree

.github/workflows/CI-tests.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ on: [push, pull_request]
88
env:
99
# Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.)
1010
BUILD_TYPE: RELWITHDEBINFO
11+
1112

1213
jobs:
1314
build:
@@ -16,7 +17,6 @@ jobs:
1617
# See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix
1718
runs-on: ubuntu-latest
1819
container: anthoak13/attpcroot-deps
19-
2020
steps:
2121
- uses: actions/checkout@v4
2222

@@ -33,5 +33,6 @@ jobs:
3333
# working-directory: ${{github.workspace}}/build
3434
# Execute tests defined by the CMake configuration.
3535
# See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail
36-
run: cd ${{github.workspace}}/build && ctest
36+
run: cd ${{github.workspace}}/build && source config.sh && ctest
37+
shell: bash
3738

AtTools/AtELossCATIMA.cxx

Lines changed: 72 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,34 @@
11
#include "AtELossCATIMA.h"
22

33
#include <FairLogger.h>
4+
namespace AtTools {
45

5-
AtTools::AtELossCATIMA::AtELossCATIMA(double density, std::vector<std::tuple<int, int, int>> materialComponents)
6+
AtELossCATIMA::AtELossCATIMA(double density, std::vector<std::tuple<int, int, int>> materialComponents)
67
: AtELossModel(density)
78
{
8-
fMaterial = std::make_unique<catima::Material>();
9-
10-
for (auto materialComponent : materialComponents)
11-
fMaterial->add_element(std::get<0>(materialComponent), std::get<1>(materialComponent),
12-
std::get<2>(materialComponent));
9+
SetMaterial(materialComponents);
1310
}
1411

15-
double AtTools::AtELossCATIMA::GetdEdx(double energy) const
12+
double AtELossCATIMA::GetdEdx(double energy) const
1613
{
1714
if (fProjectile == nullptr) {
1815
LOG(warning)
1916
<< " Warning in AtTools::AtELossCATIMA::GetdEdx : The projectile was not set! GetdEdx will return 0!";
2017
return 0;
2118
}
2219

23-
if (fProjectileMassUma <= 0) {
20+
if (fProjectileMassAmu <= 0) {
2421
LOG(error) << " Error in AtTools::AtELossCATIMA::GetdEdx : The projectile's mass in umas can not be <= 0! "
2522
"GetdEdx will return 0!";
2623
return 0;
2724
}
2825

29-
catima::Result result = catima::calculate(*fProjectile, *fMaterial, energy / fProjectileMassUma);
30-
double dEdx = result.dEdxi * fDensity;
31-
return dEdx;
26+
catima::Result result = catima::calculate(*fProjectile, *fMaterial, energy / fProjectileMassAmu);
27+
double dEdx = result.dEdxi * fDensity; // MeV/cm
28+
return dEdx / 10.0; // convert to MeV/mm
3229
}
3330

34-
double AtTools::AtELossCATIMA::GetRange(double energyIni, double energyFin) const
31+
double AtELossCATIMA::GetRange(double energyIni, double energyFin) const
3532
{
3633
if (energyFin < 0) {
3734
LOG(warning) << " Warning in AtTools::AtELossCATIMA::GetRange : The final energy was set to a negative value! "
@@ -40,14 +37,14 @@ double AtTools::AtELossCATIMA::GetRange(double energyIni, double energyFin) cons
4037
}
4138

4239
if (energyFin == 0) {
43-
fProjectile->T = energyIni / fProjectileMassUma;
40+
fProjectile->T = energyIni / fProjectileMassAmu;
4441
return catima::range(*fProjectile, *fMaterial) / fDensity * 10.;
4542
}
4643

4744
double remainingEnergy{energyIni};
4845
double range{0};
4946
while (remainingEnergy > energyFin) {
50-
catima::Result result = catima::calculate(*fProjectile, *fMaterial, remainingEnergy / fProjectileMassUma);
47+
catima::Result result = catima::calculate(*fProjectile, *fMaterial, remainingEnergy / fProjectileMassAmu);
5148
double dEdx = result.dEdxi * fDensity;
5249
double DE = dEdx * fRangeStepSize / 10.;
5350

@@ -63,12 +60,12 @@ double AtTools::AtELossCATIMA::GetRange(double energyIni, double energyFin) cons
6360
return range;
6461
}
6562

66-
double AtTools::AtELossCATIMA::GetEnergy(double energyIni, double distance) const
63+
double AtELossCATIMA::GetEnergy(double energyIni, double distance) const
6764
{
6865
double remainingEnergy{energyIni};
6966
double range{0};
7067
while (range < distance) {
71-
catima::Result result = catima::calculate(*fProjectile, *fMaterial, remainingEnergy / fProjectileMassUma);
68+
catima::Result result = catima::calculate(*fProjectile, *fMaterial, remainingEnergy / fProjectileMassAmu);
7269
double dEdx = result.dEdxi * fDensity;
7370
double DE{};
7471

@@ -86,9 +83,55 @@ double AtTools::AtELossCATIMA::GetEnergy(double energyIni, double distance) cons
8683

8784
return remainingEnergy;
8885
}
86+
double AtELossCATIMA::GetRangeVariance(double energy) const
87+
{
88+
if (fProjectile == nullptr || fMaterial == nullptr) {
89+
LOG(error) << "Projectile or material not set. Range variance is 0.";
90+
return 0;
91+
}
92+
auto range_var =
93+
catima::range_variance(*fProjectile, energy / fProjectileMassAmu, *fMaterial); // range var in (g/cm^2)^2
94+
LOG(debug) << "Range variance in (g/cm^2)^2: " << range_var << " for energy: " << energy / fProjectileMassAmu
95+
<< " MeV/u";
96+
range_var /= fDensity * fDensity; // convert to (cm)^2
97+
LOG(debug) << "Range variance in (cm)^2: " << range_var << " for energy: " << energy / fProjectileMassAmu
98+
<< " MeV/u";
99+
return range_var * 100; // convert to mm^2
100+
}
101+
102+
double AtELossCATIMA::GetElossStraggling(double energyIni, double energyFin) const
103+
{
104+
if (fProjectile == nullptr || fMaterial == nullptr) {
105+
LOG(error) << "Projectile or material not set.";
106+
return 0;
107+
}
108+
if (energyFin > energyIni) {
109+
LOG(error) << "Final energy must be less than initial energy!";
110+
return 0;
111+
}
112+
auto energy_strag = catima::energy_straggling_from_E(*fProjectile, energyIni / fProjectileMassAmu,
113+
energyFin / fProjectileMassAmu, *fMaterial);
114+
return energy_strag;
115+
}
116+
double AtELossCATIMA::GetdEdxStraggling(double energyIni, double energyFin) const
117+
{
118+
if (fProjectile == nullptr || fMaterial == nullptr) {
119+
LOG(error) << "Projectile or material not set. dEdx straggling is 0.";
120+
return 0;
121+
}
122+
auto dedx_min = GetdEdx(energyIni);
123+
auto dedx_max = GetdEdx(energyFin);
124+
if (std::abs(dedx_min - dedx_max) / dedx_min > 0.01) {
125+
LOG(warning) << "From " << energyIni << " to " << energyFin
126+
<< " MeV the dEdx is not constant. dEdx straggling calculation is unreliable.";
127+
}
128+
auto dE_st = GetElossStraggling(energyIni, energyFin);
129+
auto factor = dE_st / (energyIni - energyFin);
130+
return factor * dedx_min;
131+
}
89132

90133
std::vector<std::pair<double, double>>
91-
AtTools::AtELossCATIMA::GetBraggCurve(double energy, double rangeStepSize, double totalFractionELoss) const
134+
AtELossCATIMA::GetBraggCurve(double energy, double rangeStepSize, double totalFractionELoss) const
92135
{
93136
if (rangeStepSize == 0)
94137
return GetBraggCurve(energy, fRangeStepSize, totalFractionELoss);
@@ -99,7 +142,7 @@ AtTools::AtELossCATIMA::GetBraggCurve(double energy, double rangeStepSize, doubl
99142
double range{};
100143
while (remainingEnergy / energy > totalFractionELoss) {
101144

102-
catima::Result result = catima::calculate(*fProjectile, *fMaterial, remainingEnergy / fProjectileMassUma);
145+
catima::Result result = catima::calculate(*fProjectile, *fMaterial, remainingEnergy / fProjectileMassAmu);
103146
double dEdx = result.dEdxi * fDensity;
104147
braggCurve.push_back(std::make_pair(dEdx, range));
105148

@@ -113,3 +156,14 @@ AtTools::AtELossCATIMA::GetBraggCurve(double energy, double rangeStepSize, doubl
113156

114157
return braggCurve;
115158
}
159+
160+
void AtELossCATIMA::SetMaterial(std::vector<std::tuple<int, int, int>> materialComponents)
161+
{
162+
fMaterial = std::make_unique<catima::Material>();
163+
for (const auto &materialComponent : materialComponents) {
164+
fMaterial->add_element(std::get<0>(materialComponent), std::get<1>(materialComponent),
165+
std::get<2>(materialComponent));
166+
}
167+
}
168+
169+
} // namespace AtTools

AtTools/AtELossCATIMA.h

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,23 @@ class AtELossCATIMA : public AtELossModel {
1616
std::unique_ptr<catima::Material> fMaterial{nullptr};
1717
std::unique_ptr<catima::Projectile> fProjectile{nullptr};
1818

19-
double fProjectileMassUma{-1};
19+
double fProjectileMassAmu{-1}; /// Mass of the projectile in amu (atomic mass units).
2020

2121
double fRangeStepSize{0.1}; // mm
2222

2323
public:
2424
/**
2525
* Initializer of the CATIMA AtELossModel wrapper.
26-
* @param[in] density Density of the material.
26+
* @param[in] density Density of the material (g/cm^2).
2727
* @param[in] materialComponents Components of the material. They are passed as a vector of tuples (A, Z,
2828
* stoichiometry).
2929
*/
30+
AtELossCATIMA(double density) : AtELossModel(density) {}
3031
AtELossCATIMA(double density, std::vector<std::tuple<int, int, int>> materialComponents);
32+
AtELossCATIMA(double density, const catima::Material &material)
33+
: AtELossModel(density), fMaterial(std::make_unique<catima::Material>(material))
34+
{
35+
}
3136

3237
virtual double GetdEdx(double energy) const override;
3338
virtual double GetRange(double energyIni, double energyFin = 0) const override;
@@ -37,20 +42,27 @@ class AtELossCATIMA : public AtELossModel {
3742
}
3843
virtual double GetEnergy(double energyIni, double distance) const override;
3944

45+
virtual double GetRangeVariance(double energy) const override;
46+
virtual double GetElossStraggling(double energyIni, double energyFin) const override;
47+
virtual double GetdEdxStraggling(double energyIni, double energyFin) const override;
48+
4049
virtual std::vector<std::pair<double, double>>
4150
GetBraggCurve(double energy, double rangeStepSize = 0, double totalFractionELoss = 0.001) const override;
4251

4352
/**
4453
* Setter of the catima projectile used for calculations.
4554
* @param[in] A Mass number of the projectile.
4655
* @param[in] Z Charge number of the projectile.
47-
* @param[in] massUma Mass of the projectile in umas.
56+
* @param[in] massAmu Mass of the projectile in amu.
4857
*/
49-
void SetProjectile(double A, double Z, double massUma)
58+
void SetProjectile(double A, double Z, double massAmu)
5059
{
5160
fProjectile = std::make_unique<catima::Projectile>(A, Z);
52-
fProjectileMassUma = massUma;
61+
fProjectileMassAmu = massAmu;
5362
}
63+
void SetMaterial(const catima::Material &material) { fMaterial = std::make_unique<catima::Material>(material); }
64+
void SetMaterial(std::vector<std::tuple<int, int, int>> materialComponents);
65+
5466
/**
5567
* Setter of the range step size used for calculations. By default it is set to 0.1mm.
5668
* @param[in] stepSize The step size used for ranges. It must be input in mm.

AtTools/AtELossCATIMATest.cxx

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,99 @@
11
#include "AtELossCATIMA.h"
22

3+
#include <FairLogger.h>
4+
5+
#include <catima/catima.h>
6+
#include <catima/structures.h>
37
#include <cmath>
48
#include <gtest/gtest.h>
59
#include <tuple>
610
#include <utility>
711
#include <vector>
812

13+
/**
14+
* Test fixture for the AtELossCATIMA class that initializes a H2 gas w/ proton model.
15+
*/
16+
class AtELossCATIMATestFixture : public ::testing::Test {
17+
protected:
18+
AtTools::AtELossCATIMA model;
19+
double mass{1.007825031898}; // Mass of proton in amu
20+
21+
AtELossCATIMATestFixture() : model(6.5643e-5)
22+
{
23+
// Initialize the CATIMA model with H2 gas density and components and set projectile to proton.
24+
model.SetMaterial(catima::Material(1, 1)); // Set material to H2
25+
model.SetProjectile(1, 1, mass); // Set projectile to proton
26+
}
27+
};
28+
29+
TEST_F(AtELossCATIMATestFixture, ConstructCATIMAModel)
30+
{
31+
// Basic check: ensure model is constructed and can compute range
32+
double range = model.GetRange(1.0); // 1 MeV
33+
EXPECT_GT(range, 0.0);
34+
}
35+
36+
TEST_F(AtELossCATIMATestFixture, TestRangeStraggling)
37+
{
38+
// Check dEdx for a known energy
39+
double range_var = model.GetRangeVariance(1.0); // 1 MeV
40+
double expected = 1.99; // Expected value from LISE for H2 at 1 MeV
41+
ASSERT_NEAR(range_var, expected * expected, 0.2 * expected * expected);
42+
43+
double range_straggling = model.GetRangeStraggling(1.0); // 1 MeV
44+
ASSERT_NEAR(range_straggling, expected, 0.1 * expected);
45+
46+
// Check straggling at 10 MeV
47+
range_straggling = model.GetRangeStraggling(10.0); // 10 MeV
48+
expected = 95.933; // mm
49+
ASSERT_NEAR(range_straggling, expected, 0.1 * expected);
50+
51+
model.SetDensity(4e-5);
52+
range_straggling = model.GetRangeStraggling(1.0); // 1 MeV
53+
expected = 3.27; // mm
54+
ASSERT_NEAR(range_straggling, expected, 0.1 * expected);
55+
}
56+
57+
TEST_F(AtELossCATIMATestFixture, TestEnergyLossStraggling)
58+
{
59+
60+
double expectedSigma = 0.0084 * mass; // Expected sigma from LISE
61+
double eloss_straggling = model.GetElossStraggling(1.0, 0.75 * mass); // 1 MeV to 10 MeV
62+
ASSERT_NEAR(eloss_straggling, expectedSigma, 0.1 * expectedSigma);
63+
64+
expectedSigma = 0.0376 * mass;
65+
eloss_straggling = model.GetElossStraggling(5.0, 3.58164 * mass);
66+
ASSERT_NEAR(eloss_straggling, expectedSigma, 0.1 * expectedSigma);
67+
}
68+
69+
TEST_F(AtELossCATIMATestFixture, TestEnergyLossStragglingDistance)
70+
{
71+
double expectedSigma = 0.0084 * mass; // Expected sigma from LISE
72+
double eloss_straggling = model.GetElossStragglingDistance(1.0, 50.0); // 1 MeV over 10 mm
73+
ASSERT_NEAR(eloss_straggling, expectedSigma, 0.1 * expectedSigma);
74+
75+
expectedSigma = 0.0376 * mass;
76+
eloss_straggling = model.GetElossStragglingDistance(5.0, 1000.0); // 5 MeV over 100 mm
77+
ASSERT_NEAR(eloss_straggling, expectedSigma, 0.1 * expectedSigma);
78+
}
79+
80+
TEST_F(AtELossCATIMATestFixture, TestdEdxStraggling)
81+
{
82+
double dedx = model.GetdEdx(5.0);
83+
double dE = dedx * 50;
84+
double expected_dE = model.GetEnergyLoss(5.0, 50.0);
85+
86+
ASSERT_NEAR(dE, expected_dE, 0.01 * expected_dE); // Verify linear assumption is true
87+
88+
double E_st = model.GetElossStragglingDistance(5.0, 50.0);
89+
double dedx_straggling = model.GetdEdxStraggling(5.0, model.GetEnergy(5.0, 50.0));
90+
ASSERT_NEAR(dedx_straggling * 50, E_st, 0.01 * E_st); // Check dEdx straggling
91+
92+
double e_min = expected_dE - E_st;
93+
double e_min_dedx = dE - dedx_straggling * 50;
94+
ASSERT_NEAR(e_min, e_min_dedx, 0.01 * e_min); // Check minimum energy loss
95+
}
96+
997
TEST(AtELossCATIMATest, LISE_Match)
1098
{
1199
// Create the vector with the gas components for H2.

AtTools/AtELossModel.cxx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,7 @@ namespace AtTools {
99
*/
1010
void AtELossModel::SetDensity(double density)
1111
{
12-
if (fDensityIni == 0)
13-
throw std::invalid_argument("Cannot set the density if the density of in model is not known");
14-
1512
fDensity = density;
16-
fdEdxScale = fDensity / fDensityIni;
1713
}
1814

1915
std::vector<std::pair<double, double>>

0 commit comments

Comments
 (0)