EM Opt4 produces unphysically high energy depositions

Geant4 Version: 11.4.1 (MT build)
Operating System: Debian GNU/Linux “forky” (testing)
Compiler/Version: GCC 15.2.0 (Debian 15.2.0-17)
CMake Version: 4.3.3


Summary

G4EmStandardPhysics_option4 (EM Opt4) produces energy deposits that exceed the physically allowed upper bound in a simple G4Box geometry. The anomaly is direction-dependent, it occurs most when the detector is placed on the positive Y axis but not on the negative Y axis (primaries are shoot towards the detector). The issue is reproducible with electrons, positrons, and photons, with positrons having the most anomalous events. Switching to G4EmStandardPhysics_option3 eliminates the anomaly entirely (this is also true for option3 with fUseSafetyPlus step limit).

Geometry

  • World: 2×2×2 m³ box of G4_AIR
  • Target: 10×20×10 cm³ box of G4_CESIUM_IODIDE (CsI)
  • Target position: (0, 50, 0) cm, the 10×10cm end faces the origin
  • Particle gun: at the origin (0, 0, 0), firing toward the detector in direction (0, 1, 0)
  • Scoring: total energy deposit per event in the CsI volume

Physics List

  • G4EmStandardPhysics_option4 (EM Opt4)
  • No other physics processes registered

Primary Particles

Each particle type was simulated with 1,000,000 events at 50 MeV kinetic energy:

Particle Kinetic Energy Direction
e⁻ 50 MeV (0, 1, 0)
e⁺ 50 MeV (0, 1, 0)
γ 50 MeV (0, 1, 0)

Results

Anomalous Event Fraction

I set a energy deposition threshold:

$$E_{\text{threshold}} = (1 + \varepsilon) \times (E_{\text{kin}} + 2 m_e c^2)$$

where $\varepsilon$ is the machine epsilon for float. Any single event with Edep exceeding this threshold is flagged as anomalous.

Particle Events Simulated Anomalous Events Fraction
e⁻ 1,000,000 15 0.0015%
e⁺ 1,000,000 380 0.0380%
γ 1,000,000 153 0.0153%

Energy Deposition Spectra

In the plots below, the red line is EM Opt4 and the blue line is EM Opt3 + fUseSafetyPlus.

Electrons:

Positrons:

Photons:

Observations

  1. Directional dependence: When the CsI box is placed at (0, −50, 0) cm (i.e., on the negative Y axis, with the particle still fired toward it from the origin), no anomalous events occur.

  2. EM Opt3 is unaffected: Switching the physics list to G4EmStandardPhysics_option3 (optionally with fUseSafetyPlus) eliminates all anomalous events.

  3. Positron low-energy discrepancy: For positrons, EM Opt3 + fUseSafetyPlus shows more events in the sub-20 MeV region compared to EM Opt4 (visible as the blue line being above the red line below ~20 MeV). Given that the total event count is the same, this suggests that at least some of the anomalous high-energy events seen with Opt4 may be events whose true energy deposit belongs in the lower-energy region.

Reproduction Code

A reproduction program (main.cxx) is provided below.

Build:

mkdir build && cd build && cmake .. && make

Run:

./opt4bug 1000000 0 50 0 e+ 50

The program writes opt4bug_edep.root with per-event Edep values and prints anomalous events to stdout.

CMakeLists.txt

cmake_minimum_required(VERSION 3.16)
project(Opt4BugRepro)
find_package(Geant4 REQUIRED)
add_executable(opt4bug main.cxx)
target_link_libraries(opt4bug ${Geant4_LIBRARIES})
target_compile_options(opt4bug PRIVATE -Wall -Wextra -pedantic)

main.cxx

// Bug reproduction: G4EmStandardPhysics_option4 + G4Box
// Output: per-event Edep written to ROOT ntuple (opt4bug_edep.root)
//
// Build: mkdir build && cd build && cmake .. && make
// Run:   ./opt4bug nEvents x y z particleName energy
//   nEvents         Number of events
//   x y z           Box position in cm
//   particleName    Incident particle (e.g. gamma, e-, proton)
//   energy          Incident kinetic energy in MeV
//
//   Long side of the box always faces the origin.
//   Photon always incident on the near small face.

