万花尺背后的数学:Python模拟摆线轨迹与参数可视化

📅 2026/8/14 9:03:18
万花尺背后的数学:Python模拟摆线轨迹与参数可视化
1. 项目缘起从玩具到数学的奇妙旅程几年前我在整理旧物时翻出了一个布满灰尘的铁盒子里面装着我童年时最着迷的几样玩具一个塑料圆盘几个带孔的小齿轮还有几支快没水的圆珠笔。这就是“万花尺”一个在八九十年代风靡一时的绘图玩具。当时我只是沉醉于它画出的那些复杂而绚丽的图案觉得像变魔术一样神奇。多年后当我以工程师和数学爱好者的视角重新审视它才发现这小小的玩具背后隐藏着一个迷人的数学世界——旋轮线、内摆线与外摆线的动力学。万花尺本质上是一个几何绘图仪。它通常由一个带齿的大圆环尺规和一个带齿的小齿轮转子组成小齿轮在大圆环内侧或外侧滚动通过齿轮上的孔插入笔就能画出各种曲线。这些曲线并非随意的涂鸦而是有严格数学定义的“摆线”。这个项目就是希望系统性地研究这些轨迹的性质它们由哪些参数决定为什么有些图案对称完美有些却杂乱无章能否预测并生成特定的图案这不仅是怀旧更是一次从直观现象出发深入探究经典几何与参数方程之美的实践。无论你是对数学可视化感兴趣的编程爱好者还是想寻找跨学科项目灵感的学生或是单纯被精美图案吸引的创作者都能从中获得启发和可直接复现的代码方案。2. 核心原理摆线家族的数学描述要研究万花尺的轨迹首先必须理解其背后的数学模型。万花尺画出的曲线属于“摆线”这个大家族。当一个小圆转子半径r在一个大圆尺规半径R内部或外部无滑动地滚动时小圆上某一点P即笔尖的位置距离小圆圆心为ρ所描绘出的轨迹就是我们要研究的曲线。2.1 参数方程推导这是整个研究的基石。我们建立坐标系将大圆的圆心设为原点O(0,0)。设小圆的圆心为C。关键在于描述C和点P随时间或滚动角度t的变化关系。1. 小圆圆心C的运动轨迹当小圆在大圆内侧内摆线滚动时C在一个半径为(R - r)的圆周上运动。其坐标是C_x (R - r) * cos(t)C_y (R - r) * sin(t)这里t不是时间而是连接大圆圆心O和小圆圆心C的连线转过的角度我们称之为“公转角度”。2. 点P相对于圆心C的位置点P在小圆上它相对于圆心C还在“自转”。由于是无滑动滚动小圆滚过的弧长等于大圆上对应的弧长。由此可以推导出小圆的自转角度θ相对于某条固定半径与公转角度t的关系为θ - (R/r) * t。负号表示滚动方向通常与公转方向相反内摆线情况。 因此点P相对于C的坐标为P_relative_x ρ * cos(θ φ) ρ * cos(- (R/r) * t φ)P_relative_y ρ * sin(θ φ) ρ * sin(- (R/r) * t φ)其中φ是初始相位角决定了笔孔相对于起始位置的角度。3. 点P的绝对坐标最终轨迹方程将C的坐标和P的相对坐标相加得到内摆线的参数方程x(t) (R - r) * cos(t) ρ * cos(- (R/r) * t φ) y(t) (R - r) * sin(t) ρ * sin(- (R/r) * t φ)对于小圆在大圆外侧滚动的外摆线方程变为x(t) (R r) * cos(t) ρ * cos((R/r) * t φ) y(t) (R r) * sin(t) ρ * sin((R/r) * t φ)注意这里的(R/r)是决定曲线是否闭合、是否具有对称性的关键比值必须是一个有理数。如果它是无理数笔尖将永远不会回到起点画出的是一条永不重复、填满整个环带的曲线。2.2 核心参数及其物理/几何意义理解每个参数如何影响最终图案是进行性质研究和艺术创作的前提。固定尺规半径R决定了绘图区域的整体尺度。它像是画布的边界。转子半径r与R共同决定了核心的滚动比k R / r。k是理解图案周期的钥匙。笔尖距离ρ笔孔到小圆圆心的距离。它控制着曲线的“振幅”或“尖锐度”。当ρ r时笔尖在小圆内部画出的是“短幅摆线”曲线更圆润当ρ r时笔尖恰在小圆边上是标准的摆线当ρ r时笔尖在小圆之外画出的是“长幅摆线”曲线会出现交叉和回环图案更为复杂华丽。滚动比k R / r这是最核心的参数。k的有理数形式k m / nm, n为互质整数直接决定了曲线的对称性和闭合性。n曲线在C点公转n圈后P点才会回到起始的相对位置。m曲线在C点公转n圈的过程中P点形成的“花瓣”或“尖点”的数量。例如k5/2则n2, m5。这意味着小圆公转2圈后图案闭合期间会形成5个尖点或花瓣。初始相位φ相当于旋转整个图案。它不改变图案的形状只改变其初始朝向。3. 轨迹性质的系统性探究与可视化实现掌握了数学模型我们就可以通过编程进行系统性的探究和可视化了。我选择使用 Python因为其matplotlib和numpy库非常适合进行科学计算和绘图。下面将拆解完整的实现流程。3.1 环境准备与基础函数编写首先确保你的 Python 环境安装了必要的库numpy,matplotlib。可以通过pip install numpy matplotlib安装。我们首先编写一个核心函数用于计算轨迹点。这个函数将封装上一节推导出的参数方程。import numpy as np import matplotlib.pyplot as plt def compute_spirograph(R, r, rho, phi0.0, k_rev10, points_per_rev1000, modehypo): 计算万花尺摆线轨迹坐标。 参数: R: 固定大圆尺规半径 r: 滚动小圆转子半径 rho: 笔尖到小圆圆心的距离 phi: 初始相位角弧度 k_rev: 小圆公转的圈数用于生成足够的点使图案闭合 points_per_rev: 每公转一圈采样的点数 mode: 模式hypo为内摆线小圆在内epi为外摆线小圆在外 返回: x, y: 轨迹点的横纵坐标数组 # 计算滚动比 k R/r k R / r # 根据模式选择参数符号 if mode hypo: # 内摆线 R_effective R - r sign -1 elif mode epi: # 外摆线 R_effective R r sign 1 else: raise ValueError(模式必须是 hypo 或 epi) # 计算总的角度t范围和对应的采样点 # 图案闭合所需的最小公转圈数 滚动比分母 n (当k为有理数时) # 这里用 k_rev 确保足够圈数后续分析会优化 t np.linspace(0, k_rev * 2 * np.pi, int(k_rev * points_per_rev)) # 核心参数方程 x R_effective * np.cos(t) rho * np.cos(sign * k * t phi) y R_effective * np.sin(t) rho * np.sin(sign * k * t phi) return x, y这个函数是后续所有研究的基础。参数k_rev先设一个较大的值如10圈以确保即使k是无理数也能画出足够长的曲线。对于有理数k我们可以通过计算精确的闭合圈数来优化。3.2 核心性质一闭合性与周期规律图案是否闭合、何时闭合是第一个要研究的性质。这完全由滚动比k R/r决定。1. 闭合条件如果k是一个有理数即k m / nm, n为互质正整数那么轨迹是闭合的。小圆公转n圈后笔尖恰好回到起始位置相对位置和绝对位置都相同。2. 最小闭合圈数计算我们需要一个函数当给定R和r时自动计算最小闭合所需的公转圈数n以及图案的尖点/花瓣数m。from fractions import Fraction from math import gcd def rational_approximation(k, tol1e-12): 将浮点数k近似为最简分数 m/n用于判断闭合性。 # 使用Fraction类进行高精度近似 frac Fraction(k).limit_denominator(1000) # 限制分母大小避免过于复杂的分数 m, n frac.numerator, frac.denominator # 检查近似误差 if abs(k - m/n) tol: return m, n, True # 是有理数或在误差内可视为有理数 else: return None, None, False # 无法用合理分母的有理数近似视为无理数 def get_closing_parameters(R, r): 计算闭合参数。 k R / r m, n, is_rational rational_approximation(k) if is_rational: # 计算最小公转圈数即分母n # 但需要注意当笔尖不在转子边缘(rho ! r)时闭合圈数可能受rho影响吗 # 根据参数方程闭合性只取决于k与rho无关。rho只影响形状。 closing_revolutions n cusps_or_lobes m # 判断m的奇偶性可以预测图案是否中心对称 is_symmetric (m % 2 0) # m为偶数时图案通常具有中心对称性 return { is_closed: True, closing_revolutions: closing_revolutions, cusps_or_lobes: cusps_or_lobes, symmetry_central: is_symmetric, k_rational: (m, n) } else: # 无理数k图案永不重复不闭合 return { is_closed: False, closing_revolutions: None, cusps_or_lobes: None, symmetry_central: None, k_rational: None }3. 可视化验证我们可以设计一个实验来验证这个规律。固定R100变化r使得k分别为5(5/1),5/2,5/3观察图案。def plot_closure_demo(): R 100 rho 40 cases [ {r: 20, label: k5 (m5,n1), revs: 1}, # k5/11圈闭合5个瓣 {r: 40, label: k2.5 (m5,n2), revs: 2}, # k5/22圈闭合5个瓣 {r: 60, label: k≈1.667 (m5,n3), revs: 3}, # k5/33圈闭合5个瓣 ] fig, axes plt.subplots(1, 3, figsize(15, 5)) for ax, case in zip(axes, cases): r case[r] revs case[revs] x, y compute_spirograph(R, r, rho, k_revrevs, points_per_rev500) ax.plot(x, y, b-, linewidth0.8) ax.set_aspect(equal) ax.set_title(case[label]) ax.grid(True, alpha0.3) # 标记起点和终点 ax.plot(x[0], y[0], ro, markersize8, labelStart) ax.plot(x[-1], y[-1], gs, markersize8, labelEnd) ax.legend() plt.tight_layout() plt.show() plot_closure_demo()运行这段代码你会清晰地看到当k5时图案公转1圈就闭合了起点和终点重合k2.5时需要2圈k≈1.667时需要3圈。并且它们都具有5个主要的“花瓣”结构这与m5相对应。实操心得在编程验证时points_per_rev每圈采样点数的设置很重要。如果设置过低画出的曲线会有明显的棱角不光滑设置过高则计算量增大。对于显示用途500-1000点每圈通常足够。但在进行精确的曲率或长度计算时可能需要更高的采样率。3.3 核心性质二对称性分析万花尺图案的对称性非常丰富主要包括旋转对称和镜像对称。1. 旋转对称性旋转对称的阶数即旋转多少度能与自身重合与m和n有关。如果m和n互质那么图案的旋转对称阶数通常是m。例如k5/2图案有5重旋转对称性旋转72度重合。如果m和n有公约数d则旋转对称阶数为m/d。例如k6/2即3/1实际对称阶数是3而不是6。2. 镜像对称性镜像对称性更复杂一些与m、n以及φ都有关。当m为偶数时图案可能具有关于原点的中心对称性即旋转180度重合这同时也意味着在某些方向上存在镜像对称轴。当ρ r标准摆线且k为整数时图案具有m条镜像对称轴通过原点和每个尖点。初始相位φ会旋转整个图案从而改变对称轴的方向但不改变对称性的存在与否。我们可以编写函数来检测对称性。一个简单的方法是生成图案后进行几何变换然后与原始图案比较重合度。def check_rotational_symmetry(x, y, m): 粗略检查m重旋转对称性。 if m 2: return False # 旋转角度 theta 2 * np.pi / m # 旋转矩阵 rot_mat np.array([[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]]) # 旋转所有点 coords np.vstack((x, y)) coords_rot rot_mat coords x_rot, y_rot coords_rot[0, :], coords_rot[1, :] # 计算旋转后的点集与原始点集的距离需要对齐这里简化处理 # 更严谨的做法需要点集匹配算法这里仅作示意性输出 # 我们可以观察旋转后的图案是否与原始图案视觉上重合 return x_rot, y_rot # 示例检查 k5/2 的5重对称 R, r, rho 100, 40, 35 x, y compute_spirograph(R, r, rho, k_rev2, points_per_rev500) # 正好画闭合的2圈 x_rot, y_rot check_rotational_symmetry(x, y, 5) fig, (ax1, ax2) plt.subplots(1, 2, figsize(12,5)) ax1.plot(x, y, b-, linewidth0.8) ax1.set_aspect(equal) ax1.set_title(原始图案) ax2.plot(x, y, b-, linewidth0.8, alpha0.5, labelOriginal) ax2.plot(x_rot, y_rot, r--, linewidth0.8, alpha0.8, labelRotated 72°) ax2.set_aspect(equal) ax2.set_title(旋转72度后叠加对比) ax2.legend() plt.show()如果红色虚线与蓝色实线基本重合则说明具有5重旋转对称性。3.4 核心性质三形状与参数ρ的关系参数ρ笔尖距离对图案形态的影响是直观而强烈的。它决定了曲线是“内摆线”、“标准摆线”还是“外摆线”此处“外”指笔尖在转子外非滚动模式。def plot_rho_effect(): R, r 100, 30 k R / r rho_values [15, 30, 45, 60] # rho r, r, r labels [fρ{rho} (ρr), fρ{rho} (ρr), fρ{rho} (ρr), fρ{rho} (ρr)] fig, axes plt.subplots(2, 2, figsize(10, 10)) axes axes.flatten() for ax, rho, label in zip(axes, rho_values, labels): x, y compute_spirograph(R, r, rho, k_rev6) # 多画几圈看趋势 ax.plot(x, y, b-, linewidth0.6) ax.set_aspect(equal) ax.set_title(label) ax.grid(True, alpha0.3) # 标注关键圆 circle_R plt.Circle((0,0), R, fillFalse, colorgray, linestyle--, linewidth0.5) circle_r_trace plt.Circle((0,0), R-r, fillFalse, colorgreen, linestyle:, linewidth0.5) # 圆心轨迹圆 ax.add_patch(circle_R) ax.add_patch(circle_r_trace) ax.set_xlim(-R*1.2, R*1.2) ax.set_ylim(-R*1.2, R*1.2) plt.tight_layout() plt.show() plot_rho_effect()观察图像你会发现ρ r曲线平滑始终位于转子圆心轨迹圆绿色虚线之内形态内敛。ρ r曲线出现经典的尖点星形尖角笔尖刚好划过固定大圆的内缘。ρ r曲线开始自我交叉形成华丽的环结和回环结构笔尖的轨迹会超出转子圆心的轨迹圆。ρ越大这些环结就越夸张图案也越复杂。注意事项当ρ远大于r时曲线可能部分超出固定大圆R的边界。在物理万花尺中这是不可能的因为笔会被尺规挡住。但在数学模型中它依然可以生成有趣的图案。这提醒我们数学模型是物理玩具的抽象和扩展。4. 高级研究与自动化探索工具掌握了基本性质后我们可以构建更强大的工具来自动探索参数空间发现有趣的图案甚至将其用于艺术创作或数学教育。4.1 参数空间扫描与“有趣图案”筛选手动调整R, r, ρ三个参数来寻找美观图案效率很低。我们可以编写一个脚本在一定范围内随机或规律地生成参数自动计算并绘制然后根据一些启发式规则如对称性高、闭合圈数少、曲线复杂但有序等筛选出“有趣”的图案。import random import os def generate_random_spirograph(save_diroutputs): 生成随机参数的万花尺图案并保存。 os.makedirs(save_dir, exist_okTrue) # 参数范围可根据需要调整 R_range (50, 150) # 为了得到有理数k让r从一组整数中选取R也取整数 possible_r [20, 25, 30, 35, 40, 45, 50, 60, 75] r random.choice(possible_r) R random.randint(max(r10, R_range[0]), R_range[1]) # 确保Rr # 让k为有理数的概率大一些使R是r的整数倍或简单分数倍 if random.random() 0.7: # 70%概率生成“规整”图案 factor random.choice([2, 3, 4, 5, 1.5, 2.5, 3.5]) R int(r * factor) # 防止R超出范围 R max(R_range[0], min(R, R_range[1])) rho random.randint(int(r*0.3), int(r*2.0)) phi random.uniform(0, 2*np.pi) mode random.choice([hypo, epi]) # 计算闭合参数 params get_closing_parameters(R, r) if params[is_closed]: revs_to_plot params[closing_revolutions] # 如果闭合圈数太多图案可能过于密集限制一下 if revs_to_plot 20: revs_to_plot 20 else: revs_to_plot 10 # 非闭合图案画10圈看看 # 计算轨迹 x, y compute_spirograph(R, r, rho, phi, k_revrevs_to_plot, points_per_rev300, modemode) # 绘制 fig, ax plt.subplots(figsize(6,6)) ax.plot(x, y, b-, linewidth0.7) ax.set_aspect(equal) ax.axis(off) # 隐藏坐标轴更美观 title_info fR{R}, r{r}, ρ{rho}, k{R/r:.3f}\n if params[is_closed]: title_info fClosed in {params[closing_revolutions]} rev(s), {params[cusps_or_lobes]} lobes else: title_info Not closed (or very long period) ax.set_title(title_info, fontsize9) # 保存文件 filename fspiro_R{R}_r{r}_rho{rho}_{mode}.png filepath os.path.join(save_dir, filename) plt.savefig(filepath, dpi150, bbox_inchestight) plt.close(fig) # 关闭图形避免内存累积 print(fGenerated: {filepath}) return filepath, params # 批量生成示例 for _ in range(20): generate_random_spirograph()这个脚本会生成一批图片到outputs文件夹。你可以通过观察总结出哪些参数组合容易产生星形、玫瑰形、齿轮形等特定图案。4.2 从轨迹到艺术线条样式与色彩映射单一的蓝色线条看多了会腻。我们可以引入色彩映射让图案根据绘图进度参数t或曲率变化颜色创造出更富艺术感的图像。def plot_with_colormap(): R, r, rho 120, 45, 60 # 生成数据t也返回用于着色 t np.linspace(0, 6*2*np.pi, 3000) # 画6圈 k R / r R_eff R - r x R_eff * np.cos(t) rho * np.cos(-k * t) y R_eff * np.sin(t) rho * np.sin(-k * t) fig, ax plt.subplots(figsize(8,8)) # 使用散点图每个点根据t值着色 # 使用viridis色彩映射t值归一化到[0,1] scatter ax.scatter(x, y, ct, cmaphsv, s0.5, edgecolorsnone, alpha0.8) ax.set_aspect(equal) ax.axis(off) # 添加颜色条表示进度可选 # plt.colorbar(scatter, axax, labelDrawing Progress (t)) plt.title(万花尺轨迹 - 渐变色效果, fontsize12) plt.show() plot_with_colormap()使用scatter并按照参数t着色可以得到彩虹色般的渐变效果清晰展示了笔尖的移动路径。cmap可以替换为plasma,twilight等其他色谱。4.3 物理约束与数学模型的差异在真实的万花尺玩具中存在物理约束这些约束在纯数学模型中是不存在的。认识到这些差异能让我们对模型的理解更深刻。齿轮齿数真实的万花尺依靠齿轮啮合。R和r实际上由齿数决定。设大齿轮齿数为Z_R小齿轮齿数为Z_r则滚动比k Z_R / Z_r。这保证了k一定是有理数因此物理万花尺画出的图案总是闭合的、周期性的。尺规与转子的实际形状尺规不一定是一个完整的大圆可能只是一段圆弧转子也可能不是圆形还有其他形状如三角形、方形带齿。这些会改变参数方程产生更多样的曲线。笔孔的物理限制笔孔的位置ρ是离散的、有限的通常ρ r笔在转子内部或边缘因此很少能画出ρ r那种复杂交叉的长幅摆线。滑动与误差实际使用中齿轮可能打滑笔可能有晃动这些都会引入误差使画出的曲线与理想数学模型有细微差别。实操心得在编写模拟程序时我最初忽略了齿轮齿数必须是整数这一物理约束导致模拟出的很多“有趣”图案在现实中根本无法用标准万花尺绘制。后来我修改了随机生成逻辑让R和r的比值尽可能接近简单的有理数如 3/2, 5/2, 7/3并优先使用整数齿数比这样产生的图案才具有物理可实现性研究也更有实践意义。5. 常见问题、排查技巧与扩展思路在实际研究和编码过程中会遇到一些典型问题。这里记录下我的排查经验和解决方案。5.1 图案不闭合或终点不重合问题描述理论上应该闭合的图案画出来的终点和起点没有精确重合。原因分析采样点数不足points_per_rev设置太低离散的点无法精确描绘连续曲线导致终点计算有误差。浮点数精度误差计算机中R/r可能是无限循环小数或无理数的近似值导致k在计算机中并非精确有理数。公转圈数k_rev不足如果k的分母n很大需要公转很多圈才闭合如果k_rev设置小于n则画不完一个完整周期。解决方案增加points_per_rev比如从 500 提高到 2000。使用Fraction库或直接指定整数齿数来确保k是精确有理数。例如用R120, r48即k2.5代替R100, r40。使用get_closing_parameters函数计算精确的闭合圈数n然后用n作为k_rev来绘图。5.2 图案失真或出现奇怪直线问题描述画出的图形有不应有的尖锐转折或直线段。原因分析rho远大于R或r在数学上没问题但图形可能超出合理范围或者由于采样问题导致连线错误。参数t的范围不是2π的整数倍对于闭合图案如果t的终点不是2π*n的精确倍数起点和终点不匹配plt.plot会直接连起来形成一条割线。解决方案检查参数范围确保rho在一个合理的尺度内例如对于内摆线rho最好小于R-r。确保计算t时终点是2 * np.pi * n其中n是整数闭合圈数。使用np.linspace(0, n*2*np.pi, num_points, endpointTrue)。5.3 性能优化绘制复杂高分辨率图案当需要绘制闭合圈数很多n很大或采样率很高的图案时计算和渲染可能变慢。优化计算使用numpy的向量化运算避免循环。如果只需要最终图像可以适当降低points_per_rev。人眼对高采样率的感知在达到一定阈值后收益很小。对于确定闭合的图案只计算一个完整周期n圈的点而不是固定k_rev10。优化渲染使用linewidth很小的线条如0.1-0.5。对于彩色渐变图使用scatter时s点大小参数要设得很小并关闭边缘edgecolorsnone。考虑使用更高效的渲染后端或者将图像保存为文件而不是交互显示。5.4 扩展思路超越经典万花尺研究透经典模型后可以尝试以下扩展这能极大丰富创作空间多级万花尺让一个转子在另一个转子上滚动再在更大的尺规内滚动。这需要嵌套参数方程能产生极其复杂的分形状图案。非圆齿轮使用椭圆齿轮或其他形状的齿轮。这需要用到更复杂的微分几何知识来描述滚动接触。三维万花尺将滚动从平面扩展到球面或其他曲面。笔尖在三维空间中划出的轨迹将是迷人的空间曲线。动画绘制过程将t从0到终点动态化制作出笔尖一步步画出图案的动画非常适合用于教学演示。与音乐结合将参数t映射到时间笔尖的坐标(x, y)或曲率变化映射为音高或音量将几何图形转化为声音实现“可视化的音乐”。这个项目从一个小小的玩具出发却通向了几何、编程、艺术交叉的广阔天地。我最深的体会是许多复杂的现象背后往往有一个简洁优美的数学模型。而编程是我们探索和验证这个模型最得力的工具。当你用代码让那些方程“活”过来看到屏幕上涌现出意料之中又充满惊喜的图案时那种纯粹的乐趣和成就感正是驱动我一直探索下去的动力。不妨从修改compute_spirograph函数中的一个参数开始看看下一个等待你发现的美妙图案是什么。