KAHIBARO
Discord Login Register

25.10. Recording Detector Positions

Why Detector Positions Matter in PET

In a PET simulation you do not only care that a detector fired, you also need to know where it is in space. Each hit must be associated with a detector element, and each detector element has a known position in the scanner ring. From these positions you later build lines of response and, eventually, an image.

In Geant4, the position information exists at two levels that you will typically combine:

  1. The geometrical position of each detector crystal, defined in your geometry (for example in DetectorConstruction).
  2. The interaction position from the simulation step, given by the tracking system when a gamma interacts in the crystal.

For most PET analysis you need both: the crystal index or ID and its nominal center position, and optionally the precise interaction point inside the crystal volume.

In PET reconstruction you must always know which detector element registered the hit and where that element is located in global coordinates. Never record only local positions without a way to convert them to a common global frame.

Geometry, Copy Numbers, and Coordinates

When you build a PET detector ring you typically define one or more volumes repeatedly placed around the patient. Common patterns are a ring of identical modules, each containing an array of crystals. Geant4 lets you distinguish these repeated copies through copy numbers and lets you access their positions in either local or global coordinates.

A PET crystal hit is located by:

Geant4 uses a right‑handed Cartesian coordinate system. You define detector placements with G4PVPlacement or parameterised volumes. Each placement has a transformation matrix that maps between the local coordinates of a volume and the global coordinates of the world.

The two positions you will most often use are:

When you talk about the detector position for PET analysis you normally mean the geometrical center of the crystal that fired, in global coordinates. This gives you a stable, reproducible position, independent of the particular interaction point in that crystal.

The position from G4StepPoint::GetPosition() is always in global coordinates. If you want an interaction position in the local coordinates of a crystal you must explicitly transform it yourself.

Storing Detector Positions in Hits

To record detector positions you first need a hits class that can store them. In a PET scanner example you will usually have a custom hit type, for example PetDetectorHit, that extends G4VHit. Along with energy, time, and detector ID, you add a member to store the detector position.

A minimal design might be:

cpp
class PetDetectorHit : public G4VHit {
public:
  PetDetectorHit() = default;
  ~PetDetectorHit() override = default;
  void SetDetectorID(G4int id)      { fDetID = id; }
  void SetPosition(const G4ThreeVector& p) { fPos = p; }
  void SetTime(G4double t)          { fTime = t; }
  void SetEnergy(G4double e)        { fEnergy = e; }
  G4int          GetDetectorID() const { return fDetID; }
  const G4ThreeVector& GetPosition() const { return fPos; }
  G4double       GetTime() const     { return fTime; }
  G4double       GetEnergy() const   { return fEnergy; }
private:
  G4int          fDetID = -1;
  G4ThreeVector  fPos;
  G4double       fTime   = 0.;
  G4double       fEnergy = 0.;
};

Within your sensitive detector, you create a new hit for each relevant energy deposition. You then fill the detector ID and position information in ProcessHits.

You have two natural choices for what to store in fPos:

For PET event building and lines of response, the crystal center is usually more convenient. You can still keep the interaction point separately if you need detailed studies of positron range or depth of interaction.

Always store positions using Geant4 units, for example mm. Do not convert to unit‑less values or external units inside your hits classes. Convert only when you write out or analyze the data.

Getting Crystal Positions from the Geometry

To obtain the nominal position of a detector crystal you need to:

  1. Identify which volume was hit.
  2. Get the touchable history for that step.
  3. Read the copy numbers for your ring and crystal indices.
  4. Compute the global position of the crystal center from the touchable transform.

The touchable encapsulates the full placement information from the world down to the current volume. You access it inside ProcessHits using the pre step point:

cpp
G4StepPoint* prePoint = step->GetPreStepPoint();
const G4TouchableHandle& touchable = prePoint->GetTouchableHandle();

You can now obtain copy numbers along the hierarchy. For example, if your geometry has a ring of modules and each module has an array of crystals, you could assign copy numbers like this during construction:

In ProcessHits you can recover them:

