2023年五一杯数学建模A题无人机定点投放问题求解全过程完整文档及程序

2023年五一杯数学建模

A题 无人机定点投放问题

原题再现:

  随着科学技术的不断发展,无人机在许多领域都有着广泛的应用。对于空中执行定点投放任务的无人机,其投放精度不仅依赖于无人机的操作技术,而且还与无人机执行任务时所处状态和环境有关,例如在接近投放点时无人机的高度、速度,无人机所处位置的风速、投放点周围地理环境等因素。在本题中仅考虑喷气式无人机,请查阅相关资料,研究以下问题:
  问题1: 假设无人机以平行于水平面的方式飞行,在空中投放物资(物资为球形,半径20cm,重量50kg)到达地面指定位置。
  (1)建立数学模型,给出无人机投放距离(投放物资时无人机与地面物资指定落地点之间的直线距离)与无人机飞行高度、飞行速度、空气阻力等之间的关系。
  (2)假设无人机的飞行高度为300m,飞行速度为300km/h,风速为5m/s,风向与水平面平行。建立数学模型,分别给出无人机飞行方向与风向相同(夹角为0度)、相反(夹角为180度)、垂直(夹角为90度)情况下无人机的投放距离。
  问题2:无人机不仅能定点投放物资,而且还可以通过安装在无人机前端的发射筒发射爆炸物疏通河道。其大致过程是:无人机首先水平飞行接近障碍物所处区域,然后俯冲找准时机发射爆炸物,发射结束后无人机随即拉升飞走。现有一处河流被冰块堆积阻断,需要用无人机发射爆炸物(爆炸物为球形,半径8cm,重量5kg)对目标进行爆破。假设无人机的初始点到目标的水平距离为10000m。受环境影响,无人机必须俯冲发射,并且发射方向与无人机的飞行方向一致。
  (1)建立数学模型,给出无人机发射距离(发射点与目标的直线距离)与无人机的飞行高度、飞行速度、俯冲角度及发射速度等因素之间的关系。
  (2)假设风速为6m/s,无人机接近目标时的飞行高度为800m、飞行速度为300km/h,爆炸物的发射速度为600km/h(相对于无人机的速度)。要求发射爆炸物时无人机与目标的距离在1000 m-3000 m之间,且无人机的高度不低于300m,请给出无人机击中目标的发射策略。
  问题3:无人机发射爆炸物命中目标的精度与无人机飞行的稳定性有很大关系。相同条件下,无人机发射爆炸物时越稳定,命中目标的精度越高。开始俯冲后,无人机操控员需要不断调整无人机的飞行姿态以修正风向、风速对无人机的影响。
  (1)在飞行速度、发射速度一定的情况下,综合考虑各种因素,建立数学模型,量化无人机飞行的稳定性,给出稳定性与命中精度之间的关系,并利用数值仿真等方法对无人机的稳定性进行分析验证。
  (2)假设风速为6m/s,无人机的飞行速度范围为300 km/h -400 km/h,爆炸物的发射速度为500km/h(相对于无人机的速度)。无人机在800m高度开始俯冲,初始俯冲角度为45°,发射爆炸物时的飞行高度不低于300m,请给出为尽量保持无人机稳定而采取的飞行姿态最优调整策略。

整体求解过程概述(摘要)

  针对问题一,为了探究球形物资在风场与空气阻力共同作用下的落点规律,首先对题设质量、半径、高度、速度和风速进行参数结构化、国际单位制转换、坐标对齐和量纲校核;鉴于物体受到的阻力取决于其相对空气速度,引入三维相对风速并构建变密度二次阻力弹道微分方程。采用四阶 Runge--Kutta 方法求解落地事件,得到高度 300 m、速度 300 km/h、风速 5 m/s 时,顺风、逆风和侧风对应投放距离分别为 647.621 m、625.277 m 和 636.669 m;侧风造成 8.434 m 的横向偏移。进一步通过高度---速度响应面、阻力系数灵敏度和步长收敛图验证模型的单调性、物理合理性及数值稳定性。
  针对问题二,在问题一弹道模型基础上进一步考虑无人机俯冲姿态和发射筒赋予的相对速度。为了避免把无人机速度与发射速度错误地作标量或独立处理,将二者沿发射方向进行矢量合成,并建立发射高度、飞行速度、俯冲角、发射速度、风向与发射距离之间的映射。鉴于题目仅给定风速而未给定风向,采用逆风工况构造保守可行域,同时以顺风与侧风进行鲁棒性复核。网格求解表明,推荐在高度约 780 m、俯冲角约 40° 时发射;顺风、逆风、侧风的发射距离分别为 1080.759 m、1051.854 m、1066.591 m,均满足 1000--3000 m 的约束。侧风工况需向来风方向修正约 1.52° 的方位角。
  针对问题三,为了量化"飞行越稳定、命中精度越高",构建由姿态角误差、角速度、侧向过载、控制变化率和风估计残差组成的任务层稳定度指标,并将稳定度映射为释放高度、速度、俯冲角、航向角与风速估计的不确定度。基于前述弹道模型建立落点对释放状态的雅可比矩阵,采用一阶协方差传播与蒙特卡洛仿真双重求解。结果表明,稳定度得分从 60 提升至 90 时,CEP50 从 24.220 m 降至 7.999 m,20 m 目标圆命中概率由 37.22% 提升至 97.33%;俯冲角与航向角是最敏感的两类误差源。进一步建立兼顾动压、姿态调整速率、风场暴露时间、发射距离裕度与风向敏感性的多目标代价函数,网格搜索得到速度 300 km/h、发射高度 650 m、发射角 25° 的最优候选,并以五次光滑多项式生成连续姿态指令。
  整体而言,本文形成了"数据理解与参数校核---三维弹道建模---数值求解---灵敏度与不确定性分析---稳定性量化---多目标策略优化---鲁棒性验证"的闭环框架。模型的核心优势是三问共享统一物理底座、避免重复求解,同时通过可行域、蒙特卡洛落点云、协方差椭圆和随机风箱线图给出可解释的工程决策依据。由于题目未提供具体机型的气动导数、控制律和实测风谱,第三问的稳定性部分属于任务层代理模型;其参数可在获得飞行记录后通过系统辨识进一步校准。

