2021年深圳杯数学建模
A题 火星探测器着陆控制方案
原题再现:
新华社北京5月15日电:2021年5月15日7时18分,中华人民共和国天问一号探测器成功着陆于火星乌托邦平原南部预选着陆区,我国首次火星探测任务着陆火星取得成功。天问一号探测器着陆火星,迈出了我国星际探测征程的重要一步,实现了从地月系到行星际的跨越,在火星上首次留下中国人的印迹,这是我国航天事业发展的又一具有里程碑意义的进展。你们勇于挑战、追求卓越,使我国在行星探测领域进入世界先进行列,祖国和人民将永远铭记你们的卓越功勋!
本题聚焦于探测器从火星同步轨道出发到探测器在火星地表上方悬停的过程(以下简称着陆过程),要求参赛队收集有关天问一号探测器的音像和文字等公开资料,建立数学模型,研究如下问题:
1. 确定探测器着陆过程时间最短的操控方案(包括环绕器与着陆巡视器分离、阻尼伞打开、发动机系统点火等时间,以及发动机系统运行方案);
2. 对给定的着陆过程时间,确定消耗能量最少的操控方案;
3. 如果希望探测器着陆过程与公开的音像和文字资料尽量一致,如何设计操控方案。
整体求解过程概述(摘要)
针对问题一,为了探究探测器从火星同步轨道出发至火星表面上方悬停的时间最短操控方案,首先对国家航天局公开报道、天问一号 GNC 飞行结果论文、伞系论文及火星环境参数进行了数据理解、单位统一、事件语义对齐和可信度分级。在此基础上,引入"同步轨道转移---三自由度气动进入---非线性伞降---推力受限动力减速"的四阶段混合动力系统,并以动压、热流代理、开伞高度和 100 m 悬停为安全约束。采用可行域离散动态规划与事件驱动积分求得:在等效模型下,最优初始进入倾角为 -19.7°,大气入口至建立 100 m 悬停的最短时间为 251.23 s;若严格计入同步轨道至 125 km 大气入口的理想径向下落段,全过程理论时间下界为 15419.55 s(约 4.28 h)。结果同时给出了开伞、动力点火和悬停的事件时刻,并通过动压峰值与热环境代理量检验安全约束。
针对问题二,鉴于题目未给出唯一的数值"给定时间",建立了可参数化的固定时长最小能量模型,并以 490 s 的"大气入口至 100 m 悬停"时长作为示例。考虑到主发动机实际能耗与推力水平、工作时长均相关,而公开资料不足以恢复完整推进剂热力学模型,定义无量纲等效控制能量 J=∫(T/7500)^2dt,并同时计算总冲量与推进剂代理量。基于变分法证明固定动力段时间下最优加速度必为时间的一次函数,再与前段气动/伞系状态联合搜索。求得示例最优方案的进入倾角为 -9.0°、后段滚转角为 165°,动力段时长 48.75 s,推力在 7.227~7.317 kN 之间平滑变化,等效控制能量为 45.823,说明延长可用时间后可以用接近恒推力的方式降低控制峰值并改善能量分配。
针对问题三,为了使控制过程与公开音像和文字资料尽量一致,将开伞高度约 10 km、开伞速度 460 m/s、动力段起始速度约 95 m/s、100 m 悬停、20 m 慢降和实测 EDL 约 537 s 等信息构造成事件锚点,建立加权最小二乘一致性重构模型。通过对初始进入倾角与分段滚转角进行参数标定,并在动力段引入满足 1.5~7.5 kN 约束的 Bang-Bang---悬停---慢降控制,得到总时长 536.871 s,与 537 s 公开飞行结果的误差仅 0.129 s;模型开伞高度为 10.459 km,与"约 10 km"公开描述的偏差为 458.8 m。该结果表明,低阶物理模型通过事件约束和阶段控制能够较好复现天问一号着陆过程的宏观时序。
综合三个问题,本文形成了从"数据理解与探索---数据预处理---物理特征构造---混合动力学建模---最优控制求解---约束验证---敏感性与鲁棒性评估---结果解释"的完整数学建模流程。模型优点在于物理含义清晰、参数可追溯、不同目标函数之间可统一切换,并能在公开资料不完备的条件下避免把未知工程参数伪装成精确真值;不足在于采用了指数大气、平面点质点和等效质量等低阶假设。Monte Carlo 误差传播和参数敏感性结果显示,模型对大气密度、推力和质量扰动具有可解释的响应规律,可进一步推广到六自由度、高保真火星大气、模型预测控制与概率约束最优控制。
模型假设:
假设1:火星为球对称天体,同步轨道段采用二体引力模型,不考虑火卫一、火卫二及太阳三体摄动。该假设用于得到同步轨道至大气入口的基准时间尺度。
假设2:125 km 以下火星大气采用指数模型 ρ(h)=ρ0exp(-h/H)。ρ0 与 H 为等效参数,反映平均大气结构,不解释为某一时刻的真实气象剖面。
假设3:进入段采用平面三自由度点质点模型,忽略偏航、滚转惯性和结构弹性,仅通过滚转角改变升力方向。该假设保留了高度、速度、航迹角和升阻耦合的主要机制。
假设4:伞降段将降落伞---探测器系统等效为竖直下降质点,阻力使用等效 CdA 表示;伞衣充气瞬态、摆振和附加质量被吸收到标定参数中。
假设5:动力段以竖直方向运动为主。问题一使用最大推力近似获得时间下界;问题二在常质量近似下推导最小二次能量解析控制;问题三使用推力上下界内的分段 Bang-Bang 控制。
假设6:公开资料中"约""大约"等信息按软约束处理;飞行结果论文给出的 537 s、125 km、100 m、20 m 等信息具有更高权重。
假设7:题目未给出全部发动机与质量时序参数,因此 ENTRY_MASS、PARACHUTE_MASS、LANDER_MASS 等均为公开资料汇总后的等效建模量,并在模型评价中做敏感性分析。
假设8:在比较不同方案时,除明确改变的目标函数或控制参数外,其余环境与等效参数保持一致,从而保证比较满足控制变量原则。
问题分析:
问题一分析
针对火星探测器从大气入口至 100 米悬停最短操控方案求解需求,本题属于带多物理安全硬约束的混合最优控制问题。全过程分为同步轨道转移、气动进入、伞降减速、动力制动四大阶段,各阶段动力学方程形式差异显著,统一微分方程无法适配全流程;优化目标为 EDL 总时长最小,同时强制约束动压峰值、热流上限、最低开伞高度等工程安全条件,避免轨迹过陡引发探测器损毁。建模核心为先建立分段质点动力学方程组,再采用离散倾角网格结合事件驱动积分遍历全部可行轨迹,筛选满足所有安全边界的最小时长方案,同步输出速度、高度、推力完整时序曲线,区分轨道转移理论下界与大气内 EDL 最优时长两类结果。
问题二分析
针对固定 EDL 总时长下最小等效控制能量操控方案设计问题,本题属于带线性积分约束的凸最优控制问题,完整复用问题一气动、伞降阶段动力学模型,仅更换优化目标函数。因缺少完整推进剂热力学参数,采用推力二次积分作为可量化能耗评价指标,规避无依据燃料真值假设;利用变分法可推导出动力段最优推力为一次线性函数,无需大规模数值遍历即可得到解析解。外层离散搜索进入倾角与滚转角组合确定伞降终点初始状态,内层代入给定总时长求解最优推力时序,同时划分发动机推力可行区间,对推力越界的时长方案直接判定为不可行,绘制时长与能耗、推进代理量权衡曲线分析二者此消彼长的折衷规律。
问题三分析
针对匹配天问一号公开音像、文字资料的着陆轨迹重构问题,本题属于多稀疏事件锚点加权最小二乘标定问题,不再以时间或能耗为优化目标,转为最小化模型与公开观测信息的偏差。公开资料存在 "约 10km 开伞高度""537s 总时长" 等近似软约束与 460m/s 开伞速度、95m/s 动力起始速度等刚性阶段分界条件,需区分权重完成残差构造;动力段细分粗避障、悬停成像、低空慢降多子阶段,采用 Bang-Bang 分段推力控制贴合真实飞控流程。通过标定进入倾角、滚转角两组核心参数缩小轨迹与公开事件的误差,最终输出与官方记录时序、高度高度吻合的完整操控策略,并量化开伞高度、总时长两类关键拟合残差验证模型一致性。
模型的建立与求解整体论文缩略图

