Project
Loading...
Searching...
No Matches
O2MonopolePhysics.cxx
Go to the documentation of this file.
1// Copyright 2019-2026 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
30
34
35#include <boost/property_tree/ptree.hpp> // needed to instantiate getValueAs<>
36
37#include <TGeoManager.h>
38
39#include <fairlogger/Logger.h>
40
41#include "TG4ComposedPhysicsList.h"
43
44#include <G4VUserPhysicsList.hh>
45#include <G4ParticleTable.hh>
46#include <G4ParticleDefinition.hh>
47#include <G4PhysicsListHelper.hh>
48#include <G4ProcessManager.hh>
49#include <G4ProcessVector.hh>
50#include <G4mplIonisation.hh>
51
52#include <G4ChargeState.hh>
53#include <G4ChordFinder.hh>
54#include <G4ClassicalRK4.hh>
55#include <G4EquationOfMotion.hh>
56#include <G4FieldManager.hh>
57#include <G4MagneticField.hh>
58#include <G4PropagatorInField.hh>
59#include <G4Track.hh>
60#include <G4CoupledTransportation.hh>
61#include <G4Transportation.hh>
62#include <G4TransportationManager.hh>
63
64#include <CLHEP/Units/SystemOfUnits.h>
65#include <CLHEP/Units/PhysicalConstants.h>
66
67#include <array>
68#include <cmath>
69
70namespace o2
71{
72namespace g4config
73{
74
75namespace
76{
77// PDG codes of the O2 monopole species, as defined in
78// O2MCApplication::AddParticles() and O2DatabasePDG:
79// +-4110000 : "symmetric" monopoles
80// +-4120000 : "asymmetric" monopoles
81constexpr std::array<int, 4> gMonopolePDGs = {o2::sim::MonopolePdgSymm, -o2::sim::MonopolePdgSymm,
83
87constexpr G4double gMonopoleFieldGateMoment = 1.0e-20;
88
89// Extent of the TPC field cage, i.e. the region with the drift field (smaller than
90// the TPC_Drift volume, which extends beyond the endcaps and the field-cage rods)
91constexpr G4double gTPCFieldCageRMin = 83.5 * CLHEP::cm;
92constexpr G4double gTPCFieldCageRMax = 254.5 * CLHEP::cm;
93constexpr G4double gTPCFieldCageZMax = 249.525 * CLHEP::cm;
94
96inline double tpcDriftFieldMagnitude()
97{
98 if (gGeoManager == nullptr || gGeoManager->GetVolume("TPC_Drift") == nullptr) {
99 LOG(info) << "O2MonopolePhysics: no TPC in the geometry of this run, "
100 "the monopole is not coupled to a drift field";
101 return 0.;
102 }
103 try {
104 const double valueKVPerCm = o2::conf::ConfigurableParam::getValueAs<float>("TPCGEMParam.ElectricField[0]");
105 if (valueKVPerCm <= 0.) {
106 LOG(warn) << "O2MonopolePhysics: TPCGEMParam.ElectricField[0] = " << valueKVPerCm
107 << " kV/cm; the monopole is not coupled to a drift field";
108 return 0.;
109 }
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
120inline 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}
135} // namespace
136
137//____________________________________________________________________________
140{
141 public:
146 O2MonopoleEquation(G4Field* field, double magneticChargeEplusUnits, double tpcDriftFieldGeant4)
147 : G4EquationOfMotion(field),
148 mMagneticChargeEplus(magneticChargeEplusUnits),
149 mTPCDriftField(tpcDriftFieldGeant4)
150 {
151 }
152
155 void setMagneticChargeSign(int sign) { mMagneticChargeSign = sign; }
156
157 void SetChargeMomentumMass(G4ChargeState particleChargeState,
158 G4double /*momentum*/, G4double particleMass) override
159 {
160 mElCharge = CLHEP::eplus * particleChargeState.GetCharge() * CLHEP::c_light;
161 mMassCof = particleMass * particleMass;
162 mMagCharge = CLHEP::eplus * mMagneticChargeSign * mMagneticChargeEplus * CLHEP::c_light;
163 }
164
165 void EvaluateRhsGivenB(const G4double y[], const G4double B[3], G4double dydx[]) const override
166 {
167 const G4double pSquared = y[3] * y[3] + y[4] * y[4] + y[5] * y[5];
168 if (pSquared <= 0.) {
169 for (int i = 0; i < 8; ++i) {
170 dydx[i] = 0.;
171 }
172 return;
173 }
174 const G4double energy = std::sqrt(pSquared + mMassCof);
175 const G4double pModuleInverse = 1.0 / std::sqrt(pSquared);
176 const G4double cofEl = mElCharge * pModuleInverse;
177 const G4double cofMag = mMagCharge * energy * pModuleInverse;
178
179 dydx[0] = y[3] * pModuleInverse;
180 dydx[1] = y[4] * pModuleInverse;
181 dydx[2] = y[5] * pModuleInverse;
182
183 // magnetic charge -> force along B; electric charge -> the usual v x B
184 dydx[3] = cofMag * B[0] + cofEl * (y[4] * B[2] - y[5] * B[1]);
185 dydx[4] = cofMag * B[1] + cofEl * (y[5] * B[0] - y[3] * B[2]);
186 dydx[5] = cofMag * B[2] + cofEl * (y[3] * B[1] - y[4] * B[0]);
187
188 // Coupling of the magnetic charge to an electric field: the full dual
189 // Lorentz force is F = g*(B - v x E/c^2).
190 if (mTPCDriftField > 0.) {
191 G4double E[3] = {0., 0., 0.};
192 if (tpcDriftField(y, mTPCDriftField, E)) {
193 // Relative to cofMag this carries 1/(c*energy), so the term is of order
194 // beta*E/(c*B) compared with the g*B term (similar to G4MonopoleEq, which uses
195 // d(p)/ds = g*(c*energy*B - p x E)/(p*c)).
196 const G4double cofMagE = mMagCharge * pModuleInverse / CLHEP::c_light;
197 const G4double dEx = cofMagE * (y[4] * E[2] - y[5] * E[1]);
198 const G4double dEy = cofMagE * (y[5] * E[0] - y[3] * E[2]);
199 const G4double dEz = cofMagE * (y[3] * E[1] - y[4] * E[0]);
200 dydx[3] -= dEx;
201 dydx[4] -= dEy;
202 dydx[5] -= dEz;
203 }
204 }
205
206 dydx[6] = 0.; // not used
207 dydx[7] = energy * pModuleInverse / CLHEP::c_light; // inverse velocity
208 }
209
210 private:
211 double mMagneticChargeEplus;
212 int mMagneticChargeSign = 1;
213 G4double mTPCDriftField = 0.;
214 G4double mElCharge = 0.;
215 G4double mMagCharge = 0.;
216 G4double mMassCof = 0.;
217};
218
219//____________________________________________________________________________
222 G4ChordFinder* chordFinder = nullptr;
224};
225
226//____________________________________________________________________________
245inline MonopoleFieldSetup buildMonopoleFieldSetup(double magneticChargeEplusUnits, double tpcDriftFieldGeant4)
246{
247 auto* transportationManager = G4TransportationManager::GetTransportationManager();
248 auto* fieldManager = transportationManager != nullptr ? transportationManager->GetFieldManager() : nullptr;
249 if (fieldManager == nullptr) {
250 LOG(fatal) << "O2MonopolePhysics: no G4FieldManager, the monopole equation of motion "
251 "cannot be installed; rerun with G4.monopole=0 if that is what you want";
252 return {};
253 }
254 auto* magneticField =
255 const_cast<G4MagneticField*>(dynamic_cast<const G4MagneticField*>(fieldManager->GetDetectorField()));
256 if (magneticField == nullptr) {
257 LOG(fatal) << "O2MonopolePhysics: no magnetic field attached to the field manager, the "
258 "monopole equation of motion cannot be installed; rerun with G4.monopole=0 "
259 "if that is what you want";
260 return {};
261 }
262
263 auto* equation = new O2MonopoleEquation(magneticField, magneticChargeEplusUnits, tpcDriftFieldGeant4);
264 // A generic (non-helix) integrator is required: the helix steppers hard-code
265 // the constant-curvature motion of an electric charge. 8 variables so that the
266 // time-of-flight component is integrated too.
267 auto* stepper = new G4ClassicalRK4(equation, 8);
268
269 // keep the accuracy Geant4-VMC configured for this field
270 auto* previous = fieldManager->GetChordFinder();
271 const G4double stepMinimum = 1.0e-2 * CLHEP::mm;
272 auto* chordFinder = new G4ChordFinder(magneticField, stepMinimum, stepper);
273 if (previous != nullptr) {
274 chordFinder->SetDeltaChord(previous->GetDeltaChord());
275 }
276
277 if (tpcDriftFieldGeant4 > 0.) {
278 LOG(info) << "O2MonopolePhysics: monopole equation of motion built (F = g*(B - v x E/c^2)), "
279 "magnetic charge = "
280 << magneticChargeEplusUnits << " eplus, TPC drift field = "
281 << tpcDriftFieldGeant4 / (CLHEP::volt / CLHEP::cm) << " V/cm";
282 } else {
283 LOG(info) << "O2MonopolePhysics: monopole equation of motion built (F = g*B), magnetic charge = "
284 << magneticChargeEplusUnits << " eplus (TPC drift field coupling disabled)";
285 }
286 return {chordFinder, equation};
287}
288
289//____________________________________________________________________________
310{
311 public:
313 : G4Transportation(0), mChordFinder(fieldSetup.chordFinder), mEquation(fieldSetup.equation)
314 {
315 }
316
317 void StartTracking(G4Track* track) override
318 {
319 G4Transportation::StartTracking(track);
320 // G4Transportation::StartTracking() clears the integration state only of the chord finders
321 // installed on the field managers. Without this the step estimate of the previous monopole
322 // would carry over, and the result would depend on which events the worker simulated before
323 mChordFinder->ResetStepEstimate();
324 }
325
326 G4double AlongStepGetPhysicalInteractionLength(const G4Track& track, G4double previousStepSize,
327 G4double currentMinimumStep, G4double& currentSafety,
328 G4GPILSelection* selection) override
329 {
330 // the field manager the propagator uses in this volume, as G4Transportation finds it
331 auto* fieldManager = GetPropagatorInField()->FindAndSetFieldManager(track.GetVolume());
332 if (fieldManager->GetDetectorField() == nullptr) {
333 // volume without field: straight-line transport
334 return G4Transportation::AlongStepGetPhysicalInteractionLength(track, previousStepSize, currentMinimumStep,
335 currentSafety, selection);
336 }
337 // The sign of the magnetic charge follows the PDG sign, so monopole and anti-monopole
338 // are pushed in opposite directions along B.
339 // TODO: both species are electrically neutral currently, so their magnetic sign is
340 // the same function of the PDG sign; once they carry electric charge the
341 // asymmetric species (4120000) needs the opposite relative sign.
342 mEquation->setMagneticChargeSign(track.GetDefinition()->GetPDGEncoding() > 0 ? 1 : -1);
343
344 const G4bool previousMoment = G4Transportation::EnableMagneticMoment(true);
345 auto* previousChordFinder = fieldManager->GetChordFinder();
346 const G4bool previousChangesEnergy = fieldManager->DoesFieldChangeEnergy();
347 const G4double previousDeltaChord = mChordFinder->GetDeltaChord();
348 // Preserve the chord tolerance configured for the active field manager.
349 // The monopole chord finder is shared by global and local field managers.
350 if (previousChordFinder != nullptr) {
351 mChordFinder->SetDeltaChord(previousChordFinder->GetDeltaChord());
352 }
353 fieldManager->SetChordFinder(mChordFinder);
354 // the end kinetic energy and time of flight are then taken from the integrated track
355 fieldManager->SetFieldChangesEnergy(true);
356
357 const G4double length = G4Transportation::AlongStepGetPhysicalInteractionLength(
358 track, previousStepSize, currentMinimumStep, currentSafety, selection);
359
360 fieldManager->SetFieldChangesEnergy(previousChangesEnergy);
361 fieldManager->SetChordFinder(previousChordFinder);
362 mChordFinder->SetDeltaChord(previousDeltaChord);
363 G4Transportation::EnableMagneticMoment(previousMoment);
364 return length;
365 }
366
367 private:
368 G4ChordFinder* mChordFinder;
369 O2MonopoleEquation* mEquation;
370};
371
372//____________________________________________________________________________
376inline void installMonopoleTransport(G4ProcessManager* pmanager, const G4ParticleDefinition* particle,
377 const MonopoleFieldSetup& fieldSetup)
378{
379 G4VProcess* existing = nullptr;
380 G4ProcessVector* plist = pmanager->GetProcessList();
381 for (G4int ip = 0; ip < static_cast<G4int>(plist->size()); ++ip) {
382 if (dynamic_cast<G4Transportation*>((*plist)[ip]) != nullptr) {
383 existing = (*plist)[ip];
384 break;
385 }
386 }
387 if (existing == nullptr) {
388 LOG(fatal) << "O2MonopolePhysics: " << particle->GetParticleName()
389 << " has no transportation process to replace; the monopole could not be "
390 "coupled to the field";
391 return;
392 }
393 if (dynamic_cast<G4CoupledTransportation*>(existing) != nullptr) {
394 // Parallel worlds are in use. O2MonopoleTransportation derives from plain
395 // G4Transportation, so swapping it in would drop the parallel-world
396 // navigation; refuse rather than silently mis-navigate.
397 LOG(fatal) << "O2MonopolePhysics: " << particle->GetParticleName()
398 << " uses G4CoupledTransportation (parallel worlds); the monopole transportation "
399 "does not support that. Rerun with G4.monopole=0 or without parallel worlds";
400 return;
401 }
402
403 // One G4Transportation instance is shared by every particle
404 // (G4VUserPhysicsList::AddTransportation creates a single one), so it is
405 // detached from this particle only and must not be deleted.
406 pmanager->RemoveProcess(existing);
407
408 auto* transportation = new O2MonopoleTransportation(fieldSetup);
409 pmanager->AddProcess(transportation);
410 // Transportation has to be first in the DoIt vectors
411 //
412 // Geant4 prints "Set Ordering First is invoked twice for Transportation to
413 // <particle>" (ProcMan113, JustWarning) once per call here, because
414 // G4ProcessManager latches isSetOrderingFirstInvoked and RemoveProcess() does
415 // not clear it. The insertion is performed before that check and is correct;
416 // the warning in the stdout is expected and harmless.
417 pmanager->SetProcessOrderingToFirst(transportation, idxAlongStep);
418 pmanager->SetProcessOrderingToFirst(transportation, idxPostStep);
419}
420
421//____________________________________________________________________________
431{
432 public:
435 explicit O2MonopolePhysics(double magneticChargeEplus) : mMagneticCharge(magneticChargeEplus) {}
436
437 // Particles are already defined by O2MCApplication::AddParticles().
438 void ConstructParticle() override {}
439
440 // Cuts are handled by the reference physics list.
441 void SetCuts() override {}
442
443 void ConstructProcess() override
444 {
445 auto* table = G4ParticleTable::GetParticleTable();
446
447 // Built on the first monopole species actually found, then shared by the rest:
448 // O2MonopoleTransportation sets the sign of the magnetic charge at each step, so
449 // one chord finder serves monopoles and anti-monopoles
450 // No monopoles in the particles table == no monopole ionisation attached
451 MonopoleFieldSetup fieldSetup;
452
453 int nAttached = 0;
454 for (int pdg : gMonopolePDGs) {
455 auto* particle = table->FindParticle(pdg);
456 if (particle == nullptr) {
457 // Not necessarily an error: only the species actually produced by the
458 // generator strictly need this. Report and continue.
459 LOG(info) << "O2MonopolePhysics: monopole PDG " << pdg
460 << " not present in the particle table, skipping";
461 continue;
462 }
463 auto* pmanager = particle->GetProcessManager();
464 if (pmanager == nullptr) {
465 LOG(warning) << "O2MonopolePhysics: no process manager for PDG " << pdg << ", skipping";
466 continue;
467 }
468 // The only ionisation of the monopoles: they are defined as kPTUndefined, for which
469 // Geant4-VMC attaches no process. Ordered by G4PhysicsListHelper from the process
470 // subtype, as in the Geant4 monopole example
471 G4PhysicsListHelper::GetPhysicsListHelper()->RegisterProcess(new G4mplIonisation(mMagneticCharge), particle);
472
473 // G4Transportation only looks up the field when the particle has a
474 // non-zero electric charge or a non-zero magnetic moment (μ) and a monopole has
475 // neither, so the field would never be queried. Hence a gate magnetic moment is set
476 // to allow the query to happen.
477 if (particle->GetPDGMagneticMoment() == 0.) {
478 particle->SetPDGMagneticMoment(gMonopoleFieldGateMoment);
479 }
480
481 // Deflect the monopole in the field as well; without this only the energy
482 // loss above would act and the monopole would fly straight through, since
483 // its electric charge (and hence the usual Lorentz force) is zero.
484 if (fieldSetup.chordFinder == nullptr) {
485 fieldSetup = buildMonopoleFieldSetup(mMagneticCharge / CLHEP::eplus, tpcDriftFieldMagnitude());
486 }
487 installMonopoleTransport(pmanager, particle, fieldSetup);
488
489 ++nAttached;
490 LOG(info) << "O2MonopolePhysics: attached G4mplIonisation and monopole transport to "
491 << particle->GetParticleName() << " (PDG " << pdg
492 << "), magnetic charge = " << mMagneticCharge / CLHEP::eplus << " eplus";
493 }
494 if (nAttached == 0) {
495 LOG(warning) << "O2MonopolePhysics: no monopole particle found; no ionisation attached";
496 }
497 }
498
499 private:
500 double mMagneticCharge;
501};
502
503//____________________________________________________________________________
509{
510 public:
511 O2G4RunConfiguration(const TString& userGeometry, const TString& physicsList,
512 const TString& specialProcess, Bool_t specialStacking,
513 Bool_t mtApplication, double magneticChargeEplus)
514 : o2::g4config::G4RunConfiguration(userGeometry, physicsList, specialProcess, specialStacking, mtApplication),
515 mMagneticCharge(magneticChargeEplus)
516 {
517 }
518
520 {
521 G4VUserPhysicsList* physicsList = o2::g4config::G4RunConfiguration::CreatePhysicsList();
522 if (auto* composed = dynamic_cast<TG4ComposedPhysicsList*>(physicsList)) {
523 composed->AddPhysicsList(new O2MonopolePhysics(mMagneticCharge));
524 LOG(info) << "O2G4RunConfiguration: monopole ionisation physics registered "
525 "on the composed physics list";
526 } else {
527 LOG(fatal) << "O2G4RunConfiguration: physics list is not a TG4ComposedPhysicsList, "
528 "monopole ionisation cannot be enabled; rerun with G4.monopole=0 if that "
529 "is what you want";
530 }
531 return physicsList;
532 }
533
534 private:
535 double mMagneticCharge;
536};
537
538//____________________________________________________________________________
540 const TString& physicsList,
541 const TString& specialProcess,
542 bool specialStacking,
543 bool mtApplication,
544 double magneticChargeDirac)
545{
546 // One Dirac charge g_D = eplus / (2*alpha) ~= 68.5 eplus (Dirac quantisation).
547 // magneticChargeDirac counts Dirac charges (1.0 == classic single monopole),
548 // matching the "g_1" convention used by the O2 monopole generator input.
549 // G4mplIonisation silently replaces a zero charge with one Dirac charge, while the
550 // equation of motion would use the zero, so non-positive values are rejected
551 if (!(magneticChargeDirac > 0.)) {
552 LOG(fatal) << "O2MonopolePhysics: G4.monopoleMagneticCharge must be positive, got " << magneticChargeDirac;
553 }
554 const double gDirac = CLHEP::eplus / (2.0 * CLHEP::fine_structure_const);
555 const double magneticChargeEplus = magneticChargeDirac * gDirac;
556 LOG(info) << "Monopole ionisation enabled: magnetic charge = " << magneticChargeDirac
557 << " Dirac charge(s) = " << magneticChargeEplus / CLHEP::eplus << " eplus";
558 return new O2G4RunConfiguration(userGeometry, physicsList, specialProcess,
559 specialStacking, mtApplication, magneticChargeEplus);
560}
561
562} // namespace g4config
563} // namespace o2
int32_t i
Identification of the BSM magnetic monopoles across the simulation.
Acts::Vector3 position
Definition B.h:16
The Geant4 VMC run configuration of O2: adds the fast simulation and the local magnetic fields.
G4VUserPhysicsList * CreatePhysicsList() override
O2G4RunConfiguration(const TString &userGeometry, const TString &physicsList, const TString &specialProcess, Bool_t specialStacking, Bool_t mtApplication, double magneticChargeEplus)
Equation of motion implementing the dual Lorentz force of a magnetic charge.
void EvaluateRhsGivenB(const G4double y[], const G4double B[3], G4double dydx[]) const override
void SetChargeMomentumMass(G4ChargeState particleChargeState, G4double, G4double particleMass) override
O2MonopoleEquation(G4Field *field, double magneticChargeEplusUnits, double tpcDriftFieldGeant4)
O2MonopolePhysics(double magneticChargeEplus)
void StartTracking(G4Track *track) override
O2MonopoleTransportation(const MonopoleFieldSetup &fieldSetup)
G4double AlongStepGetPhysicalInteractionLength(const G4Track &track, G4double previousStepSize, G4double currentMinimumStep, G4double &currentSafety, G4GPILSelection *selection) override
GLint y
Definition glcorearb.h:270
GLuint GLsizei GLsizei * length
Definition glcorearb.h:790
GLdouble GLdouble GLdouble z
Definition glcorearb.h:843
void installMonopoleTransport(G4ProcessManager *pmanager, const G4ParticleDefinition *particle, const MonopoleFieldSetup &fieldSetup)
TG4RunConfiguration * createMonopoleRunConfiguration(const TString &userGeometry, const TString &physicsList, const TString &specialProcess, bool specialStacking, bool mtApplication, double magneticChargeDirac)
MonopoleFieldSetup buildMonopoleFieldSetup(double magneticChargeEplusUnits, double tpcDriftFieldGeant4)
constexpr int MonopolePdgSymm
constexpr int MonopolePdgAsymm
monopole intended to carry opposite electric and magnetic charge (not yet distinguished)
a couple of static helper functions to create timestamp values for CCDB queries or override obsolete ...
The chord finder integrating O2MonopoleEquation, and that equation.
O2MonopoleEquation * equation
to pass the sign of the magnetic charge at each step
LOG(info)<< "Compressed in "<< sw.CpuTime()<< " s"