cpp
G4int moduleID  = touchable->GetCopyNumber(1);
G4int crystalID = touchable->GetCopyNumber(0);

The indices depend on how many levels you placed below the world, and you must keep your own convention consistent between geometry construction and hit processing.

To get the global position of the crystal center, use the translation part of the transform that places the current volume:

cpp
G4ThreeVector crystalCenter =
  touchable->GetHistory()
           ->GetTopTransform()
           .Inverse()
           .TransformPoint(G4ThreeVector(0., 0., 0.));

Here G4ThreeVector(0,0,0) is the local origin of the crystal volume. Applying the inverse of the top transform converts this local point to global coordinates. The result is the nominal center of the crystal in the world frame.

You then store this position into your hit:

cpp
auto hit = new PetDetectorHit;
hit->SetDetectorID(crystalID);      // or some combined ID that includes module
hit->SetPosition(crystalCenter);

If you also want the precise interaction position you can take it directly from the step:

cpp
G4ThreeVector interactionPos = prePoint->GetPosition(); // already global

You may store both positions in your hit, for instance as two separate members, if you intend to study effects such as depth of interaction.

The GetCopyNumber(level) indices count from the bottom of the geometry hierarchy, not from the world. Always check your volume tree if the IDs look wrong, and keep a clear mapping between copy numbers and your PET detector indices.

Recording Positions in Analysis Output

Once detector positions are stored in hits, you still need to write them to an output format for later analysis, for example a ROOT file. In a PET application you typically do this in EventAction or RunAction, after the tracking is done, by looping over the hit collections and filling an analysis object such as an ntuple.

First, declare suitable ntuple columns in your analysis setup, for example in RunAction:

cpp
auto analysisManager = G4AnalysisManager::Instance();
analysisManager->CreateNtuple("PET", "PET events");
analysisManager->CreateNtupleIColumn("eventID");
analysisManager->CreateNtupleIColumn("detID");
analysisManager->CreateNtupleDColumn("Edep");
analysisManager->CreateNtupleDColumn("time");
analysisManager->CreateNtupleDColumn("x");
analysisManager->CreateNtupleDColumn("y");
analysisManager->CreateNtupleDColumn("z");
analysisManager->FinishNtuple();

Then, at the end of each event, you retrieve the hit collection and fill one ntuple row per hit:

cpp
void EventAction::EndOfEventAction(const G4Event* event)
{
  auto hce = event->GetHCofThisEvent();
  if (!hce) return;
  auto hcID = fHitsCollectionID; // stored earlier from SD registration
  auto hitsCollection =
    static_cast<PetDetectorHitsCollection*>(hce->GetHC(hcID));
  if (!hitsCollection) return;
  auto analysisManager = G4AnalysisManager::Instance();
  G4int eventID = event->GetEventID();
  for (const auto hit : *hitsCollection) {
    G4ThreeVector pos = hit->GetPosition();
    analysisManager->FillNtupleIColumn(0, eventID);
    analysisManager->FillNtupleIColumn(1, hit->GetDetectorID());
    analysisManager->FillNtupleDColumn(2, hit->GetEnergy());
    analysisManager->FillNtupleDColumn(3, hit->GetTime());
    analysisManager->FillNtupleDColumn(4, pos.x() / mm);
    analysisManager->FillNtupleDColumn(5, pos.y() / mm);
    analysisManager->FillNtupleDColumn(6, pos.z() / mm);
    analysisManager->AddNtupleRow();
  }
}

Here the coordinates are converted to millimeters as plain numbers before writing. You can apply any convention you like, but you must be consistent with the units you use in downstream analysis.

For PET, this output provides everything you need to form lines of response: the event identifier, the detector ID, and the global position of the detector element that registered the hit.

Use a clear and consistent unit convention in your output files. Document which units you use for position, time, and energy, and keep them fixed across runs so that downstream PET analysis scripts do not silently misinterpret your data.

Views: 7

Comments

Please login to add a comment.

Don't have an account? Register now!