1. 报告目标

本文档面向后续接手 UavMissionPlanning 的开发者,目标是帮助读者在较短时间内同时掌握以下 5 件事:

  1. 看懂整个解决方案的分层架构与项目边界。
  2. 理解 MissionFlightModel 及周边核心组件的设计意图。
  3. 快速完成 Gurobi 的安装、许可证配置、Visual Studio 接入和运行时排障。
  4. 能够独立编译并运行 UavExperiments 中的示例与 RCP 实验。
  5. 建立清晰的代码阅读顺序,把论文模型与代码实现对应起来。

如果时间非常紧张,建议优先阅读:

  1. README.md
  2. docs/paper-formulation.md
  3. UavCore/include/core/instance_data.h
  4. UavCore/src/parameter_builder.cpp
  5. UavModel/include/model/mission_flight_model.h
  6. UavModel/src/mission_flight_model.cpp
  7. UavExperiments/src/main.cpp

2. 项目整体架构分析

2.1 解决方案组成

整个解决方案由 4 个 Visual Studio 项目组成:

  • UavCore
  • UavModel
  • UavExperiments
  • UavTests

它们的依赖关系如下:

UavCore

UavModel

UavExperiments

UavTests

如果从职责角度理解,可以把系统看成 4 层:

  • UavCore:问题定义层。负责描述无人机、航点、禁飞区、风区、时间网格和预处理参数。
  • UavModel:优化建模层。负责把 UavCore 的输入转化为 Gurobi 的变量、约束和目标函数。
  • UavExperiments:执行与实验层。负责构造实例、配置选项、启动求解并打印结果。
  • UavTests:轻量验证层。负责验证 UavCore 中的基础逻辑是否正常,不直接覆盖求解器行为。

2.2 各子项目功能与相互关系

UavCore

UavCore 是基础数据层,也是其余项目共享的“语义字典”。

主要功能:

  • 定义问题输入结构 InstanceData
  • 定义粗时间层/细时间层映射 TimeGrid
  • 定义预处理输出 DerivedParameters
  • 提供实例工厂 makePaperDemoInstance()makeRcp2dInstance()

关键文件:

  • UavCore/include/core/instance_data.h
  • UavCore/include/core/time_grid.h
  • UavCore/include/core/parameter_builder.h
  • UavCore/src/parameter_builder.cpp

设计意图:

  • 把“问题是什么”与“如何建模求解”分离开来。
  • UavModel 只关注优化模型,而不关心输入数据是如何组织和预处理的。
UavModel

UavModel 是求解器建模层,也是整个项目最核心的逻辑所在。

主要功能:

  • 持有 GRBEnvGRBModel
  • 根据输入数据创建变量
  • 分模块添加约束
  • 设置目标函数
  • 调用 Gurobi 求解

关键文件:

  • UavModel/include/model/mission_flight_model.h
  • UavModel/include/model/model_context.h
  • UavModel/include/model/variable_store.h
  • UavModel/include/model/norm_linearizer.h
  • UavModel/src/mission_flight_model.cpp
  • UavModel/src/constraints/base_model.cpp
  • UavModel/src/constraints/flight_dynamics.cpp
  • UavModel/src/constraints/fuel_range_environment.cpp

设计意图:

  • MissionFlightModel 作为统一建模门面。
  • VariableStore 统一管理变量与索引。
  • ModelContext 把共享上下文集中传递。
  • NormLinearizer 封装范数近似逻辑,避免在多个约束文件里重复写辅助变量和包络约束。
UavExperiments

UavExperiments 是当前默认的主程序入口。

主要功能:

  • 创建 GRBEnv
  • 生成示例问题
  • 构造 MissionFlightModel
  • 选择要启用的扩展项
  • 调用 build()optimize()
  • 输出求解状态和目标值

关键文件:

  • UavExperiments/src/main.cpp

设计意图:

  • 让运行逻辑保持很薄。
  • 把大部分业务复杂度留在 UavCoreUavModel 中。
UavTests

UavTests 是一个基于 assert 的轻量测试程序。

主要功能:

  • 校验 TimeGrid 的索引逻辑
  • 校验 ParameterBuilder 的数值辅助函数
  • 校验 demo 实例是否能生成派生参数

关键文件:

  • UavTests/src/main.cpp

设计意图:

  • 在不依赖 Gurobi 的前提下,尽快发现底层数据和预处理问题。
  • 保持测试可在“未配置求解器”的环境下快速运行。

注意:

  • 它不验证 MissionFlightModel 的约束正确性。
  • 它不覆盖 Gurobi 链接、许可证、求解结果等集成问题。

2.3 主要类和模块的设计意图及交互方式

系统主线可以概括为:

InstanceFactories

InstanceData

ParameterBuilder

DerivedParameters

MissionFlightModel

ConstraintModules

GurobiOptimize

ConsoleOutput

交互顺序如下:

  1. UavCore 中的实例工厂先生成 InstanceData
  2. ParameterBuilder::build() 生成 DerivedParameters
  3. UavExperiments 创建 MissionFlightModel
  4. MissionFlightModel::build() 依次创建变量、基础约束、扩展约束和目标函数
  5. MissionFlightModel::optimize() 调用 Gurobi
  6. UavExperiments 输出状态与目标值

2.4 代码结构层次与文件组织方式

整个工程的文件结构具有比较典型的 C++ 静态库组织特点:

  • include/:放对外暴露的头文件,定义类型和接口
  • src/:放实现文件
  • src/constraints/:进一步按约束主题拆分实现
  • build/:放编译配置文件,如 gurobi.props
  • docs/:放项目说明与论文映射文档

当前目录组织方式有以下优点:

  • 接口和实现分离,便于阅读边界。
  • UavModel 的约束按主题拆分,而不是全部堆在一个文件中。
  • Gurobi 配置集中在 build/gurobi.props,避免多个项目文件重复维护。

可以把当前目录结构进一步理解为下面这张图:

UavMissionPlanning

UavCore

UavModel

UavExperiments

UavTests

build

docs

include/core

src

include/model

src

src/constraints

src/main.cpp

src/main.cpp

gurobi.props

onboarding-training-report.md

paper-formulation.md

建议把这一层次理解为:

  • *.h 负责告诉你“有什么”
  • *.cpp 负责告诉你“怎么做”

3. 核心代码结构关系图解

3.1 MissionFlightModel 与其他组件的关系

MissionFlightModel 是模型层的总入口,它不只是一个“容器”,而是整个模型装配过程的编排者。

它持有如下关键成员:

  • GRBEnv& env_
  • GRBModel model_
  • const InstanceData& instance_
  • const DerivedParameters& derived_
  • ModelOptions options_
  • ModelContext context_
  • NormLinearizer linearizer_
  • VariableStore vars_

关系图如下:

UavExperiments

GRBEnv

InstanceData

DerivedParameters

MissionFlightModel

ModelContext

VariableStore

NormLinearizer

BaseConstraints

FlightDynamics

FuelRangeEnvironment

GRBModel

