Resources for stop detection, initial-energy logging, no secondaries

Setup

World: Air. Detector: Water cylinder placed in air.

Beam Source: GPS muons (0-25 MeV) aimed at the cylinder.

Goal: Count only primary muons that stop inside the water; also record their initial kinetic energy at generation. I don’t need any secondaries or their info.

Request:

· Pointers to minimal examples/docs/any resources that show:

1. A reliable logic to detect stopped particles in a specific logical volume

2. A simple pattern to link initial KE at birth to a later stop condition (e.g., using per-track info)

Draft code so far (SteppingAction.cc):

```

#include “MargaritaSteppingAction.hh”

#include “RunAction.hh”

#include “G4Step.hh”

#include “G4Track.hh”

#include “G4StepPoint.hh”

#include “G4VPhysicalVolume.hh”

#include “G4ParticleDefinition.hh”

#include “G4SystemOfUnits.hh”

#include “G4AnalysisManager.hh”

MargaritaSteppingAction::MargaritaSteppingAction(RunAction* runAction)

: fRunAction(runAction) {}

MargaritaSteppingAction::~MargaritaSteppingAction() = default;

void MargaritaSteppingAction::UserSteppingAction(const G4Step* aStep)

{

// Handles

G4StepPoint\* pre  = aStep->GetPreStepPoint();

G4StepPoint\* post = aStep->GetPostStepPoint();

G4Track\*     trk  = aStep->GetTrack();



// Volume at post-step

G4VPhysicalVolume\* volPost =

    post->GetTouchableHandle() ? post->GetTouchableHandle()->GetVolume() : **nullptr**;

**if** (!volPost) **return**;



**const** G4String namePost = volPost->GetName();



// Target volume name

**static** **const** G4String kTargetVolumeName = "CylPV";



// Particle selection: mu- only (PDG 13)

**const** G4int pdg = trk->GetDefinition()->GetPDGEncoding();

**if** (pdg != 13) **return**;



// Primaries only

**if** (trk->GetParentID() != 0) **return**;



// Stop condition: inside target and KE \~ 0

**const** G4double eKinPost = post->GetKineticEnergy();

**const** G4double keEps    = 1.0\*eV;

**if** (namePost != kTargetVolumeName || eKinPost > keEps) **return**;



// Initial KE at creation (GPS)

**const** G4double eKinInit = trk->GetVertexKineticEnergy();



// Values at stop

**const** G4ThreeVector pos    = post->GetPosition();

**const** G4ThreeVector momDir = trk->GetMomentumDirection();

**const** G4double x = pos.x(), y = pos.y(), z = pos.z();

**const** G4double costh = momDir.cosTheta();



// ---- Fill histograms/ntuple only ----

**static** **const** G4int kH1_KE_Stop_Id   = 5; // KE at stop

**static** **const** G4int kH1_Z_Stop_Id    = 6; // Z at stop

**static** **const** G4int kH2_XY_Stop_Id   = 6; // XY at stop (H2 id space)

**static** **const** G4int kH1_CosTh_Id     = 7; // cos(theta)

**static** **const** G4int kH1_InitKE_Id    = 8; // initial KE



**auto** am = G4AnalysisManager::Instance();

am->FillH1(kH1_KE_Stop_Id, eKinPost);

am->FillH1(kH1_Z_Stop_Id,  z);

am->FillH2(kH2_XY_Stop_Id, x, y);

am->FillH1(kH1_CosTh_Id,   costh);

am->FillH1(kH1_InitKE_Id,  eKinInit);

**const** G4int nt = 1; // e.g., ntuple id for "stopped muons"

am->FillNtupleDColumn(nt, 0, eKinPost);

am->FillNtupleDColumn(nt, 1, x);

am->FillNtupleDColumn(nt, 2, y);

am->FillNtupleDColumn(nt, 3, z);

am->FillNtupleDColumn(nt, 4, momDir.x());

am->FillNtupleDColumn(nt, 5, momDir.y());

am->FillNtupleDColumn(nt, 6, momDir.z());

am->FillNtupleIColumn(nt, 7, pdg);

am->FillNtupleDColumn(nt, 8, eKinInit);

am->AddNtupleRow(nt);



// Ensuring no double counting from at-rest processing

trk->SetTrackStatus(fStopAndKill);

}

```

Geant4 Version: geant4-v11.3.2
Operating System: MacOS
Compiler/Version: Clang 17.0.0
CMake Version: Cmake 4.0.3


If you only have a specific target volume, an SD is easier: attach it to the target LV, and then Geant4 takes care of the “did it hit the volume, is it entering the volume” logic automatically so you don’t have to. Within the SD, test the track for the stopped state (track->GetTrackStatus() == fStopAndKill || track->GetTrackStatus() == fStopButAlive()).

The track’s initial kinetic energy is given to you by track->GetVertexKineticEnergy(), which you can call during any step. You don’t need to try to correlate different calls yourself.

Thanks for the suggestion. For context, I’m integrating this into a larger beamline project later, so I’m sticking with a stepping-action pattern that matches the rest of the detectors; it keeps integration simpler and avoids introducing a separate SD code path. I did consider attaching an SD (which is cleaner for a single volume), but I held off to keep the implementation consistent across the beamline.

Pretty sure the cleanest way to do this is in the tracking action.

>PreUserTrackingAction

  1. Check if particle is a muon. If not, kill it

>PostUserTrackingAction

  1. Check the current volume the particle is in. If inside water volume, record initial KE as described by @mkelsey i.e. track->GetVertexKineticEnergy(). Save to an ntuple. Counting is redundant since the length of the ntuple will give you that anyway.
  2. Grab all the secondaries of the particle and kill them all
  3. You can also check the particle ID to make sure its still a primary

RE01 is the cleanest example here. Both PreUserTrackingAction and PostUserTrackingAction are called once and only once so the tuple will not repeat entries. You could also record the event number to be sure. PreUserTrackingAction is when the particle is “born” (either from a reaction or a particle gun) and Post when the particle “dies” either from being so slow its at rest processes triggered or leaves the world volume (so make sure you catch that exception).

Since this will kill all the secondaries before they are even processed on the stack, this should be very efficient