e2m2e.algorithm.station_keeping.monte_carlo 源代码

"""三轨道结构蒙特卡洛仿真驱动(《控制方案.md》§1.5/§1.7)。

误差仿真的三轨道结构(§1.5.1):

- **目标轨道**:标称轨道(FR1 设计产物),只作参照
- **真实轨道**:入轨时刻相对目标加测定轨扰动(式 5.38),以"实际"力
  模型外推,模拟探测器的真实状态
- **测量轨道**:每个控制时刻相对真实轨道加测定轨扰动(式 5.39),以
  "理论"力模型外推,控制量基于它计算

控制量施加于真实轨道;执行误差按式 5.40 分段(< Δv_min 不开机、小量
绝对误差、大量相对误差、> Δv_max 判失败);真实轨道与控制轨道的力模型
差异为表 5-3 双配置,其中光压误差每控制弧段重新抽样、弧段内固定(表
5-3 脚注)。蒙特卡洛对每个样本独立抽样运行(§1.7,DFH 惯例 100 次),
统计总 Δv、最大 Δv 与失败次数。

传播全部走 Rust 编译力模型(``propagate_compiled*``),控制律的 STM 由
42 维增广传播给出。蒙特卡洛的样本并行在 Python 进程池(CSPICE 全局状态非线程安全,rayon
不可用,见 ``spice_ffi.rs`` 模块注释)。
"""

from __future__ import annotations

from concurrent.futures import ProcessPoolExecutor
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

import numpy as np
import numpy.typing as npt

from ...data.constants import SECONDS_PER_DAY, Datum
from ...data.types import EphemerisTable, ManeuverTable, SKStatistic
from ..coordinate.coordinate_system import CoordinateSystem
from ..coordinate.standard_axes import ICRSAxes
from ..coordinate.standard_origins import CelestialBodyOrigin
from ..dynamics import EphemerisSystem
from ..forces import ForceModel
from .error_models import (
    BoxMullerSampler,
    NavigationErrorModel,
    SrpErrorModel,
    ThrustExecutionError,
)
from .special_point import SpecialPointLaw
from .target_point import LooseTargetPointLaw, StrictTargetPointLaw

__all__ = [
    "MonteCarloResult",
    "NominalOrbit",
    "RustPropagator",
    "SampleResult",
    "SingleSampleSimulation",
    "run_monte_carlo",
]


#: 地月系统质量参数与特征尺度(与 e2m2e/dfh/cr3bp_orbits.py 的
#: earth_moon_system 一致,保证会合系转换与 design 链路同约定)。
#: 自 ADR 0022 起统一使用 DE421 基准,废弃 1965 旧值。
EARTH_MOON_MU = Datum.DE421.mu
_CHAR_LENGTH_KM = Datum.DE421.char_length_km
_CHAR_PERIOD_SEC = 2 * 3.141592653589793 * Datum.DE421.char_time_s


def _earth_moon_system():
    """构造标准地月 CR3BP 系统(仅特征尺度,供会合系转换用)。"""
    from ..dynamics.cr3bp_system import CR3BP_System

    sys_ = CR3BP_System(mu=EARTH_MOON_MU, primary="Earth", secondary="Moon")
    sys_.set_characteristic_scales(_CHAR_LENGTH_KM, _CHAR_PERIOD_SEC)
    return sys_


def _utc_iso(eph: EphemerisTable, k: int) -> str:
    return (
        f"{eph.year[k]:04d}-{eph.month[k]:02d}-{eph.day[k]:02d}"
        f"T{eph.hour[k]:02d}:{eph.minute[k]:02d}:{eph.second[k]:06.3f}"
    )


