每日算法 — 使用java实现滑冰:摩擦力动力学模拟与最优轨迹控制

滑冰是一项结合艺术与物理精妙的运动。冰刀与冰面之间的极低摩擦系数使得运动员可以长时间滑行,而转弯时的离心力与侧向摩擦力之间的博弈则决定了轨迹的曲率。本文用Java实现二维滑冰动力学模拟,运用微分方程数值积分与最优控制理论,求解给定起点和终点之间的最速滑行轨迹。

一、滑冰物理模型

1.1 坐标系与状态变量

将冰面视为二维平面,运动员的状态由五元组描述:

[(x, y, v, \theta, \omega)]

其中 ((x, y)) 为位置,(v) 为速率,(\theta) 为滑行方向角,(\omega) 为角速度(转弯速率)。

1.2 受力分析

冰刀与冰面接触时存在两种摩擦:

  • 纵向摩擦:与滑行方向相反,大小为 (f_l = \mu_l \cdot N),其中 (\mu_l \approx 0.01) 为动摩擦系数
  • 侧向摩擦:提供转弯所需的向心力,最大值为 (f_{s,max} = \mu_s \cdot N),(\mu_s \approx 0.3) 为侧向摩擦系数

转弯时,侧向摩擦力充当向心力:(f_s = m \cdot v^2 / R = m \cdot v \cdot \omega)。当所需向心力超过最大侧向摩擦力时,运动员将失控滑倒。

1.3 运动方程

[\dot{x} = v \cos\theta, \quad \dot{y} = v \sin\theta]

[\dot{v} = -\mu_l \cdot g + a_{push}]

[\dot{\theta} = \omega, \quad \dot{\omega} = \tau / I]

其中 (a_{push}) 为蹬冰产生的推进加速度(间歇性),(\tau) 为身体倾斜产生的转向力矩,(I) 为转动惯量。

二、核心算法设计

2.1 RK4数值积分

滑冰运动方程属于常微分方程组,由于存在非线性项(如转弯时的方向变化),采用四阶龙格-库塔法(RK4)进行高精度数值积分。相比欧拉法,RK4的误差为 (O(dt^4)),允许使用更大的时间步长。

2.2 最优轨迹搜索

给定起点 ((x_0, y_0))、终点 ((x_f, y_f)) 和初始方向 (\theta_0),最速滑行轨迹问题可建模为:在状态方程约束下,最小化到达时间 (T)。本文采用直接打靶法(Direct Shooting):将控制量(转向角速度 (\omega(t)))参数化为分段常数函数,通过模式搜索(Pattern Search)优化这些参数,使得最终位置尽可能接近目标。

2.3 碰撞检测与边界约束

冰场通常为矩形区域,需要检测运动员是否越界。此外,在短道速滑场景中,还需检测是否与其他选手发生碰撞(圆形包围盒检测)。

三、完整Java实现

import java.util.ArrayList;
import java.util.List;

/**
 * 滑冰动力学模拟与最优轨迹控制系统
 * 运用RK4数值积分与模式搜索优化
 */
public class IceSkating {

    // 物理常量
    private static final double G = 9.8;          // 重力加速度
    private static final double MU_LONG = 0.015;  // 纵向摩擦系数
    private static final double MU_LAT_MAX = 0.35; // 最大侧向摩擦系数
    private static final double MASS = 70.0;      // 运动员质量 (kg)
    private static final double INERTIA = 8.0;    // 转动惯量 (kg·m^2)
    private static final double MAX_PUSH = 4.0;   // 最大蹬冰加速度 (m/s^2)
    private static final double MAX_TORQUE = 25.0; // 最大转向力矩 (N·m)
    private static final double MAX_OMEGA = 2.5;   // 最大转弯角速度 (rad/s)

    // 冰场边界
    private final double rinkX, rinkY; // 冰场半宽、半长

    public IceSkating(double rinkWidth, double rinkLength) {
        this.rinkX = rinkWidth / 2;
        this.rinkY = rinkLength / 2;
    }

