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