全部论文请见下方" 只会建模 QQ名片" 点击QQ名片即可
程序代码:
python
from __future__ import annotations
import argparse
import json
import math
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Dict, List, Tuple, Optional
import numpy as np
import pandas as pd
from scipy.integrate import solve_ivp
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
plt.rcParams["font.sans-serif"] = ["Noto Sans CJK SC", "AR PL UMing CN"]
plt.rcParams["axes.unicode_minus"] = False
# ---------------------------
# 0. 全局常量与公开资料锚点
# ---------------------------
MARS_RADIUS = 3_389_500.0 # m,等效平均半径
MARS_MU = 4.282837e13 # m^3/s^2
MARS_G_SURFACE = 3.71 # m/s^2
MARS_DAY = 24 * 3600 + 37 * 60 + 22.663 # s,火星恒星日近似
RHO0 = 0.020 # kg/m^3,近地面等效密度
ATM_SCALE_HEIGHT = 10_800.0 # m,指数大气等效尺度高度
G0 = 9.80665 # m/s^2
# 公开资料锚点(用于数据理解与问题三拟合)
OBS = {
"entry_altitude_m": 125_000.0,
"entry_speed_mps": 4_800.0,
"parachute_speed_mps": 460.0,
"parachute_altitude_m": 10_000.0,
"powered_start_speed_mps": 95.0,
"hover_altitude_m": 100.0,
"slow_descent_altitude_m": 20.0,
"slow_descent_speed_mps": 1.5,
"edl_touchdown_s": 537.0,
"main_engine_constant_N": 7_500.0,
"main_engine_throttle_min_N": 1_500.0,
"main_engine_throttle_max_N": 5_000.0,
"parachute_area_m2": 200.0,
"parachute_diameter_m": 15.96,
}
# 等效建模参数:用于低阶模型标定
ENTRY_MASS = 1_700.0 # kg,进入舱等效质量
LANDER_MASS = 1_285.0 # kg,动力段等效初始质量
PARACHUTE_MASS = 1_450.0 # kg,伞降段等效质量
AEROSHELL_AREA = 15.9 # m^2,等效迎风面积
CD_ENTRY = 1.50
LIFT_TO_DRAG = 0.13
PARACHUTE_CDA_Q1 = 84.25 # m^2,问题一时间优化等效值
PARACHUTE_CDA_REF = 90.0 # m^2,问题二/三资料拟合等效值
LANDER_CDA = 4.0 # m^2,动力段等效迎风阻力面积
ISP_EQ = 230.0 # s,仅用于燃料代理量,不宣称真实比冲
@dataclass
class EntryResult:
time_s: float
h_end_m: float
v_end_mps: float
gamma_end_deg: float
q_peak_pa: float
heat_peak_proxy: float
t: np.ndarray
h: np.ndarray
v: np.ndarray
gamma: np.ndarray
q: np.ndarray
heat: np.ndarray
@dataclass
class ParaResult:
time_s: float
h_end_m: float
v_end_mps: float
t: np.ndarray
h: np.ndarray
v: np.ndarray
@dataclass
class Q1Result:
gamma0_deg: float
entry_time_s: float
para_time_s: float
power_time_s: float
hover_dwell_s: float
edl_to_hover_s: float
h_parachute_m: float
h_engine_ignite_m: float
v_engine_ignite_mps: float
q_peak_pa: float
heat_peak_proxy: float
radial_orbit_time_s: float
hohmann_like_time_s: float
total_theoretical_s: float
total_low_energy_s: float
@dataclass
class Q2Result:
given_edl_time_s: float
gamma0_deg: float
bank2_deg: float
entry_time_s: float
para_time_s: float
powered_time_s: float
h_parachute_m: float
h_power_start_m: float
thrust_min_N: float
thrust_max_N: float
equivalent_energy: float
impulse_Ns: float
propellant_proxy_kg: float
@dataclass
class Q3Result:
gamma0_deg: float
bank2_deg: float
entry_time_s: float
para_time_s: float
pdl_time_s: float
total_touchdown_s: float
parachute_altitude_m: float
power_start_altitude_m: float
fit_error_time_s: float
fit_error_parachute_alt_m: float
def mars_gravity(h: float) -> float:
return MARS_MU / (MARS_RADIUS + h) ** 2
def atmosphere_density(h: np.ndarray | float) -> np.ndarray | float:
return RHO0 * np.exp(-np.maximum(h, 0.0) / ATM_SCALE_HEIGHT)
# ---------------------------
# 1. 进入段:三自由度平面点质点模型
# ---------------------------
def simulate_entry(
gamma0_deg: float,
bank1_deg: float = 0.0,
bank2_deg: float = 0.0,
v_bank_switch: float = 2500.0,
v_terminal: float = OBS["parachute_speed_mps"],
) -> EntryResult:
"""从 125 km, 4.8 km/s 积分至降落伞开伞速度。
状态: h, v, gamma。
控制: 分段滚转角 bank1/bank2,借助升力方向调节下降率。
"""
def dyn(t, y):
h, v, gamma = y
r = MARS_RADIUS + h
g = MARS_MU / r ** 2
rho = float(atmosphere_density(h))
D = 0.5 * rho * v * v * CD_ENTRY * AEROSHELL_AREA
L = LIFT_TO_DRAG * D
bank = math.radians(bank1_deg if v > v_bank_switch else bank2_deg)
dh = v * math.sin(gamma)
dv = -D / ENTRY_MASS - g * math.sin(gamma)
dgamma = L * math.cos(bank) / (ENTRY_MASS * max(v, 1e-6)) + (
v / r - g / max(v, 1e-6)
) * math.cos(gamma)
return [dh, dv, dgamma]
def event_v(t, y):
return y[1] - v_terminal
event_v.terminal = True
event_v.direction = -1
sol = solve_ivp(
dyn,
[0, 1500],
[OBS["entry_altitude_m"], OBS["entry_speed_mps"], math.radians(gamma0_deg)],
events=event_v,
max_step=1.0,
rtol=2e-7,
atol=1e-8,
)
if len(sol.t_events[0]) == 0:
raise RuntimeError("进入段未在给定积分区间内达到开伞速度。")
t_end = float(sol.t_events[0][0])
y_end = sol.y_events[0][0]
t = sol.t
h, v, gamma = sol.y
rho = atmosphere_density(h)
q = 0.5 * rho * v ** 2
# Sutton-Graves 形式的相对热流代理;用于约束/比较,不作为真实热流定量值。
heat = np.sqrt(rho) * v ** 3
return EntryResult(
time_s=t_end,
h_end_m=float(y_end[0]),
v_end_mps=float(y_end[1]),
gamma_end_deg=math.degrees(float(y_end[2])),
q_peak_pa=float(np.max(q)),
heat_peak_proxy=float(np.max(heat)),
t=t,
h=h,
v=v,
gamma=gamma,
q=q,
heat=heat,
)
# ---------------------------
# 2. 伞降段:非线性阻力模型
# ---------------------------
def simulate_parachute_to_speed(
h0: float,
v0: float = OBS["parachute_speed_mps"],
cda: float = PARACHUTE_CDA_REF,
v_terminal: float = OBS["powered_start_speed_mps"],
) -> ParaResult:
"""下降速度取正;dh/dt=-v。"""
def dyn(t, y):
h, v = y
g = mars_gravity(h)
rho = float(atmosphere_density(h))
a_drag = 0.5 * rho * cda * v * abs(v) / PARACHUTE_MASS
return [-v, g - a_drag]
def event_v(t, y):
return y[1] - v_terminal
event_v.terminal = True
event_v.direction = -1
sol = solve_ivp(
dyn,
[0, 500],
[h0, v0],
events=event_v,
max_step=0.1,
rtol=1e-8,
atol=1e-9,
)
if len(sol.t_events[0]) == 0:
raise RuntimeError("伞降段未达到目标速度。")
t_end = float(sol.t_events[0][0])
y_end = sol.y_events[0][0]
return ParaResult(
time_s=t_end,
h_end_m=float(y_end[0]),
v_end_mps=float(y_end[1]),
t=sol.t,
h=sol.y[0],
v=sol.y[1],
)
def simulate_parachute_to_ignition_surface(
h0: float,
v0: float = OBS["parachute_speed_mps"],
cda: float = PARACHUTE_CDA_Q1,
h_hover: float = OBS["hover_altitude_m"],
) -> ParaResult:
"""问题一:以最大推力制动距离等于剩余高度作为最迟安全点火面。
该切换面体现 Bellman 最优性原理:在最大推力能够恰好制停以前,
继续使用伞系气动减速;一旦进入可达域,立即切换动力制动可避免额外慢速伞降。
"""
a_eff = OBS["main_engine_constant_N"] / LANDER_MASS - MARS_G_SURFACE
def dyn(t, y):
h, v = y
g = mars_gravity(h)
rho = float(atmosphere_density(h))
a_drag = 0.5 * rho * cda * v * abs(v) / PARACHUTE_MASS
return [-v, g - a_drag]
def switch_event(t, y):
h, v = y
stopping_distance = v * v / (2 * a_eff)
return (h - h_hover) - stopping_distance
switch_event.terminal = True
switch_event.direction = 1
sol = solve_ivp(
dyn,
[0, 300],
[h0, v0],
events=switch_event,
max_step=0.05,
rtol=1e-8,
atol=1e-9,
)
if len(sol.t_events[0]) == 0:
raise RuntimeError("未找到动力制动可达域切换面。")
t_end = float(sol.t_events[0][0])
y_end = sol.y_events[0][0]
return ParaResult(
time_s=t_end,
h_end_m=float(y_end[0]),
v_end_mps=float(y_end[1]),
t=sol.t,
h=sol.y[0],
v=sol.y[1],
)
# ---------------------------
# 3. 同步轨道段
# ---------------------------
def areosynchronous_radius() -> float:
return (MARS_MU * MARS_DAY ** 2 / (4 * math.pi ** 2)) ** (1 / 3)
def radial_fall_time_to_entry() -> float:
"""理论时间下界:分离时仅卸除切向速度,不额外赋予径向初速。"""
r0 = areosynchronous_radius()
r1 = MARS_RADIUS + OBS["entry_altitude_m"]
def dyn(t, y):
r, vr = y
return [vr, -MARS_MU / r ** 2]
def event_r(t, y):
return y[0] - r1
event_r.terminal = True
event_r.direction = -1
sol = solve_ivp(
dyn,
[0, 30000],
[r0, 0.0],
events=event_r,
max_step=2.0,
rtol=1e-10,
atol=1e-8,
)
return float(sol.t_events[0][0])
def hohmann_like_transfer_time() -> float:
"""低能量基准:同步轨道至 125 km 入口半椭圆转移的半周期。"""
r0 = areosynchronous_radius()
r1 = MARS_RADIUS + OBS["entry_altitude_m"]
a = (r0 + r1) / 2
return math.pi * math.sqrt(a ** 3 / MARS_MU)
# ---------------------------
# 4. 问题一:时间最短
# ---------------------------
def solve_q1() -> Tuple[Q1Result, Dict[str, object]]:
"""网格化动态规划/可行域筛选。
安全约束:
q_peak <= 8 kPa;
heat_proxy <= 1.80e9;
开伞高度 >= 10 km。
在这些约束下,对初始进入倾角进行一维直接搜索。
"""
candidates = []
for gamma0 in np.arange(-23.0, -11.95, 0.10):
try:
ent = simulate_entry(gamma0, 0.0, 0.0)
except RuntimeError:
continue
if ent.q_peak_pa > 8000.0:
continue
if ent.heat_peak_proxy > 1.80e9:
continue
if ent.h_end_m < 10_000.0:
continue
para = simulate_parachute_to_ignition_surface(ent.h_end_m)
a_eff = OBS["main_engine_constant_N"] / LANDER_MASS - MARS_G_SURFACE
t_power = para.v_end_mps / a_eff
hover_dwell = 20.0 # 悬停成像的最低任务时间窗(建模约束)
total = ent.time_s + para.time_s + t_power + hover_dwell
candidates.append((total, gamma0, ent, para, t_power))
if not candidates:
raise RuntimeError("问题一无可行解,请检查约束参数。")
candidates.sort(key=lambda x: x[0])
total, gamma0, ent, para, t_power = candidates[0]
t_radial = radial_fall_time_to_entry()
t_hoh = hohmann_like_transfer_time()
result = Q1Result(
gamma0_deg=float(gamma0),
entry_time_s=ent.time_s,
para_time_s=para.time_s,
power_time_s=float(t_power),
hover_dwell_s=20.0,
edl_to_hover_s=float(total),
h_parachute_m=ent.h_end_m,
h_engine_ignite_m=para.h_end_m,
v_engine_ignite_mps=para.v_end_mps,
q_peak_pa=ent.q_peak_pa,
heat_peak_proxy=ent.heat_peak_proxy,
radial_orbit_time_s=t_radial,
hohmann_like_time_s=t_hoh,
total_theoretical_s=t_radial + total,
total_low_energy_s=t_hoh + total,
)
detail = {"entry": ent, "para": para, "candidate_table": candidates}
return result, detail
# ---------------------------
# 5. 问题二:给定时间的能量最小
# ---------------------------
def affine_minimum_energy_control(
h0: float,
v0: float,
T: float,
hf: float = OBS["hover_altitude_m"],
mass: float = LANDER_MASS,
enforce_bounds: bool = True,
) -> Optional[Dict[str, np.ndarray | float]]:
"""常质量近似下的连续时间最小二次控制解析解。
最小化 J=∫(T_engine/Tmax)^2 dt,满足:
hdot=-v,
vdot=g-u,
h(T)=hf, v(T)=0,
其中 u=T_engine/m。
变分法给出 u(t)=lambda1+lambda2*(T-t) 的仿射控制。
"""
if T <= 0:
return None
g = MARS_G_SURFACE
A = v0 + g * T
B = v0 * T + 0.5 * g * T * T - (h0 - hf)
M = np.array([[T, T * T / 2], [T * T / 2, T ** 3 / 3]], dtype=float)
lam1, lam2 = np.linalg.solve(M, np.array([A, B], dtype=float))
t = np.linspace(0, T, 501)
u = lam1 + lam2 * (T - t)
thrust = mass * u
if enforce_bounds:
if np.min(thrust) < OBS["main_engine_throttle_min_N"] - 1e-6:
return None
if np.max(thrust) > OBS["main_engine_constant_N"] + 1e-6:
return None
# 数值积分状态,用于检验终端误差
dt = t[1] - t[0]
h = np.empty_like(t)
v = np.empty_like(t)
h[0], v[0] = h0, v0
for k in range(len(t) - 1):
a = g - u[k]
v[k + 1] = v[k] + a * dt
h[k + 1] = h[k] - v[k] * dt - 0.5 * a * dt * dt
J = float(np.trapezoid((thrust / OBS["main_engine_constant_N"]) ** 2, t))
impulse = float(np.trapezoid(thrust, t))
propellant_proxy = impulse / (ISP_EQ * G0)
return {
"t": t,
"h": h,
"v": v,
"thrust": thrust,
"energy": J,
"impulse": impulse,
"propellant_proxy": propellant_proxy,
"lambda1": float(lam1),
"lambda2": float(lam2),
"terminal_h_error": float(h[-1] - hf),
"terminal_v_error": float(v[-1]),
}
def solve_q2(given_edl_time: float = 490.0) -> Tuple[Q2Result, Dict[str, object]]:
"""联合搜索进入控制与动力段解析最优控制。
鉴于气动与伞系阶段不消耗主发动机能量,给定总时间下优先利用自然阻力,
再把剩余时间分配给动力段;对每个候选前段状态,动力段使用解析最小能量控制。
"""
feasible = []
for gamma0 in np.arange(-15.0, -7.99, 0.50):
for bank2 in np.arange(90.0, 180.1, 15.0):
try:
ent = simulate_entry(gamma0, 0.0, float(bank2), 2500.0)
except RuntimeError:
continue
if not (9000.0 <= ent.h_end_m <= 13_000.0):
continue
para = simulate_parachute_to_speed(ent.h_end_m, cda=PARACHUTE_CDA_REF)
if para.h_end_m <= OBS["hover_altitude_m"]:
continue
T_power = given_edl_time - ent.time_s - para.time_s
ctl = affine_minimum_energy_control(para.h_end_m, para.v_end_mps, T_power)
if ctl is None:
continue
feasible.append((float(ctl["energy"]), gamma0, float(bank2), ent, para, ctl))
if not feasible:
raise RuntimeError("给定时间下无可行解;可尝试增大 --given-edl-time。")
feasible.sort(key=lambda x: x[0])
J, gamma0, bank2, ent, para, ctl = feasible[0]
result = Q2Result(
given_edl_time_s=float(given_edl_time),
gamma0_deg=float(gamma0),
bank2_deg=float(bank2),
entry_time_s=ent.time_s,
para_time_s=para.time_s,
powered_time_s=float(given_edl_time - ent.time_s - para.time_s),
h_parachute_m=ent.h_end_m,
h_power_start_m=para.h_end_m,
thrust_min_N=float(np.min(ctl["thrust"])),
thrust_max_N=float(np.max(ctl["thrust"])),
equivalent_energy=float(ctl["energy"]),
impulse_Ns=float(ctl["impulse"]),
propellant_proxy_kg=float(ctl["propellant_proxy"]),
)
return result, {"entry": ent, "para": para, "control": ctl, "feasible": feasible}