Geant4单位系统详解:内部单位、换算与工程排错 做蒙特卡罗模拟这行的人大概都有过这样的经历几何尺寸、材料密度、粒子能量全都按自己心里想的填进去了跑出来的能谱却怎么看怎么不对回头查了三天才发现某个参数少乘了一个单位常量数值上差了一千倍。Geant4 单位系统这套东西说白了就是上游为了防止这类事故专门设计的一层防护但它只防忘记换算防不了换错单位更防不了压根没意识到这里有单位。我自己带的几个模拟项目里新人上手的第一课从来不是物理列表怎么选而是先把单位这件事讲透。这篇就把 Geant4 单位系统的来龙去脉、代码落地方式、以及那些文档里不会写的坑一次讲清楚。不管你刚准备写第一个探测器几何还是已经在维护一套跑了多年的模拟框架下面这些内容都能直接拿去对照。1. 先搞清楚 Geant4 单位系统的设计底色1.1 一套内部单位把所有混乱挡在模块边界之外Geant4 处理单位的核心思路非常朴素在整个框架内部只认一套固定的内部单位。所有物理量在内存里、在函数参数里、在类成员里存的都是这套内部单位的数值不带任何单位标签。你在代码里写10*cm编译器在编译期就把这个表达式折叠成数值100存进去的就是 100含义是 100 mm。等到这个数字流到几何模块、物理过程、径迹对象、灵敏探测器所有人看到的都是 100没有人需要问这个 100 到底是 cm 还是 mm。这个设计的好处是显而易见的。探测器几何动辄几十万个参数在模块之间传递如果每个接口都要约定单位那接口契约会膨胀到无法维护。而把所有换算压在用户输入这一层模块内部就能彻底摆脱单位语义专心做数值计算。代价也同样明显任何一个 G4double 直接打印出来看到的都是内部单位数值而不是你当初写进去的那个数。很多人第一次G4cout length打出100而不是10的时候会愣一下这就是内部单位在起作用。我一般会跟团队里新来的人打这个比方Geant4 内部单位就像公司内部的统一记账货币。你可以用任何货币跟财务报销但账本上永远只有一种货币。你要查账就得自己知道换算汇率。1.2 内部单位对照表与头文件里的常量Geant4 内部单位不是随意定的它选了一套在高能物理里数量级最顺手的单位组合。长度用毫米、能量用兆电子伏、时间用纳秒这三个基本量的选择直接决定了其它导出单位的形态。完整的内部单位对照关系大致如下物理量内部单位说明长度mm毫米几何建模的默认尺度面积mm²由长度导出体积mm³由长度导出时间ns纳秒与光速配合后长度时间量级匹配能量MeV兆电子伏动量MeV/c与能量、光速联动质量MeV/c²静止质量常用表达电荷e基本电荷量正电子电荷定义为 1电流e/ns电荷除以时间电势MeV/e能量除以电荷电场MeV/(e·mm)磁场tesla特斯拉频率1/ns温度kelvin物质的量mole角度rad弧度立体角sr球面度力newton压强pascal功率watt这张表值得打印出来贴显示器边上。尤其是角度内部单位是弧度这一条坑了无数人——你写了一行G4double angle 30.;本意是 30 度实际得到的是 30 弧度差不多转了 4.77 圈。这类错误不会报错只会让你的探测器朝向莫名其妙。对应的常量全部声明在两个头文件里G4SystemOfUnits.hh提供单位常量G4PhysicalConstants.hh提供物理常数。你在任何一个用到单位的源文件顶部加上这两行就够了#include G4SystemOfUnits.hh #include G4PhysicalConstants.hhG4SystemOfUnits.hh里按类别组织了几十组常量长度有km、m、cm、mm、um、nm、angstrom、fermi、parsec面积体积有m2、cm2、mm2、m3、cm3、mm3、liter能量从eV一路排到EeV还有joule、erg、calorie这些工程单位。磁场有tesla和gauss压强有pascal、bar、atmosphere密度有g/cm3、mg/cm3、kg/m3。基本上你能想到的常用单位它都备好了。1.3 头文件引用与版本差异带来的第一道坎如果你翻的是十年前的老教程很可能会看到#include G4UnitsTable.hh这种写法。这是早期版本的单位入口现在还在仓库里但它的定位已经变了——主要服务于G4BestUnit这类把数值翻译成人能读的字符串的功能不再是定义单位常量的地方。新代码里再拿它当单位入口用轻则编译告警重则因为常量定义位置变化踩到重复定义的坑。现在的正确姿势就是上面那两行。有一个细节值得注意G4SystemOfUnits.hh里的常量都是static const G4double在头文件里定义每个翻译单元各自持有一份副本。这在功能上没问题但意味着你不能对这些常量取地址做跨编译单元的比对也别指望它们有唯一的链接符号。我见过有人试图用cm作为哈希键那就属于想太多了。另外混用不同版本编译好的库和头文件时如果上游调整了某个单位的定义基准你会得到编译通过但结果不对这种最难查的问题。所以升级框架版本之后第一件事应该是把关键几何尺寸和材料密度用G4BestUnit打一遍跟升级前的日志对比数值必须逐位一致。2. 拆开常量看本质单位其实就是一个乘法因子2.1 从mm 1开始理解一切打开G4SystemOfUnits.hh第一行有实质意义的定义就是static const G4double mm 1.;。这一行把整个单位系统的地基钉死了毫米就是 1。后面所有长度都相对于它定义cm 10.*mmm 1000.*mmkm 1000.*mangstrom 1.e-7*mmfermi 1.e-12*mm。你会发现这些定义一点都不神秘就是纯粹的数值乘法。理解这一点之后很多东西自然就通了。10*cm为什么等于 100因为cm这个常量的数值是 10乘以 10 得 100。那为什么 100 代表 100 mm因为内部单位是 mm数值 100 的含义就是 100 mm。整个链条里没有任何单位转换函数被调用全是编译期常量折叠。同理能量那边是static const G4double MeV 1.;把兆电子伏定为 1eV 1.e-6*MeVkeV 1.e-3*MeVGeV 1000.*MeV。时间那边是static const G4double ns 1.;second 1.e9*nsps 1.e-3*ns。三个基准量各自钉死其余全部相对展开。这套机制带来一个非常实用的推论你可以自己定义任何单位只要给出它相对于内部单位的倍数。G4UnitDefinition干的就是这件事后面会细说。2.2 复合单位的定义方式与运算优先级陷阱单一的cm、MeV好理解复合单位才是真正容易翻车的地方。G4SystemOfUnits.hh里面积和体积是这么定义的mm2 mm*mmcm2 cm*cmmm3 mm*mm*mmcm3 cm*cm*cm。所以cm3的数值是 1000m3的数值是1e9。密度g/cm3的定义是g/cm3这个整体作为一个常量名数值等于g的数值除以cm3的数值。C 里/和*同级、从左到右结合所以你写1.*g/cm3会被解析成(1.*g)/cm3结果就是1/1000 0.001。这个结果是正确的——1 g/cm³ 确实等于 0.001 g/mm³。但如果你写成1.*g/cm*cm*cm那就变成((1*g)/cm)*cm*cm结果是 1差了三个数量级。这种写法错误编译器完全不管运行起来也不报错全靠你自己心里有数。我总结了一条铁律涉及体积、面积、密度这类复合量永远用头文件里现成的复合常量名不要自己拆开写乘除。g/cm3、mg/cm3、kg/m3、mm2、cm3这些都是现成的直接用。你要是需要kg/cm3这种冷门组合那就老老实实写kg/cm3让编译器按从左到右算一遍算完自己在纸上验算一次数值。再补充一个高频错误1/cm3。有人想表达每立方厘米的个数写了G4double n 1e20/cm3;。这个是对的因为cm3是 10001e20/1000 1e17含义是 1e17 个/mm³。但如果他写1e20/(cm*cm*cm)结果一样写成1e20/cm*cm*cm就变成 1e20 了差了一千倍。2.3 物理常数与单位系统的耦合关系G4PhysicalConstants.hh里的常量和单位系统是深度耦合的因为它们本身就是带单位的数值。光速c_light的定义是299.792458*mm/ns——注意它没有用国际单位制的 299792458 m/s而是直接用内部单位表达数值正好是 299.792458。这样在代码里算飞行时间就特别方便t distance/c_lightdistance 是 mmt 就是 ns不需要任何中间换算。普朗克常量h_Planck的值是4.13566733e-12*MeV*ns约化普朗克常量hbar_Planck由它除以 2π 得到。基本电荷eplus定义为 1。电子静止质量electron_mass_c2是0.510998910*MeV质子是938.272013*MeV中子939.56536*MeV。阿伏伽德罗常量Avogadro是6.02214076e23/mole。玻尔兹曼常量k_Boltzmann是8.617343e-11*MeV/kelvin。这些常量的好处是你把它们代入公式算出来的结果自动就是内部单位。举个我经常用来给新人做练习的例子算 1 keV 光子的波长。公式是 λ hc/E。G4double E 1.*keV; G4double lambda h_Planck*c_light/E;h_Planck*c_light的量纲是 MeV·mm除以 MeV 得到 mm。手算验证4.13566733e-12 × 299.792458 ≈ 1.23984e-9单位 MeV·mm除以 1e-3 MeV 得 1.23984e-6 mm也就是 1.23984 nm。跟理论值 1.24 nm 完全对上。这个例子妙的地方在于整个计算过程你没有写任何一个换算系数 1000 或者 1e-6全靠常量自带单位这就是设计得好的单位系统该有的样子。再举一个飞行时间的例子光在 1 m 真空里走多久t (1.*m)/c_light 1000/299.792458 ≈ 3.3356 ns。同样是零换算。3. 代码、宏命令与输出三条链路上的单位落地3.1 代码里定义物理量的推荐姿势先说结论任何一个表示物理量的 G4double赋值时都必须带单位常量。这条规则听起来很基础但真做到的项目不多。我见过的代码里G4double thickness 2.5;这种裸数字比比皆是作者心里想的是 2.5 mm读代码的人根本无从判断等哪天有人把它改成2.5*cm当成修 bug整个几何就崩了。推荐的写法是这样的// 几何尺寸 G4double targetThickness 1.*mm; G4double targetHalfXY 10.*cm; // 材料密度 G4double density 8.96*g/cm3; // 粒子能量 G4double gunEnergy 6.*MeV; // 时间窗口 G4double timeWindow 500.*ns; // 角度注意这里必须用 deg G4double tiltAngle 15.*deg;构造函数里也一样参数名尽量带上物理量含义赋值时带单位常量。有一类特殊情况是std::vectorG4double或者数组批量塞数值的时候尤其容易漏我的做法是先把带单位的值算好放进局部变量再 push 进去中间不出现裸数字。还有一类隐性裸数字要特别小心默认参数。比如你写void SetCut(G4double cut 0.7);这个 0.7 是内部单位含义是 0.7 mm。默认参数在头文件里很多人写的时候根本没想单位的事等到某天发现切值不对劲翻半天才想起来是这里。默认参数要写 0.7*mm而且最好在注释里标出来。G4double本质上就是double没有任何类型检查能拦住你把一个以 cm 为单位的数值塞进一个期望 mm 的位置。能拦住这种错误的只有人的纪律和代码评审。3.2 Messenger 与宏命令的单位处理命令行宏是另一个重灾区。Geant4 提供的 UI 命令类里G4UIcmdWithADouble和G4UIcmdWithADoubleAndUnit长得几乎一样行为却完全不同。前者不带单位用户输入2.5就被当作内部单位数值 2.5后者带单位用户在宏文件里可以写2.5 cm框架会自动帮你换算成 25。正确的做法是凡是物理量一律用带单位版本。fGunEnergyCmd new G4UIcmdWithADoubleAndUnit(/gun/energy, this); fGunEnergyCmd-SetUnitCategory(Energy); fGunEnergyCmd-SetUnitCandidates(eV keV MeV GeV TeV); fGunEnergyCmd-SetDefaultUnit(MeV);然后在SetNewValue里这么取void MyMessenger::SetNewValue(G4UIcommand* cmd, G4String newValue) { if (cmd fGunEnergyCmd) { G4double e fGunEnergyCmd-GetNewDoubleValue(newValue); fGun-SetParticleEnergy(e); } }关键是必须调用GetNewDoubleValue而不是GetNewDoubleValue之外的手工解析。SetUnitCategory和SetUnitCandidates决定了用户在宏文件里能写哪些单位后缀如果你需要的单位不在候选列表里用户写了2.5 mrad会被拒绝或者被当成无单位数值处理这一类问题在实际使用中非常隐蔽。我踩过的一个坑某个自定义命令只设了SetDefaultUnit(mm)但忘了SetUnitCategory结果用户写5 cm时会走一条识别不出单位就用默认值的路径cm被当成了非法后缀整个字符串解析失败最后取到的是5也就是 5 mm。用户以为自己设了 5 cm实际是 5 mm比例差了 10 倍。查了两天才定位到是这个命令的配置问题。提示所有带单位的 UI 命令至少要配齐SetUnitCategory、SetUnitCandidates、SetDefaultUnit三项缺一项就可能出现静默的错误换算。3.3 打印、日志与 G4BestUnit 的正确用法直接G4cout energy打出来的是内部单位数值比如6000。这在调试时非常不友好特别是当你在排查为什么粒子能量不对的时候看到 6000 你根本判断不了是 6 GeV 还是 6000 MeV 还是别的什么。G4BestUnit就是干这个的。你需要在文件顶部引入G4UnitsTable.hh#include G4UnitsTable.hh G4cout Energy G4BestUnit(energy, Energy) G4endl; G4cout Length G4BestUnit(length, Length) G4endl;它会自动挑一个合适的量级把 6000 打成6 GeV把 0.000123 打成0.123 um。第二个参数是量纲类别名必须和单位表里注册的类别字符串完全一致Length、Energy、Time、Mass、Angle、Surface、Volume、Density这些。大小写敏感写错一个字母就会打出一串警告然后退回原始数值。我在每个项目的RunAction::BeginOfRunAction里都会固定加一段参数回显G4cout Configuration Summary G4endl; G4cout Target thickness : G4BestUnit(targetThickness,Length) G4endl; G4cout Target density : G4BestUnit(density,Volumic Mass) G4endl; G4cout Beam energy : G4BestUnit(gunEnergy,Energy) G4endl; G4cout Production cut : G4BestUnit(defaultCut,Length) G4endl;注意密度那行的类别名是Volumic Mass不是Density这是 Geant4 内部的命名习惯容易写错。这段日志的价值在于每次跑批或者复现结果之前你扫一眼这几行就能发现 99% 的配置错误比事后查三天强得多。3.4 自定义单位注册与单位表扩展有些领域有自己习惯的单位比如放射医学里的mCi、材料科学里的mg/cm2、天文里的Jy。G4UnitDefinition允许你把它们注册进全局单位表注册之后G4BestUnit就能识别UI 命令的候选单位里也能用。#include G4UnitsTable.hh // 在初始化阶段注册注意只能注册一次 new G4UnitDefinition(millicurie, mCi, Activity, 3.7e7/ns); new G4UnitDefinition(kilogauss, kG, Magnetic flux density, 0.1*tesla);四个参数依次是长名字、短符号、类别名、相对于内部单位的数值。第三个参数决定它归到哪一类如果这个类别之前不存在会被新建如果存在会追加到已有类别里。要特别注意的是同一个短符号不能重复注册重复注册会打印警告并且后注册的失效。我见过一个项目在多个初始化函数里各注册了一遍mCi结果只有第一次生效后面的都静默失败排查的时候完全看不出问题。还有一个细节注册的单位只在G4BestUnit选优时参与竞争。如果你注册了一个数值极大的单位G4BestUnit可能会选它来展示一个很小的量看起来反而别扭。单位表的选优逻辑是按数量级自动挑没有手动干预的接口所以注册前最好先想清楚这个单位的使用频率。4. 高频翻车场景与排查手册4.1 六类典型单位错误速查把我在实际项目里见过的问题归一下类无非这么几种错误类型典型写法实际含义后果裸数字当内部单位G4double t 2.5;2.5 mm作者以为是 cm差 10 倍角度忘记 degG4double a 30.;30 rad探测器朝向完全错乱复合单位手写拆分1e20/cm*cm*cm1e20数量级差 1000 倍用了无单位 UI 命令G4UIcmdWithADouble输入即内部值用户写5 cm静默变 5打印直接输出 G4doubleG4cout e内部单位数值调试判断失误外部文件单位未标注CSV 里写10未定义后处理阶段全盘错位这六类里破坏力最大的是前两类因为它们不产生任何报错和警告程序照跑结果照出只有物理结果不对。而最容易被忽视的是最后一类——模拟跑对了数据导出到下游做分析的时候分析脚本作者不知道原始单位自己猜了一个于是整条链路错位。4.2 一次密度配置导致的能谱异常排查实录说个具体的。有个模拟项目做的是低能光子在某金属靶片中的能量沉积靶片 20 cm × 20 cm × 1 mm材料密度按 2.7 g/cm³ 配置。跑出来的透射谱在低能端出现了一个不该有的鼓包沉积能量总量比理论估算高了一个数量级。第一反应是物理列表的问题换了三套物理列表、调了生产切值、把二次粒子全部关掉重跑鼓包还在。第二反应是几何检查了有没有重叠G4GeometryManager和CheckOverlaps都跑了没发现。第三轮才回到材料参数把密度打出来一看代码里写的是G4double density 2.7;这一行在内部单位下意味着 2.7 g/mm³换算成常用单位就是 2700 g/cm³比设计值高了一千倍。材料变得致密平均自由程缩短到原来的千分之一光子几乎全在表层就发生作用低能端的散射成分被显著放大于是出现了那个鼓包。问题定位之后改起来很简单G4double density 2.7*g/cm3;但这个案例值得记的地方在于排查顺序应该反过来。我当时先怀疑物理列表是因为物理列表影响大这个直觉但物理列表的错误通常表现为某些过程缺失而这里是所有的量级都偏了量级系统性偏移的第一嫌疑永远应该是单位或者数值本身而不是物理模型。后来我把这条写进了团队的排查手册先打参数再查物理。顺便说那次排查之后我加了一个断言在材料构造完之后检查密度是否在合理区间if (density 0.001*g/cm3 || density 30.*g/cm3) { G4Exception(MyMaterial::Build, MAT001, FatalException, Density out of physical range - check units!); }这个断言后来救过我两次都是别人改代码时漏了单位常量。物理范围校验比单位校验好写因为它不依赖你提前定义正确单位只需要知道这个材料不可能是这个密度。4.3 外部文件与下游工具的单位一致性这一块是最容易出系统性问题的因为它跨越了程序边界。GDML 文件是 Geant4 原生的几何描述格式规范里明确规定了长度用 mm、能量用 MeV刚好和内部单位一致所以读写 GDML 的时候不需要额外换算这是它设计得比较省心的地方。但要注意的是如果有人手工编辑过 GDML往里面塞了别的单位的数值Geant4 读进来会当作 mm 处理不会做任何校验。CAD 导入是另一个故事。主流的 CAD 交换格式默认单位通常是 mm跟内部单位一致但有些导出工具会按 cm 或者 inch 输出导入之后模型尺寸会整体放大或缩小。我的做法是导入之后立刻量一下模型包围盒跟设计图纸上的已知尺寸比对差得多就是单位问题一次比对三十秒比事后查三天强。ROOT 输出这一环最危险因为 ROOT 的TTree分支是纯数值不带任何单位元数据。你写tree-Branch(edep, edep, edep/D)edep 是内部单位 MeV分析脚本里就得知道这是 MeV。问题在于写脚本的人可能换了一茬新来的人默认它是 GeV 或者 keV结果全错。我的习惯是在写出的时候就把单位写进分支名或者写一个专门的元数据 treeG4AnalysisManager* man G4AnalysisManager::Instance(); man-CreateNtupleDColumn(edep_MeV); man-CreateNtupleDColumn(time_ns); man-CreateNtupleDColumn(posX_mm);分支名带单位后缀虽然啰嗦但省掉了几十封这个数到底是什么单位的邮件。如果单位是内部单位那就在文档里写死所有输出均为 Geant4 内部单位长度 mm、能量 MeV、时间 ns并且把这个约定放在项目 README 的第一段。4.4 单位相关的启动期自检清单我整理了一份可以在BeginOfRunAction里逐条过的检查表实际用下来能拦住绝大部分配置错误检查项检查方式触发动作几何尺寸量级G4BestUnit打印包围盒偏离设计值 10 倍以上告警材料密度区间断言不在 0.001–30 g/cm³ 报致命错粒子能量打印并比对物理列表适用范围超出范围告警生产切值打印并比对几何最小特征尺寸切值大于特征尺寸时告警角度参数打印并检查是否在 0–2π 内超过 2π 高度怀疑 deg/rad 混用时间窗口打印并与预期飞行时间量级比对偏差 3 个量级以上告警这张表的核心思路是不直接检查单位而是检查物理量的取值范围是否合理。因为单位错误的一个显著特征就是数值偏离常识量级而物理量的合理范围你是一定知道的。这比写一堆assert(x y*cm)有效得多。5. 把单位管理变成团队规范5.1 命名、注释与代码评审的约定单位这件事光靠个人自觉是不够的必须有团队层面的约定。我们踩了几年坑之后固定下来的规则大概这么几条第一变量名里不带单位后缀的就代表它是内部单位。thickness、energy、time这些名字默认就是 mm、MeV、ns。如果因为特殊原因存了别的单位的数值变量名必须带后缀比如thickness_cm、energy_keV。这条规则的价值在于读代码的人看到一个变量名立刻知道数值的含义不需要翻到定义处。第二裸数字只允许出现在三类地方0、1 这种数学常数明确经过注释说明的物理常数比如某个经验公式里的无量纲系数以及显式的换算除法比如value/MeV。其他任何地方出现裸数字代码评审直接打回。第三所有对外暴露的配置参数都必须走带单位的 UI 命令不接受无单位命令。这条在评审时最容易漏因为无单位命令写起来少两行但后果是使用者必须自己记住内部单位是什么。第四输出到文件的数据必须标注单位分支名带后缀或者写独立元数据文件。这套规则刚开始推行的时候有人嫌啰嗦说G4double t 2.5*mm;比G4double t 2.5;长不了多少但看着累。等到第一次因为单位问题重跑了一个星期的批处理就没人再抱怨了。5.2 三个可复用的自检手段除了启动期打印还有几个手段可以常态化使用。单元测试。给核心的几何构造和材料构造写测试输入固定参数输出关键数值跟期望值比对。单位错误在单元测试里几乎必现因为测试的期望值是你手算出来的物理量只要代码里少乘一个常量数值立刻对不上。这类测试写起来成本很低一个几何类的测试也就十几行。参数扫描的合理性校验。如果你在跑参数扫描比如扫描靶厚从 0.1 mm 到 10 mm可以在扫描脚本里加一道前置检查把每个参数值代入G4BestUnit打印人工扫一眼量级不对的立刻能看出来。这个检查成本几乎为零但拦住过好几次扫描范围写成了 0.1 到 10 米这种事故。结果回归对比。同一个配置在升级框架版本、改动物理列表、重构代码之后跑一个基准算例把关键输出跟历史记录比对。单位问题往往表现为整体性的数值偏移回归对比能立刻暴露。我一般会把基准算例的完整参数日志和输出摘要存档每次改动后 diff 一次。5.3 一组可以直接抄的换算示例最后给一组我日常用到的换算写法都是验证过数值正确的可以拿去对照// 长度 G4double a 25.*cm; // 内部值 250 G4double b 3.5*um; // 内部值 3.5e-3 G4double c 1.54*angstrom; // 内部值 1.54e-7 G4double d 2.*m; // 内部值 2000 // 面积与体积 G4double area 4.*cm2; // 内部值 400 G4double vol 2.*liter; // 内部值 2e6 // 密度 G4double rho1 2.7*g/cm3; // 内部值 0.0027 G4double rho2 1.2*mg/cm3; // 内部值 1.2e-6 // 能量与动量 G4double e1 511.*keV; // 内部值 0.511 G4double e2 1.5*GeV; // 内部值 1500 G4double p 3.*GeV/c_light; // 动量内部单位 MeV/c // 时间 G4double t1 100.*ps; // 内部值 0.1 G4double t2 2.*us; // 内部值 2000 // 角度 G4double th1 45.*deg; // 内部值 0.7854 G4double th2 1.2*rad; // 内部值 1.2 // 磁场 G4double B1 1.5*tesla; // 内部单位 tesla G4double B2 500.*gauss; // 换算成 tesla 内部值还有一类经常需要用到的反向换算也就是把一个内部单位的数值转回某个常用单位做展示或者输出G4double thickness 250.; // 内部值含义 250 mm G4double inCm thickness/cm; // 得到 25 G4double inMeter thickness/m; // 得到 0.25 G4double energy 1500.; // 内部值含义 1500 MeV G4double inGeV energy/GeV; // 得到 1.5这种除以单位常量的写法本质上是把单位常量当作一个比例因子来用比手写1e-3或者1000清晰得多也不会因为将来内部单位定义变化而失效。我在输出 CSV 或者填 ROOT 分支的时候基本都用这个写法。6. 一点个人经验这套单位系统用了这么多年我最大的体会是它的设计其实非常克制只做了统一内部单位 提供换算常量这两件事没有引入任何运行时开销也没有发明新的类型系统。它的强大完全建立在用户会正确使用常量这个前提上而它的脆弱也恰恰在这里——编译器不会拦你框架不会拦你只有物理结果会拦你而那时候往往已经过去很久了。所以我现在的做法是把单位检查前移到写代码的当下而不是等结果出问题再回溯。具体就是三点所有物理量赋值必带单位常量所有启动日志必打G4BestUnit所有物理量必做范围断言。这三件事加起来每天多花不到十分钟但省下来的排查时间是以周计的。还有一个小心得分享给刚开始写模拟的人如果你不确定某个量的内部单位是什么最可靠的办法不是查文档而是在代码里把它打印出来然后手工反推。比如你设了一个 5 cm 的长度打出来是 50那就说明内部单位是 mm打出来是 500那内部单位就是 cm 量级你就要警惕是不是哪里多乘了一个常量。这种打印—反推—验证的循环做几次内部单位体系就刻在脑子里了比看十遍文档管用。至于后续还能怎么扩展一个方向是给项目做一层薄薄的自定义参数层所有物理量在进入 Geant4 之前先过一遍自定义的带单位类型构造时强制要求单位后缀编译期就能拦掉裸数字。这个做法在小项目里略显重但如果你的模拟代码要长期维护、多人协作投入产出比是划算的。另一个方向是把单位元数据写进输出文件的头信息里让下游分析脚本自动读取彻底断掉这个数是什么单位的口头传承。