ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

mx3tools:Python工具链实现Mumax3微磁学仿真批量调度与结果解析

mx3tools:Python工具链实现Mumax3微磁学仿真批量调度与结果解析

简介:mx3tools 是一套面向微磁学仿真研究者的 Python 工具集合,专为配合 Mumax3 运行与分析仿真而设计,适合已掌握基础微磁模拟、希望用脚本批量处理数据的科研人员与研究生使用。它并非通用软件包,而是聚焦于仿真流程中的实际痛点,例如将 simulation.out 目录下的多个 .ovf 文件批量读入 numpy 数组,方便后续统计与可视化。资源包共 20 个文件,以 12 个 py 脚本为核心,另含 ovf 示例数据、txt 说明、yaml 与 cfg 配置、md 文档及 license 等,压缩包约 8.55MB,结构紧凑。功能模块覆盖 ovftools、ioutil、plotutil、statutil、datautil、simrun 等,可支撑数据加载、绘图、统计与仿真运行等环节。目前已有 1521 人学习下载,读者可借此快速搭建可复用的微磁数据处理流程,减少重复编码,提升仿真分析效率。

1. 从脚本堆到工具链:mx3tools 到底解决了什么

如果你用 Mumax3 做过微磁学仿真,大概率经历过这个循环:手写.mx3脚本、命令行跑mumax3、等结果、再用 Python 读.ovf和table.txt、自己画图。单次仿真还好,一旦要扫参数——比如交换刚度Aex从 10pJ/m 扫到 15pJ/m、DMI 强度取五个值——脚本管理立刻变成灾难。mx3tools 就是冲着这个痛点来的:它是一套 Python 工具集,把 Mumax3 仿真的批量生成、执行调度、结果解析、可视化串成一条流水线。适合两类人:一是刚接触 Mumax3、不想在文件管理上翻车的新手;二是已经有一堆散落脚本、想收敛成可复用工作流的熟手。它不替代 Mumax3 本身,而是补上「Python 侧编排」这一层。

2. 环境搭建与最小可跑闭环:从安装到第一张磁化曲线

2.1 依赖关系与安装路径选择

mx3tools 本质是 Python 包,运行前提是系统里已经有可执行的mumax3。常见做法是分两步:先确认 Mumax3 本体能跑,再装 Python 侧工具。Mumax3 是 Go 编译的单文件二进制,放到PATH里即可;GPU 加速依赖 CUDA 驱动,这个必须在装 Python 包之前验证,否则后面所有报错都会指向错误方向。

# 第一步:确认 mumax3 本体可用 which mumax3 mumax3 --version # 第二步:确认 GPU 可见(Mumax3 依赖 CUDA) nvidia-smi # 第三步:安装 mx3tools pip install mx3tools

逻辑说明:which确认二进制在PATH中,--version确认能正常启动;nvidia-smi是判断 GPU 是否可用的第一道关卡,如果这里看不到设备,后面仿真会直接报 CUDA 相关错误。参数上,Mumax3 对 CUDA 版本有要求,常见做法是驱动版本不低于 CUDA 运行时要求,具体对照 Mumax3 官方说明。

提示:如果pip install报编译错误,先检查 Python 版本,mx3tools 一般要求 Python 3.8 以上,且部分依赖(如 numpy、matplotlib)需要预编译轮子。

2.2 用 Python 生成并提交一个仿真

mx3tools 的核心用法是「用 Python 对象描述仿真,再导出为.mx3脚本」。下面是一个最小示例:定义一个 200nm×200nm×10nm 的薄膜,加一个 skyrmion 初始态,跑 1ns。

from mx3tools import Simulation, Geometry, Material # 定义几何:200nm x 200nm x 10nm 薄膜 geom = Geometry(size=(200e-9, 200e-9, 10e-9), cellsize=(2e-9, 2e-9, 2e-9)) # 定义材料参数:坡莫合金典型值 mat = Material( Ms=800e3, # 饱和磁化强度 A/m Aex=13e-12, # 交换刚度 J/m Dind=1.5e-3, # DMI 强度 J/m^2 Ku=0.5e5, # 单轴各向异性 J/m^3 Ku1_axis=(0, 0, 1) ) # 创建仿真对象 sim = Simulation(geom=geom, material=mat, name="skyrmion_demo") # 设置初始磁化态:一个 skyrmion sim.set_initial_state("skyrmion", radius=50e-9, polarity=1) # 设置输出:每 10ps 存一次磁化,同时记录能量表 sim.add_output("magnetization", every=10e-12) sim.add_output("table", every=10e-12) # 设置求解器与运行时间 sim.set_solver("rk4", dt=1e-13) sim.run(duration=1e-9) # 导出为 .mx3 脚本 sim.export("skyrmion_demo.mx3")

