# 航空 **Repository Path**: huyan12138/aviation ## Basic Information - **Project Name**: 航空 - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-15 - **Last Updated**: 2026-10-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # SKYIR-SIM · 天基红外星座仿真系统 > 天基红外星座仿真系统 —— 完全自有代码,全开源技术栈,Python。 > 目标平台:Windows / 麒麟 / 统信 / 龙芯。 本文件是项目的**开发留痕**:每个版本做了什么、改了什么、验证到什么程度,都记在这里。 改动代码前请先读 [AGENTS.md](AGENTS.md)(项目最高约定)。 --- ## 一、版本记录 ### v1.24.17 · 求解器与物理内核可整包移植(2026-10-30) **用户诉求**:「本项目中的求解器和物理模型,可以随时移植、随时调用」。 **改动** - 新增根 `pyproject.toml`:包名 `skyir-solver`,只打包 `solver*` + `core*`, 运行依赖只有 numpy,入口命令 `skyir-solver`(等价 `python -m solver`)。 不把 ui/viz3d/engine/persistence 打进包。 - 新增 `scripts/export_solver.py`:一条命令产出 `dist/solver_portable/skyir_solver_v<版本>/` 与同名 zip,内含 `solver/`、`core/`、 `pyproject.toml`、`docs/solver.md`、移植说明与 `check_portable.py`。 - **自检不是复制完就算**:导出脚本把 zip 解压到临时目录,在那里运行 `check_portable.py`,断言 `solver`/`core` 从包内导入(不是开发仓库)、10 个内置 模型可列、样例项目能解、连续状态函数可用、解包指纹一致。 - `main.py` 与 `pyproject.toml` 的版本由导出脚本强制一致,防止打包版本漂移。 **用法** ```bash python scripts/export_solver.py # 产出文件夹 + zip # 目标项目:解压后加 sys.path(离线推荐),或 pip install <包目录> python -m solver list python -m solver run your_project.json --out out/solution ``` **边界**:顶层包名仍是 `core` / `solver`,目标项目有同名包时用独立 venv 或把本包 放在 `sys.path` 最前;数据文件(记录弧、GLB、贴图、大气 LUT)不在包内,按项目 JSON 的相对路径解析;`pip install` 需要 setuptools/wheel 或联网构建,离线机器走 复制目录这条路。 **验证**:`scripts/export_solver.py` 解压自检通过(10 个模型 / 2 个实体 / 指纹一致, 70 个文件 251 KB);在解压目录里 `python -m solver list` 与 `python -m solver run solver/projects/orbit_demo.json --no-save` 实跑通过; `verify_all` 增至 74 项。 ### v1.24.16 · 求解器纳入推力与落点毁伤模型(2026-10-30) **用户诉求**:接着实现下一个待迁移的物理模型 —— 把 `core.ballistic.propulsion` 与 `core.ballistic.impact_effects` 纳入通用求解器。 **改动** - 新增 `propulsion.rocket`(域 `propulsion`):推力分级表 + 高度 → 逐帧推力、 喷管出口直径、尾焰长度/最大半径/膨胀比。燃尽或未点火时几何全部置 0 —— 没有燃气就没有尾焰,不给"零推力还有喷管"的假数据。时间口径与记录弧一致 (秒,自项目历元起算)。 - 新增 `impact.kinetic`(域 `impact`):取实体时间轴**末点**为落点,按相对地面 速度与配置质量算动能当量,输出火球直径/寿命/膨胀时间、弹坑直径、 5 psi / 1 psi 冲击波半径与到达时间。落点是属性不是时变量,逐点广播同一组标量。 - 两者只调 `core.ballistic` 的公开函数;F-1 锚点、γ=1.2、膨胀半角、面爆镜像 因子等假设仍留在 core,插件不另加观感系数。 - 未复用 `ballistic` 域(求解器一个域只能选一个模型):`ballistic.recorded`、 `propulsion.rocket`、`impact.kinetic` 三者可在同一实体并存。 **验证** - `check_solver.py` 第 ⑩ 组:参考记录弧 330 个采样点上,推力逐帧等于 `thrust_at_time_s`、尾焰几何逐帧等于 `plume_geometry_m`(燃尽采样为 0); 落点毁伤 10 个字段逐值等于 core(当量 5394 kg TNT),且逐点广播; 缺 `stages`、缺 `mass_kg`、缺 `position.r_km` 上游当场拒绝。 - 内置插件 8 → 10;`python -m solver list` 可见两个新模型。 - `verify_all` 73 项 / 70 通过 / 3 跳过 / 0 失败(带达梦口令,2026-10-30)。 ### v1.24.15 · 三维卫星改从 `SceneSolution` 取状态(2026-10-30) **用户诉求**:按批准顺序推进第二步 —— 三维卫星与弹体改从 `SceneSolution` 取状态, **先不改视觉效果,只替换来源**。 **改动** - `engine/solver_bridge.py` 新增 `satellite_solution(orbits, duration_s)`: 卫星库参数集 → `(SceneSolution, {代号: 跳过原因})`。每个实体的时间轴只放 **两个端点**(0 与播放时长)界定窗口,逐帧位置仍由 `state_function(代号)` 的 解析采样器算,不靠数组插值;只收 `two_body` / `j2`,`hpop` / `sgp4` 逐星跳过 并带出原因,一颗星不支持不拖垮整页。 - `ui/scenario_main` 的 3D 模型挂载与 2D 星下点都改吃这一份解 (`scene.track(tag, solution.state_function(code))`),不再各自 import `core.orbits.ephemeris.state_function`。历元、模型尺度、逐星跳过提示保持原行为; hpop 星照旧不播放,现在日志会给出具体原因。 - `_orbit_config` 抽成小函数,场景桥接与卫星库桥接共用同一份字段名。 **验证** - `check_solver.py` 第 ⑨ 组:卫星库桥接的位置/速度逐位等于直接调 core (含播放窗口两端),`continuous is True`,越界查询报"不外推", 全不支持 / 零时长当场拒绝。 - `check_satellite_anim.py`(达梦):3 颗模型三帧位移 11947 / 11946 km, 2D 星下点逐点对齐、扫一圈最高纬度 45.2°(最大倾角 45.0°),截图正常。 - `check_gui_missile_flow.py`:像素判据不变,1 枚导弹 + 3 颗卫星逐帧运动, 成像 34 帧。 - `verify_all` 73 项 / 70 通过 / 3 跳过 / 0 失败(带达梦口令,2026-10-30)。 ### v1.24.14 · 求解器补星历选档:hpop 进解包,但不逐帧(2026-10-30) **用户诉求**:按优先级列表推进第一步 —— 求解器不能只有 `two_body` / `j2`, 星座与弹道之外的场景还要能吃 HPOP 星历。 **改动** - `core.orbits.ephemeris` 新增 `states_at(params, times_s, step_s=None)`: 一个入口给出任意时刻表上的 `State` 列表。`two_body` / `j2` 逐点解析递推; `hpop` **整段只积分一次**(`propagate_numeric`),步长取 `min(60 s, 相邻采样最小间隔)`——逐点单独积分会反复重启积分器、把误差抖成锯齿; 负时刻、非有限值、超过 200 万步当场拒绝。`state_function` 的两档限制不变。 - 求解器新增 `orbit.ephemeris`(第 8 个内置模型):按 `config.orbit.propagator` 选 `two_body` / `j2` / `hpop`,参数读取与两个固定档 插件共用一份;`mass_kg` / `area_m2` / `cd` / `cr` / `f107` / `flags` 透传给 HPOP。 - `two_body` / `j2` 仍走 `state_function`,与旧插件**逐位相同**,已接入的场景 不受影响;`hpop` 走一次数值积分,`sampler()` 返回 `None` —— 逐帧重积分是 O(n²),与 `core` 同口径不给连续状态函数,`Solver.state_functions()` 如实报 "没有连续状态函数",三维摆位要 hpop 时只能按解包采样查询。 - `sgp4`、未知 `flags` 键当场拒绝,不静默降档。 **验证** - `check_solver.py` 第 ⑧ 组:hpop 的位置/速度逐位等于 `core.orbits.ephemeris.states_at`,`continuous is False`,`state_functions()` 拒绝;`two_body` / `j2` 经同一插件仍逐位等于 `state_function`;`sgp4` 与 未知 flags 被拒绝。 - `python -m core.orbits.ephemeris` 演示项 8:`states_at` 与 `state_function` 在两档解析递推下一致(JD 往返舍入 1e-3 km 内)、hpop 600 s 位移 >1 km、 负时刻拒绝。 - `verify_all` 73 项 / 70 通过 / 3 跳过 / 0 失败(带达梦口令,2026-10-30)。 ### v1.24.13 · 旋转视角不再把地球和模型裁成"透明"(2026-10-30) **用户反馈**:"在用鼠标旋转视角的时候,地球和模型都会变得透明。" **根因:trackball 每步旋转都按全场景包围球重算近裁剪面** - `vtkInteractorStyleTrackballCamera::Rotate()` 每次 MouseMove 先调 `ResetCameraClippingRange()`,近远面按**全场景包围球**算。跟随态相机距弹体只有 **85 m**,而场景里有 GEO 卫星(**42,157 km**):实测裁剪面从 `(0.0005, 1e6) km` 被改成 **(21.411, 21411.19) km**,松开时 (22.5, 22525) km。近面在 21 km 处, 近处的地球表面与 19.6 m 的弹体整段被裁掉,只剩远处球背面——画面上的 "透明"就是被近面切掉了,不是材质或透明度设置。 **修法:守卫挂到渲染窗口的 StartEvent,每次真正绘制前修正裁剪面** - 新增 `_fit_clipping_to_view`:只在 `近面 >= 相机距焦` 时(近面越过焦点,焦点附近 必被裁)把近面压到 `max(0.5 m, 距焦×1e-3)`,远面保到 1e6 km,与 `scenario_main.view._follow_clip` 同口径。 - 守卫从交互器 `InteractionEvent` 挪到 `render_window` 的 **StartEvent** (`ui/vtk_view.py` 的 `_install_view_guard`,与"相机不穿地球"共用)。原因:交互器 观察者只能赶上样式自己渲染**之后**的第二次渲染——实测每步出两帧,第一帧近面 仍是 21 km(用户看到的坏帧),第二帧才对。StartEvent 在每次 `Render()` 真正画 之前触发,样式那次渲染也被修正,且不再重复出帧。 - 不无条件收紧近面:远景默认机位(焦点地心、相机 5 万 km)近面本就该是场景尺度, 压到 `距焦×1e-3` 会让远/近比到 2 万,24 位深度缓冲在 5 万 km 处精度掉到公里级, 地球纹理与轨道线会闪。判据 `近面 >= 距焦` 把两种情形分开。 **验证**(实机,真 GL) - 探针(地球 + 19.6 m 立方体 + 42,157 km 远目标,合成拖动):修前第一个 MouseMove 裁剪面即 **(21.4, 21411) km**;修后 10 步拖动全程 `(0.0005, 1e6)`,每步只渲染 一帧、StartEvent 入口裁剪面已是修正值;远景默认机位拖动时近面保持 7 万 km 不被收紧。 - `ui.vtk_view` 自检新增三条断言:这组几何按包围球重算确实 >1 km(证明自检有效)、 拖动 10 帧近面最大 **0.5 m** / 远面 1e6 km、远景近面保持 **30,697 km** 不变。 - `scripts/check_gui_missile_flow.py` 新增"旋转不透明"断言:跟随态拖动 9 帧, StartEvent 时近裁剪面最大 **0.5 m**(旧行为 21 km)。该脚本全流程通过。 - `verify_all` 73 项 / 70 通过 / 3 跳过 / 0 失败(带达梦口令,2026-10-30)。 ### v1.24.12 · 跟弹时画面不再一闪一闪(2026-10-30) **用户反馈**:"动画怎么一闪一闪的。" **根因:跟随态每次 tick 渲染两次,第一帧的相机还停在上一个时刻** - `_tick` 先调 `vtk3d.update_time(jd)`——它内部是 `Scene.update` + `Render()`, **出帧时相机还没跟上弹体**;随后 `_follow_translate` 才把相机平移一个 tick 的 弹体位移,再 `scene.render()` 出第二帧。同一次 tick 的两帧相机不同。 - 实测(「看尾焰」,16×,相机距弹体 85.4 m):第一帧弹体离焦点 **220~330 m**, 而 85 m 处 30° 视场的半高只有 **23 m**——整枚弹出画;第二帧相机跟上才回中心。 屏幕上就是 30 Hz 的"甩出去—回来",即用户报的"一闪一闪"。这不是抖动阈值问题, 是顺序错了:物理上同一时刻只能有一个画面。 **修法:摆位 → 相机跟上 → 只呈现一次** - `ui/scenario_main/view.py` 新增 `_render_3d(jd)`:跟随态(`follow_missile` 且有 弹体)下先 `scene.update(jd)` 只摆位,再 `_follow_translate(scene)` 把相机、 焦点同步平移,最后 `scene.render()` 一次;非跟随态仍走 `vtk_view.update_time`。 - 拖动时间轴(`seek_to_slider`)走同一入口——跟随态拖时间轴时旧顺序同样会 "甩出去再拉回"。 **验证**(实机,真 GL) - 探针包住 `render_window.Render()`,在每次呈现的入口量 `|弹体 - 焦点|`: 修前每次 tick 两帧、第一帧 220~330 m;修后 0.6 s 内 13 帧**全部 0.00 m**。 - `scripts/check_gui_missile_flow.py` 新增"跟帧不闪"断言:跟随态 0.6 s 至少呈现 10 帧、每帧弹体离焦点 < 5 m——旧顺序下第一帧必超。该脚本全流程通过。 - `verify_all` 73 项 / 70 通过 / 3 跳过 / 0 失败(带达梦口令,2026-10-30)。 ### v1.24.11 · 缩放到地表停住,不再穿过地球(2026-10-30) **用户反馈**:"视角放大的时候视角会穿过地球。" **根因:滚轮 Dolly 沿视线把相机推向焦点,焦点默认在地心** - 3D 视图用 `vtkInteractorStyleTrackballCamera`:滚轮/中键 Dolly 按约 1.21 倍/次把 相机沿视线朝焦点收(`vtkCamera::Dolly`),默认焦点 `(0,0,0)` 就是地心。实测默认 机位距地心 50,090 km,**第 11 次滚轮即跌破 6378.137 km**,第 60 次只剩 0.54 km—— 相机穿进球面后仍朝地心收,画面变成从地球内部往外看。这是交互层的相机行为, 不是几何或物理算错(场景几何、裁剪与时间轴都不受影响)。 **修法:交互器上装一条"相机不得低于地表"的守卫** - `ui/vtk_view.py` 新增 `_lift_camera_above_earth(scene)`:相机半径 < `R_EARTH + 10 m` 时沿径向抬回地表以上,**焦点与朝向不动**。焦点在地心时径向就是视线方向, 表现正好是"缩放到地表停住";焦点在弹体/落点上时只保证相机留在球外, 不夺回用户调好的距离与角度(v1.24.3 的交互口径不变)。 - 守卫挂在**交互器**上(`InteractionEvent` + `EndInteractionEvent`),覆盖滚轮、 中键 Dolly、拖动旋转与平移全部路径,只装一次(`showEvent` 会重复设样式)。 不重写 `Dolly()`:VTK 鼠标事件按 C++ 侧 observer 派发,重写 Python 同名方法 不生效——2D 样式 `_PanOnlyImageStyle` 已踩过同一个坑。 - 保底只留 10 m、不留几十公里:聚焦弹体/看爆炸时相机本就在地表上方 30 m~1.2 km, 保底取大值会在用户一旋转/缩放时把贴地机位硬拽走。2D 平面视图不装守卫 (平面没有球面可穿)。 **验证**(实机,真 GL) - `ui.vtk_view` demo 新增:默认机位连续 80 次滚轮后相机**停在地表上方 0.010 km** (未装守卫时第 11 次即穿过);焦点贴地(看爆炸机位)滚 40 次同样停在 0.010 km, 焦点逐位不变;停住后实拍 **99.9% 像素为地球纹理**(不是黑屏,近裁剪没有把地球切掉)。 - `verify_all` 73 项 / 70 通过 / 3 跳过 / 0 失败(带达梦口令,2026-10-30)。 ### v1.24.10 · 爆炸的时间史、冲击波范围与海上落点(2026-10-30) **用户要求**:"你的爆炸范围和爆炸持续时间符合现实吗?这是一个仿真软件,一切不符合 现实的地方都要修改。" **火球时间史:4.68 s 是"亮多久",不是"长多大用多久"** - v1.24.9 把 Baker 的持续时间当成了生长时间,火球在整段 4.68 s 里线性长大 (1 s 时只有 26 m,2.3 s 才到最大 30 m)。Baker 1983 / IATG 01.80:2021 表 34 只给**最大直径与总持续时间**,没有生长曲线;化学爆炸的火球在毫秒~亚秒级就到最大。 - 改用"冲击波前缘到达火球半径"的时间作膨胀时长:`fireball_expansion_time_s` = **20.3 ms**(30.18 m 半径),之后半径保持最大直到 4.68 s 熄灭。末尾 30% 淡出 仍是显示需要(模型没有亮度曲线)。 **爆炸范围:补上模型算的冲击波前缘,撤掉错误的 Sedov 全程解** - 峰值超压用 Kinney–Graham 1985 球面式 × 面爆镜像因子 2:5 psi(34.47 kPa)半径 **101.1 m**、1 psi(6.895 kPa)**297.2 m**;与 IATG 01.80:2021 表 5 的 W=1000 kg 算例差 <7%。 - 到达时间用正激波 Rankine–Hugoniot 关系 `M²=1+(γ+1)/(2γ)·Δp/p0`、`U=M c0`,从 K–G 近场积分 `dt=dR/U`:5 psi **0.172 s**、1 psi **0.718 s**。**撤掉 Sedov–Taylor `R∝t^0.4` 全程解**——它只在 Δp≫p0 时成立,末段会给出约 40 m/s 的亚声速前缘, 比声速还慢;v1.24.9 据此算出的 7.31 s 是错的。 - `viz3d/effects.py` 新增 `impact_shock_ring_actor`:贴地细环按模型曲线逐帧变径, 1 psi 后 0.5 s 淡出;半径与时间都由模型给。环本身是**示意**(真实冲击波在可见 光下通常不可见)。「看爆炸」相机从 0.5 km 拉到 **1.2 km**,才装得下 297 m 的环。 **落点水陆:北大西洋不生成地面弹坑** - 参考弧末点 15.10°N/59.998°W 在北大西洋。新增 `viz3d.earth.surface_votes` / `water_fraction`,用 3D 地球同一张等经纬贴图(3000×1500,赤道上约 13 km/像素) 采样落点 5×5 邻域,做**水/陆/中性三分类**:蓝占优 → 水,暖/绿占优 → 陆, 冰盖、白云、灰影、地图文字 → 中性。中性像素**不进分母**——否则云带会把深海 算成陆地。实测落点 **20 水 / 0 陆 / 5 中性**(一条云带的灰影)→ 有效像素水域 占比 **100%**;扩到 17×17 邻域仍有 75%,判读稳定 → 判为水域,**不生成地面 弹坑**——弹坑式只对土壤/岩石地面爆有效,海面留一个 14 m 干坑不符合现实。 水面爆炸的凹坑、水柱、波浪没有模型,就不画;火球与冲击波按能量等效空中爆炸 显示,Console 写明这一取舍与像素票数。有效像素不足 1/4 时返回"判不了",按陆地 生成弹坑并标注是假设。换到陆地场景时弹坑照常生成并随地球自转。 - **修判据的脆弱点**:旧版只看"蓝远高于红绿",云影被算成非水也进分母,同一点 只有 60%,正卡在 0.6 门限上——窗口取 5×5 是海、取 9×9 就是陆,不能用作物理 判断。三分类后落点 100%、太平洋 100%、土耳其/西藏/撒哈拉/亚马逊/澳洲 0%、 格陵兰冰盖判不了(`None`),窗口大小不再改变结论。判读并非海岸线数据,是影像 颜色判读,分辨率与云量是它的边界,README 与模块 docstring 都写明。 - 修一个潜在透明度 bug:v1.24.9 的淡出公式在寿命 90% 处算出 2.53 的透明度(被 VTK 截到 1.0),现为 `1−max((t/life−0.7)/0.3, 0)`。 **验证**(实机,真 GL) - `core.ballistic.impact_effects` demo:5394 kg TNT;火球 60.4 m/4.68 s(20 ms 长到 最大);5 psi 101 m/0.17 s、1 psi 297 m/0.72 s;K–G 与 IATG 表 5 算例差 <7%。 - `viz3d.effects` demo:火球 20 ms 到最大并保持、冲击波环 101→297 m、弹坑 14.0 m。 - `viz3d.earth` demo:贴图采样——参考弧末点 1.0(20 水/0 陆/5 中性)、太平洋 1.0、 土耳其 0.0、西藏 0.0、格陵兰 `None`(全冰盖,判不了)、澳大利亚 0.0。 - `check_ballistic_layer` ⑥:命中前后可见性、5 ms 时火球小于半满径、50 ms 到最大并 保持到 3 s、冲击波 0.1 s 与 1 psi 半径逐值对表、火球中心与落点 ECEF 差 0.000 m、 海上无弹坑(20/0/5、水域 100%)、Console 写明 Baker/Kinney/Rankine–Hugoniot。 - `check_gui_missile_flow`:开页 tag 含 `ballistic:shock`、无 `ballistic:crater`; 看爆炸取证帧(命中后 0.5 s)火球 30.2 m/透明度 0.95、冲击波环 220.6 m、相机距 落点 1237 m、爆炸+冲击波 1271 像素,截图 `docs/shots/missile-impact.png`。 - 全量 `scripts/verify_all.py`(带 `SIMULATION_DM8_PASSWORD`,2026-10-30): **73 项 / 通过 70 / 跳过 3 / 失败 0**。本轮把 v1.24.9 新增的两条 core 模块 (`core.ballistic.impact_effects`、`core.ballistic.propulsion`)补进全量清单, 此前 71 项口径不含它们。跳过项同旧口径(`sgp4` 可选依赖 + 两个按设计跳过的 界面检查)。 ### v1.24.9 · 爆炸与尾焰改由物理模型算,不再有"外观量级"(2026-10-30) **用户要求**:全量审查物理规律;"导弹的爆炸范围是否是编的,所有的都需要有现实 模型与数据支撑"。 **爆炸:能量 → 火球/弹坑,全部可溯源**(新增 `core/ballistic/impact_effects.py`, 重写 `viz3d/effects.py` 的落点几何) - 参考场景只登记了弹道与分级表,**没有战斗部当量/爆高/爆炸影像**;唯一可溯源的 能量是弧末点动能 `½mv²`:m=800 kg、v=7511.4 m/s(相对地面,与参考登记的 `impact_speed_ms` 一致)→ 22.57 GJ = **5394 kg TNT**(1 kg TNT ≡ 4.184 MJ,定义值)。 - 火球直径/寿命按 Baker et al. 1983《Explosion Hazards and Evaluation》的 `d=3.86Q^0.32` / `t=0.299Q^0.32`(Q 为燃烧质量;联合国 IATG 01.80:2021 表 34 原式)= **60.4 m / 4.68 s**;弹坑直径按 Kinney–Graham 1985 对约 200 次地面爆炸的 统计式(Ambrosini 2003 式(2) 转引)`D=0.8W^(1/3)` = **14.0 m**,原文离散系数约 30%。 半球火球按 Sedov–Taylor 的 `R∝t^0.4` 生长、末段淡出;弹坑常驻、随地球自转(地固)。 - **适用性如实标注**:参考弧没有登记战斗部,本库把"纯动能"当**能量等效 TNT 装药** 套爆炸经验式。真实动能撞击与装药爆炸的能量分配不同(大量能量进入成坑/抛射), 所以 60.4 m / 4.68 s / 14.0 m 是**量级估计**,不是实测爆炸复现;这也回答了 "爆炸范围是不是编的"——数字由 ½mv² 与文献式算出,但"动能=等效装药"这个前提 是口径假设。Kuchta 1986 的燃料火球式与此同形,但变量 W 是**燃料质量**, 不能用燃料质量替代 TNT 当量,故不采用。 - **撤掉 v1.24.8 的三层外观几何**(火球 40→150 m/7 s、亮核、尘烟 240 s)——那些 半径与时长是 assumed。现在的假设集中在**口径本身**:"纯动能、无战斗部、触地爆 + 能量等效 TNT 经验式"(参考未登记,模块 docstring 已写明),不是尺寸数字。 - 不渲染超压/杀伤半径:本轮只回答"爆炸看起来多大、留多大坑",两者都有文献式; 要画冲击波毁伤范围需另接 Kingery–Bulmash,属未要求的功能。 **尾焰:推力 → 喷管 → 随高度膨胀**(新增 `core/ballistic/propulsion.py`,逐帧重建) - 喷管出口按等室压/等出口马赫数缩放:`D_e=3.70·√(F/6.77 MN)`(F-1 锚点 6.77 MN / 3.70 m),900/300/150 kN → **1.35/0.78/0.55 m**;可见长度 = 出口半径膨胀到 r_max 的轴向行程 `(r_max−r_e)/tan15°`,欠膨胀射流边界按 `r/r_e=(p_e/p_a)^(1/2γ)` 随高度膨胀、截到 20。环境压力取自 `core.orbits.hpop` 的分层指数大气(同表同 H),不是另编一套。 - 实测:900 kN 海平面长 **13.5 m** → 400 km 高空长 **47.8 m**、半径 **13.5 m**; 逐帧按当前推力与高度重建(`Scene.animate`)。假设只有四个常量(出口压力匹配 海平面、γ=1.2、半角 15°、截断 20),全部写在模块 docstring 里。 - **修一个真实窗口 bug**:记录弧 boost→midcourse 在 201 s,分级表推力燃尽 70+70+60 = 200 s;旧可见窗口到 201 s,200~201 s 会用 0 推力去算喷管几何并抛 `ValueError`。现在窗口取 `min(201, 200) = 200 s`(两个燃尽定义里先到的那个)。 **相机:看爆炸不再钻到地下**(`ui/scenario_main/view.py`) - 旧写法用世界 z 当"上"、`cross(body_axis, z)` 当"侧";在 15°N 的陡再入段,"侧向" 实际指向地心一侧——实测相机在真球面下 **448 m**,画面里没有地面,只有一圈悬空 火球。改为当地 ENU:`up=center/|center|`、航向取飞行方向在当地水平面的投影、 `side=up×heading`、相机 `center + side·0.5 km + up·0.12 km`、`view_up=up`; 实测相机高度 **+28.6 m**(焦点是弧末点 −92 m,抬 120 m 后在地平线上)。 - 弹体 80 m 特写同样走 ENU,近垂直起降时航向退化则取任意水平方向。 **验证**(实机,真 GL) - `core.ballistic.impact_effects` / `core.ballistic.propulsion` demo:5394 kg TNT、 火球 60.4 m/4.68 s、弹坑 14.0 m;900 kN 出口 1.35 m、海平面 13.5 m → 400 km 47.8 m。 - `check_ballistic_layer` ③′/⑥:t=0/30/100 s 尾焰几何与模型对表、窗口 0~200 s; 命中前后可见性、火球半径逐帧增长(Baker/IATG 上限 30.2 m)、弹坑 33 个顶点与 `crater_mesh` 逐点一致(float32 量化)、爆心与落点 ECEF 差 **0.000 m**。 - `check_gui_missile_flow` 看爆炸:命中后弹体消失、火球 27.7→29.1 m 膨胀、弹坑 常驻;相机距落点 **514 m**、高度 **+28.6 m**(地平线上)、爆炸+弹坑占 3421 像素; 截图 `docs/shots/missile-impact.png`。 - 全量 `scripts/verify_all.py`(带 `SIMULATION_DM8_PASSWORD`,2026-10-30): **71 项 / 通过 68 / 跳过 3 / 失败 0**。跳过项同旧口径(`sgp4` 可选依赖 + 两个按设计跳过的界面检查)。本轮修掉一处被新物理打脸的旧断言: `check_missile_flow` 曾写死"尾焰 20~50 m",而 F-1 锚点下 900 kN 海平面只有 13.49 m,现改为与 `core.ballistic.propulsion.plume_geometry_m` 逐位对表。 > **v1.24.10 修正本条的三处口径**:①火球不是按 Sedov `R∝t^0.4` 在 4.68 s 里长大, > Baker 只给最大直径与持续时间,实际膨胀约 20 ms;②"不渲染超压/杀伤半径"已补上 > 5 psi/1 psi 的模型环与到达时间(K–G 超压 + 正激波 R–H),Sedov 全程解被撤掉; > ③看爆炸相机距离从 0.5 km 改为 1.2 km,才装得下 297 m 的 1 psi 环(下方 v1.24.9 > 验证里的 514 m 是旧数)。 ### v1.24.8 · 命中即起爆:落点爆炸 + 弹体退场(2026-10-29) **用户反馈**:导弹飞到目的地不爆炸、原地趴着——"不符合现实"。仿真的第一约定 就是符合现实物理。 **根因**:记录弧的最后一个采样点(t=1646.4 s、15.10°N/59.998°W、高度 −92 m)就是 触地时刻,`trajectory_from_samples` 也把它设为 `impact/impact_time`;但三维层只把 弹体摆到弧上,**弧一结束就再无任何状态变化**——没有起爆,弹体也留在落点不消失。 **改法**(几何全部落在 viz3d,界面只负责装配与播放) - `viz3d/effects.py`(新增):`impact_explosion_actors(center_ecef_km, launch_jd, impact_s)` 生成三层真实米级几何(外层火球、亮核、尘烟),每帧按 τ=(jd−launch_jd)·86400−impact_s 重算可见性、半径、上浮与透明度,位置用 `ecef_to_eci` 逐帧贴着落点(随地球自转, 与地固弹道线一致)。火球 40→150 m / 7 s、亮核 22→83 m / 4 s、尘烟 80→650 m、 90 s 上浮 250 m、240 s 散尽;**半径与时长是外观量级(assumed)——参考场景未登记 弹头当量/爆高/爆炸影像,不是实测**。 - `viz3d/scene.py`:新增 `Scene.animate(tag, fn)`——非跟踪图元的逐帧回调,在摆位与 可见性之后调用,`remove/close` 随 tag 清理。 - `ui/scenario_main/view.py`:弹体 `track(..., visible=…)` 在 t ≥ impact_time 当帧 消失;落点爆炸挂 `ballistic:impact`;左栏新增 **「看爆炸(重播再入段)」** 一键 入口(播放头对准命中前 30 s、4×、相机距落点 2 km),与「看尾焰」同一套口径: 全球视角看不见是米级物理正确,要看得聚焦。 **验证**(实机,真 GL) - `viz3d.effects` demo + `viz3d.scene` demo:命中前全隐、τ=1 s 三层全显、尺度按 时间曲线增长、爆心与落点 ECEF 逐帧重合、241 s 后全隐、`animate` 注册/注销。 - `check_ballistic_layer` ⑥:命中前 1 s 弹体在/爆炸隐,命中后 1 s 弹体隐/三层全显; 火球半径 71.4→134.3 m(上限 150 m);爆炸中心与落点 ECEF 差 **0.000 m**; `SMOKE_LIFE_S+1` 后全隐且弹体未复现。 - `check_gui_missile_flow` 一键看爆炸:命中后弹体消失、火球/尘烟可见,相机距落点 **2062 m**、爆炸占 **15679 像素**;截图 `docs/shots/missile-impact.png`。 - 全量 `scripts/verify_all.py`:**71 项 / 通过 68 / 跳过 3 / 失败 0**。 ### v1.24.7 · 弹体姿态改成"相对地面"的飞行方向(2026-10-29) **用户反馈**:截图里尾焰在喷,弹体却水平飞——"推进的时候导弹的方向不符合物理"。 **根因**(两条叠加,都在姿态的取法上) - `velocity_frame_matrix` 把速度**投影到当地水平面**再取 +X。那是给卫星的(v 本来 就近乎水平);导弹助推段弹道倾角 80°+,投影后弹体永远是"平飞"。 - 而且取的是**惯性系**速度。40°N 地表随地球自转向东 0.356 km/s,发射瞬间弹的惯性 速度几乎水平;肉眼看到的爬升方向是**相对地面**的速度 `v − ω×r`。实测改用后者, 弹道倾角与参考程序角表逐点吻合(50 s 83.05° vs 83.02°、100 s 73.95° vs 73.98°)。 **改法** - `viz3d/gltf.py` 新增 `flight_frame_matrix(state)`:+X 沿 `v − ω×r`、+Z 对地、 **不做水平投影**;垂直上升时 +Z 退化(速度与对地方向共线),退回合法正交系。 `velocity_frame_matrix` 原样留给卫星,不动。 - `ui/scenario_main/view.py`:`_missile_attitude` 与尾焰的 `track` 都改用 `flight_frame_matrix`;GLB 的 Rz(-90°) 朝向适配不变。起飞前端点钳位处的残余速度 是 0.1 s 推力冲量(3.5 m/s、垂直),弹体因此**竖立**——与 `_trajectory_state_at` 文档里"起飞前弹竖在车上"的原设计一致(旧姿态系下它一直是平躺的)。 **验证**(实机,真 GL) - `viz3d.gltf` demo:赤道垂直爬升 0.3 km/s(惯性速度被自转带偏 57°)→ +X 仍指径向; 反证惯性系姿态系把同一时刻压成水平。 - `check_ballistic_layer`:起飞前长轴仰角 **90.00°**(竖立);助推段 t+30 s **83.96°**(参考程序角 83.94°)、t+100 s **74.09°**(74.07°);与相对地面速度 夹角全程 0.00°。 - `check_gui_missile_flow`:一键看尾焰 1.5 s 墙钟后弹体仰角 **86.47°**、与相对地面 速度偏差 0.00°;截图 `docs/shots/missile-pitch-boost.png`(弹体沿橙色弹道、 尾焰朝弹尾反方向)。 - 全量 `scripts/verify_all.py`:**70 项 / 通过 67 / 跳过 3 / 失败 0**。 ### v1.24.6 · 停止后再开始:重新对准发射时刻(2026-10-29) **用户反馈**:只有第一次点「开始仿真」是对的;点「停止」再点「开始仿真」, 尾焰没了,还出现导弹车跟着导弹飞。 **根因**(实机复现):`start_sim()` 的"对准发射时刻"只写在**首次装载**分支里。 每次首启把播放头停在发射前 10 s(t=21589 s),**停止会把 `t` 归零到场景历元** (2022-07-24 03:59:59,比发射早 21599 s);再点开始时走"续播",就从历元播起—— 导弹在发射架上趴六个小时、没有尾焰;跟随相机贴着这段静止的弹,车与弹黏在同一 画面里,看上去就是"车跟着弹飞"。实测:停止后再开始,时钟起点 03:59:59、尾焰 全隐、弹↔车距离 0.00 km。 **改法**(`ui/scenario_main/view.py`) - `t <= 0`(首次 / 停止后 / 跑满 24 h 后重播)**每次都重新对准发射前 10 s** 并切 16×;`t > 0` 的点击仍是续播,用户自己拖过的时间轴不被夺回。 **验证**(实机,真 GL) - `check_gui_missile_flow.py` 新增回归:停止→再开始 1.5 s 墙钟后,尾焰两层可见、 弹体离车 3.6 km、Console 第二次出现"已对准发射时刻"。 - 全量 `scripts/verify_all.py`:**70 项 / 通过 67 / 跳过 3 / 失败 0**。 ### v1.24.5 · 「看尾焰」一键入口:主动段只有 12.6 秒墙钟(2026-10-29) **用户反馈**:v1.24.4 的截图里有尾焰,但自己运行软件看不到。 **实机复现**(真 GUI,按用户操作顺序):先点「聚焦弹体」再点「开始仿真」, 发射后 30 s / 150 s 两层尾焰都在、230 s 燃尽后消失——**功能本身正常**。 看不到的原因在时序与尺度: - 主动段只有 201 s,16× 下 **12.6 秒墙钟**就播完;之后整段中段飞行都没有火焰, 这是物理正确,不是没画。 - 全星座视角下 19.6 m 弹体是亚像素,点火那一刻画面上没有任何可见变化; 等用户找到弹体再聚焦,火焰早已燃尽。 **改法**(`ui/scenario_main/view.py`) - 左栏新增 **「看尾焰(重播主动段)」**:聚焦相机(80 m、跟弹)→ 播放头退回 发射前 10 s → 16× 起播。只做两件已有的事(聚焦 + 对准时刻),不改模型尺寸、 不延长尾焰窗口。 - 「聚焦弹体」在燃尽后/未起飞时会明说:`尾焰只在发射后 0~201 s 出现:此刻已是 发射后 331 s……点「看尾焰(重播主动段)」直接播那一段`。 - `_launch_offset_s` 扩成 `_flight_window()`(发射相对秒数 + 主动段终点), `开始仿真` 与 `看尾焰` 共用同一入口。 **验证**(实机,真 GL) - `check_gui_missile_flow.py` 新增回归:点「看尾焰」→ 1.5 s 墙钟后(≈发射后 14 s)两层尾焰可见、相机距焦 85 m、Console 有记录。 - 全量 `scripts/verify_all.py`:**70 项 / 通过 67 / 跳过 3 / 失败 0**。 **口径复议(同日)**:用户提出「尾焰应该在飞行全程一直存在,因为它在提供动力」。 核对参考数据:三级发动机 900/300/150 kN 共 **200.6 s 燃尽**,之后质量恒定 **800 kg**、加速度≈0,是无动力开普勒滑行——全程画焰等于给滑行段画上不存在的 发动机,反而违反「一切符合现实」。用户确认**仍按参考只画助推段(0~201 s)**。 ### v1.24.4 · 导弹尾焰:只在主动段出现的真实米级火焰(2026-10-29) **用户需求**:「能给导弹加尾焰吗?」——尾焰也必须服从同一条口径:真实尺寸、 不做屏幕空间装饰、不编造没有出处的辐射数据。 **口径** - **窗口**:只在记录弧的主动段(boost,0~201 s)出现;发射前、燃尽后都没有。 窗口取 `traj.events` 里 `boost->midcourse` 的切换时刻,不做任何经验延长。 - **几何**:从弹尾向后 30 m、出口半径 = 弹体半径(890.9 mm)、中段鼓到 1.6 倍后 收到尖端;外层半透明橙红 + 内芯黄白(ambient=1 自发光)。**30 m 与 1.6 倍是 外观量级**——参考场景没有登记尾焰长度/直径,不是实测;弹的红外辐射仍由成像链 的燃烧物源算,尾焰几何不参与任何辐射学计算。 - **姿态**:尾焰按 +X 头 / -X 尾建模,用 `velocity_frame_matrix`(+X 沿速度); **不套** GLB 的 Rz(-90°) 挂载——那是给 +Y 长轴的弹体资产用的,套到轴对称的 尾焰上会把火焰喷到弹头前面。 **改法** - `viz3d/scene.py`:`Scene.track(..., visible=fn(jd)->bool)` 逐帧可见性回调; `place_model` 每帧会 `SetVisibility(True)`,所以回调必须在摆位之后覆盖它; `remove/close` 一并清理。 - `viz3d/orbit.py`:`exhaust_plume_mesh`(尾端封盖→出口环→鼓包环→尖端)与 `exhaust_plume_actors`(外层 + 内芯两层)。 - `ui/scenario_main/view.py`:`_boost_end_s` 从状态机事件取主动段终点; `_mount_ballistic_layer` 挂 `ballistic:plume`,可见性 = `0 ≤ 起飞后秒数 < 燃尽点`。 **验证**(实机,真 GL) - `check_ballistic_layer.py`:外层 **30.00 m** / 内芯 16.50 m、出口 890.9 mm → 鼓包 1425.5 mm;发射前隐藏、主动段可见、燃尽后隐藏。 - `check_gui_missile_flow.py`:起飞后约 30 s 两层均可见;80 m 特写里弹体 6,174 px、 **尾焰 3,777 px**(`docs/shots/missile-scale-closeup.png`)。 - `check_missile_flow.py`(离屏):尾焰 30 m、窗口三态(前/中/后)。 - 全量 `scripts/verify_all.py`:**70 项 / 通过 67 / 跳过 3 / 失败 0**。 **注意**:用户在 t=21,930 s 那一帧看不到焰是物理正确的——那是起飞后 331 s, 已过燃尽点。要看尾焰,把播放头拖到发射后前 200 s(点「开始仿真」默认停在发射前 10 s、16× 播放,正好能看到点火与主动段)。 ### v1.24.3 · 跟弹不再夺回手动缩放;近裁剪跟着相机距离收(2026-10-29) **用户反馈**:已经把 3D 视角放大到希腊/土耳其一带、沿着橙色弹道找,还是看不到 弹体。实查不是模型没画,而是四件事叠在一起: - **尺度**:这个视角下 1 px ≈ 2~3 km,19.6 m 的弹体约 **0.01 px**。要看到它, 相机得进到约 1 km 以内(≈20 px);「聚焦弹体」的 85 m 才填得满画面。 - **方向**:默认焦点是地心,滚轮只朝地心推,相机最后落在离弹几百公里的地面上; 弹体在几百 km 高度,也不在橙色线的地面投影点上。橙色线是**整段**弹道 (330 点、1646.4 s、8952.3 km),不是弹的当前位置。 - **近裁剪**:VTK 自动近裁剪面按全场景包围盒算,贴到弹体旁边时近面可达 ~110 km, 19.6 m 的弹体被整段裁掉(探针:80 m 处自动裁剪 0 像素,近面改 0.5 m 后 3,289 px)。 - **跟随**:原 `_tick` 每帧把相机写回 `center + follow_offset`,用户手动缩放/旋转 刚做完就被夺回 85 m 机位。 **改法**(`ui/scenario_main/view.py`) - `follow_offset` → `follow_center`;播放时只按弹体的**位移**平移相机位置与焦点, 用户自己调出来的距离、角度原样保留。 - 新增 100 ms `follow_guard`:跟随态下 `near = max(0.5 m, 相机到焦点距离/1000)`、 `far = 1e6 km`;只改裁剪面,不动相机。 - 恢复全星座视角、关闭「弹道」图层都清跟随状态。 **验证**(实机,真 GL) - `check_gui_missile_flow.py` 新增回归:聚焦 85 m → 手动把相机拉到 60 m → 播放 0.5 s,距离仍是 **60 m**、近裁剪 **0.5 m**、弹体 **12,525~12,545 像素** (多次实测;`docs/shots/missile-focus-zoom.png`,黑锥头/灰弹体/四片尾翼与 Blender 一致);恢复后 176,643 km,继续播放 0.4 s 不再被拉回。 - 全量 `scripts/verify_all.py`:**70 项 / 通过 67 / 跳过 3 / 失败 0**。中途一次全量在 本项上遇到 VTK 多渲染窗口析构的偶发非零退出:进程还没冲刷 stdout,校验器只看到 “空失败详情”;该检查单跑两次均过,复跑全量也过。已给自检末尾加 `sys.stdout.flush()`, 下次即使再遇到析构崩溃,至少能看见断言是否真的过了。 ### v1.24.2 · 撤掉屏幕空间符号:飞行体只画真实尺寸的 GLB(2026-10-29) **用户否决**:把 v1.24.2 中途的 22 px 弹形轮廓与 Blender 里同一份 `files/modles/missile_daodan.glb` 截图对比后指出——3D 视图里的物体**不是 GLB**, 而且它**随视角缩放改变大小**,“仿真软件一切的一切应该与现实符合”。 屏幕空间符号的像素尺寸恒定,世界尺寸就必然随相机变,这本身违反物理; 即便 LOD 会在近距离让位给 GLB,远近两套几何也解释不清。**结论:删掉符号, 3D 视口只渲染模型本体。** **改法** - `viz3d/orbit.py`:删除 `missile_icon_mesh` / `missile_icon_actor` 整套弹形轮廓。 - `viz3d/scene.py`:删除 `Scene.billboard()`、渲染前重写矩阵的 observer 与 `_billboards` 记账,`update()` 不再保存速度。**摆位只由 `track` 的 state/姿态/尺度决定, 相机怎么动都不改写 actor 矩阵**(自检逐元素比对远近机位下的 UserMatrix)。 - `viz3d/scene.py`:`Scene.tracked_position(tag)` 保留,但改为**从摆位矩阵反算 pivot**: `t + linear @ pivot`。`place_model` 的平移项是 `position − R·(pivot·step)`, 直接读平移会差半个弹长;反算后返回的正是 `state.r`。 - `ui/scenario_main/view.py`:弹道图层只挂 `ballistic:body`(GLB,19.6 m 真实尺寸); 跟随与「聚焦弹体」改用 `tracked_position("ballistic:body")`。默认机位 19.6 m 只有 8×10⁻⁵ px,**物理上就是看不见**——要看弹体请滚轮放大到视场 < ~1 km,或点 「聚焦弹体(相机 80 m)」。 - 「聚焦弹体」与特写相机取**弹体长轴**:GLB 的长轴是本体系 +Y(`UserMatrix` 第 1 列), 代理弹体是 +X(第 0 列)。取错列会把相机放到迎头/尾后,只看到尾翼的 X; 修正后 80 m 特写是侧视,锥头/弹体/尾翼的轮廓与 Blender 里一致。 **验证**(实机) - `python -m viz3d.orbit` / `python -m viz3d.scene` 自检通过:pivot 反算在平移与旋转 两种情形都等于 `state.r`;相机换机位前后 UserMatrix 完全相同。 - `check_ballistic_layer.py`:3D 画面 tag 只剩 `ballistic:body` + `ballistic:line`, 弹体最长轴 **19.52 m**(GLB 归一化 19.6 m 的轴对齐盒),**无屏幕符号**;飞行中 被跟踪点与 `state_at` 差 **0.0000 m**,起飞前与弧首点差 0.000 m;弹道线 330 点、 地固回归 0.225 m / 0.202 m。 - `check_missile_flow.py`:弹体 **20.14 m**(19.6 m 旋转后的 AABB)、无屏幕符号、 与 `state.r` 差 **0.011 m**;红外 166 帧(162 有效 + 4 帧黑障)。 - `check_gui_missile_flow.py`:全球视角 943~1,382 种颜色(弹体是亚像素);80 m 特写里 弹体占 **6,169 像素**(真实 19.6 m 侧视投影);聚焦 85 m、恢复 176,643 km。 全球视角不再用“藏起模型前后像素差”当证据——那反而需要一个假符号才能成立。 - 完整 `scripts/verify_all.py`(带 `SIMULATION_DM8_PASSWORD`):**70 项 / 通过 67 / 跳过 3 / 失败 0**,与 v1.24.1 持平;跳过仍是缺可选 `sgp4` 与两项按设计跳过的界面检查。 首轮全量曾在 `check_gui_missile_flow` 上遇到一次 `wglMakeCurrent` 的 GL 上下文偶发 失败(输出里只有 VTK 渲染窗口报错、无 Python 堆栈);该检查单跑 4 次与第二次全量 均通过,按环境偶发记录,不当作功能回归。 ### v1.24.1 · 记录弧换历元要跟着地球转;参考弹补 assumed 温度(2026-10-29) **起因**:达梦口令可用后,把 v1.24 遗留的实机检查全跑了一遍。`check_ballistic_layer.py` 第一次真跑就暴露两件事:记录弧换历元后发射点不在登记位置、以及参考弹没有温度曲线时 成像页按设计拒绝出图。 **修掉的缺陷** - `engine/runner.py`:参考记录弧的 `r_eci/v_eci` 钉在弧自己的历元 `epoch_jd_utc=2457755.0`(2017-01-01T12:00Z)。场景历元是 2022 时,原实现只换时间戳 不转地球,等于让地球在弧底下转过 ΔGMST=131° —— **发射点从登记的 40°N/30°E 漂到 38.7°E(东移 8.73°、约 740 km)**,整条星下点轨迹跟着偏东。新增 `_rebase_arc()`: 整条弧的 `r_eci/v_eci` 同乘 `Rz(θ(to)−θ(from))`(经 `eci_to_ecef → ecef_to_eci`), 只换朝向、不改形状。修后发射点回到 **40.00°N/30.00°E**,落点经度 **−59.998°E** 与登记一致。 - `scripts/seed_reference_missile.py`:参考场景没有登记红外温度,`TARGET` 行原先把 `TEMP_CURVE_JSON` 留空 —— 成像页按契约**拒绝出图**(不编默认温度),整条成像链对 参考弹不可用。现按 `assumed` 登记一组阶段温度与 0.8 m^2 弹头投影面积,数值沿用方案 示例弹 `TGT-DEMO-01` 早已登记的同一套假设,`source` 写明“参考场景未登记、按示例弹假设、 不是实测,报告不得引用为实测值”,`source_type=assumed`,界面横幅原样显示。 种子脚本的 GBK 写库坑:公式串里的 U+2212/U+00B2 换成 ASCII `-`/`m^2` 才能进达梦。 **自检脚本自身的误报(不是产品缺陷)** - `check_ballistic_layer.py`:VTK 的 `GetOrientation()` 把角度回绕到 (−180°, 180°], 按模 360° 比较;参考弧 330 点非均匀(平均 5.00 s),t+200 s 改取最近的采样点; ⑤ 的参照物从“导弹车”改为“弧首点随地球转到当前帧”——车是地面几何,包围盒中心随 GLB 模型 pivot 偏几米,拿它当被跟踪点会把外形误差混进取证。 - `check_missile_flow.py`:远地点/射程经验比(0.20~0.27)只适用于按最小能量式重积分的 弹道;参考记录弧是参考系统自己飞的(本弧 946/8952=0.106,压低弹道),有 `arc_file` 时不再套这条尺子,只校验落点、阶段与几何。④ 的 `step` 是"每 step 个弹道点出一帧" 而不是秒:弧是 ~5 s 非均匀采样,原写死 `step=10` 实测每帧 ~50 s,再入段 8 个点整段 被跳过;改按弧的中位间隔折算(10 s/帧)。黑障帧按设计是把目标推出视场、记 E03 负样本, 所以"整段全部有效"改成"除黑障外全部有效"(本弧 162 有效 + 4 黑障)。⑤ 弹体与 `state.r` 按最近采样时刻比:200 s 落在两个采样点之间,线性内插值与端点采样值差 3.9 km。 - `check_imaging_launcher.py`:① 门限从 1e-6 像元放宽到 0.1 像元 —— 弧首点来自参考 记录文件、车走 `geocentric_to_ecef`,两条取点链路实测差 0.25 m(0.0032 像元),而 球面/椭球面取点分家是 22.8 km(约 3000 像元),差四个数量级仍抓得住;③ 单像元 DN 改为 5×5 PSF 足迹峰值 —— 点源双线性摊布再过 5×5 高斯,最近像元只 +0.3 DN 会被 量化吃掉(实测足迹里 +1 DN);序列打印按弧的实际覆盖(1646 s)报数,不再写死 40 s。 - `check_gui_missile_flow.py`:生成失败时不再干等 300 s 超时,改为把成像页 Console 末行与摘要/来源行一起抛出(本轮就是靠它直接读到“场景 target.phase_curve 为空”)。 **连带修掉的界面误报**:`ui/imaging_sim/view.py` 的 `ground_frames_note` 用 “主目标拍得到却不给车”当链路缺陷探针,只排除了 `blocked` 与侧摆超限,没排除黑障帧 —— 参考弧末段 8 帧黑障被误报成链路缺陷。探针补 `not blackout`,并在该模块自检里钉了一条 黑障用例。 **数据口径的连带修正**:弹道目标换成参考弹后,导弹车 `VEH-TEL-01` 按 `seed_missile_car.py` 的既定口径(继承弹道目标的发射点)重新灌了一次,位置从 39.9°N/116.4°E 移到 **40°N/30°E** —— 机动发射平台与弹同址,`check_imaging_launcher` 的“同帧两源”才成立。 **验证**:`SIMULATION_DM8_PASSWORD` 就位后跑完整 `scripts/verify_all.py` —— **70 项:通过 67 / 跳过 3 / 失败 0**。达梦依赖的 11 项全部实跑通过: `check_ballistic_layer.py`(开页装配、3D/2D 同源、发射点 40°N/30°E)、 `check_imaging_launcher.py`(车与弹同址起飞、飞行中分得开、5×5 足迹 +1 DN)、 `check_missile_flow.py`(整段 1646 s、0 断档、166 帧 = 162 有效 + 4 黑障负样本、 20 帧可探测)、`check_gui_missile_flow.py`(点开始仿真 → 点成像 → 34 帧、0 负样本、 5 帧可探测)。剩余 3 项跳过均非缺陷:`core.orbits.sgp4_prop` 缺可选依赖 `sgp4`; `check_gui_launch.py` / `check_gui_scene_run.py` 按设计跳过(后者会覆盖用户存过的 `scn_sim_main`,不能自动跑)。 ### v1.24 · 通用可插拔求解器落地(2026-10-29) **做了什么**:把项目里已有的轨道、弹道、热、姿态、环境模型提取成**可插拔插件**, 新增 `solver/` 层(`engine → solver → core`),用能力字段 (`provides/requires`)编译成 DAG 求解,产出一份与观察者无关的 `SceneSolution`,供三维摆位与红外成像共同消费。 **新增** - `solver/contract/`:`Project`/`Entity`/`Model`/`SolveContext`;字段名校验、 输出形状/dtype/有限值校验、出处(provenance)。 - `solver/registry/`:模型登记;id 重复默认拒绝,`load_plugins([...])` 显式装载。 - `solver/runtime/`:项目 JSON 解析(严格拒绝未知键)、能力图编译、 **缺提供者/重复提供/成环/域不匹配**编译期拒绝、拓扑求解; `state_functions()` 只建连续函数,不预采样整条时间轴。 - `solver/solution/`:`SceneSolution`/`EntitySolution`;采样点精确命中, 位置用 Hermite、姿态用 SLERP,**超出时间轴不外推**;`fingerprint()`; `save/load` 带 `arrays_sha256`,篡改一个字节即拒绝。 - `solver/observation/`:把同一份解拼回 `Trajectory`,温度逐帧经 `temperature_by_time=` 交给红外链;缺帧温度直接报错,不回退阶段曲线。 - `solver/models/`:7 个内置插件 —— `orbit.two_body`、`orbit.j2`、 `ballistic.recorded`、`thermal.prescribed`、`thermal.constant`、 `attitude.nadir`、`environment.standard`;全部只包装 `core/` 公开函数。 - `solver/projects/`:`orbit_demo.json`(三模型串联)、`missile_arc.json` (参考记录弧 + 定值温度,300 K 明示为联调假设值)。 - `solver/__main__.py`:`python -m solver list / run `。 - `engine/solver_bridge.py`:九段式场景 → solver 项目的唯一翻译点; `engine.runner.constellation_state_functions` 改为委托它。 - `scripts/check_solver.py`:7 组独立验收;已计入 `verify_all.py`。 - `docs/solver.md`:契约、插件写法、内置模型边界、验收与已知限制。 **改动的既有代码(均为提取/公开,不换物理口径)** - `core/orbits/hpop.py`:`_sun_position`/`_moon_position` → 公开 `sun_position`/`moon_position`,避免跨模块私有访问或复制公式。 - `core/ir_imaging/chain.py`:`_phase_temperature`/`_phase_emissivity` → 公开; `sequence_from_trajectory(..., temperature_by_time=None)` 支持上游解供温; 视线穿地球的帧不再去算 >90° 天顶角的大气路径(τ 记 0,不崩整段序列)。 - `engine/runner.py`:星历函数委托 `engine.solver_bridge`,报错文本不变。 **验证** - `scripts/check_solver.py`:插件登记/DAG 诊断/轨道逐位对照/解包存取与篡改/ 弹道逐点对照/观测温度逐帧一致/引擎桥接逐位一致,全部通过。 - `verify_all.py`:**70 项 / 57 通过 / 13 跳过 / 0 失败**;跳过项均为 达梦口令或 GUI/可选依赖,未伪装成通过。 - 目标回归:`core.ir_imaging.chain`、`check_radiometry_physics.py`、 `check_reference_ballistic.py --demo`、`engine.runner` 均通过。 **未做(不要当成已完成)** - 没有瞬态热网络:`thermal.*` 只有“登记曲线/定值”两档,出处写明非求解结果; - 记录弧不插值、地影无半影、姿态无动力学、轨道只到 J2; - 状态码枚举(VALID/DEGRADED/…)与完整 V&V 矩阵仍是设计,未做成可运行字段。 ### v1.23 · 真实尺寸弹体“只看得到一个点”的查看方式(2026-10-27) **现象**:用户放大到区域后只看到一个黄点/红点,没有弹体。这是**观看距离**问题: 默认机位 1 px≈237 km,19.6 m 的 GLB 只有 **8×10⁻⁵ px**;黄点是 `ballistic:marker` 的 9 px 定位符号,不是模型消失。 **新增(不改模型尺寸/物理量)** - 左栏「图层开关」下新增两个按钮: - **聚焦弹体(相机 80 m)**:相机移到弹体侧后方 80 m,30° 视场下 19.6 m 弹体 约占画面高度 1/4;播放时相机**跟随弹体**(否则 16× 下 0.1 s 就飞出视场)。 - **恢复全星座视角**:逐位还原装配时的相机(不再用 `ResetCamera` 重新拟合, 实测会从 177,000 km 漂到 231,596 km)。 - 关闭「弹道」图层时自动退出跟随,避免相机盯住一个已摘掉的点。 **验证** - `check_gui_missile_flow.py`:聚焦后相机距弹体 **85 m**、恢复后 **176,643 km**; GUI `39/39/14/0` 全过。 - `check_ballistic_layer.py`:弹体 19.06 m、定位点与 `state.r` 差 0.0000 m。 - `verify_all.py`:67 项 / 64 通过 / 3 跳过 / 0 失败。 ### v1.22 · 导弹模型替换为用户提供的 GLB(2026-10-27) **资产**:`C:\Users\admin\Downloads\Untitled.glb`(17,092 B,sha256 `d80ad5a7…abafe0a`)→ 复制为 `files/modles/missile_daodan.glb`,**加载路径不变** (`ui/scenario_main._MISSILE_GLB`)。格式:glTF 2.0 二进制,Blender 导出; 4 网格(Cone / Cube / Cube.001 / Cylinder)、3 个 PBR 材质 (Warhead_Radome / Fin_Gunmetal / Body_Aluminum),无贴图。 **两个必须适配、否则会“横着飞/只有 5 m”的点** 1. **长轴方向**:新模型长轴是 **+Y**(Cone 在 +Y 端),而 `velocity_frame_matrix` 约定模型 **+X** 沿速度。`_missile_attitude()` 只在 GLB 分支左乘 `Rz(-90°)`(`_MISSILE_MODEL_MOUNT`),把 +Y→+X;代理弹体不加这个旋转。 2. **原生尺寸**:GLB 原生最长轴 **5.0 单位**,项目登记弹长 **19.6 m**。 `_missile_body_actor()` 用 `19.6 / 5.0 = 3.920` 做**资产归一化**; `_GROUND_SCALE=1.0` 与 `_GLB_UNIT_SCALE=0.001`(m→km)都不改。 加载日志写明“原生 5.000 单位 → 归一化 ×3.920”,不把 5 m 当真实弹长。 **加载/摆位/材质配置** - 仍走 `viz3d.gltf.load_gltf` + `model_bounds` + `Scene.track`; - `pivot = model_bounds().center = (0, 2.5, 0)`,摆位后**模型 pivot 对齐 `state.r`**; - 材质完全使用 GLB 自带的 PBR 参数,代码不覆盖颜色/金属度/粗糙度; - 定位点从 `ballistic:body` 拆到独立 `ballistic:marker` tag,几何点放在模型 pivot 上, 9 px 屏幕尺寸;这样模型长度/包围盒断言只量模型本体,不会被符号污染; 弹道图层开关仍按 `ballistic*` 统一摘挂。 **验证** - `check_ballistic_layer.py`:GLB 归一化后弹体最长轴 **19.06 m**(19.6 m 旋转轴对齐盒), 定位点与 `state.r` 差 **0.0000 m**,弹道线 380 点、地固回归 0.057 m / 0.235 m。 - `check_gui_missile_flow.py`:80 m 特写弹体占 **8,389 像素**(旧代理约 2,103), 模型+定位点+弹道线都在场;界面 39/39/14/0。 - `verify_all.py`:**67 项 / 64 通过 / 3 跳过 / 0 失败**。 **说明**:新模型是用户提供的外形资产,不是参考系统原弹的实测几何;材质/尺寸来自 GLB 本身 + 项目登记弹长归一化,未做实物标定。 ### v1.21 · 弹道轨迹改地固坐标,随地球自转同步旋转(2026-10-27) **根因**:`ui/scenario_main._mount_ballistic_layer` 把 `target_trajectory` 的 **ECI** 采样点 直接交给 `orbit_actor`,而 `Scene.update()` 只对 `earth` tag 施加 GMST。结果是: - 地球按 GMST 转,弹道线留在惯性系里不动; - 播放时发射点/落点相对大陆**漂走**(截图里弹道跑到北美上空); - 飞行体按 ECI 摆位没错,但和"不跟地球转的线"只在个别时刻对得上。 **最小修改(3 处,不改物理量)** 1. `ui/scenario_main/view.py`:每个采样点先按**它自己的时刻**转 ECEF `ecef_points = [eci_to_ecef(p.state.r, p.state.t)]`,用 `lines_actor` 画地固折线; 随后 `scene3d.fix_to_earth("ballistic:line")`。 2. `viz3d/scene.py`:新增 `fix_to_earth(tag)` / `earth_fixed_tags()` 与 `_earth_fixed` 集合; `update(jd)` 对地球和所有地固 tag 施加**同一个** `GMST`。ECI actor(卫星轨道、飞行体) 不登记,保持惯性系。 3. `scripts/check_ballistic_layer.py`:逐点比对从 ECI 改为 ECEF,并新增地固回归 (发射帧线起点回到 ECI 发射点;t+200 s 飞行体仍在线上)。 **坐标口径** | 对象 | 坐标系 | 摆放 | |---|---|---| | 地球网格/贴图 | ECEF 建模 | 每帧 `Rz(+GMST)` | | 弹道线 | ECEF(逐点按采样时刻转) | 每帧 `Rz(+GMST)`,与地球同一角度 | | 飞行体 / 卫星 | ECI | `Scene.track` 按 `state_at(jd)` | | 2D 星下点 | 经纬度 | `State.to_geodetic()`,天然地固 | 飞行体在任意帧都落在"该帧采样点"对应的线点上(因为 `Rz(GMST(t))·ECEF(t) = ECI(t)`),但整条线是贴在地表的那条弧,不再相对大陆漂移。 **验证** - `check_ballistic_layer.py`:线 380 点;发射帧线起点与 ECI 发射点差 **0.057 m**; t+200 s 线随 GMST 从 **36.08° → 36.91°**,飞行体仍在线上(差 **0.235 m**)。 - `verify_all.py`:**67 项 / 64 通过 / 3 跳过 / 0 失败**。 **说明**:这是"把弹道画在地表上"的显示口径,不改变 `target_trajectory` 的 ECI 动力学; 若要画严格的惯性系轨迹(相对大陆会漂),应恢复 ECI 线并明确图例,而不是两者混用。 ### v1.20 · 正确性优先:发射率进链、亚像元落点、温度伪彩与显式边界校验(2026-10-27) **起因**:用户要求"先保证数据处理与成像结果的正确性,再兼顾性能/效率/可读性"。这一版只做 会改变**物理量或可解释性**的改动,不扩大功能面。 **关键数据校验与异常处理(全部显式,不静默回退)** - **发射率 ε**:`T_TARGET.TEMP_CURVE_JSON` 本来就允许登记波段发射率(impact/combustion 已有 `[0.2, 0.6]`),旧链却直接 `band_radiance(temp)` 等价 ε=1。现 `_phase_emissivity()` 按"阶段 ε → 顶层 ε → 1.0(黑体假设)"取值: - 标量或 `[min,max]`(区间取中值,与温度曲线同口径); - 非有限、≤0、>1、区间颠倒 → `ValueError`,不往下算; - 未登记时返回 `1.0` 并在标签/界面写明"未登记发射率,按黑体 ε=1.0(assumed)", 不把黑体假设伪装成实测。 - **点源亚像元落点**:`render_frame` 原用 `int(col)/int(row)` 截断,偏中心时最多 0.5 像元位置偏差。现 `_deposit_point()` 按小数像元双线性摊到 4 邻元;面源 (目标充满像元)仍只写整数像元,不能摊(摊布等于把面源拆成 4 个点源)。 - **温度伪彩**:新增 `band_brightness_temperature`(带内 Planck 积分反解)与 `radiance_to_band_temperature` 查表反演;调色板只作用于**亮温**,不是给 DN 套彩虹。 越界像元返回 `out_of_range` 掩码并在图例里计数,不把钳位值当实测温度。 界面默认温度伪彩,可关回原始 DN 灰度;伪彩失败(NaN/波段非法/整幅越界)时 自动回退灰度并把原因写进帧标题,不拖垮成像。 - **有限性与量纲**:`render_frame` 对目标/背景辐亮度、天顶角、观测高度、离轴角、 PSF 宽度、像移量、面积、斜距、目标高度、满量程逐项检查有限性/范围;`render_sequence` 检查时刻有限且单调不减、逐帧辐亮度有限非负、满量程有限正;`sequence_from_trajectory` 检查弹道点时刻/状态/高度、背景温度、侧摆限位、`ref_area_m2`。任何 NaN/Inf 都当场 `ValueError`,不让它污染整幅辐亮度图后"看起来像目标消失"。 - **弹道点亮温反演**:`band_brightness_temperature` 对 `band_radiance(T, band)` 二分, 量纲与入参一致(不是拿中心波长光谱量反解);超出 1~10000 K 报错,不外推。 **验证** - `scripts/check_radiometry_physics.py` 扩到 **10 条**:新增 ⑧ ε=0.5 时目标像元严格减半 (实测比值 0.500315,差值来自与 ε 无关的程辐射);⑨ 0.3 px 偏置的渲染质心 (256.000, 320.300)、误差 0.000 px;⑩ 10 类非法输入全部显式拒绝。 - `check_gui_missile_flow.py`:温度伪彩图例带 K、开关能回退 DN 灰度再切回伪彩; 39 帧几何有效 / 14 帧可探测不变。 - `verify_all.py`:**67 项 / 64 通过 / 3 跳过 / 0 失败**。 **当前数据的实话**:库里温度曲线**没有登记 boost/midcourse/reentry 的 ε**,所以当前 场景仍按 ε=1.0 黑体计算;界面会显示"未登记发射率,按黑体 ε=1.0(assumed)"。 要得到更接近真实材料的辐亮度,必须在 `TEMP_CURVE_JSON` 登记 ε(带 source), 本版不会替用户编一个 ε。 ### v1.19 · 导弹模型改为真实尺寸(19.6 m),定位改用屏幕空间符号(2026-10-27) **起因**:用户截图里 1600 km 的橙色弹体横跨半个中国,要求"按现实中导弹的真实尺寸显示"。 v1.17 的 1600 km 是**观感放大**(`_MISSILE_SPAN_KM`),不是弹体数据。 **改了什么(缩放方式与单位)** - 世界单位仍按 `viz3d` 约定是 **km**;真实弹长 `_MISSILE_LEN_M = 19.6 m`, 换算一次:`19.6 m ÷ 1000 = 0.0196 km`。 - 代理弹体几何**直接以 km 建模**:`missile_actor(length=0.0196, radius=0.00089)`, 直径 = 19.6 / 长细比 11 = **1.78 m**(长细比只决定外形,不是登记数据)。 摆位用 `Scene.track(..., scale=1.0, glb_unit_scale=1.0, pivot=(0,0,0))`, **不再乘任何观感系数**;模型中心对称,pivot 取几何中心,摆位后盒中心仍等于 `state.r`。 - GLB 路径不变:GLB 单位是 **m**,`glb_unit_scale=0.001` → km,`scale=_GROUND_SCALE`, `pivot` 取 `model_bounds()` 的几何中心。 - 新增 `viz3d.orbit.missile_marker_actor`:一个 **9 px 屏幕空间定位点**(VTK `PointSize` 按像素、不随缩放变化)。真实 19.6 m 在默认机位 1 px≈237 km 下只有 **8e-5 px**, 没有这个点就完全找不到;它**不是模型的一部分**,不参与包围盒/尺寸断言, 日志里写明"仅指示位置,不代表尺寸"。 - 移除 `_MISSILE_SPAN_KM`;`check_ballistic_layer.py` 的弹体窗口改按 `_aabb_window(_MISSILE_LEN_M)`;`check_missile_flow.py` / `check_gui_missile_flow.py` 增加"模型最长轴 ∈ [11.3, 19.6] m、定位点 9 px"的断言。 **验证** - `check_ballistic_layer.py`:弹体世界包围盒最长轴 **17.20 m**(19.6 m 旋转后的轴对齐盒, 窗口 [10.75, 20.58]),tag 下 2 个 actor(模型 + 定位点)、点径 9 px。 - `check_missile_flow.py`:真实长度断言通过;盒中心与 `state.r` 仍差 0.003 m。 - `check_gui_missile_flow.py`:点「开始仿真」后模型 + 定位点都在场,逐帧运动正常。 **直接后果(必须说明)**:默认全球视角(装下 42157 km 的 GEO 轨道)下 19.6 m 是亚像素, **看不到弹体形状**——这是物理正确的显示结果。要看到弹体请滚轮放大到视场 < ~20 km (或看 2D 平面图的弹道弧);屏幕上的 9 px 点是定位符号,不是弹体被放大。 ### v1.18 · 成像正确性审计:修掉大气与噪声两处量值错误,并纠正"39 帧全有效"的口径(2026-10-27) **起因**:用户追问「目前的成像是正确的吗?」。这不是要更多图,而是要一条能被独立证伪的证据链。 审计按「原始输入 → 公式 → 代码 → 独立复算」做,**不复用被测函数当答案**;辐射链、时空几何、 可探测性三层分别给结论。 **结论一:数值层有两处确认缺陷(已修)** - **大气路径名不副实**:`band_transmittance` 的 `altitude_km` 是卫星高度,目标高度被当成 0。 中段目标在 162.9 km、卫星 8367 km,代码仍按"地面→卫星"整层大气给 τ=0.705;真实路径只从 目标高度往上,τ≈1 —— **目标信号被低估 42%**。同时背景与目标共用同一个 τ/L_path, 而两者是两条不同路径。现按 `exp(-z_t/H) − exp(-z_s/H)` 算路径、目标与背景分开传; 天顶角也从固定 0° 改成**逐帧目标处真实天顶角**(本场景 38~43°,旧值把 airmass 少算约 1.4 倍)。 - **噪声单位混用**:`netd_to_noise_radiance` 返回 **每微米**辐亮度噪声,却被直接加到 **波段积分**辐亮度图上。3~5 μm 下噪声低估 **2.35 倍**(8.66e-4 vs 2.03e-3 W·m⁻²·sr⁻¹)。 现新增 `netd_to_noise_radiance_band`(带内积分导数),`ImagingFrame.noise_radiance` 与 `band_radiance_map` 同量纲,`frame_quality` 不再靠"乘 Δλ"打补丁。 **结论二:三维与成像用的不是同一份星历(已修)** - 三维摆位走 `core.orbits.ephemeris.state_function`(J2),成像/跟踪走 `constellation.states_at()`(开普勒平均运动)。实测同一颗星差 **LEO 23~58 km、 HEO 9~18 km、GEO 1~2 km**。现 `engine.runner.constellation_state_functions` 给两处 同一份 J2 星历;`engine.scene.scene_from_rows` 把 `orbit.propagator` 写进九段式。 - 发射时刻原先取"第一个整段可拍的候选",恰好落在**临边**:25.9° off-nadir、目标处天顶角 85°、airmass 11.5。现按"整段最坏 off-nadir 最小"选:**t=8188 s、13.46°、天顶角 38~43°**。 **结论三:这款载荷看不见中段——v1.17 的说法必须更正** - 中段 0.8 m² / 290 K / 斜距 1.3 万公里,目标像元信号只有 **0.041 DN**,噪声 σ=**19.3 DN** (SNR≈0.002)。39 帧里只有 **14 帧**(助推/再入/落地)达到 SNR≥3。 - v1.17 写的"39 帧全部有效"**只是几何判据**(遮挡/侧摆/视场),不代表拍到了弹; 当时"助推 834 DN > 中段 42 DN"里的 42 DN 是 32 万像元高斯噪声的最大值(≈5σ), **不是目标信号**。这条按本版更正。 - 这不是 bug,是 3~5 μm 点目标的探测限:没有实测标定或外部基准前,只能称 "在给定假设下内部一致",不能称"物理上正确"。 **改动**(依赖方向照旧单向向下) - `core/environment/atmosphere_radiation.py`:`band_transmittance(..., target_altitude_km=0)` 与 provider 第 4 参;目标低于地面/高于卫星当场拒绝;`target_altitude_km=0` 时与原实现逐位一致。 - `core/sensor/model.py`:新增 `netd_to_noise_radiance_band`;旧函数标注"只用于光谱量"。 - `core/ir_imaging/chain.py`:`render_frame` 分目标/背景两条大气路径、带内噪声、 `ImagingFrame.background_transmittance`;`sequence_from_trajectory` 逐帧算目标处天顶角、 目标高度、目标路径 τ,并写 `target_signal_dn / noise_dn / snr / detectable` 标签; `frame_quality` 明确"画面峰值/噪声 ≠ 目标 SNR"。 - `engine/runner.py`:`constellation_state_functions`(与三维同档)、`_off_nadir_score`、 `derive_launch_time` 选最优几何;`build_target_plan` 接受统一星历表。 - `ui/imaging_sim/view.py`:观测星与星历统一;帧标题/摘要增"目标信号·噪声·SNR·可探测", 摘要把"几何有效"与"可探测"分成两个数;SNR 卡改用物理目标 SNR;AGC 明示只改显示。 - `ui/scenario_main/view.py`:飞行体与轨道不变(本版未改三维口径)。 - 新增 `scripts/check_radiometry_physics.py`(7 条独立复算,不连库); `check_missile_flow.py` 改用逐帧 `target_signal_dn` 判信号;`check_ballistic_layer.py` 的"飞行中"取样改成按 64× 实播到 30% 弹道(进度条一格 864 s,放不进 379 s 的弹道); `check_imaging_launcher.py` 统一星历;`verify_all.py` 增至 67 项。 **验证(本机带口令实跑)** - `check_radiometry_physics.py`:Planck 带内积分相对偏差 5.55e-6;点源链 0;PSF 通量守恒 2.2e-6; 大气 τ=0.621(斜程 42.7° 地面)/1.000(162 km),同帧目标信号比 1.610 = 1/τ; 带内噪声与细网格差 9.4e-7、32.7 万像元样本 σ/σ₀=0.9987;量化与量纲共 7 条全过。 - `check_missile_flow.py`:3 颗星、弹道 380 点/379.0 s/远地点 156.0 km/落点差 13.1 km; 跟踪 1 弧段 0 断档、380/380 覆盖、侧摆 12.1~13.5°;39 帧几何有效、**14 帧可探测**; 中段 0.041 DN < σ 19.3 DN(物理探测限)。 - `check_gui_missile_flow.py`:真窗口点按钮 —— **总帧 39 · 有效 39 · 可探测 14 · 负样本 0**。 - `verify_all.py`:**67 项 / 通过 64 / 跳过 3 / 失败 0**(跳过项:sgp4 缺包、 `check_gui_launch` 已装真 PySide6、`check_gui_scene_run` 库内已有场景)。 **仍未证实 / 明确限制** 1. 没有实测 SRF/PSF/NETD 标定、没有大气实测(`_EXTINCTION` 是 assumed 参数化模型)、 没有外部基准 —— 因此**不能**宣称"与真实传感器一致"。 2. 落点差 13.1 km:球面最小能量解 + 自转的科氏项未做二维打靶。 3. 背景温度整段只取一个值(按发射点),未随目标星下点变化;PSF 5×5 截断。 4. 黑障门限 80 km / 3 km/s 使本弹无黑障帧(再入 2.5 km/s),属物理结论。 5. 目标面积 0.8 m²、温度曲线均为库内 assumed 数据,本版未改。 ### v1.17 · 「开始仿真」出 1 枚导弹 + 3 颗卫星并逐帧运动;「成像」出整段飞行的红外序列(2026-10-26) **起因**:用户口径「点开始仿真后应出现一枚导弹与 3 颗卫星的轨道及其运动动画,点成像后应能看到 导弹整个飞行过程中的红外成像」。实测两条都不成立,各有硬证据: - **导弹根本看不见**:缺弹体资产时代理只是一根 **19.6 m** 的体轴线,而默认机位 1 px ≈ 67~237 km —— 亚像素,等于没画;而且它在**开页装配**时挂上、不点按钮也在,与"点开始仿真后出现"对不上。 - **成像只覆盖飞行的一小段**:观测计划算的是**固定观测点**(覆盖对象/发射点)的过境窗口, 目标一飞出去就出 ±30° 侧摆锥 —— 实测 631 km 射程的弹 101 帧里只有 5 帧有效,其余全按 E03 负样本。 - **弹道本身算错两次**:`launch_state` 的初速只做坐标旋转、没带 ω×r(落点偏西 134.6 km); 引擎又在冲量初速上**再叠一张 assumed 的 200 kN 推力表**(Δv 算两遍)→ 远地点 2379 km、 落点偏 1400 km、飞行 2329 s。射程 631 km 的弹不该这样飞。 **做了什么**(依赖方向照旧单向向下;每条都带可复跑的取证) - `core/ballistic/state_machine.py` - `min_energy_launch_speed` 换成**球面最小能量解**(a=(2r+c)/4、γ=45°−Φ/4),631 km 射程上 发射速度 2.427 km/s、发射角 43.58°(平地式给 2.487/45°,把落点打远 22 km)。 - `launch_state` 的初速改走 `ecef_to_eci_velocity`(**补 ω×r**):发射点在惯性系里随自转东行 0.354 km/s,漏了这项落点系统性偏西 134.6 km。 - `BLACKOUT_SPEED_KMS` 2.0 → **3.0 km/s**(公开口径是高超声速 Mach 10 量级;2.0 无出处)。 代价如实:本库这条 631 km 弹再入速度约 2.5 km/s,**达不到门限 ⇒ 没有黑障帧**。 - `core/coverage/access.py`:`earth_occludes` 判"在地球内部"改 `f < 1 − 1e-9`。原先严格不等号让 浮点噪声决定贴面点是否被遮挡(GEO 对椭球面地面点会误判成遮挡)。 - `core/scheduling/handover.py` 新增 `build_tracking_plan`:**移动目标**逐帧择星(能拍的星里取 侧摆最小),相邻同类帧合并成弧段、按 E01/E03 记断档,`conservation_check` 照样成立。 与 `build_observation_plan`(固定点接力、四判据)**分工并存,互不替代**。 - `engine/runner.py` - 新增 `build_target_plan`:按**每一帧目标所在位置**判遮挡/侧摆,交给上一条编排。 - 新增 `derive_launch_time`:在卫星过境窗口里采样候选发射时刻,逐个用粗栅格弹道验证 "整段飞行是否有星能拍",取第一个全程无断档的,写进 `meta.provenance`(derived)。 本库结果:**t=4070 s**,SAT-IMG-02(HEO,8367 km 高度)全程可拍,侧摆 24.7~25.9° ≤ 30°。 - `target_trajectory`:推力表 source 是 `ASSUMED_THRUST_SOURCE` 时**不叠加推力**(只当阶段时钟), 落点回到登记值。 - `viz3d/orbit.py` 新增 `missile_mesh` / `missile_actor`:**代理弹体**(首尾双锥+中段柱体,+X 沿速度), 故意做成关于原点**中心对称**——任意姿态下 AABB 中心恒等于被跟踪点,`check_ballistic_layer.py` 的"盒中心 == state.r"断言才不会被形状偏置误伤。 - `ui/scenario_main/view.py`:弹体改用代理模型、画面长度 `_MISSILE_SPAN_KM=1600` km(**观感放大**, 登记弹长 19.6 m 写进日志);首次点「开始仿真」补挂弹道图层、把播放头停到**发射前 10 s**、 倍速切 **16×**(默认 256× 下 379 s 的弹道只播 1.5 s,看不清在飞); `seed_page` 改 `QTimer.singleShot(0, ...)`、并 `atexit` 等后台线程收尾(见下"踩到的坑")。 - `ui/imaging_sim/view.py`:`_compute` 改用 `build_target_plan` 逐帧择星,观测星写进每帧标签与 帧标题;未联动路径的场景时长回到**一天**(截到 1000 s 会把发射时刻候选切光); 删掉 `pick_observation`/`_covering`(v1.12 时代的固定点口径,已无调用方)。 - `core/ir_imaging/chain.py`:`sequence_from_trajectory` 的抽帧**必带末点**——`[::10]` 停在 370 s, 落地帧会被漏掉。 - 新增 `scripts/check_missile_flow.py`(离屏端到端)与 `scripts/check_gui_missile_flow.py` (真窗口点按钮),都进 `verify_all`;`check_ballistic_layer.py` 的弹体窗口改为代理口径、 `drift` 门限按 float32 模型精度放到 0.1 m;`check_imaging_launcher.py` 改用逐帧择星。 两个 v1.12 前提的界面检查(`check_gui_imaging_sat`/`check_gui_sim_imaging`)**移出 verify_all** 并在文件头标注被谁取代。 - 文献:`LITERATURE.md` 补"弹道与再入""红外点目标与噪声"两节,逐条给可查链接。 **验证**(本机带口令实跑) - `scripts/check_missile_flow.py`:3 颗星轨道线 3 条、弹道 380 点 / 379.0 s、远地点 156.0 km (0.247×射程,公开经验 0.20~0.27)、落点与登记值差 **13.1 km**、四段齐全; 跟踪计划 **1 条弧段 / 0 段断档**、逐帧覆盖 **380/380**;红外序列 **39 帧全部有效**、 背景之上信号 助推 834 DN > 中段 42 DN;弹体 230 s 位移 422 km、盒中心与 `state.r` 差 0.003 m。 - `scripts/check_gui_missile_flow.py`:点「开始仿真」→ 3 个 `sat:` 模型 + 3 条轨道线 + 弹道线/弹体 都在场,2.5 s 墙钟内导弹挪 77 km、SAT_1/2/3 各挪 305/218/123 km;点「成像」→ **总帧 39 · 有效 39 · 负样本 0**,帧标题点名观测星 SAT-IMG-02,画布 1280×1024 真帧。 - `scripts/verify_all.py`:**66 项 / 通过 63 / 跳过 3 / 失败 0**(带口令);无口令 53/13/0。 **踩到的坑(都已写进代码注释)** - `QT_QPA_PLATFORM=offscreen` + QVTK = 进程直接 abort(`failed to get valid pixel format` / `GLEW could not be initialized`)。界面级脚本一律走原生窗口。 - 页面在 `build_view()` 里**同步**起装配线程 → `ui/_selfcheck.py` 建 3 个推演页时留下 3 个跑着的 QThread,进程退出时 `QThread: Destroyed while thread is still running` → abort(exit -1073740791)。 改为 `QTimer.singleShot(0, ...)` 起线程 + `atexit` 里 `wait()`。 ### v1.16 · 推演页飞的弹道改用参考记录弧,弹体沿弧逐帧走(2026-10-23) **起因**:用户口径「弹道要参考里真飞出来的那条,连位置一起用参考值」「开页就该有导弹车, 和 3 颗卫星一样不用点 Save」。前一版把 `DAODAN_1` 的**分级表**搬进了库、按推力重积分,实测 远地点约 11432 km —— 与参考逐帧输出的 946 km 对不上。取证脚本 `scripts/check_reference_ballistic.py` 证明这不是本仓库的 bug:参考记录的**速度列**与**位置列**在二体+登记表下互不自洽(按它自己的 地速推二体会得 ~4300 km,位置列却只有 946 km),说明它不是「二体+登记表」能积出来的弧。 结论落定:**登记的逐帧位置是权威,重积分只作交叉核对**。 **做了什么**(依赖方向照旧单向向下) - `core/ballistic/state_machine.py` 新增工厂 `trajectory_from_samples(samples, epoch_jd, burnout_s)`: 把参考自己飞出来的记录弧(键 `t_s/r_eci/v_eci/mass_kg`)包成 `Trajectory`,只贴分段标签 (boost/midcourse/reentry/impact)+ 补黑障位,**不插值、不外推、不重算**;落点由末点 ECI→ECEF→测地反解。 - `engine/scene.py`:`TargetSpec` 加 `arc_file`/`arc_sha256`;`scene_from_rows` 从分级表首级 `context.archive` 抽出弧引用(路径 + 现算 sha256 + 点数 + 出处)透传进九段式 `target.arc`, `build_target` 收进 spec。**大文件不入库**:库里只存引用,逐帧 330 点留在 `files/reference/` 里。 - `engine/runner.py`:`target_trajectory` 有 `arc_file` 时走记录弧、忽略 duration/step(弧是死的), 装载时把 sha256 对回来(不符→拒绝用可疑弹道,不静默回退);没登记弧的旧场景**回退按分级表重积分, 口径不变**。 - `scripts/seed_reference_missile.py`:把 `TGT-DAODAN-1` 实体写进 `T_TARGET`(发射 40°N/30°E 用参考值、 落点/时刻取参考逐帧末行),3 级推力分级表**结构干重显式登记**(Σ=3300 kg,非 10% 默认的 1590); 弧引用塞进 `BURN_TABLE_JSON[0].context.archive`(T_TARGET 无 `ARC_FILE` 列,不改 schema)。 `--demo`/`--dry-run` 全程离线;连库写需要 `SIMULATION_DM8_PASSWORD`。 - `ui/scenario_main/view.py`:开页只读装配即挂弹道图层(3D 折线+飞行体、2D 星下点), 弹体沿 `target_trajectory` 采样点逐帧走、发射前钳在车上,3D/2D 同刻同源;真实米级(车 8.2 m、 弹体登记 19.6 m 体轴线)。 - `scripts/check_ballistic_layer.py`:开页取证自检(读 actor 包围盒/顶点/tag,不点按钮),入 `verify_all`。 `verify_all.py` 支持带参数的脚本条目。 **实测**:`python -m core.ballistic.state_machine` / `engine.scene` / `engine.runner` / `ui.scenario_main.view` 全绿;用真实 `seed_reference_missile.build_target_row()` 喂进 `scene_from_rows→assemble→target_trajectory`,得 **330 点、远地点 946.25 km、起飞点 0.2 m(站在车上)、 飞行 1646.4 s**,弧文件 sha256 `da481b08…8155f3b1`(93,589 B)装载校验通过。`verify_all` 全量 **66 项:通过 53 / 跳过 13 / 失败 0**(跳过项全是缺达梦口令或缺可选依赖的实机检查,须在目标机器复跑)。 **本轮修掉的球/椭球基准错(关键)**:记录弧原先直接抄参考 ECEF 位置列(字段 11-13,立在 **WGS84 椭球面**),而本仓库从 `launch_state`、`_launcher_frames` 到重力/地球网格/`State.altitude` 全在 **R_EARTH 球面**(把纬度当地心纬度)。同一组 40°N/30°E 两基准差 22.8 km——于是导弹一开机就飘在 自己导弹车 **22,829 m** 之外、发射点测地高报成 **−8.8 km**。`flown_trajectory` 改成对每帧用它自己的 测地经纬高经 `geocentric_to_ecef` 取点(形状仍是参考逐帧值,只换基准面),起飞点与车残差降到 **2.2 m** (参考首帧自带 40.000017°N/30.000013°E/0.18 m,登记车取整到 40/30/0 的量化差,非基准错)。代价是落点 经 `ecef_to_geodetic` 报测地纬时带球心→椭球的固有 +0.096° 偏移:弧末点报 **15.0997°N**,而参考登记的 椭球落点 **15.0032°N** 原样存于 `targets.impact_ll` / `target_summary.impact_given`。这与纯二体重积分 路径同性质、全局自洽。取证门限随之校正:`check_ballistic_layer` 起飞前"弹在车上"从旧 `1e-6` 放宽到 `5 m`(离线核过 gap 恒 2.208 m、离站反证仍 320 km 量级,比失效情形紧五个数量级)。 **如实登记的缺口**:① 参考弹体模型只有**二进制** OpenFlight `daodan.IVE`(magic 0x04030201), 本机无 osgconv、`grep '^AC_3D'` 全树零命中 → **参考弹体在本机不可恢复**,与用户口径 #2「一定要参考 的模型」正面冲突;弹体暂按登记 19.6 m 画体轴线,`scripts/import_missile_model.py` 只在**另行拿到明文 `.glf`** 时才转得出 GLB(已修其 CLI 取参 off-by-one 并改正误导性 docstring)。② 发射时区源文件未标, 按 UTC 用(assumed);发射点用参考的 40°N/30°E 而非旧场景的 39.9°N/116.4°E。③ 本环境无 `SIMULATION_DM8_PASSWORD`,`check_ballistic_layer.py` 与所有连库脚本**跳过**,界面开页装配与逐帧动画的 真机取证须在有库的机器上复跑。 ### v1.15 · 场景里加一台导弹车,成像里真拍得到(2026-10-22) **起因**:用户要求「在 `D:\hy\卫星仿真\SpaceSim6.1\scenes` 里找到拥有导弹的例子,在仿真推演 界面中添加一个导弹车,并且在成像中体现它」。带导弹的参考场景是三条: `Demo_0501_Missile_GroundTarget`(实体 `DAODAN_1`:LLH 30°N/40°E/0 km,三级固体,真空推力 900/300/150 kN、工作时间 70/70/60 s、起飞 20 t、载荷 800 kg,弹体模型 `data/model/daodan.IVE`, 配套 `射程高度.txt / 时间速度.txt / 时间高度.txt / 时间俯仰角.txt`)、`Demo_0502_Missile_SpaceTarget`、 `Demo_0503_Rocket_Launch`。导弹车模型取同目录 `data/model/missileCar22.gltf`(30 MB 文本 GLTF, 内嵌 22.67 MB base64 缓冲),`scripts/import_missile_car.py` 只把数据 URI 解成 GLB 的 BIN chunk、 不碰几何,产出 `files/modles/missile_car.glb`:22,693,344 B, sha256 `592f8dbb762058c9372253efea291435d7ab8e8108db814a0d051f48d9c57897` (`files/*` 在 .gitignore 内,按约定不进版本库,需要时重跑导入脚本即可)。 **做了什么**(依赖方向照旧单向向下) 1. **数据**:`scripts/seed_missile_car.py` 往 `T_TARGET` 灌一条 `TARGET_TYPE='ground'` (`VEH-TEL-01`,与弹道目标同一发射点)。温度 345 K、等效辐射面积 20 m² 没有实测也没有文献 登记,所以 `SOURCE_TYPE='assumed'`、`SOURCE` 明写「无实测,产出的影像与统计不能用于任何 工程结论」;脚本幂等,`--purge` 只删自己那一行。 2. **装配**:`engine.runner.build_scene_from_db` 按 `TARGET_TYPE` 拆目标行 —— `ballistic` 给 弹道与观测点,`ground` 挂成 `target.launcher` 侧键(逐字段 value/source,拆分口径本身记进 `meta.provenance`,`source_type=derived`)。库里没有 ground 行时装配结果与拆分前逐字节相同。 3. **成像链**:`sequence_from_trajectory(ground_targets=)` / `render_frame(extra_sources=)` 把车当第二红外源与弹**同帧**解算,逐帧写真值标签 `ground`(视线夹角、距像面中心像元、 是否在幅宽内、折算 DN)。 4. **界面**:推演页把 GLB 立在发射点(3D + 2D 平面图标记与标签),成像页 Console 逐帧核对 「几帧落在幅宽内」并如实交代为什么。 **两个真缺陷是修在 core 的,不是在界面打补丁** 1. `core/coverage/access.earth_occludes` 原来用**赤道球**做弦测试:39.9°N 的站心点地心距比 赤道半径小 8.5 km,于是车每一帧都被判「在地球内部、视线被挡」。改成与 **WGS84 椭球**求交 (线段参数化取最近点判据),空间点与地面点从此**用同一个函数**,`isl_links` 也改成复用 它(原来自己抄了一份弦数学)。中纬台基的回归断言写进 `core/ir_imaging/chain._demo()`。 2. 弹道的发射点本来就是球面(`R_EARTH=6378.137` + 质点重力 + `|r|-R_EARTH` 高度),车若按 WGS84 椭球面放,两者在 39.9°N/116.4°E 实测相距 **22.80 km**(水平 21.0 + 径向 8.8; 球面点的测地纬度是 40.0892°、测地高 8.8249 km)。起飞时刻弹与车视线夹角 2.51° = 3506 像元, 而跨轨幅宽只有 ±0.229° ⇒「弹从车上起飞」这件事在像面上压根体现不出来。新增 `core.time_ref.state.geocentric_to_ecef` 作为唯一取点约定,`launch_state`、成像页、推演页 三处共用,`state_machine.surface_point_ecef` 这个中间产物已删。 **实测:车在像面上是一个分得开的热点**(真库、场景驱动,观测星 SAT-IMG-01) | 抽帧间隔 | 序列 | 车在幅宽内 | 那几帧的样子 | |---|---|---|---| | 10 s(页面默认) | 101 帧 / 有效 5 | **1 帧** | t=0,夹角 0.0000°、偏移 0 像元、DN 16383(与弹重合) | | 1 s | 41 帧 / 有效 41 | **2 帧** | t=0 重合;t=1 s 偏移 213 像元 = 0.1523°,是**独立**一个点 | `scripts/check_imaging_launcher.py`(只读真库)钉住三件事:起飞帧弹车必须同一条视线 (< 1e-6 像元,取点约定一分家立刻断);必须有一帧分开 > 20 像元且仍在幅宽内;那一帧车所在 像元必须比**同种子、不给地面源**的对照帧更亮。实测 t=1 s 帧的像元 (256,533) DN 4742 → 7696(中位背景 4740),量化前辐亮度 +0.3104 W/m²/sr,`extra_sources_dropped == 0`。 **结论是几何,不是渲染 bug**:相机跟的是导弹,导弹一起飞,站在发射点的车就掉出 IFOV×列数 = 12.5 μrad × 640 = 0.458° 的跨轨幅宽(半幅宽 ±0.229°,700 km 处约 ±2.8 km), 车只在点火后头 1~2 s 内还在像面上 —— 要在图上把它当独立目标看清,成像页抽帧间隔得设 1 s。 这段说明由 `ground_frames_note()` 从 core 回传的标签现算(窗口长度实测多少写多少, 之前写死的「几十秒」比实测差两个数量级,已改成数据驱动)。 **3D 的「球转车不转」**:世界系是 ECI,地球 actor 在 `Scene.update()` 里按 GMST 转,而车原来 只在挂载时按 ECEF 摆一次 ⇒ 半小时飘到别的经度,还与自己发射的弹分家。新增 `_launcher_frames()`(位置逐帧旋到 ECI、姿态吃同一个旋转、速度取 ω×r), `Scene._jd` 公开为 `Scene.jd` 让新挂的 actor 能按当前帧补摆。 `scripts/check_car_rotation.py` 离屏取证:t+0/6/12 天反旋回 ECEF 的落点恒为 39.9192°N/116.8216°E(偏差 45.5 km = 放大后车体几何中心与站心的差,容差 200 km), ECI 里 6 h 位移 507 km,车与地球同向转 11.83°。 **顺着截图又挖出一个更早就存在的符号错**:修完上面那条之后截图里车仍然落在**北大西洋**。 根因不在车,在 `viz3d/earth.earth_orientation_deg` —— 地球网格、UV、经纬网全在 **ECEF** 里生成, 画进 ECI 世界必须乘 `ecef_to_eci = Rz(+GMST)`,而 VTK 的 `SetOrientation(0,0,θ)` 正是 Rz(θ), 所以 θ 只能是 **+GMST**;原实现写的是 −GMST ⇒ 整张贴图相对真实自转**反向**转了 2·GMST (GMST=101.08° 时偏到 41°W,与截图逐度对上)。这不是新代码引入的,是三维层一直就有、 之前没有任何东西钉在地球表面上所以看不出来。改正后 `viz3d/earth`、`viz3d.scene` 两处 `_demo()` 的断言从"锁死错符号"翻成锁 +GMST,并用 `ecef_to_eci` 交叉核对 ECEF +X 的朝向; `check_car_rotation.py` 的第三条断言也改回车与地球朝向角**直接相等**(原来因为符号相反, 比的是互补角,等于把 bug 写进了测试)。截图 `docs/shots/car-3d.png` 现在车站在东亚。 **坑(都落进注释)**:车与卫星共用 `_MODEL_SPAN_KM = 1600` 的观感放大口径。GLB 实测 8.19×2.32×2.25 m,最长轴是车体纵向(`_launcher_rotation` 把 GLB +Z 映到 −北、+Y 映到天顶), 放大后在默认机位(距 50,090 km、视场 30°、400 px 高、1 px≈67 km)占 23.8×6.8 px,与卫星 模型同量级;竖向 ±219 km 是"贴地标记"的代价,画面说明里明写非真实比例。库里一旦有 GEO, 卫星按 `max(_MODEL_SPAN_KM, 0.05×轨道半径)` 涨到 ~2100 km,车仍按 1600 km —— 两者都不是真尺寸。 **验证**:`core.coverage.access` / `core.ir_imaging.chain` / `core.time_ref.state` / `core.ballistic.state_machine` / `ui.scenario_main.view` 五个模块自检 + `scripts/check_imaging_launcher.py`(只读真库)+ `scripts/check_car_rotation.py`(离屏 vtk) 全绿。`verify_all` 现 **63 项**,带达梦口令实跑 **58 通过 / 3 跳过 / 2 失败**:两个失败项 `check_gui_imaging_sat.py`、`check_gui_sim_imaging.py` 是 **v1.12 时代的界面级检查**,前提 ("下拉选颗星就该出图")在 v1.14 场景驱动改造后就不成立了;已在 HEAD(0518ab5)的干净工作树 里复跑,同样失败 ⇒ **不是本轮引入**,无口令时它们历来"跳过",所以过去那句"0 失败"从来不含它们。 已记进缺口清单,改写口径见下面「还没做」。 **还没做 / 要用户定** 1. **改写那两个过时检查**:要么先落场景+跑次再断言有效帧,要么改成当前契约(下拉能选到库里的星、 横幅点名观测来源)—— 后者已由 `check_gui_scene_run.py` 覆盖(它因 `scn_sim_main` 已存在而跳过, 见第 3 条)。 2. `T_MODEL` 里没有导弹车与卫星 GLB 的登记行(`files/*` 也不进版本库)。第四节约定大文件应 「路径 + sha256 + 大小 + 创建者」入库,这轮没写 —— 超出既往写库授权,要补请说一声。 3. `scripts/seed_missile_car.py` 写了 `T_TARGET`(ID=2,SCENE_ID=33,`VEH-TEL-01`),这张表 不在上一轮「只允许 T_SCENE / T_SCENE_PARAM / T_SIM_RUN / T_OBS_SEGMENT」的授权里。 要留着就什么都不用做,要撤掉跑 `python scripts/seed_missile_car.py --purge`。 4. `scn_sim_main`(`T_SCENE` ID=77,CREATOR=admin)留在库里,`check_gui_scene_run.py` 因此 按设计跳过(脚本不覆盖用户存过的场景)。删不删由用户定,本轮没动它。 5. 车仍然不进调度 —— 仰角判据算的是固定观测点而不是飞行目标(缺口 #10),所以 955/1001 帧 仍是 E03 负样本。 ### v1.14 · 九段式落库 → engine 真推演 → 场景驱动成像(2026-10-22) **起因**:用户问「成像模拟页点『生成影像序列』,出的是仿真推演里那 3 颗卫星生成的吗」。 v1.12 把观测几何接上了轨道,但**两边看的不是同一份场景**:推演页的「保存」还是 `TODO(persistence)` 空桩,成像页自己 `_build_demo_trajectory` 造一条弹道、载荷取库里 第一行。所以那句话当时仍不成立。本轮写库授权只限四张表 (`T_SCENE` / `T_SCENE_PARAM` / `T_SIM_RUN` / `T_OBS_SEGMENT`)。 **做了什么** 1. **装配**:`engine/scene.scene_from_rows` 读库拼九段式,81 对 `value/source` 溯源 (assumed 70 / derived 11),说明逐条进 `meta.provenance`。 2. **落库**:推演页「保存」→ `T_SCENE` + `T_SCENE_PARAM`(场景 JSON 逐字节回读一致, 版本号递增 + sha256);「运行推演」→ `engine.run_scenario` 真跑 86400 s @ 10 s (8641 帧)→ `T_SIM_RUN` + 5 条 `T_OBS_SEGMENT`。指标卡、时间轴、断档说明全部读真结果。 3. **通道**:`ui/registry.py` 加 `SCENE` / `RUN` 与 `clear_scene_run()`,页面间只经这里汇聚。 4. **成像**:删掉 `_build_demo_trajectory`,载荷 / 弹道 / 温度曲线 / 观测星全部来自那份场景 与那次跑次;三条取数路径(联动 / 未联动只读库自装配 / 都不行只报原因)在来源横幅与日志里 点名,**绝不退回自造弹道**。整段渲染放后台线程 —— 同步跑要 5 s,会把窗口冻住。 5. **发射时刻**:`build_scene_from_db` 先推星历、编排观测计划,再把发射时刻对齐到 第一条观测弧段的**中点**(原来取起点)。 **跑出来的结果(真库实测)** | 项 | 值 | |---|---| | 观测覆盖率 / 最长断档 | 0.2079 / 24433 s(6.79 h) | | 交班成功率 | 0.25(判定 4 次、过 1 次) | | 双星以上同时可见 | 0.0117 | | 弧段 / 断档 | 5 条 / 7 处(原因码全 E01,即临边遮挡) | | 成像序列(1000 s @ 10 s 抽帧) | 101 帧,**有效 5** / 负样本 96(E03) | **96 帧 E03 不是丢帧,是几何**:弧段的仰角判据算的是**固定观测点**(场景 `target.launch` 的经纬度),不是飞行中的目标。实测 SAT-IMG-01 对发射点峰值仰角 77.4° (t=2000 s),但弹道起飞 200 s 就到 655 km 高度、东经 119.9°,该星需要的侧摆角变成 91° (平台限位 30°)→ 判 E03。发射时刻对齐弧段起点时**一帧都拍不到**(那时刻卫星刚出地平, 仰角 4.7° ⇒ 需侧摆 67.7°),改中点后起飞前 40 s 的 5 帧是真几何像。成像页日志把这段 原因如实打出来,不假装是链路丢帧。 **踩到的坑**(都落进代码注释) 1. **库字符集是 GBK**(`SF_GET_UNICODE_FLAG()=0`):场景说明里一个 U+2212「−」就让 dmPython 在绑定阶段抛错,整份场景存不进去。改成文案只用 GBK 装得下的字符并加 `_gbk_bad` 自检;**不改写副本再落库** —— 改写等于改数据血统,宁可失败。 2. **`T_AUDIT_LOG` 的 `CK_T_AUDIT_LOG_ACTION`** 只允许 create/update/delete/rollback/run/ export/login/grant。`audit_log(..., "save_scene", ...)` 在场景已提交之后才抛约束错误 = 半截写入 + 界面静默失败,已删(也超出本轮写库授权)。 3. **`FK_T_SIM_RUN_SCENE`**:清理顺序必须 弧段 → 跑次 → 参数 → 场景。 4. **`T_OBS_SEGMENT.SAT_ID` NOT NULL**:末尾断档没有归属卫星,存不进去,界面如实标注。 5. **`plan_metrics` 的交班成功率原本恒为 0**:弧段首尾严格相接(重叠恒等于 0),而 `_add_gap(prev_end, next_start)` 的区间被裁成长度 0 压根进不了 gaps(E02~E04 结构性 记不上)。重算必须用**可见窗口**而不是弧段。 6. 界面级取证:`QTreeWidget` 里没有 `QLabel`;无交互桌面时 VTK 的 `wglMakeCurrent` 会刷 几百行 GL 报错埋掉真失败原因,`vtkLogger.SetStderrVerbosity(VERBOSITY_OFF)` 关掉 (9.3.1 没有 `VERBOSITY_QUIET`)。 **验证**:`scripts/check_scene_run.py`(真库端到端:装配 → 溯源体检 → 推演 → 写库回读 → 清理) 与 `scripts/check_gui_scene_run.py`(真窗口点「保存」「运行推演」「生成影像序列」,断言场景 回读一致、入库行数 == 弧段数、**有效帧 > 0**、断档时刻如实改判)。 `verify_all` 现 61 项 → 无口令 50 通过 / 11 跳过 / 0 失败。 截图 [`docs/shots/scene-run-persist.png`](docs/shots/scene-run-persist.png)、 [`scene-run-imaging.png`](docs/shots/scene-run-imaging.png)。 **还没做**:要"跟着导弹拍",得给 `core/scheduling` 加**按弹道目标排观测**的口径 (现在只有按固定覆盖对象排),已记进 `.workbuddy/context/PROJECT-STATE.md` 缺口 #10。 ### v1.13 · 补上点目标的辐射链:面积、1/R²、大气(2026-10-21) **起因**:v1.12 之后几何是卫星的了,但辐射还不是 —— 实测 SAT_2(7021 km)与 SAT_3(35779 km)拍到的同一个目标**中段都是 6202 DN**,距离差 5 倍亮度却一样。 根因:`target_radiance` 只由温度算出来,既没有目标面积也没有 1/R²。 **做了什么** 1. **点目标辐射链**(`render_frame` 新增 `target_area_m2` / `target_range_km`): ``` I = L_target × A_target # 辐射强度 W/sr E = I / R² # 入瞳辐照度 W/m² L_eq = E / Ω_pixel # 折成像元等效辐亮度,Ω_pixel = IFOV² radiance_map[row, col] += L_eq # **叠加**在背景上,不是替换 ``` 两个参数必须成对给,缺一个直接报错。不给则沿用旧语义(目标充满视场)。 **从"替换"改成"叠加"**是这一步的关键:点目标在地球背景上,替换会把背景整个抹掉。 2. **大气**:新增 `core/environment/atmosphere_radiation.py`,参数化模型 (`τ = exp(-k·airmass)`,程辐射按 260 K 灰体估)。按波段取 k: 2.7~2.9 μm 最暗(τ₀≈0.20)、3~5 μm 窗口(0.70)、8~12 μm 最透(0.86)。 **不是 MODTRAN**,只保证量级与波段间相对关系正确。 3. **目标投影面积**:`T_TARGET.TEMP_CURVE_JSON` 加 `ref_area_m2 = 0.8`(弹头量级, **assumed**,助推段尾焰未单独建模)。 4. **新增预警波段载荷** `PLD-WARN-01`(2.7~2.9 μm,挂在 SAT-IMG-03 / SAT_3 上), 成像页动作栏加「载荷」下拉 —— 波段决定看不看得见目标,不能隐式取第一条。 **跑出来的结果(同一帧、同一颗星,只换波段)** | 载荷 | 波段 | 助推段对比度 | 说明 | |---|---|---|---| | PLD-IMG-01 | 3~5 μm | **+2%** | 目标淹没在 280 K 地球背景里,几乎看不见 | | PLD-WARN-01 | 2.7~2.9 μm | **+154%** | 背景辐亮度只有 1/570,目标是个清晰亮点 | **这就是预警卫星为什么用 2.7~2.9 μm 的实证**:不是信号强,是背景几乎为零 (280 K 在该段只有 0.0015 W·m⁻²·sr⁻¹,比 3~5 μm 的 0.8605 低三个数量级)。 界面日志现在会打印 **目标对比度**(峰值/背景中位 − 1);低于 10% 时直接提示换波段, 不让人对着灰屏猜"是不是没拍到"。 **修掉的显示问题**(改完辐射链后暴露) 1. **2.7~2.9 μm 帧又变雪花**:该波段背景辐亮度掉三个数量级,满量程跟着变小, 同样的 DN 噪声在窗宽里占比剧增。窗宽下限从"8% 满量程"改成 `max(15% 满量程, 32σ)` —— 15% 是实测折中,32σ 是总宽(每边 16σ), 噪声 ±3σ 只占 9%。 2. **σ 不能用 MAD 估**:在"一半云一半地表"这种真有结构的帧上,`|arr−median|` 的 中位数被结构本身主导(可达数千 DN),窗宽被撑到十几万,云和地表一起压平。 改用**相邻像素差分**的中位数 —— 低频结构被差掉,剩下的才是噪声。 **验证**:57 项自检 → **通过 55 / 跳过 2 / 失败 0**。 对比图 [`docs/shots/ir-band-swir.png`](docs/shots/ir-band-swir.png)(2.7~2.9 μm, 噪声海里一个亮点)与 [`ir-band-mwir.png`](docs/shots/ir-band-mwir.png)(3~5 μm,几乎纯灰)。 ### v1.12 · 成像几何接上真实卫星轨道(2026-10-21) **起因**:用户问"现在的成像是根据那 3 颗卫星来的吗"。查下来答案是否 —— 数据库里 `T_PAYLOAD → T_SATELLITE → T_SATELLITE_LIB` 这条链只是外键,成像链根本没读轨道: 观测高度写死 600 km,离轴角恒 0,弹道是独立算的。**这不叫"这颗星拍的"。** **做了什么** 1. **`sequence_from_trajectory` 加 `observer_at`**(卫星状态函数)。给了它之后逐帧: - 观测高度 = `|r_sat| − R_EARTH`(用卫星真实轨道,不再是 600 km) - **需要的侧摆角** = 卫星→目标视线与星下点法线的夹角 (`core.coverage.access.off_nadir_deg`,它本来就注明"即需要的侧摆角") - **地球遮挡** = `earth_occludes`(视线是否被地球本体挡住) - 侧摆超限(>30°,方案里的 `SIDESWAY_LIMIT_DEG`)或视线被挡 → 该帧断档 E03 - `labels` 里带 `off_nadir_deg` / `sat_altitude_km` / `blocked` / `sidesway_ok` 2. **`_build_demo_trajectory` 支持指定发射点**:旧代码把发射点钉在 ECI 的 x 轴, 弹道落在西半球 100.7°W,而 GEO 星的星下点在 147.6°E —— 相隔 111.7°, **目标一直在地球背面**,全序列断档。现在发射点取观测星的星下点, 速度方向在该点的本地 ENU 里构造(朝东 + 抬升角),再转回 ECI。 3. **成像页动作栏加「观测卫星」下拉**:选哪颗星,就用它的轨道算几何。 **跑出来的真实结论**(不是 bug) | 观测星 | 高度 | 需要的侧摆角 | 有效帧 | |---|---|---|---| | SAT_1 | 493 km | **55~75°** | **0 / 101** | | SAT_2 | 7021~11143 km | 0~8.4° | 98 / 101 | | SAT_3 | 35779 km | 0~4.4° | 99 / 101 | **低轨卫星看比自己高的弹道目标(顶点 704 km),需要 55° 以上的侧摆,超出平台 30° 能力 —— 一帧都拍不到。** 这不是判据写错,是几何事实:目标在卫星上方。真实系统里 STSS 这类低轨跟踪星要靠专门设计才能看中段。 界面在全断档时会直接说明这一点并提示换星,不让用户对着空画面猜。 **踩过的坑** 1. **遮挡判据用错场景**:`earth_occludes` 是给"两个空间点之间的视线"设计的, 而"从卫星看地面点"必须用仰角判据(函数 docstring 里就写了)。 不过这次真正的错误不在判据 —— 是我一开始把发射点放在卫星背面, 判成遮挡是**对的**,只是看着像 bug。 2. **`off_nadir_deg` 给出 7.58° 而地心夹角 111.7°** 看着矛盾,其实是: 视线从 GEO 出发**穿过地球**打到另一侧的地表点,几何上离轴角可以很小, 但目标在球背面 —— 所以离轴角合法**不代表能拍到**,必须再加遮挡判据。 **验证**:57 项自检 → **通过 55 / 跳过 2 / 失败 0**。 新增 `scripts/check_gui_imaging_sat.py`:选 SAT_3 断言 99/101 有效、高度是 GEO 量级、 0 帧遮挡;选 SAT_1 断言 0/101 有效且日志给出解释。 ### v1.11 · 宽幅对地红外成像:背景有海陆和云(2026-10-21) **起因**:成像页的画面是一块均匀灰 + 一个白点。调研(`docs/infrared-imaging-reality.md`) 查明真实红外图不是这样 —— 背景由海陆、纬度、昼夜、云叠出来。用户要"对地视角、 有地物结构"的成像。 **先说一个绕不过去的物理约束**:当前载荷 `IFOV=12.5 μrad × 640 像素`,600 km 高度下 **视场只有 4.8 km**。这个尺度上地表本来就近乎均匀,装不下大陆轮廓。所以新增的是 **另一种成像模式**(宽幅 2000 km),不是把原相机改大 —— 点目标模式(看导弹)原样保留。 **做了什么** 1. 新增 **`core/ir_imaging/background.py`** —— 背景场: - **海陆**:从 `files/textures/world_equirect.jpg` 按颜色判读派生,是**真实地理**; - **地表温度**:`300 − 50·(|lat|/90)^1.5` 纬度基线 + 海陆昼夜振幅(陆地 ±12 K、 海洋 ±1.5 K,体现热惯性差异)—— **模型值**,不是实测亮温; - **云**:多层分形噪声,云顶固定 245 K(对流层顶典型值)—— 同样是模型。 2. 新增 **`core/ir_imaging/display.py`** —— DN → 灰度的唯一实现,脚本与界面共用。 3. `render_frame` 加 `background_map` 参数:逐像素背景,不再只能是常数。 4. 成像页动作栏加 **「宽幅对地」** 按钮:以卫星星下点为中心、2000×1600 km 幅宽, 满量程按背景 99% 分位定(地物占满量程主体),对地成像不叠目标。 **修掉的四个坑** 1. **地图上的地名被判成陆地**:Blue Marble 印着 "CHINA"、"EAST CHINA SEA" 等白字, 三通道都高,按"蓝不占优"全被判成陆地 —— 画面正中直接冒出一排方块字形的"大陆"。 5×5 中值去不掉(笔画 6~8 px),**9×9 才吃干净**。 2. **中值滤波在原图 3000×1500 上做要 6 秒**:先缩到 1200×600 再滤,只要 0.75 秒, 效果一样(缩小后笔画也变细)。 3. **显示映射是单向的**:原来"背景中位数固定 38%、最亮点到白",于是**所有比背景暗的 结构被压成同一个灰** —— 云顶比地表冷,整片云直接消失成均匀色块。改成双向: 中位数 38%,向下展开到 1% 分位、向上到 99.8% 分位。 4. **`target_radiance=0` 会在背景里戳一个纯黑像素**:`render_frame` 无条件把目标写进 像元,0 就成了黑洞。改成 `> 0` 才写,0 表示"本帧不叠目标"。 5. **显示映射把纯背景帧放大成雪花屏**(改完第 3 条之后暴露的回归):点目标模式的背景 是常数,帧内动态范围几乎只有噪声那么宽;按分位自适应时上下界就变成"背景 ± 噪声", 噪声被拉到接近满灰度 —— **实测灰度标准差 72,整屏雪花**。修法:给窗宽加下限 (`MIN_WINDOW_FRACTION = 8%` 满量程),帧内跨度小于它时按最小窗宽居中显示。 修后同一帧标准差 5.8,是"均匀灰 + 细颗粒 + 一个白点",与真实点目标图像一致。 **诚实边界**(日志里也写了) - 海陆轮廓是**真实地理**,但精度约 0.3°(地图 3000×1500,18° 视场里每格约 4 像素), 海岸线会有可见的像素化; - 地表温度、云量、云顶温度都是**模型值(assumed)**,不是实测亮温。 要实测背景必须接 MODIS/FY-4 的亮温产品 —— 那是另一条数据链,没有数据就不编造。 **验证**:56 项自检 → **通过 54 / 跳过 2 / 失败 0**。 效果见 [`docs/shots/ir-wide-cloud.png`](docs/shots/ir-wide-cloud.png)(黄海,含云)、 [`ir-wide-gui.png`](docs/shots/ir-wide-gui.png)(界面画布)。 ### v1.10 · 轨道数据换成 SpaceSim6.1 场景的三条(2026-10-21) **目标**:把卫星库里的示例轨道(`DEMO-SAT-01/02/03`,SSO 500/600/700 km)换成 `D:\hy\卫星仿真\SpaceSim6.1\scenes\Demo_0101_Orbit_Basic` 里的三条。 **源场景分析**(`Demo_0101_Orbit_Basic.json` → `Scenario.SAT`) | 代号 | a (km) | e | i (°) | Ω | ω | M | 近/远地点高度 | 周期 | 颜色 | |---|---|---|---|---|---|---|---|---|---| | SAT_1 | 6871.30 | 0 | 45 | 0 | 0 | -10 | 493.2 / 493.2 | 94.5 min | #00ff00 | | SAT_2 | 16371.0 | 0.2 | 20 | 0 | 0 | -20 | 6718.7 / 13267.1 | 347.4 min | #ff0000 | | SAT_3 | 42157.0 | 0 | 0 | 0 | 0 | 0 | 35778.9 / 35778.9 | 1435.7 min | #ffff00 | 历元取 `begin_datetime` = **2022-07-24 04:00:00 UTC**(不是 `fangzhen_datetime`, 那是界面当前时刻,不是根的历元)。 **场景文件里其实有 4 条 SAT**:第 4 条 `SAT_4`(a=42166.3、e=0、i=20°、青色)是倾斜 GEO 备份星。但 6 个 Output 文件(2023-04-23 → 2026-10-21)的表头**全都只有 SAT_1/2/3**,SAT_4 从未参与输出 —— 与用户说的"3 条"一致,故只导入前三条。 要补第 4 条:`--only SAT_4` 再跑一次即可(不用 `--reset`,不会动现有三条)。 **做了什么** 1. 新增 **`scripts/import_spacesim_scene.py`** —— 从场景 JSON 直接读参数,不手工转抄: 六根数、历元、质量、颜色都从源文件取,`T_SATELLITE_PARAM` 的 source 精确到 `SAT[SAT_1].a` 这种键。支持 `--dry-run / --only / --replace / --reset`。 2. 写入 3 条(id 30/31/32,每条 9 行溯源)。`T_SATELLITE` 里 3 条成像卫星 (`SAT-IMG-01/02/03`)的 `LIB_ID` 按顺序指回新星(18/19/20 → 30/31/32)。 3. **相机按最外层轨道自适应**(距离 = 最大轨道半径 × 4.2):写死的 `(0, -26000, 14000)` 只够看 LEO,换成含 GEO 的场景后 42157 km 的轨道整个落在 视野外(实测一屏只剩一条 LEO 轨道,看着像"没导入成功")。焦点仍在**地心**, 不能用 `reset_camera()`:它按所有 actor 的包围盒中心取景,轨道不对称会把地心挤出画面中心。 4. **模型尺度跟着最外层轨道走**:`span = max(1600 km, 最大轨道半径 × 5%)`。GEO 场景里 1600 km 的模型在十几万 km 视距下只有 2×1 像素,等于看不见(实测藏起模型前后只差 20 个像素)。 **两处与源场景的已知差异**(都写进了日志与溯源,不静默) 1. **递推模型 HPOP → j2**:源场景用全摄动,但它的 `hpop_parameter` 是全局开关, 没有逐星高阶模型参数,凑不出可信的 HPOP 初值。J2 长期项在 LEO 一天内差数十公里, 画轨道线足够;精确定轨要另配参数。 2. **近/远地点高度由 a、e 派生**,不抄源文件字段:源文件里 `apogee/perigee` 不可信(SAT_1 的 a=6871 km 而 apogee 写 35786 km,是 GEO 默认值残留), 但 SAT_4 那个字段反而是对的 —— 同一文件里两种含义,只能统一按根数算。 **修掉的两个缺陷** 1. **`dao.execute()` 不能跑 DML**。它把结果物化成列表,而 DML 的 CursorResult 不返回行, 迭代直接抛 `ResourceClosedError: This result object does not return rows`。 受影响的有三处:`seed_satellites.py --purge`、本版导入脚本的删除、`seed_imaging._purge`。 新增 **`dao.execute_dml()`** 返回受影响行数,三处全部改过来。 2. **`seed_imaging._purge()` 写死 `CODE LIKE 'DEMO-SAT-%'`**:卫星库换轨后一条都找不到, 清理会漏掉。改成按 `T_SATELLITE.CODE LIKE 'SAT-IMG-%'` 反查它们引用的 LIB 行 —— 判断依据从"卫星库叫什么"变成"我灌的成像卫星引用了谁",换轨后仍然成立。 **验证**:52 项自检 → **通过 50 / 跳过 2 / 失败 0**。 效果见 [`docs/shots/sim-satellite-start.png`](docs/shots/sim-satellite-start.png) (三条轨道 + 地球)、[`sim-2d-follow.png`](docs/shots/sim-2d-follow.png)(星下点: SAT_1 是 45° 倾角的正弦波、SAT_2 小振幅摆动、SAT_3 作为 GEO 几乎静止)。 **注意**:LEO 与 GEO 同屏必然"地球很小、模型很小"(相差 6.6 倍),要看细节用鼠标滚轮 放大;卫星库 CODE 已从 `DEMO-SAT-*` 变为 `SAT_1/2/3`,按旧代号写的脚本/文档需同步。 ### v1.9 · 开始仿真后卫星沿轨道飞(2026-10-21) **目标**:点「▶ 开始仿真」后,3D 画布加载 `files/modles/satellite.glb` (目录名就是 `modles`,不是笔误),并让模型沿卫星库算出的轨道持续移动。 **做了什么** 1. **`core/orbits/ephemeris.py` 新增 `state_function(params)`** —— 参数集 → 连续状态函数 `jd → State`。与画轨道线用的 `sample_states` 同一个历元状态、 同一个递推器,所以模型不会跑离轨道线。`hpop` 档明确报错(逐帧要从历元重新 数值积分,越跑越慢),不静默换成 j2。 2. **`viz3d/gltf.py`**:新增 `model_bounds()`(模型包围盒/几何中心)、 `rotation_matrix()`(四元数或 3×3 矩阵都能收)、`velocity_frame_matrix()` (+X 沿速度、+Z 对地的飞行姿态);`place_model()` 加 `pivot` 参数。 3. **`viz3d/scene.py`**:`track()` 支持 `pivot` 与**可调用姿态**(`fn(state) → 姿态`), 卫星沿轨道飞时姿态跟着转。 4. **`ui/scenario_main/view.py`**:`_mount_satellite_models()` 把模型挂到卫星库里 每颗在轨卫星上(同一 GLB 只读一次,其余 `ShallowCopy` 复用几何); 开始/暂停/停止/加速/减速全部接线,QTimer 30 fps 按「墙钟 × 倍速」推进。 默认 256×,一圈 5677 s 约 22 秒走完。 5. 新增 **`scripts/check_satellite_anim.py`**(离屏跑完整链路 + 出三张图)与 **`scripts/check_gui_sim_start.py`**(真窗口点按钮,验证挂模型/推进/暂停/停止/重播)。 **本版修掉的五个真缺陷** 1. **`load_gltf` 一调用就 AttributeError,GLB 从来没被成功加载过**。 `vtkGLTFImporter` 继承 `vtkImporter`,**没有 `CanReadFile()`** (那是 `vtkGLTFReader` 的接口),VTK 9.3 上必抛。改为按扩展名判断, 内容坏了由"没有 actor"兜底。 2. **`place_model` 的 `SetUserMatrix` 会把模型炸开**。importer 靠给**每个部件** 设 `UserMatrix`(本项目是 0.1 倍缩放 + 平移)来装配模型,再覆盖一次, 每个部件当场放大 10 倍、位置全错(渲染图里太阳翼只剩一片)。 修法:加载时把部件矩阵**烘进 polydata**(`vtkTransformPolyDataFilter`), actor 的 UserMatrix 就空出来专供摆放。 3. **借用场景渲染窗口时,把地球和轨道线当成"模型部件"收走了**。 importer 会复用已有的 renderer,`GetRenderer().GetActors()` 数的是**整个场景**, 于是 15 个 actor 全被当成模板克隆给每颗星 —— 画面上凭空出现 3 个缩小的地球, 而真正的场景被移出了渲染器。修法:导入前按 C++ 地址记下原有 actor,只认新增的。 4. **模型满载后 3D 视口只剩底色**(前两次改动的中间态):先误移除了调用方的 renderer,后又在自有窗口与借用窗口之间反复切换 GL 上下文。最终定案: 导入必须借用 `scene.render_window`(纹理也建在同一个上下文里), 导入后只摘掉 importer 的新增渲染器、绝不动调用方的那个。 5. **点「开始仿真」会弹一个 `vtkOutputWindow` 原生窗口**。VTK 在 Windows 上 默认用 `vtkWin32OutputWindow`:任何一条警告都会让它建窗。加载 GLB 时 `vtkGLTFDocumentLoader` 报两条 `KHR_materials_specular` / `KHR_materials_ior` 扩展不支持的警告,那个框就弹出来了。 `ui/app.py` 新增 `quiet_vtk_output()`(`build_main_window()` 首行调用), 把输出窗口换成 `vtkStringOutputWindow` —— 只攒字符串不建窗;控制台照旧能 看到这些警告(vtkLogger 自己写 stderr,与这个出口是两路)。 **踩过的坑(记录以免重复排查)** - `actor.GetBounds()` 在 GLTF importer 产出的 actor 上**不可信**(实测给 4.57×1.07×0.66,真几何 8.62×2×2)。烘焙之后才可以直接读。 - 卫星模型在 6400 km 地球上必须**放大到量级 1600 km** 才看得出在动 (`_MODEL_SPAN_KM`,纯观感,非真实尺寸)。LEO 轨道只比地表高 11%, 卫星几乎总贴着地球轮廓,太小就完全看不见,且大部分时间被地球挡住。 - 点击后 `window.grab()` 截出来视口是黑的属正常(Qt 抓不到 OpenGL 层, 与 v1.8 记录一致);验证一律从 VTK 窗口直接读像素。 **验证**:52 项自检 → **通过 50 / 跳过 2 / 失败 0**(跳过项与 v1.8 相同)。 效果见图 [`docs/shots/sim-satellite-start.png`](docs/shots/sim-satellite-start.png) (全景)、[`sim-satellite-closeup.png`](docs/shots/sim-satellite-closeup.png)(模型特写)。 **续:2D 星下点跟随 3D(同日)** 3D 里卫星在跑,2D 平面图上的星下点却停在历元位置 —— 两个画布各说各话。补齐这一段: 1. **`viz3d/map2d.py` 新增 `place_on_map()`**:图元几何建在原点,位置由 `SetPosition` 逐帧给。逐帧重建 `vtkRegularPolygonSource` 太贵,摆一个已存在的 actor 只是改一次变换矩阵。`marker_actor` / `label_actor` 改为"建在原点 + 摆位", 对外行为不变(建即在指定位置)。 2. **`ui/scenario_main/view.py`** 拆成两层:`_star_latlon(followers, jd)` 只算星下点 (纯计算、不碰 VTK,测试可直接调),`_update_2d(jd)` 负责摆图元。 位置取自与 3D 模型**同一个** `state_function`,所以两个画布同刻同源。 3. 接线三处:每帧 `_tick`、`start_sim`(首次定位到历元)、`stop_sim`(回起点)。 另外把原来 TODO 的**进度条拖动**接上了(`sliderMoved` → `seek_to_slider`), 拖动时 3D 与 2D 一起连续跟手;用 `sliderMoved` 而不是 `valueChanged`, 后者连程序每帧 `setValue` 都会触发,会和 `_tick` 互相打架。 4. **标记太小的问题一并修了**:原来圆点半径 `size/10 = 0.9°`,在 640px 宽的地图上 只有 1~2 像素,看起来像"没在动"。改成 `MARKER_SIZE=24`(半径 2.4°≈4px)并把 标签避让从 1.5° 拉到 `LABEL_OFFSET=(4,3)°`,否则文字直接压住圆点。 边界情况(都写进了断言):2D 视口没挂上 → 整体跳过不影响 3D;单颗星算失败 → 只跳过它自己且同一颗星只报一次,不刷屏;极区纬度做钳位(状态推算会带 1e-10 级越界); 跨 ±180° 经线时星下点瞬移到地图另一侧属等经纬投影的固有性质(轨迹线已按 `split_antimeridian` 断开),不是算错。 验证:`check_satellite_anim.py` 新增**逐点对齐**断言(marker 平面坐标 == 同一时刻 星下点,容差 1e-9)+ 扫一整圈检查极区/跨经线不越界不抛;`check_gui_sim_start.py` 新增"点击后 2D 有 6 个图元位置更新""拖到 50% 停在 43200 s 且 2D 跟着定位" "2D 视口读出 2 万+ 种颜色"。效果见 [`docs/shots/sim-2d-follow.png`](docs/shots/sim-2d-follow.png)。 ### v1.8 · 装 VTK 点亮三维视口(2026-10-16) **目标**:装 VTK,把「仿真推演」页的 3D/2D 视口从空容器变成真实渲染。 **做了什么** 1. 项目 `.venv` 装 `vtk==9.3.1`(连带 matplotlib,VTK 的依赖) 2. 新增 **`ui/vtk_view.py`** —— 界面层与 viz3d 之间**唯一**的接触面: 界面侧拿 `VtkView` 当普通 QWidget 用,viz3d 侧不知道被谁嵌着。 三级降级:无 vtk → 中文提示占位;有 vtk → 挂载渲染;两者都有 → 可交互。 3. `ui/scenario_main/view.py` 的两个视口改用 `VtkView`,并加 `_build_scene()` 装配地球与时间轴入口(真实轨道待 `engine/` 供给,**不预置编造轨道**) 4. 新增 **`scripts/viz3d_smoke.py`** —— 离屏渲一张三维图,独立验证"渲染链本身能出图" 5. `scripts/capture_modules.py` 截图时把 VTK 内容合成回图像 **本版修掉的两个真缺陷**(都是"真机一跑才暴露") 1. **`SetRenderWindow` 不生效,视口渲染成纯黑**。 我原以为把 Scene 的 render_window 换给 `QVTKRenderWindowInteractor` 就行, 实测 `interactor.GetRenderWindow() is scene.render_window` 为 **False**、 交互器窗口上**渲染器数为 0** —— 渲染器挂在了没人渲染的窗口上。 正确做法是反过来:把 `scene.renderer` 加到交互器**自己**的窗口上, 再让 `Scene.render_window` 指向它。已加**像素级断言**(画面颜色数必须 > 1), 光看 `ready=True` 抓不住这个 bug。 2. **`class VtkView(QWidget)` 在缺 PySide6 时炸掉整条降级路径**。 没有 PySide6 时 `ui.QWidget` 是 `None`,于是**类定义本身**抛 `TypeError: NoneType takes no arguments`,连带 `import ui.registry` 一起失败。 修法:`ui/__init__.py` 新增 `qwidget_base()`,模块级继承 Qt 控件的类一律用它。 **踩过的坑(记录以免重复排查)** - 截图里 VTK 区域是黑的**不代表渲染失败**:`QWidget.grab()` 走 Qt 自己的绘制缓冲, OpenGL 子窗口在其中必然是黑的。实测视口直接读像素有 300+ 种颜色(真画出了地球)。 所以截图脚本改为 grab 后再把 VTK 画面按控件位置贴回去。 - 控制台大量 `wglMakeCurrent failed` 报错出现在**进程退出时**(`Clean()`), 不影响渲染;判断成败要看产物与断言,不要只看 stderr。 **验证**:45 项自检 | 环境 | 结果 | |---|---| | 项目 `.venv`(有 PySide6 + VTK) | 通过 43 / 跳过 2 / 失败 0 | | 运行时解释器(两者皆无) | 通过 39 / 跳过 6 / 失败 0 | `viz3d.scene` 首次从"跳过"变为**通过**。渲染效果见 [`docs/shots/05-scenario_main.png`](docs/shots/05-scenario_main.png)(界面内视口)。 > `scripts/viz3d_smoke.py` 每次跑都会生成 `docs/shots/viz3d-smoke.png`, > 该文件是**自检产物、未入库**(`.gitignore` 不跟踪);需要看图时本地跑一次自检即可。 **已知取舍** | 位置 | 上限 | 升级路径 | |---|---|---| | `_build_scene()` | 只挂地球与相机,**无轨道/视锥/弹道** | `engine/` 装配场景后由 `viz3d.orbit` 与 `Scene.add_cone` 填充 | | 2D 星下点视图 | 目前与 3D 同挂地球,未做二维投影 | 用 `viz3d.orbit.ground_track` 画星下点与覆盖圈 | | VTK 体积 | 约 100MB,属可选依赖 | 打包时按需裁剪 | --- ### v1.7 · 合并重复页「场景编辑器」(2026-10-15) **用户指出:「其实仿真推演就是场景编辑器,两个重了」—— 判断正确,这是我的实现错误。** **错在哪**:方案线框图 `slide22-image11` 这**一个页面**,我在 v1.5 建成了 `ui/scenario_main`(仿真推演),v1.6 又按同一张图建成了 `ui/scenario_editor`(场景编辑器)。 **同一页建了两遍**,两个模块内容高度重叠,纯属重复。 **怎么改的**(删除优于新增): - 保留 `ui/scenario_main/`(用户最初要求的「场景主界面」,KEY 稳定不动) - 把 v1.6 那版更完整的布局并入它 —— 动作栏文件操作组、3D 视口右上角指标浮窗、 底部 UTC 行、事件时间轴、图层开关 chips、属性就地编辑标注 - **删除整个 `ui/scenario_editor/` 目录** - 顶栏页签由 9 个减为 **8 个**(线框图写了 9 个名字,其中两个是同一页) - `ui/registry.py` 加断言:`scenario_editor` 不得再作为独立模块注册,防止以后又加回来 **线框图本身的重复**(已在 `docs/wireframe/README.md` 与 `AGENTS.md` 记录): 线框图顶栏的「场景编辑器」与「仿真推演」两个页签名,内容与标题行都一样。 本项目合并为一页,故顶栏比线框图少一个页签。 **本版修掉的缺陷** 1. **`scripts/check_gui_real.py` 写死 `len(labels) == 9`**,合并后直接失败。 改为从 `WIREFRAME_TABS` 常量派生(校验与常量一致 + 无重名), 不再把"线框图名字数"当成实现必须满足的数字。 2. **`ui/_selfcheck.py` 里那句"场景编辑器必须在册"的断言**随合并失效, 改为断言**它不在册**(反向断言,正是 v1.6 那个重复错误的回归守卫)。 **验证**:43 项自检 | 环境 | 结果 | |---|---| | 项目 `.venv`(有 PySide6) | 通过 40 / 跳过 3 / 失败 0 | | 运行时解释器(无 PySide6) | 通过 39 / 跳过 4 / 失败 0 | 8 个页签实拍见 `docs/shots/`;`05-scenario_main.png` 即合并后的「仿真推演」页。 --- ### v1.6 · 场景编辑器(2026-10-15)⚠️ 本版为重复实现,已在 v1.7 撤销 > **本版是错的,保留记录以免重蹈。** 用户指出「仿真推演就是场景编辑器,两个重了」—— > 同一张线框图(`slide22-image11`)被建了两遍。`ui/scenario_editor/` 目录已于 v1.7 删除, > 其布局并入 `ui/scenario_main/`。顶栏页签由 9 个回到 8 个。 **目标**:按用户给的界面图实现「场景编辑器」页(线框图 `docs/wireframe/slide22-image11.png`)。 **新增 `ui/scenario_editor/`**(第 8 个界面模块),逐区对应线框图: | 区域 | 实现 | |---|---| | 动作栏 | `Open Save 全屏 │ ▶开始仿真 │ ⏸暂停 ■停止 减速 加速 跳转 │ 任务命令 │ 仿真时间 [滑块] ×倍速 │ 成像 轨道云图 空间环境` | | 左栏 | `Object Viewer(对象树)`场景树 + 图层开关 chips(轨道/覆盖圈/视锥/弹道/星空/地面站) | | 中栏 | `3D Graphics`(**右上角指标浮窗** + 底部 UTC 行)+ `2D Graphics(星下点+覆盖圈)` + 事件时间轴 | | 右栏 | `属性与指标`(轨道/载荷可编辑,斜距/仰角/太阳角只读)+ `运行指标(实时刷新)` 4 张指标卡 + `Console 日志` | **一处需要对你说清的图上矛盾**:你给的图里**标题行写「仿真推演」,但高亮页签是「场景编辑器」**。 方案 PPTX 的幻灯片标题是 `页面线框 4/6:场景编辑器` / `页面线框 5/6:…仿真推演主界面`, 这张图的内容(开始仿真 + 时间控制 + 3D/2D 双子窗口)对应 **slide22-image11**。 按你的要求实现为「场景编辑器」页签,并沿用图中的布局与元素。 **本版修掉的缺陷** 1. **`ui/_selfcheck.py` 把模块数写死为 7**,加入第 8 个模块后自检直接失败。 这已是**第三次**因为硬编码模块数而报错。改为校验结构不变量 (非空、`MODULE_KEY` 唯一、`MODULE_TITLE` 唯一、`build_view` 可调用、 且「场景编辑器」必须在册),以后加模块不用再改断言。 2. **桩的 Qt 枚举靠人工逐个补,打地鼠**。本轮为 `Orientation` 又崩一次。 修法:给桩的 `Qt` 命名空间加元类 `__getattr__`,任意枚举都能解析; 显式声明的成员仍优先(真实值不被覆盖)。 3. **`scenario_editor/view.py` 顶层 import PySide6 会断掉无图形环境的降级路径** (`import ui.registry` 直接失败)。改为函数内取 `Qt`,与其余视图保持一致。 **验证**:43 项自检 | 环境 | 结果 | |---|---| | 项目 `.venv`(有 PySide6) | 通过 40 / 跳过 3 / 失败 0 | | 运行时解释器(无 PySide6) | 通过 39 / 跳过 4 / 失败 0 | 9 个页签实拍见 `docs/shots/`,其中 `05-scenario_editor.png` 为本版新增页。 **已知取舍** | 位置 | 上限 | 升级路径 | |---|---|---| | 3D/2D 视口 | 仅挂载容器,VTK 未装无法渲染 | 装 `vtk` 后由 `viz3d/` 挂入渲染窗 | | 时间轴 | 空轴,不预置任何时刻 | `engine/` 事件表供给后填点火/关机/再入/命中与断档区间 | | 「工程」页签 | 仍为占位页 | 新建 `ui/engineering/` 并注册 | --- ### v1.5 · 对齐线框图(2026-10-15) **起因**:用户反馈"界面还是跟线框图不一样"。核对后确认属实 —— 上一版只做到了 "配色对",**布局骨架基本没实现**。 **核对方法**:把方案 PPTX 里 12 张线框图按幻灯片关系逐张抽出(落 `docs/wireframe/`), 与实现逐项比对。差异是结构性的,不是调色能解决的: | 线框图有 | 上一版 | 本版 | |---|---|---| | 标题行 `● SKYIR-SIM │ 页面名 │ 上下文标签` | 无 | `ui/widgets.title_bar` | | 顶栏 **9** 个页签(含工程、场景编辑器) | 7 个 | 9 个(`TAB_TO_MODULE`) | | 动作栏(主操作青底 + 次按钮 + 开关) | 散落各页 | `ui/widgets.action_bar` | | **三栏**(左资源树 │ 中主视图 │ 右指标+日志) | 单栏纵向堆叠 | `ui/widgets.three_column` | | **指标卡**(大号数字 + 判据副标签) | **完全没有** | `ui/widgets.metric_card` / `metric_grid` | | Console 日志栏 | 无 | `ui.widgets.console_log` | | 状态栏右端技术栈 | 无 | 保留(`core/xxx · 表名`) | **关于「场景编辑器」**:用户问"场景编辑器和场景主界面不是同一个页面吗"。 用线框图回答:**不是**。方案 slide 2 目录把页名列得很清楚 —— 「场景管理」是场景**列表**(多场景/版本/打开/复制), 「场景编辑器」是单场景**内容编辑**(资源树拖入 + 3D 态势 + 属性面板), 「仿真推演」才是跑仿真(时间轴 + 3D/2D 双窗口 + Console)。 本项目的「场景主界面」= 线框图的「仿真推演」(对应正确); 「场景编辑器」当时先做了占位页,**已于 v1.6 实现**;「工程」仍为占位页。 **新增 `ui/widgets.py`**:把线框图的通用骨架抽成可复用控件,各模块只填内容。 自检断言 9 个页签名与顺序、指标卡未接入时显示「—」、日志框只读等。 **新增 `docs/wireframe/`**:12 张线框图 + 系统框线图 + 索引说明(页面↔幻灯片↔模块对应表)。 这是界面外观的**设计依据**,后续改 UI 应以此为准,不再靠印象。 **本版修掉的缺陷** 1. **`QScrollArea`、`QCheckBox` 不在共享控件清单里**,两个子代理只能各自在 view 内 函数级 import,破坏了"无 PySide6 时优雅降级"的路径(无 Qt 环境直接 ImportError)。 修法:补进 `ui.QT_WIDGETS`。 2. **`ui/__init__.py` 的 import 手抄清单与 `QT_WIDGETS` 脱节**。这已是第 4、5 次同类偏差 (QColorDialog 放错子模块 → QTabBar 漏 3 处 → QScrollArea/QCheckBox 漏)。 修法:**改为从清单自动生成 import**(`for name in QT_WIDGETS: globals()[name] = getattr(...)`), 加控件只改一处,不再有第二份名单可脱节。 3. **桩的类级枚举访问崩溃**。Qt 大量枚举挂在**类**上(`QScrollArea.Shape.NoFrame`、 `QDialogButtonBox.StandardButton.Ok`),只给实例加 `__getattr__` 不够 —— 类属性查找走元类。两个桩(`ui/_selfcheck.py`、`scripts/check_gui_launch.py`)都缺这层, 症状是"无 Qt 环境下构建窗口 AttributeError"。修法:给桩加元类 `__getattr__` + `Qt` 命名空间改为可解析任意枚举的桩。 4. **`check_gui_launch.py` 的 `Qt` 是空对象**,`Qt.TextFormat.RichText` 取不到值。 修法同上。 5. **子代理用 PowerShell `Get-Content | -replace | Set-Content` 改文件,把三个 view 的 UTF-8 中文写坏**(变成私用区乱码,且有截断)。子代理自查发现并用 `write` 工具重写修复。 **教训:本项目中文源码不要用 PowerShell 文本管道回写,用 `write`/`edit` 工具。** **验证**:43 项自检 | 环境 | 结果 | |---|---| | 项目 `.venv`(有 PySide6) | 通过 40 / 跳过 3 / 失败 0 | | 运行时解释器(无 PySide6) | 通过 39 / 跳过 4 / 失败 0 | 9 个页签实拍见 `docs/shots/`,可逐张与 `docs/wireframe/` 对照。 **已知取舍** | 位置 | 上限 | 升级路径 | |---|---|---| | `ui/imaging_sim`、`ui/constellation` 的开关 | 用 `☐` 文本占位,非真 `QCheckBox` | 控件清单已含 `QCheckBox`,换成真控件并接 `stateChanged` | | 各处 `_scrollable()` | 三个 view 各写一份私有实现 | 并发编辑结束后提到 `ui/widgets.py` | | 「工程」「场景编辑器」 | 占位页,如实标注未实现 | 新建 `ui//` 并在 `ui/registry.py` 注册 | --- ### v1.4 · 深空科技风主题(2026-10-15) **目标**:界面此前是 Qt 默认浅色,与方案要求的「深空科技风统一重绘」不符。本版按方案线框图重做外观。 **色值不是凭感觉调的**:方案原文写明「页面线框设计(12 张,深空科技风统一重绘)」, 于是把 PPTX 里那 12 张线框图(`ppt/media/image2~13.png`)抽出来**逐像素采样**, 得到真实配色并据此写 `ui/theme.py`: | 用途 | 采样值 | 说明 | |---|---|---| | 深空底 | `#0B1221` / `#111C2F` | 最外层与应用底 | | 面板 | `#141F2F` / `#152031` | 分区背景 | | 卡片 | `#162239` / `#1E293B` | 圆角描边卡片 | | 边框 | `#334155` | slate-700 | | **强调色** | `#22D3EE` | 顶栏品牌圆点实测值(青) | | 成功 / 警告 / 危险 | `#48D878` / `#FBBF24` / `#F87171` | 体检结论、校验状态 | | 正文 / 次级 | `#F1F5F9` / `#94A3B8` | slate-100 / slate-400 | 整色调即 Tailwind slate 色阶 + cyan 强调,与线框图一致。 **新增 `ui/theme.py`** - 设计 Token 常量 + `apply_theme(app)`:`setStyle("Fusion")`(原生 Windows 样式会抢绘制,QSS 覆盖不全)+ QSS + QPalette - QSS 覆盖主窗口/菜单/状态栏/分组框/按钮/输入/表格/页签/列表/标签/滚动条/复选/提示等 - QPalette 兜住 QSS 覆盖不到的原生绘制(原生对话框、禁用态文字) - 自检断言:Token 均为 6 位十六进制、底色亮度 < 60(确保是深色而非浅色)、正文亮度 > 200(对比度)、 QSS 选择器齐全、无未替换占位符、**花括号配平**(QSS 语法错会让 Qt 静默忽略整段样式) **结构对齐线框图**:主导航由左侧树改为**顶栏页签**(线框图顶栏即「◉ SKYIR-SIM + 模块页签」), 页签选中态用青色下划线;主操作入口(上传 GLB / 生成星座 / 开始仿真 / 开始成像)用青色实底 `primary=True`。 **本版修掉的缺陷** 1. **输入框 placeholder 上下被裁**。QSS 的 `padding` 会压缩内容矩形,但 Qt 的 `sizeHint` 不把 padding 算进去, 控件停在默认高度,文字上下各缺一截(截图放大 3 倍后确认,非预览缩放假象)。 修法:给 `QLineEdit`/`QSpinBox`/`QComboBox` 显式 `min-height: 20px`。 2. **`check_gui_real.py` 没调 `apply_theme()`**,导致它截出来的是 Qt 默认浅色 —— 截图脚本与真实启动路径不一致, 会给出"主题已生效"的假证据。修法:与 `main.py` 保持一致,建窗前套主题;并断言窗口底色亮度 < 60。 3. **桩清单与源码脱节**(两处)。新增 `QTabBar` 后,`check_gui_launch.py` 与 `ui/_selfcheck.py` 的假 PySide6 缺少该名字,症状是"启动路径返回 1"这种远因难查的失败。 修法:两处补齐,并在 `check_gui_launch.py` 加**前置守卫** —— 用 `ast` 扫描 `ui/__init__.py` 与 `ui/theme.py` 实际从 PySide6 导入的名字,与桩清单求差,缺谁直接点名报错。该守卫当场又抓出 `theme.py` 要的 `QColor`/`QPalette`。 **新增 `scripts/capture_modules.py`** 逐个模块截图并**断言页签与视图联动**(点第 i 个页签,堆叠窗口必须停在第 i 个), 产物落 `docs/shots/`,用于并排核对七个页面的风格一致性。 **验证** | 环境 | 结果 | |---|---| | 项目 `.venv`(有 PySide6) | 通过 39 / 跳过 2 / 失败 0 | | 运行时解释器(无 PySide6) | 通过 38 / 跳过 3 / 失败 0 | 七个模块实拍见 `docs/shots/`,主界面见 [`docs/screenshot-main.png`](docs/screenshot-main.png)。 --- ### v1.3 · 真机跑通界面(2026-10-15) **目标**:让界面**真的能双击跑起来**,而不是只显示"缺依赖"提示。 **做法**:在项目内建 `.venv`(`.gitignore` 已忽略),装 PySide6 6.8.1.1。 `start.bat` 本就把 `.venv` 作为第一优先级,所以装完即生效,无需改脚本。 用 `--system-site-packages` 创建,让运行时已有的 numpy / python-docx 继续可见,只需补图形依赖。 **本版修掉的缺陷**(都是"真机一跑就暴露、无图形环境永远测不出"的那类) 1. **`QColorDialog` 导入位置错,导致装了 PySide6 反而判"缺依赖"**。 `ui/__init__.py` 写的是 `from PySide6.QtGui import QAction, QColorDialog`, 但 **`QColorDialog` 属于 `QtWidgets`,`QtGui` 里没有** —— 该行必然 `ImportError`, 于是 `MISSING = "PySide6"`,界面永远起不来。 更糟的是 `except ImportError` 把"自己导入写错"与"依赖没装"混为一谈,用户会去反复重装 PySide6。 **修法**:把 `QColorDialog` 移到 `QtWidgets`;并让 `except` 分支区分两种情况 —— `import PySide6` 成功却导入控件失败时,直接报"是导入清单有误,不是依赖缺失,请勿重装"。 2. **`verify_all.py` 的输出解码写死 utf-8,把"跳过"静默误报成"通过"**。 子进程(尤其 `python -m`)往中文 Windows 控制台写的是 **GBK**, 按 utf-8 解会让中文全变 `U+FFFD`,`SKIP_MARKERS` 一个都匹配不上 —— 于是 `sgp4_prop`、`viz3d.scene` 这些**没被真正验证**的模块显示为"通过"。 这是"测试给自己发绿灯"的典型,比测试失败更危险。 **修法**:`_decode()` 按候选编码(utf-8 / 系统首选 / gbk / mbcs)取替换字符最少的结果; 同时让子脚本显式 `reconfigure(encoding="utf-8")`,不再依赖猜测。 修好后同一环境下"通过 41"变为"通过 38 / 跳过 3"—— 数字变小了,但**可信了**。 3. **`ui/_selfcheck.py` 只在"没装 PySide6"时才能通过**。 它硬断言 `MISSING == "PySide6"`,所以装了 PySide6 的环境里自检反而失败; 而在真 Qt 下构造控件又缺 `QApplication`,直接 fatal。 **修法**:按 `MISSING` 分支走两条路径;有真 PySide6 时跳过缺依赖断言、先建 `QApplication` 再建控件。 4. **版本号不一致**:`main.py` 还是 `1.0.0`,README 已到 v1.2。已升为 `1.3.0`。 **新增 `scripts/check_gui_real.py`** 用**真实** PySide6 建窗、渲染、截图,并断言导航树里七个业务模块齐全、堆叠视图数为 7。 与 `check_gui_launch.py`(桩,无依赖也能跑)分工:桩证明"调用链对",真机测试证明"真 Qt 下能跑"。 上面第 1 个缺陷正是只有真机测试才能暴露的 —— 桩和源码犯了同样的错,桩自然测不出来。 缺 PySide6 时本脚本自行"跳过"(退出 0),不伪装成通过。 **验证**:两个环境分别全量自检 | 环境 | 结果 | |---|---| | 项目 `.venv`(有 PySide6) | 通过 39 / 跳过 2 / 失败 0(跳过 `sgp4`、`vtk`) | | 运行时解释器(无 PySide6) | 通过 38 / 跳过 3 / 失败 0(多跳过 `check_gui_real`) | 界面实拍见 [`docs/screenshot-main.png`](docs/screenshot-main.png)(1750×1125)。 --- ### v1.2 · 双击启动与启动路径缺陷修复(2026-10-15) **目标**:项目根目录加一个 `start.bat`,双击即可启动;顺带把"启动"这条路径上暴露的缺陷修掉。 **新增 `start.bat`** - 编码 GBK、行尾 CRLF(与中文 Windows 控制台一致;文件内已注明勿另存为 UTF-8) - 开头 `chcp 936`:用户若开了 Windows 的"Beta: 使用 Unicode UTF-8",控制台默认代码页会变成 65001,本文件的 GBK 字节就会乱码;显式固定代码页避免这个坑 - 解释器查找顺序(逐级兜底,每级都**真跑一次**校验,不只是 `where` 找到就算): 1. 项目自带 `.venv\Scripts\python.exe` 2. `py -3` 启动器(取 `sys.executable` 真实路径;不直接用 `py -3`,因为带空格的命令没法整体加引号调用) 3. PATH 上的 `python` 4. 本机 `.dsh` 运行时内置 Python - 带参数时透传给 `main.py`(`start.bat --version` / `start.bat --verify core`) - 缺 `PySide6` 时不报错退出,而是提示安装命令 + 打印各层依赖探测 + 给出不依赖图形环境的自检命令 - 不自动安装任何依赖,不修改系统环境 **本版修掉的缺陷** 1. **`main.py` 启动界面必崩**(潜在缺陷,只在装了 PySide6 的机器上暴露)。 `main.py` 调 `ui_main(argv)`,而 `ui.app.main()` **不接受参数** —— 一点启动就 `TypeError`。 本机没有 PySide6,`python main.py` 一直走"缺依赖"分支返回 1,所以这个缺陷在无图形环境里从未暴露。 修法:改调 `ui_main()`(Qt 要的是含程序名的完整 `sys.argv`,`QApplication` 内部自己取,传切过的 `argv` 反而不对)。 同时新增 `scripts/check_gui_launch.py`:用桩 PySide6 把 `main → ui.app.main → QApplication.show → exec` 这条主线跑通并断言, 使该路径在无图形环境下也能被覆盖。已并入 `verify_all.py`,验证项从 39 增至 **40**。 2. **`main.py --verify <过滤词>` 静默失配**。 原来只把脚本名传给子进程,`--verify core` 之后的过滤参数被丢掉, 于是"过滤"看起来生效、实际跑了全部模块。修法:把 `--verify` 之后的参数原样透传。 **踩过的坑(记录以省后来人时间)** - 本机 PATH 上唯一的 `python.exe` 是 **Windows 应用商店的 0 字节占位符**(`WindowsApps\python.exe`,调用返回 9009), 且**没有 `py` 启动器**。所以 `start.bat` 不能假设 `python` 可用 —— 必须逐级兜底并真跑校验。 - 验证 bat 中文是否正常时,**经 PowerShell 管道/重定向看到的中文是乱码,这不能说明 bat 有问题**。 实测:`start.bat` 的字节是合法 GBK(`天`=`CC EC`、`基`=`BB F9`…),GBK 解码干净、可无损回编; 乱码来自测试管道自身的解码,双击运行的 `cmd.exe`(代码页 936)显示正常。 判断编码问题要**看字节,不要看管道输出**。 --- ### v1.1 · 全六层贯通与端到端最小闭环(2026-10-15) **目标**:把六层全部落地,并让"场景 → 星历 → 观测计划 → 逐帧成像 → 结果打包"跑通一条真实闭环。 **本版新增** | 层 | 新增 | 说明 | |---|---|---| | 物理内核层 | `core/attitude/`、`core/navigation/`、`core/comm/`、`core/environment/`、`core/collision/` | 姿态指向、导航 ICD/DOP、天线与链路预算、空间环境、碰撞预警 | | 仿真引擎层 | `engine/scene.py`、`engine/timeline.py`、`engine/packaging.py`、`engine/runner.py`、`engine/__init__.py` | 场景装配、时序推进、结果打包、端到端主控 | | 三维可视化层 | `viz3d/earth.py`、`orbit.py`、`gltf.py`、`cone.py`、`scene.py` | VTK 渲染,尺度约定 1 单位 = 1 km | | 模型库服务层 | `models/glb_check.py`、`physical.py`、`library.py` | GLB 自解析体检、物理属性模板、资产版本与引用计数 | | 数据持久层 | `persistence/schema.sql`、`schema.json`、`dm.py`、`dao.py` | 达梦8 十五表 DDL、九段式 JSON Schema、环境变量驱动的连接工厂、DAO | | 界面层 | `ui/` 七模块 + `ui/app.py`、`ui/registry.py` | 主窗口、导航、七个业务模块面板 | | 工程配套 | `main.py`、`scripts/verify_all.py`、`scripts/usage_report.py`、`requirements.txt`、`.gitignore` | 入口、全量自检、用量统计、依赖清单 | **端到端闭环**(`engine/runner.py::run_scenario`) ``` 九段式场景 JSON → engine.scene.assemble() 装配(剥溯源对象、校验九段齐全) → engine.runner.propagate_*() 逐帧递推全星座星历(J2 长期项) → core.coverage.access_windows 按覆盖对象算可见窗口 → core.scheduling.build_plan() 按交接 4 判据编排接力计划 + 守恒断言 → core.ir_imaging.render_frame 逐帧走完七步成像链 → engine.packaging.write_* 写 manifest / 星历 + sha256 索引 ``` **本版修掉的实际缺陷** 1. **`open()` 时区缺失导致的历元错误**(`engine/scene.py`)。ISO 8601 无时区时按裸值解析会得到本地时区结果,统一补 UTC。 2. **`time.step > time.duration` 未拦截**(`engine/scene.py`)。会产生 `frame_count = 0` 的"空仿真"却看起来正常,现在直接拒绝。 3. **Path 对象无法 JSON 序列化**(`engine/packaging.py`)。`relative_to()` 返回 `WindowsPath`,写 manifest 时抛 `TypeError`。修法:`RunResult.__post_init__` 归一化为字符串 + `relative_posix()` 统一正斜杠(保证跨平台 manifest 可逐字节比对)。 4. **覆盖对象坐标被忽略**(`engine/runner.py`)。读了 `geometry` 却没用,退回目标发射点,导致有明确观测点的场景报"无法定位"。修法:优先取 `geometry.lat/lon`,其次 `center_lat/center_lon`,再退目标发射点,都没有才报错。 5. **`Timeline.step()` 走完最后一步不清播放标志**(`engine/timeline.py`)。末帧返回 `False` 但 `playing` 仍为 `True`,调用方需自己兜底。修法:末步自动暂停。 6. **`--verify` 在 GBK 控制台崩溃**(`scripts/verify_all.py`)。打印含替换字符的失败输出时 `UnicodeEncodeError` 反过来把自检脚本弄崩。修法:标准输出重配 UTF-8 + `errors="replace"`。 7. 三维/模型库子代理另修:正二十面体细分边中点配错(产生 40 条边界边、非闭合流形)、极点纹理接缝负 u 被钳、`glb_check` 对 13 类畸形上传直接崩溃、`library` 坏索引漏原始英文异常且引用计数不一致会让误删保护失效。 **验证**(`python scripts/verify_all.py`):**39 个模块自检全部通过(39 通过 / 0 跳过 / 0 失败)**。 **子代理另修的缺陷**(各层自查抓出,均非"跑通即放过") - 三维层:正二十面体细分把边中点配错,产生 40 条边界边(非闭合流形);极点处经度退化导致纹理接缝负 u 被钳、极点顶点被 12 个扇区共用。 - 模型库层:`glb_check` 对 13 类畸形上传直接抛 `KeyError/IndexError/AttributeError`;`library` 坏索引漏原始英文异常,且 `refs`/`ref_by` 不一致会让"误删保护"失效。 - 通信层:**链路预算是 457 dB 的错**(`k = 1.38e-23` 取对数后为负,"加 k" 实际减了 228.6 dB),而断言是从同一个错公式推出来的,所以照样通过 —— 已改为 `+228.599`,并用**功率域独立复算**交叉验证(吻合到 3.6e-15)。 - 碰撞层:`probability.py` 的 erf 归约写错(y 方向高斯中心不在 0,必须是两项 erf 之差);筛选只判"最小值低于门限"会重复报接近事件(已改为要求无约束极小点落在弧段内);两份星历各自用首个历元当时间原点,导致相隔 10 天的弧段被判为完全重叠。 - 环境层:大气表与 IGRF-13 系数**无网络无法回源比对**,已用内部自洽交叉验证(相邻节点反解标高 27/28 段吻合 <0.1%;偶极系数独立复现公开磁极位置 80.65°N/72.68°W)。**发布前仍需人工比对 USSA1976 / CIRA 与 `igrf13coeffs.txt`**,两个模块 docstring 已标注。 **`enu_frame` 行约定陷阱(跨层,已修)** `core/time_ref/state.py::enu_frame` 把 ENU 三个基矢量放在**行**上,因此 ENU→ECEF 必须用 `enu.T @ local`。 用错转置**不会报错**:`enu` 是正交矩阵,`enu @ v` 与 `enu.T @ v` 模长相同,任何"只查长度"的校验都发现不了; 在北京纬度上两者方向差 **20.18°**,会把整条几何静默转歪。 修法:docstring 写明行约定与两个方向的用法,新增 `enu_to_ecef()` / `ecef_to_enu()` 让正确写法成为最省事的写法, 并在 `time_ref` 自检里加回归断言(锁死 `enu[2] == 测地法线`,并断言用错转置的方向偏差 > 15°)。 **本版刻意的取舍** | 位置 | 上限 | 升级路径 | |---|---|---| | `engine/packaging.py` 星历 | 只实现 CSV/JSON;OEM/SP3 未实现并**明确报错** | 与实测样例对齐头部与时标规范后再补 | | `engine/runner.py` 目标温度 | 取温度曲线首点,无曲线时用 350K 参考 | 接入完整弹道温度曲线插值 | | `engine/runner.py` 单观测点 | 只编排首个覆盖对象的观测计划 | 多对象并行编排 + 优先级调度 | | `core/environment/atmosphere_density.py` | 静态球对称廓线 + 活动指数订正,无纬度/地方时/季节项 | 换 pymsis / NRLMSISE-00 | | `core/environment/geomagnetic.py` | 仅偶极项(n=1),非偶极项占表面场 10~20% | 补 n≤13 球谐求和或换 ppigrf | | `core/environment/radiation_belt.py`、`debris.py` | 二维表接口,**内置无任何通量数据**,缺数据即 `LookupError` | 挂 AE8/AP8、ORDEM/MASTER 真实数据 | | `core/collision/conjunction.py` | 线性相对运动筛选(粗步长内无加速度项) | 二次相对运动或自动细分 | | `core/collision/probability.py` | 瞬时二维 TCA 积分,忽略沿迹向时间维不确定度 | Alfriend 1999 三维时空积分 | | `core/attitude/pointing.py` | 地面足印半径用局部球近似(30° 侧摆时个位数 % 误差) | 逐点椭球测地线求交 | | `core/comm/antenna.py` | 忠实实现 ITU-R S.465/S.580 **原文**,含 φ_m 处约 4 dB 跳变 | 需连续包络时改用平滑模型(docstring 已给路径) | | `viz3d/earth.py` 地球 | 球近似(极点差 21km 量级) | 换椭球网格 | | `persistence/` ORM | 无声明式映射,纯参数化 SQL | 装 SQLAlchemy 后补 | --- ### v1.0 · 骨架与物理内核(2026-10-12) **目标**:按《天基红外星座仿真系统开发方案 V2.0》把六层目录立起来,并把依赖链最底层的物理内核做到"可自检、可独立交付"。 **目录结构**(六层各自独立文件夹,依赖单向向下) ``` SKYIR-SIM/ ├── AGENTS.md # 项目约定(含 ponytail 技能强制条款) ├── README.md # 本文件:开发留痕 ├── core/ # 物理内核层 · 零 UI 依赖,可独立交付/测试 ├── engine/ # 仿真引擎层 · 场景装配/时序/结果打包 ├── models/ # 模型库服务层 · GLB 体检/资产/版本/物理属性 ├── persistence/ # 数据持久层 · 达梦8 十五表 ├── viz3d/ # 三维可视化层 · VTK 渲染 ├── ui/ # 界面层 · PySide6 ├── data/ files/ # 数据与产物(影像/星历/GLB 落文件系统) ├── docs/ scripts/ tests/ ``` **界面层按七个模块分子文件夹**(与业务闭环六环节对应) | 目录 | 中文模块名 | 业务环节 | |---|---|---| | `ui/model_library/` | 模型库 | ① 模型库 | | `ui/constellation/` | 星座构型与集群协同 | ★ 星座集群 | | `ui/satellite_mgmt/` | 卫星管理 | ② 卫星管理 | | `ui/scenario_mgmt/` | 场景管理 | ③ 场景管理 | | `ui/scenario_main/` | 场景主界面 | ④ 仿真推演 | | `ui/imaging_sim/` | 成像模拟 | ⑤ 成像模拟 | | `ui/simulation_report/` | 仿真报告 | ⑥ 仿真报告 | **物理内核层按模块分子文件夹**(用户要求:每个模块单独成文件夹,如大气模型、辐射模型) | 目录 | 中文模块名 | 内容 | |---|---|---| | `core/time_ref/` | 时空基准 | UTC/TAI/TT/TDB/GPST、J2000/ITRF/RTN/ENU、State 契约 | | `core/orbits/` | 轨道 | 二体、J2 长期项、HPOP 数值积分、机动、TLE/SGP4 | | `core/constellation/` | 星座构型 | Walker 三式 P/S/F、多壳层叠加 | | `core/coverage/` | 覆盖与可见性 | 仰角/遮挡判据、覆盖效能、ISL 星间路由 | | `core/scheduling/` | 接力观测调度 | 交接 4 判据、断档原因码 E01~E04、滚动贪心编排 | | `core/radiation/` | 辐射模型 | 普朗克、维恩、亮温反解、灰体、辐射传输 | | `core/atmosphere/` | 大气模型 | 可插拔 LUT(QRadtran/CART/HAPI/简化)、30 项参数分组 | | `core/sensor/` | 传感器物理 | 光学、探测器、IFOV、PSF、NETD 噪声、量化 | | `core/ir_imaging/` | 红外成像链 | 七步链、燃烧物辐射源、逐帧渲染、manifest | | `core/ballistic/` | 导弹弹道 | 四段状态机、再入黑障(负样本来源) | | `core/attitude/` | 姿态与指向 | 四元数、对地定向、侧摆 ±30°、视场锥 | | `core/navigation/` | 星座导航 | 公开 ICD 常数表、GDOP/PDOP 解算 | | `core/comm/` | 天线与通信 | ITU-R 增益模型、链路预算、ISL、干扰几何 | | `core/environment/` | 空间环境 | 大气密度、地磁、辐射带、碎片 | | `core/collision/` | 碰撞预警 | Hoots 筛选、接近事件、PC 碰撞概率 | **本版修掉的实际缺陷**(都是自检抓出来的,不是猜的) 1. **阻力加速度量级错 100 万倍**(`orbits/hpop.py`、`ballistic/state_machine.py`)。 kg/m³ 配 km/s 时换算因子应为 `1e3`,原写成 `1e9`。后果:再入段瞬间刹停(2.5 km/s → 0.009 km/s)。 用 ISS 量级反算校验:正确值 1.26e-6 m/s²,与公开量级一致。 2. **主动段被误判为再入黑障**(`ballistic/state_machine.py`)。 导弹在 17 km 高度就已加速到 2 km/s,"低空+高速"判据把助推段整段标成黑障(实测错标 79 帧)。 修法:黑障判据增加"必须处于再入段"的限定。 3. **段切换与状态记录不同源**(`ballistic/state_machine.py`)。 原来用推进前的状态判段、用推进后的状态记点,产生"标着 midcourse 的黑障点"这种自相矛盾记录。 修法:段切换改到推进之后、用同一状态判定。 4. **质量记账重复扣推进剂**(`ballistic/state_machine.py`)。 工作段按流量扣一次、级分离又扣一次,导致质量提前归零、推力除以零。 修法:级分离只扣结构干重。 5. **可见性判据混用球/椭球**(`coverage/access.py`)。 用"地心张角 ≤ 地平张角之和"判定遮挡,与椭球上的站位半径混用,极区算出 `arccos(>1)`。 修法:对地可见性统一用本地 ENU 仰角判定(仰角 ≥ 0 等价于不被地球遮挡);星间链路另用线段-球体求交。 6. **离轴角用了地心径向**(`coverage/access.py`)。 nadir 应是椭球法线反向,用地心径向在中纬度引入约 0.19° 的系统性侧摆角偏差。 7. **调度断档重复记账**(`scheduling/handover.py`)。 同一段"无星可用"被几何判据与交接判据各记一次,守恒断言算出双倍断档。 修法:登记断档前做重叠裁剪,使"弧段总时长 + 断档总时长 == 仿真时长"成为硬不变量。 8. **交判据③结构性恒真**(`scheduling/handover.py`)。 原式测"交接时刻到帧栅格的距离",该量天然不超过半帧,判据永远不会触发。 修法:改测两颗星的帧栅格**相位失配**,并注明默认 0.5 帧是宽门限、需按传感器同步精度标定。 9. **逐帧成像自适应归一化**(`ir_imaging/chain.py`)。 每帧各自归一化会把清空/雾霾两帧都拉到满量程,帧间 DN 不可比,辐射定标失效。 修法:`render_sequence` 全序列共用一个满量程。 10. **HPOP 历元累加丢精度**(`orbits/hpop.py`)。 反复 `jd += dt/86400` 在 JD 2.4e6 量级上累积误差,末点历元对不上。 修法:用累计秒数一次性换算历元。 11. **TLE 自检样例伪造校验位**(`orbits/sgp4_prop.py`)。 原样例借用了真实卫星编号却编造了数字,校验位不匹配。 修法:改为明确标注的**合成样例**(编号 00001,校验位按规则算好),并在注释里写明真实根数必须在线拉取并带 source。 **本版刻意的取舍**(`# ponytail:` 注释已就地标记) | 位置 | 上限 | 升级路径 | |---|---|---| | `orbits/hpop.py` 大气密度 | 静默指数模型,无纬度/地方时/季节变化 | 换 NRLMSISE-00 | | `orbits/hpop.py` 日月星历 | 低精度解析式 | 换 JPL DE 星历 | | `orbits/j2.py` | 只含 J2 长期项,24h 位置误差约 1km | 走 `orbits/hpop.py` 数值积分 | | `ballistic/state_machine.py` 级干重 | 按推进剂 10% 估 | 按型号填入显式干重 | | `ballistic/state_machine.py` 射程解析 | 平地常重力近似,< 1000km 可用 | 椭圆弹道方程 | | `scheduling/handover.py` 编排 | 滚动贪心,非全局最优 | 区间调度 DP | | `sensor/model.py` 卷积 | O(H·W·k²),小核够用 | FFT 卷积 | | `atmosphere/lut.py` 简化模型 | 单一均匀消光系数 | 挂 QRadtran/CART LUT | **验证方式**:每个模块带一个 `assert` 自检(`python -m <模块>`),断言的是物理量级与守恒关系, 不是"跑通不报错"。已通过的模块与其关键断言见下节"验证清单"。 **已知缺口**(本版明确没做,不是遗漏) - 达梦8 未实机连接(环境无 dmPython),持久层只验证到 SQL 与 Schema 结构。 - 大气 LUT 无实测数据:`qradtran_lut` / `cart_lut` / `hapi_lbl` 在无数据时**明确报错**,不返回估算值。 - 辐射带 AE8/AP8 与碎片 ORDEM/MASTER 同理:只定义加载接口,无数据即报错,不编造通量。 - 三维渲染与界面未实机运行(环境无 VTK / PySide6),只验证到接线与缺依赖提示。 --- ## 二、从字段到轨道:一次完整的算例 这一节回答一个问题:**数据库里的哪些字段,喂给哪个物理模型,算出什么,接下来又拿去做什么,最后怎么变成一条轨道。** 整条链是单向的,每一环的输入输出都写清楚。看懂这条链,就看懂了项目的骨架。 ### 第 0 步:数据从哪来 两张表承载全部输入: | 表 | 提供什么 | |---|---| | `T_SATELLITE_LIB` | 一颗星的**轨道参数集**:六根数 + 历元 + 模型选择 + 摄动系数 | | `T_SATELLITE_PARAM` | 上面每个数值的**溯源**(来源 / 类型 / 引用 / 不确定度) | `T_SATELLITE_LIB` 的字段分组(详细见 [TABLES.md](TABLES.md)): ``` 标识 CODE, NAME 模型选择 PROPAGATOR, ORBIT_INPUT, FRAME 六根数 SEMI_MAJOR_AXIS, ECCENTRICITY, INCLINATION, RAAN, ARG_PERIGEE, MEAN_ANOMALY 历元 EPOCH_UTC 高度冗余 APOGEE_ALT, PERIGEE_ALT TLE TLE_LINE1, TLE_LINE2 摄动系数 MASS_KG, AREA_M2, AREA_MASS_RATIO, DRAG_CD, REFLECT_CR, PERTURBATIONS_JSON ``` ### 第 1 步:字段 → 参数集(`core/orbits/elements.py`) **调用**:`from_db_row(row)` → `OrbitParams` | 输入字段 | 变成 `OrbitParams` 的 | 处理 | |---|---|---| | `EPOCH_UTC`(TIMESTAMP) | `epoch_jd` | `datetime` → UTC 儒略日(`jd_from_datetime`) | | `ORBIT_INPUT='kepler'` | `input_mode` | 直取六根数 | | `ORBIT_INPUT='apogee_perigee'` | `input_mode` | `a=R_E+(h_p+h_a)/2`,`e=(h_a−h_p)/(h_a+h_p)` | | `ORBIT_INPUT='tle'` | `input_mode` | 交给 SGP4 解析,**不转瞬时根数** | | `SEMI_MAJOR_AXIS` / `ECCENTRICITY` | `a_km` / `e` | 构造期校验:`a>0`、`0≤e<1`、近地点不穿地 | | `INCLINATION` / `RAAN` / `ARG_PERIGEE` | 同名(度) | 进 `core/` 才转弧度,单位只在边界转一次 | | `MEAN_ANOMALY`(度) | `mean_anomaly_deg` | 界面上给的是平近点角 M | | `PROPAGATOR` | `propagator` | 决定第 2 步走哪个递推器 | | `MASS_KG` / `AREA_M2` / `DRAG_CD` / `REFLECT_CR` | 同名 | HPOP 阻力/光压项 | | `PERTURBATIONS_JSON` | `flags` | 六个摄动开关 | **这一步产出**:一个**已校验**的参数集。非法组合当场报错,不会让 NaN 流到下游。 典型拒绝:`e≥1`、近地点低于地表、远地点低于近地点、`input_mode='tle'` 却配 `propagator='j2'`。 同时算出派生量:`period_s`(开普勒第三定律)、`mean_motion_rev_day`、`apogee_alt`、`perigee_alt`。 > 为什么历元必须单列:六根数脱离历元只是**轨道几何**,不是「某时刻的状态」。 > 依据见 [LITERATURE.md](LITERATURE.md) 里 CCSDS 502.0-B 的 ODM 定义。 ### 第 2 步:参数集 → 历元状态(`state_at_epoch`) | `propagator` | 调用的模型 | 得到 | |---|---|---| | `kepler` / 六根数档 | `core/orbits/twobody.py::elements_to_rv` | `State(epoch_jd, r, v, "J2000")`,**km / km·s⁻¹** | 具体动作:`mean_anomaly_deg` →(`mean_to_true`,Newton 迭代解开普勒方程)→ 真近点角 ν →(`elements_to_rv`,三次旋转矩阵)→ 位置 `r` 与速度 `v`。 **这一步产出**:`State` —— 全项目统一的状态契约(`core/time_ref/state.py`), `t`(儒略日) + `r`(km) + `v`(km/s) + `frame`。**所有模块之间只认这个契约,不传散装数组。** 若 `propagator='sgp4'`,则走 `parse_tle` → `propagate_tle`(平均根数由 SGP4 自己吸收摄动)。 ### 第 3 步:历元状态 → 一圈采样星历(`core/orbits/ephemeris.py`) **调用**:`sample_states(params, revs=1.0, samples_per_rev=90)` 分派到三档递推器之一: | `propagator` | 物理模型 | 每步做什么 | 精度量级 | |---|---|---|---| | `two_body` | 开普勒解析解 `propagate_kepler` | 真近点角加 `n·Δt`,直接重算 `r,v` | 随弧长发散 | | `j2` | 二体 + J2 长期项 `propagate_j2` | Ω、ω 按 J2 进动率线性漂移 | 24 h 约 1 km | | `hpop` | RK4 全摄动 `propagate_numeric` | 每步求加速度:J2~J4 带谐 + 指数大气阻力 + 柱面光压 + 日月三体 | 亚 km/天 | | `sgp4` | SGP4 `propagate_tle` | 平均根数递推 | 官方模型精度 | 采样策略:一圈默认 90 点(`MIN_SAMPLES_PER_REV=60` 下限,低于此轨道线呈折线); `hpop` 档另受 `MAX_NUMERIC_STEP_S=60 s` 积分步长约束,且**整段积分一次再抽点**—— 逐点重启积分器会把误差抖成锯齿。 **这一步产出**:`(states, meta)` —— `states` 是 `[State, ...]`(一圈闭合,末点与首点重合已去掉), `meta` 带周期、近远地点、采样数、历元等派生信息。 ### 第 4 步:星历 → 两条可视化(`viz3d/`) 同一串 `State` 喂给两个**互相独立**的场景: | 视口 | 调用 | 输入 | 输出 | |---|---|---|---| | 3D Graphics | `viz3d/orbit.py::orbit_actor` | `[State]` 的 `r`(km) | 空间轨道折线(透视投影) | | 2D Graphics | `viz3d/map2d.py::track_actor` | 每点 `State.to_geodetic()` 的 (纬度, 经度) | 星下点轨迹(等经纬正交投影) | 2D 那步的关键处理:`eci_to_ecef`(含 GMST 旋转)→ 大地坐标 → 投影到平面。 跨 ±180° 经线必须断开,否则折线会从 +179° 连回 −179°,在地图上横穿一条假线。 **最后落进界面**:`ui/scenario_main/view.py::_build_scene` 把两串 actor 分别塞进两个 `Scene`, `ui/vtk_view.py::attach` 挂到 Qt 控件上。 ### 第 5 步:之后接着做什么(轨道算出来只是起点) 轨道是**所有下游物理的公共输入**,算完才有下面这些: | 下游 | 消费轨道的什么 | 调用 | 产出 | |---|---|---|---| | 覆盖与可见性 | 卫星 `r`、目标位置 | `core/coverage/access.py::access_windows` | 可见时间窗、仰角、斜距 | | 覆盖效能 | 时间窗 + 地面目标几何 | `coverage_metrics` | 重访间隔、覆盖率、Gap 分布 | | 星间链路 | 两星 `r` + 地球遮挡 | `isl_links` / `isl_route` | 链路可用性、路数 → 落 `T_ISL_ROUTE` | | 传感器指向 | 卫星 `r` + 目标 `r` | `core/attitude/pointing.py::ground_footprint` / `fov_rad` | 侧摆角、视场锥、地面足印 | | 光照条件 | 卫星 `r` + 太阳方向 | `coverage.sun_angle_deg` | 太阳角、地影判定 | | 红外成像 | 足印 + 载荷参数 | `core/ir_imaging/chain.py::render_frame` | 逐帧辐亮度 → DN → 成像结果 | | 接力调度 | 各星时间窗 | `core/scheduling/handover.py` | 观测段、交接、断档原因码 E01~E04 → 落 `T_OBS_SEGMENT` | | 碰撞预警 | 两星 `r,v` | `core/collision/conjunction.py` | 接近事件、TCA、碰撞概率 | 于是数据库里多出运行结果:`T_SIM_RUN`(一次运行)、`T_RESULT_FILE`(影像/星历文件索引)、 `T_OBS_SEGMENT`(观测段)、`T_ISL_ROUTE`(路由)、`T_REPORT`(报告)、`T_AUDIT_LOG`(审计)。 ### 一句话概括 ``` T_SATELLITE_LIB 的 7 个轨道字段 └─ core/orbits/elements.py 校验 + 归一化 + 历元转儒略日 └─ state_at_epoch 开普勒方程 → State(r, v) ← 全项目统一契约 └─ sample_states 二体 / J2 / HPOP / SGP4 选一 → [State…] ├─ orbit_actor → 3D 空间轨道线 ├─ track_actor → 2D 星下点地图 └─ coverage / attitude / imaging / scheduling / collision → 下游结果 ``` 自检这条链: ```powershell python -m core.orbits.elements # 参数集:三种输入方式 + 9 类非法输入拦截 python -m core.orbits.ephemeris # 星历:三档递推 + 闭合性 + J2 进动 + 多圈 ``` --- ## 二·一、验证清单 一条命令跑全部(推荐): ```powershell python scripts/verify_all.py # 全部(61 项) python scripts/verify_all.py core # 只跑物理内核层 python scripts/verify_all.py -v # 打印每个模块的输出 ``` 或双击 [`start.bat`](start.bat),也可以 `start.bat --verify` 直接让启动器代跑。 **想跑图形界面**:项目内已备 `.venv`(含 PySide6),双击 `start.bat` 即用它启动。 若 `.venv` 不存在或想重建: ```powershell python -m venv .venv --system-site-packages # 继承运行时已有的 numpy / python-docx .venv\Scripts\python.exe -m pip install -r requirements.txt ``` `start.bat` 的查找顺序是 `.venv` → `py -3` → PATH `python` → `.dsh` 内置, 所以重建 venv 后无需改脚本。 脚本会区分**通过 / 跳过 / 失败**:缺可选依赖(如未装 vtk)的模块算"跳过"并单独列出, 不把跳过伪装成通过。任何模块非 0 退出即判失败。 除 48 个模块自检外,还包含 13 项脚本自检(缺依赖或缺达梦口令时自行"跳过"并单列): - `scripts/check_gui_launch.py` —— 用桩 PySide6 覆盖"启动界面"调用链(无依赖也能跑) - `scripts/check_gui_real.py` —— 用真 PySide6 建窗渲染并截图(缺依赖时自行"跳过") - `scripts/viz3d_smoke.py` —— 离屏出一张三维图,验 viz3d + VTK 渲染链 - `scripts/check_db.py` / `scripts/check_scene_run.py` —— 达梦8 实机:DAO 全链路读写、 以及「库内基线 → 装配九段式 → 溯源体检 → 真推演 → 写库回读 → 清理」端到端 - `scripts/check_imaging.py` / `scripts/check_ir_wide.py` —— 载荷 → 传感器 → 七步链 → DN 帧; 宽幅对地(海陆 + 云) - `scripts/check_satellite_anim.py` 与 `check_gui_sim_start.py` / `check_gui_ir_wide.py` / `check_gui_imaging_sat.py` / `check_gui_sim_imaging.py` / `check_gui_scene_run.py` —— 界面级:真窗口点按钮,验卫星逐帧摆位、成像页三条取数路径、以及 「落库场景 + 跑次 → 影像序列」这条联动链(后者断言**有效帧 > 0**) 也可手工逐模块跑(每个模块都自带 `assert` 自检): ```powershell $py = "C:\Users\admin\.dsh\dsh-runtimes\dsh-primary-runtime\dependencies\python\python.exe" foreach ($m in @( "core.time_ref.state", "core.orbits.twobody", "core.orbits.j2", "core.orbits.hpop", "core.orbits.maneuver", "core.orbits.sgp4_prop", "core.constellation.walker", "core.coverage.access", "core.scheduling.handover", "core.radiation.planck", "core.atmosphere.lut", "core.sensor.model", "core.ir_imaging.chain", "core.ballistic.state_machine", "core.attitude.pointing", "core.navigation.icd", "core.navigation.dop", "core.comm.antenna", "core.comm.link_budget", "core.environment.atmosphere_density", "core.environment.geomagnetic", "core.environment.radiation_belt", "core.environment.debris", "core.collision.conjunction", "core.collision.probability", "models.glb_check", "models.physical", "models.library", "viz3d.earth", "viz3d.orbit", "viz3d.gltf", "viz3d.cone", "viz3d.scene", "persistence.dao", "engine.scene", "engine.timeline", "engine.packaging", "engine.runner", "ui._selfcheck" )) { & $py -m $m } & $py scripts\check_gui_launch.py # 启动界面主线(桩 PySide6) ``` 全量编译检查:`& $py -m compileall -q core engine models persistence viz3d ui scripts main.py` 环境与依赖探测:`python main.py --version`(双击 `start.bat` 亦同) **环境依赖现状**(本机实测) | 包 | 状态 | 影响 | |---|---|---| | numpy | 已装 | 物理内核可完整自检 | | python-docx | 已装 | 报告导出可用 | | PySide6 / vtk / trimesh | 未装 | 界面与三维层只验证接线,启动时给明确缺依赖提示 | | sgp4 | 未装 | TLE 递推抛明确中文错误,提示改用 HPOP | | scipy / astropy / matplotlib / sqlalchemy | 未装 | 已用 numpy/标准库等价实现替代;持久层未实机连库 | > 说明:`core/` 只用 numpy 与标准库即可完整运行,这是刻意的 —— 物理内核要能独立交付、 > 独立测试,不被上层依赖拖累。 ## 二·五、用量统计(可选) ```powershell python scripts/usage_report.py # 当前工作区 python scripts/usage_report.py --all # 全部会话 ``` 统计的是 **token 数**(未缓存输入 / 缓存读取 / 输出)。积分余额与账单金额不在本地任何 文件里,脚本不会去猜一个货币数字,请到 DeepSeek 控制台或客户端用量面板查看。 --- ## 三、下一步计划 - **v1.2**:达梦8 实机连库 —— 十五表建库 + 九段式场景读写 + 版本回退 + 大文件索引落库验证。 - **v1.3**:装 PySide6 / VTK / trimesh 后实机联调三维与界面(VTK 地球/轨道/视锥/弹道动画)。 当前这三个依赖缺失,`viz3d.scene` 的渲染路径只用内存桩验证过,需在真机复跑 `python -m viz3d.scene`。 - **v1.4**:成像链挂真实大气 LUT(QRadtran / CART 数据转换脚本),30 项参数逐项溯源。 - **v1.5**:报告自动装配(`python-docx`)+ 指标对照判据 + 影像抽帧。 - **v1.6**:补 `_TODO` 数据(BDS MEO 面数/相位因子、GEO/IGSO 定点经度、各系统 RAAN 参考), 并人工比对 USSA1976 / CIRA 大气表与 `igrf13coeffs.txt`。 - **v1.7**:三平台打包(Windows x64 / 麒麟 / 统信 ARM64),龙芯绿色部署。 **已知缺口**(明确没做,不是遗漏) | 缺口 | 现状 | 影响 | |---|---|---| | 达梦8 未实机连接 | 只验证到 DDL 与 JSON Schema 结构、DAO 的纯标准库部分 | 建库与读写需实机复跑 | | 大气 LUT 无实测数据 | `qradtran_lut`/`cart_lut`/`hapi_lbl` 无数据时**明确报错** | 要真实大气结果需先挂 LUT | | 辐射带 / 碎片无通量数据 | 只定义加载接口,缺数据即 `LookupError` | 需要 AE8/AP8、ORDEM/MASTER 数据 | | 三维 / 界面未实机运行 | 环境无 VTK / PySide6,只验证接线与缺依赖提示 | 需装依赖后在真机验证渲染与交互 | | 星历 OEM/SP3 格式 | 未实现并明确报错 | 需要实测样例对齐格式规范 | | ICD 部分常数待补 | `_TODO` dict 逐系统标注 | 星座级 DOP 研究前必须补齐 | --- ## 四、如何参与 1. 先读 [AGENTS.md](AGENTS.md)。**所有代码编辑必须使用 `C:\Users\admin\.codebuddy\skills\ponytail-4.10.0` 下的技能**(阶梯决策:YAGNI → 复用 → 标准库 → 原生 → 已装依赖 → 一行 → 最小实现)。 2. 目录与 Python 包一律英文小写下划线;中文只出现在文档、界面标题与注释里。 3. 真实数据必须带 `value`/`source` 溯源对,**禁止编造数据**。缺数据时明确报错,不要返回估算值。 4. 非平凡逻辑必须留一个可运行校验(`assert` 自检即可,不引测试框架)。 5. 改完代码跑一遍上面的验证清单,并把改动补记到本文件的版本记录里。