从设计上看:

  • MissionFlightModel 负责“调度”
  • VariableStore 负责“存变量”
  • ModelContext 负责“传上下文”
  • NormLinearizer 负责“复用范数近似逻辑”
  • base_model.cppflight_dynamics.cppfuel_range_environment.cpp 负责“落具体约束”

3.2 MissionFlightModel::build() 的装配顺序

MissionFlightModel::build() 是理解全系统协作关系的最佳入口。它的逻辑可以概括为:

  1. configureModel()
  2. buildVariables()
  3. 根据开关预处理部分风区变量
  4. addBaseConstraints()
  5. 条件性调用扩展约束模块
  6. setObjective()

这意味着阅读模型代码时,应该始终沿着这个顺序理解,而不是按文件名随机跳读。

对应的装配流程如下:

渲染错误: Mermaid 渲染失败: Parse error on line 2: ... BuildStart[build()] ConfigureMod -----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PS'

3.3 VariableStore 变量存储系统的设计原理

VariableStore 的核心思想是:

  • 以逻辑分组存储变量
  • 以一维 std::vector<GRBVar> 存储具体对象
  • 通过索引函数把多维问题映射为线性数组下标

它存储的变量大体分为几类:

  • 细时间层连续变量:rxryrzvxvyspeedaxayaccelvzPlusvzMinusfuel
  • 粗时间层状态变量:airbornestartedactiveminSpeedActive
  • 任务与模式变量:visitband
  • 环境和冲突变量:collisionDirPoscollisionDirNegrestrictedOutsidewindFaceInsidewindActive

设计原因主要有 4 点:

  1. Gurobi 变量数量很大,用统一容器便于批量管理。
  2. 建模代码里需要频繁按 utftcwband 等维度索引变量。
  3. 如果直接在多个文件里散落索引公式,后续维护成本会很高。
  4. 一维数组更容易和固定维度公式配合,也便于按规模预分配。

例如,以下函数体现了这一点:

  • fineIndex(int u, int tf)
  • coarseIndex(int u, int tc)
  • visitIndex(int u, int w, int tc)
  • bandIndex(int u, int altBand, int throttleBand, int tc)
  • collisionIndex(int pairIndex, int axis, int tc)

这套设计的优点:

  • 变量访问统一
  • 下标含义清晰
  • 减少重复公式和越界风险

这套设计的代价:

  • 阅读者需要先理解索引映射规则
  • 调试时看到的一维下标不如多维结构直观

可以把 VariableStore 理解成下面这种“按时间层和语义分区”的结构:

VariableStore

FineLayerVars

CoarseLayerVars

AssignmentAndBandVars

EnvironmentAndCollisionVars

rx ry rz vx vy speed ax ay accel vzPlus vzMinus fuel

airborne started active minSpeedActive

visit band

collisionDirPos collisionDirNeg restrictedOutside windFaceInside windActive

索引函数与变量分区之间的关系也可以简化为:

input dims

u

tf

tc

w

altBand throttleBand

pair axis

index functions

flat GRBVar vectors

3.4 ModelContext 的作用

ModelContext 是一个轻量的上下文容器,里面打包了:

  • GRBEnv* env
  • GRBModel* model
  • const InstanceData* instance
  • const DerivedParameters* derived
  • ModelOptions options

它的设计意图是降低函数间传参复杂度。

如果没有它,辅助组件和约束模块可能需要反复接收:

  • 求解器环境
  • 模型对象
  • 原始输入
  • 派生参数
  • 选项开关

在当前工程里,ModelContext 的作用更接近“依赖注入包”,方便 NormLinearizer 和各约束实现共享同一套状态。

3.5 NormLinearizer 的作用

NormLinearizer 负责处理范数相关的 MILP 线性近似。

当前提供两个接口:

  • addAbsEnvelope2d()
  • addAbsEnvelope3d()

它的设计意义在于:

  • 将“绝对值辅助变量 + 包络约束”的重复模板抽离出来
  • 避免在多个约束场景下复制粘贴相似代码
  • base_model.cpp 中的核心约束表达更集中于业务含义,而不是包络细节

当前主要使用场景包括:

  • 地面控制站航程约束中的平面距离近似
  • 速度范数近似
  • 加速度范数近似
  • 航点访问时的三维距离近似

它在模型中的位置可以用下面的图快速理解:

raw vars x y z

aux abs vars ax ay az

envelope constraints

norm var

business constraint

需要注意的是:

  • ModelOptions::normDirectionCount2d 已经定义,但当前实现还没有用它控制离散方向数。
  • 现阶段 NormLinearizer 使用的是固定包络形式,而不是可配置精度的多方向线性化。

4. Gurobi 优化器详细配置指南

4.1 从零开始的安装与环境准备

建议按以下顺序完成 Gurobi 准备工作:

  1. 从 Gurobi 官网下载 Windows 版本安装包。
  2. 安装到本机目录,例如:
    C:\gurobi1300\win64
  3. 确认以下子目录存在:
    • include
    • lib
    • bin
  4. 准备许可证文件 gurobi.lic
  5. 打开 UavMissionPlanning.sln
  6. 编译并运行 UavExperiments

本工程当前验证通过的安装位置为:

C:\gurobi1300\win64

关键目录说明:

  • include:C/C++ 头文件目录,包含 gurobi_c++.h
  • lib:链接阶段所需 .lib 文件
  • bin:运行阶段所需 .dll 与命令行工具

4.2 本工程中 Gurobi 的接入方式

本项目没有把 Gurobi 路径直接写死在每个 .vcxproj 内,而是通过统一属性表管理:

  • build/gurobi.props

导入方式:

  • UavModel.vcxproj 导入该属性表
  • UavExperiments.vcxproj 导入该属性表
  • UavTests.vcxproj 没有导入,因为它不依赖求解器

这种集中式做法的优点:

  • 路径只维护一次
  • 易于升级 Gurobi 版本
  • 便于多项目共享
  • 运行时依赖和调试环境也可以集中配置

接入链路可以概括成下面这张图:

GurobiInstall

GUROBI_HOME

gurobi.props

UavModel.vcxproj

UavExperiments.vcxproj

compiler include path

linker lib path

runtime env

UavExperiments.exe

4.3 gurobi.props 参数逐项解释

当前文件内容可以概括为 4 组配置:路径、编译链接、调试环境、运行时 DLL 复制。

GUROBI_HOME
<GUROBI_HOME Condition="'$(GUROBI_HOME)' == ''">C:\gurobi1300\win64</GUROBI_HOME>

含义:

  • 定义 Gurobi 安装根目录
  • 只有当外部环境变量没有显式设置时,才使用这里的默认值

好处:

  • 如果团队成员机器路径不同,可以优先通过系统环境变量覆盖
  • 如果没有设置环境变量,项目本身仍有默认兜底
GRB_LICENSE_FILE
<GRB_LICENSE_FILE Condition="'$(GRB_LICENSE_FILE)' == '' and Exists('$(GUROBI_HOME)\bin\gurobi.lic')">$(GUROBI_HOME)\bin\gurobi.lic</GRB_LICENSE_FILE>

