Defining Geant4.jl Geometries#

Currently we can define G4 geometries by constructing them using the API of the C++ classes or by reading a GDML file. This notebook will guide you on this task.

It is very convenient to be able to visualize the geometry on the way. For this purpose we need to load the G4Vis extension. This is done this way:

using Geant4
using Geant4.SystemOfUnits                # defines the units used by Geant4 (e.g. MeV, mm, ns, ...) 
using CairoMakie, Rotations, LinearAlgebra, IGLWrap_jll  # to force loading G4Vis extension

Defining Solids#

The first step is to define the solids. The following shapes are defined: G4Box, G4Cons, G4Tubs, G4Orb, G4Sphere, G4Trd, G4CutTubs, G4Para, G4Trap, G4Torus, G4Polycone, G4Polyhedra, G4EllipticalTube, G4Ellipsoid, G4EllipticalCone, G4Paraboloid, G4Hype, G4Tet, G4ExtrudedSolid, G4TwistedBox, G4TwistedTrap, G4TwistedTrd, G4GenericTrap, G4TwistedTubs. In addition, there are the boolean shapes: G4UnionSolid, G4SubtractionSolid, G4IntersectionSolid. The definition of the constructor parameters can be found in the documentation

Lets define a tube section like the following:

rmin = 10cm
rmax = 20cm
halfz = 10cm
ϕ₀ = 0
Δϕ = 60deg
tube = G4Tubs("tube", rmin, rmax, halfz, ϕ₀, Δϕ ) 
Geant4.G4TubsAllocated(Ptr{Nothing} @0x0000000004d91940)

We can easily draw the just constructed solid

img = draw(tube,  wireframe=true)
display("image/png", img)   # This is needed specially for notebooks
_images/56a56fd8e286d9b4c63910671bbb4fff25d6b1aa4db019dda17c1a6d44b1fb92.png
para = G4Para("parallelepiped", 30,40,60,0, π/4, π/6)
img = draw(para)
display("image/png", img)
_images/f0010de7b7421ecc4017c51970d620ac78fa78b0427051e19078f9094d4e69f4.png

Building Boolean solids#

We can build more complex geometries by combining with boolean operations simple shapes. For example building a 3D cross.

box = G4Box("box", 1, 1, 4)
t = G4Transform3D(G4RotationMatrix(0,π/2,0), G4ThreeVector())
cross = G4UnionSolid("cross", CxxPtr(box), CxxPtr(box), t)
img = draw(cross)
isnothing(img) || display("image/png", img)
_images/220741174f90ae1c429777ce56de917c13810bc220014abb5564e7d14d4c6222.png

Lets use some geometry functionality to experiment with the created solid. In this example we get the limits for the boolean solid and create a number if radom 3D points within the limits. Then select the ones that return kInside when calling the function Inside(solid, point). And finally, we plot the points with scatter function from Makie.

using StaticArrays
const Vector3 = SVector{3}
N = 100000
lo = G4ThreeVector()
hi = G4ThreeVector()
BoundingLimits(cross, lo, hi)                                  # get the bounding limits
dim = hi - lo                 
points = (lo + dim * rp for rp in rand(Vector3{Float64}, N))   # random points within the limits 
result = [Vector3{Float64}(x(p), y(p), z(p)) for p in points if Geant4.Inside(cross, p) == kInside]
img = scatter(result, color=:black, markersize=1)
display("image/png", img)
_images/c52a1858f37c5e563e613bb3c80a000eec2a0d956533e73230103df04ba702aa.png

Defining Materials#

In nature, materials (chemical compounds, mixtures) are made of elements, and elements are made of isotopes. Geant4 offers the 3 classes G4Isotope, G4Element and G4Materialto define from first principles any material. However is often that pre-defined materials are available in Geant4 from the NIST database of elements and isotope compositions.

This is the easiest way to get a material.

nist = G4NistManager!Instance()
m_air = FindOrBuildMaterial(nist, "G4_AIR")
@show GetDensity(m_air)/(g/cm3)

m_water = FindOrBuildMaterial(nist, "G4_WATER")
@show GetDensity(m_water)/(g/cm3)
GetDensity(m_air) / (g / cm3) = 0.0012047899999999999
GetDensity(m_water) / (g / cm3) = 1.0
1.0

or alternatively we can construct it from isotopes

U5 = G4Isotope("U235", z=92, n=235, a=235.01*g/mole)
U8 = G4Isotope("U238", z=92, n=238, a=238.03*g/mole)

U  = G4Element("Enriched Uranium", "U", 2)
AddIsotope(U, move!(U8), 0.90)       # Please notice the use of move!() to transfer ownership 
AddIsotope(U, move!(U5), 0.10)       # Please notice the use of move!() to transfer ownership 

m_eU =  G4Material("enriched U", density= 19.1*g/cm3, ncomponents=1)
AddElement(m_eU, move!(U), natoms=1) # Please notice the use of move!() to transfer ownership 

