System 与 Dynamics:两棵类层次的数据流
e2m2e/algorithm/dynamics/ 下有两棵类层次:System 一侧描述"这是什么系统"
(μ、特征尺度、平动点、星历、单位、坐标系),Dynamics 一侧描述"怎么把它积分
出来"(积分器、容差、STM、事件、结果缓存)。包 docstring 概括为"System(数据
上下文)+ Dynamics(传播编排)"(e2m2e/algorithm/dynamics/__init__.py:1)。
本文按数据流动的方向,把这两个家族各自是什么、谁读它们的什么成员、一次传播里
数据走哪条路,逐一讲清楚。文中所有行号以当前代码为准。
从一次传播说起
先看最常见的一个场景:传播一条地月 CR3BP 轨迹。测试套件里它的标准形态是 (tests/conftest.py:13-25):
system = CR3BP_System(mu=Datum.DE421.mu, primary="Earth", secondary="Moon")._with_default_scales()
dynamics = CR3BP_Dynamics(system=system)
result = dynamics.propagate(state0, (0.0, 6.3))
数据在这条链上走四段路。
第一段,构造 System。 CR3BP_System.__init__ 只收质量参数 μ、两个天体名和
可选的天体半径,特征长度、特征时间、五个平动点全部置为 None
(e2m2e/algorithm/dynamics/cr3bp_system.py:71-128)。此时的系统还不能参与计算:
DU/TU/VU 属性在尺度未设时抛"系统未初始化"
(cr3bp_system.py:194-212)。_with_default_scales() 按天体对补上特征尺度
(cr3bp_system.py:130-160),平动点则推迟到首次需要时才解算:
get_libration_point 发现没算过就先调 compute_libration_points
(cr3bp_system.py:293-317)。System 的构造因而是两段式的:先定"哪个系统",
再定"用什么尺度量化它"。
第二段,构造 Dynamics。 CR3BP_Dynamics(system) 做的事很轻:存下 system
引用,填一套默认积分器配置(RK45、rtol/atol 1e-12、max_step 0.01),把结果缓存
置空(e2m2e/algorithm/dynamics/dynamics.py:78-97、544-553)。它不复制系统的任何
参数。μ 在每次算加速度时经 self.system.mu 现取(dynamics.py:593)。
第三段,propagate。 无事件时 CR3BP 走 Rust 快速路径,Python 侧只把 mu、
时间区间、初值和积分配置这组标量传过 FFI(dynamics.py:851-859);轨迹在 Rust
侧算完,以 {"time", "states"} 字典回来,同时写进 self.last_trajectory 缓存
(dynamics.py:861-870)。这一段的细节见后文"propagate 内部"一节。
第四段,结果进数据容器。 设计链路把返回字典装进 Orbit,并把 system 引用
一并塞进去:Orbit(states=result["states"], times=result["time"], system=dynamics.system)(e2m2e/algorithm/design/design_orbit.py:550)。从此这组
浮点数带着自己的单位与坐标系解释者旅行;之后谁想再做计算,可以从
orbit.system 重新长出一个 dynamics,StabilityAnalysis 正是这样按需重建的
(e2m2e/algorithm/stability.py:95-98)。
四段路合起来就是两个家族的分工:System 是长期持有的模型上下文,Dynamics 是 围着它转的、带配置与缓存的传播者。
System:模型上下文
基类只承诺三个成员
System ABC 定义的最小接口只有三项:frame(坐标框架)、unit_system
(单位系统)、gravitational_parameter(body)(引力参数)
(e2m2e/algorithm/dynamics/system.py:15-51)。docstring 点名了什么刻意不进基类:
mu、body_state(body, t)、coordinate_system 属于特定实现的概念
(system.py:25-27)。基类另有一个非抽象的 get_body_position,默认抛
NotImplementedError:星历专属能力,放在基类只是给一个明确的报错位置
(system.py:54-66)。
接口薄是有后果的:想对两种系统多态的代码,只能依赖这三个成员,其余都得用
getattr/hasattr 现探。下文"消费面"一节会看到这是真实发生的模式。
CR3BP_System:无量纲、自治、两段式初始化
构造注入五样东西:mu、主/次天体名、可选的主/次天体半径
(cr3bp_system.py:71-80)。构造时做两道校验:半径若给必须为正
(cr3bp_system.py:93-98),μ 必须落在 (0, 0.5)(cr3bp_system.py:102-106)。
构造完成后还有两步初始化可选:set_characteristic_scales(distance, period) 由
距离与周期推出特征长度/时间/速度并置 is_initialized(cr3bp_system.py:214-235);
compute_libration_points() 用 fsolve 解三个共线点、解析给出两个三角点
(cr3bp_system.py:237-291,其中 264-266 数值解、268-274 解析解)。
对基类三成员,它的回答是:会合(旋转)坐标系、无量纲单位、约定总质量为 1 时
primary 的 GM 是 1 - mu、secondary 的是 mu(cr3bp_system.py:163-191)。
初始化之后,这个对象向外提供四类数据:
特征尺度:
DU(km)、TU(天)、VU(m/s)三个属性,外加characteristic_length/time/velocity原始字段(cr3bp_system.py:108-110、194-212)。平动点:
L1–L5与L_points字典(cr3bp_system.py:112-117)。Jacobi 常数:
get_jacobi_constant(state),Parker 约定 (cr3bp_system.py:319-354)。单位换算与稳定性:
dimensionless_to_physical/physical_to_dimensionless(cr3bp_system.py:356-396)、compute_stability_index(cr3bp_system.py:398-442)。
半径字段(primary_radius_km/secondary_radius_km)本身不参与动力学,是碰撞
检测的数据来源,流向见"事件与碰撞"小节。
EphemerisSystem:SPICE 查询的统一入口
构造注入五样东西:天体名列表、已完成内核加载的 SPICEManager、参考原点
(默认 "EARTH")、坐标框架(默认 J2000)、可选的 CoordinateSystem
(e2m2e/algorithm/dynamics/ephemeris_system.py:35-55)。对基类三成员,它的回答
是:构造时给定的框架、物理单位、GM 直通 spice.get_gm
(ephemeris_system.py:59-103)。
与 CR3BP 的两段式初始化不同,这里的"第二步"是给 coordinate_system 赋值,且
发生在构造之后、经 property setter 完成(ephemeris_system.py:69-75)。编排层
的标准写法是先构造再补:system = EphemerisSystem(...) 接着
system.coordinate_system = CoordinateSystem(...)
(e2m2e/algorithm/propagation.py:118-123)。力模型传播依赖这个字段,不设会被
ForceModel 拒绝(e2m2e/algorithm/forces/force_model.py:57-58)。
星历数据经四个查询方法流出:gravitational_parameter/get_gm(单体 GM)、
get_gm_values(按 bodies 顺序的 GM 数组)、get_body_position(位置)、
get_body_state(六维状态)(ephemeris_system.py:94-150)。另有一个
update_coordinate_systems(t, state) 用于推进动态坐标系
(ephemeris_system.py:77-92);ForceModel 传播下沉 Rust 后,逐步更新由 Rust
内部完成,e2m2e/ 内已无调用点,仅测试保留
(tests/numerical/forces/container/test_force_model_dynamic_axes.py:143-186)。
BCR4BPSystem:继承 CR3BP_System,叠加太阳
BCR4BPSystem 继承 CR3BP_System
(e2m2e/algorithm/dynamics/bcr4bp_system.py:23),构造时多收四个太阳参数:
sun_mass、sun_distance、sun_angular_rate、sun_phase0;前两个缺省时按
DE440 常量和日地平均距离推导(bcr4bp_system.py:50-106,缺省推导在 89-92)。
sun_angular_rate 特殊:它依赖特征时间,直接构造不给时暂存 None,由
set_characteristic_scales 覆写方法按儒略年公转推导(bcr4bp_system.py:148-161);
因此标准入口是类方法 BCR4BPSystem.earth_moon(),一步完成构造与尺度设置
(bcr4bp_system.py:109-132)。
太阳位置不查星历,是时间 t 的解析函数 sun_position(t),即会合系里的共面圆周
(bcr4bp_system.py:163-183)。gravitational_parameter 在 "primary"/"secondary"
之外多接受 "sun"(bcr4bp_system.py:185-193)。注意 docstring 的提醒:BCR4BP 无
Jacobi 积分,compute_libration_points 给出的是对应 CR3BP 的平动点,仅作参考
位置(bcr4bp_system.py:41-42)。
Dynamics:传播编排者
基类持有的三类状态
Dynamics.__init__ 只收一个 system(dynamics.py:78-84),随后在实例上放三类
东西:
system 引用:
self.system,传播全程只读不写(dynamics.py:84)。积分器配置:
integrator、rtol、atol、max_step,默认值挂在类常量上 (dynamics.py:86-91)。这些是公开字段,调用方构造后直接改:并行搜索按任务 改容差与步长(e2m2e/algorithm/transfer/search_parallel.py:837-842),多重打靶 构造后立刻覆写三项(e2m2e/algorithm/solver/multiple_shooting.py:97-101),测试 fixture 为提速放宽星历传播的容差(tests/conftest.py:92-100)。结果缓存:
last_trajectory、last_stm,每次传播结束时覆写 (dynamics.py:93-94;写入点见后文)。CR3BP_Dynamics再加jacobi_history/jacobi_error两个 Jacobi 监测缓存(dynamics.py:552-553)。
也就是说,Dynamics 是有态工人:配置与最近一次的结果都留在实例上,供调用方
事后取(如 compute_state_transition_matrix 内部就是一次传播后取末态 STM,
dynamics.py:986-999)。
模板方法:propagate 是骨架,子类填钩子
propagate() 是模板方法:基类定算法骨架(参数规范化 → 校验 → 分发 → 结果
装配),子类经两组钩子参与:_get_eom_func(with_stm) 给出 ODE 右端,
_get_max_step(t_span) 给步长上限(dynamics.py:100-125)。分发只有两支:
_propagate_with_stm(42 维增广状态)与 _propagate_state_only(6 维)
(dynamics.py:223-229)。基类的两支实现都走 scipy solve_ivp;三个子类各把
这两支覆写为 Rust 快速路径优先(事件场景的处理因子类而异,见后文)。
三个子类,两种继承选择
CR3BP_Dynamics(dynamics.py:522)与 EphemerisDynamics
(e2m2e/algorithm/dynamics/ephemeris_dynamics.py:47)分别实现自治无量纲 CR3BP
方程与含时物理单位 N 体方程。BCR4BP_Dynamics 直接继承 Dynamics
(e2m2e/algorithm/dynamics/bcr4bp_dynamics.py:42),不继承 CR3BP_Dynamics,
尽管它的方程就是 CR3BP 加一项太阳摄动。代码里能看到这个选择的理由:
雅可比签名不同:CR3BP 自治,
compute_jacobian_A(state)(dynamics.py:611); BCR4BP 含时,compute_jacobian_A(t, state)(bcr4bp_dynamics.py:123)。Jacobi 语义反转:CR3BP_Dynamics 构造即带 Jacobi 监测缓存 (dynamics.py:552-553),而 BCR4BP 是时间周期系统、无 Jacobi 积分,必须把
compute_jacobi_constant与_handle_jacobi都实现为抛 NotImplementedError (bcr4bp_dynamics.py:457-463),继承来的能力要逐个点掉。STM 入口多一个参数:含时系统的 Φ 依赖起止时刻,
compute_state_transition_matrix多一个t0(bcr4bp_dynamics.py:438-455)。Rust 入口不同:BCR4BP 的传播函数要多带四个太阳参数 (bcr4bp_dynamics.py:293-303)。
继承 CR3BP_Dynamics 意味着几乎每个公开方法都要覆写、还要压掉 Jacobi 机制。
仓库里有过同型教训:ForceModel 一度形式上继承 Dynamics 只为复用几个数据属性,
实则全部重写 propagate 并对 STM/Jacobi 抛错,被认定为 LSP 违反(假继承)而
改为独立类(e2m2e/algorithm/forces/force_model.py:30-38)。
消费面:System 出了这个包,被谁读
分离是否承重,取决于 System 被多少不构造 Dynamics 的代码消费。逐个核实如下。
力模型侧:读 coordinate_system、spice、origin、gravitational_parameter
ForceModel 持有 system(类型标注就是 Any),构造时强制
system.coordinate_system 已设置(force_model.py:45-58);传播时把每个力模型
序列化为 Rust 元组(force.to_rust_spec(self.system)),并读 system.origin
作为 observer 传入(force_model.py:281-293);system.spice 是否存在被用作
"资源缺失还是能力缺失"的分流依据(force_model.py:226-233)。它从不构造
Dynamics,也明确不继承 Dynamics(force_model.py:30-38)。
PhysicalModel._resolve_mu(system) 是力模型侧对 System 最直接的消费:显式 μ
缺失时调 system.gravitational_parameter(self._body)
(e2m2e/algorithm/forces/physical_model.py:29-41)。ConicalShadowModel. flux_factor(t, state, system) 经 require_inertial_frame 从 system 取出
coordinate_system、spice、原点三件事,再查太阳与遮挡体位置算光照份额
(e2m2e/algorithm/forces/shadow.py:166-197;physical_model.py:69-84)。
转移与求解侧:读 origin 与 gravitational_parameter
低推力三件套都把 system 当参数包用。qlaw_guess(system, ...) 只从中解析中心体
μ:先查力模型里的 PointMassGravity,查不到再
system.gravitational_parameter(origin),origin 本身用 getattr 兜底成
"EARTH"(e2m2e/algorithm/transfer/qlaw.py:210、265-282)。
LowThrustShooting 与 LowThrustCollocation 构造时各做两件事:把力模型
to_rust_spec(system) 预序列化,把 origin 存为 observer
(e2m2e/algorithm/transfer/lowthrust_shooting.py:125-162;
e2m2e/algorithm/transfer/lowthrust_collocation.py:59-88)。
NormalFormContext 从 system 只取 μ,且是 getattr(system, "mu", None) 探测:
CR3BP_System 有就取,没有就回退固化常量
(e2m2e/algorithm/normal_form/context.py:64、114、196-201)。
坐标转换侧:读 mu、特征尺度、spice
SynodicJ2000System 持有 CR3BP_System 与 spice:逐点转换读 mu(质心平移)
与 characteristic_time(时间量纲化),批量路径把 mu 与时间单位两个标量传给
Rust(e2m2e/algorithm/coordinate/synodic_j2000.py:28-41、52、96-97)。
rho_bridge 的一组函数以 EphemerisSystem 为参:读 system.spice 构造
SynodicAxes、读 system.get_body_state("MOON", et) 取月球状态
(e2m2e/algorithm/coordinate/rho_bridge.py:47-63、110)。
轨道保持与预报:构造并配置 EphemerisSystem
control_orbit 构造十体 EphemerisSystem、赋 coordinate_system,然后交给
蒙特卡洛流程(e2m2e/algorithm/station_keeping/controller.py:228-247);工作进程里
按参数重建同一个系统(e2m2e/algorithm/station_keeping/monte_carlo.py:644-650),
另建一个仅带特征尺度的 CR3BP_System 供会合系转换用(monte_carlo.py:66-72、
707-716)。propagate_orbit 同样先构造 EphemerisSystem、补 coordinate_system,
再 ForceModel.from_config(force_config, system)(propagation.py:118-124)。
这两个模块消费 System,但传播由 ForceModel 完成,不经过 Dynamics。
其余散点
PoincareSection.periapsis(center, system) 读 mu 与主/次天体名来定截面中心
(e2m2e/algorithm/manifold/sections.py:202-228)。design_orbit 里同一个 CR3BP
system 实例同时喂给四路消费:构造 CR3BP_Dynamics、直接调
get_jacobi_constant、读 characteristic_time 做时间换算、构造
SynodicJ2000System 做坐标转换(design_orbit.py:983-987、1030-1032)。
按 System 抽象签名的多态函数
签名标注 system: System 的入口有:PhysicalModel._resolve_mu /
to_rust_spec(physical_model.py:29、45)、ConicalShadowModel.flux_factor
(shadow.py:170)、qlaw_guess 与 make_shooter_for_qlaw(qlaw.py:210、290)、
LowThrustShooting.__init__ 与 LowThrustCollocation.__init__
(lowthrust_shooting.py:125;lowthrust_collocation.py:59)、
NormalFormContext.__init__(normal_form/context.py:64)。
把这些入口实际读到的成员摊开看,真正对 CR3BP 与星历两种系统都成立的多态只
剩基类那一项:gravitational_parameter,它在 CR3BP 侧接受 "primary"/"secondary"
返回无量纲值,星历侧接受 SPICE 天体名返回 km³/s²(system.py:42-51 的 docstring
把这个双语义写明)。其余入口虽标着 System,实际读的都是实现侧成员
(origin、coordinate_system、spice、mu),靠 getattr/hasattr 兜底。
契约测试把双系统同一接口这件事固定下来:同一组断言跑在 CR3BP_System 与
EphemerisSystem 两个实现上(tests/algorithm/dynamics/test_system_contract.py:53-64)。
鸭子类型:名义 System,实际结构
类型标注之下,真实契约比注解更薄,三处实证:
数据层的
Orbit/OrbitFamily把 system 存为Any,docstring 明说"数据层 不依赖算法层",只用hasattr(system, "get_jacobi_constant")判断能力 (e2m2e/data/types/orbit.py:8-12、50、418-429)。低推力测试直接传
SimpleNamespace(origin="EARTH")当 system 用 (tests/algorithm/transfer/test_lowthrust_collocation.py:23; tests/algorithm/transfer/test_qlaw_failure.py:37)。RelativeDynamics.linear_model用 try/except 在compute_jacobian_A(t, state)与compute_jacobian_A(state)两种签名间适配 (e2m2e/algorithm/proximity/relative_dynamics.py:145-150)。
propagate 内部:一次调用里数据怎么动
入口编排
Dynamics.propagate 本体(dynamics.py:144-233)按固定次序处理输入:
initial_state转 ndarray,max_step经钩子取得(dynamics.py:199-200)。事件规范化:单个 callable 包成列表;空列表等价于无事件,直接置 None (dynamics.py:201-205)。
若开碰撞检测,先把碰撞事件造出来并入事件列表,同时检查初始状态是否已在 某天体半径内(dynamics.py:207-209)。
backend校验:只许 "scipy"/"rust",有事件时必须显式给,否则报错 (dynamics.py:211-214)。初始即在半径内的,短路返回单点轨迹加即时碰撞标记:scipy 事件不会对 g<0 的起点触发,必须显式处理(dynamics.py:217-221、474-493)。
按
with_stm分发到两支(dynamics.py:223-229)。若开碰撞检测,从结果的事件段提取碰撞信息(dynamics.py:231-232、495-510)。
scipy 与 Rust 两条后端路径
scipy 路径(基类实现):_propagate_state_only 拿 _get_eom_func(False)
的 ODE 右端调 solve_ivp,result.y 转置成 (n, 6);失败或空结果抛
PropagationFailure,不允许拿空轨迹伪装成功(dynamics.py:291-325)。
_propagate_with_stm 先把 6×6 单位阵展平拼成 42 维增广状态再积分,回来按
前 6 维/后 36 维拆开(dynamics.py:254-271)。两条 scipy 路径都在返回前写
last_trajectory(含 STM 时连 last_stm)(dynamics.py:273-274、327)。
Rust 路径(子类覆写):无事件时三个子类都要求 Rust 扩展可用,缺失即抛错、 不降级 scipy。传过 FFI 的东西逐类不同:
CR3BP:只有
mu一个系统参数,加时间区间、t_eval、初值、容差、步长 (dynamics.py:851-859;STM 版 dynamics.py:782-790)。BCR4BP:
mu加太阳四参数mu_sun/sun_distance/sun_angular_rate/sun_phase0(bcr4bp_dynamics.py:360-370;STM 版 293-303)。星历:
bodies、origin、gm_values三个从 system 现取的序列 (ephemeris_dynamics.py:124-126、133-142)。天体位置不经过 Python 对象传递: Rust 侧在积分内环直接查进程内 SPICE(先查星历缓存,未命中回退 cspice) (crates/e2m2e-spice/src/spk_accel.rs:46-56)。在这条链上,EphemerisSystem 的角色收敛为一个参数包。
Rust 路径回来后都做长度防御校验(返回点数不等于请求点数即抛错),再写
last_trajectory(dynamics.py:792-804、861-870)。
事件时的第三支:有事件且 backend="rust" 时,CR3BP/BCR4BP 走通用 Rust
积分器 solve_ivp_events,ODE 右端仍以 Python 回调形式传入,事件函数被
折算成 (g, terminal, direction) 三元组(dynamics.py:700-756、721-723;
bcr4bp_dynamics.py:225-271)。EphemerisDynamics 不支持事件:events 非 None
直接 NotImplementedError(ephemeris_dynamics.py:85-113、162-189)。
返回字典的键在何时出现
time/states 恒有。其余键的出现条件:
stm:with_stm=True时(dynamics.py:276-280)。status/cause:只在纯状态路径出现:基类 scipy 版与 CR3BP 的两个 Rust 纯状态分支(dynamics.py:329-334、872-877、926-931)。STM 路径、 EphemerisDynamics 与 BCR4BP_Dynamics 的 Rust 分支都不带这两个键。jacobi/jacobi_error:with_jacobi=True且为 CR3BP 时。基类_handle_jacobi是 no-op(dynamics.py:345-358),CR3BP_Dynamics 覆写为逐点 计算并写缓存(dynamics.py:1012-1022),BCR4BP 覆写为抛错 (bcr4bp_dynamics.py:461-463)。t_events/y_events:传了 events 时,逐事件的触发时刻与状态 (dynamics.py:282-284、336-338)。collision:collision_detection=True时,未碰撞为 None,否则是{"body", "t", "state"}(dynamics.py:231-232、507-509)。
事件与碰撞检测
事件函数是 scipy 语义的 g(t, state) -> float,可挂 terminal/direction
属性;PoincareSection.event(...) 是主要的构造者
(tests/algorithm/dynamics/test_events.py:21-26)。with_stm=True 时事件函数
收到的是 42 维增广状态(test_events.py:86-97)。
碰撞检测把"撞天体"翻译成终端事件:_collision_specs 从 system 读
primary_radius_km/secondary_radius_km(都没注入则报错)、读 mu 定两天体
在会合系的固定位置 [-μ,0,0] 与 [1-μ,0,0](dynamics.py:398-428);
_setup_collision_detection 再读 DU 把半径从 km 折成无量纲,构造
g = |r - center| - R 的 terminal 事件并追加到用户事件之后
(dynamics.py:447-472)。碰撞事件在事件列表末尾这一顺序被
_extract_collision 反向利用:按"列表尾部 n 个"索引回每个天体的触发记录
(dynamics.py:501-509)。这就是碰撞数据流全程读 system 的三个字段:半径、mu、
DU。
三条传播链的差异
CR3BP:无量纲、自治。EOM 不显含 t;时间区间、t_eval 都是无量纲量; 系统侧只贡献
mu。BCR4BP:在 CR3BP 右端上叠加太阳直接项与间接项,太阳位置由
system.sun_position(t)解析给出,EOM 显式含时 (bcr4bp_dynamics.py:72-121)。雅可比左下块在伪势能 Hessian 上再加太阳项 偏导(bcr4bp_dynamics.py:123-159)。无 Jacobi 积分。星历:物理单位(km、km/s、et 秒)、含时。EOM 对每个天体查 GM、对非原点 天体查星历位置,原点天体出中心项、其余出第三体摄动加间接项 (ephemeris_dynamics.py:235-293、256-291)。
max_step默认 60 秒且按传播 时长自适应收紧(ephemeris_dynamics.py:65-83)。Python 侧单步算加速度要逐 天体过 SPICE;Rust 快速路径把这个查询挪进 Rust 内环,Python 只递参数包。
这份分离保护了什么
一个 System 服务多个 Dynamics 与别的消费者
System 实例被共享的实证在测试与生产两侧都有。测试侧,BCR4BP 对照实验把坐标
转换器持有的 system 直接拿来再建一个动力学:
CR3BP_Dynamics(spice_syn_j2000.cr3bp_system)
(tests/algorithm/dynamics/test_bcr4bp_model.py:168)。生产侧,design_orbit 里
同一个 system 实例同时喂动力学、Jacobi 计算、时间换算、坐标转换四路
(design_orbit.py:983-1032);StabilityAnalysis 与不变流形各自从 orbit 上挂的
system 重建自己的 CR3BP_Dynamics(stability.py:95-98;
e2m2e/algorithm/manifold/manifolds.py:114)。反向的共享同样成立:同一
earth_moon_system fixture 上既可以挂 earth_moon_dynamics
(tests/conftest.py:13-25),也可以被任何只要系统参数的测试直接消费。
与之对照,Dynamics 的配置是按任务改的:同一个搜索任务族内,dynamics 构造后 被覆写 integrator/rtol/atol/max_step(search_parallel.py:837-842)。若配置留在 System 上,共享 system 的消费者之间会互相踩配置;分离让"模型参数"与"这一次 怎么积"各有其主。
多态缝与契约测试
System ABC 是 CR3BP 与星历两个世界之间唯一的类型级接缝,缝宽三个成员
(system.py:15-51)。消费面普查显示:真正跨缝多态的调用几乎都收敛到
gravitational_parameter 一项,其余访问都是 getattr/hasattr 探测实现侧成员。
两个契约测试文件把这个格局固定下来:test_system_contract.py 对两个实现跑
同一组接口断言(tests/algorithm/dynamics/test_system_contract.py:53-64),
test_dynamics_contract.py 对 propagate 的输出形状断言 (n, 6) 与 (n, 6, 6)
(tests/algorithm/dynamics/test_dynamics_contract.py:28-38)。
生命周期的差异
System 的寿命跟着数据走:Orbit 持有 system 引用,序列化再加载后引用还在
(orbit.py:50、253、275),稳定性分析、流形计算可以随时从它再长出 dynamics。
Dynamics 的寿命跟着任务走:构造、改配置、传播、读缓存,然后被丢弃;
_corrected_dro_cached 与 dro_corrector 两个 fixture 各自建独立的
system+dynamics 对,互不共享(tests/algorithm/conftest.py:62-88)。两类对象的
生命周期差一个量级,这是分离在运行时最直观的体现。
两侧的真实重叠
分离并不彻底,有三处已知的重叠,各有来由:
Jacobi 常数双入口。
CR3BP_System.get_jacobi_constant是定义所在 (cr3bp_system.py:319-354);CR3BP_Dynamics.compute_jacobi_constant一行委托 (dynamics.py:1001-1010),供_handle_jacobi在传播后逐点调用。只想要 Jacobi 值、不想碰传播的调用方走 system 侧(design_orbit.py:987; e2m2e/algorithm/family/axial_initial_guess.py:166)。测试断言两入口数值一致 (tests/algorithm/dynamics/test_cr3bp_model.py:66-69)。A 矩阵两处构造。
CR3BP_System.compute_stability_index内部拼一份 6×6 线性化矩阵用于平动点特征值分析(cr3bp_system.py:418-423);CR3BP_Dynamics.compute_jacobian_A拼同构矩阵供 STM 变分方程与延拓模块 复用(dynamics.py:611-638)。两者共用pseudo_potential_hessian(e2m2e/algorithm/dynamics/potential.py:14-58),Hessian 只有一份,拼装各 归各的语境:一个在"系统性质"里,一个在"传播配套"里。碰撞半径与 DU 的跨层读取。碰撞检测是 Dynamics 的职责,但半径存在 System 上、折无量纲要用 System 的
DU(dynamics.py:398-428、447-472)。 半径是天体属性而非积分配置,故数据留在 system,事件构造留在 dynamics。
附:文件地图
System 侧:
system.py(ABC)、cr3bp_system.py(CR3BP_System + LibrationPoint)、ephemeris_system.py(EphemerisSystem)、bcr4bp_system.py(BCR4BPSystem)。Dynamics 侧:
dynamics.py(Dynamics 基类 + CR3BP_Dynamics +propagate_state_at_orbit_time)、ephemeris_dynamics.py(EphemerisDynamics)、bcr4bp_dynamics.py(BCR4BP_Dynamics)。共享:
potential.py(伪势能 Hessian,供 CR3BP/BCR4BP 的雅可比与平动点 稳定性分析共用)。行为写照:
tests/algorithm/dynamics/(契约、事件、碰撞、变分方程)。