逻辑说明:Geometry定义空间离散,cellsize决定网格精度,太小会爆显存,太大则丢物理细节;Material里Dind是界面 DMI,skyrmion 稳定存在需要它;set_initial_state是 mx3tools 封装的便捷方法,底层会生成对应的m.set或relax语句;add_output控制输出频率,every参数直接对应.mx3里的autosave和tableautosave。导出后,用命令行执行:

mumax3 skyrmion_demo.mx3

这一步会生成skyrmion_demo.out/目录,里面有.ovf文件和table.txt。

2.3 读取结果并画第一张图

仿真跑完后,mx3tools 提供解析接口,不用自己手写 OVF 解析器。

from mx3tools import load_table, load_ovf import matplotlib.pyplot as plt # 读取能量表 table = load_table("skyrmion_demo.out/table.txt") plt.plot(table["t"], table["E_total"], label="Total energy") plt.xlabel("Time (s)") plt.ylabel("Energy (J)") plt.legend() plt.savefig("energy.png") # 读取最后一帧磁化 m = load_ovf("skyrmion_demo.out/magnetization000100.ovf") m.plot_slice(z=0, component="mz", cmap="RdBu") plt.savefig("mz_slice.png")

逻辑说明:load_table返回的是字典式结构,键名对应table.txt的列名,常见的有t、E_total、E_exch、E_dmi等;load_ovf返回的对象带plot_slice方法,z=0表示取中间层,component指定分量。参数上,cmap="RdBu"适合看mz的正负分布,skyrmion 会呈现典型的红蓝同心圆。

3. 参数扫描与批量调度:把 50 组仿真压成一条命令

3.1 扫描逻辑的设计:为什么不用 shell 循环

很多人第一反应是写 bash 循环,改一个参数、跑一次、换个目录。问题是:参数组合一多,目录命名、结果收集、失败重跑全得手动管。mx3tools 的做法是把扫描定义成 Python 里的参数网格,每个组合生成独立仿真对象,再统一提交。

from mx3tools import Simulation, Geometry, Material, ParameterSweep geom = Geometry(size=(200e-9, 200e-9, 10e-9), cellsize=(2e-9, 2e-9, 2e-9)) # 定义扫描:Aex 取 3 个值,Dind 取 4 个值,共 12 组 sweep = ParameterSweep( base_material=Material(Ms=800e3, Aex=13e-12, Dind=1.5e-3, Ku=0.5e5), params={ "Aex": [10e-12, 13e-12, 16e-12], "Dind": [1.0e-3, 1.5e-3, 2.0e-3, 2.5e-3] } ) # 为每个组合生成仿真 sims = [] for i, mat in enumerate(sweep): sim = Simulation(geom=geom, material=mat, name=f"sweep_{i:03d}") sim.set_initial_state("skyrmion", radius=50e-9) sim.add_output("table", every=10e-12) sim.run(duration=1e-9) sim.export(f"sweep_{i:03d}.mx3") sims.append(sim)

逻辑说明:ParameterSweep接收基础材料和参数字典,内部做笛卡尔积;enumerate遍历时每个mat已经是改好参数的Material对象。参数上,Aex和Dind的单位分别是 J/m 和 J/m²,写的时候注意量级。导出后批量执行:

# 串行执行(简单但慢) for f in sweep_*.mx3; do mumax3 "$f"; done # 并行执行(按 GPU 数量决定并发数) ls sweep_*.mx3 | xargs -P 2 -I {} mumax3 {}

-P 2表示同时跑 2 个,具体数字取决于 GPU 显存和单仿真占用。常见做法是先跑一个测显存,再决定并发。