含义:

  • 指定 Gurobi 许可证文件位置
  • 如果系统没有设置该环境变量,并且默认路径下确实存在许可证文件,则自动采用该路径

好处:

  • 降低“本地调试时找不到许可证”的概率
AdditionalIncludeDirectories
<AdditionalIncludeDirectories>$(GUROBI_HOME)\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>

含义:

  • 把 Gurobi 头文件目录加入编译器搜索路径

作用:

  • #include "gurobi_c++.h" 能被编译器找到
AdditionalLibraryDirectories
<AdditionalLibraryDirectories>$(GUROBI_HOME)\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>

含义:

  • 把 Gurobi 的 .lib 所在目录加入链接器搜索路径

作用:

  • 让链接器能找到 gurobi130.libgurobi_c++*.lib
AdditionalDependencies

Debug 配置:

<AdditionalDependencies>gurobi_c++mdd2017.lib;gurobi130.lib;%(AdditionalDependencies)</AdditionalDependencies>

Release 配置:

<AdditionalDependencies>gurobi_c++md2017.lib;gurobi130.lib;%(AdditionalDependencies)</AdditionalDependencies>

含义:

  • 明确告诉链接器本项目需要哪些 Gurobi 库

当前库说明:

  • gurobi130.lib:Gurobi 13.0 主库
  • gurobi_c++mdd2017.lib:Debug C++ API 封装库
  • gurobi_c++md2017.lib:Release C++ API 封装库

注意:

  • 库名并不总是随 Visual Studio 版本简单变化,必须以本机 lib 目录中的实际文件名为准。
  • Debug/Release 混用错误库是最常见的链接问题之一。
LocalDebuggerEnvironment
<LocalDebuggerEnvironment>PATH=$(GUROBI_HOME)\bin;%PATH%
GRB_LICENSE_FILE=$(GRB_LICENSE_FILE)</LocalDebuggerEnvironment>

含义:

  • 给 Visual Studio 本地调试器注入额外环境变量

具体效果:

  • 运行调试时,PATH 会临时包含 $(GUROBI_HOME)\bin
  • GRB_LICENSE_FILE 会传递给调试进程

好处:

  • 即使系统级环境变量没有完全配置好,VS 内启动程序仍有很大概率直接运行成功
CopyGurobiRuntime
<Target Name="CopyGurobiRuntime" AfterTargets="Build" Condition="'$(ConfigurationType)' == 'Application'">
  <Copy SourceFiles="$(GUROBI_HOME)\bin\gurobi130.dll" DestinationFolder="$(OutDir)" SkipUnchangedFiles="true" />
</Target>

含义:

  • 在应用程序项目编译结束后,把 gurobi130.dll 复制到输出目录

作用:

  • 避免运行时由于 DLL 不在可执行文件附近而报错

注意:

  • 这里只对 Application 类型项目生效,不会对静态库项目执行复制

4.4 Visual Studio 2022 中的正确配置方式

当前工程已经通过 gurobi.props 自动完成主要配置,因此最推荐的方式不是手工给每个项目加路径,而是直接使用现有属性表。

建议步骤:

  1. 打开 UavMissionPlanning.sln
  2. 选择 Debug|x64Release|x64
  3. 确保 UavModelUavExperiments 均导入 ..\build\gurobi.props
  4. 将启动项目设为 UavExperiments
  5. 执行“生成解决方案”
  6. 启动调试或直接运行

如果你需要手工核对 VS 中的设置,可以检查:

  • C/C++ -> 常规 -> 附加包含目录
  • 链接器 -> 常规 -> 附加库目录
  • 链接器 -> 输入 -> 附加依赖项
  • 调试 -> 环境

这些值最终都应能映射到 gurobi.props 中的内容。

4.5 许可证配置和运行时依赖管理

本工程当前推荐的许可证路径为:

C:\gurobi1300\win64\bin\gurobi.lic

推荐的许可证配置方式有两层:

  1. 系统层
    • 设置用户环境变量 GRB_LICENSE_FILE
  2. 工程层
    • 使用 gurobi.props 中的默认兜底逻辑

运行时依赖主要是:

  • gurobi130.dll

当前工程的处理方式:

  • 在 Visual Studio 调试环境中把 $(GUROBI_HOME)\bin 加入 PATH
  • 构建后复制 gurobi130.dll 到输出目录

这两层同时存在,能显著降低运行时报 DLL 缺失的概率。

许可证与运行时依赖关系可以简化成:

gurobi.lic

GRB_LICENSE_FILE

gurobi.props fallback

LocalDebuggerEnvironment

gurobi130.dll

output dir

solver process

5. 项目运行和调试指导

5.1 如何编译和运行 UavExperiments

在 Visual Studio 中运行

推荐步骤:

  1. 打开 UavMissionPlanning.sln
  2. 选择 Debug|x64
  3. 右键 UavExperiments,设置为启动项目
  4. 生成解决方案
  5. 启动调试或执行不调试运行

执行路径如下:

open solution

build solution

set startup project

launch UavExperiments

create GRBEnv

build instance

build model

optimize

print result

命令行编译

如果系统中没有把 msbuild 加入 PATH,可以直接调用 Visual Studio 自带的 MSBuild.exe

& "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\MSBuild.exe" `
  "d:\ATKdevelop\三维飞机复现\UavMissionPlanning\UavMissionPlanning.sln" `
  /p:Configuration=Debug /p:Platform=x64
命令行运行
$env:GRB_LICENSE_FILE = "C:\gurobi1300\win64\bin\gurobi.lic"
& "d:\ATKdevelop\三维飞机复现\UavMissionPlanning\x64\Debug\UavExperiments.exe"

5.2 Demo 实例的执行方法

默认运行 UavExperiments 时,会执行 runDemo()

  • 创建 GRBEnv
  • 调用 makePaperDemoInstance()
  • 调用 ParameterBuilder::build()
  • 启用大多数扩展项
  • 构造 MissionFlightModel
  • 调用 build()optimize()
  • 输出 StatusObjective

这个模式适合:

  • 验证 Gurobi 是否正确接入
  • 观察完整模型装配路径
  • 做最基本的功能烟雾测试

runDemo() 的流程图如下:

runDemo

GRBEnv

makePaperDemoInstance

ParameterBuilder build

enable main options

MissionFlightModel

build

optimize

read status objective

5.3 RCP 研究的执行方法

命令行带上 rcp 参数时,会执行 runRcp()

& "d:\ATKdevelop\三维飞机复现\UavMissionPlanning\x64\Debug\UavExperiments.exe" rcp

也可以带参数:

& "d:\ATKdevelop\三维飞机复现\UavMissionPlanning\x64\Debug\UavExperiments.exe" rcp 4 20

含义:

  • 第一个数字:无人机数量
  • 第二个数字:最大离散时间段数

