"""
三体问题动力学模块
包含通用 Dynamics 基类和 CR3BP_Dynamics 类,用于计算和积分圆型限制性三体问题的动力学方程。
物理背景
--------
在圆型限制性三体问题 (CR3BP) 中,两个主天体(如地球和月球)绕其公共质心做圆周运动,
第三体(航天器)质量小到不影响两个主天体的运动。采用以质心为原点的旋转坐标系,
使得两个主天体固定在 x 轴上。
坐标系约定:
- 原点:系统质心
- x 轴:从质心指向较大天体(质量 1-μ)的方向
- 较大天体位于 x = -μ,较小天体(质量 μ)位于 x = 1-μ
- y 轴在轨道平面内垂直于 x 轴
- z 轴与 x-y 平面正交
所有量均采用无量纲化单位(距离单位 DU = 主天体间距,时间单位 TU 使主天体角速度为 1)。
重构说明 (v4.0 MBSE)
--------------------
采用 Template Method 模式统一传播逻辑:
- 基类 ``Dynamics`` 拥有 ``propagate()`` 模板方法
- 子类覆写 ``_get_eom_func()`` 和 ``_get_max_step()`` 钩子方法
- 结果提取逻辑统一在基类中,保证 states 形状始终为 ``(n_points, 6)``
"""
from __future__ import annotations
from collections.abc import Callable
from typing import TYPE_CHECKING, Any
import numpy as np
import numpy.typing as npt
from scipy.integrate import solve_ivp
from .system import CR3BP_System
if TYPE_CHECKING:
from .orbit import Orbit
[文档]
class Dynamics:
"""通用天体系统动力学基类
采用 Template Method 模式:基类定义 ``propagate()`` 的算法骨架,
子类通过钩子方法提供具体的 ODE 函数和步长配置。
契约(对应 MBSE REQ-002):
- ``propagate()`` 返回的 ``states`` 形状始终为 ``(n_points, 6)``
- ``stm``(如果存在)形状为 ``(n_points, 6, 6)``
Attributes:
system: 关联的系统对象
integrator: 数值积分器类型
rtol: 相对积分容差
atol: 绝对积分容差
max_step: 最大积分步长
last_trajectory: 最近一次积分的轨迹 [t, y]
last_stm: 最近一次积分的状态转移矩阵
cross_section_tolerance: 截面检测容差
last_crossing: 上次穿过截面的点和时间
initialized: 初始化完成标志
"""
DEFAULT_TOLERANCE = 1e-12 # 默认积分容差,双精度机器精度量级,确保数值解精度
DEFAULT_MAX_STEP = 0.01 # 无量纲(CR3BP),EphemerisDynamics 覆写为秒
STATE_DIM = 6 # 状态向量维度 [x, y, z, vx, vy, vz]
STM_DIMENSION = STATE_DIM + STATE_DIM * STATE_DIM # 42 = 6 + 36
[文档]
def __init__(self, system: Any) -> None:
"""初始化动力学
Args:
system: 系统对象(CR3BP_System 或 EphemerisSystem)
"""
self.system = system
# --- 积分器配置 ---
self.integrator: str = "RK45"
self.rtol: float = self.DEFAULT_TOLERANCE
self.atol: float = self.DEFAULT_TOLERANCE
self.max_step: float = self.DEFAULT_MAX_STEP
# 缓存最近一次积分结果
self.last_trajectory: tuple[np.ndarray, np.ndarray] | None = None
self.last_stm = None # STM 矩阵数组
# 截面检测参数
self.cross_section_tolerance = 1e-8
self.last_crossing = None
self.initialized = True
def _get_eom_func(self, with_stm: bool) -> Callable:
"""获取运动方程函数(钩子方法,子类可覆写)
Args:
with_stm: 是否需要 STM 版本的运动方程
Returns:
ODE 右端函数
"""
if with_stm:
raise NotImplementedError("子类须实现 equations_with_stm 或覆写 _get_eom_func")
return self.equations_of_motion
def _get_max_step(self, t_span: tuple[float, float]) -> float:
"""获取当前传播的最大步长(钩子方法,子类可覆写)
默认实现直接返回 self.max_step。子类(如 EphemerisDynamics)
可覆写此方法以实现自适应步长。
Args:
t_span: 积分时间区间
Returns:
最大步长
"""
return self.max_step
[文档]
def equations_of_motion(
self, t: float, state: npt.NDArray[np.floating]
) -> npt.NDArray[np.floating]:
"""运动方程(子类需实现)
Args:
t: 时间
state: 状态向量
Returns:
状态导数
Raises:
NotImplementedError: 子类未实现此方法
"""
raise NotImplementedError("子类必须实现此方法")
[文档]
def propagate(
self,
initial_state: npt.ArrayLike,
t_span: tuple[float, float],
t_eval: npt.ArrayLike | None = None,
with_stm: bool = False,
with_jacobi: bool = False,
) -> dict[str, Any]:
"""传播轨迹(Template Method)
统一的传播入口,保证:
- states 形状为 (n_points, 6)(REQ-002)
- stm 形状为 (n_points, 6, 6)(当 with_stm=True 时)
- time 数组单调递增
Args:
initial_state: 初始状态向量
t_span: 时间区间 [t0, tf]
t_eval: 评估时间点数组(可选)
with_stm: 是否计算状态转移矩阵
with_jacobi: 是否沿轨迹逐点计算 Jacobi 常数
Returns:
轨迹结果字典,包含 ``time`` 和 ``states`` 键;
当 ``with_stm=True`` 时额外包含 ``stm`` 键;
当 ``with_jacobi=True`` 时额外包含 ``jacobi`` 与 ``jacobi_error`` 键
"""
initial_state = np.asarray(initial_state, dtype=float)
max_step = self._get_max_step(t_span)
if with_stm:
return self._propagate_with_stm(initial_state, t_span, t_eval, max_step, with_jacobi)
else:
return self._propagate_state_only(initial_state, t_span, t_eval, max_step, with_jacobi)
def _propagate_with_stm(
self,
initial_state: np.ndarray,
t_span: tuple[float, float],
t_eval: npt.ArrayLike | None,
max_step: float,
with_jacobi: bool,
) -> dict[str, Any]:
"""增广状态积分(含 STM)
初始 STM 设为单位矩阵,拼接为 STATE_DIM + STATE_DIM² 维增广状态后积分。
"""
initial_stm = np.eye(self.STATE_DIM).flatten()
augmented_state = np.concatenate([initial_state, initial_stm])
eom_func = self._get_eom_func(with_stm=True)
result = solve_ivp(
eom_func,
t_span,
augmented_state,
method=self.integrator,
t_eval=t_eval,
rtol=self.rtol,
atol=self.atol,
max_step=max_step,
)
# 从增广结果中分离状态和 STM
n = self.STATE_DIM
states = result.y[:n, :].T
stm_matrices = result.y[n:, :].T.reshape(-1, n, n)
self.last_trajectory = (result.t, states)
self.last_stm = stm_matrices
out: dict[str, Any] = {
"time": result.t,
"states": states,
"stm": stm_matrices,
}
if with_jacobi:
out = self._handle_jacobi(states, out)
return out
def _propagate_state_only(
self,
initial_state: np.ndarray,
t_span: tuple[float, float],
t_eval: npt.ArrayLike | None,
max_step: float,
with_jacobi: bool,
) -> dict[str, Any]:
"""纯状态积分(不含 STM)"""
eom_func = self._get_eom_func(with_stm=False)
result = solve_ivp(
eom_func,
t_span,
initial_state,
method=self.integrator,
t_eval=t_eval,
rtol=self.rtol,
atol=self.atol,
max_step=max_step,
)
# result.y 形状为 (6, n_points),转置为 (n_points, 6) — REQ-002
states = result.y.T
self.last_trajectory = (result.t, states)
out: dict[str, Any] = {
"time": result.t,
"states": states,
}
if with_jacobi:
out = self._handle_jacobi(states, out)
return out
def _handle_jacobi(self, states: np.ndarray, out: dict[str, Any]) -> dict[str, Any]:
"""沿轨迹计算 Jacobi 常数的钩子方法。
基类默认为 no-op。CR3BP_Dynamics 覆写此方法以计算 Jacobi 常数。
EphemerisDynamics 继承 no-op(N 体问题无 Jacobi 积分)。
Args:
states: 状态序列,形状 (n, 6)
out: 输出字典
Returns:
更新后的输出字典(基类直接返回不修改)
"""
return out
[文档]
def compute_jacobi_constant(self, state: npt.ArrayLike) -> float:
"""计算能量常数(子类需实现)
Args:
state: 状态向量
Returns:
能量常数
Raises:
NotImplementedError: 子类未实现此方法
"""
raise NotImplementedError("子类必须实现此方法")
[文档]
def check_cross_section(self, state: npt.ArrayLike, plane: str, value: float) -> bool:
"""检查是否穿过指定截面
Args:
state: 状态向量
plane: 截面平面 ('x', 'y', 'z')
value: 平面值
Returns:
是否穿过截面
Raises:
ValueError: 无效的平面参数
"""
state = np.asarray(state, dtype=float)
if plane == "x":
return abs(state[0] - value) < self.cross_section_tolerance
elif plane == "y":
return abs(state[1] - value) < self.cross_section_tolerance
elif plane == "z":
return abs(state[2] - value) < self.cross_section_tolerance
else:
raise ValueError(f"无效的平面: {plane}。可用平面: 'x', 'y', 'z'")
def __str__(self):
return f"{self.__class__.__name__}(system={self.system})"
def __repr__(self):
return (
f"{self.__class__.__name__}("
f"system={self.system}, integrator='{self.integrator}', rtol={self.rtol})"
)
[文档]
class CR3BP_Dynamics(Dynamics):
"""CR3BP动力学方程
封装了CR3BP的动力学模型,提供状态传播、状态转移矩阵计算、
Jacobi常数计算等核心功能。支持6维状态向量(位置+速度)和
42维增广状态向量(状态+状态转移矩阵)的数值积分。
CR3BP 运动方程(旋转坐标系中):
ẍ - 2ẏ = ∂Ω/∂x
ÿ + 2ẋ = ∂Ω/∂y
z̈ = ∂Ω/∂z
其中 Ω 为伪势能(见 equations_of_motion 方法的详细注释),
等号左侧的 2ẏ、-2ẋ 项为科里奥利力(Coriolis),伪势能中
已包含离心力项 x²/2 + y²/2。
"""
[文档]
def __init__(self, system: CR3BP_System) -> None:
"""初始化CR3BP动力学
Args:
system: CR3BP_System对象,包含质量参数μ等系统常数
"""
super().__init__(system)
# Jacobi 常数监测(仅 CR3BP 有定义,不在基类中)
self.jacobi_history: list[float] = []
self.jacobi_error: float = 0.0
def _get_eom_func(self, with_stm: bool) -> Callable:
"""返回 CR3BP 运动方程函数"""
if with_stm:
return self.equations_with_stm
return self.equations_of_motion
[文档]
def equations_of_motion(
self, t: float, state: npt.NDArray[np.floating]
) -> npt.NDArray[np.floating]:
"""6维状态向量的运动方程
实现 CR3BP 在旋转坐标系中的运动方程。旋转坐标系以两个主天体的
公共质心为原点,与主天体同步旋转(角速度 ω = 1),因此两个主天体
在坐标系中固定不动。
在旋转坐标系中,运动方程为:
ẍ - 2ẏ = ∂Ω/∂x (x 方向:离心力 + 引力 + 科里奥利力)
ÿ + 2ẋ = ∂Ω/∂y (y 方向:离心力 + 引力 + 科里奥利力)
z̈ = ∂Ω/∂z (z 方向:仅引力,无科里奥利力)
伪势能 Ω = (x² + y²)/2 + (1-μ)/r₁ + μ/r₂,其偏导数为:
∂Ω/∂x = x - (1-μ)(x+μ)/r₁³ - μ(x-1+μ)/r₂³
∂Ω/∂y = y - (1-μ)y/r₁³ - μy/r₂³
∂Ω/∂z = - (1-μ)z/r₁³ - μz/r₂³
因此加速度各项的物理含义:
- "x" / "y" 项:离心力(伪势能中的二次项贡献)
- "(1-μ)(x+μ)/r₁³" 等:较大天体(如地球)的引力加速度
- "μ(x-1+μ)/r₂³" 等:较小天体(如月球)的引力加速度
- "2vy" / "-2vx":科里奥利力(旋转坐标系中的虚拟力)
Args:
t: 时间(旋转坐标系中,CR3BP方程不显含时间,即自治系统)
state: 状态向量 [x, y, z, vx, vy, vz]
Returns:
状态导数 [vx, vy, vz, ax, ay, az]
"""
mu = self.system.mu # 质量参数 μ = m₂/(m₁+m₂),m₂ 为较小天体质量
x, y, z, vx, vy, vz = state
# r₁:航天器到较大天体(质量 1-μ,位于 x=-μ)的距离
r1 = np.sqrt((x + mu) ** 2 + y**2 + z**2)
# r₂:航天器到较小天体(质量 μ,位于 x=1-μ)的距离
r2 = np.sqrt((x - 1 + mu) ** 2 + y**2 + z**2)
# --- x 方向加速度 ---
ax = 2 * vy + x - (1 - mu) * (x + mu) / r1**3 - mu * (x - 1 + mu) / r2**3
# --- y 方向加速度 ---
ay = -2 * vx + y - (1 - mu) * y / r1**3 - mu * y / r2**3
# --- z 方向加速度 ---
az = -(1 - mu) * z / r1**3 - mu * z / r2**3
return np.array([vx, vy, vz, ax, ay, az])
[文档]
def compute_jacobian_A(self, state: npt.NDArray[np.floating]) -> np.ndarray:
"""计算 CR3BP 状态方程的雅可比矩阵 A(t)
A(t) 是 6x6 矩阵,满足 dΦ/dt = A(t)·Φ。
结构为:
| 0₃ₓ₃ I₃ₓ₃ | 位置方程的雅可比:∂(v)/∂(r,v) = [0, I]
| U_ij Ω | 速度方程的雅可比:∂(a)/∂(r,v) = [U, Ω]
此方法提取自 equations_with_stm,供 Continuation 等模块复用(REQ-103)。
Args:
state: 状态向量 [x, y, z, vx, vy, vz]
Returns:
6x6 雅可比矩阵 A
"""
mu = self.system.mu
x, y, z = state[0], state[1], state[2]
r1 = np.sqrt((x + mu) ** 2 + y**2 + z**2)
r2 = np.sqrt((x - 1 + mu) ** 2 + y**2 + z**2)
# 伪势能 Hessian 矩阵元素 U_ij = ∂²Ω/∂rᵢ∂rⱼ
U_xx = (
1
- (1 - mu) * (1 / r1**3 - 3 * (x + mu) ** 2 / r1**5)
- mu * (1 / r2**3 - 3 * (x - 1 + mu) ** 2 / r2**5)
)
U_yy = 1 - (1 - mu) * (1 / r1**3 - 3 * y**2 / r1**5) - mu * (1 / r2**3 - 3 * y**2 / r2**5)
U_zz = -(1 - mu) / r1**3 - mu / r2**3
U_xy = 3 * (1 - mu) * (x + mu) * y / r1**5 + 3 * mu * (x - 1 + mu) * y / r2**5
U_xz = 3 * (1 - mu) * (x + mu) * z / r1**5 + 3 * mu * (x - 1 + mu) * z / r2**5
U_yz = 3 * (1 - mu) * y * z / r1**5 + 3 * mu * y * z / r2**5
A = np.array(
[
[0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 1],
[U_xx, U_xy, U_xz, 0, 2, 0],
[U_xy, U_yy, U_yz, -2, 0, 0],
[U_xz, U_yz, U_zz, 0, 0, 0],
]
)
return A
[文档]
def equations_with_stm(
self, t: float, augmented_state: npt.NDArray[np.floating]
) -> npt.NDArray[np.floating]:
"""42维增广状态向量的运动方程(包含状态转移矩阵)
同时积分状态向量和状态转移矩阵(STM),满足 dΦ/dt = A(t)·Φ。
状态转移矩阵 Φ(t, t₀) 将初始状态的微小扰动映射到当前时刻:
δx(t) = Φ(t, t₀) · δx(t₀)
通过将 Φ 拉伸为 36 维向量并与 6 维状态拼接为 42 维增广状态,
可以用标准的 ODE 积分器同时求解轨道和 STM。
Args:
t: 时间
augmented_state: 增广状态向量 [6状态 + 36个STM元素]
Returns:
增广状态导数
"""
state = augmented_state[:6]
stm = augmented_state[6:].reshape((6, 6))
state_derivative = self.equations_of_motion(t, state)
A = self.compute_jacobian_A(state)
stm_dot = A @ stm
return np.concatenate([state_derivative, stm_dot.flatten()])
[文档]
def propagate_orbit_state_at_time(
self,
orbit: Orbit,
t: float,
integration_dt: float = 0.01,
) -> npt.NDArray[np.floating]:
"""从轨道首点状态积分到给定时刻对应的相位(周期轨道上对周期取模)
利用周期轨道的周期性,将目标时间对周期取模后从轨道起始状态
重新积分,得到该相位处的精确状态。
Args:
orbit: 周期轨道数据(须含 ``states``、``times``、有效 ``period``)
t: 与轨道 ``times`` 一致的时间坐标(绝对时间)
integration_dt: 构造 ``t_eval`` 的步长
Returns:
积分末端状态 ``[x, y, z, vx, vy, vz]``
Raises:
ValueError: 轨道无状态或周期无效
"""
if orbit.states.shape[0] < 1:
raise ValueError("轨道无状态")
if orbit.period is None or orbit.period <= 0:
raise ValueError("轨道周期无效,无法沿周期外推")
t0 = float(orbit.times[0])
period = float(orbit.period)
t_rel = float(np.mod(t - t0, period))
if t_rel < 1e-14:
return np.asarray(orbit.states[0], dtype=float).copy()
n_steps = max(int(np.ceil(t_rel / integration_dt)) + 1, 2)
t_eval = np.linspace(t0, t0 + t_rel, n_steps)
result = self.propagate(
initial_state=orbit.states[0],
t_span=(t0, t0 + t_rel),
t_eval=t_eval,
with_stm=False,
with_jacobi=False,
)
return np.asarray(result["states"][-1], dtype=float)
[文档]
def compute_state_transition_matrix(
self, initial_state: npt.ArrayLike, t: float
) -> npt.NDArray[np.floating]:
"""计算状态转移矩阵
Args:
initial_state: 初始状态向量
t: 积分终止时间
Returns:
状态转移矩阵 (6x6)
"""
result = self.propagate(initial_state, (0.0, float(t)), with_stm=True, with_jacobi=False)
return result["stm"][-1]
[文档]
def compute_jacobi_constant(self, state: npt.ArrayLike) -> float:
"""计算Jacobi常数
Args:
state: 状态向量 [x, y, z, vx, vy, vz]
Returns:
Jacobi常数
"""
return self.system.get_jacobi_constant(state)
def _handle_jacobi(self, states: np.ndarray, out: dict[str, Any]) -> dict[str, Any]:
"""沿轨迹逐点计算 Jacobi 常数"""
self.jacobi_history = [self.compute_jacobi_constant(state) for state in states]
if len(self.jacobi_history) > 1:
self.jacobi_error = float(np.max(np.abs(np.diff(self.jacobi_history))))
else:
self.jacobi_error = 0.0
out["jacobi"] = self.jacobi_history
out["jacobi_error"] = self.jacobi_error
return out
def __str__(self):
return f"CR3BP_Dynamics(system={self.system}, integrator='{self.integrator}')"
def __repr__(self):
return (
f"CR3BP_Dynamics(system={self.system}, integrator='{self.integrator}', "
f"rtol={self.rtol}, atol={self.atol}, max_step={self.max_step})"
)
[文档]
def propagate_state_at_orbit_time(
orbit: Any,
t: float,
dynamics: CR3BP_Dynamics,
integration_dt: float = 0.01,
) -> npt.NDArray[np.floating]:
"""委托 :meth:`CR3BP_Dynamics.propagate_orbit_state_at_time`,便于顶层导入兼容
Args:
orbit: 周期轨道数据
t: 目标时间(绝对时间)
dynamics: CR3BP动力学对象
integration_dt: 积分步长
Returns:
积分末端状态
"""
return dynamics.propagate_orbit_state_at_time(orbit, t, integration_dt)