KAHIBARO
Discord Login Register

25.5 Creating a Positron Source

Context in a PET Simulation

In a PET scanner, the source produces positron emitting radionuclides inside or near the detector ring. Your Geant4 positron source should mimic this as closely as needed for your study. The source configuration controls where positrons are born, with what energy spectrum, in which direction, and at what rate. In a basic PET example, you usually start with a simple geometry, such as a small cylindrical or spherical volume at the center of the detector ring, and emit positrons from there.

You can create the positron source either with a G4ParticleGun or with G4GeneralParticleSource (GPS). For a first implementation, a simple particle gun defined in PrimaryGeneratorAction is usually enough, then you can move to GPS if you need more realistic distributions or want to configure the source via macro commands.

Choosing the Positron Particle

The key step for a PET source is to select the correct Geant4 particle definition that represents a positron. Geant4 provides a built in particle for positrons, so you do not need to define any custom particle type.

Inside your PrimaryGeneratorAction constructor, or in an initialization method for the source, you first access the particle table and retrieve the positron definition:

cpp
#include "G4ParticleTable.hh"
#include "G4ParticleDefinition.hh"
#include "G4ParticleGun.hh"
#include "G4SystemOfUnits.hh"
PrimaryGeneratorAction::PrimaryGeneratorAction()
{
  G4int nParticle = 1;
  fParticleGun = new G4ParticleGun(nParticle);
  auto particleTable = G4ParticleTable::GetParticleTable();
  auto positron = particleTable->FindParticle("e+");
  fParticleGun->SetParticleDefinition(positron);
}

This sets the primary particle type to a positron. From this point, the physics list controls the positron interactions, such as energy loss and annihilation, which are central to PET imaging.

If you prefer to use GPS instead of a manual particle gun, you would select the positron via a macro command:

tcl
/gps/particle e+

The important point is that the PET source always emits e+, not gamma rays directly. The back to back annihilation photons are then produced by the physics processes when the positron slows down and annihilates in the surrounding material.

In a PET simulation, the primary particle should be the positron e+, not the 511 keV gammas, if you want to study effects of positron range and non collinearity. Using gammas as primaries skips the positron transport and changes the physics of the problem.

Setting Positron Energy

Real PET radionuclides emit positrons with a continuous beta plus energy spectrum. The endpoint energy depends on the isotope, for example about 0.63 MeV for F 18 and about 3.4 MeV for O 15. A full treatment would reproduce the full beta spectrum, but for an introductory example you can start with a monoenergetic or a simple distribution.

With a G4ParticleGun, you set a fixed kinetic energy:

cpp
fParticleGun->SetParticleEnergy(0.5*MeV);

This gives all primary positrons the same kinetic energy of 0.5 MeV. This already allows you to see positron transport, annihilation, and coincidence detection.

If you need a more realistic energy spectrum, there are two common beginner friendly approaches. The first approach uses GPS energy distribution commands in a macro, for example a simple uniform or Gaussian approximation to the beta spectrum:

tcl
/gps/particle e+
/gps/energytype Gauss
/gps/mono 0.63 MeV
/gps/sigmaE 0.1 MeV

The second approach is to sample the energy yourself in C++ in GeneratePrimaries and then call SetParticleEnergy() for each event. For instance, a very crude uniform spectrum between 0 and the endpoint energy could be done as:

cpp
void PrimaryGeneratorAction::GeneratePrimaries(G4Event* event)
{
  G4double eMax = 0.63*MeV;
  G4double rnd = G4UniformRand();
  G4double eKin = rnd * eMax;
  fParticleGun->SetParticleEnergy(eKin);
  fParticleGun->GeneratePrimaryVertex(event);
}

This is not a physically accurate beta spectrum, but it demonstrates how to vary the positron energy event by event and how the detector response changes with energy.

For most introductory PET examples, starting with a monoenergetic positron energy is acceptable, as long as you clearly understand that this simplifies the problem and is not realistic for a particular isotope.

Source Position and Spatial Distribution

In a simple PET tutorial, the positron source is usually placed at the center of the detector ring. This allows you to focus on coincidence detection and energy spectra before introducing more complex spatial distributions.

To place a point source at the center, set the gun position in your primary generator:

cpp
fParticleGun->SetParticlePosition(G4ThreeVector(0., 0., 0.));

If you defined a “phantom” or a patient volume for PET, the center of that volume is often taken as the source position. Make sure the units are consistent with your geometry and that the world volume is large enough to contain the source and detector ring.

To simulate a distributed activity, for example a small spherical or cylindrical source at the center, you can sample the emission point within the desired shape. A simple spherical source of radius R can be implemented as:

cpp
void PrimaryGeneratorAction::GeneratePrimaries(G4Event* event)
{
  G4double R = 5.0*mm;
  G4ThreeVector pos;
  do {
    G4double x = (2.*G4UniformRand() - 1.) * R;
    G4double y = (2.*G4UniformRand() - 1.) * R;
    G4double z = (2.*G4UniformRand() - 1.) * R;
    pos = G4ThreeVector(x,y,z);
  } while (pos.mag() > R);
  fParticleGun->SetParticlePosition(pos);
  fParticleGun->GeneratePrimaryVertex(event);
}

This gives a uniform activity distribution inside a sphere of radius 5 mm. For many PET studies, a small spherical source or a uniform line or disk can represent test objects used for system calibration and image quality evaluation.

If you use GPS, you can define the same idea via position commands in a macro, which you will connect to this PET example later when you configure more complex sources.

Emission Direction and Isotropy