#include "G4Accumulable.hh"
#include "G4AccumulableManager.hh"
#include "G4AnalysisManager.hh"
#include "G4Box.hh"
#include "G4EmStandardPhysics_option3.hh"
#include "G4EmStandardPhysics_option4.hh"
#include "G4Event.hh"
#include "G4LogicalVolume.hh"
#include "G4NistManager.hh"
#include "G4ParticleGun.hh"
#include "G4ParticleTable.hh"
#include "G4PhysicalConstants.hh"
#include "G4PVPlacement.hh"
#include "G4Run.hh"
#include "G4RunManager.hh"
#include "G4RunManagerFactory.hh"
#include "G4Step.hh"
#include "G4SystemOfUnits.hh"
#include "G4Threading.hh"
#include "G4UserEventAction.hh"
#include "G4UserRunAction.hh"
#include "G4UserSteppingAction.hh"
#include "G4VModularPhysicsList.hh"
#include "G4VUserActionInitialization.hh"
#include "G4EmParameters.hh"
#include "G4VUserDetectorConstruction.hh"
#include "G4VUserPrimaryGeneratorAction.hh"

#include <atomic>
#include <cmath>
#include <iomanip>
#include <memory>
#include <string>

namespace {
std::atomic<int> gProgressCounter{0};
constexpr int kProgressInterval{1000};
} // namespace

// ============================================================================
// DetectorConstruction
// ============================================================================
class DetectorConstruction : public G4VUserDetectorConstruction {
public:
    explicit DetectorConstruction(const G4ThreeVector& pos) :
        fPosition{pos} {}

    auto Construct() -> G4VPhysicalVolume* override {
        auto nist{G4NistManager::Instance()};
        auto checkOverlaps{true};

        // --- World: 2x2x2 m^3 box of G4_AIR ---
        auto solidWorld(new G4Box{"World", 1.0 * m, 1.0 * m, 1.0 * m});
        auto logicWorld(new G4LogicalVolume{solidWorld, nist->FindOrBuildMaterial("G4_AIR"), "World"});
        auto physWorld(new G4PVPlacement{nullptr, G4ThreeVector{}, logicWorld, "World", nullptr, false, 0, checkOverlaps});

        // --- Target: 10×20×10 cm³ CsI box, long side (Y) faces origin ---
        auto solidECAL(new G4Box{"ECAL", 5.0 * cm, 10.0 * cm, 5.0 * cm});
        auto logicECAL(new G4LogicalVolume{solidECAL, nist->FindOrBuildMaterial("G4_CESIUM_IODIDE"), "ECAL"});
        fTargetVolume = logicECAL;

        // Build rotation: local Y axis → dirToOrigin (= -fPosition.unit())
        auto colY{-fPosition.unit()};
        auto ref((std::abs(colY.x()) < 0.9) ? G4ThreeVector{1, 0, 0} : G4ThreeVector{0, 0, 1});
        auto colZ{colY.cross(ref).unit()};
        auto colX{colY.cross(colZ).unit()};
        auto rot(new G4RotationMatrix{colX, colY, colZ});

        new G4PVPlacement{rot, fPosition, logicECAL,
                          "ECAL", logicWorld, false, 0, checkOverlaps};

        return physWorld;
    }

    auto GetECALVolume() const -> G4LogicalVolume* { return fTargetVolume; }

private:
    G4LogicalVolume* fTargetVolume{nullptr};
    G4ThreeVector fPosition;
};

// ============================================================================
// PrimaryGeneratorAction
// ============================================================================
class PrimaryGeneratorAction : public G4VUserPrimaryGeneratorAction {
public:
    PrimaryGeneratorAction(const G4ThreeVector& dir,
                           const std::string& particleName,
                           double energy) :
        fGun{1} {
        fGun.SetParticleDefinition(G4ParticleTable::GetParticleTable()->FindParticle(particleName));
        fGun.SetParticleEnergy(energy);
        fGun.SetParticlePosition(G4ThreeVector{0.0, 0.0, 0.0});
        fGun.SetParticleMomentumDirection(dir.unit());
    }

    auto GeneratePrimaries(G4Event* event) -> void override {
        fGun.GeneratePrimaryVertex(event);
    }

    auto GetGun() const -> const G4ParticleGun* { return &fGun; }

private:
    G4ParticleGun fGun;
};

// ============================================================================
// RunAction
// ============================================================================
class RunAction : public G4UserRunAction {
public:
    RunAction() {
        G4AccumulableManager::Instance()->Register(fTotalEvents);
        G4AccumulableManager::Instance()->Register(fAnomalousEvts);

        // --- ROOT analysis setup ---
        auto am{G4AnalysisManager::Instance()};
        am->SetDefaultFileType("root");
        am->SetNtupleMerging(true); // required for MT merging
        am->SetVerboseLevel(0);     // suppress per-event analysis chatter
        am->SetFileName("opt4bug");

        // Book ntuple with a single double column: Edep (MeV)
        am->CreateNtuple("Edep", "Energy deposit per event");
        am->CreateNtupleFColumn("Edep");
        am->FinishNtuple();
        // Use a separate file for the ntuple (opt4bug_edep.root)
        am->SetNtupleFileName(0, "opt4bug_edep");
    }

