Skip to content

Commit 1d62ef1

Browse files
committed
Implemented Sandro comments + give monopoles own transport process
Previous version was configuring O2MonopolePhysics globally which replaced the chord finder on the run G4FieldManager and also called G4Transportation::EnableMa gneticMoment(true), which are botbh global switched. Since NystromRK4 is incompatible with the monopole physics, the G4ClassicalRK4 was used, which I think is wrong, as I wanted to keep the original stepper the VMC macro asks for. Now the monopoles chord finder is built and not installed. In addition some minor things that were raised by Sandro's review and found while rechecking the code: - Now SetMaxstep(1.e10) is not called for monopoles, as that limit belongs to the dri ft volume rather than the track - TPC and ITS ProcessHits look up the monopole PDG code for neutral tracks only (reduction in calls) - Throw fatal in case Monopole Physics is requested but not installed properly The commit, together with the rest has been validated by running with Schwinger pair monopoles + various gD options.
1 parent 39381ca commit 1d62ef1

3 files changed

Lines changed: 154 additions & 34 deletions

File tree

‎Detectors/ITSMFT/ITS/simulation/src/Detector.cxx‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -321,9 +321,12 @@ Bool_t Detector::ProcessHits(FairVolume* vol)
321321
// This method is called from the MC stepping
322322
// Electrically neutral magnetic monopoles deposit energy in the
323323
// silicon through G4mplIonisation (Ahlen stopping power), so they must not be
324-
// rejected by the electric-charge gate
325-
const bool isMonopole = o2::sim::isMonopole(fMC->TrackPid());
326-
if (!(fMC->TrackCharge()) && !isMonopole) {
324+
// rejected by the electric-charge gate. PDG lookup
325+
// never runs for ordinary charged production.
326+
// To-do: handle dyons.
327+
const bool isNeutral = (fMC->TrackCharge() == 0);
328+
const bool isMonopole = isNeutral && o2::sim::isMonopole(fMC->TrackPid());
329+
if (isNeutral && !isMonopole) {
327330
return kFALSE;
328331
}
329332

‎Detectors/TPC/simulation/src/Detector.cxx‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,17 +122,22 @@ Bool_t Detector::ProcessHits(FairVolume* vol)
122122
// LOG(info) << "tpc::ProcessHits";
123123
const double trackCharge = fMC->TrackCharge();
124124
// Magnetic monopoles have zero electric charge but ionise the gas through
125-
// their magnetic charge energy loss (G4mplIonisation).
126-
const int trackPdg = fMC->TrackPid();
127-
const bool isMonopole = o2::sim::isMonopole(trackPdg);
125+
// their magnetic charge energy loss (G4mplIonisation), so they must not be
126+
// rejected by the electric-charge gate. The PDG lookup is only worth doing for neutral particle
127+
// To-do: add dyons case
128+
bool isMonopole = false;
128129
if (static_cast<int>(trackCharge) == 0) {
129-
// Fall through only for monopoles when ionisation is enabled.
130-
// The behaviour for the other neutral particles remains as before.
131-
if (!isMonopole || fMC->Edep() <= 0.) {
130+
isMonopole = o2::sim::isMonopole(fMC->TrackPid());
131+
if (!isMonopole) {
132132
// set a very large step size for neutral particles
133133
fMC->SetMaxStep(1.e10);
134134
return kFALSE; // take only charged particles
135135
}
136+
if (fMC->Edep() <= 0.) {
137+
// The monopole deposits nothing when no ionisation process is attached to
138+
// it (G4.monopole=0), so no hit to make.
139+
return kFALSE;
140+
}
136141
}
137142

138143
// ===| SET THE LENGTH OF THE NEXT ENERGY LOSS STEP |=========================

‎Detectors/gconfig/src/O2MonopolePhysics.cxx‎

Lines changed: 137 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
#include <G4MagneticField.hh>
6161
#include <G4Track.hh>
6262
#include <G4TrackingManager.hh>
63+
#include <G4CoupledTransportation.hh>
6364
#include <G4Transportation.hh>
6465
#include <G4TransportationManager.hh>
6566

@@ -234,8 +235,20 @@ class O2MonopoleEquation : public G4EquationOfMotion
234235
};
235236

236237
//____________________________________________________________________________
237-
/// Make the global field integrate O2MonopoleEquation instead of the default
238-
/// electric-charge-only equation.
238+
/// The field manager of the run, together with a second chord finder that
239+
/// integrates O2MonopoleEquation
240+
struct MonopoleFieldSetup {
241+
G4FieldManager* fieldManager = nullptr;
242+
G4ChordFinder* chordFinder = nullptr;
243+
};
244+
245+
//____________________________________________________________________________
246+
/// Build a chord finder that integrates O2MonopoleEquation instead of the
247+
/// default electric-charge-only equation.
248+
///
249+
/// It is deliberately NOT installed on the field manager here. The default
250+
/// chord finder is what Geant4-VMC configured for this run (NystromRK4 unless
251+
/// the macro says otherwise) and every ordinary particle keeps it
239252
///
240253
/// SetUserEquationOfMotion() in Geant4-VMC is not usable here: it
241254
/// registers the object with TG4GeometryManager, and the field integrator is
@@ -244,20 +257,22 @@ class O2MonopoleEquation : public G4EquationOfMotion
244257
///
245258
/// \param magneticChargeEplusUnits monopole magnetic charge in eplus units
246259
/// \param tpcDriftFieldGeant4 TPC drift field in Geant4 units (0 = disabled)
247-
inline void installMonopoleFieldIntegrator(double magneticChargeEplusUnits, double tpcDriftFieldGeant4)
260+
inline MonopoleFieldSetup buildMonopoleFieldSetup(double magneticChargeEplusUnits, double tpcDriftFieldGeant4)
248261
{
249262
auto* transportationManager = G4TransportationManager::GetTransportationManager();
250263
auto* fieldManager = transportationManager != nullptr ? transportationManager->GetFieldManager() : nullptr;
251264
if (fieldManager == nullptr) {
252-
LOG(error) << "O2MonopolePhysics: no G4FieldManager, monopole equation of motion NOT installed";
253-
return;
265+
LOG(fatal) << "O2MonopolePhysics: no G4FieldManager, the monopole equation of motion "
266+
"cannot be installed; rerun with G4.monopole=0 if that is what you want";
267+
return {};
254268
}
255269
auto* magneticField =
256270
const_cast<G4MagneticField*>(dynamic_cast<const G4MagneticField*>(fieldManager->GetDetectorField()));
257271
if (magneticField == nullptr) {
258-
LOG(error) << "O2MonopolePhysics: no magnetic field attached to the field manager, "
259-
"monopole equation of motion NOT installed";
260-
return;
272+
LOG(fatal) << "O2MonopolePhysics: no magnetic field attached to the field manager, the "
273+
"monopole equation of motion cannot be installed; rerun with G4.monopole=0 "
274+
"if that is what you want";
275+
return {};
261276
}
262277

263278
auto* equation = new O2MonopoleEquation(magneticField, magneticChargeEplusUnits, tpcDriftFieldGeant4);
@@ -273,17 +288,111 @@ inline void installMonopoleFieldIntegrator(double magneticChargeEplusUnits, doub
273288
if (previous != nullptr) {
274289
chordFinder->SetDeltaChord(previous->GetDeltaChord());
275290
}
276-
fieldManager->SetChordFinder(chordFinder);
277291

278292
if (tpcDriftFieldGeant4 > 0.) {
279-
LOG(info) << "O2MonopolePhysics: monopole equation of motion installed (F = g*(B - v x E/c^2)), "
293+
LOG(info) << "O2MonopolePhysics: monopole equation of motion built (F = g*(B - v x E/c^2)), "
280294
"magnetic charge = "
281295
<< magneticChargeEplusUnits << " eplus, TPC drift field = "
282296
<< tpcDriftFieldGeant4 / (CLHEP::volt / CLHEP::cm) << " V/cm";
283297
} else {
284-
LOG(info) << "O2MonopolePhysics: monopole equation of motion installed (F = g*B), magnetic charge = "
298+
LOG(info) << "O2MonopolePhysics: monopole equation of motion built (F = g*B), magnetic charge = "
285299
<< magneticChargeEplusUnits << " eplus (TPC drift field coupling disabled)";
286300
}
301+
return {fieldManager, chordFinder};
302+
}
303+
304+
//____________________________________________________________________________
305+
/// Transportation for the monopole species only.
306+
///
307+
/// These must be true for the monopoles and false for every other particle:
308+
///
309+
/// - the field manager has to use the monopole chord finder, otherwise O2MonopoleEquation
310+
/// might not be evaluated at all (such as it happens with the default NystromRK4)
311+
/// - G4Transportation has to consider the magnetic moment
312+
///
313+
/// Ordinary tracks keep the stepper the run was configured with, and neutral particles that
314+
/// happen to carry a magnetic moment (neutrons above all) keep their
315+
/// straight-line transport.
316+
///
317+
/// This mirrors G4MonopoleTransportation from the Geant4 monopole example, but
318+
/// by derives directly from G4Transportation, so that it follows the Geant4 versions
319+
class O2MonopoleTransportation : public G4Transportation
320+
{
321+
public:
322+
O2MonopoleTransportation(G4FieldManager* fieldManager, G4ChordFinder* monopoleChordFinder)
323+
: G4Transportation(0), mFieldManager(fieldManager), mMonopoleChordFinder(monopoleChordFinder)
324+
{
325+
}
326+
327+
G4double AlongStepGetPhysicalInteractionLength(const G4Track& track, G4double previousStepSize,
328+
G4double currentMinimumStep, G4double& currentSafety,
329+
G4GPILSelection* selection) override
330+
{
331+
const G4bool previousMoment = G4Transportation::EnableMagneticMoment(true);
332+
auto* previousChordFinder = mFieldManager->GetChordFinder();
333+
mFieldManager->SetChordFinder(mMonopoleChordFinder);
334+
335+
const G4double length = G4Transportation::AlongStepGetPhysicalInteractionLength(
336+
track, previousStepSize, currentMinimumStep, currentSafety, selection);
337+
338+
mFieldManager->SetChordFinder(previousChordFinder);
339+
G4Transportation::EnableMagneticMoment(previousMoment);
340+
return length;
341+
}
342+
343+
private:
344+
G4FieldManager* mFieldManager; ///< field manager of the run, not owned
345+
G4ChordFinder* mMonopoleChordFinder; ///< installed only for the duration of a monopole step, not owned
346+
};
347+
348+
//____________________________________________________________________________
349+
/// Replace the transport of one monopole species with
350+
/// O2MonopoleTransportation, keeping it first in the DoIt vectors exactly as
351+
/// G4VUserPhysicsList::AddTransportation() left it.
352+
inline bool installMonopoleTransport(G4ProcessManager* pmanager, const G4ParticleDefinition* particle,
353+
const MonopoleFieldSetup& fieldSetup)
354+
{
355+
G4VProcess* existing = nullptr;
356+
G4ProcessVector* plist = pmanager->GetProcessList();
357+
for (G4int ip = 0; ip < static_cast<G4int>(plist->size()); ++ip) {
358+
if (dynamic_cast<G4Transportation*>((*plist)[ip]) != nullptr) {
359+
existing = (*plist)[ip];
360+
break;
361+
}
362+
}
363+
if (existing == nullptr) {
364+
LOG(fatal) << "O2MonopolePhysics: " << particle->GetParticleName()
365+
<< " has no transportation process to replace; the monopole could not be "
366+
"coupled to the field";
367+
return false;
368+
}
369+
if (dynamic_cast<G4CoupledTransportation*>(existing) != nullptr) {
370+
// Parallel worlds are in use. O2MonopoleTransportation derives from plain
371+
// G4Transportation, so swapping it in would drop the parallel-world
372+
// navigation; refuse rather than silently mis-navigate.
373+
LOG(fatal) << "O2MonopolePhysics: " << particle->GetParticleName()
374+
<< " uses G4CoupledTransportation (parallel worlds); the monopole transportation "
375+
"does not support that. Rerun with G4.monopole=0 or without parallel worlds";
376+
return false;
377+
}
378+
379+
// One G4Transportation instance is shared by every particle
380+
// (G4VUserPhysicsList::AddTransportation creates a single one), so it is
381+
// detached from this particle only and must not be deleted.
382+
pmanager->RemoveProcess(existing);
383+
384+
auto* transportation = new O2MonopoleTransportation(fieldSetup.fieldManager, fieldSetup.chordFinder);
385+
pmanager->AddProcess(transportation);
386+
// Transportation has to be first in the DoIt vectors
387+
//
388+
// Geant4 prints "Set Ordering First is invoked twice for Transportation to
389+
// <particle>" (ProcMan113, JustWarning) once per call here, because
390+
// G4ProcessManager latches isSetOrderingFirstInvoked and RemoveProcess() does
391+
// not clear it. The insertion is performed before that check and is correct;
392+
// the warning in the stdout is expected and harmless.
393+
pmanager->SetProcessOrderingToFirst(transportation, idxAlongStep);
394+
pmanager->SetProcessOrderingToFirst(transportation, idxPostStep);
395+
return true;
287396
}
288397

289398
//____________________________________________________________________________
@@ -310,6 +419,13 @@ class O2MonopolePhysics : public G4VUserPhysicsList
310419
void ConstructProcess() override
311420
{
312421
auto* table = G4ParticleTable::GetParticleTable();
422+
423+
// Built once and shared by all monopoles species: O2MonopoleEquation reads the
424+
// sign of the magnetic charge off the track, so one chord finder serves
425+
// monopoles and anti-monopoles
426+
const MonopoleFieldSetup fieldSetup =
427+
buildMonopoleFieldSetup(mMagneticCharge / CLHEP::eplus, tpcDriftFieldMagnitude());
428+
313429
int nAttached = 0;
314430
for (int pdg : gMonopolePDGs) {
315431
auto* particle = table->FindParticle(pdg);
@@ -357,25 +473,20 @@ class O2MonopolePhysics : public G4VUserPhysicsList
357473
if (particle->GetPDGMagneticMoment() == 0.) {
358474
particle->SetPDGMagneticMoment(gMonopoleFieldGateMoment);
359475
}
476+
477+
// Deflect the monopole in the field as well; without this only the energy
478+
// loss above would act and the monopole would fly straight through, since
479+
// its electric charge (and hence the usual Lorentz force) is zero.
480+
installMonopoleTransport(pmanager, particle, fieldSetup);
481+
360482
++nAttached;
361-
LOG(info) << "O2MonopolePhysics: attached G4mplIonisation to "
483+
LOG(info) << "O2MonopolePhysics: attached G4mplIonisation and monopole transport to "
362484
<< particle->GetParticleName() << " (PDG " << pdg
363485
<< "), magnetic charge = " << mMagneticCharge / CLHEP::eplus << " eplus";
364486
}
365487
if (nAttached == 0) {
366488
LOG(warning) << "O2MonopolePhysics: no monopole particle found; no ionisation attached";
367489
}
368-
369-
// This static switch is what makes G4Transportation consider the μ of the monopole
370-
// It is global, so electrically neutral particles that already carry a momentum (neutrons) are
371-
// now propagated through the field as well; O2MonopoleEquation gives them
372-
// exactly zero force, so their trajectories are unchanged.
373-
G4Transportation::EnableMagneticMoment(true);
374-
375-
// Deflect the monopole in the field as well; without this only the energy
376-
// loss above would act and the monopole would fly straight through, since
377-
// its electric charge (and hence the usual Lorentz force) is zero.
378-
installMonopoleFieldIntegrator(mMagneticCharge / CLHEP::eplus, tpcDriftFieldMagnitude());
379490
}
380491

381492
private:
@@ -406,8 +517,9 @@ class O2G4RunConfiguration : public o2::fastsim::G4RunConfiguration
406517
LOG(info) << "O2G4RunConfiguration: monopole ionisation physics registered "
407518
"on the composed physics list";
408519
} else {
409-
LOG(error) << "O2G4RunConfiguration: physics list is not a TG4ComposedPhysicsList, "
410-
"monopole ionisation could NOT be enabled";
520+
LOG(fatal) << "O2G4RunConfiguration: physics list is not a TG4ComposedPhysicsList, "
521+
"monopole ionisation cannot be enabled; rerun with G4.monopole=0 if that "
522+
"is what you want";
411523
}
412524
return physicsList;
413525
}

0 commit comments

Comments
 (0)