3.2 结果聚合:从 12 个 table.txt 到一张相图

批量跑完后,最烦的是把结果拼起来。mx3tools 提供聚合接口。

from mx3tools import aggregate_tables import pandas as pd # 收集所有 table.txt results = aggregate_tables("sweep_*.out/table.txt") # 提取每个仿真的末态能量和 skyrmion 半径 summary = [] for name, df in results.items(): final = df.iloc[-1] summary.append({ "name": name, "E_total": final["E_total"], "E_dmi": final["E_dmi"], "E_exch": final["E_exch"] }) summary_df = pd.DataFrame(summary) summary_df.to_csv("sweep_summary.csv", index=False)

逻辑说明:aggregate_tables用 glob 匹配多个table.txt,返回{仿真名: DataFrame}的字典;iloc[-1]取最后一帧,代表弛豫后的稳态。参数上,如果仿真没跑完就中断,最后一帧可能不是稳态,常见做法是检查t是否达到设定时长,或者看能量是否收敛。

3.3 用相图判断 skyrmion 稳定区

把summary_df和扫描参数对应起来,就能画相图。

import matplotlib.pyplot as plt import numpy as np # 假设 summary_df 已按扫描顺序排列 Aex_vals = [10e-12, 13e-12, 16e-12] Dind_vals = [1.0e-3, 1.5e-3, 2.0e-3, 2.5e-3] # 构造能量矩阵 E_matrix = summary_df["E_total"].values.reshape(len(Aex_vals), len(Dind_vals)) plt.imshow(E_matrix, aspect="auto", origin="lower", cmap="viridis") plt.xticks(range(len(Dind_vals)), [f"{d*1e3:.1f}" for d in Dind_vals]) plt.yticks(range(len(Aex_vals)), [f"{a*1e12:.0f}" for a in Aex_vals]) plt.xlabel("Dind (mJ/m^2)") plt.ylabel("Aex (pJ/m)") plt.colorbar(label="E_total (J)") plt.savefig("phase_diagram.png")

逻辑说明:reshape的行列顺序必须和扫描顺序一致,否则相图会错位——这是血泪经验,扫描时参数顺序和 reshape 顺序对不上,图看起来「有规律」但物理意义全错。参数上,origin="lower"让 y 轴从下往上增,符合常规相图习惯。

4. 避坑与排查:那些让仿真白跑的细节

4.1 现象:仿真启动即报 CUDA out of memory

原因:cellsize太小或几何尺寸太大,显存不够。Mumax3 的显存占用大致和网格点数成正比,200×200×10nm 用 2nm 网格是 100×100×5=5 万点,用 1nm 网格就变成 20 万点,显存翻四倍。

解决:先算网格点数,常见做法是控制在 GPU 显存的 60% 以内。如果必须高分辨率,可以分区域跑,或者用sim.set_solver里的demag选项调整退磁计算精度。

4.2 现象:table.txt 里能量震荡不收敛

原因:dt太大,RK4 求解器不稳定。微磁学里交换作用对应的特征时间很短,dt一般取 1e-13 到 1e-14 量级。

解决:把dt调小一个量级重跑,或者换用rk45自适应步长。注意rk45会动态调整步长,table的输出间隔仍然由every控制,不受影响。

4.3 现象:OVF 文件读出来全是零

原因:输出路径写错,或者add_output的every设得比总时长还大,导致一帧都没存。

解决:检查.out目录里有没有magnetization*.ovf文件;如果有但读出来是零,检查load_ovf的路径是否指向了table.txt而不是 OVF。常见做法是先用ls确认文件存在,再用load_ovf读第一帧测试。

4.4 现象:批量扫描时部分仿真结果缺失

原因:并行执行时 GPU 显存被多个进程争抢,部分进程被系统杀掉,但xargs不会报错。

解决:降低-P并发数,或者在每个仿真前加显存检查。更稳妥的做法是用mx3tools的调度接口,它会在提交前检查 GPU 可用显存。另外,跑完后用aggregate_tables时如果发现某个仿真缺失,检查对应.out目录是否完整。

4.5 现象:skyrmion 初始态设置后立刻消失