    auto BeginOfRunAction(const G4Run*) -> void override {
        G4AccumulableManager::Instance()->Reset();
        auto am{G4AnalysisManager::Instance()};
        am->Reset();
        am->OpenFile();
    }

    auto EndOfRunAction(const G4Run*) -> void override {
        G4AccumulableManager::Instance()->Merge();

        auto nTotal{fTotalEvents.GetValue()};
        auto nAnomal{fAnomalousEvts.GetValue()};

        if (IsMaster()) {
            auto fraction{nTotal > 0 ? 100.0 * nAnomal / nTotal : 0.0};
            G4cout << G4endl
                   << "============================================================" << G4endl
                   << "  Run Summary (Master)  —  " << nTotal << " events" << G4endl
                   << "============================================================" << G4endl
                   << "  Anomalous events:      " << nAnomal
                   << "  (" << std::fixed << std::setprecision(3) << fraction << "%)" << G4endl
                   << "  ROOT output:           opt4bug_edep.root" << G4endl
                   << "============================================================" << G4endl;
        }

        // Write and close analysis files
        auto am{G4AnalysisManager::Instance()};
        am->Write();
        am->CloseFile(false);
    }

    auto CountEvent() -> void {
        fTotalEvents += 1;
    }

    auto CountAnomalous() -> void {
        fAnomalousEvts += 1;
    }

private:
    G4Accumulable<int> fTotalEvents{0};
    G4Accumulable<int> fAnomalousEvts{0};
};

// ============================================================================
// EventAction
// ============================================================================
class EventAction : public G4UserEventAction {
public:
    EventAction(RunAction* runAction, double threshold) :
        fRunAction{runAction},
        fThreshold{threshold} {}

    auto BeginOfEventAction(const G4Event*) -> void override {
        fEdep = 0.0;
    }

    auto EndOfEventAction(const G4Event*) -> void override {
        fRunAction->CountEvent();

        if (fEdep > fThreshold) {
            fRunAction->CountAnomalous();
            G4cout << "  *** ANOMALOUS! Edep = " << fEdep / MeV
                   << " MeV > " << fThreshold / MeV << " MeV ***" << G4endl;
        }

        // Fill ROOT ntuple
        auto am{G4AnalysisManager::Instance()};
        am->FillNtupleFColumn(0, fEdep / MeV);
        am->AddNtupleRow();

        // Print progress every kProgressInterval completed events (across all threads)
        auto done{++gProgressCounter};
        if (done % kProgressInterval == 0) {
            G4cout << ">>> Progress: " << done << " events completed" << G4endl;
        }
    }

    auto AddEdep(double e) -> void { fEdep += e; }

private:
    RunAction* fRunAction{nullptr};
    double fThreshold;
    double fEdep{0.0};
};

// ============================================================================
// SteppingAction
// ============================================================================
class SteppingAction : public G4UserSteppingAction {
public:
    SteppingAction(EventAction* evtAction) :
        fEventAction{evtAction} {}

    auto UserSteppingAction(const G4Step* step) -> void override {
        if (!fScoringVolume) {
            const auto det{
                static_cast<const DetectorConstruction*>(
                    G4RunManager::GetRunManager()->GetUserDetectorConstruction())};
            fScoringVolume = det->GetECALVolume();
        }
        auto vol{step->GetPreStepPoint()->GetTouchableHandle()->GetVolume()->GetLogicalVolume()};
        if (vol == fScoringVolume) {
            fEventAction->AddEdep(step->GetTotalEnergyDeposit());
        }
    }

private:
    EventAction* fEventAction{nullptr};
    G4LogicalVolume* fScoringVolume{nullptr};
};

// ============================================================================
// ActionInitialization
// ============================================================================
class ActionInitialization : public G4VUserActionInitialization {
public:
    ActionInitialization(const G4ThreeVector& direction,
                         const std::string& particleName,
                         double energy,
                         double threshold) :
        fDirection{direction},
        fParticleName{particleName},
        fEnergy{energy},
        fThreshold{threshold} {}

    auto BuildForMaster() const -> void override {
        SetUserAction(new RunAction);
    }

    auto Build() const -> void override {
        auto runAction{new RunAction};
        auto evtAction(new EventAction{runAction, fThreshold});
        SetUserAction(new PrimaryGeneratorAction{fDirection, fParticleName, fEnergy});
        SetUserAction(runAction);
        SetUserAction(evtAction);
        SetUserAction(new SteppingAction{evtAction});
    }

private:
    G4ThreeVector fDirection;
    std::string fParticleName;
    double fEnergy;
    double fThreshold;
};

