通过 3D 动画直观演示球形瓶内可滑动半圆机构在倾倒时防粘盖、回正时刮膏体的运动学过程。
```python
-*- coding: utf-8 -*-
"""
球形瓶防粘盖滑动半圆机构 · 数字化实施例(仿真验证程序)
============================================================
本程序对应专利交底书附图 3「运动时序仿真图」,用于验证:
(1) 瓶体倾倒时,半圆滑动件在重力分量作用下由夹层 13 滑出,
于瓶口内侧形成凹面朝上的屏障,阻断膏体 30 与瓶盖 16 接触;
(2) 瓶体回正并开盖时,半圆滑动件滑回夹层 13,
其赤道刮缘 22 将黏附于凹面的膏体 30 刮落回瓶腔。
部件编号与说明书一致:
10 球形瓶体 11 外壁 12 上半球内壁 13 环形夹层腔
14 下腔 15 瓶口 16 瓶盖 20 半圆滑动件
21 半圆球面主体 22 赤道刮缘 30 膏体
运行环境:Python 3.9+ / numpy / matplotlib
输出:sphere_bottle_slider.gif
"""
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation, PillowWriter
from mpl_toolkits.mplot3d import Axes3D # noqa: F401
中文字体(按系统已有字体回退)
plt.rcParams"font.sans-serif" = ["Noto Sans CJK SC", "SimHei",
"Microsoft YaHei", "PingFang SC", "DejaVu Sans"]
plt.rcParams"axes.unicode_minus" = False
======================================================================
1. 实施例几何参数(单位:归一化无量纲量,实物按比例放大)
======================================================================
R_OUT = 1.00 # 11 外壁半径
R_CAV = 0.96 # 14 内腔半径(壁厚 0.04)
R_SLD = 0.78 # 21 半圆球面主体半径
Z_RET = 0.16 # 20 收纳位(位于 13 夹层腔内)
Z_DEP = -0.50 # 20 展开位(位于 14 内腔下部)
CAP_R = 0.30 # 16 瓶盖半径
======================================================================
2. 运动时序(对应附图 3,横轴为时间 t / s)
======================================================================
FPS, T_END = 24, 9.0
def smoothstep(x):
"""S 型速度曲线,避免启停冲击"""
x = np.clip(x, 0.0, 1.0)
return x * x * (3.0 - 2.0 * x)
def timeline(t):
"""返回 (瓶体倾角 theta, 滑片轴向位置 z_off, 开盖系数 cap_open)"""
if t < 1.2: # ① 正立,滑片收纳
return 0.0, Z_RET, 0.0
if t < 2.8: # ② 倾倒,滑片滑出
k = smoothstep((t - 1.2) / 1.6)
theta = np.pi * k
zo = Z_RET + (Z_DEP - Z_RET) * smoothstep((t - 1.6) / 1.0)
return theta, zo, 0.0
if t < 5.2: # ③ 倒置保持,膏体被托住
return np.pi, Z_DEP, 0.0
if t < 7.0: # ④ 回正
k = smoothstep((t - 5.2) / 1.8)
return np.pi * (1.0 - k), Z_DEP, 0.0
if t < 8.6: # ⑤ 开盖 + 滑片回收刮料
k = smoothstep((t - 7.0) / 1.6)
return 0.0, Z_DEP + (Z_RET - Z_DEP) * k, smoothstep((t - 7.0) / 0.5)
return 0.0, Z_RET, 1.0 # ⑥ 复位
======================================================================
3. 膏体 30 的离散粒子动力学(简化的黏塑性流体模型)
======================================================================
class PasteSim:
"""以 N 个示踪粒子表征膏体,含重力、阻尼、器壁约束与滑片约束"""
def init(self, n=120, seed=20240607):
rng = np.random.default_rng(seed)
d = rng.normal(size=(n, 3))
d /= np.linalg.norm(d, axis=1, keepdims=True)
d *= 0.86 * np.cbrt(rng.random((n, 1)))
d:, 2 = -np.abs(d:, 2) - 0.05 # 初始堆积于瓶底
self.p = d
self.v = np.zeros((n, 3))
self._zprev = Z_RET
def advance(self, theta, z_off, dt=1.0 / 48.0, sub=2):
"""瓶坐标系下的时间推进(世界重力随瓶体旋转投影到瓶坐标)"""
R = rot_x(theta)
g = R.T @ np.array(0.0, 0.0, -1.0) # 瓶坐标下的重力单位向量
for _ in range(sub):
self.v += g * dt * 10.0 # 重力项
self.v *= 0.90 # 内摩擦阻尼
self.p += self.v * dt
self._wall() # 约束 1:瓶腔 14
if z_off < 0.10: # 滑片处于展开状态
self._slider(z_off) # 约束 2:滑片凹面 21
self._zprev = z_off
return self.p
def _wall(self):
"""器壁 11/14 的硬约束:粒子投影回球腔内并清除法向穿透速度"""
p, v = self.p, self.v
r = np.linalg.norm(p, axis=1)
over = r > R_CAV
if np.any(over):
n = pover / rover, None
pover = n * R_CAV
vn = np.einsum("ij,ij->i", vover, n)
vover -= np.maximum(vn, 0.0):, None * n
def _slider(self, z_off):
"""滑片 21 的凹面约束 + 回收时的刮落效应"""
p, v = self.p, self.v
rho = np.hypot(p:, 0, p:, 1)
mask = rho < R_SLD - 1e-9
zs = np.full(len(p), np.inf)
zsmask = z_off + np.sqrt(np.maximum(R_SLD ** 2 - rhomask ** 2, 0.0))
hit = p:, 2 < zs
if not np.any(hit):
return
phit, 2 = zshit # 投影到凹面
vhit, 2 = np.maximum(vhit, 2, 0.0) # 清除法向穿透速度
------ 刮料机理:滑片回程(z_off 增大)时,赤道刮缘 22 将膏体向轴心刮落
dz = z_off - self._zprev
if dz > 1e-6:
push = min(dz * 60.0, 2.5)
rh = np.hypot(phit, 0, phit, 1)
rhrh \< 1e-9 = 1.0
vhit, 0 -= phit, 0 / rh * push
vhit, 1 -= phit, 1 / rh * push
======================================================================
4. 三维几何与渲染
======================================================================
def rot_x(a):
c, s = np.cos(a), np.sin(a)
return np.array(\[1.0, 0.0, 0.0, 0.0, c, -s, 0.0, s, c])
def rot_apply(X, Y, Z, R):
"""将瓶坐标系网格整体旋转到世界坐标"""
shp = X.shape
P = np.stack(np.asarray(X).ravel(), np.asarray(Y).ravel(), np.asarray(Z).ravel())
Q = R @ P
return Q0.reshape(shp), Q1.reshape(shp), Q2.reshape(shp)
def sph(r, n=36, v0=0.0, v1=np.pi):
u = np.linspace(0.0, 2.0 * np.pi, n)
v = np.linspace(v0, v1, max(n // 2, 8))
uu, vv = np.meshgrid(u, v)
return r * np.sin(vv) * np.cos(uu), r * np.sin(vv) * np.sin(uu), r * np.cos(vv)
def hemi(r, zoff, n=40, m=12):
u = np.linspace(0.0, 2.0 * np.pi, n)
v = np.linspace(0.0, np.pi / 2.0, m)
uu, vv = np.meshgrid(u, v)
return r * np.sin(vv) * np.cos(uu), r * np.sin(vv) * np.sin(uu), r * np.cos(vv) + zoff
def stage_text(t):
if t < 1.2: return "① 正立 · 半圆滑动件 20 收纳于夹层 13"
if t < 2.8: return "② 倾倒 · 20 受重力分量滑出,凹面朝上形成屏障"
if t < 5.2: return "③ 倒置 · 膏体 30 滞留于凹面,不接触瓶盖 16"
if t < 7.0: return "④ 回正 · 膏体随 20 回落瓶腔"
if t < 8.6: return "⑤ 开盖 · 20 回程,刮缘 22 将膏体刮回瓶内"
return "⑥ 复位 · 半圆滑动件重新收纳于夹层,等待下次倾倒"
def render(ax, theta, z_off, sim, cap_open, t):
ax.clear()
R = rot_x(theta)
ax.set_xlim(-1.2, 1.2); ax.set_ylim(-1.2, 1.2); ax.set_zlim(-1.2, 1.2)
ax.set_box_aspect((1, 1, 1))
ax.view_init(elev=16, azim=-60)
ax.set_axis_off()
------ 11 外壁(透明球壳)
X, Y, Z = sph(R_OUT, 44)
ax.plot_surface(*rot_apply(X, Y, Z, R), color="#a8c8dd",
alpha=0.055, linewidth=0, shade=False, antialiased=True)
------ 14 内腔面
X, Y, Z = sph(R_CAV, 36, np.pi / 2, np.pi)
ax.plot_surface(*rot_apply(X, Y, Z, R), color="#9fbfd0",
alpha=0.10, linewidth=0, shade=False)
X, Y, Z = sph(R_CAV, 36, 0, np.pi / 2)
ax.plot_surface(*rot_apply(X, Y, Z, R), color="#9fbfd0",
alpha=0.045, linewidth=0, shade=False)
------ 13 环形夹层腔示意(虚线圈)
tt = np.linspace(0, 2 * np.pi, 64)
for dz in (0.0, 0.12):
ex = R_SLD * 1.03 * np.cos(tt)
ey = R_SLD * 1.03 * np.sin(tt)
ez = np.full_like(tt, Z_RET - dz)
ax.plot(*rot_apply(ex, ey, ez, R), color="#6f93aa",
lw=1.0, alpha=0.65, ls="--")
------ 20 半圆滑动件(21 主体)
X, Y, Z = hemi(R_SLD, z_off, 44, 14)
ax.plot_surface(*rot_apply(X, Y, Z, R), color="#e07b39",
alpha=0.52, linewidth=0, shade=True, antialiased=True)
------ 22 赤道刮缘
ex = R_SLD * np.cos(tt); ey = R_SLD * np.sin(tt)
ez = np.full_like(tt, z_off)
ax.plot(*rot_apply(ex, ey, ez, R), color="#8a3b0a", lw=2.4)
------ 30 膏体粒子
P = (R @ sim.p.T).T
ax.scatter(P:, 0, P:, 1, P:, 2, s=17, c="#f2e3b3",
edgecolors="#b89b45", linewidths=0.4, depthshade=True)
------ 16 瓶盖
if cap_open < 0.5:
X, Y, Z = sph(CAP_R, 26, 0, np.pi / 2)
ax.plot_surface(*rot_apply(X, Y, Z + 0.98, R), color="#4a4a4a",
alpha=0.55, linewidth=0, shade=False)
ax.text2D(0.02, 0.95, stage_text(t), transform=ax.transAxes, fontsize=10.5)
ax.text2D(0.02, 0.05, "theta=%4.1f deg z_off=%+.2f" % (np.degrees(theta), z_off),
transform=ax.transAxes, fontsize=9, color="#555555")
======================================================================
5. 主程序
======================================================================
def main(save="sphere_bottle_slider.gif"):
sim = PasteSim()
fig = plt.figure(figsize=(7.2, 7.2), dpi=100)
ax = fig.add_subplot(111, projection="3d")
frames = np.arange(int(FPS * T_END))
def update(fi):
t = fi / FPS
theta, z_off, cap = timeline(t)
sim.advance(theta, z_off)
render(ax, theta, z_off, sim, cap, t)
return (ax,)
anim = FuncAnimation(fig, update, frames=frames, interval=1000.0 / FPS, blit=False)
anim.save(save, writer=PillowWriter(fps=FPS))
print("已输出动画:", save)
return anim
if name == "main":
main()
```
结构演示与动态时序解析
· 阶段① 正立收纳:瓶体直立,半圆滑动件藏在环形夹层腔里,膏体安静地堆积在瓶底。
· 阶段② 倾倒滑出:瓶身倾斜时,重力分量把半圆滑动件从夹层拉出来,凹面朝上形成物理屏障。
· 阶段③ 倒置阻挡:膏体粒子被滑片凹面托住,不会流到瓶盖位置。
· 阶段④ 回正回落:瓶体回正后,膏体随滑片一起落回瓶腔。
· 阶段⑤ 刮料回收:打开瓶盖,滑片沿轴向滑回夹层,赤道边缘把黏在凹面上的膏体刮回瓶内。
· 阶段⑥ 复位待用:滑动件重新收纳,等待下次倾倒。
禁止一切转载商用。