模型假设:

  假设 1:地面在任务区域内近似为水平平面,取 z=0;地球曲率和科里奥利力在 1--2 km 弹道尺度内可忽略。
  假设 2:物资与爆炸物均按刚性球体处理,不考虑旋转引起的马格努斯力、姿态翻滚、烧蚀和质量变化。
  假设 3:空气阻力服从二次阻力定律,基准阻力系数取 Cd=0.47;考虑到球体阻力系数随雷诺数变化,另在 0.25--0.65 范围内进行敏感性分析。
  假设 4:空气密度随高度按标准大气对流层公式变化;0--800 m 内温度梯度视为常数。
  假设 5:风在一次弹道飞行期间视为水平、均匀、定常;第三问通过随机化风速与风向模拟阵风和估计误差。
  假设 6:问题一释放瞬间物资速度等于无人机水平飞行速度;问题二、三发射方向与无人机飞行方向一致,地速初值等于无人机速度与相对发射速度的矢量和。
  假设 7:发射筒长度和爆炸物在筒内的加速过程相对于整体飞行时间可忽略,发射瞬间视为速度突变。
  假设 8:第三问中的稳定度是面向任务的综合得分,而不是特定机型的适航稳定性判据;其不确定度映射需在获得实测飞行数据后再校准。
  假设 9:命中精度评价采用落点相对名义目标的平面误差,取 20 m 为示例有效目标半径;同时报告不依赖目标尺寸的 CEP50 与 R90。
  假设 10:问题三继承问题二的发射距离 1000--3000 m 约束,并将鲁棒下限提高到 1050 m,以保留 50 m 安全裕度。

问题分析:

  问题一问题分析
  针对无人机水平匀速投放球形物资场景,本题属于带风场耦合的三维二次阻力弹道求解问题。核心研究高度、飞行速度、不同风向对物资落地直线投放距离的影响,难点在于空气阻力由物体相对空气速度决定,顺、逆、侧风会改变相对气流速度,使三轴运动微分方程相互耦合,不存在解析解,必须采用数值积分求解。建模首先统一所有物理量量纲,构建标准大气密度模型与球体二次阻力受力方程组,以 z=0 落地为终止事件,使用四阶龙格库塔算法求解轨迹;分别计算顺风、逆风、侧风工况下投放距离,同时开展阻力系数、飞行参数灵敏度分析与数值步长收敛验证,验证模型物理合理性与计算稳定性。
  问题二问题分析
  针对无人机先水平巡航、俯冲后发射爆炸物的任务场景,本题在问题一统一弹道物理模型基础上拓展初速度矢量计算,属于带约束弹道可行域优化问题。发射初速度为无人机飞行速度与发射筒相对速度的矢量叠加,俯冲角直接改变初速度水平、竖直分量,高度、俯冲角共同决定发射距离;题目仅给定风速、未限定风向,需以逆风作为保守校核工况保证方案通用可行。通过网格遍历高度与俯冲角组合,筛选满足发射距离 1000--3000m、发射高度不低于 300m 的可行区间,选取具备安全距离裕度的俯冲发射策略,同时计算侧风条件下横向偏移量,给出航向修正角度,并通过单因素灵敏度分析各参数对发射距离的影响程度。
  问题三问题分析
  针对无人机飞行稳定性与爆炸物命中精度耦合优化问题,本题分为稳定度量化、落点误差传播、多目标姿态优化三层建模,完全复用问题二俯冲发射弹道框架。难点是无完整飞控六自由度参数,需构建包含姿态、角速度、风估偏差的综合稳定度得分函数,建立稳定度到发射状态随机误差的映射关系;采用雅可比矩阵一阶协方差传播结合蒙特卡洛仿真双重方法量化落点 CEP、目标圆命中概率,识别俯冲角、航向角为核心误差来源。在此基础上构建兼顾飞行平稳、风鲁棒性、发射距离裕度的多目标代价函数,在速度、高度、俯冲角约束内网格搜索最优发射姿态,设计无突变五次平滑姿态切换指令,并通过随机风场仿真验证最优策略的鲁棒性。

模型的建立与求解整体论文缩略图

全部论文请见下方" 只会建模 QQ名片" 点击QQ名片即可

程序代码:

部分程序如下:
python 复制代码
from __future__ import annotations

import math
import os
from pathlib import Path
from dataclasses import dataclass
from typing import Iterable

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib import font_manager
from matplotlib.patches import Ellipse
from scipy.integrate import solve_ivp

# ----------------------------- 全局设置 -----------------------------
SEED = 202351
RNG = np.random.default_rng(SEED)
OUT = Path(__file__).resolve().parent / "uav_modeling_outputs"
OUT.mkdir(parents=True, exist_ok=True)

FONT_PATH = Path("/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc")
if FONT_PATH.exists():
    font_manager.fontManager.addfont(str(FONT_PATH))
plt.rcParams.update({
    "font.family": "sans-serif",
    "font.sans-serif": ["Noto Sans CJK JP", "Noto Sans CJK SC", "Microsoft YaHei", "SimHei", "Arial Unicode MS", "DejaVu Sans"],
    "axes.unicode_minus": False,
    "figure.dpi": 150,
    "savefig.dpi": 220,
    "axes.titlesize": 13,
    "axes.labelsize": 11,
    "legend.fontsize": 9,
    "grid.alpha": 0.25,
})
COLORS = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#17becf"]

# ----------------------------- 物理常数 -----------------------------
G = 9.80665
T0 = 288.15
P0 = 101325.0
R_AIR = 287.05287
LAPSE = 0.0065
MU0 = 1.7894e-5  # 近地面动力黏度,Pa·s
CD_BASE = 0.47    # 球体保守基准阻力系数;另做敏感性分析


@dataclass(frozen=True)
class Sphere:
    mass: float
    radius: float
    cd: float = CD_BASE

    @property
    def area(self) -> float:
        return math.pi * self.radius ** 2

    @property
    def ballistic_coefficient(self) -> float:
        return self.mass / (self.cd * self.area)