    /** 系统状态 */
    public static class State {
        double x, y, v, theta, omega;
        double t;
        State(double x, double y, double v, double theta, double omega, double t) {
            this.x = x; this.y = y; this.v = v;
            this.theta = theta; this.omega = omega; this.t = t;
        }
        State copy() {
            return new State(x, y, v, theta, omega, t);
        }
    }

    /** 控制输入 */
    public static class Control {
        double aPush;  // 推进加速度
        double torque; // 转向力矩
        Control(double aPush, double torque) {
            this.aPush = aPush; this.torque = torque;
        }
    }

    /**
     * 计算状态导数(用于RK4积分)
     */
    private double[] derivative(State s, Control c) {
        double fLong = -MU_LONG * G + c.aPush;
        double alpha = c.torque / INERTIA;

        // 侧向摩擦约束:如果转弯太急,限制角加速度
        double reqLatForce = MASS * Math.abs(s.v * s.omega);
        double maxLatForce = MU_LAT_MAX * MASS * G;
        if (reqLatForce > maxLatForce) {
            // 滑倒:角速度归零,产生大幅减速
            alpha = -s.omega * 5.0;
            fLong -= 3.0; // 摔倒减速
        }

        return new double[]{
            s.v * Math.cos(s.theta),           // dx/dt
            s.v * Math.sin(s.theta),           // dy/dt
            fLong,                              // dv/dt
            s.omega,                            // dtheta/dt
            alpha                               // domega/dt
        };
    }

    /**
     * RK4单步积分
     */
    private State rk4Step(State s, Control c, double dt) {
        double[] k1 = derivative(s, c);

        State s2 = s.copy();
        s2.x += k1[0] * dt / 2; s2.y += k1[1] * dt / 2;
        s2.v += k1[2] * dt / 2; s2.theta += k1[3] * dt / 2; s2.omega += k1[4] * dt / 2;
        double[] k2 = derivative(s2, c);

        State s3 = s.copy();
        s3.x += k2[0] * dt / 2; s3.y += k2[1] * dt / 2;
        s3.v += k2[2] * dt / 2; s3.theta += k2[3] * dt / 2; s3.omega += k2[4] * dt / 2;
        double[] k3 = derivative(s3, c);

        State s4 = s.copy();
        s4.x += k3[0] * dt; s4.y += k3[1] * dt;
        s4.v += k3[2] * dt; s4.theta += k3[3] * dt; s4.omega += k3[4] * dt;
        double[] k4 = derivative(s4, c);

        State next = s.copy();
        next.x += (k1[0] + 2*k2[0] + 2*k3[0] + k4[0]) * dt / 6;
        next.y += (k1[1] + 2*k2[1] + 2*k3[1] + k4[1]) * dt / 6;
        next.v += (k1[2] + 2*k2[2] + 2*k3[2] + k4[2]) * dt / 6;
        next.theta += (k1[3] + 2*k2[3] + 2*k3[3] + k4[3]) * dt / 6;
        next.omega += (k1[4] + 2*k2[4] + 2*k3[4] + k4[4]) * dt / 6;
        next.t += dt;

        // 边界约束
        next.v = Math.max(0, next.v);
        next.omega = Math.max(-MAX_OMEGA, Math.min(MAX_OMEGA, next.omega));

        return next;
    }

    /**
     * 模拟一段控制序列
     * @param init 初始状态
     * @param controls 控制序列(分段常数)
     * @param segmentTime 每段控制持续时间
     * @param dt RK4积分步长
     */
    public List<State> simulate(State init, Control[] controls,
                                 double segmentTime, double dt) {
        List<State> trajectory = new ArrayList<>();
        State s = init.copy();
        trajectory.add(s.copy());

        for (Control c : controls) {
            int steps = (int) Math.round(segmentTime / dt);
            for (int i = 0; i < steps; i++) {
                s = rk4Step(s, c, dt);
                // 检测越界
                if (Math.abs(s.x) > rinkX || Math.abs(s.y) > rinkY) {
                    s.v = 0; // 撞墙停止
                }
                trajectory.add(s.copy());
                if (s.v <= 0.1) break;
            }
            if (s.v <= 0.1) break;
        }
        return trajectory;
    }

