Skip to content

Commit 523edfa

Browse files
author
Marco Giacalone
committed
Include full Lorentz dual force for monopoles
1 parent 0fb2da0 commit 523edfa

3 files changed

Lines changed: 252 additions & 5 deletions

File tree

Common/SimConfig/include/SimConfig/G4Params.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ struct G4Params : public o2::conf::ConfigurableParamHelper<G4Params> {
6262
bool monopole = false;
6363
// Monopole magnetic charge in units of the Dirac charge g_D = eplus/(2*alpha)
6464
// (~68.5 eplus). 1.0 corresponds to a single classic Dirac monopole.
65+
// The monopole is transported with the full dual Lorentz force
66+
// F = g*(B - v x E/c^2). The electric part acts inside the TPC field cage,
67+
// the only volume with an electric field, and its magnitude is the TPC's own
68+
// TPCGEMParam.ElectricField[0]
6569
float monopoleMagneticCharge = 1.f;
6670

6771
O2ParamDef(G4Params, "G4");

Detectors/TPC/simulation/src/Detector.cxx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ Bool_t Detector::ProcessHits(FairVolume* vol)
198198
// TODO: Add discussion about drawback
199199

200200
Int_t numberOfElectrons = 0;
201-
// I.H. - the type expected in addHit is short
201+
// I.H. - the type expected in addHit is float
202202

203203
// ---| Stepsize in cm |---
204204
const double stepSize = fMC->TrackStep();
@@ -219,9 +219,9 @@ Bool_t Detector::ProcessHits(FairVolume* vol)
219219
// Ionisation electrons calculated directly from the energy deposited in this step: Nel = Edep / Wion.
220220
// To check by TPC experts if this is actually the best way...
221221
numberOfElectrons = static_cast<int>(fMC->Edep() / static_cast<double>(gasParam.Wion));
222-
// The number of electrons is stored as a short in the HitGroup; cap it to
223-
// avoid overflow for the very high monopole dE/dx.
224-
numberOfElectrons = TMath::Min(numberOfElectrons, 32000);
222+
// The number of electrons is stored as a float in the HitGroup: maximum cap at
223+
// 2^24 (16777216) ==> largest integer a IEEE-754 float can represent exactly
224+
numberOfElectrons = TMath::Min(numberOfElectrons, 16777216);
225225
} else {
226226
// ---| mean number of collisions and random for this event |---
227227
const double meanNcoll = stepSize * trackCharge * trackCharge * primaryElectronsPerCM;
@@ -244,7 +244,7 @@ Bool_t Detector::ProcessHits(FairVolume* vol)
244244
const double rndm = fMC->GetRandom()->Rndm();
245245
const double eDep = TMath::Power((kMax - kMin) * rndm + kMin, oneOverAlpha_p1);
246246
int nel_step = static_cast<int>(((eDep - eMin) / wIon) + 1);
247-
nel_step = TMath::Min(nel_step, 300); // 300 electrons corresponds to 10 keV
247+
nel_step = TMath::Min(nel_step, gasParam.MaxElePerStep); // 300 electrons corresponds to 10 keV
248248
numberOfElectrons += nel_step;
249249
}
250250
}

Detectors/gconfig/src/O2MonopolePhysics.cxx

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@
2929
/// Geant4 libraries (libG4processes)
3030

3131
#include "SimSetup/O2MonopolePhysics.h"
32+
#include "CommonUtils/ConfigurableParam.h"
33+
34+
#include <boost/property_tree/ptree.hpp> // needed to instantiate getValueAs<>
35+
36+
#include <TGeoManager.h>
3237

3338
#include <fairlogger/Logger.h>
3439

@@ -43,10 +48,24 @@
4348
#include <G4VEnergyLossProcess.hh>
4449
#include <G4mplIonisation.hh>
4550

51+
#include <G4ChargeState.hh>
52+
#include <G4ChordFinder.hh>
53+
#include <G4ClassicalRK4.hh>
54+
#include <G4EquationOfMotion.hh>
55+
#include <G4EventManager.hh>
56+
#include <G4FieldManager.hh>
57+
#include <G4MagIntegratorStepper.hh>
58+
#include <G4MagneticField.hh>
59+
#include <G4Track.hh>
60+
#include <G4TrackingManager.hh>
61+
#include <G4Transportation.hh>
62+
#include <G4TransportationManager.hh>
63+
4664
#include <CLHEP/Units/SystemOfUnits.h>
4765
#include <CLHEP/Units/PhysicalConstants.h>
4866

4967
#include <array>
68+
#include <cmath>
5069
#include <vector>
5170