SUPPLY = Sphere(mass=50.0, radius=0.20)
EXPLOSIVE = Sphere(mass=5.0, radius=0.08)


def atmosphere_density(z: np.ndarray | float) -> np.ndarray | float:
    """国际标准大气对流层近似,适用于本题 0~800 m。"""
    zz = np.maximum(np.asarray(z, dtype=float), 0.0)
    temp = T0 - LAPSE * zz
    pressure = P0 * (temp / T0) ** (G / (R_AIR * LAPSE))
    rho = pressure / (R_AIR * temp)
    return float(rho) if np.ndim(z) == 0 else rho


def terminal_speed(obj: Sphere, z: float = 0.0) -> float:
    return math.sqrt(2 * obj.mass * G / (atmosphere_density(z) * obj.cd * obj.area))


def acceleration(state: np.ndarray, wind: np.ndarray, obj: Sphere) -> np.ndarray:
    """可同时处理形状 (..., 6) 的状态数组。"""
    pos = state[..., :3]
    vel = state[..., 3:]
    rel = vel - wind
    speed = np.linalg.norm(rel, axis=-1, keepdims=True)
    rho = np.asarray(atmosphere_density(pos[..., 2]))
    rho_factor = rho[..., None] if rho.ndim > 0 else rho
    drag = -0.5 * rho_factor * obj.cd * obj.area * speed * rel / obj.mass
    acc = drag.copy()
    acc[..., 2] -= G
    return np.concatenate([vel, acc], axis=-1)


def impact_rk4(h: float, v0: Iterable[float], wind: Iterable[float], obj: Sphere,
               dt: float = 0.05, tmax: float = 120.0) -> tuple[float, np.ndarray, np.ndarray]:
    """固定步长四阶 RK,返回飞行时间、落地状态、轨迹。"""
    state = np.array([0.0, 0.0, h, *v0], dtype=float)
    wind_arr = np.asarray(wind, dtype=float)
    traj = [state.copy()]
    t = 0.0
    while t < tmax and state[2] > 0:
        prev = state.copy()
        k1 = acceleration(state, wind_arr, obj)
        k2 = acceleration(state + 0.5 * dt * k1, wind_arr, obj)
        k3 = acceleration(state + 0.5 * dt * k2, wind_arr, obj)
        k4 = acceleration(state + dt * k3, wind_arr, obj)
        state = state + dt * (k1 + 2*k2 + 2*k3 + k4) / 6
        t += dt
        if state[2] <= 0:
            lam = prev[2] / (prev[2] - state[2])
            state = prev + lam * (state - prev)
            t = t - dt + lam * dt
            traj.append(state.copy())
            break
        traj.append(state.copy())
    return t, state, np.asarray(traj)


def impact_solve_ivp(h: float, v0: Iterable[float], wind: Iterable[float], obj: Sphere) -> tuple[float, np.ndarray]:
    """高精度基准解,用于验证 RK4。"""
    y0 = np.array([0.0, 0.0, h, *v0], dtype=float)
    w = np.asarray(wind, dtype=float)

    def fun(_t, y):
        return acceleration(y, w, obj)

    def event_ground(_t, y):
        return y[2]
    event_ground.terminal = True
    event_ground.direction = -1
    sol = solve_ivp(fun, (0, 120), y0, events=event_ground, rtol=1e-10, atol=1e-12, max_step=0.03)
    return float(sol.t_events[0][0]), sol.y_events[0][0]


def impact_batch(h: np.ndarray, v0: np.ndarray, wind: np.ndarray, obj: Sphere,
                 dt: float = 0.04, tmax: float = 80.0) -> np.ndarray:
    """向量化 Monte Carlo 弹道积分,仅返回落点二维坐标。"""
    h = np.asarray(h, float)
    n = h.size
    state = np.zeros((n, 6), dtype=float)
    state[:, 2] = h
    state[:, 3:] = v0
    wind = np.asarray(wind, float)
    impacts = np.full((n, 2), np.nan)
    active = np.ones(n, dtype=bool)
    steps = int(tmax / dt)
    for _ in range(steps):
        if not np.any(active):
            break
        idx = np.where(active)[0]
        s = state[idx]
        w = wind[idx]
        prev = s.copy()
        k1 = acceleration(s, w, obj)
        k2 = acceleration(s + 0.5*dt*k1, w, obj)
        k3 = acceleration(s + 0.5*dt*k2, w, obj)
        k4 = acceleration(s + dt*k3, w, obj)
        new = s + dt*(k1 + 2*k2 + 2*k3 + k4)/6
        crossed = new[:, 2] <= 0
        if np.any(crossed):
            lam = prev[crossed, 2] / (prev[crossed, 2] - new[crossed, 2])
            hit = prev[crossed] + lam[:, None]*(new[crossed]-prev[crossed])
            impacts[idx[crossed]] = hit[:, :2]
            active[idx[crossed]] = False
        state[idx[~crossed]] = new[~crossed]
    return impacts


def savefig(name: str):
    plt.tight_layout()
    plt.savefig(OUT / name, bbox_inches="tight")
    plt.close()


def fire_solution(h: float, theta_deg: float, aircraft_kmh: float, launch_kmh: float,
                  wind=(0.0, 0.0, 0.0), obj: Sphere = EXPLOSIVE, dt=0.05):
    theta = math.radians(theta_deg)
    speed = (aircraft_kmh + launch_kmh) / 3.6
    v0 = (speed*math.cos(theta), 0.0, -speed*math.sin(theta))
    tf, state, traj = impact_rk4(h, v0, wind, obj, dt=dt)
    rxy = math.hypot(state[0], state[1])
    distance = math.hypot(h, rxy)
    return {"time": tf, "state": state, "traj": traj, "range": rxy, "distance": distance}


