说实话,第一次在实验室的服务器上敲下cmake ..再执行make,我对着屏幕等编译等到怀疑人生。Geant4 这套东西,编译时间长、报错又抽象、概念术语还特别多,刚开始接触的人真的很容易被劝退。但只要熬过第一周,把它的设计思路捋清楚,后面写模拟程序会越来越顺手。
这篇文章写给所有打算入门 Geant4 的同学:我会从基本概念讲起,把工具包的结构、强制类、可选类这几个最让人头疼的名词一次说清楚,最后再给一个能直接跑通的迷你示例。如果你之前没接触过蒙特卡洛模拟,也完全没关系,我尽量用大白话解释,保证你看完能对 Geant4 有个整体框架式的理解。
1. 先把Geant4到底是什么搞清楚
1.1 名字背后的来头
Geant4 全称是GEometry ANd Tracking,直译过来就是“几何与追踪”。它来自欧洲核子研究中心(CERN)主导的跨国合作项目,脱胎于早期的 Fortran 程序 GEANT3,从 1994 年左右开始用 C++ 彻底重写,到如今已经成了高能物理、核技术、医学物理、辐射防护、空间科学等领域最主流的蒙特卡洛模拟工具之一。
我最早接触它是因为要做粒子探测器的响应模拟。那会儿我误以为它是个“装好就能用的仿真软件”,结果一打开发现连个图形界面都没有,只能写 C++ 代码,心态直接崩了。后来才明白,Geant4 根本不是一个“软件”,而是一套C++ 类库的工具包。你想让它干什么,就得自己写代码去调用、去组合这些类。
1.2 蒙特卡洛方法到底在算什么东西
Geant4 的核心计算思想是蒙特卡洛(Monte Carlo)方法。说白了,就是利用随机数模拟大量粒子的运动轨迹,再对这些轨迹的统计结果进行物理分析。
打个比方:你想知道一滴滴入清水中的墨水最后会扩散成什么样。理论上你可以去解扩散方程,但那很复杂。蒙特卡洛的思路是——我直接扔几百万个墨水滴进去,让每个水滴都随机游走,最后统计每个区域有多少水滴,就得到了浓度分布。粒子物理里的情况也类似:一个高能电子打进材料里,它可能在某个位置被原子散射,可能在某个位置损失能量产生次级粒子,整个过程本身就带有随机性,所以蒙特卡洛方法天然适合做这件事。
Geant4 做的事就是:告诉你每一步有哪些物理过程可能发生、概率多大,然后用随机抽样决定“走哪条路”。大量粒子跑完之后,你统计能量沉积、粒子通量、径迹长度等等,就得到了想要的模拟结果。
1.3 为什么强调“工具包”而不是“软件”
这是很多人入门时最大的认知误区。像一些商业仿真软件,装完打开,填参数点运行就能出结果。Geant4 不是这个路子,它只提供一堆半成品的类,比如“定义一个几何体”的类、“定义一种材料”的类、“给定一个粒子源”的类。你需要自己写一个 main 函数,在程序里把这些类组织起来,再交给你自己的 C++ 编译器编译成一个可执行文件。
网上搜“Geant4 工具包”的时候,经常会混进来一堆完全不相关的名字,什么 CTF 工具包、adb 工具包、SARscape 的 gacos 补丁包之类的,其实跟我们要学的 Geant4 八竿子打不着。这也侧面说明“工具包”这个词在不同领域含义差别挺大。在 Geant4 这儿,它指的就是一套供你组装和扩展的 C++ 类库,它的设计哲学是“框架在你手上,怎么组合由你定”。
2. 工具包整体结构:四大模块怎么分工
2.1 从几何、物理、粒子到追踪
Geant4 的功能可以拆成四大块,理解这四块之后,你再去看那些类名就不会晕了。
第一块是几何模块(Geometry)。它负责描述模拟环境长什么样,包括有什么材料、什么形状、怎么摆放。比如你要模拟一台 CT 机器,那就得在这个模块里定义 X 射线管、探测器晶体、准直器、甚至人体的各个组织器官。几何模块的核心问题是“粒子在哪儿”,所有和空间位置打交道的类都归它管。
第二块是物理模块(Physics)。它负责定义“粒子会跟物质发生哪些相互作用”,比如光电效应、康普顿散射、轫致辐射、电离能损等等。物理模块决定了粒子在每一步会经历什么过程,以及这些过程的概率是多少。这是 Geant4 最复杂、也最需要经验的一块,因为不同的物理列表(Physics List)专门适用不同的能量范围和物理场景。
第三块是粒子与追踪模块(Tracking)。粒子在这个模块里被发射出来,一步步地在几何中穿行,每走一小段都要判断有没有发生相互作用、有没有进入新材料、有没有飞出边界。追踪模块把一个连续的物理过程拆成了无数个离散的“步”(Step),每个 Step 都要记录粒子的能量变化和位置变化。
第四块是事件与管理模块(Run / Event)。它负责把上面这些过程组织成可以批量执行的任务。一次完整的模拟叫一个 Run(运行),一次发射一颗粒子的完整处理叫一个 Event(事件),一颗粒子从出生到死亡的全部轨迹叫一个 Track(径迹),路径上每一步就是一个 Step。这四个层级的概念非常重要,后面所有强制类和可选类都是挂在这四个层级上的。
2.2 Run、Event、Track、Step四个层级的关系
我见过很多教材用专业术语解释这四个词,结果越解释越糊涂。我用拍电影来打个比方:
- Run相当于一整部电影。一个 Run 包含你指定的所有事件,比如你要求模拟一百万颗粒子,那这整个一百万次发射就是同一个 Run。一个 Run 开始前要初始化很多东西,结束后要汇总所有统计结果。
- Event相当于电影里的一个镜头。每次从源发出一个粒子(或者一束粒子)并处理完,就是一个 Event。在一百万颗粒子的模拟中,就有一百万个 Event,每个 Event 之间是独立且互不干扰的。
- Track相当于一个演员的完整表演。一颗粒子从诞生到被吸收、逃逸或者能量低于阈值,这整个生命周期就是一条 Track。一个 Event 里可能包含许多条 Track,因为粒子碰撞会产生次级粒子,次级粒子又会有自己的 Track。
- Step相当于表演中的一个动作。Track 被切成很多小段,每走一小段就是一个 Step。每次 Step 的起点和终点之间,粒子可能跨越了几何边界、经历了能量损失或发生了散射。
理解这四个层级之后,“强制类”和“可选类”就好懂了——因为它们本质上是为这四级服务的钩子。
3. 强制类:少写一个都跑不起来的三个类
3.1 为什么“强制”这三个类
在 Geant4 的官方文档里,有一个词叫 User Initialization Classes,中文常翻译成“用户初始化类”。其中三个类是必须由用户自己实现的,否则程序就跑不起来,这三个类统称为强制类:
G4VUserDetectorConstruction:负责搭建几何与探测器G4VUserPhysicsList:负责定义物理过程G4VUserPrimaryGeneratorAction:负责发射粒子
我当年第一次看官方教程时,看到“VUser”这个前缀很不理解。后来才知道 V 代表 Virtual(虚基类),意思是 Geant4 已经给你定义好了接口框架,但具体内容是一张白纸,你必须自己去画。这三个类就是三个必须由你填写的空表格,少填一张,整个程序就运转不起来。
3.2 DetectorConstruction:先把“舞台”搭出来
第一个强制类要解决的是“粒子在什么样的环境里跑”。如果你的模拟没有几何体,那粒子就没有可以发生碰撞的对象,整个模拟就没有意义。
继承G4VUserDetectorConstruction之后,你需要重写一个Construct()方法,并返回一个G4VPhysicalVolume*。这个物理体积就是整个模拟世界的“世界体”,所有其他几何体都必须被包含它里面。
在实际写代码时,一个最简单的 DetectorConstruction 需要做这几件事:
第一,用G4NistManager创建材料。NIST 材料数据库里已经内置了上百种常见材料,直接用名称调用就行,比如G4_Al是铝,G4_WATER是水,G4_AIR是空气,不用自己去手动设置密度和元素比例。
第二,用各种 Solid 类定义形状。比如G4Box是长方体,G4Tubs是圆柱壳,G4Sphere是球壳。你给它长宽高或者半径尺寸,它就帮你构建出一个几何形状。
第三,把形状和材料组成逻辑体积(Logical Volume)。逻辑体积是一个“没有位置”的抽象体积,它只关心形状和材料。
第四,把逻辑体积摆放到物理空间里,成为物理体积(Physical Volume)。一个逻辑体积可以被重复放置很多次,位置不同、旋转角度不同,就形成了多个实体。
很多人容易搞混“逻辑体积”和“物理体积”的区别。我自己的理解是:逻辑体积像“模具”,物理体积像“用模具倒出来的实物”。同一个模具可以倒出很多个实物放在不同位置,这样设计是为了节省内存、方便批量管理。
3.3 PhysicsList:决定粒子在这个世界里的“行为规则”
第二个强制类要解决的是“粒子会经历哪些物理过程”。如果你的物理列表是空的,粒子就会像幽灵一样穿过所有物质,什么都不发生。
在旧版本 Geant4 中,用户通常直接继承G4VUserPhysicsList并重写三个方法:ConstructParticle()(构造粒子)、ConstructProcess()(构造物理过程)、SetCuts()(设置截断参数)。但在新版本里,官方更推荐直接使用现成的物理列表组合,通过G4PhysListFactory调用即可。
我实际入门时用的是比较省事的方式:
#include "G4PhysListFactory.hh" G4PhysListFactory factory; G4VModularPhysicsList* physicsList = factory.GetReferencePhysList("FTFP_BERT");这种叫法里的字母都是有含义的。FTF 表示 Fritiof 弦模型,适用于高能强子;BERT 表示 Bertini 级联模型,适用于中低能中子;还有 QGSP 表示夸克胶子弦模型。不夸张地说,物理列表的选择直接决定了你模拟结果靠不靠谱,也决定了程序跑得快不快。入门阶段直接用官方推荐列表就好,不要自己手动去组合一个个物理过程,很容易漏,漏了结果就错。
3.4 PrimaryGeneratorAction:往舞台上“扔”粒子
第三个强制类解决的是“粒子从哪里来、什么种类、多大能量、飞向哪里”。最常用的方式是G4ParticleGun(粒子枪),它像一把可以自由设定的枪,每次开火发射一颗或一束指定粒子。
这个类里面要重写GeneratePrimaries()方法。你需要在这里完成:
第一,选择粒子类型。比如G4ParticleTable::GetParticleTable()->FindParticle("e-")可以得到电子,同理还有"gamma"(光子)、"proton"(质子)、"neutron"(中子)等。
第二,设置粒子能量。注意 Geant4 内部默认的单位体系是毫米、纳秒、兆电子伏,写的时候要显式乘上单位,比如gun->SetParticleEnergy(5 * MeV),而不是写5。
第三,设置粒子位置和方向。位置用G4ThreeVector表示,方向也用它来表示,系统会自动帮你归一化。
除了粒子枪,后续你还会遇到G4GeneralParticleSource(GPS),它可以定义更复杂的源分布,比如空间展宽源、能量谱源、方向分布源。但入门阶段先掌握粒子枪就足够跑通第一个示例了。
3.5 三个强制类如何被组装到一起
这三个强制类最终都要在 main 函数里交给 G4RunManager 去管理,这也是整个程序的主心骨。
G4RunManager* runManager = new G4RunManager(); runManager->SetUserInitialization(new DetectorConstruction()); runManager->SetUserInitialization(new PhysicsList()); runManager->SetUserAction(new PrimaryGeneratorAction()); runManager->Initialize(); runManager->BeamOn(1000);看到没有,DetectorConstruction和PhysicsList是通过SetUserInitialization注册的,而PrimaryGeneratorAction是通过SetUserAction注册的。这暗示了两者在生命周期上的差异:前两个在程序初始化阶段就要创建好,而粒子源是在每次 BeamOn 时被反复调用的。
4. 可选类:让模拟结果真正可用的关键扩展
4.1 RunAction:跑完整个Run后做什么
如果你只写强制类,Geant4 确实能跑起来,但跑完之后你什么数据都拿不到。这时候就需要“可选类”登场了。可选类的共同点是它们都继承自 G4UserXXXAction,并且挂在不同的事件层级上。
G4UserRunAction挂在整个 Run 层面。它有BeginOfRunAction()和EndOfRunAction()两个虚函数,分别在 Run 开始之前和 Run 结束之后被调用。
最常见的用法是在EndOfRunAction()里输出统计汇总。比如你模拟了一百万颗电子,你可以在 RunAction 里读到一共发生了多少次相互作用、探测器里总沉积了多少能量,然后写到文件或者直接打印到屏幕上。我还习惯在 RunAction 里创建分析管理器(G4AnalysisManager)的输出文件,比如 ROOT 文件或者 CSV 文件,这样整个 Run 的数据结构可以统一管理。
4.2 EventAction:每个Event的起止时机
G4UserEventAction对应的是 Event 层级。它也有两个接口:BeginOfEventAction()和EndOfEventAction()。
有些物理量只在事件级别才有意义。比如你想知道“每个事件里探测器沉积了多少能量”,那就需要记录每个 Event 的能量沉积,然后在EndOfEventAction()里进行统计并清零。
我经常把 RunAction 和 EventAction 配合使用:EventAction 负责每个事件的累加,RunAction 负责所有事件的汇总。要注意的是,在多线程模式下,每个线程会有自己独立的 RunAction 和 EventAction 实例,跨线程汇总数据需要额外处理合并逻辑。
4.3 SteppingAction:最精细也最耗性能的探针
G4UserSteppingAction是粒度最细的可选类,它在每个 Step 结束之后都会被调用。你可以通过G4Step对象拿到当前步的很多信息,比如能量沉积、粒子的当前位置、所在的物理体积、当前过程的类型等等。
SteppingAction 非常重要,但我建议新手谨慎使用它。原因是它的调用频率实在太高了。一个事件可能有几千个 Step,一百万个事件就是几十亿次调用,如果你在 Step 里写了复杂的计算或 I/O 操作,程序能慢到让你怀疑人生。
我自己的经验是:能用 EventAction 或 RunAction 解决的统计需求,就别开 SteppingAction。只有当你要做非常精细的空间剂量分布,比如记录粒子在各层材料里的能量沉积二维分布时,才值得用 SteppingAction,而且最好只记录你关心的那一小部分体积,通过判断step->GetPreStepPoint()->GetPhysicalVolume()->GetName()来过滤。
4.4 TrackingAction与StackingAction:容易被忽视的两个助手
除了上面三个,还有两个可选类在实际项目中也非常有用:G4UserTrackingAction和G4UserStackingAction。
G4UserTrackingAction挂靠在 Track 层级,提供PreUserTrackingAction()和PostUserTrackingAction()。它的典型用途是“追踪粒子的一生”:某个粒子在哪个位置诞生、走了多远、最后因为什么过程结束。想统计探测器里的粒子径迹长度时,用它很方便。
G4UserStackingAction则负责管理粒子的“待办清单”。它有一个ClassifyNewTrack()接口,你可以在这里决定新产生的次级粒子是“马上处理”(fPush)、“放入待处理队列”(fWaiting)还是“直接丢弃”(fKill)。比如有的模拟场景里你只关心光子,不关心电子和正电子,就可以在 StackingAction 里把电子类的次级粒子直接丢掉,省去大量计算时间。
4.5 该选哪些可选类:按需选择,不要全都实现
新手容易犯的一个毛病是:看到教程里介绍了这几个可选类,就一股脑全继承一遍,往 main 函数里全部注册进去。这其实没有必要,还会拖慢程序速度。
我建议按需选择的标准是:
- 只想要总沉积能量:RunAction 就够,或者 EventAction 也可以。
- 想要每个事件的沉积能量分布:EventAction 足够。
- 想要空间分辨的剂量分布:SteppingAction 或 SensitiveDetector 才需要。
- 想要粒子种类和数量统计:TrackingAction 配合 RunAction。
- 想要通过剪裁降低计算量:StackingAction。
记住一点,可选类的本质是“钩子”,钩子越多,程序干预越频繁,性能开销越大。够用就好,别贪多。
5. 实操示例:让一束电子穿过一块铝靶
5.1 环境准备:编译Geant4工具包
开始写代码之前,得先把 Geant4 编译出来。这个过程本身就是不少人卡住的第一道坎。
我的建议是不要自己从零开始定制编译选项,直接用 CMake 的默认配置加上你要用的图形界面(可选)就行。大致的步骤是:
wget https://geant4-data.web.cern.ch/releases/geant4-v11.2.0.tar.gz tar -xzf geant4-v11.2.0.tar.gz mkdir geant4-build && cd geant4-build cmake -DCMAKE_INSTALL_PREFIX=/path/to/geant4-install \ -DGEANT4_USE_QT=ON \ -DGEANT4_USE_OPENGL_X11=ON \ ../geant4-v11.2.0 make -j4 make install编译时间取决于机器性能,少则几十分钟,多则一两小时,这是正常的。装完之后记得 source 一下环境变量脚本,一般是/path/to/geant4-install/bin/geant4.sh,这样 CMake 才能找到 Geant4 的包。
5.2 搭一个最小工程:CMakeLists.txt的写法
Geant4 官方推荐用 CMake 来构建用户程序。一个最小工程的CMakeLists.txt长这样:
cmake_minimum_required(VERSION 3.10) project(FirstGeant4) find_package(Geant4 REQUIRED) include(${Geant4_USE_FILE}) add_executable(firstGeant4 main.cc src/DetectorConstruction.cc src/PhysicsList.cc src/PrimaryGeneratorAction.cc ) target_link_libraries(firstGeant4 ${Geant4_LIBRARIES})这里有几个容易踩坑的地方。find_package如果报错,说明环境变量没 source 对,检查一下Geant4_DIR是否指向安装目录的lib/cmake/Geant4。include(${Geant4_USE_FILE})会自动帮你加一堆编译宏和头文件路径,所以别漏掉这一行。
5.3 main.cc:把强制类串起来
main 文件非常短,核心就是创建 RunManager、注册三个强制类、初始化、跑起来。
#include "G4RunManager.hh" #include "G4UImanager.hh" #include "DetectorConstruction.hh" #include "PhysicsList.hh" #include "PrimaryGeneratorAction.hh" int main() { G4RunManager* runManager = new G4RunManager(); runManager->SetUserInitialization(new DetectorConstruction()); runManager->SetUserInitialization(new PhysicsList()); runManager->SetUserAction(new PrimaryGeneratorAction()); runManager->Initialize(); // 批量跑5000个事件,不启动交互界面 runManager->BeamOn(5000); delete runManager; return 0; }如果你后面想加图形界面,可以在BeamOn之前加一行启动 UI 会话的代码,但这个例子先不加,目标就是一个批处理式的后台模拟程序,能跑出结果就够了。
5.4 DetectorConstruction:建一个世界体和一块铝靶
这个类的实现重点在于返回一个世界体物理体积。世界体里面放一块铝靶,电子从外面射进去。
#include "DetectorConstruction.hh" #include "G4NistManager.hh" #include "G4Box.hh" #include "G4LogicalVolume.hh" #include "G4PVPlacement.hh" G4VPhysicalVolume* DetectorConstruction::Construct() { G4NistManager* nist = G4NistManager::Instance(); G4Material* worldMat = nist->FindOrBuildMaterial("G4_AIR"); G4Material* alMat = nist->FindOrBuildMaterial("G4_Al"); // 世界体:30 cm 的立方体 G4Box* worldBox = new G4Box("WorldBox", 15*cm, 15*cm, 15*cm); G4LogicalVolume* worldLog = new G4LogicalVolume(worldBox, worldMat, "WorldLog"); G4PVPlacement* worldPhys = new G4PVPlacement(nullptr, G4ThreeVector(0,0,0), worldLog, "WorldPhys", nullptr, false, 0); // 铝靶:5 cm x 5 cm x 2 cm,放在原点 G4Box* targetBox = new G4Box("TargetBox", 2.5*cm, 2.5*cm, 1*cm); G4LogicalVolume* targetLog = new G4LogicalVolume(targetBox, alMat, "TargetLog"); new G4PVPlacement(nullptr, G4ThreeVector(0,0,0), targetLog, "TargetPhys", worldLog, false, 0); return worldPhys; }注意这里 G4Box 构造函数的参数是“半长”,也就是说你写 15cm,它实际是 30cm 的边长。我第一次用的时候就栽过这个跟头,导致几何体和预期相差一倍。铝靶放在世界体中间,电子从 z 轴负方向射向正方向,这样会穿过铝靶,产生韧致辐射和电离等一系列过程。
5.5 PhysicsList:用工厂类直接选择物理列表
这个类的实现最省事,因为我们要用的是现成的组合物理列表。
#include "PhysicsList.hh" #include "G4PhysListFactory.hh" PhysicsList::PhysicsList() : G4VModularPhysicsList() { G4PhysListFactory factory; G4VModularPhysicsList* base = factory.GetReferencePhysList("FTFP_BERT"); // 把工厂产生的物理列表里的所有过程复制到当前列表 G4PhysicsListHelper::GetPhysicsListHelper()->AddPhysicsList(base); }不过稍微要注意一下,继承G4VModularPhysicsList后直接复制另一个列表的方式,在新版本里有点绕。更常见也更直接的做法是干脆不自己写这个类,直接在 main 里头实例化工厂生成的物理列表:
G4PhysListFactory factory; G4VModularPhysicsList* physicsList = factory.GetReferencePhysList("FTFP_BERT"); runManager->SetUserInitialization(physicsList);这样连 PhysicsList.cc 都可以不写了。我分享这个写法的原因是想告诉你:Geant4 工具包的很多功能不一定要通过继承去实现,直接用现成工厂类组合也是一种高效方式。强制类里的 PhysicsList 之所以仍是“强制”,是因为你总要设置物理列表,至于用哪种方式设置,可以灵活处理。
5.6 PrimaryGeneratorAction:用粒子枪发射5 MeV电子
#include "PrimaryGeneratorAction.hh" #include "G4ParticleGun.hh" #include "G4ParticleTable.hh" #include "G4SystemOfUnits.hh" PrimaryGeneratorAction::PrimaryGeneratorAction() { fGun = new G4ParticleGun(1); G4ParticleTable* table = G4ParticleTable::GetParticleTable(); G4ParticleDefinition* electron = table->FindParticle("e-"); fGun->SetParticleDefinition(electron); fGun->SetParticleEnergy(5 * MeV); fGun->SetParticlePosition(G4ThreeVector(0, 0, -5*cm)); fGun->SetParticleMomentumDirection(G4ThreeVector(0, 0, 1)); }我这里设了 5 MeV 的电子,方向沿 z 轴正方向,初始位置在离原点 5 cm 的地方。注意能量必须写成5 * MeV,而不是5,这一点在 Geant4 里非常关键。官方所有示例代码都用这种方式,目的就是让你显式地指定单位,避免一不留心用错单位制。
5.7 编译运行与结果解读
工程文件都准备好之后,编译运行:
mkdir build && cd build cmake .. make ./firstGeant4程序跑完会输出一堆初始化信息和每一事件的处理状态。因为我们在 main 里没有加统计输出,所以 Geant4 只会打印最基础的处理信息,告诉你模拟跑完了。想要输出能量沉积,就需要把第 4 节的可选类加进来,比如写一个简单的 RunAction,在EndOfRunAction里通过G4AccumulableManager拿到总沉积能量。
这一步做通之后,你就已经跑通了“几何 + 物理 + 粒子源”的最小闭环,后面的路就比较顺了。
6. 常见问题与排查技巧实录
6.1 单位没乘,结果离谱得没法看
这是新手最容易犯、也最隐蔽的问题。Geant4 内部默认单位是毫米(mm)、纳秒(ns)、兆电子伏(MeV),但你在写代码时不写单位,数字就只是一个裸数值。比如你写SetParticleEnergy(5),它会被认为是 5 MeV,如果你本意是 5 GeV,那结果就差了一千倍。反过来,你写new G4Box("box", 10, 10, 10)那不是 10 米,而是 10 毫米。
解决方法是永远显式地乘上单位,比如10 * mm、5 * MeV、0.1 * cm。Geant4 的G4SystemOfUnits.hh里定义了这些单位常量,include 一下就能用。我后来复查代码的时候,第一步永远是检查有没有裸数字,凡是没乘单位的,基本都能揪出问题。
6.2 世界体太小,粒子直接飞出边界被杀死
粒子在模拟中跑出世界体边界之后,就会被标记为“逃逸”并停止追踪。如果你发现很多粒子消失得很快、物理过程没跑完,很可能就是世界体建小了。尤其是模拟高能粒子时,次级粒子可能飞得很远,世界体至少应该覆盖你关心的区域,再往外留一些余量。
另外,几何重叠也会引发告警。Geant4 会在初始化阶段做几何检查,如果两个物理体积在空间上有重叠,它会打印G4Exception警告。这种情况会导致粒子在边界处的行为不可预测,必须回头改几何,不要硬着头皮跑下去。
6.3 物理列表选错,粒子像幽灵一样穿透
如果你发现粒子穿过了探测器但几乎没有能量沉积,先别怀疑代码,检查一下物理列表。某些物理列表只包含特定能量范围的过程,如果能量范围严重不匹配,粒子就几乎不发生相互作用。
入门阶段我建议直接记住这几个常用组合:一般粒子物理实验用FTFP_BERT,医学物理中电子光子输运常用G4EmStandardPhysics_option4(更精确的电磁过程),中子相关用QGSP_BERT或HP系列。不要自己手动逐个加过程,除非你确实清楚每个过程的有效能量范围。
6.4 多线程模式下统计量对不上
从 Geant4 10.0 版本开始默认开启了多线程(MT),一个 Run 会被拆成多个线程并行处理。如果每个线程的 EventAction 都维护自己的累加变量,那最后汇总时如果不做归并,统计结果就会对不上。
Geant4 提供了G4AccumulableManager来帮你做线程间归并,建议直接用。用起来也很简单,在 RunAction 的构造函数里注册你要统计的量,然后在对应的地方通过Accumulable对象累加,结束时管理器会自动帮你合并所有线程的数据。如果不想碰多线程,可以在 main 里设置G4MTRunManager之前先设定线程数为 1:
G4int nThreads = 1; G4RunManager::SetNumberOfThreads(nThreads);这样虽然牺牲了并行性能,但统计逻辑简单,适合入门调试。
6.5 编译报错:链接库没找到、头文件路径不对
Geant4 的报错信息默认很详细,但太多了反而让人找不着重点。我的排查习惯是:先看第一条 error,不要往下翻。最常见的两类问题是:
第一,fatal error: G4RunManager.hh: No such file or directory,说明 CMake 的include(${Geant4_USE_FILE})没有生效,检查find_package是否真的找到了 Geant4。
第二,undefined reference to 'DetectorConstruction::Construct()',说明对应的.cc文件没有加进add_executable,或者头文件里有声明但源文件没实现。
还有一个小技巧:如果你同时在多个工程之间切换,记得每次重新 build 之前rm -rf build再mkdir build,因为 Geant4 的 CMake 配置如果找不到库,会把错误状态缓存下来,不清缓存的话怎么改都报同样的错。
最后再分享一点个人小体会。我见过很多初学者一上来就抱着官方文档几百页慢慢啃,啃到一半就放弃了。入门 Geant4 最快的路径其实是一条直线:先把最小示例跑通,再回头逐个理解概念。我第一次跑通那个 5 MeV 电子打铝靶的程序时,根本记不住那么多类的细节,但脑子里已经有了“RunManager 接三个强制类”的主干,后面不管学什么新功能,都是往这个主干上挂分支。记住这个节奏,别急,慢慢来,你会看到模拟数据出来那一刻,还是很值得的。