    /**
     * 轨迹损失:到目标点的距离 + 角度偏差惩罚
     */
    public double trajectoryLoss(List<State> traj, double targetX,
                                  double targetY, double targetTheta) {
        State end = traj.get(traj.size() - 1);
        double dist = Math.hypot(end.x - targetX, end.y - targetY);
        double angleDiff = Math.abs(normalizeAngle(end.theta - targetTheta));
        return dist + 0.5 * angleDiff; // 距离为主,角度为辅
    }

    private double normalizeAngle(double a) {
        while (a > Math.PI) a -= 2 * Math.PI;
        while (a < -Math.PI) a += 2 * Math.PI;
        return a;
    }

    /**
     * 模式搜索优化控制参数
     * 将控制序列参数化为 [torque1, torque2, ..., torqueN]
     * 推进加速度采用周期性蹬冰策略
     */
    public Control[] optimizeTrajectory(State init, double targetX,
                                         double targetY, double targetTheta,
                                         int nSegments, double segmentTime) {
        double[] params = new double[nSegments];
        // 初始猜测:朝向目标的大致方向做转弯
        double initAngle = Math.atan2(targetY - init.y, targetX - init.x);
        double turn = normalizeAngle(initAngle - init.theta);
        for (int i = 0; i < nSegments; i++) {
            params[i] = turn * 2.0 / nSegments; // 均匀分配转向
        }

        double stepSize = 0.5;
        double bestLoss = evaluate(params, init, targetX, targetY,
                                    targetTheta, segmentTime);

        int iter = 0;
        while (stepSize > 0.01 && iter < 2000) {
            boolean improved = false;
            for (int i = 0; i < nSegments; i++) {
                for (int dir = -1; dir <= 1; dir += 2) {
                    double[] trial = params.clone();
                    trial[i] += dir * stepSize;
                    trial[i] = Math.max(-MAX_TORQUE, Math.min(MAX_TORQUE, trial[i]));
                    double loss = evaluate(trial, init, targetX, targetY,
                                            targetTheta, segmentTime);
                    if (loss < bestLoss) {
                        bestLoss = loss;
                        params = trial;
                        improved = true;
                    }
                }
            }
            if (!improved) stepSize *= 0.7;
            iter++;
        }
        System.out.println("  模式搜索迭代 " + iter + " 次,最终损失="
                         + String.format("%.4f", bestLoss));

        // 转换为控制序列(周期性蹬冰 + 优化转向)
        Control[] controls = new Control[nSegments];
        for (int i = 0; i < nSegments; i++) {
            // 周期性蹬冰:每3段蹬一次
            double push = (i % 3 == 0) ? MAX_PUSH * 0.6 : 0;
            controls[i] = new Control(push, params[i]);
        }
        return controls;
    }

    private double evaluate(double[] torques, State init, double targetX,
                            double targetY, double targetTheta,
                            double segmentTime) {
        Control[] controls = new Control[torques.length];
        for (int i = 0; i < torques.length; i++) {
            double push = (i % 3 == 0) ? MAX_PUSH * 0.6 : 0;
            controls[i] = new Control(push, torques[i]);
        }
        List<State> traj = simulate(init, controls, segmentTime, 0.005);
        return trajectoryLoss(traj, targetX, targetY, targetTheta);
    }