# ----------------------------- 数据理解 -----------------------------
def data_understanding():
    rows = []
    for name, obj, v in [("投放物资", SUPPLY, 300/3.6), ("爆炸物", EXPLOSIVE, 900/3.6)]:
        re = atmosphere_density(300) * v * (2*obj.radius) / MU0
        rows.append({
            "对象": name, "质量/kg": obj.mass, "半径/m": obj.radius, "迎风面积/m2": obj.area,
            "基准阻力系数": obj.cd, "弹道系数/kg/m2": obj.ballistic_coefficient,
            "代表雷诺数": re, "海平面终端速度/m·s-1": terminal_speed(obj)
        })
    df = pd.DataFrame(rows)
    df.to_csv(OUT / "table_01_dimensionless_parameters.csv", index=False, encoding="utf-8-sig")

    fig, ax = plt.subplots(figsize=(8.5, 4.8))
    metrics = ["弹道系数/kg/m2", "海平面终端速度/m·s-1"]
    x = np.arange(len(metrics))
    width = 0.34
    for i, row in df.iterrows():
        vals = [row[m] for m in metrics]
        ax.bar(x + (i-0.5)*width, vals, width, label=row["对象"], color=COLORS[i])
        for j, val in enumerate(vals):
            ax.text(x[j] + (i-0.5)*width, val*1.02, f"{val:.1f}", ha="center", fontsize=9)
    ax.set_xticks(x, ["弹道系数", "终端速度"])
    ax.set_ylabel("数值(量纲见数据标签)")
    ax.set_title("题设对象的空气动力学尺度对比")
    ax.grid(axis="y")
    ax.legend()
    savefig("fig_01_aerodynamic_scale.png")
    return df


# ----------------------------- 问题一 -----------------------------
def problem1():
    h = 300.0
    v = 300/3.6
    winds = {
        "顺风(0°)": (5, 0, 0),
        "逆风(180°)": (-5, 0, 0),
        "侧风(90°)": (0, 5, 0),
        "无风基准": (0, 0, 0),
    }
    rows = []
    trajectories = {}
    for label, w in winds.items():
        tf, state, traj = impact_rk4(h, (v, 0, 0), w, SUPPLY, dt=0.025)
        D = math.sqrt(h*h + state[0]**2 + state[1]**2)
        rows.append({"工况": label, "飞行时间/s": tf, "纵向位移/m": state[0], "侧向位移/m": state[1], "投放距离/m": D})
        trajectories[label] = traj
    df = pd.DataFrame(rows)
    df.to_csv(OUT / "table_02_problem1_results.csv", index=False, encoding="utf-8-sig")

    fig, ax = plt.subplots(figsize=(8.6, 5.1))
    for i, (label, traj) in enumerate(trajectories.items()):
        ax.plot(traj[:, 0], traj[:, 2], lw=2.2, label=label, color=COLORS[i])
    ax.set_xlabel("沿航向水平位移 x / m")
    ax.set_ylabel("高度 z / m")
    ax.set_title("不同风向下物资投放轨迹")
    ax.grid(True); ax.legend()
    savefig("fig_02_p1_trajectories.png")

    fig, ax = plt.subplots(figsize=(7.2, 5.6))
    for i, row in df.iterrows():
        ax.scatter(row["纵向位移/m"], row["侧向位移/m"], s=90, color=COLORS[i], label=row["工况"])
        ax.annotate(f"({row['纵向位移/m']:.1f}, {row['侧向位移/m']:.1f})", (row["纵向位移/m"], row["侧向位移/m"]), xytext=(6,7), textcoords="offset points")
    ax.axhline(0, color="gray", lw=0.8)
    ax.set_xlabel("纵向落点位移 / m"); ax.set_ylabel("侧向落点位移 / m")
    ax.set_title("不同风向下落点平面分布")
    ax.grid(True); ax.legend(loc="best")
    savefig("fig_03_p1_landing_points.png")

    heights = np.linspace(100, 800, 15)
    speeds = np.linspace(180, 420, 17)
    Z = np.zeros((len(heights), len(speeds)))
    for i, hh in enumerate(heights):
        for j, vv in enumerate(speeds):
            _, st, _ = impact_rk4(hh, (vv/3.6, 0, 0), (0, 0, 0), SUPPLY, dt=0.07)
            Z[i, j] = math.sqrt(hh*hh + st[0]**2)
    pd.DataFrame(Z, index=heights, columns=speeds).to_csv(OUT / "table_03_p1_height_speed_surface.csv", encoding="utf-8-sig")
    fig, ax = plt.subplots(figsize=(8.8, 5.5))
    cs = ax.contourf(speeds, heights, Z, levels=18, cmap="viridis")
    cbar = fig.colorbar(cs, ax=ax); cbar.set_label("投放距离 / m")
    ax.contour(speeds, heights, Z, levels=8, colors="white", linewidths=0.5, alpha=0.7)
    ax.set_xlabel("无人机速度 / (km·h^-1)"); ax.set_ylabel("飞行高度 / m")
    ax.set_title("无风条件下投放距离对高度与速度的响应面")
    savefig("fig_04_p1_height_speed_contour.png")

    cds = np.linspace(0.25, 0.65, 17)
    dvals = []
    for cd in cds:
        obj = Sphere(50, 0.2, cd)
        _, st, _ = impact_rk4(300, (v, 0, 0), (0, 0, 0), obj, dt=0.04)
        dvals.append(math.sqrt(300**2 + st[0]**2))
    fig, ax = plt.subplots(figsize=(7.8, 4.8))
    ax.plot(cds, dvals, marker="o", lw=2, color=COLORS[0])
    ax.axvline(0.47, ls="--", color=COLORS[3], label="基准 Cd=0.47")
    ax.set_xlabel("阻力系数 Cd"); ax.set_ylabel("投放距离 / m")
    ax.set_title("阻力系数不确定性对投放距离的影响")
    ax.grid(True); ax.legend()
    savefig("fig_05_p1_cd_sensitivity.png")

    # 数值方法收敛验证
    t_ref, st_ref = impact_solve_ivp(300, (v, 0, 0), (5, 0, 0), SUPPLY)
    dts = np.array([0.20, 0.10, 0.05, 0.025])
    errors = []
    for dt in dts:
        tt, st, _ = impact_rk4(300, (v, 0, 0), (5, 0, 0), SUPPLY, dt=dt)
        errors.append(math.hypot(st[0]-st_ref[0], tt-t_ref))
    fig, ax = plt.subplots(figsize=(7.4, 4.8))
    ax.loglog(dts, errors, marker="o", lw=2)
    ax.set_xlabel("RK4 步长 Δt / s"); ax.set_ylabel("综合误差")
    ax.set_title("固定步长 RK4 相对高精度 solve_ivp 的收敛性")
    ax.grid(True, which="both")
    savefig("fig_06_numerical_convergence.png")
    return df