// ============================================================================
// main
// ============================================================================
auto main(int argc, char** argv) -> int {
    // --- Require exactly 6 positional arguments ---
    if (argc != 7) {
        G4cout << "Usage: " << argv[0]
               << " nEvents x y z particleName energy(MeV)" << G4endl
               << G4endl
               << "  nEvents         Number of events" << G4endl
               << "  x y z           Box position in cm" << G4endl
               << "  particleName    Incident particle (e.g. gamma, e-, proton)" << G4endl
               << "  energy          Incident kinetic energy in MeV" << G4endl
               << G4endl
               << "Example: " << argv[0] << " 100000 0 50 0 gamma 60" << G4endl;
        return 1;
    }

    // --- Parse arguments in strict order ---
    int nEvents{std::atoi(argv[1])};
    G4ThreeVector boxPos{std::atof(argv[2]) * cm,
                         std::atof(argv[3]) * cm,
                         std::atof(argv[4]) * cm};
    std::string particleName{argv[5]};
    double energy{std::atof(argv[6]) * MeV};

    if (nEvents <= 0) {
        G4cout << "Error: nEvents must be > 0" << G4endl;
        return 1;
    }

    auto direction{boxPos.unit()}; // from origin to box

    // --- Run manager (MT) ---
    G4MTRunManager runManager;

    // --- Detector ---
    runManager.SetUserInitialization(new DetectorConstruction{boxPos});

    // --- Physics: EM only ---
    auto physList{new G4VModularPhysicsList{}};
    physList->RegisterPhysics(new G4EmStandardPhysics_option4{});
    G4EmParameters::Instance()->SetMscStepLimitType(fUseSafetyPlus);
    G4EmParameters::Instance()->SetMscMuHadStepLimitType(fUseSafetyPlus);
    runManager.SetUserInitialization(physList);

    // --- User actions ---
    auto threshold{(1 + FLT_EPSILON) * (energy + 2 * electron_mass_c2)};
    runManager.SetUserInitialization(new ActionInitialization{direction, particleName, energy, threshold});

    // --- Initialize ---
    runManager.SetNumberOfThreads(G4Threading::G4GetNumberOfCores());
    runManager.Initialize();

    // --- Batch run ---
    G4cout << G4endl
           << "============================================================" << G4endl
           << "  Starting " << nEvents << " events with G4EmStandardPhysics_option4" << G4endl
           << "  Primary: " << particleName
           << ", E_kin = " << energy / MeV << " MeV"
           << ", E_total = " << threshold / MeV << " MeV" << G4endl
           << "  Anomaly threshold: Edep > " << threshold / MeV << " MeV" << G4endl
           << "  Target: G4_CESIUM_IODIDE box 10*20*10 cm3 (G4Box)" << G4endl
           << "  Box position: (" << boxPos.x() / cm << ", " << boxPos.y() / cm << ", " << boxPos.z() / cm << ") cm" << G4endl
           << "  Photon direction: (" << direction.x() << ", " << direction.y() << ", " << direction.z() << ")" << G4endl
           << "  ROOT file: opt4bug_edep.root" << G4endl
           << "============================================================" << G4endl
           << G4endl;

    runManager.BeamOn(nEvents);

    return 0;
}

Hello Shihan,

This looks like a real geant4 bug in opt4… Could you please open a ticket in the Geant4 bugzilla ?

Thanks!

Hello,

Thanks for the reply. I will report this to Geant4 bugzilla.

I have reported this in 2737 – EM Opt4 produces unphysically high energy depositions

Hello @Zhao_Shihan thanks a lot for the detailed report and for the findings. I think it is a good idea to open a Bugzilla report, as this matter definitely deserves further investigation. Firstly, I will try to reproduce it using your code. I am curious to see a /tracking/verbose 1 dump of one of these unphysical events, so to sort out where the extra energy is coming from.

The most puzzling part to me is the direction-dependence, as I expect all models within Opt4 to be direction-agnostic. It could be that this effect comes from an interplay between physics and tracking.

We’ll followup on the Bugzilla, thanks a lot,

Luciano

Hi @Zhao_Shihan

Thank you very much for your detailed explanation and for providing a simple reproducer.

I have just tested it with Geant4 version 11.4 patch 2 (released yesterday), and it does not produce any anomalous events. This patch includes several fixes for EM physics. Below are the results for the different versions:

Version 11.4 patch 02

============================================================
  Run Summary (Master)  —  1000000 events
============================================================
  Anomalous events:      0  (0.000%)
  ROOT output:           opt4bug_edep.root
============================================================

Version 11.4 patch 01

============================================================
  Run Summary (Master)  —  1000000 events
============================================================
  Anomalous events:      381  (0.038%)
  ROOT output:           opt4bug_edep.root
============================================================

Thank you for your time and effort, it is greatly appreciated!

Best,

Alvaro