    // ==================== 主程序 ====================
    public static void main(String[] args) {
        System.out.println("===== 滑冰动力学模拟与最优轨迹控制 =====\n");

        // 标准冰场 30m × 60m
        IceSkating rink = new IceSkating(30, 60);

        // 场景1:直线滑行(测试RK4精度)
        System.out.println("【场景1】直线滑行测试(无转向)");
        State start1 = new State(0, -25, 0, Math.toRadians(90), 0, 0);
        Control[] straight = { new Control(MAX_PUSH * 0.5, 0) };
        List<State> traj1 = rink.simulate(start1, straight, 5.0, 0.005);
        State end1 = traj1.get(traj1.size() - 1);
        System.out.println("  5秒滑行距离: " + String.format("%.2f", end1.y + 25)
                         + "m, 末速度: " + String.format("%.2f", end1.v) + "m/s");

        // 场景2:90度转弯(测试侧向摩擦约束)
        System.out.println("\n【场景2】高速90度转弯测试");
        State start2 = new State(0, -20, 12, Math.toRadians(90), 0, 0);
        Control[] turn = {
            new Control(0, MAX_TORQUE * 0.8),
            new Control(0, MAX_TORQUE * 0.8),
            new Control(0, 0)
        };
        List<State> traj2 = rink.simulate(start2, turn, 1.5, 0.005);
        State end2 = traj2.get(traj2.size() - 1);
        System.out.println("  转弯后位置: ("
                         + String.format("%.2f", end2.x) + ", "
                         + String.format("%.2f", end2.y) + ")"
                         + ", 末速度: " + String.format("%.2f", end2.v) + "m/s"
                         + ", 方向: " + String.format("%.1f", Math.toDegrees(end2.theta)) + "°");

        // 场景3:最优轨迹规划(从一角到对角)
        System.out.println("\n【场景3】最优轨迹:从左下角到右上角");
        State start3 = new State(-10, -25, 0, 0, 0, 0);
        double targetX = 10, targetY = 25;
        double targetTheta = Math.toRadians(90);

        Control[] optControls = rink.optimizeTrajectory(
            start3, targetX, targetY, targetTheta, 12, 0.8);
        List<State> optTraj = rink.simulate(start3, optControls, 0.8, 0.005);
        State end3 = optTraj.get(optTraj.size() - 1);
        System.out.println("  终点位置: ("
                         + String.format("%.2f", end3.x) + ", "
                         + String.format("%.2f", end3.y) + ")"
                         + ", 目标: (" + targetX + ", " + targetY + ")"
                         + ", 用时: " + String.format("%.2f", end3.t) + "s");

        // 场景4:短道速滑200m(近似椭圆轨迹)
        System.out.println("\n【场景4】短道速滑200m一圈轨迹模拟");
        State start4 = new State(0, -15, 0, Math.toRadians(90), 0, 0);
        // 简化为:先直道加速,再弯道转向,再直道,再弯道
        Control[] race = new Control[20];
        for (int i = 0; i < 20; i++) {
            double push = (i % 2 == 0) ? MAX_PUSH : 0;
            double torque = 0;
            if (i >= 4 && i <= 7) torque = MAX_TORQUE * 0.5;  // 第一弯
            if (i >= 14 && i <= 17) torque = -MAX_TORQUE * 0.5; // 第二弯
            race[i] = new Control(push, torque);
        }
        List<State> raceTraj = rink.simulate(start4, race, 1.0, 0.005);
        State end4 = raceTraj.get(raceTraj.size() - 1);
        double dist = 0;
        for (int i = 1; i < raceTraj.size(); i++) {
            State a = raceTraj.get(i - 1), b = raceTraj.get(i);
            dist += Math.hypot(b.x - a.x, b.y - a.y);
        }
        System.out.println("  近似滑行距离: " + String.format("%.1f", dist) + "m"
                         + ", 用时: " + String.format("%.2f", end4.t) + "s"
                         + ", 平均速度: " + String.format("%.2f", dist / end4.t) + "m/s");
    }
}

四、复杂度分析

模块 时间复杂度 空间复杂度 说明
RK4单步积分 (O(1)) (O(1)) 固定四次导数计算
轨迹模拟 (O(N \cdot T/dt)) (O(N \cdot T/dt)) (N)为控制段数
模式搜索 (O(k \cdot N \cdot T/dt)) (O(N)) (k)为迭代次数
损失评估 (O(T/dt)) (O(1)) 仅读取终点状态

模式搜索不依赖梯度信息,对非平滑损失函数具有良好鲁棒性。通过逐步衰减步长,可收敛到局部最优解。

五、扩展思考

  1. 模型预测控制(MPC):将模式搜索与滚动时域结合,每经过一个控制段就重新规划剩余轨迹,适应动态环境变化。
  2. 空气阻力与风阻:高速滑行时(>15m/s),空气阻力与速度平方成正比,可达总阻力的30%以上,应在模型中加入二次阻力项。
  3. 多人碰撞避免:在短道速滑场景中,引入其他运动员的预测轨迹,通过势场法或模型预测控制实现实时避障。

滑冰动力学模拟将经典力学、数值积分与优化算法融为一体,是理解物理引擎底层原理的绝佳入门案例。