# ----------------------------- 问题二 -----------------------------
def problem2():
    heights = np.arange(300, 801, 20)
    angles = np.arange(5, 60.1, 2.5)
    D = np.zeros((len(heights), len(angles)))
    # 风向未指定,采用沿航向逆风作为保守可行域
    for i, h in enumerate(heights):
        for j, th in enumerate(angles):
            D[i, j] = fire_solution(h, th, 300, 600, wind=(-6, 0, 0), dt=0.065)["distance"]
    pd.DataFrame(D, index=heights, columns=angles).to_csv(OUT / "table_04_p2_feasible_grid.csv", encoding="utf-8-sig")

    fig, ax = plt.subplots(figsize=(9.0, 5.5))
    cs = ax.contourf(angles, heights, D, levels=20, cmap="cividis")
    fig.colorbar(cs, ax=ax, label="发射距离 / m")
    ax.contour(angles, heights, D, levels=[1000, 3000], colors=["red", "red"], linewidths=2)
    ax.fill_between(angles, 300, 800, where=((D >= 1000) & (D <= 3000)).any(axis=0), color="white", alpha=0.05)
    ax.scatter([40], [780], marker="*", s=220, color="#e31a1c", edgecolor="white", label="推荐策略")
    ax.set_xlabel("俯冲角 / °"); ax.set_ylabel("发射高度 / m")
    ax.set_title("逆风 6 m/s 下发射距离可行域(300 km/h + 600 km/h)")
    ax.legend()
    savefig("fig_07_p2_feasible_region.png")

    strategy_h, strategy_th = 780.0, 40.0
    wind_cases = {"顺风": (6,0,0), "逆风": (-6,0,0), "侧风": (0,6,0)}
    rows = []
    fig, ax = plt.subplots(figsize=(8.6, 5.1))
    for i, (lab, w) in enumerate(wind_cases.items()):
        r = fire_solution(strategy_h, strategy_th, 300, 600, wind=w, dt=0.025)
        tr = r["traj"]
        rows.append({"风况": lab, "发射高度/m": strategy_h, "俯冲角/deg": strategy_th,
                     "飞行时间/s": r["time"], "水平射程/m": r["range"], "发射距离/m": r["distance"],
                     "侧偏/m": r["state"][1]})
        ax.plot(tr[:,0], tr[:,2], lw=2.2, color=COLORS[i], label=f"{lab}:D={r['distance']:.1f} m")
    df = pd.DataFrame(rows)
    df.to_csv(OUT / "table_05_p2_strategy.csv", index=False, encoding="utf-8-sig")
    ax.set_xlabel("沿瞄准方向位移 / m"); ax.set_ylabel("高度 / m")
    ax.set_title("推荐发射策略在三类典型风向下的弹道")
    ax.grid(True); ax.legend()
    savefig("fig_08_p2_strategy_trajectories.png")

    az = np.linspace(0, 360, 73)
    d_az = []
    lateral = []
    for a in az:
        w = (6*math.cos(math.radians(a)), 6*math.sin(math.radians(a)), 0)
        r = fire_solution(strategy_h, strategy_th, 300, 600, wind=w, dt=0.055)
        d_az.append(r["distance"]); lateral.append(r["state"][1])
    fig = plt.figure(figsize=(7.2, 6.2))
    ax = fig.add_subplot(111, projection="polar")
    ax.plot(np.radians(az), d_az, lw=2, color=COLORS[0])
    ax.fill(np.radians(az), d_az, alpha=0.16, color=COLORS[0])
    ax.set_title("风向方位角对发射距离的影响(风速 6 m/s)", pad=18)
    savefig("fig_09_p2_wind_polar.png")

    # 局部灵敏度:每个变量改变一个代表增量所引起的距离变化
    base = fire_solution(strategy_h, strategy_th, 300, 600, wind=(-6,0,0), dt=0.03)["distance"]
    perturbations = [
        ("发射高度 +10 m", fire_solution(strategy_h+10, strategy_th, 300, 600, (-6,0,0), dt=0.03)["distance"]),
        ("俯冲角 +1°", fire_solution(strategy_h, strategy_th+1, 300, 600, (-6,0,0), dt=0.03)["distance"]),
        ("无人机速度 +10 km/h", fire_solution(strategy_h, strategy_th, 310, 600, (-6,0,0), dt=0.03)["distance"]),
        ("发射速度 +10 km/h", fire_solution(strategy_h, strategy_th, 300, 610, (-6,0,0), dt=0.03)["distance"]),
        ("逆风减弱 1 m/s", fire_solution(strategy_h, strategy_th, 300, 600, (-5,0,0), dt=0.03)["distance"]),
        ("Cd +0.05", fire_solution(strategy_h, strategy_th, 300, 600, (-6,0,0), obj=Sphere(5,.08,.52), dt=0.03)["distance"]),
    ]
    labels = [p[0] for p in perturbations]
    changes = [p[1]-base for p in perturbations]
    fig, ax = plt.subplots(figsize=(8.5, 4.9))
    y = np.arange(len(labels))
    bar_colors = [COLORS[2] if x >= 0 else COLORS[3] for x in changes]
    ax.barh(y, changes, color=bar_colors)
    ax.set_yticks(y, labels); ax.axvline(0, color="black", lw=0.8)
    ax.set_xlabel("发射距离变化量 / m"); ax.set_title("推荐策略的局部单因素灵敏度")
    ax.grid(axis="x")
    savefig("fig_10_p2_sensitivity_tornado.png")

    # 任务几何图
    fig, ax = plt.subplots(figsize=(9.0, 4.8))
    x0 = -10000
    release_x = -df.loc[df["风况"]=="逆风", "水平射程/m"].iloc[0]
    ax.plot([x0, release_x-500], [800,800], lw=3, color=COLORS[0], label="水平接近段")
    ax.plot([release_x-500, release_x], [800, strategy_h], lw=3, color=COLORS[1], label="姿态建立/俯冲段")
    ax.scatter([release_x], [strategy_h], s=90, color=COLORS[3], label="发射点")
    ax.scatter([0], [0], marker="*", s=180, color="black", label="目标")
    ax.plot([release_x,0], [strategy_h,0], ls="--", lw=1.6, color="gray", label="发射点---目标视线")
    ax.set_xlim(-10200, 300); ax.set_ylim(-20,900)
    ax.set_xlabel("相对目标水平坐标 / m"); ax.set_ylabel("高度 / m")
    ax.set_title("问题二推荐飞行---发射任务几何示意")
    ax.grid(True); ax.legend(ncol=3, loc="upper center")
    savefig("fig_11_p2_mission_geometry.png")
    return df