[文档] class NominalOrbit: """标称轨道视图:``EphemerisTable`` + SPICE 时间轴,``state_at(t)`` 线性插值。 Attributes: t_start/t_end: 星历时间轴覆盖范围(et 秒) """ def __init__(self, ephemeris: EphemerisTable, spice: Any) -> None: et_all = np.array([spice.utc_to_et(_utc_iso(ephemeris, k)) for k in range(len(ephemeris))]) # DFH 会把端点历元写两遍(数值相同),按时间戳去重 keep = np.unique(et_all, return_index=True)[1] keep.sort() self._et = et_all[keep] self._states = np.column_stack( [ ephemeris.position_km[keep], ephemeris.velocity_mps[keep] / 1000.0, # m/s → km/s ] ) @property def t_start(self) -> float: return float(self._et[0]) @property def t_end(self) -> float: return float(self._et[-1])
[文档] def state_at(self, t: float) -> npt.NDArray[np.floating]: """t(et 秒)时刻的标称状态(GCRS,km, km/s),逐分量线性插值。""" return np.array([np.interp(t, self._et, self._states[:, i]) for i in range(6)])
[文档] class RustPropagator: """Rust 编译力模型传播器(``StmPropagator`` 实现,单样本)。 直接调 ``propagate_compiled*_py`` (不经 ``ForceModel``),绕开其 STM 路径对 SRP 的排除(站保控制模型含光压,STM 必须含光压贡献;Rust 侧 已补 SRP 雅可比,见 ``compiled.rs::acceleration_and_jacobian``)。 """ def __init__( self, observer: str, forces_py: list[tuple[Any, ...]], rtol: float = 1e-10, atol: float = 1e-10, max_step: float = 3600.0, max_steps: int = 500_000, ) -> None: from e2m2e.integrators import propagate_compiled_stm_py self._observer = observer self._forces_py = forces_py self._rtol = rtol self._atol = atol self._max_step = max_step self._max_steps = max_steps self._propagate_compiled_stm_py = propagate_compiled_stm_py @staticmethod def _align_t_eval(t0: float, t_eval: npt.ArrayLike) -> tuple[npt.NDArray[np.floating], bool]: """Rust STM 传播要求 t_eval 首点 == t0(否则只输出起点状态与恒等 STM,实测单点 [t_j] 返回的 B 块为 0)。首点不是 t0 时前置补 t0, 返回 (对齐后的 t_eval, 是否补过)。 """ t_eval = np.asarray(t_eval, dtype=float) if len(t_eval) == 0 or t_eval[0] == t0: return t_eval, False return np.concatenate([[t0], t_eval]), True
[文档] def propagate_with_stm( self, state0: npt.ArrayLike, t0: float, t_eval: npt.ArrayLike ) -> dict[str, npt.NDArray[np.floating]]: t_eval, prepended = self._align_t_eval(t0, t_eval) state0 = np.asarray(state0, dtype=float) try: result = self._propagate_compiled_stm_py( self._observer, self._forces_py, (float(t0), float(t_eval[-1])), [float(x) for x in t_eval], [float(x) for x in state0], self._rtol, self._atol, self._max_step, self._max_steps, ) except RuntimeError: # 崩溃上下文(站保调试):状态位置/速度量级与弧段 r = float(np.linalg.norm(state0[:3])) v = float(np.linalg.norm(state0[3:])) raise RuntimeError( f"STM propagation failed: state0 r={r:.3f} km, v={v:.6f} km/s, " f"t0={t0:.3f}, dt={float(t_eval[-1]) - t0:.3f} s" ) from None states = np.asarray(result["states"]) stm = np.asarray(result["stm"]).reshape(-1, 6, 6) times = np.asarray(result["time"]) if prepended: states, stm, times = states[1:], stm[1:], times[1:] return {"time": times, "states": states, "stm": stm}
[文档] def propagate( self, state0: npt.ArrayLike, t0: float, t_eval: npt.ArrayLike ) -> npt.NDArray[np.floating]: """无 STM 传播,返回 ``(n, 6)`` 状态序列。 走 STM 传播路径(``propagate_compiled_stm_py``):``propagate_compiled`` 的步长无上限,稀疏 t_eval 下自适应步长失控(实测 2 点 vs 31 点 网格 30 天结果差 22 万 km);STM 路径受 ``max_step`` 限制,网格 无关。STM 计算为附带成本。 """ t_eval, prepended = self._align_t_eval(t0, t_eval) result = self._propagate_compiled_stm_py( self._observer, self._forces_py, (float(t0), float(t_eval[-1])), [float(x) for x in t_eval], [float(x) for x in np.asarray(state0)], self._rtol, self._atol, self._max_step, self._max_steps, ) states = np.asarray(result["states"]) return states[1:] if prepended else states
class PropagatorFactory: """力模型配置 → 传播器工厂(真实力模型的光压乘子每弧段可换)。 容差/步长取站保统计量级够用的默认(1e-10、3600 s):历史标定对比的 对象是统计特征(±30% 量级),1e-12 容差只增加耗时(实测 4 控制 × 2 样本 156 s → 约 3 倍提速空间)。 """ def __init__( self, system: Any, observer: str = "EARTH", rtol: float = 1e-10, atol: float = 1e-10, max_step: float = 3600.0, max_steps: int = 500_000, ) -> None: self._system = system self._observer = observer self._rtol = rtol self._atol = atol self._max_step = max_step self._max_steps = max_steps def _forces_py( self, force_config: dict[str, Any], srp_cr_scale: float = 1.0 ) -> list[tuple[Any, ...]]: """力配置 → Rust 元组列表;srp 的 cr 乘上光压弧段误差乘子。""" fm = ForceModel.from_config(force_config, self._system) out: list[tuple[Any, ...]] = [] for entry in fm._entries: # noqa: SLF001 # 与 force_model 内部一致 if not entry.enabled: continue spec = entry.force.to_rust_spec(self._system) if spec is None: continue if spec[0] == "srp": area, mass, cr, shadows = spec[1], spec[2], spec[3], spec[4] spec = ("srp", area, mass, cr * srp_cr_scale, shadows) out.append(spec) return out def make(self, force_config: dict[str, Any], srp_cr_scale: float = 1.0) -> RustPropagator: return RustPropagator( self._observer, self._forces_py(force_config, srp_cr_scale), rtol=self._rtol, atol=self._atol, max_step=self._max_step, max_steps=self._max_steps, )
[文档] @dataclass class SampleResult: """单样本蒙特卡洛结果。 Attributes: total_delta_v_mps: 总 Δv(m/s),角动量管理模式含轨道+姿态联合 Δv max_delta_v_mps: 最大单次 Δv(m/s) failed: 是否控制失败(单次 > Δv_max 或累计 > 总上限) maneuver_times: 控制时刻(et 秒),形状 ``(n,)`` maneuver_dv_mps: 各控制时刻施加的 Δv(m/s),形状 ``(n,)`` controlled_times: 受控星历时间(et 秒),形状 ``(m,)`` controlled_states: 受控真实轨道状态(GCRS,km, km/s),形状 ``(m, 6)`` attitude_delta_v_mps: 姿态总 Δv(m/s),仅角动量管理模式非零 attitude_delta_v_independent_mps: 姿态不影响轨道时的 Δv(m/s),即 纯角动量卸载(不含轨道控制贡献)的 Δv 累计量 """ total_delta_v_mps: float max_delta_v_mps: float failed: bool maneuver_times: npt.NDArray[np.floating] maneuver_dv_mps: npt.NDArray[np.floating] controlled_times: npt.NDArray[np.floating] controlled_states: npt.NDArray[np.floating] attitude_delta_v_mps: float = 0.0 attitude_delta_v_independent_mps: float = 0.0
[文档] @dataclass class MonteCarloResult: """蒙特卡洛批量结果(对应 DFH SK_STATISTIC/MANEUVERS/受控星历)。 Attributes: total_delta_v: 每样本总 Δv(m/s),形状 ``(n_samples,)`` max_delta_v: 每样本最大 Δv(m/s),形状 ``(n_samples,)`` failed_mask: 每样本失败标记,形状 ``(n_samples,)`` num_failed: 失败样本数 maneuvers: 第一个样本的机动序列(et 秒, m/s) controlled_ephemeris: 第一个样本的受控星历(可为 None) attitude_delta_v: 每样本姿态总 Δv(m/s),形状 ``(n_samples,)`` (角动量管理) attitude_delta_v_independent: 每样本姿态独立 Δv(m/s),形状 ``(n_samples,)`` """ total_delta_v: npt.NDArray[np.floating] max_delta_v: npt.NDArray[np.floating] failed_mask: npt.NDArray[np.bool_] num_failed: int maneuvers: npt.NDArray[np.floating] = field(default_factory=lambda: np.empty((0, 2))) controlled_ephemeris: EphemerisTable | None = None attitude_delta_v: npt.NDArray[np.floating] = field(default_factory=lambda: np.empty(0)) attitude_delta_v_independent: npt.NDArray[np.floating] = field( default_factory=lambda: np.empty(0) )
[文档] def sk_statistic(self) -> SKStatistic: """组装 SK_STATISTIC 表(3 列或 5 列版)。 无角动量管理时 3 列(仿真序号/总 Δv/最大 Δv,序号在写出时追加); 含角动量管理时 5 列(追加姿态总 Δv、姿态独立 Δv)。 对齐外部工具口径:失败样本不写统计行(历史标定样本中失败行缺失)。 """ keep = ~self.failed_mask if self.attitude_delta_v.size > 0: rows = np.column_stack( [ self.total_delta_v[keep], self.max_delta_v[keep], self.attitude_delta_v[keep], self.attitude_delta_v_independent[keep], ] ) else: rows = np.column_stack([self.total_delta_v[keep], self.max_delta_v[keep]]) return SKStatistic(rows=rows, num_failed=self.num_failed)
[文档] def maneuver_table(self) -> ManeuverTable: """组装 MANEUVERS 表(MJD(TDB) = 51544.5 + et/86400,SPICE et 原点为 J2000 历元 JD 2451545.0,即 MJD 51544.5)。""" mjd = 51544.5 + self.maneuvers[:, 0] / SECONDS_PER_DAY return ManeuverTable(mjd_tdb=mjd, delta_v_mps=self.maneuvers[:, 1])
[文档] @dataclass class SingleSampleSimulation: """单样本闭环仿真(三轨道结构 + 分段控制误差 + 光压弧段误差)。 Attributes: nominal: 标称轨道视图 law: 控制律(special/strict/loose 之一) control_interval_sec: 控制时间间隔(秒) num_controls: 控制次数(总时间 = (N-1)·间隔) output_step_sec: 受控星历输出间隔(秒) nav_error: 测定轨误差模型 thrust_error: 分段控制误差模型 srp_error: 光压弧段误差模型 thrust_total_mps: 累计 Δv 上限(超过判失败) factory: 传播器工厂(控制/真实力模型由 ``force_config_ctrl/true`` 构造) force_config_ctrl: 控制(理论)力模型配置 force_config_true: 真实(实际)力模型配置 sampler: 标准正态采样器 engine_layout: 姿态发动机布局(None 表示无角动量管理) momentum_interval_sec: 角动量卸载间隔(秒),0 表示与轨道控制同步 srp_offset_m: SRP 压心相对质心偏移(m),None 表示无 SRP 力矩 spacecraft_mass_kg: 航天器质量(kg) srp_torque_nm: 常值 SRP 力矩(N·m),None 时由 srp_offset_m 和 SRP 力模型计算 """ nominal: NominalOrbit law: Any control_interval_sec: float num_controls: int output_step_sec: float nav_error: NavigationErrorModel thrust_error: ThrustExecutionError srp_error: SrpErrorModel thrust_total_mps: float factory: PropagatorFactory force_config_ctrl: dict[str, Any] force_config_true: dict[str, Any] sampler: BoxMullerSampler engine_layout: Any = None # EngineLayout | None momentum_interval_sec: float = 0.0 srp_offset_m: npt.NDArray[np.floating] | None = None spacecraft_mass_kg: float = 1000.0 srp_torque_nm: npt.NDArray[np.floating] | None = None
[文档] def run(self) -> SampleResult: from .momentum_management import ( compute_delta_m as _compute_dm, ) from .momentum_management import ( solve_joint_control as _solve_joint, ) from .momentum_management import ( solve_momentum_unload as _solve_unload, ) nominal = self.nominal t0 = nominal.t_start # 式 5.37:测量初值相对目标扰动;式 5.38:真实初值相对测量扰动 x_meas = self.nav_error.perturb(nominal.state_at(t0), self.sampler) x_true = self.nav_error.perturb(x_meas, self.sampler) prop_ctrl = self.factory.make(self.force_config_ctrl) # DFH 惯例(MANEUVERS 结构,golden 实测):t0 为参考行(Δv=0), # 控制动作发生在 t0 + k·间隔(k = 1..NumControls-2,共 # NumControls-2 次);总时长 (NumControls-1)·间隔,最后一个控制 # 时刻(跨度终点)不施加控制。 maneuver_times = [t0] maneuver_dv = [0.0] controlled_times = [t0] controlled_states = [x_true.copy()] total_dv = 0.0 max_dv = 0.0 attitude_total_dv = 0.0 attitude_independent_dv = 0.0 failed = False t_prev = t0 # 角动量管理:SRP 力矩常值(用户输入 srp_torque_nm) has_mm = self.engine_layout is not None srp_torque = ( np.asarray(self.srp_torque_nm, dtype=float) if self.srp_torque_nm is not None else None ) layout = self.engine_layout mass = self.spacecraft_mass_kg delta_m_cum = np.zeros(3) # 累积角动量(上次卸载以来) # 构造合并事件时间表(轨道控制 + 角动量卸载) # event_type[t] = "orbital" | "momentum" | "both" orbital_times: set[float] = set() momentum_times: set[float] = set() if has_mm and self.momentum_interval_sec > 0: t_end = t0 + (self.num_controls - 1) * self.control_interval_sec orbital_times = { t0 + k * self.control_interval_sec for k in range(1, self.num_controls - 1) } t_m = t0 + self.momentum_interval_sec while t_m < t_end: # 与最近轨道控制时刻对齐(相差 < 1 秒视为同时) if any(abs(t_m - t_o) < 1.0 for t_o in orbital_times): # 合并到轨道事件(标记为 both) pass else: momentum_times.add(t_m) t_m += self.momentum_interval_sec events = sorted(orbital_times | momentum_times) elif has_mm: # 卸载间隔=0:与轨道控制同步 events = [t0 + k * self.control_interval_sec for k in range(1, self.num_controls - 1)] else: # 无角动量管理 events = [t0 + k * self.control_interval_sec for k in range(1, self.num_controls - 1)] for t_k in events: # 弧段 [t_prev, t_k] 真实轨道传播(实际力模型 + 本弧段光压误差, # 表 5-3 脚注:每控制弧段重新抽样、弧段内固定) cr_scale = self.srp_error.sample_cr_scale(self.sampler) prop_true = self.factory.make(self.force_config_true, srp_cr_scale=cr_scale) n_out = max(1, int(round((t_k - t_prev) / self.output_step_sec))) t_eval = np.linspace(t_prev, t_k, n_out + 1) states = prop_true.propagate(x_true, t_prev, t_eval) controlled_times.extend(t_eval[1:]) controlled_states.extend(states[1:]) x_true = states[-1] # 式 5.39:控制时刻的测量状态 = 真实 + 测定轨扰动 x_meas = self.nav_error.perturb(x_true, self.sampler) # 角动量累积(SRP 力矩 × 弧段时间) if has_mm and srp_torque is not None: delta_m_cum += _compute_dm(srp_torque, t_k - t_prev) # 判断事件类型 # is_orbital:无角动量管理时所有事件都是轨道控制;有管理时按事件表 # 或同步模式(卸载间隔=0 与轨道控制合并)判定。has_mm=False 时不可 # 用 ``t_k in orbital_times`` 判定——orbital_times 仅在有 MM 时构造。 is_orbital = not has_mm or t_k in orbital_times or self.momentum_interval_sec <= 0 is_momentum = has_mm and (t_k in momentum_times or self.momentum_interval_sec <= 0) # 控制量计算 if has_mm and is_orbital and is_momentum: # 联合控制:轨道 + 角动量一次开机 dv_c = self.law.compute_maneuver(x_meas, t_k, propagator=prop_ctrl, nominal=nominal) if dv_c is not None: V = _solve_joint(layout.E, layout.E_r, dv_c * 1000.0, delta_m_cum, mass) dv_orbital = layout.E @ V # 姿态独立 Δv:纯卸载需要的 Δv(与联合控制对比用) dv_att_ind = _solve_unload(layout.E_r, delta_m_cum, mass) attitude_independent_dv += float(np.linalg.norm(layout.E @ dv_att_ind)) dv_r_orbital, failed_k = self.thrust_error.apply(dv_orbital, self.sampler) # 联合控制姿态贡献:纯卸载所需的发动机输出投影到轨道面 if failed_k: pass elif np.linalg.norm(delta_m_cum) > 0: att_contribution = float(np.linalg.norm(layout.E @ dv_att_ind)) attitude_total_dv += att_contribution else: dv_r_orbital = None failed_k = False if np.linalg.norm(delta_m_cum) > 0: V = _solve_unload(layout.E_r, delta_m_cum, mass) dv_att_ind = V attitude_independent_dv += float(np.linalg.norm(layout.E @ dv_att_ind)) else: V = None if failed_k: failed = True break elif has_mm and not is_orbital: # 纯角动量卸载 dv_r_orbital = None failed_k = False if np.linalg.norm(delta_m_cum) > 0: V = _solve_unload(layout.E_r, delta_m_cum, mass) dv_att_ind = V attitude_independent_dv += float(np.linalg.norm(layout.E @ dv_att_ind)) dv_att, failed_k = self.thrust_error.apply(layout.E @ V, self.sampler) if failed_k: failed = True break if dv_att is not None: att_mag = float(np.linalg.norm(dv_att)) attitude_total_dv += att_mag total_dv += att_mag max_dv = max(max_dv, att_mag) x_true = x_true.copy() x_true[3:] += dv_att / 1000.0 V = None else: # 纯轨道控制(无角动量管理 或 角动量卸载间隔=0 与轨道同步) dv_c = self.law.compute_maneuver(x_meas, t_k, propagator=prop_ctrl, nominal=nominal) if dv_c is None: # 控制律未产出机动(不收敛/未找到穿越点):计为失败样本, # 不把失败藏进成功统计(#352) failed = True break dv_r_orbital, failed_k = self.thrust_error.apply(dv_c * 1000.0, self.sampler) if failed_k: failed = True break # 同步模式下角动量卸载与轨道控制合并 V = None if has_mm and np.linalg.norm(delta_m_cum) > 0 and self.momentum_interval_sec <= 0: if dv_c is not None: V = _solve_joint(layout.E, layout.E_r, dv_c * 1000.0, delta_m_cum, mass) dv_att_ind = _solve_unload(layout.E_r, delta_m_cum, mass) attitude_independent_dv += float(np.linalg.norm(layout.E @ dv_att_ind)) else: V = _solve_unload(layout.E_r, delta_m_cum, mass) dv_att_ind = V attitude_independent_dv += float(np.linalg.norm(layout.E @ dv_att_ind)) att_applied, att_failed = self.thrust_error.apply( layout.E @ V, self.sampler ) if att_failed: failed = True break if att_applied is not None: att_mag = float(np.linalg.norm(att_applied)) attitude_total_dv += att_mag total_dv += att_mag max_dv = max(max_dv, att_mag) x_true = x_true.copy() x_true[3:] += att_applied / 1000.0 # 累计轨道控制 Δv mag = 0.0 if dv_r_orbital is None else float(np.linalg.norm(dv_r_orbital)) if is_orbital: maneuver_times.append(t_k) maneuver_dv.append(mag) total_dv += mag max_dv = max(max_dv, mag) if total_dv > self.thrust_total_mps: failed = True break if dv_r_orbital is not None: x_true = x_true.copy() x_true[3:] += dv_r_orbital / 1000.0 # 施加于真实轨道(m/s → km/s) # 清零累积角动量(卸载完成) if has_mm: delta_m_cum = np.zeros(3) t_prev = t_k return SampleResult( total_delta_v_mps=total_dv, max_delta_v_mps=max_dv, failed=failed, maneuver_times=np.array(maneuver_times), maneuver_dv_mps=np.array(maneuver_dv), controlled_times=np.array(controlled_times), controlled_states=np.array(controlled_states), attitude_delta_v_mps=attitude_total_dv, attitude_delta_v_independent_mps=attitude_independent_dv, )
# ── 进程池:CSPICE 全局状态非线程安全,样本并行用进程隔离 ── _CTX: dict[str, Any] = {} def _kernel_paths(kernel_dir: str | None = None) -> list[str]: """Rust cspice 域的内核清单(对齐 dfh/design_orbit 的 _BODY_FIXED_KERNELS)。""" import os kernel_dir = kernel_dir or os.environ.get( "SPICE_KERNEL_DIR", str(Path(__file__).resolve().parents[3] / "kernels") ) paths = [] for name in [ "de440s.bsp", "de430.bsp", "earth_latest_high_prec.bpc", "pck00010.tpc", "SPICELunaCurrentKernel.bpc", "SPICELunaFrameKernel.tf", "naif0012.tls", "naif0011.tls", ]: p = os.path.join(kernel_dir, name) if os.path.exists(p): paths.append(p) return paths def _init_worker(params: dict[str, Any]) -> None: """worker ?????? SPICE ????""" from ...data.kernels.manager import SPICEManager from ..design.design_orbit import load_design_kernels mgr = SPICEManager() # 完整内核(行星历 + body-fixed 帧 + 行星名注册),与 design 链路一致 load_design_kernels(mgr, params["kernel_dir"]) sys = EphemerisSystem(bodies=params["bodies"], spice=mgr, origin=params["observer"]) sys.coordinate_system = CoordinateSystem( axes=ICRSAxes(), origin=CelestialBodyOrigin(body="EARTH", spice=mgr), ) _CTX["spice"] = mgr _CTX["system"] = sys _CTX["nominal"] = NominalOrbit(params["nominal_ephemeris"], mgr) def _run_sample(spec: dict[str, Any]) -> SampleResult: """单样本运行(进程池任务)。``spec`` 为除种子外的公共参数。""" sim = _build_simulation( spec, BoxMullerSampler(spec["seed"]), _CTX["nominal"], _CTX["system"], _CTX["spice"] ) return sim.run() class SynodicJ2000Adapter: """``SynodicView`` 协议适配:包装 ``SynodicJ2000System`` (et 时间轴)。 ``SynodicJ2000System`` 的接口是 ``j2000_to_synodic(state, t_syn, et0)`` (无量纲时间轴),特征点控制律需要 ``to_synodic(states, ets)`` 批量 转换(绝对 et 时间轴)+ 旋转矩阵。 """ def __init__(self, synodic_system: Any, et0: float) -> None: self._sys = synodic_system self._et0 = float(et0) self._t_c = synodic_system._get_time_unit() # noqa: SLF001 def to_synodic(self, states: npt.ArrayLike, ets: npt.ArrayLike) -> npt.NDArray[np.floating]: t_syn = (np.asarray(ets, dtype=float) - self._et0) / self._t_c return self._sys.batch_j2000_to_synodic(states, t_syn, self._et0) def rotation_matrix(self, et: float) -> npt.NDArray[np.floating]: return self._sys.synodic_axes.rotation_matrix(et) def _make_law( control_mode: int, special_mode: int, special_crossings: int, feedback_arc_days: float, horizon_sec: float, spice: Any, et0: float, tight_tolerance_km: float = 0.1, tight_max_iter: int = 6, special_damping_factor: float = 1.0, ) -> Any: """按 DFH 控制模式构造控制律(1=宽松、2=严格、3=特征点)。""" if control_mode == 1: return LooseTargetPointLaw(feedback_arc_days=feedback_arc_days) if control_mode == 2: return StrictTargetPointLaw( feedback_arc_days=feedback_arc_days, tolerance_km=tight_tolerance_km, max_iter=tight_max_iter, ) if control_mode == 3: from ..coordinate.synodic_j2000 import SynodicJ2000System cr3 = _earth_moon_system() syn_system = SynodicJ2000System(cr3bp_system=cr3, spice=spice) synodic = SynodicJ2000Adapter(syn_system, et0) return SpecialPointLaw( special_mode=special_mode, crossings=special_crossings, horizon_sec=horizon_sec, synodic=synodic, damping_factor=special_damping_factor, v_c=syn_system.cr3bp_system.characteristic_velocity, ) raise ValueError(f"control_mode 必须为 1/2/3(角动量管理 4-6 归属 #261),当前 {control_mode}") def _build_simulation( spec: dict[str, Any], sampler: BoxMullerSampler, nominal: NominalOrbit, system: Any, spice: Any ) -> SingleSampleSimulation: return SingleSampleSimulation( nominal=nominal, law=_make_law( spec["control_mode"], spec["special_mode"], spec["special_crossings"], spec["feedback_arc_days"], spec["control_interval_days"] * SECONDS_PER_DAY, spice, nominal.t_start, tight_tolerance_km=spec.get("tight_tolerance_km", 0.1), tight_max_iter=spec.get("tight_max_iter", 6), special_damping_factor=spec.get("special_damping_factor", 1.0), ), control_interval_sec=spec["control_interval_days"] * SECONDS_PER_DAY, num_controls=spec["num_controls"], output_step_sec=spec["output_step_sec"], nav_error=NavigationErrorModel( position_sigma_m=spec["position_accuracy_m"], velocity_sigma_mps=spec["velocity_accuracy_mps"], ), thrust_error=ThrustExecutionError( dv_min=spec["thrust_min_mps"], dv_mid=spec["thrust_mean_mps"], dv_max=spec["thrust_max_mps"], abs_sigma_mps=spec["thrust_abs_err_mps"], rel_sigma=spec["thrust_rel_err"], angle_sigma_deg=spec["thrust_angle_err_deg"], ), srp_error=SrpErrorModel(error_level=spec["srp_error_level"]), thrust_total_mps=spec["thrust_total_mps"], factory=PropagatorFactory(system, observer=spec["observer"]), force_config_ctrl=spec["force_config_ctrl"], force_config_true=spec["force_config_true"], sampler=sampler, engine_layout=spec.get("engine_layout"), momentum_interval_sec=spec.get("momentum_interval_sec", 0.0), srp_offset_m=spec.get("srp_offset_m"), spacecraft_mass_kg=spec.get("spacecraft_mass_kg", 1000.0), srp_torque_nm=spec.get("srp_torque_nm"), )
[文档] def run_monte_carlo( nominal_ephemeris: EphemerisTable, *, spice: Any, system: Any, force_config_ctrl: dict[str, Any], force_config_true: dict[str, Any], control_mode: int = 1, special_mode: int = 1, special_crossings: int = 3, control_interval_days: float = 30.0, feedback_arc_days: float = 28.0, num_controls: int = 120, num_monte_carlo: int = 5, output_step_sec: float = SECONDS_PER_DAY, position_accuracy_m: float = 1500.0, velocity_accuracy_mps: float = 0.002, thrust_angle_err_deg: float = 0.333, thrust_mean_mps: float = 10.0, thrust_rel_err: float = 0.003, thrust_abs_err_mps: float = 0.033, thrust_min_mps: float = 0.1, thrust_max_mps: float = 100.0, thrust_total_mps: float = 1000.0, srp_error_level: float = 0.10, seed: int | None = None, n_workers: int = 1, kernel_dir: str | None = None, engine_layout: Any = None, momentum_interval_days: float = 0.0, srp_offset_m: npt.ArrayLike | None = None, spacecraft_mass_kg: float = 1000.0, srp_torque_nm: npt.ArrayLike | None = None, tight_tolerance_km: float = 0.1, tight_max_iter: int = 6, special_damping_factor: float = 1.0, ) -> MonteCarloResult: """运行蒙特卡洛站保仿真(DFH 功能码 2 的数值核心)。 Args: nominal_ephemeris: 标称轨道星历(FR1 ``design_orbit`` 产物) spice: SPICE 管理器(``n_workers>1`` 时仅用于构造标称视图) system: 星历动力学系统(地心 ICRF) force_config_ctrl: 控制(理论)力模型配置(表 5-3 左列) force_config_true: 真实(实际)力模型配置(表 5-3 右列) control_mode: 1=目标点宽松、2=目标点严格、3=特征点 special_mode: 特征点模式 1=Lissajous、2=Halo/NRHO special_crossings: 特征点目标穿越次数 control_interval_days: 控制时间间隔(天) feedback_arc_days: 目标点模式反馈弧段(天) num_controls: 控制次数(总时间 = (N-1)·间隔) num_monte_carlo: 蒙特卡洛样本数 output_step_sec: 受控星历输出间隔(秒) position_accuracy_m/velocity_accuracy_mps: 测定轨 1-sigma thrust_*: 分段控制误差参数(§1.5.2) srp_error_level: 光压弧段随机误差量级(百分比/100) seed: 随机种子(同种子同结果) n_workers: 进程池大小(>1 时样本并行;需 ``kernel_dir`` 供 worker 重建 SPICE 上下文) kernel_dir: SPICE 内核目录(``n_workers>1`` 时必填) engine_layout: ``EngineLayout`` 实例(None 表示无角动量管理) momentum_interval_days: 角动量卸载间隔(天),0 表示与轨道控制同步 srp_offset_m: SRP 压心相对质心偏移(m) spacecraft_mass_kg: 航天器质量(kg) srp_torque_nm: 常值 SRP 力矩(N·m) Returns: :class:`MonteCarloResult` (含 SK_STATISTIC/MANEUVERS 组装方法) """ nominal = NominalOrbit(nominal_ephemeris, spice) observer = getattr(system, "origin", "EARTH") spec: dict[str, Any] = { "control_mode": control_mode, "special_mode": special_mode, "special_crossings": special_crossings, "control_interval_days": control_interval_days, "feedback_arc_days": feedback_arc_days, "num_controls": num_controls, "output_step_sec": output_step_sec, "position_accuracy_m": position_accuracy_m, "velocity_accuracy_mps": velocity_accuracy_mps, "thrust_angle_err_deg": thrust_angle_err_deg, "thrust_mean_mps": thrust_mean_mps, "thrust_rel_err": thrust_rel_err, "thrust_abs_err_mps": thrust_abs_err_mps, "thrust_min_mps": thrust_min_mps, "thrust_max_mps": thrust_max_mps, "thrust_total_mps": thrust_total_mps, "srp_error_level": srp_error_level, "force_config_ctrl": force_config_ctrl, "force_config_true": force_config_true, "observer": observer, "engine_layout": engine_layout, "momentum_interval_sec": momentum_interval_days * SECONDS_PER_DAY, "srp_offset_m": np.asarray(srp_offset_m, dtype=float) if srp_offset_m is not None else None, "spacecraft_mass_kg": spacecraft_mass_kg, "srp_torque_nm": ( np.asarray(srp_torque_nm, dtype=float) if srp_torque_nm is not None else None ), "tight_tolerance_km": tight_tolerance_km, "tight_max_iter": tight_max_iter, "special_damping_factor": special_damping_factor, } rng = np.random.default_rng(seed) seeds = [int(rng.integers(0, 2**31 - 1)) for _ in range(num_monte_carlo)] if n_workers > 1: if not kernel_dir: raise ValueError("n_workers>1 时需提供 kernel_dir(worker 重建 SPICE 上下文)") worker_params = { **spec, "kernel_dir": kernel_dir, "bodies": list(getattr(system, "bodies", [])), "nominal_ephemeris": nominal_ephemeris, } with ProcessPoolExecutor( max_workers=n_workers, initializer=_init_worker, initargs=(worker_params,) ) as ex: results = list(ex.map(_run_sample, [dict(spec, seed=s) for s in seeds])) else: results = [ _build_simulation(spec, BoxMullerSampler(s), nominal, system, spice).run() for s in seeds ] total_dv = np.array([r.total_delta_v_mps for r in results]) max_dv = np.array([r.max_delta_v_mps for r in results]) failed_mask = np.array([r.failed for r in results], dtype=bool) num_failed = int(failed_mask.sum()) att_dv = np.array([r.attitude_delta_v_mps for r in results]) att_dv_ind = np.array([r.attitude_delta_v_independent_mps for r in results]) first = results[0] if results else None maneuvers = ( np.column_stack([first.maneuver_times, first.maneuver_dv_mps]) if first is not None else np.empty((0, 2)) ) controlled = _build_controlled_ephemeris(first, nominal, spice) if first is not None else None return MonteCarloResult( total_delta_v=total_dv, max_delta_v=max_dv, failed_mask=failed_mask, num_failed=num_failed, maneuvers=maneuvers, controlled_ephemeris=controlled, attitude_delta_v=att_dv, attitude_delta_v_independent=att_dv_ind, )
def _build_controlled_ephemeris( sample: SampleResult, nominal: NominalOrbit, spice: Any ) -> EphemerisTable | None: """把样本的受控真实轨道组装成 DFH 同格式星历表(UTC + GCRS + 会合系)。 会合系列与 ``design_orbit`` 同约定:地心归一(月球在 +x),内部转换器 输出质心归一后 x 加 mu 平移。星历为空(控制失败早退)时返回 None。 """ if sample.controlled_times.size == 0: return None from datetime import datetime from ..coordinate.synodic_j2000 import SynodicJ2000System cr3 = _earth_moon_system() syn_j2000 = SynodicJ2000System(cr3bp_system=cr3, spice=spice) et_grid = sample.controlled_times states = sample.controlled_states t_c = cr3.characteristic_time t_syn = (et_grid - nominal.t_start) / t_c synodic = syn_j2000.batch_j2000_to_synodic(states, t_syn, nominal.t_start)[:, :3] synodic[:, 0] += cr3.mu n = len(et_grid) year = np.empty(n, dtype=int) month = np.empty(n, dtype=int) day = np.empty(n, dtype=int) hour = np.empty(n, dtype=int) minute = np.empty(n, dtype=int) second = np.empty(n, dtype=float) for k in range(n): dt = datetime.fromisoformat(spice.et_to_utc(float(et_grid[k]))) year[k], month[k], day[k] = dt.year, dt.month, dt.day hour[k], minute[k] = dt.hour, dt.minute second[k] = dt.second + dt.microsecond / 1e6 return EphemerisTable( year=year, month=month, day=day, hour=hour, minute=minute, second=second, position_km=states[:, :3].copy(), velocity_mps=states[:, 3:] * 1000.0, synodic_position=synodic, )