简介:本资源是面向高能物理仿真与光学光子传播研究者的Geant4进阶实践示例,聚焦于G4Opticks光学扩展模块的集成与应用,适用于已掌握C++基础及Geant4核心框架的科研人员与研究生。项目完整实现了光学光子在定制化几何介质中的生成、传输与探测模拟,涵盖环境配置(NVIDIA OptiX 6.5 + CUDA 11.0 + Geant4 10.6.p03等)、GDML几何建模、敏感探测器定义(RadiatorSD/DetectorConstruction)及批量运行脚本(run_G4_grid.bash/merge.bash等),具备典型蒙特卡洛光学仿真全流程参考价值。压缩包共105个文件,含27个C++源码(.cc/.hh)、11个GDML几何描述、11个Shell脚本、9个宏命令(.mac)及Python/ROOT绘图脚本(plote.C/bench.C),总大小仅179KB,结构紧凑、模块职责清晰。目前已有599人学习下载,可直接复现光学环境搭建、调试光子追踪逻辑,并快速迁移至液氩TPC等实际探测器仿真场景。
1. Geant4光学仿真不是“加个光源就跑通”:G4OpticksTest 实际是光学光子传播链的最小可验证闭环
很多人第一次点开G4OpticksTest,以为只是个“带玻璃块的Geant4示例”,跑完看到几行日志就关掉——结果在真实项目里做光纤探测器响应建模时,发现光子根本进不了光电倍增管,反射率算得再准也没用。这恰恰暴露了 G4OpticksTest 的真实价值:它不是教你怎么放一个光源,而是强制你走完光学光子从产生、表面交互、体吸收/散射到最终被吸收或逃逸的全路径闭环。它用最简几何(球壳+圆柱+平面)、最少物理列表(仅G4OpticalPhysics)、最直白的用户动作(GeneratePrimaryVertex→SteppingAction→TrackingAction),把Geant4光学模块中容易被忽略的耦合点全部暴露出来——比如G4OpBoundaryProcess何时触发、G4OpAbsorption如何依赖波长表、G4OpRayleigh的散射角采样是否被禁用。适合正在调试闪烁体-光导-光电阴极耦合效率、水切伦科夫探测器光子收集率、或塑料闪烁体条端读出信噪比的开发者。如果你的模型里光子“消失得莫名其妙”,或者G4Step::GetTotalEnergyDeposit()在光学介质里恒为0,那这个测试不是可选项,而是必过门槛。
2. 从零构建 G4OpticksTest:不依赖预编译包,手写核心类与物理列表
2.1 创建光学专用 DetectorConstruction:介质定义必须绑定光学属性表
光学仿真的起点不是光源,而是介质的光学属性定义是否可被物理过程实时查表。G4OpticksTest 要求每个G4Material必须显式绑定G4MaterialPropertiesTable,且表中至少包含RINDEX、ABSLENGTH(吸收长度)、SCATTERINGLENGTH(散射长度)三项——缺一不可,否则G4OpBoundaryProcess会静默跳过边界判断。
// 在 DetectorConstruction::Construct() 中定义光学介质 G4Material* water = new G4Material("Water", 1.0*g/cm3, 2); water->AddElement(H, 2); water->AddElement(O, 1); // 关键:必须创建并绑定光学属性表 G4MaterialPropertiesTable* mpt = new G4MaterialPropertiesTable(); // 波长数组(单位:nm),必须严格递增 std::vector<G4double> photonEnergy = {1.55*eV, 2.00*eV, 3.10*eV}; // ~800nm, 620nm, 400nm std::vector<G4double> rindex = {1.33, 1.34, 1.35}; std::vector<G4double> absLength = {100.*cm, 50.*cm, 10.*cm}; std::vector<G4double> scatterLength = {500.*cm, 200.*cm, 50.*cm}; mpt->AddProperty("RINDEX", &photonEnergy[0], &rindex[0], photonEnergy.size()); mpt->AddProperty("ABSLENGTH", &photonEnergy[0], &absLength[0], photonEnergy.size()); mpt->AddProperty("SCATTERINGLENGTH", &photonEnergy[0], &scatterLength[0], photonEnergy.size()); // 必须设置为“光学活性”标志,否则物理过程不加载 mpt->AddConstProperty("MATERIAL_TYPE", 1.); // 1=optical, 0=non-optical water->SetMaterialPropertiesTable(mpt);提示:
MATERIAL_TYPE是隐式开关,不设或设为0会导致G4OpAbsorption等过程直接跳过该介质。很多翻车案例源于复制了RINDEX却漏掉这行。
2.2 注册光学物理过程:G4OpticalPhysics 必须启用且不可裁剪
Geant4 10.7+ 版本中,G4OpticalPhysics已成为独立物理构造器,但不能简单RegisterPhysics(new G4OpticalPhysics())就完事。其内部默认禁用Cerenkov和Scintillation过程(因 G4OpticksTest 专注纯光学传播),但OpBoundary、OpAbsorption、OpRayleigh必须显式启用:
// 在 PhysicsList::ConstructProcess() 中 void MyPhysicsList::ConstructProcess() { // 先调用基类注册所有基础过程(电磁、强子等) AddTransportation(); // 光学物理:必须用指针管理,确保生命周期 G4OpticalPhysics* opticalPhysics = new G4OpticalPhysics(); // 关键:强制启用边界过程(默认可能关闭) opticalPhysics->Configure(kCerenkov, false); // 关闭切伦科夫(非本例重点) opticalPhysics->Configure(kScintillation, false); // 关闭闪烁(非本例重点) opticalPhysics->Configure(kOpAbsorption, true); // 必须开启 opticalPhysics->Configure(kOpRayleigh, true); // 必须开启 opticalPhysics->Configure(kOpBoundary, true); // 必须开启!否则无反射/折射 RegisterPhysics(opticalPhysics); }参数说明:
kOpBoundary启用后,Geant4 才会在每一步G4Step结束时调用G4OpBoundaryProcess::PostStepDoIt(),进而触发G4OpBoundaryProcess::ChooseReflection()判断是反射、折射还是全反射。若此处为false,所有光学表面将视为“完全吸收”,光子直接死亡。
2.3 主要动作类:SteppingAction 中必须捕获光学过程名
G4OpticksTest 的调试核心在于确认光子是否真正触发了预期的光学过程。仅靠G4Step::GetTotalEnergyDeposit()无法区分是电离损失还是光学吸收。必须在SteppingAction::UserSteppingAction()中检查G4Step::GetPostStepPoint()->GetProcessDefinedStep():
void SteppingAction::UserSteppingAction(const G4Step* step) { G4Track* track = step->GetTrack(); if (track->GetDefinition() != G4OpticalPhoton::OpticalPhotonDefinition()) return; G4StepPoint* postStep = step->GetPostStepPoint(); const G4VProcess* process = postStep->GetProcessDefinedStep(); if (process) { G4String procName = process->GetProcessName(); // 关键日志:只打印光学相关过程,过滤掉 transportation if (procName.contains("Op")) { G4cout << "Optical photon #" << track->GetTrackID() << " @ " << postStep->GetPosition() << " triggered: " << procName << " (E=" << track->GetKineticEnergy()/eV << " eV)" << G4endl; } } }逻辑说明:此段代码是 G4OpticksTest 的“黑匣子记录仪”。当输出中出现
OpAbsorption、OpRayleigh、OpBoundary时,证明光学过程链已激活;若只有Transportation,说明光子未被任何光学过程拦截——大概率是介质未绑定属性表,或G4OpticalPhysics配置错误。
3. 光源与几何:为什么 G4OpticksTest 用球壳而非立方体?表面法向才是关键
3.1 光源位置必须严格位于光学介质内部,且避免表面重合
G4OpticksTest 使用G4SPSPosDistribution在球壳内均匀采样,而非G4GeneralParticleSource的默认点源。原因在于:光学光子必须从介质内部发射,且初始位置不能恰好落在几何表面网格上。若G4PrimaryVertex坐标与G4VSolid表面距离小于kCarTolerance(默认 1nm),Geant4 会判定光子“已在表面外”,导致G4OpBoundaryProcess直接跳过,光子瞬间被吸收。
// 正确:在球壳内随机生成,且预留安全距离 void PrimaryGeneratorAction::GeneratePrimaries(G4Event* event) { G4SPSPosDistribution* posDist = fParticleGun->GetPosDist(); posDist->SetCentreCoords(G4ThreeVector(0.,0.,0.)); posDist->SetRadius(9.9*cm); // 球壳半径10cm,此处用9.9cm确保不触边 fParticleGun->SetParticleDefinition(G4OpticalPhoton::OpticalPhotonDefinition()); fParticleGun->SetParticleEnergy(2.5*eV); // 496nm蓝光 fParticleGun->SetParticleMomentumDirection(G4RandomDirection()); // 各向同性 fParticleGun->GeneratePrimaryVertex(event); }参数说明:
SetRadius(9.9*cm)是血泪经验——用10.0*cm在某些网格划分下会触发表面容差误判。实际项目中建议预留0.5%半径余量。
3.2 几何表面必须显式设置光学表面属性(G4OpticalSurface)
G4OpticksTest 包含一个空心球壳(inner radius 9cm, outer radius 10cm),其内外表面需分别定义G4OpticalSurface。这是唯一能控制反射率、漫反射比例、表面粗糙度的入口。若省略此步,所有表面默认为dielectric_dielectric类型,但反射率由斯涅尔定律自动计算,无法模拟真实涂层(如TiO2漫反射层)或机械加工粗糙度。
// 在 DetectorConstruction::Construct() 中为球壳内表面添加光学表面 G4Sphere* shellSolid = new G4Sphere("Shell", 9.*cm, 10.*cm, 0., 2*pi, 0., pi); G4LogicalVolume* shellLog = new G4LogicalVolume(shellSolid, water, "ShellLog"); // 定义内表面(光子从水射向空气) G4OpticalSurface* innerSurface = new G4OpticalSurface("InnerSurface"); innerSurface->SetType(dielectric_dielectric); innerSurface->SetModel(glisur); // 或 unified / LUT innerSurface->SetFinish(groundfrontpainted); // 漫反射涂层 // 创建表面属性表:控制反射行为 G4MaterialPropertiesTable* surfMPT = new G4MaterialPropertiesTable(); std::vector<G4double> surfPhotonEnergy = {1.55*eV, 2.00*eV, 3.10*eV}; std::vector<G4double> reflectivity = {0.95, 0.92, 0.88}; // TiO2涂层典型值 surfMPT->AddProperty("REFLECTIVITY", &surfPhotonEnergy[0], &reflectivity[0], 3); innerSurface->SetMaterialPropertiesTable(surfMPT); // 绑定表面到逻辑体积的特定面(此处为内球面) new G4LogicalBorderSurface("InnerSurface", shellLog, // 逻辑体A worldLog, // 逻辑体B(空气) innerSurface);注意:
G4LogicalBorderSurface的两个参数顺序决定法向方向。shellLog到worldLog表示“从水到空气”,若反写则反射率表失效。
4. 避坑:G4OpticksTest 最常踩的 4 个坑与现场排查法
4.1 现象:光子轨迹在日志中显示Step#0后立即终止,无任何OpXXX过程名
原因:G4OpticalPhoton的G4ParticleDefinition未正确注册,或G4OpticalPhysics未被RegisterPhysics()加载。常见于自定义 PhysicsList 中遗漏AddTransportation()或RegisterPhysics(opticalPhysics)调用。
解决:在PhysicsList::ConstructProcess()开头添加G4cout << "PhysicsList loaded" << G4endl;,并在G4OpticalPhysics构造后立即打印G4cout << "Optical physics registered" << G4endl;。若后者不输出,说明构造器未执行。
4.2 现象:SteppingAction日志中持续输出Transportation,但从不出现OpBoundary
原因:介质未绑定G4MaterialPropertiesTable,或绑定的表中缺失RINDEX属性(G4OpBoundaryProcess强依赖折射率计算临界角)。
解决:在DetectorConstruction::Construct()中,对每个光学介质添加断言:
if (!material->GetMaterialPropertiesTable()) { G4Exception("DetectorConstruction", "NoMPT", FatalException, "Material has no MPT!"); } G4MaterialPropertiesTable* mpt = material->GetMaterialPropertiesTable(); if (!mpt->GetProperty("RINDEX")) { G4Exception("DetectorConstruction", "NoRINDEX", FatalException, "RINDEX property missing in MPT!"); }4.3 现象:光子进入某介质后轨迹突然变直(无散射弯曲),且OpRayleigh不触发
原因:SCATTERINGLENGTH属性值设为DBL_MAX或负数,导致G4OpRayleigh::PostStepGetPhysicalInteractionLength()返回0,过程被跳过。
解决:检查SCATTERINGLENGTH数组值,确保全部为正有限数。临时调试可设为固定值:std::vector<G4double> scatterLength = {100.*cm, 100.*cm, 100.*cm};。
4.4 现象:光子在表面发生全反射,但G4OpBoundaryProcess::ChooseReflection()返回unified模式下的dielectric_metal分支
原因:表面G4OpticalSurface::SetFinish()设置为polishedbackpainted或groundbackpainted,导致 Geant4 将背面涂层误判为金属界面。
解决:对纯介质-介质界面,SetFinish()必须为polished(镜面)或ground(漫反射),禁用backpainted类型。若需模拟背面涂层,应单独建模为薄层介质,而非表面属性。
5. 验证光学传播链:用 G4AnalysisManager 记录光子命运与能量沉积分布
5.1 定义分析树:捕获每个光子的完整生命周期事件
G4OpticksTest 的终极验证不是看日志,而是用G4AnalysisManager导出结构化数据,统计光子在各介质中的吸收/散射次数、逃逸位置、最终能量沉积点。这需要在TrackingAction中记录起始,在SteppingAction中追加过程,在EventAction中写入:
// 在 EventAction::BeginOfEventAction() 中初始化 void EventAction::BeginOfEventAction(const G4Event* event) { fAnalysisManager = G4AnalysisManager::Instance(); fEventID = event->GetEventID(); fPhotonCount = 0; } // 在 TrackingAction::PreUserTrackingAction() 中记录光子诞生 void TrackingAction::PreUserTrackingAction(const G4Track* track) { if (track->GetDefinition() == G4OpticalPhoton::OpticalPhotonDefinition()) { fAnalysisManager->FillNtupleIColumn(0, fEventID); fAnalysisManager->FillNtupleIColumn(1, track->GetTrackID()); fAnalysisManager->FillNtupleDColumn(2, track->GetVertexPosition().x()/cm); fAnalysisManager->FillNtupleDColumn(3, track->GetVertexPosition().y()/cm); fAnalysisManager->FillNtupleDColumn(4, track->GetVertexPosition().z()/cm); fAnalysisManager->FillNtupleDColumn(5, track->GetVertexKineticEnergy()/eV); fAnalysisManager->AddNtupleRow(0); } } // 在 SteppingAction::UserSteppingAction() 中追加过程事件 void SteppingAction::UserSteppingAction(const G4Step* step) { G4Track* track = step->GetTrack(); if (track->GetDefinition() != G4OpticalPhoton::OpticalPhotonDefinition()) return; G4StepPoint* postStep = step->GetPostStepPoint(); const G4VProcess* process = postStep->GetProcessDefinedStep(); if (!process || !process->GetProcessName().contains("Op")) return; fAnalysisManager->FillNtupleIColumn(0, fEventID); fAnalysisManager->FillNtupleIColumn(1, track->GetTrackID()); fAnalysisManager->FillNtupleSColumn(2, process->GetProcessName()); fAnalysisManager->FillNtupleDColumn(3, postStep->GetPosition().x()/cm); fAnalysisManager->FillNtupleDColumn(4, postStep->GetPosition().y()/cm); fAnalysisManager->FillNtupleDColumn(5, postStep->GetPosition().z()/cm); fAnalysisManager->FillNtupleDColumn(6, track->GetKineticEnergy()/eV); fAnalysisManager->AddNtupleRow(1); // 第二张表:过程事件 }参数说明:此处定义两张表:表0记录光子出生信息(事件ID、轨迹ID、位置、初始能量),表1记录每次光学过程事件(事件ID、轨迹ID、过程名、位置、当前能量)。运行1000个事件后,可用Python脚本统计
OpAbsorption发生次数与位置分布,验证吸收是否集中在预期介质中。
5.2 用 ROOT 脚本验证:光子逃逸率与理论值比对
导出的analysis.root文件可用以下 ROOT 宏快速验证:
// verify_optics.C void verify_optics() { TFile* f = new TFile("analysis.root"); TTree* t1 = (TTree*)f->Get("tree1"); // 过程事件表 TH1F* hEscape = new TH1F("hEscape", "Escape Position Z", 100, -15, 15); TH1F* hAbsorb = new TH1F("hAbsorb", "Absorption Position Z", 100, -15, 15); Double_t zPos, energy; TString procName; t1->SetBranchAddress("pos_z", &zPos); t1->SetBranchAddress("process", &procName); t1->SetBranchAddress("energy", &energy); Long64_t nentries = t1->GetEntries(); for (Long64_t i=0; i<nentries; i++) { t1->GetEntry(i); if (procName == "OpBoundary" && TMath::Abs(zPos) > 9.9) { hEscape->Fill(zPos); } else if (procName == "OpAbsorption") { hAbsorb->Fill(zPos); } } // 理论逃逸率:球壳外表面面积 / 总表面积 ≈ (4π×10²)/(4π×10² + 4π×9²) ≈ 61% Double_t escapeRate = hEscape->GetEntries() / (hEscape->GetEntries() + hAbsorb->GetEntries()); printf("Measured escape rate: %.2f%% (theory: 61%%)\n", escapeRate*100); }技巧:若实测逃逸率显著低于61%,说明存在未预期的吸收(如
ABSLENGTH设得太小);若高于65%,则可能是SCATTERINGLENGTH过大导致光子未充分散射即逃逸。此时应调整属性表中的数值,而非修改几何。
6. 进阶技巧:用 G4OpticksTest 快速验证新光学材料的波长依赖性
6.1 构建波长扫描循环:自动化测试不同波长下的吸收/散射主导区域
G4OpticksTest 的核心价值在于快速验证材料光学参数的波长敏感性。手动改photonEnergy数组再编译太慢,可将其改为命令行参数驱动:
// 在 main() 中解析波长参数 int main(int argc, char** argv) { G4String wavelengthStr = "400"; if (argc > 1) wavelengthStr = argv[1]; // 如 ./G4OpticksTest 450 G4double lambda = std::stod(wavelengthStr) * nm; G4double energy = h_Planck * c_light / lambda; // 在 PrimaryGeneratorAction 中使用该能量 fParticleGun->SetParticleEnergy(energy); // 在 MaterialPropertiesTable 中插值 RINDEX/ABSLENGTH // (此处省略插值代码,实际需用 TGraph 或线性插值) }落地效果:运行
for wl in 400 450 500 550 600; do ./G4OpticksTest $wl; done,即可批量生成不同波长下的analysis.root。用 Python 脚本提取各波长下OpAbsorption事件数,绘制absorption_rate vs wavelength曲线——这正是验证荧光粉斯托克斯位移、或水体光学窗口的关键步骤。
6.2 用 G4VPrimitiveScorer 捕获体吸收空间分布:定位热点区域
G4OpticksTest默认只记录事件级统计,但真实探测器需知道“光子在哪一微米被吸收”。此时应弃用SteppingAction日志,改用G4VPrimitiveScorer:
// 在 DetectorConstruction::ConstructSDandField() 中 void DetectorConstruction::ConstructSDandField() { G4MultiFunctionalDetector* det = new G4MultiFunctionalDetector("OpticalDet"); G4VPrimitiveScorer* scorer = new G4PSEnergyDeposit("Edep"); det->RegisterPrimitive(scorer); SetSensitiveDetector("ShellLog", det); // 绑定到球壳逻辑体 }参数说明:
G4PSEnergyDeposit会自动对每个G4Step在ShellLog内的能量沉积求和,并按体素(voxel)空间分箱。运行后analysis.root中将新增Edep树,含x,y,z,edep字段。用TH3F投影可直观看到吸收热点是否集中在球壳内壁(验证全反射效率)或均匀分布(验证体吸收模型)。
我坚持在每个新光学项目启动前,先用 G4OpticksTest 跑通三组参数:一组用文献值,一组把ABSLENGTH放大10倍,一组把SCATTERINGLENGTH设为1cm。三次结果的差异会立刻暴露我对该材料光学行为的理解盲区——比如发现放大吸收长度后逃逸率未升反降,才意识到散射主导了光子输运路径。这种“用错误参数逼出正确理解”的习惯,比读十篇论文都管用。希望帮到你。
本文还有配套的精品资源,点击获取