简介:一份面向混沌系统研究与教学的三阶动态系统仿真代码包,集合了Lorenz(洛伦茨)吸引子、Rossler(罗斯勒)吸引子、Chua(蔡氏电路)、Rikitake、Duffing映射以及Nose-Hoover振荡器等经典模型。虽然标题中带有Matlab字样,实际实现则基于Python 3,并依赖NumPy、SciPy、Pandas和Matplotlib等科学计算库,能够绘制三维相图、频谱图并生成动态演示,适合高校物理、电子与自动化专业的高年级学生和科研人员用于课堂演示、课程设计及课题复现。代码包共38个文件,整体大小为3.74MB,以Python源码(20个py文件)为主体,同时包含10张PNG三维/频谱结果图、1个GIF动态图、Dockerfile、依赖清单、README说明以及覆盖主要模型的测试脚本。源码结构上将动态系统建模、数值求解与可视化绘制分开,测试用例可帮助初学者验证Lorenz和Chua等模型的正确性;由于遵循GNU GPL 3.0开源协议,使用者还可依据自身需要修改参数或扩展新的混沌模型。目前已有1404人学习或下载,是一套便于快速上手、可复现的混沌系统仿真工具箱。
1. 蔡氏电路和这几只“蝴蝶”,到底要我们仿真什么
如果你下载过以 chaospy 为名的这套 MATLAB 仿真代码资源,大概率是想把 Lorenz、Rossler、Rikitake 这类混沌吸引子跑出图,再顺手把蔡氏电路的双涡卷画出来。很多开发者拿到这种资源后第一反应是直接点运行,结果要么是满屏 NaN,要么是轨迹跑到无穷远,要么是画出来的图跟资源配图长得完全不像。问题往往不在代码写错,而在混沌系统对初值、步长、求解器这几个变量出奇地敏感。
这篇文章我就按这套代码资源实际要解决的问题来讲:先立住三个经典混沌系统的数学模型和参数套路,再给出能在 MATLAB 里直接跑通的 Lorenz、Rossler、Rikitake 最小脚本,然后单独拆开蔡氏电路的分段线性建模,最后把初值敏感、瞬态污染、Lyapunov 指数验证这些坑逐个讲透。适合正在做混沌电路教学演示、保密通信预研,或者需要一张能放进论文的吸引子相图的开发者。
2. 三种混沌吸引子的数学底子:方程、参数与初值
混沌仿真看着是画图,其实是在解一组常微分方程。Lorenz、Rossler、Rikitake 这三个系统全部是三维自治 ODE,也就是说方程右边不显含时间 t。你不需要学会推导它们从哪来,但必须知道你正在积分的是什么,因为每个系统的参数都有固定的物理背景或动力学含义,参数稍微偏离混沌区间,画出来的就不是蝴蝶而是衰减曲线或极限环。
2.1 Lorenz 系统:三个方程为何能做到“不可预测”
Lorenz 系统的标准形式是三方程耦合:
dx/dt = σ(y - x) dy/dt = x(ρ - z) - y dz/dt = xy - βz
这里的核心参数是 σ、ρ、β。σ 是普朗特数,ρ 是瑞利数的归一化量,β 是几何尺寸相关的比例。最经典的一组参数是 σ=10、ρ=28、β=8/3,这套参数让系统落入混沌区。初值我一般取 (1,1,1),实际上只要不是恰好落在某个不稳定的平衡点上,取什么初值最终都会收敛到同一个吸引子形状,但轨迹的具体走位完全不同。
很多人不理解“确定但不可预测”这个说法:方程完全确定,给定初值给定步长,理论上轨迹是唯一的。但两个相差 10 的负十六次方的初值,积分到几十秒后轨迹就会完全错开,这就是初值敏感性。所以你用 ode45 跑两次 Lorenz,哪怕初值只差 1e-15,画出来的蝴蝶骨架一样,但同一时刻的点位置差很远。这不是求解器坏了,而是混沌系统的本质。
2.2 Rossler 与 Rikitake:参数套路完全不同
Rossler 系统是比 Lorenz 更“省料”的混沌系统,它只有一个非线性项 z(x-c),其他两项几乎是线性的:
dx/dt = -y - z dy/dt = x + ay dz/dt = b + z(x - c)
经典参数是 a=0.2、b=0.2、c=5.7,初值我一般给 (0.1,0.1,0.1)。这个系统进入混沌的机制是拉伸和折叠交替发生:z 变量在 x>c 时快速增长,把轨迹推离平面,然后又折回到原点附近。这里有个容易踩坑的地方:c=5.7 是临界值附近,如果你不小心把 c 写成 5.6,系统可能还在混沌,但写成 5.5 就退化成周期轨了。所以 Rossler 这个系统用参数扫描做分岔图时,c 的步长要取到小数点后两位。
Rikitake 系统来自双盘发电机的动力学模型,它在数学形式上更像蔡氏电路,容易产生类似双涡卷的轨迹。常见写法是:
dx/dt = -μx + yz dy/dt = -μy + x(z - a) dz/dt = 1 - xy
这里 μ 和 a 是控制参数。我在复现时习惯先取 μ=1、a=0.5 试跑,轨迹会围绕两个不稳定的焦点绕圈,形成一对涡卷。Rikitake 的收敛区间比 Rossler 更挑剔,不同文献给出的 μ 和 a 差异不小,如果轨迹直接发散到无穷,优先把 μ 调到 1.2 左右,或者把 a 往 0.3~1.0 区间里挪,再观察相图是否被限制在有限范围内。
3. 用 MATLAB 把 Lorenz / Rossler / Rikitake 逐个跑起来
这一章给你的都是可以直接复制进 .m 文件的最小实现。我不建议把三个系统塞进同一个脚本里用 switch 切换,因为后续做参数扫描、庞加莱截面时会改起来很难看。一个系统一个函数加一个主脚本,是这套东西最舒服的组织方式。
3.1 最小可运行脚本:Lorenz 吸引子 20 行内画出来
先写系统函数,注意 MATLAB 的 ODE 函数签名必须包含 t 作为第一参数,即使方程不显含 t 也要留位置。
function dx = lorenz_system(t, x, sigma, rho, beta) % 输入t为时间自变量,x为三维状态向量,sigma/rho/beta为系统参数 dx = zeros(3,1); dx(1) = sigma * (x(2) - x(1)); dx(2) = x(1) * (rho - x(3)) - x(2); dx(3) = x(1) * x(2) - beta * x(3); end这段代码没有任何技巧,就是把方程逐行搬进向量。接下来是主脚本:
sigma = 10; rho = 28; beta = 8/3; x0 = [1; 1; 1]; tspan = [0 50]; [t, X] = ode45(@(t,x) lorenz_system(t,x,sigma,rho,beta), tspan, x0); figure; plot3(X(:,1), X(:,2), X(:,3), 'LineWidth', 0.5); xlabel('x'); ylabel('y'); zlabel('z'); view([45 30]);参数说明:tspan 取 0 到 50 足够了,Lorenz 在这个时间范围内能把蝴蝶翅膀绕很多圈;再长只会让线条更密,读图反而费劲。初值 (1,1,1) 落在混沌吸引子的收敛域内,不需要预热时间。LineWidth 设成 0.5 是为了让轨迹看起来像细线,而不是一团黑。view([45 30]) 是调三维视角,默认视角下蝴蝶的立体感出不来。
3.2 Rossler 与 Rikitake 的 ode45 函数与画图调整
Rossler 的系统函数同样直接搬运方程:
function dx = rossler_system(t, x, a, b, c) dx = zeros(3,1); dx(1) = -x(2) - x(3); dx(2) = x(1) + a * x(2); dx(3) = b + x(3) * (x(1) - c); end主脚本参数取 a=0.2、b=0.2、c=5.7,初值 (0.1,0.1,0.1),tspan 依然用 [0 50]。画图时我建议把颜色跟时间绑定,让轨迹从深色渐变到浅色,这样能直观看到系统在吸引子上的运动顺序:
a = 0.2; b = 0.2; c = 5.7; x0 = [0.1; 0.1; 0.1]; [t, X] = ode45(@(t,x) rossler_system(t,x,a,b,c), [0 50], x0); figure; col = t / max(t); scatter3(X(:,1), X(:,2), X(:,3), 1, col, '.'); xlabel('x'); ylabel('y'); zlabel('z');scatter3 的第四个参数 1 是点大小,用点而不是线,是因为 Rossler 吸引子的带状结构密集,连线后层次不清晰。颜色渐变能让你判断轨迹是先绕大圈还是先进小涡旋。
Rikitake 的系统函数按前面那组方程写:
function dx = rikitake_system(t, x, mu, a) dx = zeros(3,1); dx(1) = -mu * x(1) + x(2) * x(3); dx(2) = -mu * x(2) + x(1) * (x(3) - a); dx(3) = 1 - x(1) * x(2); end我用 μ=1、a=0.5 跑出来是一对近似对称的涡卷。如果你的图跟这个描述不一致,先把 a 改成 1、μ 改成 1.2 再试。Rikitake 的相图对初值也比较敏感,建议初值取 (0.5,0.5,0.5) 附近,不要从 (0,0,0) 起步,因为在原点附近 dz/dt=1,系统会被直接推走,形成很长的瞬态尾巴。
3.3 固定步长 RK4 与 ode45 什么时候必须换
上面三个系统用 ode45 都能跑,但 ode45 是变步长求解器,它返回的时间点不均匀。这对画图无所谓,对后续做分岔图、庞加莱截面、Lyapunov 指数计算就是麻烦:你要求“每隔固定时间采一个点”,变步长输出就不合适。另一个问题是混沌方程在吸引子的折叠处梯度变化大,ode45 会在这些位置不断缩小步长,导致单步误差累积得不可控。
我一般会在这时候换固定步长 RK4。下面这段循环可以直接替代 ode45,输出均匀时间点上的状态序列:
dt = 0.005; T = 0:dt:100; N = length(T); Y = zeros(3, N); Y(:,1) = [1; 1; 1]; sigma = 10; rho = 28; beta = 8/3; for k = 1:N-1 f1 = lorenz_system(0, Y(:,k), sigma, rho, beta); f2 = lorenz_system(0, Y(:,k) + dt/2*f1, sigma, rho, beta); f3 = lorenz_system(0, Y(:,k) + dt/2*f2, sigma, rho, beta); f4 = lorenz_system(0, Y(:,k) + dt*f3, sigma, rho, beta); Y(:,k+1) = Y(:,k) + dt/6 * (f1 + 2*f2 + 2*f3 + f4); end plot3(Y(1,:), Y(2,:), Y(3,:));注意:自治系统传给 RK4 的时间参数 t 其实用不到,我统一传 0 省事。dt=0.005 在大多数混沌系统上够用,Lorenz 用更小的步长结果肉眼几乎没差别。做参数扫描时如果嫌满,先把 dt 放到 0.01 试跑,确认相图形状不变再逐步加大。
4. 蔡氏电路 MATLAB 仿真:分段线性电阻是关键
蔡氏电路和前面三个系统最大的不同在于它来自真实电路:两个电容、一个电感、一个非线性电阻。这个非线性电阻的伏安特性是分段线性的,这让系统方程变成分段光滑,积分时会在折点处遇到导数突变。这也是为什么蔡氏电路仿真比 Lorenz 更容易出现步长相关的问题。
4.1 归一化状态方程与 chua_system 函数
蔡氏电路常见做法是先归一化,得到无量纲状态方程:
dx/dτ = α(y - h(x)) dy/dτ = x - y + z dz/dτ = -βy
其中 h(x) 是分段线性函数,描述非线性电阻的伏安特性:
h(x) = m1·x + 0.5(m0 - m1)(|x+1| - |x-1|)
这个 h(x) 在 |x|<1 时斜率是 m1,在 |x|>1 时斜率是 m0。两个斜率对应非线性电阻工作在两个不同的线性区,这是蔡氏电路产生双涡卷吸引子的根源。MATLAB 里写起来很直接:
function dx = chua_system(t, x, alpha, beta, m0, m1) % x = [x1; x2; x3] 对应归一化状态 (x, y, z) h = m1*x(1) + 0.5*(m0-m1)*(abs(x(1)+1) - abs(x(1)-1)); dx = zeros(3,1); dx(1) = alpha * (x(2) - h); dx(2) = x(1) - x(2) + x(3); dx(3) = -beta * x(2); end这个函数里 h 的计算是整段仿真的灵魂,abs 表达式的目的是在 |x|=1 处产生折点。如果你要改斜率或者改变折点位置,只需要替换 h 的表达式,不用动积分循环。
4.2 双涡卷吸引子的参数与三维作图
经典的一组双涡卷参数是 α=15.6、β=28、m0=-1/7、m1=2/7。这里提醒一下 m0 和 m1 的位置别写反了:|x|<1 的区域斜率是 m1=2/7,|x|>1 的区域斜率是 m0=-1/7。写反了就跑出单涡卷或极限环。主脚本用 RK4 固定步长:
alpha = 15.6; beta = 28; m0 = -1/7; m1 = 2/7; x0 = [0.1; 0; 0]; dt = 0.02; T = 0:dt:80; X = zeros(3, length(T)); X(:,1) = x0; for k = 1:length(T)-1 f1 = chua_system(0, X(:,k), alpha, beta, m0, m1); f2 = chua_system(0, X(:,k)+dt/2*f1, alpha, beta, m0, m1); f3 = chua_system(0, X(:,k)+dt/2*f2, alpha, beta, m0, m1); f4 = chua_system(0, X(:,k)+dt*f3, alpha, beta, m0, m1); X(:,k+1) = X(:,k) + dt/6*(f1 + 2*f2 + 2*f3 + f4); end figure; plot3(X(1,:), X(2,:), X(3,:), 'LineWidth', 0.5); xlabel('x'); ylabel('y'); zlabel('z');dt 取 0.02,时间上限取 80,目的是让轨迹在左右两个涡卷之间来回穿越多次。画出来后应该是两个对称的涡卷,中间用螺旋线连接。如果只看到一个涡卷,基本可以判定是瞬态数据混进来了或者某个参数写错。蔡氏电路对初值没有那么挑,但最好从 (0.1, 0, 0) 这种非零小扰动开始,让系统自然进入振荡。
4.3 从仿真回到物理电路:两个电容一个电感一个非线性电阻
归一化参数和实际元件值之间不是一一对应的,你需要一组量纲变换才能把 α、β 映射到 R、L、C 上。实际做法是:先确定非线性电阻的伏安特性斜率 m0、m1,再按时间尺度选择电容和电感。这个映射关系在教科书里常见版本是把 α 设成 C2/C1 相关的量,β 与 L 和 C 的谐振频率相关。
真搭建电路时要注意仿真里完全忽略的细节:电感有直流内阻,电容有等效串联电阻,非线性电阻需要运放和电阻搭成,它的折点电压不一定恰好在 ±1V。这些误差会导致实际电路的混沌区间比仿真窄,甚至完全看不到双涡卷。我的建议是先仿真确认参数,再搭电路,然后用示波器的 X-Y 模式看 v_C2 对 v_C1 的相图,和仿真图对照。不要指望第一次上电就能看到蝴蝶,混沌电路对噪声和元件公差相当敏感。
5. 混沌仿真翻车避坑:初值、步长、瞬态与“看起来像”的问题
写混沌仿真代码翻车是常态,这块我踩过的坑比写过的成功代码还多。下面五条按“现象→原因→解决”写,每一条都值得在换系统代码时检查一遍。
5.1 同一个 Lorenz 程序两次跑出不同轨迹,先别怀疑写错了
现象:同一份脚本,关闭 MATLAB 重新运行,轨迹走势大致相同,但细节不一样,甚至某一段明显错开。
原因:混沌系统的初值敏感性。虽然代码里写的初值完全相同,但每次运行时的内存布局、并行计算中的浮点运算顺序可能有细微差异,这个差异会被系统指数放大。这是混沌系统的本质,不是 Bug。
解决:先确认吸引子骨架不变,也就是整体结构依然是那双翅膀。如果只是轨迹细节不同,属于正常。若需要可复现的实验,改成固定步长 RK4,并把初值写成字面量而不是计算出来的值,同时设置rng没有意义,因为这里根本没用随机数,关键是固定求解步长。
5.2 轨迹飞出坐标范围直到 NaN,常见原因是步长失控
现象:蔡氏电路或 Rikitake 跑到一半,变量直接变成 Inf 或 NaN,绘图时坐标范围飙到 1e300。
原因:变步长求解器在分段折点处反复压缩步长,误差估计失真,导致某一步数值翻出吸引子;或者参数本身已不在混沌区间,系统事实上发散。
解决:先检查参数是否落在混沌区间,再用固定步长 RK4 跑一遍,把 dt 改小到原来的五分之一。蔡氏电路出现 NaN 时,我第一条排查就是看有没有把 m0 和 m1 接到 h(x) 的正确位置。如果都检查了还发散,把初值拉回小量区间,比如 (0.1, 0.1, 0.1)。
5.3 分岔图上一片噪声:把瞬态和稳态混在一起画
现象:扫参画分岔图,图上一片密密麻麻的点,看不到清晰的分岔结构。
原因:系统从初值出发后需要一段时间才能收敛到吸引子上,这段时间是瞬态。瞬态轨迹会根据初值不同散落到各处,把它画进分岔图,就成了噪声。
解决:采集时跳过前段数据。比如固定步长 RK4 里 dt=0.005,前 20 秒对应 4000 个点,要记录极值就从第 4001 点开始。我在实际脚本里会这样写:
z = Y(3, 4001:end); local_max = z(find(diff(z(1:end-1))>0 & diff(z(2:end))<0) + 1);这里4001:end就是丢弃前 4000 个瞬态点,后面再用相邻差分找出局部极大值。很多初画分岔图的人把 Y(:,1:end) 全用上了,得到的就是一条噪声带。
5.4 画出来的波形“很像周期”,但系统其实是混沌
现象:看某一变量的时域曲线,像是一段振荡再一段振荡,感觉有周期性。直接把图放进报告里,被问“这不就是个周期信号吗”。
原因:混沌信号有宽频带连续谱,但在有限时间尺度上看确实像准周期。只盯时域曲线做判断一定会误判。
解决:至少做两件事。第一,看相图,周期系统在相空间是一条闭合曲线,混沌系统是分形结构。第二,对轨迹做 FFT 看频谱,混沌信号频谱是连续谱而不是离散谱线。能用这两步确认,再下结论说是混沌,血泪经验告诉我这样写进报告才稳。
5.5 没算 Lyapunov 指数就下“混沌”结论,评审会拦你
现象:论文或项目报告里只说“相图是蝴蝶形状,所以系统混沌”,结果被追问最大 Lyapunov 指数是多少,答不上来。
原因:大于零的最大 Lyapunov 指数是混沌的定量判据,只靠相图像不行。没有这个数,任何严谨的审查都会认为你的“混沌”是玄学。
解决:常见做法是把状态方程和变分方程同时推进,每隔固定步数对变分矩阵做一次 QR 分解,累加 R 阵对角元对数,再除以总时间,就是最大 Lyapunov 指数。这个方向明确了之后,你可以自己写,不用依赖第三方工具箱。我一般算完指数后还会顺带记录庞加莱截面上的点分布,两者互相印证才敢下结论。
6. 从蝴蝶到应用:庞加莱截面与一条验证习惯
分岔图是扫描参数的粗看方法,庞加莱截面则是压缩维度的精确验证手段。对 Lorenz 系统,取 z=0 平面作为截面,记录轨迹穿过该平面时的 (x,y) 坐标,就能把三维流形降到二维映射。这样做的意义是:混沌吸引子的庞加莱截面通常呈现一条由密集点构成的带状分形结构,而周期系统的截面上只会留下有限个点。
tol = 0.05; % 允许的平面接近误差 idx = find(abs(X(3,:)) < tol); cross_x = X(1, idx); cross_y = X(2, idx); plot(cross_x, cross_y, '.', 'MarkerSize', 2);注意:同一平面会被轨迹从两个方向穿过,如果你想看单侧穿过,可以再加上X(3, idx-1) < X(3, idx)这类方向条件。庞加莱截面上的点越多,说明轨迹在吸引子上绕得越密,这也是判断吸引子维度的一个辅助手段。
我自己的习惯是做完仿真后固定做三件事:先是固定步长 RK4 重跑一遍,和 ode45 结果叠图对比;然后算庞加莱截面;最后确认参数落在混沌区间而不是边界上。某次我在模拟项目X里偷懒用了变步长求解器做分岔图,结果图上的临界参数点整整偏移了 0.3,排查了一整天最后发现是求解误差把分岔点“抹平”了。从那以后,凡是涉及参数扫描,我只信固定步长。做混沌仿真,别急着贴图,先让数据经得起这三问:初值是什么,步长多小,瞬态丢没丢。希望帮到你。
本文还有配套的精品资源,点击获取