# ----------------------------- 问题三 -----------------------------
def uncertainty_sigmas(score: float) -> dict[str, float]:
    u = 1-score/100
    return {
        "h": 0.8 + 7.0*u,
        "v": 0.25 + 3.0*u,
        "theta": math.radians(0.12 + 2.8*u),
        "yaw": math.radians(0.15 + 2.3*u),
        "wind": 0.25 + 1.8*u,
    }


def monte_carlo_accuracy(score: float, n=1000, nominal=(650.0, 25.0, 300.0), target_radius=20.0):
    h0, th0, vair0 = nominal
    w0 = np.array([-6.0, 0.0, 0.0])
    base = fire_solution(h0, th0, vair0, 500, wind=w0, dt=0.025)
    target = base["state"][:2]
    sig = uncertainty_sigmas(score)
    h = RNG.normal(h0, sig["h"], n)
    vair = RNG.normal(vair0/3.6, sig["v"], n)
    theta = RNG.normal(math.radians(th0), sig["theta"], n)
    yaw = RNG.normal(0.0, sig["yaw"], n)
    wind = np.column_stack([
        RNG.normal(w0[0], sig["wind"], n),
        RNG.normal(w0[1], sig["wind"], n),
        np.zeros(n)
    ])
    total = vair + 500/3.6
    v0 = np.column_stack([total*np.cos(theta)*np.cos(yaw), total*np.cos(theta)*np.sin(yaw), -total*np.sin(theta)])
    impacts = impact_batch(h, v0, wind, EXPLOSIVE, dt=0.045)
    errors = impacts - target
    radii = np.linalg.norm(errors, axis=1)
    cep50 = float(np.nanquantile(radii, 0.5))
    r90 = float(np.nanquantile(radii, 0.9))
    phit = float(np.mean(radii <= target_radius))
    return errors, {"稳定度得分": score, "CEP50/m": cep50, "R90/m": r90, "20m命中概率": phit}


def jacobian_impact(nominal=(650.0, 25.0, 300.0), wind=(-6.0,0.0,0.0)):
    h, th, vair = nominal
    base = fire_solution(h, th, vair, 500, wind=wind, dt=0.025)["state"][:2]
    names = ["高度 h", "空速 V", "俯冲角 θ", "航向角 ψ", "风速 wx", "侧风 wy"]
    steps = [1.0, 1.0, 0.1, 0.1, 0.2, 0.2]  # V 单位 km/h;角度单位 deg
    J = np.zeros((2,6))
    for k in range(6):
        hp, thp, vp = h, th, vair
        wp = list(wind)
        if k == 0: hp += steps[k]
        elif k == 1: vp += steps[k]
        elif k == 2: thp += steps[k]
        elif k == 4: wp[0] += steps[k]
        elif k == 5: wp[1] += steps[k]
        if k == 3:
            yaw = math.radians(steps[k])
            speed=(vair+500)/3.6
            theta=math.radians(th)
            v0=(speed*math.cos(theta)*math.cos(yaw), speed*math.cos(theta)*math.sin(yaw), -speed*math.sin(theta))
            _, st, _ = impact_rk4(h, v0, wind, EXPLOSIVE, dt=0.025)
            val=st[:2]
        else:
            val=fire_solution(hp, thp, vp, 500, wind=wp, dt=0.025)["state"][:2]
        J[:,k]=(val-base)/steps[k]
    return names, J