执行逻辑:

  • 从较小 intervals 开始循环
  • 每次调用 makeRcp2dInstance(uavCount, intervals)
  • 关闭部分与 RCP 研究无关的扩展,如航点、禁飞区、风区和范围扩展
  • 保留燃油与避碰逻辑
  • 找到第一个可接受状态时返回结果

这个模式适合:

  • 观察模型在不同时间离散粒度下的行为
  • 快速做序贯规模实验

runRcp() 的流程图如下:

yes

no

exhausted

runRcp

loop intervals

makeRcp2dInstance

ParameterBuilder build

disable non RCP features

MissionFlightModel

build

optimize

status acceptable

return intervals

continue

return -1

5.4 调试建议

如果要调试建模逻辑,推荐按下面顺序断点:

  1. UavExperiments/src/main.cpp
  2. MissionFlightModel 构造函数
  3. MissionFlightModel::build()
  4. MissionFlightModel::buildVariables()
  5. addBaseConstraints()
  6. 对应扩展模块函数
  7. setObjective()
  8. optimize()

这样可以清楚看到:

  • 实例内容是否正确
  • 变量规模是否符合预期
  • 某个开关是否真的生效
  • 哪类约束导致求解失败或变慢

推荐的调试断点路线如下:

main

runDemo or runRcp

MissionFlightModel ctor

build

buildVariables

addBaseConstraints

optional extension calls

setObjective

optimize

6. 常见错误排查和解决方案

6.1 No Gurobi license found

原因:

  • 程序找不到许可证

优先检查:

  • gurobi.lic 是否存在
  • GRB_LICENSE_FILE 是否正确
  • gurobi.props 中的默认许可证路径是否有效

推荐路径:

C:\gurobi1300\win64\bin\gurobi.lic

6.2 cannot open include file: gurobi_c++.h

原因:

  • 编译器没有找到 Gurobi 头文件目录

优先检查:

  • build/gurobi.props 是否被正确导入
  • GUROBI_HOME 是否正确
  • $(GUROBI_HOME)\include 是否存在

6.3 unresolved external symbol

原因:

  • 链接阶段缺少 .lib 或 Debug/Release 配置不匹配

优先检查:

  • Debug 是否使用了 gurobi_c++mdd2017.lib
  • Release 是否使用了 gurobi_c++md2017.lib
  • gurobi130.lib 是否存在于 lib 目录

6.4 程序运行时报缺少 DLL

原因:

  • gurobi130.dll 不在运行时可搜索路径中

优先检查:

  • CopyGurobiRuntime 是否执行成功
  • 输出目录是否存在 gurobi130.dll
  • LocalDebuggerEnvironment 是否把 bin 目录加入了 PATH

6.5 程序能运行但选项效果不明显

原因可能包括:

  • 部分开关是“脚手架式”或边界控制,而不是完全独立的功能模块

当前需要特别说明的点:

  • addRangeExtensions() 当前基本是空实现,范围约束已在基础模型中处理
  • normDirectionCount2d 已定义但尚未真正驱动 NormLinearizer
  • restrictedOutsidewindFaceInside 虽在 VariableStore 中声明,但当前相关约束实现仍是现场创建局部二进制变量

这类现象不是编译错误,但会影响后续开发者对代码完备度的判断。

可以按下面的排障路径快速定位问题:

start issue

build fails

run fails

behavior odd

include path or props

lib mismatch

license path

dll path

feature switch caveat

7. 代码阅读路径建议

7.1 面向新开发者的推荐学习顺序

建议按下面顺序阅读:

  1. README.md
    先理解工程目标、工具链和文档入口。
  2. docs/paper-formulation.md
    建立论文公式与代码模块的对应关系。
  3. UavCore/include/core/instance_data.h
    先弄清楚问题输入由哪些结构组成。
  4. UavCore/include/core/time_grid.h
    理解粗时间层和细时间层的离散映射。
  5. UavCore/src/parameter_builder.cpp
    理解预处理逻辑和示例实例来源。
  6. UavModel/include/model/mission_flight_model.h
    建立模型门面的整体认识。
  7. UavModel/src/mission_flight_model.cpp
    理解模型装配顺序和变量创建方式。
  8. UavModel/src/constraints/base_model.cpp
    先看骨架约束和目标函数。
  9. UavModel/src/constraints/flight_dynamics.cpp
    再看飞行动力学扩展。
  10. UavModel/src/constraints/fuel_range_environment.cpp
    最后理解燃油、环境和避碰扩展。
  11. UavExperiments/src/main.cpp
    回到主入口,形成完整闭环。

7.2 面向模型开发者的阅读顺序

如果你接手后的工作重点是“改模型、加约束、改变量”,推荐顺序略有不同:

  1. mission_flight_model.h
  2. mission_flight_model.cpp
  3. variable_store.h
  4. model_context.h
  5. norm_linearizer.h
  6. base_model.cpp
  7. flight_dynamics.cpp
  8. fuel_range_environment.cpp

这种顺序更适合快速理解:

  • 变量规模
  • 约束入口
  • 模块边界
  • 哪些位置适合扩展

7.3 关键算法与约束实现的解读

基础模型

base_model.cpp 负责基础骨架,主要包括:

  • 起点和终点位置约束
  • 基于细时间层的运动学更新
  • 航程近似与通信范围限制
  • 高度、速度、加速度边界
  • 起飞/在航状态逻辑
  • 速度带和高度带选择
  • 航点访问逻辑
  • 目标函数

这是最应该先吃透的部分,因为大多数扩展约束都建立在它之上。

飞行动力学扩展

flight_dynamics.cpp 增加的是“基础模型之外的更细致动态约束”,包括:

  • 过渡窗口内的最小速度激活逻辑
  • 不同带下的最大速度、最大下降率、最大上升率
  • 结合状态的最小高度和加速度约束
燃油、环境与避碰扩展

fuel_range_environment.cpp 将多个主题放在一起:

  • 燃油递推
  • 燃油对高度、速度、加速度上界的影响
  • 禁飞区外部性约束
  • 风区激活逻辑
  • 多机避碰逻辑

阅读这一部分时,建议先按函数拆分理解,而不是把它看成一整块。

7.4 论文模型到代码实现的映射关系

当前工程已通过 docs/paper-formulation.md 给出论文到代码的映射思路。

建议这样使用这份映射文档:

  1. 先看论文中某一组公式属于哪个主题
  2. 再定位到对应的约束实现文件
  3. 最后结合 MissionFlightModel::build() 中的调用顺序理解它在整体模型中的位置

一个实用的映射规则是:

  • 论文中的基础模型约束,优先看 base_model.cpp
  • 论文中的飞行动力学修正,优先看 flight_dynamics.cpp
  • 论文中的燃油、环境和碰撞约束,优先看 fuel_range_environment.cpp

如果想把学习路径也可视化,可以按下面的顺序推进:

README

paper formulation

instance_data and time_grid

parameter_builder

mission_flight_model.h

mission_flight_model.cpp

constraints cpp

UavExperiments main

7.5 论文公式与实际源码片段对照

