Skip to content
Open
17 changes: 17 additions & 0 deletions Common/SimConfig/include/SimConfig/G4Params.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

#include "CommonUtils/ConfigurableParam.h"
#include "CommonUtils/ConfigurableParamHelper.h"
#include "SimulationDataFormat/MonopoleParticles.h"

namespace o2
{
Expand Down Expand Up @@ -55,6 +56,22 @@ struct G4Params : public o2::conf::ConfigurableParamHelper<G4Params> {
bool g4scoring = false;
bool g4fluenceweight = false;

// Enable magnetic-monopole ionisation as defined in Detectors/gconfig/O2MonopolePhysics.
// The G4mplIonisation process (Ahlen stopping power) is attached to
// the O2 monopole particles (PDG +-4110000 / +-4120000) on top of the chosen
// reference physics list. Off by default
bool monopole = false;
// Monopole magnetic charge in units of the Dirac charge g_D = eplus/(2*alpha)
// (~68.5 eplus). 1.0 corresponds to a single classic Dirac monopole.
// The monopole is transported with the full dual Lorentz force
// F = g*(B - v x E/c^2). The electric part acts inside the TPC field cage,
// the only volume with an electric field, and its magnitude is the TPC's own
// TPCGEMParam.ElectricField[0]
float monopoleMagneticCharge = 1.f;
// Monopole mass in GeV, preliminarly applied to all the monopoles species
// To-do: allow different monopoles masses in the same run
float monopoleMass = static_cast<float>(o2::sim::MonopoleMassDefaultGeV);

// Fast simulation. Empty fastSimModels (the default) disables the feature
// entirely; see Detectors/gconfig/include/SimSetup/G4FastSimulation.h.
std::string fastSimModels = ""; // comma-separated model names to activate
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2019-2026 CERN and copyright holders of ALICE O2.
// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders.
// All rights not expressly granted are reserved.
//
// This software is distributed under the terms of the GNU General Public
// License v3 (GPL Version 3), copied verbatim in the file "COPYING".
//
// In applying this license CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.

/// \file MonopoleParticles.h
/// \brief Identification of the BSM magnetic monopoles across the simulation
/// @author M. Giacalone - September 2026
///
/// The monopole PDG codes are defined in O2MCApplication::AddParticles() and in
/// O2DatabasePDG. They are collected here for better syncing inside the simulation code
///
/// To do: add dyons and other monopoles configurations when needed

#ifndef ALICEO2_SIMULATION_MONOPOLEPARTICLES_H_
#define ALICEO2_SIMULATION_MONOPOLEPARTICLES_H_

namespace o2::sim
{

/// Both species are electrically neutral for now, and the transport gives them the
/// same magnetic charge, with its sign taken from the PDG sign.
/// monopole intended to carry equal electric and magnetic charge
constexpr int MonopolePdgSymm = 4110000;
/// monopole intended to carry opposite electric and magnetic charge (not yet distinguished)
constexpr int MonopolePdgAsymm = 4120000;

/// Default monopole mass in GeV.
constexpr double MonopoleMassDefaultGeV = 100.;

/// true for the monopole and antimonopole of both species
constexpr bool isMonopole(int pdg) noexcept
{
const int abspdg = pdg < 0 ? -pdg : pdg;
return abspdg == MonopolePdgSymm || abspdg == MonopolePdgAsymm;
}

} // namespace o2::sim

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
#ifndef O2_O2DATABASEPDG_H
#define O2_O2DATABASEPDG_H

#include <cmath>
#include <string>
#include "TDatabasePDG.h"
#include "TParticlePDG.h"
#include "SimulationDataFormat/MonopoleParticles.h"

namespace o2
{
Expand All @@ -44,9 +46,28 @@ class O2DatabasePDG
}

// adds ALICE particles to a given TDatabasePDG instance
static void addALICEParticles(TDatabasePDG* db = TDatabasePDG::Instance());
// monopoleMass is the mass of all monopole species in GeV; it has to match
// G4Params.monopoleMass or the generator and the transport will disagree
// To fix: all quantities in the MCTrack dependent from GetMass() will get the default 100 GeV.
// A possible fix would be to stop deriving the masses from the compiled-in table,
// or a better solution should be found in the future
static void addALICEParticles(TDatabasePDG* db = TDatabasePDG::Instance(),
double monopoleMass = o2::sim::MonopoleMassDefaultGeV);
static void addParticlesFromExternalFile(TDatabasePDG* db);

// true if all monopole species are registered with the given mass in GeV; TDatabasePDG
// keeps the first registration, so an earlier call with another mass makes this false
static bool hasMonopoleMass(TDatabasePDG* db, double monopoleMass)
{
for (int pdg : {o2::sim::MonopolePdgSymm, -o2::sim::MonopolePdgSymm, o2::sim::MonopolePdgAsymm, -o2::sim::MonopolePdgAsymm}) {
const auto* particle = db->GetParticle(pdg);
if (particle == nullptr || std::abs(particle->Mass() - monopoleMass) > 1.e-6 * monopoleMass) {
return false;
}
}
return true;
}

// get particle's (if any) mass
static Double_t MassImpl(TParticlePDG* particle, bool& success)
{
Expand Down Expand Up @@ -84,7 +105,7 @@ class O2DatabasePDG

// by keeping this inline, we can use it in other parts of the code, for instance Framework or Analysis,
// without needing to link against this library
inline void O2DatabasePDG::addALICEParticles(TDatabasePDG* db)
inline void O2DatabasePDG::addALICEParticles(TDatabasePDG* db, double monopoleMass)
{
//
// Add ALICE particles to the ROOT PDG data base
Expand Down Expand Up @@ -189,6 +210,14 @@ inline void O2DatabasePDG::addALICEParticles(TDatabasePDG* db)
db->AddParticle("CHI2P_B2", " ", 10.269, kFALSE, 0.0, 0, "meson", 100555);
db->AddParticle("UPSLON4S", " ", 10.580, kFALSE, 0.0, 0, "meson", 300553);

// BSM targeted inclusions
// Monopoles with same electric and magnetic charge
db->AddParticle("Monopole_symm", "Monopole_symm", monopoleMass, kTRUE, 0.0, 0, "BSM", 4110000);
db->AddParticle("AntiMonopole_symm", "AntiMonopole_symm", monopoleMass, kTRUE, 0.0, 0, "BSM", -4110000);
// Monopoles with opposite electric and magnetic charge
db->AddParticle("Monopole_asymm", "Monopole_asymm", monopoleMass, kTRUE, 0.0, 0, "BSM", 4120000);
db->AddParticle("AntiMonopole_asymm", "AntiMonopole_asymm", monopoleMass, kTRUE, 0.0, 0, "BSM", -4120000);

// IONS
//
// Done by default now from Pythia6 table
Expand Down
16 changes: 15 additions & 1 deletion Detectors/ITSMFT/ITS/simulation/src/Detector.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@

#include "DetectorsBase/Stack.h"
#include "SimulationDataFormat/TrackReference.h"
#include "SimulationDataFormat/MonopoleParticles.h"
#include "SimConfig/G4Params.h"
#include "fairlogger/Logger.h" // for LOG, LOG_IF

// FairRoot includes
Expand Down Expand Up @@ -318,7 +320,15 @@ void Detector::InitializeO2Detector()
Bool_t Detector::ProcessHits(FairVolume* vol)
{
// This method is called from the MC stepping
if (!(fMC->TrackCharge())) {
// Electrically neutral magnetic monopoles deposit energy in the
// silicon through G4mplIonisation (Ahlen stopping power), so they must not be
// rejected by the electric-charge gate. The PDG lookup only runs for neutral
// particles in runs with monopole physics enabled.
// To-do: handle dyons.
static const bool sMonopoleIonisation = o2::conf::G4Params::Instance().monopole;
const bool isNeutral = (fMC->TrackCharge() == 0);
const bool isMonopole = isNeutral && sMonopoleIonisation && o2::sim::isMonopole(fMC->TrackPid());
if (isNeutral && !isMonopole) {
return kFALSE;
}

Expand Down Expand Up @@ -386,6 +396,10 @@ Bool_t Detector::ProcessHits(FairVolume* vol)
mTrackData.mHitStarted = true;
}
if (stopHit) {
// A monopole that deposited nothing in the sensor leaves no hit
if (isMonopole && mTrackData.mEnergyLoss <= 0.) {
return kFALSE;
}
TLorentzVector positionStop;
fMC->TrackPosition(positionStop);
// Retrieve the indices with the volume path
Expand Down
35 changes: 30 additions & 5 deletions Detectors/TPC/simulation/src/Detector.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

#include "DetectorsBase/Stack.h"
#include "SimulationDataFormat/TrackReference.h"
#include "SimulationDataFormat/MonopoleParticles.h"
#include "SimConfig/G4Params.h"

#include "FairVolume.h" // for FairVolume

Expand Down Expand Up @@ -120,11 +122,24 @@ Bool_t Detector::ProcessHits(FairVolume* vol)
/* This method is called from the MC stepping for the sensitive volume only */
// LOG(info) << "tpc::ProcessHits";
const double trackCharge = fMC->TrackCharge();
// Magnetic monopoles have zero electric charge but ionise the gas through
// their magnetic charge energy loss (G4mplIonisation), so they must not be
// rejected by the electric-charge gate. The PDG lookup is only worth doing for neutral particle
// To-do: add dyons case
bool isMonopole = false;
if (static_cast<int>(trackCharge) == 0) {

// set a very large step size for neutral particles
fMC->SetMaxStep(1.e10);
return kFALSE; // take only charged particles
static const bool sMonopoleIonisation = o2::conf::G4Params::Instance().monopole;
isMonopole = sMonopoleIonisation && o2::sim::isMonopole(fMC->TrackPid());
if (!isMonopole) {
// set a very large step size for neutral particles
fMC->SetMaxStep(1.e10);
return kFALSE; // take only charged particles
}
if (fMC->Edep() <= 0.) {
// An ionising monopole depositing nothing on this step: there
// is no hit to create, and the step limit is deliberately left alone
return kFALSE;
}
}

// ===| SET THE LENGTH OF THE NEXT ENERGY LOSS STEP |=========================
Expand Down Expand Up @@ -199,7 +214,7 @@ Bool_t Detector::ProcessHits(FairVolume* vol)
// TODO: Add discussion about drawback

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

// use Geant4 energy deposit directly for ionisation (Kr-83m calibration simulations)
if (detParam.UseGeant4Edep) {
Expand All @@ -210,6 +225,16 @@ Bool_t Detector::ProcessHits(FairVolume* vol)
if (meanIon > 0.) {
numberOfElectrons = static_cast<int>(gasParam.FanoFactorG4 * Gamma(meanIon / gasParam.FanoFactorG4));
}
} else if (isMonopole) {
// ---| MONOPOLE IONISATION |--------
// A magnetic monopole ionises the gas via G4mplIonisation (Ahlen stopping
// power), which is not described by the electric-charge model (no e-charge)
// Ionisation electrons calculated directly from the energy deposited in this step: Nel = Edep / Wion.
// To check by TPC experts if this is actually the best way...
numberOfElectrons = static_cast<int>(fMC->Edep() / static_cast<double>(gasParam.Wion));
// The number of electrons is stored as a float in the HitGroup: maximum cap at
// 2^24 (16777216) ==> largest integer a IEEE-754 float can represent exactly
numberOfElectrons = TMath::Min(numberOfElectrons, 16777216);
} else {
// ---| Stepsize in cm |---
const double stepSize = fMC->TrackStep();
Expand Down
2 changes: 1 addition & 1 deletion Detectors/gconfig/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ o2_add_library(G3Setup
)

o2_add_library(G4Setup
SOURCES src/G4Config.cxx
SOURCES src/G4Config.cxx src/O2MonopolePhysics.cxx
PUBLIC_LINK_LIBRARIES MC::Geant4VMC MC::Geant4 FairRoot::Base O2::SimulationDataFormat O2::Generators O2::SimSetup O2::FastSim
)

Expand Down
23 changes: 17 additions & 6 deletions Detectors/gconfig/g4Config.C
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ R__LOAD_LIBRARY(libgeant4vmc)
#include "TG4RunConfiguration.h"
#include "SimConfig/G4Params.h"
#include "SimConfig/FluenceWeightCalculator.h"
#include "SimSetup/O2MonopolePhysics.h"
#include "SimConfig/G4ScoringMerger.h"
#include "G4ScoringManager.hh"
#include "G4VScoringMesh.hh"
Expand Down Expand Up @@ -119,12 +120,22 @@ void Config()
LOG(fatal) << "Unsupported geometry navigation mode";
}

// o2::fastsim::G4RunConfiguration differs from TG4RunConfiguration only in
// providing the fast-simulation hook; with G4.fastSimModels empty it behaves
// identically.
auto runConfiguration = new o2::fastsim::G4RunConfiguration(geomNavStr, physicsSetup,
"stepLimiter+specialCuts",
specialStacking, mtMode);
TG4RunConfiguration* runConfiguration = nullptr;
if (g4Params.monopole) {
// Reference physics list + magnetic-monopole ionisation attached
// The reference physics is unchanged.
std::cout << "Monopole ionisation physics requested (G4.monopole=1)\n";
runConfiguration = o2::g4config::createMonopoleRunConfiguration(
geomNavStr, physicsSetup, "stepLimiter+specialCuts", specialStacking, mtMode,
g4Params.monopoleMagneticCharge);
} else {
// o2::fastsim::G4RunConfiguration differs from TG4RunConfiguration only in
// providing the fast-simulation hook; with G4.fastSimModels empty it behaves
// identically.
runConfiguration = new o2::fastsim::G4RunConfiguration(geomNavStr, physicsSetup,
"stepLimiter+specialCuts",
specialStacking, mtMode);
}
if (g4Params.g4scoring) {
runConfiguration->SetUseOfG4Scoring();
if (g4Params.g4fluenceweight) {
Expand Down
51 changes: 51 additions & 0 deletions Detectors/gconfig/include/SimSetup/O2MonopolePhysics.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright 2019-2026 CERN and copyright holders of ALICE O2.
// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders.
// All rights not expressly granted are reserved.
//
// This software is distributed under the terms of the GNU General Public
// License v3 (GPL Version 3), copied verbatim in the file "COPYING".
//
// In applying this license CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.

/// \author M+Giacalone - July 2026

#ifndef O2_G4CONFIG_O2MONOPOLEPHYSICS_H_
#define O2_G4CONFIG_O2MONOPOLEPHYSICS_H_

#include <TString.h>

class TG4RunConfiguration;

namespace o2
{
namespace g4config
{

/// Build a Geant4-VMC run configuration which attaches the magnetic-monopole ionisation process
/// (G4mplIonisation) to the requested reference physics list.
/// the monopole particles already defined by O2 (PDG +-4110000 / +-4120000).
///
/// Only used when G4Params.monopole == true;
/// By default, the standard TG4RunConfiguration is created
///
/// \param userGeometry VMC geometry-navigation string (as for TG4RunConfiguration)
/// \param physicsList the reference physics-list selection string
/// \param specialProcess the VMC special-process selection string
/// \param specialStacking VMC special-stacking flag
/// \param mtApplication multithreading flag
/// \param magneticChargeDirac magnetic charge of the monopole in units of the
/// Dirac charge g_D = eplus/(2*alpha) (~68.5 eplus);
/// 1.0 (default) reproduces the classic single Dirac monopole
TG4RunConfiguration* createMonopoleRunConfiguration(const TString& userGeometry,
const TString& physicsList,
const TString& specialProcess,
bool specialStacking,
bool mtApplication,
double magneticChargeDirac);

} // namespace g4config
} // namespace o2

#endif // O2_G4CONFIG_O2MONOPOLEPHYSICS_H_
Loading
Loading