5271
namespace o2
@@ -61,8 +80,213 @@ namespace
6180
// +-4110000 : "symmetric" monopoles
6281
// +-4120000 : "asymmetric" monopoles
6382
constexpr std::array<int, 4> gMonopolePDGs = {4110000, -4110000, 4120000, -4120000};
83+
84+
inline bool isMonopolePDG(int pdg)
85+
{
86+
const int abspdg = std::abs(pdg);
87+
return abspdg == 4110000 || abspdg == 4120000;
88+
}
89+
90+
/// GEANT4 only queries the magnetic field when a particle has non-zero electric
91+
/// charge or non-zero magnetic moment (μ) and the monopole has neither. So this is a workaround
92+
/// so that G4Transportation queries the field for it
93+
constexpr G4double gMonopoleFieldGateMoment = 1.0e-20;
94+
95+
// Extent of the TPC drift region
96+
constexpr G4double gTPCFieldCageRMin = 83.5 * CLHEP::cm;
97+
constexpr G4double gTPCFieldCageRMax = 254.5 * CLHEP::cm;
98+
constexpr G4double gTPCFieldCageZMax = 249.525 * CLHEP::cm;
99+
100+
/// Magnitude of the TPC drift field, in Geant4 units; 0 when there is no TPC.
101+
inline double tpcDriftFieldMagnitude()
102+
{
103+
if (gGeoManager == nullptr || gGeoManager->GetVolume("TPC_Drift") == nullptr) {
104+
LOG(info) << "O2MonopolePhysics: no TPC in the geometry of this run, "
105+
"the monopole is not coupled to a drift field";
106+
return 0.;
107+
}
108+
try {
109+
const double valueKVPerCm = o2::conf::ConfigurableParam::getValueAs<float>("TPCGEMParam.ElectricField[0]");
110+
return valueKVPerCm * CLHEP::kilovolt / CLHEP::cm;
111+
} catch (...) {
112+
LOG(warn) << "O2MonopolePhysics: the TPC is in the geometry but TPCGEMParam is not "
113+
"registered; no drift-field coupling for the monopole";
114+
return 0.;
115+
}
116+
}
117+
118+
/// Electric field of the TPC drift region at a specific position.
119+
/// Returns false outside the field cage.
120+
inline bool tpcDriftField(const G4double position[3], G4double driftField, G4double E[3])
121+
{
122+
const G4double z = position[2];
123+
if (std::fabs(z) >= gTPCFieldCageZMax) {
124+
return false;
125+
}
126+
const G4double r2 = position[0] * position[0] + position[1] * position[1];
127+
if (r2 < gTPCFieldCageRMin * gTPCFieldCageRMin || r2 > gTPCFieldCageRMax * gTPCFieldCageRMax) {
128+
return false;
129+
}
130+
E[0] = 0.;
131+
E[1] = 0.;
132+
E[2] = (z >= 0.) ? -driftField : driftField;
133+
return true;
134+
}
64135
} // namespace
65136