def problem3():
    scores = [60, 75, 90, 95]
    results = []
    errors_by_score = {}
    for s in scores:
        errors, met = monte_carlo_accuracy(s, n=900)
        errors_by_score[s] = errors
        results.append(met)
    df = pd.DataFrame(results)
    df.to_csv(OUT / "table_06_p3_accuracy_by_stability.csv", index=False, encoding="utf-8-sig")

    # 落点云
    fig, ax = plt.subplots(figsize=(7.5, 6.2))
    for i, s in enumerate([60, 75, 90]):
        e = errors_by_score[s]
        ax.scatter(e[:,0], e[:,1], s=7, alpha=0.30, color=COLORS[i], label=f"稳定度 {s}")
    circle = plt.Circle((0,0), 20, fill=False, ls="--", lw=2, color="black", label="20 m 目标圆")
    ax.add_patch(circle)
    ax.set_aspect("equal", adjustable="box")
    ax.set_xlabel("纵向误差 / m"); ax.set_ylabel("侧向误差 / m")
    ax.set_title("不同飞行稳定度下的蒙特卡洛落点云")
    ax.grid(True); ax.legend()
    savefig("fig_12_p3_monte_carlo_scatter.png")

    # 连续稳定度曲线
    score_grid = np.arange(55, 99, 5)
    met_grid=[]
    for s in score_grid:
        _, met=monte_carlo_accuracy(float(s), n=450)
        met_grid.append(met)
    dg=pd.DataFrame(met_grid)
    dg.to_csv(OUT / "table_07_p3_score_curve.csv", index=False, encoding="utf-8-sig")
    fig, ax = plt.subplots(figsize=(7.8, 4.8))
    ax.plot(dg["稳定度得分"], dg["CEP50/m"], marker="o", lw=2.2, color=COLORS[0], label="CEP50")
    ax.plot(dg["稳定度得分"], dg["R90/m"], marker="s", lw=2.0, color=COLORS[1], label="R90")
    ax.set_xlabel("飞行稳定度得分 S"); ax.set_ylabel("误差半径 / m")
    ax.set_title("飞行稳定度与命中散布半径的定量关系")
    ax.grid(True); ax.legend()
    savefig("fig_13_p3_cep_vs_stability.png")

    fig, ax = plt.subplots(figsize=(7.8, 4.8))
    ax.plot(dg["稳定度得分"], 100*dg["20m命中概率"], marker="o", lw=2.2, color=COLORS[2])
    ax.set_xlabel("飞行稳定度得分 S"); ax.set_ylabel("落入 20 m 目标圆的概率 / %")
    ax.set_ylim(0,102); ax.set_title("稳定度提升对任务命中概率的增益")
    ax.grid(True)
    savefig("fig_14_p3_hit_probability.png")

    names, J = jacobian_impact()
    # 使用稳定度 75 的代表标准差换算为落点贡献
    sig=uncertainty_sigmas(75)
    std_inputs=np.array([sig["h"], sig["v"]*3.6, math.degrees(sig["theta"]), math.degrees(sig["yaw"]), sig["wind"], sig["wind"]])
    contributions=np.linalg.norm(J*std_inputs[None,:],axis=0)
    pd.DataFrame({"变量":names,"落点标准差贡献/m":contributions}).to_csv(OUT / "table_08_p3_jacobian_sensitivity.csv",index=False,encoding="utf-8-sig")
    fig, ax = plt.subplots(figsize=(8.2, 4.8))
    ax.bar(names, contributions, color=COLORS[:len(names)])
    ax.set_ylabel("一标准差扰动引起的落点位移 / m")
    ax.set_title("落点对释放状态与风估计误差的灵敏度排序")
    ax.tick_params(axis="x", rotation=20); ax.grid(axis="y")
    savefig("fig_15_p3_jacobian_sensitivity.png")

    # 线性协方差椭圆与 MC 比较
    e=errors_by_score[75]
    cov=np.cov(e.T)
    vals, vecs=np.linalg.eigh(cov); order=vals.argsort()[::-1]; vals=vals[order]; vecs=vecs[:,order]
    angle=math.degrees(math.atan2(vecs[1,0],vecs[0,0]))
    fig,ax=plt.subplots(figsize=(6.8,6.0))
    ax.scatter(e[:,0],e[:,1],s=7,alpha=.22,color=COLORS[0],label="Monte Carlo")
    for k,prob_scale in [(1,1.0),(2,2.0)]:
        ell=Ellipse((0,0),2*k*math.sqrt(vals[0]),2*k*math.sqrt(vals[1]),angle=angle,fill=False,lw=2,label=f"{k}σ 协方差椭圆")
        ax.add_patch(ell)
    ax.set_aspect("equal","box"); ax.set_xlabel("纵向误差 / m"); ax.set_ylabel("侧向误差 / m")
    ax.set_title("协方差传播与蒙特卡洛落点云的一致性验证")
    ax.grid(True); ax.legend()
    savefig("fig_16_p3_covariance_validation.png")

    # 多目标网格搜索:继承问题二 1000~3000 m 距离约束,并增加 50 m 鲁棒裕度
    speeds=np.arange(300,401,20)
    heights=np.arange(550,751,50)
    angles=np.arange(15,40.1,5)
    best_map=np.full((len(heights),len(speeds)),np.nan)
    best_theta=np.full_like(best_map,np.nan)
    records=[]
    for i,h in enumerate(heights):
        for j,V in enumerate(speeds):
            best=None
            for th in angles:
                Ds=[fire_solution(h,th,V,500,w,dt=.075)["distance"] for w in [(-6,0,0),(6,0,0),(0,6,0)]]
                if min(Ds)<1050 or max(Ds)>3000:
                    continue
                t=(800-h)/((V/3.6)*math.sin(math.radians((45+th)/2)))
                if not (2.0<=t<=7.0):
                    continue
                rate=abs(45-th)/t
                # 归一化多目标代价:动压、角速率、暴露时间、风向敏感性、姿态改变量、距离裕度
                cost=(0.16*(V/400)**2 + 0.20*(rate/8)**2 + 0.16*(t/5) +
                      0.16*((max(Ds)-min(Ds))/60) + 0.10*(abs(45-th)/35) +
                      0.12*(abs(np.mean(Ds)-1150)/250) + 0.10*math.exp(-(min(Ds)-1000)/80))
                rec={"速度/kmh":V,"发射高度/m":h,"发射角/deg":th,"代价":cost,
                     "逆风距离/m":Ds[0],"顺风距离/m":Ds[1],"侧风距离/m":Ds[2],
                     "调整时间/s":t,"平均角速率/deg_s":rate}
                records.append(rec)
                if best is None or cost<best[0]: best=(cost,th)
            if best:
                best_map[i,j]=best[0]; best_theta[i,j]=best[1]
    rdf=pd.DataFrame(records).sort_values("代价")
    rdf.to_csv(OUT / "table_09_p3_optimization_candidates.csv",index=False,encoding="utf-8-sig")
    optimum=rdf.iloc[0]
    fig,ax=plt.subplots(figsize=(8.2,5.0))
    masked=np.ma.masked_invalid(best_map)
    im=ax.imshow(masked,origin="lower",aspect="auto",extent=[speeds[0]-10,speeds[-1]+10,heights[0]-25,heights[-1]+25],cmap="magma_r")
    fig.colorbar(im,ax=ax,label="最小综合代价")
    ax.scatter([optimum["速度/kmh"]],[optimum["发射高度/m"]],marker="*",s=220,color="cyan",edgecolor="black",label=f"最优角 {optimum['发射角/deg']:.0f}°")
    ax.set_xlabel("无人机速度 / (km·h^-1)"); ax.set_ylabel("发射高度 / m")
    ax.set_title("满足鲁棒距离约束时的速度---高度最小代价图")
    ax.legend()
    savefig("fig_17_p3_optimization_heatmap.png")

    # 最优姿态采用五次光滑过渡,避免俯仰角速度和角加速度突变
    V=float(optimum["速度/kmh"]); hf=float(optimum["发射高度/m"]); thf=float(optimum["发射角/deg"])
    T=float(optimum["调整时间/s"])
    tt=np.linspace(0,T,250); s=tt/T
    smooth=10*s**3-15*s**4+6*s**5
    theta=45+(thf-45)*smooth
    ds=(30*s**2-60*s**3+30*s**4)/T
    q=(thf-45)*ds
    altitude=np.empty_like(tt); altitude[0]=800
    for k in range(1,len(tt)):
        altitude[k]=altitude[k-1]-(V/3.6)*math.sin(math.radians((theta[k]+theta[k-1])/2))*(tt[k]-tt[k-1])
    # 为使末端高度与网格候选一致,对时间轴作轻微比例校正只用于策略示意
    altitude=800-(800-hf)*(800-altitude)/(800-altitude[-1])
    pd.DataFrame({"t/s":tt,"俯冲角/deg":theta,"俯仰角速度/deg_s":q,"高度/m":altitude}).to_csv(OUT/"table_10_p3_optimal_attitude_schedule.csv",index=False,encoding="utf-8-sig")
    fig,ax=plt.subplots(figsize=(8.2,4.8))
    ax.plot(tt,theta,lw=2.4,color=COLORS[0],label="俯冲角")
    ax.set_xlabel("调整时间 / s"); ax.set_ylabel("俯冲角 / °")
    ax2=ax.twinx(); ax2.plot(tt,altitude,lw=2.2,color=COLORS[1],label="高度")
    ax2.set_ylabel("高度 / m")
    ax.set_title("最优姿态策略:五次平滑俯冲角指令与高度变化")
    ax.grid(True)
    lines=ax.get_lines()+ax2.get_lines(); ax.legend(lines,[l.get_label() for l in lines],loc="center right")
    savefig("fig_18_p3_optimal_pitch_altitude.png")

    fig,ax=plt.subplots(figsize=(8.0,4.6))
    ax.plot(tt,q,lw=2.3,color=COLORS[3])
    ax.axhline(0,color="gray",lw=.8)
    ax.set_xlabel("调整时间 / s"); ax.set_ylabel("俯仰角速度 / (°·s⁻¹)")
    ax.set_title("五次平滑策略的俯仰角速度曲线")
    ax.grid(True)
    savefig("fig_19_p3_pitch_rate.png")

    # 风速不确定性鲁棒性:最优策略与对照策略比较
    strategies={"优化策略":(V,hf,thf),"直接保持45°":(350,780,45),"低空急修正":(350,550,20)}
    boxdata=[]; labels=[]; summary=[]
    for lab,(vv,hh,aa) in strategies.items():
        ds=[]
        for _ in range(50):
            ws=max(0,RNG.normal(6,1.5)); az=RNG.uniform(0,2*math.pi)
            w=(ws*math.cos(az),ws*math.sin(az),0)
            ds.append(fire_solution(hh,aa,vv,500,w,dt=.12)["distance"])
        boxdata.append(ds); labels.append(lab)
        summary.append({"策略":lab,"平均距离/m":np.mean(ds),"标准差/m":np.std(ds),"最小距离/m":np.min(ds),"最大距离/m":np.max(ds)})
    pd.DataFrame(summary).to_csv(OUT/"table_11_p3_robustness.csv",index=False,encoding="utf-8-sig")
    fig,ax=plt.subplots(figsize=(8.2,4.9))
    ax.boxplot(boxdata,tick_labels=labels,showmeans=True)
    ax.axhline(1000,ls="--",color=COLORS[3],label="距离下限")
    ax.set_ylabel("发射距离 / m"); ax.set_title("随机风速与风向下不同姿态策略的鲁棒性")
    ax.grid(axis="y"); ax.legend()
    savefig("fig_20_p3_robustness_boxplot.png")
    return df, rdf, optimum