In a clinical PET scanner, at the moment of emission, the positron direction is approximately isotropic in the rest frame of the parent nucleus. To reproduce this at the simulation level, you can set the primary direction to be random over the full solid angle.

With a G4ParticleGun, you can either use its built in isotropic option with GPS, or you sample a random unit vector in your primary generator. For example, a simple isotropic direction can be obtained by sampling azimuthal and polar angles:

cpp
void PrimaryGeneratorAction::GeneratePrimaries(G4Event* event)
{
  // Sample isotropic direction
  G4double cosTheta = 2.*G4UniformRand() - 1.;
  G4double sinTheta = std::sqrt(1. - cosTheta*cosTheta);
  G4double phi = 2.*CLHEP::pi*G4UniformRand();
  G4double ux = sinTheta*std::cos(phi);
  G4double uy = sinTheta*std::sin(phi);
  G4double uz = cosTheta;
  fParticleGun->SetParticleMomentumDirection(G4ThreeVector(ux,uy,uz));
  // Set position and energy as before
  // ...
  fParticleGun->GeneratePrimaryVertex(event);
}

If you use GPS with macro commands, you can get isotropic emission with:

tcl
/gps/ang/type iso

For some testing scenarios, you may prefer a fixed direction instead of isotropic emission, for example when you want to see how the detector responds to a beam like positron source aligned along a particular axis. In that case you simply use:

cpp
fParticleGun->SetParticleMomentumDirection(G4ThreeVector(0., 0., 1.));

However, for PET performance studies, the isotropic source is usually more appropriate because it better reflects clinical conditions.

Time Structure of the Positron Source

PET systems count annihilation photons continuously over an acquisition time. In a simple simulation you can ignore realistic timing and let Geant4 assign identical or trivial global times to all primary events. This is often enough when you focus only on spatial and energy distributions.

The global time of a primary is controlled by the primary vertex time. With a basic G4ParticleGun, if you do not set the time explicitly, Geant4 uses a default of zero. All events will then appear as happening at the same global time, which is fine for many simple studies but does not represent a counting rate.

If you want to emulate a constant activity over time, you can assign increasing times to primary vertices. For example, assume a fixed time increment $\Delta t$ between events:

cpp
void PrimaryGeneratorAction::GeneratePrimaries(G4Event* event)
{
  static G4double currentTime = 0.*ns;
  G4double deltaT = 100.*ns;  // time between events
  fParticleGun->SetParticleTime(currentTime);
  currentTime += deltaT;
  fParticleGun->GeneratePrimaryVertex(event);
}

You can interpret $\Delta t$ as related to event rate $R$ via $R = 1 / \Delta t$. This simple approach can already be used to test dead time, pile up, or coincidence window logic once you start evaluating timing information in your PET reconstruction.

For more realistic radioactive decay timing, Geant4 provides a radioactive decay module, but that belongs to a more advanced application. For an introductory PET example, manually controlling primary times is usually sufficient.

If you want to study time based effects such as coincidence timing, always make sure you explicitly set primary vertex times. Leaving all events at time zero removes the time dimension from your PET simulation and makes time of flight effects impossible to study.

Practical Implementation in PrimaryGeneratorAction

Putting all the basic elements together, a minimal yet PET relevant positron source using a particle gun could look like this:

cpp
#include "PrimaryGeneratorAction.hh"
#include "G4ParticleGun.hh"
#include "G4ParticleTable.hh"
#include "G4SystemOfUnits.hh"
#include "Randomize.hh"
PrimaryGeneratorAction::PrimaryGeneratorAction()
: G4VUserPrimaryGeneratorAction(),
  fParticleGun(nullptr)
{
  G4int nParticle = 1;
  fParticleGun = new G4ParticleGun(nParticle);
  auto particleTable = G4ParticleTable::GetParticleTable();
  auto positron = particleTable->FindParticle("e+");
  fParticleGun->SetParticleDefinition(positron);
  fParticleGun->SetParticleEnergy(0.5*MeV);
  fParticleGun->SetParticlePosition(G4ThreeVector(0.,0.,0.));
}
PrimaryGeneratorAction::~PrimaryGeneratorAction()
{
  delete fParticleGun;
}
void PrimaryGeneratorAction::GeneratePrimaries(G4Event* event)
{
  // Isotropic direction
  G4double cosTheta = 2.*G4UniformRand() - 1.;
  G4double sinTheta = std::sqrt(1. - cosTheta*cosTheta);
  G4double phi = 2.*CLHEP::pi*G4UniformRand();
  G4double ux = sinTheta*std::cos(phi);
  G4double uy = sinTheta*std::sin(phi);
  G4double uz = cosTheta;
  fParticleGun->SetParticleMomentumDirection(G4ThreeVector(ux,uy,uz));
  // Optional: set source time or position distribution here
  fParticleGun->GeneratePrimaryVertex(event);
}

This short class definition already provides:

A correct positron primary.
A simple but meaningful kinetic energy.
A central source position.
An isotropic emission pattern.

From here, you can extend the source in later parts of the PET example: move to GPS for flexible configuration, implement realistic energy spectra for particular radionuclides, or combine several spatial sources to mimic complex activity distributions.

In summary, creating a positron source in Geant4 for a PET simulation means choosing the e+ particle, assigning an appropriate energy distribution, defining the source location and spatial spread, setting a realistic emission direction pattern, and optionally introducing a time structure that matches the physics questions you want to study.

Views: 8

Comments

Please login to add a comment.

Don't have an account? Register now!