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
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
5271namespace o2
@@ -61,8 +80,213 @@ namespace
6180// +-4110000 : "symmetric" monopoles
6281// +-4120000 : "asymmetric" monopoles
6382constexpr 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