Dynamically load detector geometry in visualization mode

Geant4 Version: 11.3.2
Operating System: Ubuntu 24.04 LTS (Noble Numbat)
Compiler/Version: g++ (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0
CMake Version: 3.28.3


Hi :slight_smile:
I’m wondering if it’s possible to dynamically load a geometry in the visualization tool, what I’m currently doing is building a layered detector by using a Custom G4UImessenger

Basically I add layer by layer in a macro with a command similar to:

/detector/addLayer name_of_the_layer size_x size_y size_z material_name is_active_layer

Currently, if I start my simulation in visualization mode and I try to open a .mac file containing the geometry definition, I get a segmentation fault!

This are my visualization scripts:

init_vis.mac

# Macro file for the initialization in interactive session

# Set some default verbose
/control/verbose 2
/control/saveHistory
/run/verbose 2

# Initialize kernel
/run/initialize

# Visualization setting
/control/execute macros/vis.mac

And vis.mac


# Specify a viewer, e.g., /vis/open OGL, or allow a system choice:
/vis/open OGL

# Disable auto refresh and quieten vis messages whilst scene and trajectories are established:
/vis/viewer/set/autoRefresh false
/vis/verbose errors

# Draw geometry:
/vis/drawVolume

# Specify view angle:
/vis/viewer/set/viewpointVector -1 0 0
/vis/viewer/set/lightsVector -1 0 0

# Specify style (surface, wireframe, auxiliary edges,...)
/vis/viewer/set/style wireframe
/vis/viewer/set/auxiliaryEdge true
/vis/viewer/set/lineSegmentsPerCircle 100

# Draw smooth trajectories at end of event, showing trajectory points
# as markers 2 pixels wide:
/vis/scene/add/trajectories smooth
/vis/modeling/trajectories/create/drawByCharge
/vis/modeling/trajectories/drawByCharge-0/default/setDrawStepPts true
/vis/modeling/trajectories/drawByCharge-0/default/setStepPtsSize 2
# (if too many tracks cause core dump => /tracking/storeTrajectory 0)

# Draw hits at end of event:
/vis/scene/add/hits

/vis/scene/endOfEventAction accumulate

# Axes, scale, etc.
/vis/scene/add/scale   # Simple scale line
/vis/scene/add/axes    # Simple axes: x=red, y=green, z=blue.
/vis/scene/add/eventID # Drawn at end of event
/vis/scene/add/date    # Date stamp
/vis/scene/add/logo2D  # Simple logo
/vis/scene/add/logo    # 3D logo

/vis/viewer/set/viewpointThetaPhi 120 150

# Re-establish auto refreshing and verbosity:
/vis/viewer/set/autoRefresh true
/vis/verbose warnings

In my message, I also have a command to update geometry (once that I have added all the layers, and it can be used in case of multiple runManager->Initialize(); calls in the same macro) that calls this function in the Geometry construction class:

void LayeredDetectorConstruction::UpdateGeometry()
{
    G4RunManager::GetRunManager()->ReinitializeGeometry(true);
}

Do you have any suggestions, or do you see something wrong in my approach?

Thanks in advance,
Michael

Build your geometry first in a macro. Then, once the geometry is in place, run vis.mac afterward.

1 Like

Hi thanks,
I tried to call the macro for the geometry (with just the line for defining the detector layout) in the init script, shown above, before the line /run/initialize command. But it was not shown in the visualization

Thanks in advance for the help :grinning_face_with_smiling_eyes:

Hi Michael

Have a look at How to modify a geometry using messengers in interactive mode - #12 by allison, in particular my post of 10 May.

Let us know if that helps.

John

Hi @allison

Sorry for the late reply – I’ve been busy with other topics. First of all, thank you!

What is still not clear to me is that I’m currently using ReinitializeGeometry(true), but the visualizer gets killed. I will attach here my constructor for reference:

LayeredDetectorConstruction.h

#ifndef LAYEREDDETECTORCONSTRUCTION_H
#define LAYEREDDETECTORCONSTRUCTION_H

#include "G4VUserDetectorConstruction.hh"
#include "G4ThreeVector.hh"
#include "globals.hh"

#include <vector>
#include <memory>

class G4VPhysicalVolume;
class G4LogicalVolume;
class UserMaterials;
class ActiveAreaSD;
class LayeredDetectorMessenger;

/// Simple value type describing a detector layer.
struct LayerConfig
{
    G4String name;
    G4ThreeVector size{0, 0, 0}; // full size (X, Y, Z)
    G4String material;           // material name (resolved via UserMaterials)
    G4bool isSensitive{false};

    LayerConfig() = default;
    LayerConfig(const G4String &n, const G4ThreeVector &s, const G4String &m, G4bool sens)
        : name(n), size(s), material(m), isSensitive(sens) {}
};

/// Builds a stack of rectangular layers inside a bounding “casing” with vacuum world.
/// - World is auto-sized to fit the stack plus margins.
/// - A vacuum bounding box hosts the placed layers.
/// - An aluminum casing surrounds the bounding box (open on top).
class LayeredDetectorConstruction : public G4VUserDetectorConstruction
{
public:
    LayeredDetectorConstruction();
    ~LayeredDetectorConstruction() override;

    G4VPhysicalVolume *Construct() override;
    void ConstructSDandField() override;

    // Configuration API
    void AddLayer(const LayerConfig &layer);
    void ClearLayers();
    const std::vector<LayerConfig> &GetLayers() const noexcept { return fLayers; }

    /// Position of the TOP SURFACE of the first layer, in world coordinates.
    void SetTopPosition(const G4ThreeVector &pos) noexcept { fTopPosition = pos; }

    /// Rebuild geometry after changes (safe to call from UI/messenger).
    void UpdateGeometry();

private:
    // Debug helpers
    void DebugInfoLogicalVolume(G4LogicalVolume *lv) const;
    void PrintAllLogicalVolumes() const;

private:
    // Non-owning: Geant4 owns created solids/volumes; user owns the materials registry.
    std::unique_ptr<UserMaterials> fUserMaterials;        // thin wrapper around G4 registries
    std::unique_ptr<LayeredDetectorMessenger> fMessenger; // UI command handler

    // Geometry description
    G4ThreeVector fTopPosition{0, 0, 0};
    std::vector<LayerConfig> fLayers;

    // Built artifacts (non-owning)
    G4LogicalVolume *fBoundingLogic{nullptr};
    std::vector<G4LogicalVolume *> fLayerLVs; // layer logicals in placement order
};

#endif

LayeredDetectorConstruction.cpp

#include "detector_construction/LayeredDetectorConstruction.h"
#include "detector_construction/LayeredDetectorMessenger.h"

#include "UserMaterials.h"
#include "ActiveAreaSD.h"
#include "Names.h"

#include "RunMetadata.h"

#include "G4Box.hh"
#include "G4SubtractionSolid.hh"
#include "G4LogicalVolume.hh"
#include "G4PVPlacement.hh"
#include "G4LogicalVolumeStore.hh"
#include "G4PhysicalVolumeStore.hh"
#include "G4NistManager.hh"
#include "G4SDManager.hh"
#include "G4RunManager.hh"
#include "G4UnitsTable.hh"
#include "G4SystemOfUnits.hh"
#include "G4Threading.hh"

#include <algorithm>
#include <utility>

namespace
{
    // Utility: validate strictly positive dimensions
    inline bool HasPositiveSize(const G4ThreeVector &full)
    {
        return (full.x() > 0) && (full.y() > 0) && (full.z() > 0);
    }
}

LayeredDetectorConstruction::LayeredDetectorConstruction()
    : fUserMaterials(std::make_unique<UserMaterials>()),
      fMessenger(std::make_unique<LayeredDetectorMessenger>(this))
{
    // Default top position can be left at (0,0,0) and set via macro.
    AddLayer(LayerConfig("default_layer1", G4ThreeVector(5. * mm, 5. * mm, 1. * mm), "SiliconCarbide", true));
    AddLayer(LayerConfig("default_layer2", G4ThreeVector(5. * mm, 5. * mm, 1. * mm), "SiliconCarbide", true));
}

LayeredDetectorConstruction::~LayeredDetectorConstruction() = default;

void LayeredDetectorConstruction::AddLayer(const LayerConfig &layer)
{
    // Ignore degenerate layers
    if (!HasPositiveSize(layer.size))
    {
        G4cerr << "[LayeredDetectorConstruction] Ignoring layer '" << layer.name
               << "' with non-positive size." << G4endl;
        return;
    }
    fLayers.push_back(layer);
}

void LayeredDetectorConstruction::ClearLayers()
{
    fLayers.clear();
    fLayerLVs.clear();
}

G4VPhysicalVolume *LayeredDetectorConstruction::Construct()
{
    if (fLayers.empty())
    {
        G4Exception("LayeredDetectorConstruction::Construct",
                    "GEOM001", FatalException,
                    "No layers defined. Use macro commands to add layers.");
    }

    // --- Compute total envelope needed by the stacked layers
    G4double totalZ = 0.;
    G4double maxX = 0.;
    G4double maxY = 0.;
    for (const auto &l : fLayers)
    {
        totalZ += l.size.z();
        maxX = std::max(maxX, l.size.x());
        maxY = std::max(maxY, l.size.y());
    }

    // Margins around the overall assembly
    const G4double margin = 1.0 * cm;

    // World half-dimensions: add margin and absolute top position components
    const G4double worldDimX = std::abs(fTopPosition.x()) + maxX + margin;
    const G4double worldDimY = std::abs(fTopPosition.y()) + maxY + margin;
    const G4double worldDimZ = std::abs(fTopPosition.z()) + totalZ + margin;

    // --- World (vacuum)
    auto *solidWorld = new G4Box("solidWorld", worldDimX, worldDimY, worldDimZ);
    auto *logicWorld = new G4LogicalVolume(solidWorld, fUserMaterials->GetVacuum(), "logicWorld");
    auto *physWorld = new G4PVPlacement(
        nullptr, {}, logicWorld, "physWorld", nullptr, false, 0, true);

    // --- Bounding box that hosts the layers (vacuum)
    const G4ThreeVector boundingSize(maxX, maxY, totalZ);
    const G4ThreeVector boundingCenter(fTopPosition.x(),
                                       fTopPosition.y(),
                                       fTopPosition.z() + totalZ / 2.0);

    auto *boundingSolid = new G4Box("detectorBoundingBox_solid",
                                    boundingSize.x() / 2.0,
                                    boundingSize.y() / 2.0,
                                    boundingSize.z() / 2.0);

    fBoundingLogic = new G4LogicalVolume(
        boundingSolid, fUserMaterials->GetVacuum(), "detectorBoundingBox_logic");

    // Use a large but unique copy number (998) for ease of debugging
    new G4PVPlacement(
        nullptr, boundingCenter, fBoundingLogic,
        "detectorBoundingBox_phys", logicWorld, false, 998, true);

    // --- Aluminum casing (open on top)
    const G4double casingThickness = 1.0 * mm;
    const G4ThreeVector casingOuterSize =
        boundingSize + G4ThreeVector(2 * casingThickness, 2 * casingThickness, casingThickness);

    auto *solidCasingOuter = new G4Box("casing_outer",
                                       casingOuterSize.x() / 2.0,
                                       casingOuterSize.y() / 2.0,
                                       casingOuterSize.z() / 2.0);

    auto *solidCasingInner = new G4Box("casing_inner",
                                       boundingSize.x() / 2.0,
                                       boundingSize.y() / 2.0,
                                       boundingSize.z() / 2.0);

    // Offset inner cavity upward so the top remains open
    const G4ThreeVector cavityOffset(0, 0, (casingOuterSize.z() - boundingSize.z()) / 2.0);

    auto *solidCasing = new G4SubtractionSolid("casing_solid",
                                               solidCasingOuter,
                                               solidCasingInner,
                                               nullptr,
                                               cavityOffset);

    auto *logicCasing = new G4LogicalVolume(
        solidCasing, fUserMaterials->GetAluminum(), "detectorCasing_logic");

    const G4ThreeVector casingCenter = boundingCenter + G4ThreeVector(0, 0, casingThickness / 2.0);

    // Unique copy number (999) for casing
    new G4PVPlacement(
        nullptr, casingCenter, logicCasing,
        "detectorCasing_phys", logicWorld, false, 999, true);

    // --- Record high-level metadata (world + detector top position)
    {
        auto meta = RunMetadata::Instance();
        meta->SetWorldSize(G4ThreeVector(worldDimX * 2, worldDimY * 2, worldDimZ * 2)); // convert half-size -> full size
        meta->SetDetectorPosition(fTopPosition);
        meta->ClearLayers();
    }

    // --- Create layer logical volumes and place them into the bounding box
    fLayerLVs.clear();
    fLayerLVs.reserve(fLayers.size());

    G4double currentZ = fTopPosition.z();

    for (size_t i = 0; i < fLayers.size(); ++i)
    {
        const auto &layer = fLayers[i];

        // Solid / logical
        auto *solid = new G4Box(layer.name + "_solid",
                                layer.size.x() / 2, layer.size.y() / 2, layer.size.z() / 2);

        auto *material = fUserMaterials->GetMaterialByName(layer.material);
        if (!material)
        {
            G4Exception("LayeredDetectorConstruction::Construct",
                        "MAT001", FatalException,
                        ("Unknown material: " + layer.material).c_str());
        }

        auto *logic = new G4LogicalVolume(solid, material, layer.name + "_logic");

        // Compute placement: increment by half thickness, place, then increment again
        currentZ += layer.size.z() / 2.0;
        const G4ThreeVector globalPos(fTopPosition.x(), fTopPosition.y(), currentZ);
        const G4ThreeVector localPos = globalPos - boundingCenter;

        // Copy numbers: 1..N for layers (avoid 0 and 998–999 already used)
        new G4PVPlacement(nullptr, localPos, logic,
                          layer.name + "_phys", fBoundingLogic, false,
                          static_cast<G4int>(i + 1), true);

        fLayerLVs.push_back(logic);
        currentZ += layer.size.z() / 2.0;

        // Metadata per layer
        RunMetadata::LayerInfo li;
        li.name = layer.name;
        li.size = layer.size;
        li.position = globalPos; // absolute position of the layer center
        li.material = layer.material;
        li.isActive = layer.isSensitive;
        RunMetadata::Instance()->AddLayer(li);

        DebugInfoLogicalVolume(logic);
    }

    return physWorld;
}

void LayeredDetectorConstruction::ConstructSDandField()
{
    auto *sdManager = G4SDManager::GetSDMpointer();

    // Create a unique SD name per thread to avoid collisions in MT
    const G4String uniqueName =
        Names::activeAreaSensitiveDetector + "_thread" + std::to_string(G4Threading::G4GetThreadId());

    if (sdManager->FindSensitiveDetector(uniqueName, false))
    {
        // Already created for this thread
        return;
    }

    auto *sd = new ActiveAreaSD(uniqueName);
    sdManager->AddNewDetector(sd);

    // Register active/inactive layers, attach SD to each logical
    for (size_t i = 0; i < fLayers.size(); ++i)
    {
        auto *lv = fLayerLVs[i];
        const auto isActive = fLayers[i].isSensitive;

        sd->RegisterActiveLayer(lv->GetName(), isActive);
        SetSensitiveDetector(lv, sd); // attaches even if inactive → SD can gate internally
    }

    PrintAllLogicalVolumes();
}

void LayeredDetectorConstruction::DebugInfoLogicalVolume(G4LogicalVolume *lv) const
{
    if (!lv)
        return;

    G4cout << "[LayeredDetectorConstruction] LV: " << lv->GetName() << G4endl;

    if (auto *solid = dynamic_cast<G4Box *>(lv->GetSolid()))
    {
        const G4ThreeVector fullSize(2 * solid->GetXHalfLength(),
                                     2 * solid->GetYHalfLength(),
                                     2 * solid->GetZHalfLength());
        G4cout << "  Size: "
               << G4BestUnit(fullSize.x(), "Length") << " × "
               << G4BestUnit(fullSize.y(), "Length") << " × "
               << G4BestUnit(fullSize.z(), "Length") << G4endl;
    }
    if (auto *mat = lv->GetMaterial())
    {
        G4cout << "  Material: " << mat->GetName() << G4endl;
    }
}

void LayeredDetectorConstruction::UpdateGeometry()
{
    // Rebuild geometry; true → destroy old geometry before re-construct
    G4RunManager::GetRunManager()->ReinitializeGeometry(true);
}

void LayeredDetectorConstruction::PrintAllLogicalVolumes() const
{
    G4cout << "[Debug] List of Logical Volumes:" << G4endl;
    auto *store = G4LogicalVolumeStore::GetInstance();
    for (auto *lv : *store)
    {
        const auto *mat = lv->GetMaterial();
        G4cout << " - " << lv->GetName();
        if (mat)
            G4cout << " (" << mat->GetName() << ")";
        G4cout << G4endl;
    }
}

I’m currently starting the geometry (see constructor above) with 2 default layers, and everything works fine. Once the Qt window is started, I call the following sequence of macro commands:

  • Remove the default layers with ClearLayers().
  • Add the layers I want by loading a macro with a series of AddLayer() commands (3 to 5 layers).
  • Update the geometry and visualization with UpdateGeometry(), which calls ReinitializeGeometry(true).

As soon as I hit this last command, the Qt window closes and I get the following error:

# -----------------------------
# Detector
# -----------------------------
# Polyamide ()
/detector/addLayer kapton_layer 5. mm 5. mm 7. um Kapton false
# Aluminum (Al)
/detector/addLayer aluminum_layer 5 mm 5 mm 1. um Aluminum false
# Nickel Silicide (Ni2Si)
/detector/addLayer nickelsilicide_layer 5 mm 5 mm 0.1 um NickelSilicide false
# Silicon Carbide (SiC)
# p+
/detector/addLayer sic_p_plus 5 mm 5 mm 0.3 um SiliconCarbideDoped(Al_1e18) false
/detector/addLayer sic_active_layer 5 mm 5 mm 100. um SiliconCarbideDoped(N_8e13) true
/detector/addLayer sic_layer_back 5 mm 5 mm 149.7 um SiliconCarbideDoped(N_8e13) false
/detector/update
#### Assemblies, Volumes and Solids Stores are wiped out.
#### Region <DefaultRegionForParallelWorld> is cleared.
/run/reinitializeGeometry

-------- WWWW ------- G4Exception-START -------- WWWW -------
*** G4Exception : modeling0015
      issued by : G4PhysicalVolumeModel::Validate
Attempt to validate a volume that is no longer in the physical volume store.
*** This is just a warning message. ***
-------- WWWW -------- G4Exception-END --------- WWWW -------

WARNING: Model "G4PhysicalVolumeModel physWorld:0 BasePath: TOP" is no longer valid - being removed
  from scene "scene-1"
/vis/scene/notifyHandlers scene-1
terminate called after throwing an instance of 'std::bad_alloc'
  what():  std::bad_alloc
Aborted (core dumped)

I also include an expanded backtrace for debugging using gdb:

/control/execute /home/michael/Michael/geant4/NeutronSimulation/build/macros/detector/SiC0005_5mm_t250.mac
# -----------------------------
# Detector
# -----------------------------
# Polyamide ()
/detector/addLayer kapton_layer 5. mm 5. mm 7. um Kapton false
# Aluminum (Al)
/detector/addLayer aluminum_layer 5 mm 5 mm 1. um Aluminum false
# Nickel Silicide (Ni2Si)
/detector/addLayer nickelsilicide_layer 5 mm 5 mm 0.1 um NickelSilicide false
# Silicon Carbide (SiC)
# p+
/detector/addLayer sic_p_plus 5 mm 5 mm 0.3 um SiliconCarbideDoped(Al_1e18) false
/detector/addLayer sic_active_layer 5 mm 5 mm 100. um SiliconCarbideDoped(N_8e13) true
/detector/addLayer sic_layer_back 5 mm 5 mm 149.7 um SiliconCarbideDoped(N_8e13) false
/detector/update
#### Assemblies, Volumes and Solids Stores are wiped out.
#### Region <DefaultRegionForParallelWorld> is cleared.
/run/reinitializeGeometry

-------- WWWW ------- G4Exception-START -------- WWWW -------
*** G4Exception : modeling0015
      issued by : G4PhysicalVolumeModel::Validate
Attempt to validate a volume that is no longer in the physical volume store.
*** This is just a warning message. ***
-------- WWWW -------- G4Exception-END --------- WWWW -------

WARNING: Model "G4PhysicalVolumeModel physWorld:0 BasePath: TOP" is no longer valid - being removed
  from scene "scene-1"
/vis/scene/notifyHandlers scene-1
terminate called after throwing an instance of 'std::bad_alloc'
  what():  std::bad_alloc

Thread 1 "sim" received signal SIGABRT, Aborted.
Download failed: Invalid argument.  Continuing without source file ./nptl/./nptl/pthread_kill.c.
__pthread_kill_implementation (no_tid=0, signo=6, threadid=<optimized out>) at ./nptl/pthread_kill.c:44
warning: 44	./nptl/pthread_kill.c: No such file or directory
(gdb) bt
#0  __pthread_kill_implementation (no_tid=0, signo=6, threadid=<optimized out>) at ./nptl/pthread_kill.c:44
#1  __pthread_kill_internal (signo=6, threadid=<optimized out>) at ./nptl/pthread_kill.c:78
#2  __GI___pthread_kill (threadid=<optimized out>, signo=6, signo@entry=<error reading variable: Cannot access memory at address 0x7fffffffb7e8>) at ./nptl/pthread_kill.c:89
#3  0x00007ffff604527e in __GI_raise (sig=<error reading variable: Cannot access memory at address 0x7fffffffb7e8>, sig@entry=6) at ../sysdeps/posix/raise.c:26
#4  0x00007ffff60288ff in __GI_abort () at ./stdlib/abort.c:79
Backtrace stopped: Cannot access memory at address 0x7fffffffb8f8

Thanks a lot for your help, I really appreciate it! :slightly_smiling_face:
Best regards,
Michael

Hi Michael

In my post on the other thread, I say:

“If you want to destroy your geometry and reconstruct it, you can add an argument: ReinitializeGeometry(true). It will destroy the existing geometry, and the vis manager will remove the geometry model. Then it seems you have to /run/initialize and /vis/scene/add/volume to re-establish the geometry and the view.”

Alternatively, try adding

`G4RunManager::GetRunManager()->InitializeGeometry(true);`

and maybe

G4UImanager::GetUIpointer()->ApplyCommand("/vis/drawVolume");

after ReinitializeGeometry in UpdateGeometry.

If still problems, I’ll dig further. Strange that gdb didn’t give more information. It would be good to know where the crash happened.

John