@show GetDensity(m_eU)/(g/cm3)
@show GetA(m_eU)/(g/mole)
GetDensity(m_eU) / (g / cm3) = 19.1
GetA(m_eU) / (g / mole) = 237.72800000000004
237.72800000000004

or build a molecule

O = FindOrBuildElement(nist, "O")
Si = FindOrBuildElement(nist, "Si")
Lu = FindOrBuildElement(nist, "Lu")
Lu₂SiO₅ = G4Material("Lu2SiO5", density=7.4*g/cm3, ncomponents=3)
AddElement(Lu₂SiO₅, Lu, natoms=2)  # no need to transfer ownership, we just got a pointer not constructed
AddElement(Lu₂SiO₅, Si, natoms=1)  
AddElement(Lu₂SiO₅, O, natoms=5)
@show GetRadlen(Lu₂SiO₅)/cm
GetRadlen(Lu₂SiO₅) / cm = 1.1427324172087883

1.1427324172087883

Defining Logical Volumes#

The Logical Volume manages the information associated with detector elements represented by a given Solid and Material, independently from its physical position in the detector.

To create a G4LogicalVolume just provide a solid and a material

cryst_dX = 6cm
cryst_dY = 6cm
cryst_dZ = 3cm
gap = 0.5mm 

dX = cryst_dX - gap
dY = cryst_dY - gap
solidCryst = G4Box("crystal", dX/2, dY/2, cryst_dZ/2)
logicCryst = G4LogicalVolume(solidCryst, CxxPtr(Lu₂SiO₅), "CrystalLV")
Geant4.G4LogicalVolumeAllocated(Ptr{Nothing} @0x0000000003964400)

We can also draw a logical volume

img = draw(logicCryst)
display("image/png", img)
_images/ae64a7c0c04b7aae6d76dba292ba6201d867e33b671637a0f661e05a9c1dd30a.png

Placing Logical Volumes#

Placing a logical volume (solid+material) inside a mother volume is done by creating a G4PhysicalVolume.

nb_cryst = 32
Δϕ = 2π/nb_cryst 
ring_R1 = 0.5 * cryst_dY/tan(Δϕ/2)
ring_R2 = (ring_R1+cryst_dZ)/cos(Δϕ/2)

# mother volume
solidRing = G4Tubs("Ring", ring_R1, ring_R2, cryst_dX/2, 0., 2π)
logicRing = G4LogicalVolume(solidRing, m_air, "Ring")

# placing the crystals
for icrys in 0:nb_cryst-1
    ϕ = icrys*Δϕ
    rotm  = G4RotationMatrix()
    rotateY(rotm, π/2)
    rotateZ(rotm, ϕ)
    position = (ring_R1 + cryst_dZ/2) * G4ThreeVector(cos(ϕ), sin(ϕ), 0)
    transform = G4Transform3D(rotm, position)

    G4PVPlacement(transform,             # rotation,position
                  logicCryst,            # its logical volume
                  "crystal",             # its name
                  logicRing,             # its mother  volume
                  false,                 # no boolean operation
                  icrys,                 # copy number
                  false)                 # checking overlaps
  end

A draw it

img = draw(logicRing)
display("image/png", img)
_images/98c6e765816aa52a8595a84268d38431a4684b015001d05ebe0985da43ec35ff.png

Building the full detector#

The detector construction is triggered by the G4RunManager at the right moment during the initialization steps. For this the user needs to provide a detector constructor function that will be called by the toolkit. The signature of the function is

function construct(det::<DetectorParameters>)::CxxPtr{G4VPhysicalVolume}
   ...
   return world
end

It receives an instance of a user defined structure with all the parameters of the detector and returns a ‘pointer’ to the ‘world’ physical volume. The user needs to declare the constructor function by defining a function specific to the Parameters structure like this:

Geant4.getConstructor(::<DetectorParameters>)::Function = construct

Here is a an example:

using Parameters   # we use the Parameters package to be able to define structure with keyword arguments with defaults

@with_kw mutable struct DetectorB3 <: G4JLDetector
    cryst_dX::Float64 = 6cm
    cryst_dY::Float64 = 6cm
    cryst_dZ::Float64 = 3cm
    nb_cryst::Int32 = 32
    nb_rings::Int32 = 9
    patient_radius::Float64 = 8cm
    patient_dZ::Float64 = 10cm
    checkOverlaps::Bool = false
end

detector = DetectorB3(nb_cryst = 64)
DetectorB3
  cryst_dX: Float64 60.0
  cryst_dY: Float64 60.0
  cryst_dZ: Float64 30.0
  nb_cryst: Int32 64
  nb_rings: Int32 9
  patient_radius: Float64 80.0
  patient_dZ: Float64 100.0
  checkOverlaps: Bool false

then we define the detector constructor function which includes the material definitions, and eventually the visualization attributes