这一节专门解决“论文里的公式到底落在代码哪里”这个问题。下面按 docs/paper-formulation.md 中的公式分组顺序,给出对应的实际源码片段。阅读方式建议如下:

  1. 先定位论文公式编号
  2. 再看本节对应的源码片段
  3. 最后回到对应的 .cpp 文件阅读全文
时间层映射 tau(tf) = floor(tf / (nf + 1))

对应文件:

  • UavCore/include/core/time_grid.h

这部分代码实现了论文中粗时间层 T 和细时间层 Tf 的索引映射关系,尤其是 coarseOfFine()fineOfCoarse()

[[nodiscard]] int coarseOfFine(int fineStep) const {
    if (fineStep < 0 || fineStep > fineIntervals()) {
        throw std::out_of_range("fineStep out of range");
    }
    return fineStep / refinementFactor();
}

[[nodiscard]] int fineOfCoarse(int coarseStep) const {
    if (coarseStep < 0 || coarseStep > coarseIntervals_) {
        throw std::out_of_range("coarseStep out of range");
    }
    return coarseStep * refinementFactor();
}
公式组 (2.1) - (2.2):起点与终点状态约束

对应文件:

  • UavModel/src/constraints/base_model.cpp

这段代码把每架无人机在首末细时间点的位置固定到起点和终点,对应论文中的初始状态和终止状态约束。

model_.addConstr(vars_.rx[vars_.fineIndex(u, 0)] == vehicle.start.x, "start_x_" + std::to_string(u));
model_.addConstr(vars_.ry[vars_.fineIndex(u, 0)] == vehicle.start.y, "start_y_" + std::to_string(u));
model_.addConstr(vars_.rz[vars_.fineIndex(u, 0)] == vehicle.start.z, "start_z_" + std::to_string(u));

const int finalTf = Tf - 1;
model_.addConstr(vars_.rx[vars_.fineIndex(u, finalTf)] == vehicle.finish.x, "end_x_" + std::to_string(u));
model_.addConstr(vars_.ry[vars_.fineIndex(u, finalTf)] == vehicle.finish.y, "end_y_" + std::to_string(u));
model_.addConstr(vars_.rz[vars_.fineIndex(u, finalTf)] == vehicle.finish.z, "end_z_" + std::to_string(u));
公式组 (2.3) - (2.9):运动学、航程、高度、速度、加速度

对应文件:

  • UavModel/src/constraints/base_model.cpp

这一组公式在代码中主要分成两部分:一部分是几何/上界约束,另一部分是状态递推方程。

  1. 航程、高度、速度、加速度上界:
const GRBLinExpr rangeBound =
    derived_.byUav[static_cast<std::size_t>(u)].rangeApproximation.slope * vars_.rz[vars_.fineIndex(u, tf)] +
    derived_.byUav[static_cast<std::size_t>(u)].rangeApproximation.intercept;
GRBVar rangeNorm =
    model_.addVar(0.0, GRB_INFINITY, 0.0, GRB_CONTINUOUS, "rangeNorm_" + std::to_string(u) + "_" + std::to_string(tf));
GRBVar dx =
    model_.addVar(-GRB_INFINITY, GRB_INFINITY, 0.0, GRB_CONTINUOUS, "rangeDx_" + std::to_string(u) + "_" + std::to_string(tf));
GRBVar dy =
    model_.addVar(-GRB_INFINITY, GRB_INFINITY, 0.0, GRB_CONTINUOUS, "rangeDy_" + std::to_string(u) + "_" + std::to_string(tf));
model_.addConstr(dx == vars_.rx[vars_.fineIndex(u, tf)] - vehicle.groundControl.x);
model_.addConstr(dy == vars_.ry[vars_.fineIndex(u, tf)] - vehicle.groundControl.y);
linearizer_.addAbsEnvelope2d(dx, dy, rangeNorm, "range_" + std::to_string(u) + "_" + std::to_string(tf));
model_.addConstr(rangeNorm <= rangeBound, "range_cap_" + std::to_string(u) + "_" + std::to_string(tf));

model_.addConstr(vars_.rz[vars_.fineIndex(u, tf)] >= type.minAltitude * vars_.airborne[vars_.coarseIndex(u, tc)]);
model_.addConstr(vars_.rz[vars_.fineIndex(u, tf)] <= type.maxAltitude * vars_.airborne[vars_.coarseIndex(u, tc)]);

linearizer_.addAbsEnvelope2d(vars_.vx[vars_.fineIndex(u, tf)],
                             vars_.vy[vars_.fineIndex(u, tf)],
                             vars_.speed[vars_.fineIndex(u, tf)],
                             "speed_" + std::to_string(u) + "_" + std::to_string(tf));

linearizer_.addAbsEnvelope2d(vars_.ax[vars_.fineIndex(u, tf)],
                             vars_.ay[vars_.fineIndex(u, tf)],
                             vars_.accel[vars_.fineIndex(u, tf)],
                             "accel_" + std::to_string(u) + "_" + std::to_string(tf));

model_.addConstr(vars_.speed[vars_.fineIndex(u, tf)] >= type.minVelocity * vars_.airborne[vars_.coarseIndex(u, tc)]);
model_.addConstr(vars_.speed[vars_.fineIndex(u, tf)] <= type.maxVelocity * vars_.airborne[vars_.coarseIndex(u, tc)]);
model_.addConstr(vars_.accel[vars_.fineIndex(u, tf)] <= type.maxAcceleration * vars_.airborne[vars_.coarseIndex(u, tc)]);
  1. 运动学状态递推:
model_.addConstr(vars_.rx[vars_.fineIndex(u, tf + 1)] ==
                 vars_.rx[vars_.fineIndex(u, tf)] +
                     dtFine * vars_.vx[vars_.fineIndex(u, tf)] +
                     dtFine * windContribution(instance_, vars_, u, tc, 0) +
                     0.5 * dtFine * dtFine * vars_.ax[vars_.fineIndex(u, tf)]);
model_.addConstr(vars_.ry[vars_.fineIndex(u, tf + 1)] ==
                 vars_.ry[vars_.fineIndex(u, tf)] +
                     dtFine * vars_.vy[vars_.fineIndex(u, tf)] +
                     dtFine * windContribution(instance_, vars_, u, tc, 1) +
                     0.5 * dtFine * dtFine * vars_.ay[vars_.fineIndex(u, tf)]);
model_.addConstr(vars_.vx[vars_.fineIndex(u, tf + 1)] ==
                 vars_.vx[vars_.fineIndex(u, tf)] + dtFine * vars_.ax[vars_.fineIndex(u, tf)]);
model_.addConstr(vars_.vy[vars_.fineIndex(u, tf + 1)] ==
                 vars_.vy[vars_.fineIndex(u, tf)] + dtFine * vars_.ay[vars_.fineIndex(u, tf)]);
