gprMax devel分支中的重构:从过程式仿真程序到分层科学计算框架 📅 2026/7/4 9:59:25 目录1. 先看最明显的变化gprMax.py 不再负责一切2. 从“运行函数”到“运行上下文”3. MPI 不再只有一种含义4. 配置不再只是一个不断变化的 args4.1 SimulationConfig整次仿真的配置4.2 ModelConfig单个模型的配置5. Scene 的出现改变了模型表达方式6. 用户对象和内部对象被明确分成两个层次7. run_model() 被拆成了模型生命周期7.1 Scene用户想模拟什么7.2 Model计算机真正需要计算什么7.3 Solver使用什么方式推进计算8. 求解后端从条件逻辑变成策略对象9. Python API 从包装器变成了建模接口10. 日志系统不只是替换了 print()11. 用一张图看懂旧版架构12. 当前 devel 架构更像什么SimulationConfigContextModelConfigSceneUserObjectModelSolver13. 这是否属于架构级重构13.1 入口文件显著瘦身13.2 过程函数被生命周期对象替代13.3 模型表达和数值计算被分层13.4 输入文件不再是唯一模型表示13.5 并行机制被拆成不同层级13.6 求解后端通过统一接口替换13.7 配置按照生命周期拆分14. 但它不是一次完全推倒重写15. 这次重构解决了什么问题15.1 新运行模式更容易加入15.2 新后端更容易接入15.3 Python API 更适合工作流集成15.4 多模型状态更容易管理15.5 并行语义更加清晰15.6 用户建模和内部计算可以独立演进15.7 测试边界更加明确15.8 图形化和自动化工具更容易建立16. 这次重构体现了哪些设计模式16.1 模板方法16.2 策略模式16.3 工厂方法16.4 分层架构16.5 中间表示16.6 前端与后端分离17. 如何正确描述旧版与当前版本的关系18. 总结gprMax 正在从“程序”变成“框架”gprMax 当前devel分支真正发生变化的是系统开始用一种不同的方式描述仿真任务。旧版 gprMax 更像一个以入口函数为中心的仿真驱动程序。命令行参数、GPU 检测、模型循环、MPI 调度、基准测试和单模型执行都由少数几个过程函数串联起来。当前devel分支则逐步形成了另一种结构SimulationConfig ↓ Context ↓ Scene ↓ Model ↓ Solver配置、运行环境、用户场景、离散模型和数值后端开始拥有各自独立的职责。这意味着gprMax 正在从一个“由主函数驱动的仿真程序”迁移为一个“由多个具有明确边界的对象协作完成计算的科学计算框架”。本文将沿着这条变化主线拆解旧版和当前devel分支之间最重要的架构差异。1. 先看最明显的变化gprMax.py不再负责一切理解一次架构重构最直接的方法通常不是先看类图而是先看程序入口。因为入口文件反映了一个系统如何理解自己的职责。在旧版master分支中gprMax.py承担了大量工作包括命令行参数解析Python API 封装主机和 GPU 检测运行模式判断标准串行调度基准测试MPI Spawn 任务农场MPI No Spawn 任务农场Taguchi 优化任务分发多模型循环控制其主要调用关系可以概括为main() / api() ↓ run_main() ↓ run_std_sim() run_benchmark_sim() run_mpi_sim() run_mpi_no_spawn_sim() ↓ run_model()这是一种很典型的过程式组织方式。入口函数不仅负责接收用户参数还负责回答大量执行层问题当前应该运行几个模型从哪个模型编号开始使用 CPU 还是 GPU哪个 GPU 分配给哪个任务是否启用 MPIMPI 是动态派生进程还是使用已有进程模型应该以什么顺序执行每个模型何时开始何时结束换句话说旧版gprMax.py不只是入口。它同时还是配置中心、调度中心和执行控制中心。而在当前devel分支中gprMax.py已经明显收缩。它的核心职责基本可以归纳为三步接收 CLI 或 API 参数 ↓ 创建 SimulationConfig ↓ 选择 Context 并执行 context.run()核心逻辑接近config.sim_configconfig.SimulationConfig(args)ifconfig.sim_config.args.taskfarm:contextTaskfarmContext()elifconfig.sim_config.args.mpiisnotNone:contextMPIContext()else:contextContext()resultscontext.run()入口文件不再亲自管理模型循环也不再承担不同并行模式的完整执行逻辑。标准运行、MPI 空间分解和任务农场被移动到contexts.py等专门模块中。因此最直观的变化可以写成旧版 gprMax.py 入口 配置 调度 MPI 基准测试 模型循环 devel gprMax.py 入口 Context 选择入口文件变短本身并不能证明发生了架构重构。真正重要的是原本集中在入口中的职责被重新分配给了新的架构对象。2. 从“运行函数”到“运行上下文”旧版通过不同函数表示不同运行模式run_std_sim()run_mpi_sim()run_mpi_no_spawn_sim()run_benchmark_sim()run_main()根据命令行参数通过if/elif分支选择其中一个函数。这种模式的核心思想是每一种运行方式对应一套独立执行过程。标准仿真有自己的流程MPI 有自己的流程基准测试也有自己的流程。问题在于不同流程之间实际上存在大量共同生命周期开始仿真确定模型范围创建模型配置获取模型场景构建模型选择求解器执行计算保存结果结束仿真如果这些步骤分别写在多个函数中公共逻辑很容易重复差异逻辑则散落在不同条件分支里。当前版本使用显式的运行上下文来重新表达这些执行方式Context MPIContext TaskfarmContext标准上下文中的生命周期非常清晰defrun(self):self._start_simulation()foriinself.model_range:self._run_model(i)self._end_simulation()单模型执行则进一步拆解为model_configself._create_model_config(model_num)sceneself._get_scene(model_num)modelself._create_model()scene.create_internal_objects(model)model.build()solvercreate_solver(model)model.solve(solver)这里发生了一个重要变化。旧版通过一组并列函数表示不同模式run_std_sim() run_mpi_sim() run_mpi_no_spawn_sim()当前版本则通过类体系表示不同模式Context ├── MPIContext └── TaskfarmContext这不再只是“函数移动到另一个文件”。系统表达运行模式的方式已经发生改变。标准生命周期由父类定义不同上下文只需要覆盖特定步骤。例如MPIContext可以改变模型创建方式将普通Model替换为MPIModelTaskfarmContext则可以保留单模型内部生命周期只改变模型任务如何分发。这种结构带有明显的模板方法特征父类规定执行骨架子类替换局部步骤公共生命周期保持一致运行模式通过多态表达因此旧版和新版之间的变化可以概括为旧版 每种运行模式是一组独立过程函数 devel 每种运行模式是一种 Context 类型这是一项真正的架构变化因为系统从“条件分支驱动”转向了“对象协作与多态驱动”。3. MPI 不再只有一种含义并行机制的变化是理解这次重构时最容易混淆的部分。旧版中的-mpi主要表示 MPI 任务农场。其基本模式是模型 1 → worker 1 模型 2 → worker 2 模型 3 → worker 3每个 worker 负责一个相对独立的模型并分别调用run_model()。这种方式非常适合B 扫描多位置 A 扫描参数扫描多模型批量仿真优化问题中的多候选模型计算它的并行单位是“模型”。也就是说模型之间互不依赖可以被分配给不同工作进程。旧代码中的注释也将其直接描述为MPI task farm for models当前devel分支则将两类 MPI 并行明确拆开。第一类是--taskfarm它仍然表示模型级任务农场多个模型 ↓ 分配给多个 MPI worker第二类是--mpi x y z它表示模型内部的空间分解一个模型 ↓ 沿 x、y、z 方向划分空间 ↓ 多个 MPI rank 协同计算这两种并行方式解决的是完全不同的问题。任务农场关注的是如何同时运行更多模型空间分解关注的是如何让多个进程共同运行一个更大的模型因此旧版与新版之间不能简单写成run_mpi_sim() → MPIContext更准确的关系是旧版 run_mpi_sim() ↓ 新版 TaskfarmContext而新版MPIContext更接近新增或显著强化的模型内部空间分解能力。可以把两种并行方式放在一起比较并行方式并行单位适用任务Taskfarm多个独立模型B 扫描、参数扫描、多模型批处理MPI 空间分解单个模型中的子区域超大网格、单模型分布式求解这项拆分非常重要。旧版把“MPI”近似等同于“多个模型分发”。当前版本则开始明确区分模型级并行模型内部并行这种区分不仅体现在命令行参数上也体现在上下文类型和模型类型上。从架构角度看这意味着并行策略不再是一个模糊的全局开关而是被拆分为不同层次的执行概念。4. 配置不再只是一个不断变化的args旧版的运行状态主要保存在两个对象中args usernamespace其中args通常来自argparse.Namespace或者来自 API 临时构造的ImportArguments对象。下游函数直接读取和修改这些字段args.gpugpus[0]args.gpugpus args.n args.restart args.task这类设计在脚本式程序中很常见。它的优点是直接。任何函数拿到args都可以访问当前运行参数。但随着运行模式增加同一个字段可能逐渐承载不同含义。例如旧版中的args.gpu在不同阶段可能表示一个 GPU 设备编号多个 GPU 设备编号一个 GPU 对象多个 GPU 对象当前模型分配到的 GPU整次仿真可用的 GPU 集合字段名字没有变化但它所代表的状态会随着执行过程变化。这是一种典型的过程式共享状态。当前版本引入了两个生命周期不同的配置对象SimulationConfig ModelConfig4.1SimulationConfig整次仿真的配置SimulationConfig管理的是仿真级状态例如输入文件输出路径模型运行数量起始模型编号模型运行范围求解器类型可用计算设备MPI 分解维度是否启用任务农场日志级别日志文件进度条配置Scene 列表全局输出设置这些信息在整次仿真过程中通常保持稳定。4.2ModelConfig单个模型的配置ModelConfig则服务于某一次具体模型运行。它可能保存当前模型编号当前输出文件当前计算设备当前材料状态网格尺寸时间步长内存估算数值色散信息当前模型相关输出参数代码中的说明非常直接classModelConfig:Configuration parameters for a model. N.B. Multiple models can exist within a simulation 这句话揭示了两个不同层次一次 Simulation ↓ 包含多个 Model因此配置也被拆成SimulationConfig ↓ 管理整次仿真 ModelConfig ↓ 管理一次模型运行这种拆分并不是为了“让类更多”。它实际上是在回答一个重要的架构问题某个状态究竟属于整次仿真还是只属于当前模型旧版的很多状态都集中在一个可变args中。当前版本则开始按照生命周期管理状态。这会直接改善状态含义类型稳定性多模型运行并行执行测试可控性API 可维护性因此这一变化可以概括为旧版 一个 args 对象贯穿全局 devel SimulationConfig 管理仿真级状态 ModelConfig 管理模型级状态5.Scene的出现改变了模型表达方式如果只选择一个最能代表本次重构的对象Scene很可能是最关键的候选。旧版 gprMax 主要围绕输入文件运行。run_model()接收inputfile usernamespace随后解析输入文件中的命令建立网格、材料、几何体、源、接收器和输出对象。在这种架构中用户模型和.in文件语法高度绑定。可以近似理解为输入文件 ≈ 用户模型当前版本引入了显式的SceneScene用来保存用户创建的高层建模对象并按照职责分类self.single_use_objects self.grid_objects self.geometry_objects self.output_objects self.subgrid_objects这些分类不是简单的容器分组。它们反映了不同用户对象在模型构建过程中的作用single_use_objects只能出现一次的场景级命令grid_objects控制网格、时间窗或计算域geometry_objects描述材料分布和几何结构output_objects描述接收器和输出要求subgrid_objects描述子网格或多尺度结构Scene还负责检查必要对象是否存在验证对象之间的约束组织对象构建顺序建立网格相关对象处理几何对象处理子网格对象将用户对象转换为内部模型对象关键调用是scene.create_internal_objects(model)这个接口将两件过去高度耦合的事情拆开输入文件解析与模型场景表达当前流程更接近.in 输入文件 ↓ 解析为 UserObject ↓ 加入 Scene而 Python API 也可以绕过.in文件直接创建sceneScene()再调用run(scenes[scene])这意味着.in文件的地位发生了根本变化。旧版中它近似是模型本身。当前版本中它只是构造Scene的一种前端。可以把这种变化表达为旧版 输入文件是模型的主要表示 devel Scene 是模型的高层表示 输入文件只是 Scene 的一种来源这是一种典型的输入层与领域模型解耦。一旦Scene成为核心中间表示系统就可以支持更多前端.in 文件 Python API 图形化建模界面 参数化建模脚本 外部数据转换器 自动场景生成工具这些前端最终都可以汇聚到同一种对象模型Scene这正是科学计算程序向可编程平台演化时非常关键的一步。6. 用户对象和内部对象被明确分成两个层次Scene的引入还带来了另一个重要变化用户对象与计算对象之间的边界更加清晰。当前架构可以大致分成两个对象层次。第一层是面向用户的对象例如Domain Material Cylinder Box Receiver Source GeometryView这些对象关注的是物理语义参数命名输入合法性建模易用性API 可读性用户可以按照电磁建模概念创建对象而不必直接操作底层数组。第二层是内部计算对象例如网格材料数组 内部 Receiver 离散源对象 几何索引 更新系数 PML 数据结构 场分量数组 后端相关缓冲区这些对象关注的是数组索引内存布局数据类型并行访问计算效率后端兼容性两层之间通过类似下面的过程连接UserObject ↓ build() Internal Object或者从整体流程看Scene 中的用户对象 ↓ scene.create_internal_objects(model) ↓ Model 中的内部对象旧版当然也存在“输入命令转换为内部结构”的过程。任何仿真程序都必须完成这一步。区别在于当前版本将这个层次显式表达出来用户 API 使用UserObjectScene组织用户对象build()负责转换Model保存计算对象这使系统可以同时优化两个目标。用户层可以保持直观Cylinder(...)Receiver(...)Material(...)内部层则可以为计算效率服务连续数组 紧凑索引 预计算系数 后端专用数据结构这种分层在科学计算框架中非常常见。因为对用户友好的对象结构通常并不是对计算机最有效的对象结构。一个良好的架构不会强迫二者使用同一种表示。7.run_model()被拆成了模型生命周期旧版的核心单模型执行边界是run_model(...)它承担了单个模型的输入处理、模型构建和数值执行。从外部看整个过程近似是一个黑盒run_model() ├── 解析 ├── 构建 └── 求解随着项目复杂度增加这种统一函数会逐渐遇到问题。因为“解析”“构建”和“求解”属于不同层次。它们变化的原因也不同输入语法变化会影响解析新建模对象会影响场景构建网格结构变化会影响离散模型新硬件后端会影响求解器MPI 空间分解会影响模型和通信日志变化不应该影响数值算法当前版本将单模型运行显式拆解为获取 Scene ↓ 创建 Model ↓ Scene 创建内部对象 ↓ Model.build() ↓ create_solver(Model) ↓ Model.solve(Solver)对应代码路径接近model_configself._create_model_config(model_num)sceneself._get_scene(model_num)modelself._create_model()scene.create_internal_objects(model)model.build()solvercreate_solver(model)model.solve(solver)这些步骤看起来只是多了几个对象但它们代表不同的架构语义。7.1Scene用户想模拟什么Scene描述的是高层物理场景计算域网格设置材料几何体激励源接收器时间窗输出要求它是用户意图的表达。7.2Model计算机真正需要计算什么Model保存的是离散化后的计算结构网格尺寸材料编号数组电磁参数数组电场和磁场数组更新系数PML内部源内部接收器时间步进参数后端所需数据它是数值模型的表达。7.3Solver使用什么方式推进计算Solver负责执行时间推进和后端计算例如OpenMPCUDAOpenCLMetal它回答的是这个离散模型应该由哪一个计算后端执行因此当前架构的分层关系是Scene ↓ 描述物理问题 Model ↓ 描述离散计算问题 Solver ↓ 执行数值时间推进这比一个统一的run_model()更容易扩展。因为未来新增功能时可以更加准确地定位修改点新增几何对象修改用户对象和 Scene 构建新增离散策略修改 Model 构建新增计算后端实现新的 Solver新增运行方式实现新的 Context新增输入格式增加新的 Scene 构造前端这就是分层架构的实际价值。8. 求解后端从条件逻辑变成策略对象旧版 gprMax 的计算后端主要围绕OpenMP CPU CUDA GPUGPU 检测和设备选择在入口层完成再通过args.gpu传递给下游。随着硬件后端增多这种方式会让入口层逐渐充满设备相关条件逻辑。当前版本支持的后端更加广泛OpenMP CUDA OpenCL Metal命令行和 API 中分别提供了相应参数gpu opencl metal但更重要的变化不只是后端数量增加。关键在于求解器选择被收敛到统一接口solvercreate_solver(model)model.solve(solver)这个结构表明求解后端开始被当作可替换策略。从调用者角度看Context 并不需要了解每个后端的全部实现细节。它只需要根据 Model 创建 Solver ↓ 让 Model 使用 Solver 计算这种设计可以近似理解为策略模式Model ↓ 使用统一 Solver 接口 Solver ├── OpenMPSolver ├── CUDASolver ├── OpenCLSolver └── MetalSolver不同后端可以在内部处理自己的内存分配数据迁移核函数并行执行设备同步性能统计而上层模型生命周期保持不变Model.build() ↓ create_solver() ↓ Model.solve()这比在入口文件中写大量ifgpu:...elifopencl:...elifmetal:...更容易维护。它也说明 gprMax 的求解层正在从“内嵌条件分支”转向“可插拔后端”。9. Python API 从包装器变成了建模接口旧版 API 的典型形式是api(inputfile,n1,...)它的主要工作是将 Python 函数参数转换成一个类似命令行参数的临时对象然后进入与 CLI 相同的run_main()流程。因此旧版 API 的本质是用 Python 调用一个以输入文件为中心的程序。用户仍然需要先准备.in文件。Python 只负责启动仿真、传递运行参数和控制模型数量。当前版本中的 API 更接近run(scenesNone,inputfileNone,outputfileNone,n1,mpiNone,taskfarmFalse,gpuNone,openclNone,metalNone,...)这里最重要的变化是scenesNoneAPI 不再只能接收输入文件。用户可以直接在 Python 中创建多个Scene然后执行run(scenes[scene])这使 Python API 不再只是 CLI 的包装器而开始成为真正的建模接口。两种 API 定位可以对比为旧版 API Python → 参数包装 → 输入文件程序 devel API Python → Scene 对象 → 仿真框架这项变化的影响远大于“函数签名变了”。它意味着 gprMax 可以更自然地用于参数化模型生成批量实验优化算法机器学习数据生成不确定性分析仿真工作流自动化外部软件集成交互式建模Python 数据处理流水线例如过去一个参数扫描任务可能需要生成大量.in文件修改文本模板保存临时文件调用 gprMax再整理输出文件当前架构则更容易形成scenes[]forradiusinradii:sceneScene()scene.add(Cylinder(radiusradius,...))scenes.append(scene)resultsrun(scenesscenes)无论当前 API 的具体细节是否仍在演进架构方向已经非常明确.in文件不再是唯一的一等公民Scene对象正在成为核心建模接口。10. 日志系统不只是替换了print()旧版中大量运行信息通过print(...)直接输出。入口文件负责显示主机信息CPU 信息GPU 信息开始时间结束时间模型进度运行模式错误提示在单机脚本中这种方式通常足够。但在 MPI 和多后端环境中直接print()会产生很多问题。例如所有 rank 同时输出日志顺序混乱很难区分不同进程无法统一控制日志级别无法选择只记录 rank 0无法为每个 rank 写独立日志测试中难以捕获输出API 调用者难以控制输出行为当前版本引入了独立日志配置例如logging_config(...) logger.basic(...) logger.debug(...) logger.warning(...) logger.error(...)同时提供更细粒度的参数log_level log_file log_all_ranks show_progress_bars hide_progress_bars这些变化说明日志已经被视为独立基础设施而不是散落在业务代码中的输出语句。特别是在 MPI 环境中日志系统需要明确处理是否只显示 rank 0 是否记录所有 rank 不同 rank 是否写不同文件 进度条是否只由主进程显示 错误信息是否带进程上下文因此日志重构不是单纯的代码风格升级。它是 gprMax 从本地脚本式程序走向并行科学计算框架时必须补齐的工程基础设施。11. 用一张图看懂旧版架构旧版架构可以简化为CLI / Python API ↓ gprMax.py ├── 参数处理 ├── 主机检测 ├── GPU 检测 ├── 标准调度 ├── MPI 任务农场 ├── MPI No Spawn ├── benchmark ├── optimisation └── 多模型循环 ↓ run_model() ├── 输入解析 ├── 模型构建 └── 数值求解这个结构有一个非常明显的中心gprMax.py大量控制逻辑从入口向下分发。而单模型执行又集中在run_model()所以旧版可以理解为两个大型控制边界gprMax.py ↓ 负责整次仿真 run_model() ↓ 负责单个模型这种结构并不意味着旧版设计错误。对早期科学计算程序而言它具有明显优势实现直接调用链短易于快速增加功能便于研究代码迭代对核心开发者容易理解但随着功能增长入口文件会逐渐承担越来越多职责。最终运行模式、硬件后端、模型状态和输入解析会相互影响。此时继续增加条件分支会让系统扩展成本迅速上升。12. 当前devel架构更像什么当前devel分支可以概括为CLI / Python API ↓ SimulationConfig ↓ Context ├── Context ├── MPIContext └── TaskfarmContext ↓ ModelConfig ↓ Scene ↓ UserObject.build() ↓ Model.build() ↓ create_solver() ↓ Model.solve()这条链路中的每个对象都回答一个不同问题。SimulationConfig回答整次仿真要怎样运行Context回答多个模型和计算资源要怎样组织ModelConfig回答当前这个模型使用什么局部配置Scene回答用户想模拟什么物理场景UserObject回答用户如何描述材料、几何体、源和接收器Model回答这些物理对象如何转换成离散计算结构Solver回答离散模型由哪个后端执行这种结构的最大变化不在于类数量而在于职责边界。系统不再由一个入口函数“知道所有事情”。不同对象只处理自己所属的层次。13. 这是否属于架构级重构答案是肯定的。判断一次变化是否属于架构重构不能只看文件是否重命名函数是否移动类是否增加代码行数是否减少更应该看系统的核心职责边界是否被重新定义。在 gprMax 当前devel分支中至少有以下边界发生了实质变化。关注点旧版当前devel程序入口main()与api()cli()与run()全局配置松散的argsSimulationConfig单模型配置函数参数和共享状态ModelConfig执行模式run_*_sim()函数Context类体系用户模型表示以输入文件为中心显式Scene用户命令解析后参与构建分类的UserObject单模型执行集中的run_model()Model.build()与Model.solve()求解后端条件逻辑和参数传递create_solver()MPI主要表示任务农场空间分解与任务农场分离Python API输入文件调用包装器可直接传入Scene日志print()为主独立日志系统硬件后端OpenMP、CUDAOpenMP、CUDA、OpenCL、Metal从架构特征看这次变化同时具备以下典型信号。13.1 入口文件显著瘦身入口只负责参数接收、配置创建和运行上下文选择。13.2 过程函数被生命周期对象替代不同运行模式不再主要依赖平行函数和条件分支而是由 Context 类体系表达。13.3 模型表达和数值计算被分层Scene、Model和Solver分别承担用户建模、离散结构和后端求解。13.4 输入文件不再是唯一模型表示.in文件成为构造Scene的一种方式而不是唯一入口。13.5 并行机制被拆成不同层级任务农场负责模型级并行MPIContext 负责单模型空间分解。13.6 求解后端通过统一接口替换后端选择被收敛到create_solver()上层生命周期不再依赖大量设备分支。13.7 配置按照生命周期拆分仿真级状态和模型级状态分别由SimulationConfig和ModelConfig管理。这些都不是局部代码整理能够解释的。更准确的表述应当是gprMax 正在从以入口函数和run_model()为中心的过程式仿真程序迁移为以SimulationConfig → Context → Scene → Model → Solver为主线的分层科学计算框架。14. 但它不是一次完全推倒重写将这次变化称为架构重构并不意味着 gprMax 被从零重写。恰恰相反它更像一次围绕既有数值内核展开的渐进式迁移。底层大量能力仍然得到保留和延续例如FDTD 更新方程材料处理源接收器PML几何构建CUDA 加速输出文件结构既有数值工具一部分历史模块在迁移过程中也仍然能够看到历史结构留下的痕迹。例如#python输入块仍被保留但已经被标记为 deprecated某些调用堆栈中仍可能出现model_build_run.py新旧参数和兼容逻辑可能同时存在部分对象已经重构部分底层代码仍沿用原有组织模块命名可能仍保留历史语义这正是渐进式重构的典型状态。大型科学计算项目通常很难通过一次性重写完成架构迁移。原因很现实数值正确性必须保持已有用户模型不能轻易失效不同后端需要逐步迁移MPI 和 GPU 功能难以同时重写科研用户依赖旧输入格式底层优化代码具有较高验证成本因此更合理的方式是保留成熟数值内核 ↓ 逐步建立新对象边界 ↓ 迁移入口和调度逻辑 ↓ 迁移模型表达 ↓ 迁移求解后端 ↓ 逐渐废弃历史接口这也是为什么当前代码中会同时存在新架构和历史痕迹。它不是架构判断的反例。相反它说明项目正处于真实的软件演化过程中。15. 这次重构解决了什么问题将架构拆成多个对象之后收益并不只是代码看起来更整齐。它解决的是科学计算项目发展到一定规模后必然出现的扩展问题。15.1 新运行模式更容易加入过去增加一种运行方式可能需要修改入口分支参数解析模型循环GPU 分配输出管理错误处理当前架构中可以通过新增或扩展 Context 来表达新的执行模式。15.2 新后端更容易接入只要新后端能够满足 Solver 接口上层 Scene 和 Model 生命周期可以保持稳定。15.3 Python API 更适合工作流集成用户不必始终生成输入文件而可以直接构建 Scene。15.4 多模型状态更容易管理仿真级配置和模型级配置不再混在同一个可变对象中。15.5 并行语义更加清晰模型级任务农场和模型内部空间分解被明确区分。15.6 用户建模和内部计算可以独立演进用户对象可以追求可读性内部对象可以追求性能。15.7 测试边界更加明确可以分别测试配置解析Scene 验证UserObject 构建Model 构建Solver 选择Context 生命周期15.8 图形化和自动化工具更容易建立一旦 Scene 成为统一中间表示GUI、参数化脚本和外部转换工具就不必直接依赖.in文本语法。16. 这次重构体现了哪些设计模式不必强行给每一段代码贴设计模式标签但当前架构确实呈现出几个明显的软件设计特征。16.1 模板方法Context.run()定义总体生命周期开始仿真 ↓ 遍历模型 ↓ 运行单模型 ↓ 结束仿真子类只覆盖特定步骤例如模型创建或任务分发。16.2 策略模式不同 Solver 表示不同计算后端OpenMP CUDA OpenCL MetalModel 可以通过统一接口使用不同求解策略。16.3 工厂方法create_solver(model)根据模型配置和硬件参数创建适合的求解器。16.4 分层架构系统被拆成输入层 配置层 运行上下文层 场景层 离散模型层 求解器层16.5 中间表示Scene开始承担统一高层模型表示的作用。不同输入方式最终都转换为 Scene。16.6 前端与后端分离.in文件和 Python API 是建模前端。OpenMP、CUDA、OpenCL 和 Metal 是计算后端。Scene和Model位于二者之间。17. 如何正确描述旧版与当前版本的关系讨论 gprMax 的版本演化时需要避免几个过度简化的说法。第一种不准确说法是当前版本只是把gprMax.py拆成了多个文件。这忽略了 Context、Scene、Model 和 Solver 之间新的职责划分。第二种不准确说法是旧版 MPI 直接变成了新版 MPIContext。旧版run_mpi_sim()更接近当前TaskfarmContext而当前MPIContext主要表示单模型空间分解。第三种不准确说法是新版完全抛弃了旧版架构。实际上大量底层 FDTD 和物理建模能力仍然被继承。第四种不准确说法是新版已经完成了彻底重写。当前更像处于渐进迁移阶段新旧结构可能同时存在。一个更准确的总结是当前devel分支正在围绕既有 FDTD 数值内核进行渐进式架构迁移。重构重点不在底层算法重写而在上层模型表达、状态管理、运行调度、并行方式和多后端求解的重新组织。18. 总结gprMax 正在从“程序”变成“框架”旧版 gprMax 的核心组织方式是入口函数 ↓ 运行模式函数 ↓ run_model()当前devel分支正在形成的新组织方式是SimulationConfig ↓ Context ↓ Scene ↓ Model ↓ Solver这条变化主线可以用一句话概括旧版更关注“如何把一次仿真运行起来”当前架构则开始关注“如何用可扩展对象体系描述仿真、组织仿真并执行仿真”。因此这不是简单的文件拆分也不是一次普通代码清理。它是一次较明确的架构级重构。更准确地说gprMax 正在从以 gprMax.py 和 run_model() 为中心的过程式仿真驱动程序逐步转向以 SimulationConfig、Context、Scene、Model 和 Solver 为核心的分层科学计算框架底层 FDTD 数值能力仍然延续但上层的系统边界已经发生实质变化。对开发者而言这意味着未来理解 gprMax 时不应再只围绕“程序从哪个函数开始执行”来阅读代码。更有效的方式是沿着五个问题展开SimulationConfig整次仿真如何配置 Context模型和计算资源如何组织 Scene用户想模拟什么 Model物理场景如何离散化 Solver离散模型由什么后端执行当这五个问题被分开之后gprMax 的新架构也就变得清晰了。它不再只是一个能够运行.in文件的电磁仿真程序。它正在成为一个可以被脚本化、组合、扩展并嵌入其他科学工作流的电磁建模框架。