def main():
    understanding=data_understanding()
    p1=problem1()
    p2=problem2()
    p3, opt_table, optimum=problem3()
    with open(OUT/"summary.txt","w",encoding="utf-8") as f:
        f.write("无人机定点投放数学建模数值结果摘要\n")
        f.write("="*50+"\n")
        f.write("\n问题一结果:\n"+p1.to_string(index=False)+"\n")
        f.write("\n问题二推荐策略(h=780 m, 俯冲角=40°):\n"+p2.to_string(index=False)+"\n")
        f.write("\n问题三稳定度---精度:\n"+p3.to_string(index=False)+"\n")
        f.write("\n问题三网格搜索最优候选:\n"+optimum.to_string()+"\n")
        f.write("\n数据性质说明:所有样本均为物理模型仿真数据。\n")
    print(f"运行完成,结果位于:{OUT}")
    print(p1.to_string(index=False))
    print("\n问题三最优候选:")
    print(optimum.to_string())


if __name__ == "__main__":
    main()
全部论文请见下方" 只会建模 QQ名片" 点击QQ名片即可
相关推荐
科学实验家1 小时前
dp子序列问题,好难呀
算法
wabs66610 小时前
关于图论【卡码网117.软件构建的思考】
数据结构·算法·软件构建·图论·卡码网
mifengxing10 小时前
LeetCode 41.缺失的第一个正数|Hard题O(n)+O(1)最优解法深度解析
java·算法·leetcode·排序算法
Tongzhi202611 小时前
从部署到运维:通芝科技无感考勤一体机的全流程效率解析
运维·数据结构·科技·算法·贪心算法
Wang's Blog11 小时前
AI Agent白手起家30: 动态示例选择器之根据长度选择 Few Shot 示例
算法
oyguyteggytrrwwwrt13 小时前
自制交叉线路识别算法
算法
手写码匠13 小时前
华为云Flexus+DeepSeek征文|Dify 多智能体协同编排实战:R1 规划 + V3 执行,构建企业 Agent 团队
人工智能·深度学习·算法·aigc
晓天衡宇•评测社区14 小时前
FSR-Bench 前沿科学推理榜单发布:GPT-5.5 居首,“会推理”未必“能答对”
算法
程序喵大人15 小时前
【C++进阶】STL算法与函数对象 - 02 sort为什么需要随机访问迭代器
开发语言·c++·算法