model_.addConstr(vars_.rz[vars_.fineIndex(u, tf + 1)] ==
                 vars_.rz[vars_.fineIndex(u, tf)] +
                     dtFine * (vars_.vzPlus[vars_.fineIndex(u, tf)] - vars_.vzMinus[vars_.fineIndex(u, tf)]));
公式组 (2.10) - (2.12):起飞/在航/降落状态逻辑

对应文件:

  • UavModel/src/constraints/base_model.cpp

这组公式通过 startedactiveairborne 三类粗时间层二进制变量建立起飞和结束逻辑。

for (int tc = 0; tc < Tc; ++tc) {
    model_.addConstr(vars_.airborne[vars_.coarseIndex(u, tc)] ==
                     vars_.started[vars_.coarseIndex(u, tc)] + vars_.active[vars_.coarseIndex(u, tc)] - 1.0);
}
for (int tc = 0; tc < Tc - 1; ++tc) {
    model_.addConstr(vars_.active[vars_.coarseIndex(u, tc + 1)] <= vars_.active[vars_.coarseIndex(u, tc)]);
    model_.addConstr(vars_.started[vars_.coarseIndex(u, tc)] <= vars_.started[vars_.coarseIndex(u, tc + 1)]);
}
公式组 (2.13) - (2.18):航点访问、禁飞区、碰撞避免

对应文件:

  • UavModel/src/constraints/base_model.cpp
  • UavModel/src/constraints/fuel_range_environment.cpp

这一组公式在工程里不是集中写在同一个函数中,而是按主题拆成多个实现。

  1. 航点访问约束:
const GRBVar& visit = vars_.visit[vars_.visitIndex(u, w, tc)];
assigned += visit;
if (tc < waypoint.window.coarseStart || tc > waypoint.window.coarseEnd) {
    model_.addConstr(visit == 0.0, "visit_tw_" + std::to_string(u) + "_" + std::to_string(w) + "_" + std::to_string(tc));
    continue;
}

const int tf = grid.fineOfCoarse(tc);
GRBVar distNorm =
    model_.addVar(0.0, GRB_INFINITY, 0.0, GRB_CONTINUOUS, "visitDist_" + std::to_string(u) + "_" + std::to_string(w) + "_" + std::to_string(tc));
// ...
linearizer_.addAbsEnvelope3d(dx, dy, dz, distNorm,
                             "visit_norm_" + std::to_string(u) + "_" + std::to_string(w) + "_" + std::to_string(tc));
model_.addConstr(distNorm <= vehicle.waypointOperationalRange +
                                 instance_.bigM.dist * (1.0 - visit));
// ...
model_.addConstr(assigned <= 1.0, "assign_once_" + std::to_string(w));
  1. 禁飞区约束:
GRBLinExpr outsideCount = 0.0;
for (int face = 0; face < static_cast<int>(zone.faces.size()); ++face) {
    const auto& halfspace = zone.faces[static_cast<std::size_t>(face)];
    GRBVar selector = context_.model->addVar(
        0.0, 1.0, 0.0, GRB_BINARY,
        "restricted_" + std::to_string(u) + "_" + std::to_string(q) + "_" + std::to_string(face) + "_" + std::to_string(tc));
    outsideCount += selector;
    context_.model->addConstr(
        halfspace.normal.x * vars_.rx[vars_.fineIndex(u, tf)] +
            halfspace.normal.y * vars_.ry[vars_.fineIndex(u, tf)] +
            halfspace.normal.z * vars_.rz[vars_.fineIndex(u, tf)] >=
            halfspace.rhs - instance_.bigM.dist * (1.0 - selector));
}
context_.model->addConstr(outsideCount >= 1.0,
                          "restricted_outside_" + std::to_string(u) + "_" + std::to_string(q) + "_" + std::to_string(tc));
  1. 碰撞避免约束:
context_.model->addConstr(vars_.rx[vars_.fineIndex(u1, tf)] - vars_.rx[vars_.fineIndex(u2, tf)] >=
                          instance_.safetyDistance.x - instance_.bigM.dist * (1.0 - exPos));
context_.model->addConstr(vars_.ry[vars_.fineIndex(u1, tf)] - vars_.ry[vars_.fineIndex(u2, tf)] >=
                          instance_.safetyDistance.y - instance_.bigM.dist * (1.0 - eyPos));
context_.model->addConstr(vars_.rz[vars_.fineIndex(u1, tf)] - vars_.rz[vars_.fineIndex(u2, tf)] >=
                          instance_.safetyDistance.z - instance_.bigM.dist * (1.0 - ezPos));
// ...
context_.model->addConstr(exPos + eyPos + ezPos + exNeg + eyNeg + ezNeg >=
                          vars_.airborne[vars_.coarseIndex(u1, tc)] + vars_.airborne[vars_.coarseIndex(u2, tc)] - 1.0,
                          "collision_logic_" + std::to_string(u1) + "_" + std::to_string(u2) + "_" + std::to_string(tc));
公式组 (2.19) - (2.23):燃油初值、带选择、速度带与高度带

对应文件:

  • UavModel/src/constraints/base_model.cpp

这组公式主要通过 band 变量以及速度/高度联动约束实现。

if (!options_.enableFuelDependentExtensions) {
    model_.addConstr(vars_.fuel[vars_.fineIndex(u, 0)] == type.maxFuel, "fuel_init_" + std::to_string(u));
}

for (int tc = 0; tc < Tc; ++tc) {
    GRBLinExpr bandSum = 0.0;
    for (int i = 0; i < static_cast<int>(type.altitudeBands.size()); ++i) {
        const auto& altitudeBand = type.altitudeBands[static_cast<std::size_t>(i)];
        for (int j = 0; j < static_cast<int>(altitudeBand.throttleBands.size()); ++j) {
            bandSum += vars_.band[vars_.bandIndex(u, i, j, tc)];
        }
    }
    model_.addConstr(bandSum == vars_.airborne[vars_.coarseIndex(u, tc)], "band_sum_" + std::to_string(u) + "_" + std::to_string(tc));
}

GRBLinExpr speedBandExpr = 0.0;
GRBLinExpr altLower = 0.0;
GRBLinExpr altUpper = 0.0;
for (int i = 0; i < static_cast<int>(type.altitudeBands.size()); ++i) {
    const auto& altitudeBand = type.altitudeBands[static_cast<std::size_t>(i)];
    for (int j = 0; j < static_cast<int>(altitudeBand.throttleBands.size()); ++j) {
        const GRBVar& selector = vars_.band[vars_.bandIndex(u, i, j, tc)];
        speedBandExpr += altitudeBand.throttleBands[static_cast<std::size_t>(j)].speed * selector;
        altLower += altitudeBand.lower * selector;
        altUpper += altitudeBand.upper * selector;
    }
}
model_.addConstr(vars_.speed[vars_.fineIndex(u, tf)] == speedBandExpr, "speed_band_" + std::to_string(u) + "_" + std::to_string(tf));
model_.addConstr(vars_.rz[vars_.fineIndex(u, tf)] >= altLower, "alt_low_" + std::to_string(u) + "_" + std::to_string(tf));
model_.addConstr(vars_.rz[vars_.fineIndex(u, tf)] <= altUpper + type.maxAltitude * (1.0 - vars_.airborne[vars_.coarseIndex(u, tc)]),
                 "alt_high_" + std::to_string(u) + "_" + std::to_string(tf));