137+
//____________________________________________________________________________
138+
/// Equation of motion implementing the dual Lorentz force of a magnetic charge.
139+
class O2MonopoleEquation : public G4EquationOfMotion
140+
{
141+
public:
142+
/// \param field the magnetic field to integrate in
143+
/// \param magneticChargeEplusUnits monopole magnetic charge expressed in eplus
144+
/// units (one Dirac charge = 1/(2*alpha) ~ 68.5)
145+
/// \param tpcDriftFieldGeant4 TPC drift field in Geant4 units
146+
O2MonopoleEquation(G4Field* field, double magneticChargeEplusUnits, double tpcDriftFieldGeant4)
147+
: G4EquationOfMotion(field),
148+
mMagneticChargeEplus(magneticChargeEplusUnits),
149+
mTPCDriftField(tpcDriftFieldGeant4)
150+
{
151+
}
152+
153+
void SetChargeMomentumMass(G4ChargeState particleChargeState,
154+
G4double /*momentum*/, G4double particleMass) override
155+
{
156+
mElCharge = CLHEP::eplus * particleChargeState.GetCharge() * CLHEP::c_light;
157+
mMassCof = particleMass * particleMass;
158+
159+
// Non-zero only while an O2 monopole is being transported. The sign follows
160+
// the PDG sign, so monopole and anti-monopole are pushed in opposite
161+
// directions along B.
162+
double signedMagneticCharge = 0.;
163+
int pdg = 0;
164+
if (const G4Track* track = currentTrack()) {
165+
pdg = track->GetDefinition()->GetPDGEncoding();
166+
if (isMonopolePDG(pdg)) {
167+
signedMagneticCharge = (pdg > 0) ? mMagneticChargeEplus : -mMagneticChargeEplus;
168+
}
169+
}
170+
mMagCharge = CLHEP::eplus * signedMagneticCharge * CLHEP::c_light;
171+
}
172+
173+
void EvaluateRhsGivenB(const G4double y[], const G4double B[3], G4double dydx[]) const override
174+
{
175+
const G4double pSquared = y[3] * y[3] + y[4] * y[4] + y[5] * y[5];
176+
if (pSquared <= 0.) {
177+
for (int i = 0; i < 8; ++i) {
178+
dydx[i] = 0.;
179+
}
180+
return;
181+
}
182+
const G4double energy = std::sqrt(pSquared + mMassCof);
183+
const G4double pModuleInverse = 1.0 / std::sqrt(pSquared);
184+
const G4double cofEl = mElCharge * pModuleInverse;
185+
const G4double cofMag = mMagCharge * energy * pModuleInverse;
186+
187+
dydx[0] = y[3] * pModuleInverse;
188+
dydx[1] = y[4] * pModuleInverse;
189+
dydx[2] = y[5] * pModuleInverse;
190+
191+
// magnetic charge -> force along B; electric charge -> the usual v x B
192+
dydx[3] = cofMag * B[0] + cofEl * (y[4] * B[2] - y[5] * B[1]);
193+
dydx[4] = cofMag * B[1] + cofEl * (y[5] * B[0] - y[3] * B[2]);
194+
dydx[5] = cofMag * B[2] + cofEl * (y[3] * B[1] - y[4] * B[0]);
195+
196+
// Coupling of the magnetic charge to an electric field: the full dual
197+
// Lorentz force is F = g*(B - v x E/c^2).
198+
// mMagCharge is zero for every non-monopole, so nothing else is affected.
199+
if (mMagCharge != 0. && mTPCDriftField > 0.) {
200+
G4double E[3] = {0., 0., 0.};
201+
if (tpcDriftField(y, mTPCDriftField, E)) {
202+
// Relative to cofMag this carries 1/(c*energy), so the term is of order
203+
// beta*E/(c*B) compared with the g*B term (similar to G4MonopoleEq, which uses
204+
// d(p)/ds = g*(c*energy*B - p x E)/(p*c)).
205+
const G4double cofMagE = mMagCharge * pModuleInverse / CLHEP::c_light;
206+
const G4double dEx = cofMagE * (y[4] * E[2] - y[5] * E[1]);
207+
const G4double dEy = cofMagE * (y[5] * E[0] - y[3] * E[2]);
208+
const G4double dEz = cofMagE * (y[3] * E[1] - y[4] * E[0]);
209+
dydx[3] -= dEx;
210+
dydx[4] -= dEy;
211+
dydx[5] -= dEz;
212+
}
213+
}
214+
215+
dydx[6] = 0.; // not used
216+
dydx[7] = energy * pModuleInverse / CLHEP::c_light; // inverse velocity
217+
}
218+
219+
private:
220+
static const G4Track* currentTrack()
221+
{
222+
auto* eventManager = G4EventManager::GetEventManager();
223+
if (eventManager == nullptr) {
224+
return nullptr;
225+
}
226+
auto* trackingManager = eventManager->GetTrackingManager();
227+
return trackingManager != nullptr ? trackingManager->GetTrack() : nullptr;
228+
}
229+
230+
double mMagneticChargeEplus; ///< |g| in eplus units (1 g_D = 1/(2*alpha) ~ 68.5)
231+
G4double mTPCDriftField = 0.; ///< TPC drift field, Geant4 units; 0 disables the coupling
232+
G4double mElCharge = 0.;
233+
G4double mMagCharge = 0.;
234+
G4double mMassCof = 0.;
235+
};
236+
237+
//____________________________________________________________________________
238+
/// Make the global field integrate O2MonopoleEquation instead of the default
239+
/// electric-charge-only equation.
240+
///
241+
/// SetUserEquationOfMotion() in Geant4-VMC is not usable here: it
242+
/// registers the object with TG4GeometryManager, and the field integrator is
243+
/// built before that registration is consulted, so the equation is never
244+
/// actually called.
245+
///
246+
/// \param magneticChargeEplusUnits monopole magnetic charge in eplus units
247+
/// \param tpcDriftFieldGeant4 TPC drift field in Geant4 units (0 = disabled)
248+
inline void installMonopoleFieldIntegrator(double magneticChargeEplusUnits, double tpcDriftFieldGeant4)
249+
{
250+
auto* transportationManager = G4TransportationManager::GetTransportationManager();
251+
auto* fieldManager = transportationManager != nullptr ? transportationManager->GetFieldManager() : nullptr;
252+
if (fieldManager == nullptr) {
253+
LOG(error) << "O2MonopolePhysics: no G4FieldManager, monopole equation of motion NOT installed";
254+
return;
255+
}
256+
auto* magneticField =
257+
const_cast<G4MagneticField*>(dynamic_cast<const G4MagneticField*>(fieldManager->GetDetectorField()));
258+
if (magneticField == nullptr) {
259+
LOG(error) << "O2MonopolePhysics: no magnetic field attached to the field manager, "
260+
"monopole equation of motion NOT installed";
261+
return;
262+
}
263+
264+
auto* equation = new O2MonopoleEquation(magneticField, magneticChargeEplusUnits, tpcDriftFieldGeant4);
265+
// A generic (non-helix) integrator is required: the helix steppers hard-code
266+
// the constant-curvature motion of an electric charge. 8 variables so that the
267+
// time-of-flight component is integrated too.
268+
auto* stepper = new G4ClassicalRK4(equation, 8);
269+
270+
// keep the accuracy Geant4-VMC configured for this field
271+
auto* previous = fieldManager->GetChordFinder();
272+
const G4double stepMinimum = 1.0e-2 * CLHEP::mm;
273+
auto* chordFinder = new G4ChordFinder(magneticField, stepMinimum, stepper);
274+
if (previous != nullptr) {
275+
chordFinder->SetDeltaChord(previous->GetDeltaChord());
276+
}
277+
fieldManager->SetChordFinder(chordFinder);
278+
279+
if (tpcDriftFieldGeant4 > 0.) {
280+
LOG(info) << "O2MonopolePhysics: monopole equation of motion installed (F = g*(B - v x E/c^2)), "
281+
"magnetic charge = "
282+
<< magneticChargeEplusUnits << " eplus, TPC drift field = "
283+
<< tpcDriftFieldGeant4 / (CLHEP::volt / CLHEP::cm) << " V/cm";
284+
} else {
285+
LOG(info) << "O2MonopolePhysics: monopole equation of motion installed (F = g*B), magnetic charge = "
286+
<< magneticChargeEplusUnits << " eplus (TPC drift field coupling disabled)";
287+
}
288+
}
289+
66290
//____________________________________________________________________________
67291
/// Minimal physics list whose only job is to attach the magnetic-monopole
68292
/// ionisation process to the already-defined O2 monopole particles.
@@ -126,6 +350,14 @@ class O2MonopolePhysics : public G4VUserPhysicsList
126350
// Ordering (AtRest, AlongStep, PostStep) = (-1, 1, 1) as in the Geant4
127351
// monopole example: continuous-and-discrete energy loss, not active at rest.
128352
pmanager->AddProcess(new G4mplIonisation(mMagneticCharge), -1, 1, 1);
353+
354+
// G4Transportation only looks up the field when the particle has a
355+
// non-zero electric charge or a non-zero magnetic moment (μ) and a monopole has
356+
// neither, so the field would never be queried. Hence a gate magnetic moment is set
357+
// to allow the query to happen.
358+
if (particle->GetPDGMagneticMoment() == 0.) {
359+
particle->SetPDGMagneticMoment(gMonopoleFieldGateMoment);
360+
}
129361
++nAttached;
130362
LOG(info) << "O2MonopolePhysics: attached G4mplIonisation to "
131363
<< particle->GetParticleName() << " (PDG " << pdg
@@ -134,6 +366,17 @@ class O2MonopolePhysics : public G4VUserPhysicsList
134366
if (nAttached == 0) {
135367
LOG(warning) << "O2MonopolePhysics: no monopole particle found; no ionisation attached";
136368
}
369+
370+
// This static switch is what makes G4Transportation consider the μ of the monopole
371+
// It is global, so electrically neutral particles that already carry a momentum (neutrons) are
372+
// now propagated through the field as well; O2MonopoleEquation gives them
373+
// exactly zero force, so their trajectories are unchanged.
374+
G4Transportation::EnableMagneticMoment(true);
375+
376+
// Deflect the monopole in the field as well; without this only the energy
377+
// loss above would act and the monopole would fly straight through, since
378+
// its electric charge (and hence the usual Lorentz force) is zero.
379+
installMonopoleFieldIntegrator(mMagneticCharge / CLHEP::eplus, tpcDriftFieldMagnitude());
137380
}
138381

139382
private:

0 commit comments

Comments
 (0)