31.4. Avoiding Hard-Coded Values
Table of Contents
Why Hard-Coded Values Are a Problem
In a small test program, hard-coded numbers can seem harmless. In a Geant4 simulation, they quickly become a source of errors, confusion, and wasted time. A hard-coded value is any literal that encodes a physical quantity, configuration choice, or file path directly in the C++ code, such as:
auto box = new G4Box("box", 5.0*cm, 10.0*cm, 2.0*cm);If those dimensions represent a real detector, they are now buried inside your source file. When you later change the detector design, you must search and edit many lines manually, and you risk missing one. If you use the same number in multiple places, you might update some occurrences but not others, producing inconsistent geometry or analysis.
Hard-coded values also make it difficult to reuse code. A class that is written with specific numbers built in cannot easily be adapted to a slightly different detector layout, a new beam energy, or a different material. To create flexible and maintainable Geant4 applications, you should replace hard-coded numbers with clearly defined parameters that can be adjusted, shared, and documented.
Always avoid "magic numbers" in your Geant4 code. Define named parameters, use units, and keep physics or geometry choices in a small number of well-organized configuration points.
Using Constants and Parameters in C++
The simplest step away from hard-coded values is to define constants. Instead of repeating a literal everywhere, give it a name. In Geant4, you typically do this in header files for geometry, physics, or analysis configuration.
A good approach is to define constexpr constants or const variables with explicit units:
// DetectorDimensions.hh
#ifndef DetectorDimensions_hh
#define DetectorDimensions_hh
#include "G4SystemOfUnits.hh"
namespace DetectorDimensions {
constexpr G4double worldSizeXY = 2.0*m;
constexpr G4double worldSizeZ = 2.0*m;
constexpr G4double crystalThickness = 25.0*mm;
constexpr G4double crystalWidth = 50.0*mm;
constexpr G4double crystalHeight = 50.0*mm;
}
#endif
You then use these values inside your DetectorConstruction:
#include "DetectorDimensions.hh"
auto solidWorld = new G4Box("World",
DetectorDimensions::worldSizeXY/2,
DetectorDimensions::worldSizeXY/2,
DetectorDimensions::worldSizeZ/2);By centralizing the definitions, you get two immediate benefits. First, you change your design in one place and the entire code follows. Second, the names describe the meaning of each number, which documents the intent of your geometry.
The same idea applies to physics or analysis parameters, such as production cuts, energy thresholds, or histogram limits. Place them in a small number of configuration headers or classes instead of spreading literals across the code.
If a number has physical meaning or may ever need to change, give it a name and define it once, not many times.
Organizing Configuration in Classes
For more complex simulations, a single header with constants becomes hard to manage. It is often better to encapsulate configuration in one or more C++ classes that hold parameters and provide access methods. This makes the configuration part of your application architecture instead of a collection of scattered constants.
For example, you can create a simple configuration class:
// DetectorConfig.hh
#ifndef DetectorConfig_hh
#define DetectorConfig_hh
#include "G4SystemOfUnits.hh"
class DetectorConfig {
public:
DetectorConfig();
~DetectorConfig() = default;
G4double GetCrystalWidth() const { return fCrystalWidth; }
G4double GetCrystalHeight() const { return fCrystalHeight; }
G4double GetCrystalThickness() const { return fCrystalThickness; }
G4int GetNumberOfCrystals() const { return fNumberOfCrystals; }
private:
G4double fCrystalWidth;
G4double fCrystalHeight;
G4double fCrystalThickness;
G4int fNumberOfCrystals;
};
#endifWith an implementation:
// DetectorConfig.cc
#include "DetectorConfig.hh"
DetectorConfig::DetectorConfig()
: fCrystalWidth(50.0*mm),
fCrystalHeight(50.0*mm),
fCrystalThickness(25.0*mm),
fNumberOfCrystals(16)
{}
Your DetectorConstruction then receives a DetectorConfig object, either through its constructor or via a setter, and uses these values consistently. If you later decide to read parameters from a file or from macros, you can adjust the configuration class while leaving the detector building logic unchanged.
This structure has several advantages. Configuration and geometry code are separated. You can share the same configuration between geometry, primary generation, and analysis. It also becomes easier to test or compare different designs, because you can create multiple configuration objects with different parameter sets.
Centralizing Geometry and Physics Parameters
Hard-coded values are especially problematic when they are duplicated in several parts of the application. A common example is using the same dimension in geometry and analysis. Suppose you define detector thickness in the geometry file and then repeat the same number in the analysis code to decide which region to study. If you change the geometry and forget to update the analysis value, you silently introduce an error.
To avoid such problems, centralize parameters that are shared between components. A simple strategy is to collect all geometry-related parameters in a single place and include them wherever needed. For shared physics or run parameters, such as beam energy, run durations, or thresholds, use a separate configuration class or header.
The following table shows typical parameter types that should be centralized:
| Parameter type | Examples |
|---|---|
| Geometry dimensions | World size, detector sizes, gaps |
| Counts and indices | Number of crystals, layers, modules |
| Physics thresholds | Energy cuts, step limits, time windows |
| Source configuration | Particle type, beam energy, beam size |
| Analysis ranges | Histogram ranges, bin numbers, cuts |
By centralizing each category, you reduce the risk of inconsistent updates. When you change a detector layer thickness, you update one constant or configuration value. Every geometry placement and analysis calculation that depends on it will automatically use the new value.
Never copy-paste the same physical number into multiple files. Define it once, reuse it everywhere.
Using Macros and UI Commands for Configuration
C++ constants are useful, but changing them still requires recompilation. In Geant4, many parameters can and should be controlled at run time using UI commands and macro files, particularly for users who may not edit or compile the code themselves.
The typical pattern is to expose configuration through a "messenger" class. The messenger creates UI commands such as /detector/setThickness and forwards new values to your configuration or detector class. Then you can adjust parameters from a macro without touching the C++ source.
For example, you might add a setter in your configuration:
void DetectorConfig::SetCrystalThickness(G4double value) {
fCrystalThickness = value;
}The messenger connects a UI command to this setter. From a macro file, you can write:
#/control/verbose 1
/detector/setThickness 30 mm
/run/initialize
/run/beamOn 10000This approach removes the need to hard-code detector sizes, beam energies, or source positions in C++. Instead, you provide reasonable default values in the configuration class and give users macro commands to override them as needed.
In practice, you combine both ideas. Core geometry and physics logic use named parameters, and many of these parameters are modifiable at runtime through UI commands. This combination keeps the code readable and allows flexible configuration without recompiling.
Using External Configuration Files
For more advanced or long-lived projects, you may want to keep most configuration outside the code entirely. External configuration files can describe detector sizes, material choices, source properties, or run conditions. Common formats include plain text, JSON, XML, or simple key-value files.
A simple pattern is to parse a configuration file once at startup, populate your configuration classes, and then build the geometry and sources using those values. For example, you might have a text file:
crystal_width_mm = 50.0
crystal_height_mm = 50.0
crystal_thickness_mm = 25.0
number_of_crystals = 16
beam_energy_MeV = 1.0A small parser translates these lines into numbers and multiplies by the appropriate Geant4 units. This avoids repeating numeric literals and makes it easy to store different configurations for different simulation scenarios.
When using external files, you must be careful with units and validation. Each value should be clearly documented in the file, for example by including comments or explicit unit names. The code should check that parsed values are within reasonable ranges. If a value is missing or invalid, the application should report a clear message instead of silently creating an unphysical geometry.
External configuration is particularly useful for parameter scans and systematic studies. Instead of editing and recompiling, you can generate multiple configuration files or change macro-variable combinations and run many simulations in batch.
Avoiding Hidden Dependencies in Analysis
Hard-coded values often appear outside the geometry and physics list, especially in analysis code. A typical example is a histogram range or a selection cut that directly includes a number:
auto hE = analysisManager->CreateH1("Edep", "Energy deposition",
100, 0., 10. * MeV);If 10 MeV is the maximum expected energy from your detector, you have now encoded a physics assumption in the analysis. If your beam energy changes, but you do not adjust this limit, you may truncate your spectrum or misinterpret the results.
Instead, define analysis parameters in a shared place, similar to geometry parameters. You might introduce a small analysis configuration class or reuse an existing one:
namespace AnalysisConfig {
constexpr G4double eDepMax = 20.0 * MeV;
constexpr G4int eDepBins = 200;
}
Then create histograms using these constants. If you later change the beam energy, you adjust eDepMax and not every CreateH1 call.
The same applies to selection cuts, such as time windows or energy thresholds used to compute efficiencies. If your detector resolution or timing model changes, update a named parameter instead of searching for literals like 2.5*ns scattered through SteppingAction or EventAction.
Consistent Use of Units
Geant4 requires explicit units for every physical quantity. Hard-coded numbers without units are dangerous, because they are easy to misinterpret. A value of 10.0 could be a length, an energy, or a time, depending on context. To avoid confusion, always combine numerical values with Geant4 units and use named variables.
A good pattern is:
G4double beamEnergy = 1.0 * GeV;
fParticleGun->SetParticleEnergy(beamEnergy);
Avoid using pure numbers or values that depend on implied default units. If you use configuration files, ensure that unit conversion is explicit at the point where you read the file. For example, if a file lists beam_energy_GeV, multiply by GeV in C++.
By consistently attaching units to your parameters, you make the code easier to read and safer to modify. This also helps prevent subtle bugs where a value is accidentally interpreted in the wrong unit system.
Never use unitless literals for physical quantities in Geant4. Always multiply by the appropriate Geant4 unit and assign the result to a named variable.
Practical Patterns to Replace Hard-Coded Values
Several simple patterns help you systematically remove hard-coded values from a Geant4 project.
First, introduce a small "parameters" header early in the project. Whenever you find yourself writing a numeric value that has clear meaning, move it into the parameters header and replace the literal with the named constant. This habit keeps the code clean and makes future changes trivial.
Second, use constructor arguments instead of fixed values for reusable components. If you create a helper class for a detector module, let its constructor take dimensions or materials as arguments rather than fixing them inside the class. The calling code can then decide which values to use, possibly from a configuration object or macro commands.
Third, where appropriate, expose parameters through messengers so that users can control them at runtime. Begin with the most likely changes, such as detector distances, source energies, or cut values. Provide sensible defaults so the simulation runs without extra configuration, but allow overrides for more detailed studies.
Finally, document the meaning of every parameter. Even if you avoid hard-coded values, unclear or poorly named parameters can be just as confusing. Use self-explanatory names, comments, and, if needed, a short text file describing the configuration choices.
By systematically removing hard-coded numbers and replacing them with structured configuration, you create Geant4 applications that are easier to maintain, easier to share, and far less prone to subtle errors when the design evolves.
Views: 10
KAHIBARO