公式 (3):基础目标函数

对应文件:

  • UavModel/src/constraints/base_model.cpp

这段代码实现了基础目标函数,包括航点收益、在航惩罚、燃油/高度/速度项。

for (int w = 0; w < vars_.waypointCount(); ++w) {
    const double score = instance_.waypoints[static_cast<std::size_t>(w)].score;
    for (int tc = 0; tc < vars_.coarsePointCount(); ++tc) {
        objective += score * vars_.visit[vars_.visitIndex(u, w, tc)];
    }
}

for (int tc = 0; tc < vars_.coarsePointCount(); ++tc) {
    objective -= (static_cast<double>(tc) / static_cast<double>(std::max(1, grid.coarseIntervals()))) *
                 instance_.weights.air * vars_.airborne[vars_.coarseIndex(u, tc)];
}

for (int tf = 0; tf < vars_.finePointCount(); ++tf) {
    objective += (1.0 / instance_.weights.fuel) * vars_.fuel[vars_.fineIndex(u, tf)];
    objective += (1.0 / instance_.weights.altitude) * vars_.rz[vars_.fineIndex(u, tf)];
    objective -= (1.0 / instance_.weights.velocity) * vars_.speed[vars_.fineIndex(u, tf)];
}

model_.setObjective(objective, GRB_MAXIMIZE);
公式 (2.24):最小速度激活窗口

对应文件:

  • UavModel/src/constraints/flight_dynamics.cpp

这段代码实现了过渡窗口上的最小速度激活条件,是飞行动力学扩展的入口。

const int transitionSteps =
    static_cast<int>(std::ceil(type.minVelocity / std::max(1.0, type.maxAcceleration * instance_.coarseDeltaT)));

for (int tc = transitionSteps; tc < Tc - transitionSteps; ++tc) {
    GRBLinExpr window = 0.0;
    for (int k = -transitionSteps; k <= transitionSteps; ++k) {
        window += vars_.airborne[vars_.coarseIndex(u, tc + k)];
    }
    model_.addConstr(vars_.minSpeedActive[vars_.coarseIndex(u, tc)] >= window - 2.0 * transitionSteps,
                     "prop1_" + std::to_string(u) + "_" + std::to_string(tc));
}
公式组 (2.8')(2.4')(2.8'')(2.22')(2.25)(2.26)(2.9')

对应文件:

  • UavModel/src/constraints/flight_dynamics.cpp

这些修正式都集中体现在飞行动力学扩展中:速度上界、最低高度、爬升/下降率和加速度上界都会根据速度带和状态切换而调整。

GRBLinExpr maxSpeedExpr = 0.0;
GRBLinExpr maxDescentExpr = 0.0;
GRBLinExpr maxClimbExpr = type.takeoffClimbRate;

for (int i = 0; i < static_cast<int>(type.altitudeBands.size()); ++i) {
    const auto& altitudeBand = type.altitudeBands[static_cast<std::size_t>(i)];
    const double altitudeBandMaxSpeed = altitudeBand.throttleBands.empty()
                                            ? type.maxVelocity
                                            : altitudeBand.throttleBands.back().speed;
    for (int j = 0; j < static_cast<int>(altitudeBand.throttleBands.size()); ++j) {
        const auto& throttleBand = altitudeBand.throttleBands[static_cast<std::size_t>(j)];
        const GRBVar& selector = vars_.band[vars_.bandIndex(u, i, j, tc)];
        maxSpeedExpr += altitudeBandMaxSpeed * selector;
        maxDescentExpr += throttleBand.descentRate * selector;
        maxClimbExpr += -(type.takeoffClimbRate - throttleBand.climbRate) * selector;
    }
}

model_.addConstr(vars_.speed[vars_.fineIndex(u, tf)] >=
                 type.minVelocity * vars_.minSpeedActive[vars_.coarseIndex(u, tc)],
                 "min_speed_ext_" + std::to_string(u) + "_" + std::to_string(tf));
model_.addConstr(vars_.speed[vars_.fineIndex(u, tf)] <= maxSpeedExpr +
                                                  type.maxVelocity * (1.0 - vars_.airborne[vars_.coarseIndex(u, tc)]),
                 "max_speed_ext_" + std::to_string(u) + "_" + std::to_string(tf));
model_.addConstr(vars_.rz[vars_.fineIndex(u, tf)] >=
                 type.minAltitude * vars_.minSpeedActive[vars_.coarseIndex(u, tc)],
                 "min_alt_ext_" + std::to_string(u) + "_" + std::to_string(tf));
model_.addConstr(vars_.vzMinus[vars_.fineIndex(u, tf)] <= maxDescentExpr,
                 "descent_band_" + std::to_string(u) + "_" + std::to_string(tf));
model_.addConstr(vars_.vzPlus[vars_.fineIndex(u, tf)] <= maxClimbExpr,
                 "climb_band_" + std::to_string(u) + "_" + std::to_string(tf));
model_.addConstr(vars_.accel[vars_.fineIndex(u, tf)] <=
                 type.maxAcceleration *
                     (vars_.airborne[vars_.coarseIndex(u, tc)] -
                      0.05 * vars_.minSpeedActive[vars_.coarseIndex(u, tc)]),
                 "accel_takeoff_cruise_" + std::to_string(u) + "_" + std::to_string(tf));
公式组 (2.20i) - (2.20iv):按在航状态分裂的燃油递推

对应文件:

  • UavModel/src/constraints/fuel_range_environment.cpp

这一组四条不等式共同实现论文中分段/Big-M 风格的燃油递推。

const GRBLinExpr burn =
    dtFine * (type.climbFuelSurplus * vars_.vzPlus[vars_.fineIndex(u, tf)] + bandFuel +
              type.phiFuel * vars_.fuel[vars_.fineIndex(u, tf)]);

model_.addConstr(vars_.fuel[vars_.fineIndex(u, tf + 1)] <=
                 type.maxFuel * (1.0 - vars_.airborne[vars_.coarseIndex(u, tc)]) +
                     vars_.fuel[vars_.fineIndex(u, tf)] - burn,
                 "fuel_dyn_ub1_" + std::to_string(u) + "_" + std::to_string(tf));
model_.addConstr(vars_.fuel[vars_.fineIndex(u, tf + 1)] >=
                 -type.maxFuel * (1.0 - vars_.airborne[vars_.coarseIndex(u, tc)]) +
                     vars_.fuel[vars_.fineIndex(u, tf)] - burn,
                 "fuel_dyn_lb1_" + std::to_string(u) + "_" + std::to_string(tf));