function construct(det::DetectorB3)::G4VPhysicalVolume
    (; cryst_dX, cryst_dY, cryst_dZ, nb_cryst, nb_rings, patient_radius, checkOverlaps, patient_dZ) = det

    #---Derived parameters-------------------------------------------------------------------------
    dPhi = 2π/nb_cryst 
    half_dPhi = 0.5*dPhi
    cosdPhi = cos(half_dPhi)
    tandPhi = tan(half_dPhi)
    ring_R1 = 0.5*cryst_dY/tandPhi
    ring_R2 = (ring_R1+cryst_dZ)/cosdPhi
    detector_dZ = nb_rings*cryst_dX
    world_sizeXY = 2.4*ring_R2
    world_sizeZ  = 1.2*detector_dZ
  
    #---Materials----------------------------------------------------------------------------------
    nist = G4NistManager!Instance()
    default_mat = FindOrBuildMaterial(nist, "G4_AIR")
    cryst_mat   = FindOrBuildMaterial(nist, "Lu2SiO5")
    
    #---Volumes------------------------------------------------------------------------------------
    solidWorldS = G4Box("world", world_sizeXY/2, world_sizeXY/2, world_sizeZ/2)
    logicWorld  = G4LogicalVolume(solidWorldS, default_mat, "World")
    physWorld   = G4PVPlacement(nothing, 
                                G4ThreeVector(),                # at (0,0,0)
                                logicWorld,                     # its logical volume
                                "World",                        # its name
                                nothing,                        # its mother  volume
                                false,                          # no boolean operations
                                0,                              # copy number
                                checkOverlaps)                  # checking overlaps

    solidRing = G4Tubs("Ring", ring_R1, ring_R2, cryst_dX/2, 0., 2π)
    logicRing = G4LogicalVolume(solidRing, default_mat, "Ring")

    gap = 0.5mm 
    dX = cryst_dX - gap
    dY = cryst_dY - gap
    solidCryst = G4Box("crystal", dX/2, dY/2, cryst_dZ/2)
    logicCryst = G4LogicalVolume(solidCryst, cryst_mat, "CrystalLV")

    # place crystals within a ring
    for icrys in 0:nb_cryst-1
      ϕ = icrys*dPhi
      rotm  = G4RotationMatrix()
      rotateY(rotm, π/2)
      rotateZ(rotm, ϕ)
      position = (ring_R1 + cryst_dZ/2) * G4ThreeVector(cos(ϕ), sin(ϕ), 0)
      transform = G4Transform3D(rotm, position)
  
      G4PVPlacement(transform,             # rotation,position
                    logicCryst,            # its logical volume
                    "crystal",             # its name
                    logicRing,             # its mother  volume
                    false,                 # no boolean operation
                    icrys,                 # copy number
                    checkOverlaps)         # checking overlaps
    end
  
    # full detector
    solidDetector = G4Tubs("Detector", ring_R1, ring_R2, detector_dZ/2, 0., 2π)
    logicDetector = G4LogicalVolume(solidDetector, default_mat, "Detector")

    # place rings within detector
    OG = -0.5*(detector_dZ + cryst_dX)
    for iring in 0:nb_rings-1
      OG += cryst_dX
      G4PVPlacement(nothing,                  # no rotation
                    G4ThreeVector(0, 0, OG),  # position
                    logicRing,                # its logical volume
                    "ring",                   # its name
                    logicDetector,            # its mother  volume
                    false,                    # no boolean operation
                    iring,                    # copy number
                    checkOverlaps)            # checking overlaps
    end
    
    # place detector in world
    G4PVPlacement(nothing,                  # no rotation
                  G4ThreeVector(),          # at (0,0,0)
                  logicDetector,            # its logical volume
                  "Detector",               # its name
                  logicWorld,               # its mother  volume
                  false,                    # no boolean operation
                  0,                        # copy number
                  checkOverlaps)            # checking overlaps

    # patient
    patient_mat = FindOrBuildMaterial(nist, "G4_BRAIN_ICRP")
    solidPatient = G4Tubs("Patient", 0., patient_radius, patient_dZ/2, 0., 2π)
    logicPatient = G4LogicalVolume(solidPatient, patient_mat,"PatientLV")
    G4PVPlacement(nothing  ,                # no rotation
                  G4ThreeVector(),          # at (0,0,0)
                  logicPatient,             # its logical volume
                  "Patient",                # its name
                  logicWorld,               # its mother  volume
                  false,                    # no boolean operation
                  0,                        # copy number
                  checkOverlaps)            # checking overlaps
                

    #---Visualization attributes-------------------------------------------------------------------
    SetVisAttributes(logicWorld, G4VisAttributes!GetInvisible())
    SetVisAttributes(logicRing, G4VisAttributes!GetInvisible())
    SetVisAttributes(logicDetector, G4VisAttributes!GetInvisible())

    #---Always return the physical world-----------------------------------------------------------
    return physWorld
end
construct (generic function with 1 method)
world = construct(detector)
Geant4.G4PVPlacementAllocated(Ptr{Nothing} @0x0000000005866cb0)
img = draw(world)
display("image/png", img)
_images/e0314d522cbb305fba1df0d3ce75873385024dc3045762a46809204bbb93de5a.png