原因:radius太小或Dind不够强,skyrmion 无法稳定。skyrmion 的稳定存在需要 DMI 和交换作用、各向异性之间满足一定条件。

解决:先跑一个弛豫步骤,用sim.relax()让系统找到能量极小态,再开始动力学。或者增大Dind到 2e-3 以上测试。参数上,radius一般取 30-80nm,太小会被网格离散化吃掉。

5. 进阶技巧:把 mx3tools 接进自己的分析流水线

5.1 自定义输出与后处理钩子

mx3tools 允许在仿真对象上挂后处理函数,每存一帧就调用一次。这个功能适合做实时监控,比如每 10ps 算一次 skyrmion 半径,不用等仿真跑完。

from mx3tools import Simulation, Geometry, Material import numpy as np def measure_radius(m): """从磁化场估算 skyrmion 半径""" mz = m[:, :, m.shape[2]//2, 2] # 取中间层 mz # 找 mz 过零点的位置作为边界 zero_crossings = np.where(np.diff(np.sign(mz[0, :])))[0] if len(zero_crossings) >= 2: return (zero_crossings[-1] - zero_crossings[0]) * 2e-9 / 2 return 0.0 sim = Simulation(geom=Geometry(size=(200e-9, 200e-9, 10e-9), cellsize=(2e-9, 2e-9, 2e-9)), material=Material(Ms=800e3, Aex=13e-12, Dind=1.5e-3, Ku=0.5e5), name="radius_monitor") sim.set_initial_state("skyrmion", radius=50e-9) sim.add_output("magnetization", every=10e-12) sim.add_hook(measure_radius, every=10e-12, output="radius.txt") sim.run(duration=1e-9) sim.export("radius_monitor.mx3")

逻辑说明:add_hook注册的回调函数接收当前磁化场,返回值写入radius.txt。参数上,every控制调用频率,和add_output独立。这个钩子在仿真进程内执行,所以不能太重,否则拖慢仿真。

5.2 用 Python 做参数拟合:从相图反推材料参数

如果你有一组实验测到的 skyrmion 半径,想反推Dind,可以用 mx3tools 跑扫描再拟合。

from mx3tools import ParameterSweep, Simulation, Geometry, Material import numpy as np from scipy.optimize import minimize_scalar def simulate_radius(Dind): """给定 Dind,跑一个短仿真返回 skyrmion 半径""" sim = Simulation(geom=Geometry(size=(200e-9, 200e-9, 10e-9), cellsize=(2e-9, 2e-9, 2e-9)), material=Material(Ms=800e3, Aex=13e-12, Dind=Dind, Ku=0.5e5), name="fit_tmp") sim.set_initial_state("skyrmion", radius=50e-9) sim.relax() sim.run(duration=1e-10) sim.export("fit_tmp.mx3") # 这里假设已执行并读取结果 return 50e-9 # 占位,实际从结果读 # 目标半径 60nm,反推 Dind result = minimize_scalar( lambda d: abs(simulate_radius(d) - 60e-9), bounds=(0.5e-3, 3.0e-3), method="bounded" ) print(f"Fitted Dind = {result.x:.3e} J/m^2")

逻辑说明:minimize_scalar做一维搜索,每次调用simulate_radius跑一个短仿真。参数上,bounds根据物理合理范围设定,太宽会浪费计算。这个流程适合参数少的情况,参数多时改用网格搜索加插值。

5.3 验证方法:用解析解交叉检查

跑完仿真后,常见做法是用解析近似验证。比如 skyrmion 半径和Dind的关系,在薄板近似下有经验公式。如果仿真结果和解析解偏差超过 20%,先检查网格精度和dt,再检查材料参数单位。

检查项常见错误验证方法
网格尺寸大于交换长度对比 1nm 和 2nm 网格结果
时间步长大于特征时间减半 dt 看能量是否收敛
DMI 符号正负写反看 skyrmion 手性是否与预期一致
输出频率太稀漏掉瞬态加密 every 看曲线是否平滑

从那以后我每次跑扫描前,都强制先用一个参数组合跑通全流程——生成、执行、读取、画图——确认闭环没问题再铺开。这个习惯帮我省了至少三次「跑了一夜发现输出路径写错」的后悔药。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进