model_.addConstr(vars_.fuel[vars_.fineIndex(u, tf + 1)] <=
                 vars_.fuel[vars_.fineIndex(u, tf)] + type.maxFuel * vars_.airborne[vars_.coarseIndex(u, tc)],
                 "fuel_dyn_ub2_" + std::to_string(u) + "_" + std::to_string(tf));
model_.addConstr(vars_.fuel[vars_.fineIndex(u, tf + 1)] >=
                 vars_.fuel[vars_.fineIndex(u, tf)] - type.maxFuel * vars_.airborne[vars_.coarseIndex(u, tc)],
                 "fuel_dyn_lb2_" + std::to_string(u) + "_" + std::to_string(tf));
公式 (2.27):燃油相关的可达高度上界

对应文件:

  • UavModel/src/constraints/fuel_range_environment.cpp
const double altitudeDenom = type.maxFuel * std::max(1e-6, 1.0 - type.phiAlt1);
GRBLinExpr maxReachableAltitude =
    type.maxAltitude *
    (1.0 + ((1.0 - type.phiAlt2) / altitudeDenom) *
               (type.phiAlt1 * type.maxFuel - vars_.fuel[vars_.fineIndex(u, tf)]));
model_.addConstr(vars_.rz[vars_.fineIndex(u, tf)] <= maxReachableAltitude,
                 "fuel_altitude_" + std::to_string(u) + "_" + std::to_string(tf));
公式组 (2.28) - (2.29):燃油相关的速度/加速度上界

对应文件:

  • UavModel/src/constraints/fuel_range_environment.cpp
GRBLinExpr massFactor =
    1.0 + (phiAcc / (type.maxFuel + type.emptyWeight)) * vars_.fuel[vars_.fineIndex(u, tf)];
model_.addConstr(vars_.speed[vars_.fineIndex(u, tf)] <= type.maxVelocity * massFactor +
                                                  type.maxVelocity * (1.0 - vars_.airborne[vars_.coarseIndex(u, tc)]),
                 "fuel_speed_" + std::to_string(u) + "_" + std::to_string(tf));
model_.addConstr(vars_.accel[vars_.fineIndex(u, tf)] <= type.maxAcceleration * massFactor,
                 "fuel_accel_" + std::to_string(u) + "_" + std::to_string(tf));
公式 (3'):带可变初始燃油的扩展目标函数

对应文件:

  • UavModel/src/constraints/base_model.cpp

当启用燃油相关扩展时,目标函数会额外惩罚初始燃油量,这一项就是论文中扩展目标函数的代码体现。

if (options_.enableFuelDependentExtensions) {
    const auto& type = typeOf(u);
    objective -= (1.0 / (instance_.weights.fuel * std::max(1.0, type.maxFuel))) *
                 vars_.fuel[vars_.fineIndex(u, 0)];
}
几何处理:范数线性化、禁飞区、风区

对应文件:

  • UavModel/src/mission_flight_model.cpp
  • UavModel/src/constraints/fuel_range_environment.cpp

docs/paper-formulation.md 中还特别强调了几何处理方式。对应实现主要有三块。

  1. NormLinearizer 的二维/三维绝对值包络:
void NormLinearizer::addAbsEnvelope2d(GRBVar x, GRBVar y, GRBVar norm, const std::string& tag) const {
    GRBModel& model = *context_.model;
    GRBVar ax = model.addVar(0.0, GRB_INFINITY, 0.0, GRB_CONTINUOUS, tag + "_ax");
    GRBVar ay = model.addVar(0.0, GRB_INFINITY, 0.0, GRB_CONTINUOUS, tag + "_ay");

    model.addConstr(ax >= x, tag + "_ax_pos");
    model.addConstr(ax >= -x, tag + "_ax_neg");
    model.addConstr(ay >= y, tag + "_ay_pos");
    model.addConstr(ay >= -y, tag + "_ay_neg");

    model.addConstr(norm >= ax, tag + "_norm_ge_ax");
    model.addConstr(norm >= ay, tag + "_norm_ge_ay");
    model.addConstr(norm >= (ax + ay) / std::sqrt(2.0), tag + "_norm_ge_diag");
    model.addConstr(norm <= ax + ay, tag + "_norm_le_l1");
}
  1. 风区激活逻辑:
GRBLinExpr insideCount = 0.0;
for (int face = 0; face < static_cast<int>(zone.faces.size()); ++face) {
    const auto& halfspace = zone.faces[static_cast<std::size_t>(face)];
    GRBVar inside = context_.model->addVar(
        0.0, 1.0, 0.0, GRB_BINARY,
        "windFace_" + std::to_string(u) + "_" + std::to_string(p) + "_" + std::to_string(face) + "_" + std::to_string(tc));
    insideCount += inside;
    context_.model->addConstr(
        halfspace.normal.x * vars_.rx[vars_.fineIndex(u, tf)] +
            halfspace.normal.y * vars_.ry[vars_.fineIndex(u, tf)] +
            halfspace.normal.z * vars_.rz[vars_.fineIndex(u, tf)] <=
            halfspace.rhs + instance_.bigM.dist * (1.0 - inside));
}

context_.model->addConstr(insideCount >= static_cast<double>(zone.faces.size()) * active,
                          "wind_inside_lb_" + std::to_string(u) + "_" + std::to_string(p) + "_" + std::to_string(tc));
context_.model->addConstr(insideCount <= static_cast<double>(zone.faces.size()) + active - 1.0,
                          "wind_inside_ub_" + std::to_string(u) + "_" + std::to_string(p) + "_" + std::to_string(tc));

通过这几个片段,读者可以直接把论文中的公式编号映射到具体源码位置,也能更快地验证某条理论约束在代码中是否真的被实现。

8. 已知边界与维护建议

为了避免后续读者误判当前实现的完备度,下面几点需要明确写出:

  • UavTests 只覆盖 UavCore 的轻量逻辑,不覆盖求解器集成测试。
  • 当前实例数据主要来自代码内置工厂,而不是外部文件加载器。
  • addRangeExtensions() 目前基本为空,范围限制已由基础模型处理。
  • normDirectionCount2d 目前尚未在 NormLinearizer 中真正生效。
  • VariableStore 中部分环境相关变量具有脚手架性质,当前实现仍有局部变量现场创建的情况。

后续维护建议:

  • 如果增加新的数据来源,优先在 UavCore 中转换为 InstanceData
  • 如果新增约束,尽量继续按主题拆分到 src/constraints/
  • 如果升级 Gurobi,优先修改 build/gurobi.props
  • 如果加强测试体系,建议新增真正覆盖 UavModel 和求解流程的集成测试

9. 总结

UavMissionPlanning 的整体思路可以概括为:

  • UavCore 定义问题与预处理
  • UavModel 负责 Gurobi 建模
  • UavExperiments 负责组织实验与执行求解
  • UavTests 负责轻量验证
  • build/gurobi.props 负责统一管理 Gurobi 接入

对于后续开发者来说,只要先看懂“输入数据如何变成模型变量与约束”,再掌握 gurobi.props 的集中配置逻辑,就能比较快地进入项目维护和模型扩展阶段。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