e2m2e.algorithm.normal_form.center_manifold 源代码

"""高阶中心流形化简(qiao Code10–Code11)。

在 quasi-Floquet 坐标(切片 #172)基础上,用逐阶 Lie 变换消去
Hamiltonian 的双曲-中心耦合项,把非线性 Hamiltonian 化简为仅依赖
作用量的函数:

- **Step 1 ``"invariant"``** (qiao ``Code10_DepartInvarManifold``):
  消去 ``q_1^k·p_1^l`` (``k ≠ l``)的双曲交叉项,判别条件
  ``pow(1)==pow(4)`` 保留双曲"循环坐标" ``I_1 = q_1·p_1``。经此步后
  双曲方向与中心方向解耦,中心部分可独立分析;
- **Step 2 ``"center"``** (qiao ``Code11_DepartCenterManifold``):
  在 Step 1 结果上,消去两个中心方向 ``(q_2,p_2)``/``(q_3,p_3)`` 之间
  的非共振耦合,更严判别条件
  ``pow(1)==pow(4) && pow(2)==pow(5) && pow(3)==pow(6)``,结果只剩
  作用量 ``I_1 = q_1·p_1``、``I_2 = (q_2²+p_2²)/2``、
  ``I_3 = (q_3²+p_3²)/2``。

算法(两步结构相同):

1. 逐阶(``3..max_order``)解同调方程 ``{H_2, W_i} = -H_i^{待消}}``。
   ``W_i`` 由频域 ODE 求解器 :func:`_solve_wfunc_fft` /
   :func:`_solve_wfunc_fft_imag` 给出特解 ``ẏ = k·y + f(t)``;
   特征频率 ``k = (j_1−i_1)·λ + (j_2−i_2)·i·ω_p + (j_3−i_3)·i·ω_v``;
2. ``W_i`` 对各高阶 Hamiltonian 的贡献由 Poisson 括号链
   ``ad_{W_i}^n / n!`` (Lie 级数)累加,再加上 ``Ẇ_i`` 项更新各阶;
3. 第 ``i`` 阶按判别条件删去被 ``W_i`` 消去的项。

与 qiao 数值实现一致,每个化简步骤前后做复基底变换(
:func:`_linear_basis_change`):先虚变换 ``X = D·Y`` (实坐标 → 复坐标,
二阶部分成复对角形 ``λ·y1·y4 + i·ω_p·y2·y5 + i·ω_v·y3·y6``,同调方程
的复值特征频率 ``k`` 只有在此坐标系下才与 ``H_2`` 的泊松谱匹配),
Lie 变换完成后实变换 ``Y = D⁻¹·X`` 映回实坐标并取实部(吸收数值虚部
噪声)。生成函数 ``W`` 保持复坐标——:mod:`.coord_trans.qf_cm` 的
QF↔CM Lie 流在复域消费它(先 ``Re2Im`` 再应用 ``W`` 再 ``Im2Re``)。

单位约定:内部 Hamiltonian 系数全部在 qiao 归一化单位(TU)下运算;
与 SI 之间换算只能经 :class:`NormalFormContext` 与 :mod:`.units`。
``sympy`` 仅在 Legendre/Hamiltonian 构造时惰性导入;本模块不依赖
sympy。``joblib`` 为可选优化(qiao 用其并行 Poisson 括号),本模块
串行实现并显式注明。

Public API:

- :class:`CenterManifoldResult` —— 化简结果句柄;
- :class:`CenterManifoldReducer` —— 上下文绑定的 reducer。

输入 :class:`QuasiFloquetResult` 只提供二阶实标准形 ``D`` (频率
``λ``、``ω_p``、``ω_v``);高阶非线性 Hamiltonian 项需经
``hamiltonian_terms`` 参数注入(对应 qiao ``Code09`` 的
``L?_QF_Hamilton.npz``:``{pow_tuple: coef_array}``,系数为时间序列
``ndarray``)。若不注入,reducer 退化为只含二阶项的平凡情形
(仍能跑通,但无高阶项可消,仅供 smoke)。
"""

from __future__ import annotations

from collections.abc import Mapping
from dataclasses import dataclass, field
from math import comb, factorial
from typing import TYPE_CHECKING, Any, Literal

import numpy as np
import numpy.typing as npt

from .polynomial import poly_poisson, polylist_simplify
from .quasi_floquet import QuasiFloquetResult

if TYPE_CHECKING:
    from .context import NormalFormContext

# ---------------------------------------------------------------------------
# 默认参数
# ---------------------------------------------------------------------------

#: 默认展开阶数(与 qiao ``Code10``/``Code11`` 一致)。
DEFAULT_MAX_ORDER: int = 10
#: 频域 ODE 求解器边界延拓比例(qiao ``Solve_Wfunc_fft`` 默认值)。
_DEFAULT_EXTENSION_RATIO: float = 0.2
#: MAD 离群抑制阈值乘子(qiao ``Solve_Wfunc_fft_imag`` 默认值)。
_DEFAULT_MAD_KVAL: float = 1e6
#: 数值微分中心差分阶数(qiao ``list_deriv`` 默认值)。
_DEFAULT_DERIV_N: int = 14


# ---------------------------------------------------------------------------
# 结果容器
# ---------------------------------------------------------------------------


[文档] @dataclass(frozen=True) class CenterManifoldResult: """中心流形化简结果句柄。 Attributes: context: 关联 :class:`NormalFormContext`。 order: 化简截断阶数(``max_order``)。 W_series: 各步、各阶生成函数 ``W`` 的系数表。结构 ``{step_name: {order: {pow_tuple: coef_array}}}``,封装访问; ``step_name ∈ {"invariant", "center"}``。系数为**复值** 时间 序列 ``ndarray`` (``invariant`` 步 W 天然实值、虚部≈0; ``center`` 步 W 为纯虚、实部≈0)——与 qiao Code10/Code11 输出的复值 ``.npz`` 一致,供 ``coord_trans`` 的 QF↔CM Lie 流在复域消费。不直接暴露 qiao 的 ``powers``/``coefficients`` 扁平数组命名。 hamiltonian_terms: 化简后 Hamiltonian 的 ``{pow_tuple: coef_array}`` 系数表(实 QF 坐标)。 steps_performed: 实际执行的化简步骤名元组(按顺序)。 metadata: 自由扩展字段(频率、项数等诊断信息)。 """ context: NormalFormContext order: int W_series: dict[str, dict[int, dict[tuple[int, ...], npt.NDArray[np.complex128]]]] hamiltonian_terms: dict[tuple[int, ...], npt.NDArray[np.floating]] steps_performed: tuple[str, ...] metadata: dict[str, object] = field(default_factory=dict) @property def max_hyperbolic_coupling(self) -> float: """化简后剩余双曲-中心**耦合** 项系数的最大绝对值。 耦合指双曲方向不平衡(``pow[0] != pow[3]``)且涉及中心方向的 项——Step 1(``invariant``)的消去对象。双曲平衡的作用量项 (如 ``I₁³·I₂²``)在中心流形上(``q₁=p₁=0``)自动为零,不是 耦合,不统计。本属性是步骤 ``"invariant"`` 化简效果的核心 诊断量:值越小,化简越彻底。 Returns: ``max_{k∈terms} |coef_k|``,其中 ``k`` 遍历双曲不平衡且 含中心方向的项;无此类项时返回 ``0.0``。 """ mx = 0.0 for pow_tuple, coef in self.hamiltonian_terms.items(): if pow_tuple[0] != pow_tuple[3]: center = pow_tuple[1] + pow_tuple[4] + pow_tuple[2] + pow_tuple[5] if center > 0: m = float(np.max(np.abs(coef))) if np.ndim(coef) else abs(float(coef)) if m > mx: mx = m return mx
[文档] def W_for(self, step: str, order: int) -> dict[tuple[int, ...], npt.NDArray[np.complex128]]: """封装访问器:取出某步某阶的 ``W`` 系数表。 Args: step: 步骤名(``"invariant"`` / ``"center"``)。 order: 阶数。 Returns: ``{pow_tuple: coef_array}``;该步/阶未执行或无项时返回 ``{}``。 Raises: KeyError: ``step`` 未执行。 """ if step not in self.W_series: raise KeyError(f"步骤 {step!r} 未执行;已执行:{self.steps_performed}") return dict(self.W_series[step].get(order, {}))
# --------------------------------------------------------------------------- # 复基底变换(虚变换 / 实变换,迁移自 qiao Code10/Code11 的 # virtual_transform_symbolic) # --------------------------------------------------------------------------- #: 6×6 复对角化矩阵 D(与 coord_trans/qf_cm.py 的 ``_D`` 逐元素一致, #: 亦与 qiao Code10/Code11 的虚变换矩阵一致)。双曲方向 q1/p1(位 0/3) #: 保持实;平面中心对 q2/p2(位 1/4)、垂直中心对 q3/p3(位 2/5)通过 #: ``(1/√2, ±i/√2)`` 组合拆成 ±i 模式。实变换 ``X = D·Y`` 把实正规形 #: ``(ω/2)(q²+p²)`` 化为复对角形 ``i·ω·q·p``(Gómez vol III §2.7.1)。 _D: npt.NDArray[np.complex128] = np.zeros((6, 6), dtype=complex) _D[0, 0] = 1.0 _D[1, 1] = 1.0 / np.sqrt(2.0) _D[1, 4] = 1j / np.sqrt(2.0) _D[2, 2] = 1.0 / np.sqrt(2.0) _D[2, 5] = 1j / np.sqrt(2.0) _D[3, 3] = 1.0 _D[4, 4] = 1.0 / np.sqrt(2.0) _D[4, 1] = 1j / np.sqrt(2.0) _D[5, 5] = 1.0 / np.sqrt(2.0) _D[5, 2] = 1j / np.sqrt(2.0) #: ``D`` 的逆(预计算,供实变换复用)。 _D_INV: npt.NDArray[np.complex128] = np.linalg.inv(_D).astype(np.complex128) def _add_exp(pow_tuple: tuple[int, ...], idx: int, delta: int) -> tuple[int, ...]: """幂次向量的单分量加法(``pow[idx] += delta``)。""" lst = list(pow_tuple) lst[idx] += delta return tuple(lst) def _sub_exp(pow_tuple: tuple[int, ...], idx: int, delta: int) -> tuple[int, ...]: """幂次向量的单分量减法(``pow[idx] -= delta``)。""" lst = list(pow_tuple) lst[idx] -= delta return tuple(lst) def _linear_basis_change( H_by_order: Mapping[int, Mapping[tuple[int, ...], npt.ArrayLike]], M: npt.NDArray[np.complex128], ) -> dict[int, dict[tuple[int, ...], npt.NDArray[np.complex128]]]: """线性基底变换:对每个单项式做 ``v_i → Σ_j M[i,j]·w_j`` 替换展开。 用于中心流形化简的虚变换(``M = _D``,实坐标 → 复坐标)与实变换 (``M = _D_INV``,复坐标 → 实坐标),对应 qiao ``Code10``/``Code11`` 的符号替换 ``subs(expr, X_coord, M * Y_coord)``:逐项展开成新变量的 多项式。系数保持为时间序列(复值)。 ``M`` 每行至多 2 个非零元素(``D`` 的结构),每个因子 ``(m0·w_j0 + m1·w_j1)^n`` 用二项式展开 ``Σ_s C(n,s)·m0^{n-s}·m1^s·w_j0^{n-s}·w_j1^s``,纯数组实现、不依赖 sympy。 Args: H_by_order: ``{order: {pow: coef}}`` 系数表(变量为 ``v``)。 M: 6×6 复变换矩阵(``v → M·w``)。 Returns: ``{order: {new_pow: coef}}``,系数为复值时间序列(变量为 ``w``)。 """ out: dict[int, dict[tuple[int, ...], npt.NDArray[np.complex128]]] = {} for order, poly in H_by_order.items(): acc: dict[tuple[int, ...], npt.NDArray[np.complex128]] = {} for pow_tuple, coef in poly.items(): # 当前单项式的展开表 {new_pow: 复标量系数} terms: dict[tuple[int, ...], complex] = {tuple(int(p) for p in pow_tuple): 1.0} for i, ni in enumerate(int(p) for p in pow_tuple): if ni == 0: continue row = [(j, M[i, j]) for j in range(6) if abs(M[i, j]) > 1e-12] if not row: continue # 先移除被展开变量的原幂次 ni(展开式从 ni·e_i 分解而来) if len(row) == 1: j0, m0 = row[0] terms = { _add_exp(_sub_exp(pow_old, i, ni), j0, ni): c * (m0**ni) for pow_old, c in terms.items() } else: (j0, m0), (j1, m1) = row new_terms: dict[tuple[int, ...], complex] = {} for pow_old, c in terms.items(): reduced = _sub_exp(pow_old, i, ni) for s in range(ni + 1): npow = _add_exp(_add_exp(reduced, j0, ni - s), j1, s) cnew = c * comb(ni, s) * (m0 ** (ni - s)) * (m1**s) new_terms[npow] = new_terms.get(npow, 0.0) + cnew terms = new_terms for new_pow, c in terms.items(): arr = acc.get(new_pow, 0) + np.asarray(coef, dtype=complex) * c acc[new_pow] = arr out[order] = acc return out # --------------------------------------------------------------------------- # 频域 ODE 求解器(迁移自 qiao Solve_Wfunc_fft / Solve_Wfunc_fft_imag) # --------------------------------------------------------------------------- def _characteristic_freq(pow_tuple: tuple[int, ...], lam: float, wp: float, wv: float) -> complex: """同调方程的特征频率 ``k`` (复值)。 ``k = (j_1 − i_1)·λ + (j_2 − i_2)·i·ω_p + (j_3 − i_3)·i·ω_v``, 其中 ``pow_tuple = (i_1, i_2, i_3, j_1, j_2, j_3)`` 对应 ``(q_1, q_2, q_3, p_1, p_2, p_3)``。``λ`` 是双曲特征指数(实), ``ω_p``/``ω_v`` 是平面/垂直中心频率。对应 qiao ``Solve_Wfunc_fft`` 中 ``k`` 的定义。 ``k == 0`` (共振项)时同调方程不可解,调用方负责跳过此类项。 """ i1, i2, i3, j1, j2, j3 = (int(p) for p in pow_tuple) return complex( (j1 - i1) * lam, (j2 - i2) * wp + (j3 - i3) * wv, ) def _solve_wfunc_fft( tlist: npt.NDArray[np.floating], forcing: npt.NDArray[np.floating], k: complex, *, extension_ratio: float = _DEFAULT_EXTENSION_RATIO, ) -> npt.NDArray[np.complex128]: """频域求解 ``ẏ = k·y + f(t)`` 的特解(实值 ``f``)。 迁移自 qiao ``Solve_Wfunc_fft``:对 ``f`` 做镜像边界延拓后 FFT, 乘传递函数 ``H(ω) = 1/(iω − k)`` 再 IFFT,截取原区间。频域 ODE 求解的离散泄漏由边界延拓抑制(qiao 用 mirror,本实现一致)。 Args: tlist: ``(N,)`` 等距时间序列。 forcing: ``(N,)`` 强迫项 ``f(t)``。 k: 特征频率(复值)。 extension_ratio: 边界延拓比例。 Returns: ``(N,)`` 复值特解 ``y(t)``。 """ t_arr = np.asarray(tlist, dtype=float).ravel() f_vals = np.asarray(forcing, dtype=float).ravel() N = t_arr.size if N < 2: return np.zeros(N, dtype=complex) # 常数(自治系统)输入:同调方程退化为代数除法 ``W = -f/k`` # (Gómez vol III §2.7.1 / Ross (9.7.3))。CR3BP 路径的系数是常数 # 时间序列,走此路径避免 FFT 的离散频率误差。 if np.max(np.abs(f_vals - f_vals[0])) <= 1e-2 * max(1.0, np.max(np.abs(f_vals))): return np.full(N, -f_vals[0] / k, dtype=complex) dt = float(t_arr[1] - t_arr[0]) M = int(np.ceil(extension_ratio * N)) # mirror 边界延拓 f_ext = np.concatenate([f_vals[:M][::-1], f_vals, f_vals[-M:][::-1]]) N_ext = f_ext.size F = np.fft.fft(f_ext) # 频率轴用 fftfreq(与 fft 输出顺序一致:DC 在 index 0,正频在前、 # 负频在后)。不要用「-N/2..N/2-1 + fftshift」构造——N_ext 为奇数时 # fftshift 的 DC 位置(ceil(N/2))与 arange 的 DC 位置((N-1)/2) # 错位 1 个频率 bin,使常数输入的解产生 O(Δω/k) 的系统偏差 # (161 点窗口实测残差 4.8%)。 freq = np.fft.fftfreq(N_ext, dt) omega = 2.0 * np.pi * freq omega[np.abs(omega) < 1e-12] = 1e-12 # 避免零频奇异 H = 1.0 / (1j * omega - k) y_ext = np.fft.ifft(F * H) return y_ext[M : M + N] def _limit_fft_outliers_mad( fft_result: npt.NDArray[np.complex128], k_val: float ) -> tuple[npt.NDArray[np.complex128], bool]: """MAD(中位绝对偏差)离群抑制。 迁移自 qiao ``Solve_Wfunc_fft_imag._limit_fft_outliers_mad``:对 FFT 幅值超过 ``med_A + k_val·MAD/0.6745`` 的频点按比例缩回阈值,抑制 共振峰在频域产生的数值尖刺。 Returns: ``(corrected_fft, corrected_flag)``。 """ A = np.abs(fft_result) med_A = float(np.median(A)) mad = float(np.median(np.abs(A - med_A))) # MAD=0 时频谱无统计离散度(如常系数输入,FFT 仅零频非零), # ``threshold = med_A + k·0 = med_A`` 会把所有高于中位数的频点 # (含唯一非零的零频)当成离群点缩到 med_A,使整个 W 归零。此时 # "离群点"概念不成立——抑制对象是共振尖刺,而此类信号无尖刺, # 跳过即可。对 qiao 真实时变输入(MAD>0)零影响。 if mad == 0.0: return fft_result.copy(), False threshold = med_A + k_val * (mad / 0.6745) outliers = threshold < A corrected = bool(np.any(outliers)) if not corrected: return fft_result.copy(), False fft_limited = fft_result.copy() idx = np.where(outliers)[0] scale = threshold / A[idx] fft_limited[idx] = fft_result[idx] * scale return fft_limited, corrected def _solve_wfunc_fft_imag( tlist: npt.NDArray[np.floating], forcing: npt.NDArray[np.floating], k: complex, *, extension_ratio: float = _DEFAULT_EXTENSION_RATIO, mad_kval: float = _DEFAULT_MAD_KVAL, ) -> tuple[npt.NDArray[np.complex128], bool]: """频域求解 ``ẏ = k·y + f(t)`` (复值 ``f``,含 MAD 离群抑制)。 迁移自 qiao ``Solve_Wfunc_fft_imag``:结构与 :func:`_solve_wfunc_fft` 一致,但额外对 FFT 结果做 MAD 离群抑制,返回 ``(y, corrected)``。 ``corrected=True`` 表示发生了离群抑制(调用方据此切换 ``Ẇ`` 的 数值微分路径)。 """ t_arr = np.asarray(tlist, dtype=float).ravel() f_vals = np.asarray(forcing, dtype=complex).ravel() N = t_arr.size if N < 2: return np.zeros(N, dtype=complex), False # 常数输入:代数特解 W = -f/k(同 _solve_wfunc_fft)。同时避开 MAD # 抑制把唯一非零 DC 尖峰当离群点缩回(常数输入下 MAD 判定无意义)。 if np.max(np.abs(f_vals - f_vals[0])) <= 1e-2 * max(1.0, np.max(np.abs(f_vals))): return -np.full(N, f_vals[0], dtype=complex) / k, False dt = float(t_arr[1] - t_arr[0]) M = int(np.ceil(extension_ratio * N)) f_ext = np.concatenate([f_vals[:M][::-1], f_vals, f_vals[-M:][::-1]]) N_ext = f_ext.size F = np.fft.fft(f_ext) # 频率轴用 fftfreq(与 fft 输出顺序一致);见 _solve_wfunc_fft 注释。 freq = np.fft.fftfreq(N_ext, dt) omega = 2.0 * np.pi * freq omega[np.abs(omega) < 1e-12] = 1e-12 H = 1.0 / (1j * omega - k) Y_freq = F * H y_ext = np.fft.ifft(Y_freq) y = y_ext[M : M + N] Y_corrected, corrected = _limit_fft_outliers_mad(Y_freq, mad_kval) if corrected: y_corr = np.fft.ifft(Y_corrected)[M : M + N] return y_corr, True return y, False # --------------------------------------------------------------------------- # 高阶数值微分(迁移自 qiao list_deriv) # --------------------------------------------------------------------------- def _vandermonde_deriv_coeffs(n: int, mode: int) -> npt.NDArray[np.floating]: """Vandermonde 系统求数值微分系数。 ``mode=0`` 中心、``1`` 前向、``-1`` 后向,节点数 ``n+1``。 对应 qiao ``list_deriv._vandermonde_coeffs``。 """ if mode == 0: k = np.arange(-n // 2, n // 2 + 1) elif mode == 1: k = np.arange(0, n + 1) elif mode == -1: k = np.arange(-n, 1) else: raise ValueError(f"未知 mode: {mode}") if k.size <= 1: return np.array([0.0]) V = np.vander(k, increasing=True).T b = np.zeros(n + 1) b[1] = 1.0 return np.linalg.solve(V, b)
[文档] def list_deriv( y: npt.ArrayLike, h: float, n: int = _DEFAULT_DERIV_N, swi: int = 4, ord_boundary: int = 10, ) -> npt.NDArray[np.floating]: """等距采样高阶数值微分。 迁移自 qiao ``list_deriv``:内部 ``n`` 阶中心差分,两端用 ``swi`` 点前/后向差分(阶 ``ord_boundary``),过渡区用递降阶中心 差分。复值 ``y`` 自动转实/虚部分别处理。 Args: y: ``(N,)`` 等距采样数据(实或复)。 h: 采样间距。 n: 中心差分阶(偶数)。 swi: 端点前/后向差分点数。 ord_boundary: 端点差分阶。 Returns: ``(N,)`` 导数 ``dy/dt``。 """ y_arr = np.asarray(y) if np.iscomplexobj(y_arr): return list_deriv(y_arr.real, h, n, swi, ord_boundary) + 1j * list_deriv( y_arr.imag, h, n, swi, ord_boundary ) y_arr = y_arr.ravel() N = y_arr.size dy = np.zeros(N, dtype=float) half = n // 2 # 内部:n 阶中心差分 if n < N: c = _vandermonde_deriv_coeffs(n, mode=0) for idx_k, kk in enumerate(range(-half, half + 1)): dy[half : N - half] += c[idx_k] * y_arr[half + kk : N - half + kk] dy[half : N - half] /= h # 前向/后向端点 for i in range(min(swi, N)): ord_eff = min(ord_boundary, N - i - 1) if ord_eff < 1: dy[i] = (y_arr[i + 1] - y_arr[i]) / h if i + 1 < N else 0.0 else: c_fwd = _vandermonde_deriv_coeffs(ord_eff, mode=1) dy[i] = float(np.dot(c_fwd, y_arr[i : i + ord_eff + 1])) / h for i in range(min(swi, N)): ri = N - 1 - i ord_eff = min(ord_boundary, ri) if ord_eff < 1: dy[ri] = (y_arr[ri] - y_arr[ri - 1]) / h if ri > 0 else 0.0 else: c_bwd = _vandermonde_deriv_coeffs(ord_eff, mode=-1) dy[ri] = float(np.dot(c_bwd, y_arr[ri - ord_eff : ri + 1])) / h # 过渡区(swi..half-1):递降阶中心差分 for i in range(swi, min(half, N - swi)): span = 2 * i if span < 2: continue c_center = _vandermonde_deriv_coeffs(span, mode=0) offset = span // 2 acc = 0.0 for idx_k, kk in enumerate(range(-offset, offset + 1)): acc += c_center[idx_k] * y_arr[i + kk] dy[i] = acc / h ri = N - 1 - i acc = 0.0 for idx_k, kk in enumerate(range(-offset, offset + 1)): acc += c_center[idx_k] * y_arr[ri + kk] dy[ri] = acc / h return dy
# --------------------------------------------------------------------------- # 同调方程判别条件 # --------------------------------------------------------------------------- def _is_invariant_term(pow_tuple: tuple[int, ...]) -> bool: """Step 1(invariant)保留条件:``pow(1)==pow(4)``。 满足此条件的 ``q_1^k·p_1^k`` 项是双曲"循环坐标"作用量 ``I_1 = q_1·p_1`` 的分量,**保留**;其余 ``q_1^k·p_1^l (k≠l)`` 双曲 交叉项被 ``W`` 消去。对应 qiao ``Code10`` 的 ``if n1 == n4`` 分支。 """ return int(pow_tuple[0]) == int(pow_tuple[3]) def _is_center_term(pow_tuple: tuple[int, ...]) -> bool: """Step 2(center)保留条件:三对共轭全部平衡。 ``pow(1)==pow(4) && pow(2)==pow(5) && pow(3)==pow(6)``。满足时该项 仅依赖作用量(``I_1``、``I_2``、``I_3`` 的幂次),**保留**;其余 含中心方向非共振耦合的项被消去。对应 qiao ``Code11`` 的 ``if n1 == n4 and n2 == n5 and n3 == n6`` 分支。 """ return ( int(pow_tuple[0]) == int(pow_tuple[3]) and int(pow_tuple[1]) == int(pow_tuple[4]) and int(pow_tuple[2]) == int(pow_tuple[5]) ) # --------------------------------------------------------------------------- # 单步 Lie 变换(Code10 / Code11 通用骨架) # --------------------------------------------------------------------------- def _polylist_to_complex( poly: Mapping[tuple[int, ...], npt.ArrayLike], ) -> dict[tuple[int, ...], npt.NDArray[np.complex128]]: """把实/复值系数表统一转为复值(W 封装用,保留 invariant/center 两步的复值 W)。""" return {k: np.asarray(v, dtype=complex) for k, v in poly.items()} def _lie_transform_step( H_by_order: dict[int, dict[tuple[int, ...], npt.NDArray[np.complex128]]], *, max_order: int, tlist: npt.NDArray[np.floating], lam: float, wp: float, wv: float, keep_criterion, delete_criterion, use_imag_solver: bool, ) -> tuple[ dict[int, dict[tuple[int, ...], npt.NDArray[np.complex128]]], dict[int, dict[tuple[int, ...], npt.NDArray[np.complex128]]], ]: """执行一步中心流形化简的 Lie 变换(Code10/Code11 通用骨架)。 Args: H_by_order: ``{order: {pow: coef}}``,被原地更新。系数为**复坐标** (调用方已做虚变换),同调方程的复值特征频率 ``k`` 与复对角形 ``H_2`` 的泊松谱匹配。 max_order: 截断阶数。 tlist: 时间序列。 lam, wp, wv: 频率(双曲/平面/垂直)。 keep_criterion: 保留判据(决定**是否对该项求 W**);满足者跳过 (W 记零)。Code10 用 ``_is_invariant_term`` (``pow1==pow4``), Code11 用 ``_is_center_term`` (三对全平衡)。 delete_criterion: 该阶处理完后**删除** 不满足此判据的项。 Code10 与 Code11 不同:Code10 删双曲不平衡项 (``pow1≠pow4``),Code11 只删真正求过 W 的项 (``list_iseliminate``)。 use_imag_solver: ``True`` 用复值 + MAD 求解器(Step 2), ``False`` 用实值求解器(Step 1)。 Returns: ``(H_by_order, W_series)``:``W_series`` 为 ``{order: {pow: W_coef}}`` (复值,``W_coef`` 时间序列)。 Notes: 忠实迁移 qiao ``Code10``/``Code11`` 的逐阶循环: 1. 对每个 ``order ∈ [3, max_order]``,遍历 ``H_order`` 中不满足 ``keep_criterion`` 的项,求同调方程特解 ``W`` 与 ``Ẇ``; 2. Poisson 括号链累加 ``ad_W^n / n!`` 到更高阶 Hamiltonian (``j`` 从 2 起,``cur_order = j + order − 2`` 递增); 3. 加 ``Ẇ`` 项,按 ``delete_criterion`` 删除该阶残余/新生项。 """ W_series: dict[int, dict[tuple[int, ...], npt.NDArray[np.complex128]]] = {} N = tlist.size for order in range(3, max_order + 1): if order not in H_by_order: H_by_order[order] = {} H_order = H_by_order[order] W_temp: dict[tuple[int, ...], npt.NDArray[np.complex128]] = {} Wd_temp: dict[tuple[int, ...], npt.NDArray[np.complex128]] = {} eliminated: list[tuple[int, ...]] = [] for pow_tuple, coef_arr in list(H_order.items()): if keep_criterion(pow_tuple): # 保留项(共振/作用量项):不生成 W,W 记零 W_temp[pow_tuple] = np.zeros(N, dtype=complex) continue coef_c = np.asarray(coef_arr, dtype=complex) k = _characteristic_freq(pow_tuple, lam, wp, wv) # Step 1:Ẇ = -k·W - coef(qiao Code10 同调方程闭合形式)。 # Step 2:先用复值+MAD 求 W;若 MAD 抑制触发则 Ẇ 走数值微分, # 否则 Ẇ = k·W + coef(再取负,见下方 if-else 实现)。 if not use_imag_solver: W_func = _solve_wfunc_fft(tlist, coef_c.real, k) Wd_func = -k * W_func - coef_c else: W_func, corrected = _solve_wfunc_fft_imag(tlist, coef_c, k) if not corrected: Wd_func = k * W_func + coef_c else: dt = float(np.mean(np.diff(tlist))) Wd_func = list_deriv(W_func, dt) Wd_func = -Wd_func W_temp[pow_tuple] = W_func Wd_temp[pow_tuple] = Wd_func eliminated.append(pow_tuple) W_series[order] = W_temp # Poisson 括号链:ad_W^n / n! 累加到高阶 Hamiltonian for j in range(2, max_order): if j not in H_by_order or not H_by_order[j]: continue H_j = H_by_order[j] cur_order = j + order - 2 if cur_order > max_order: continue num = 1 # poly_poisson 在本数值路径下输入/输出系数均为 ndarray,但其签名 # 为 object(兼顾 sympy 路径);用 Any 局部标注表达 cascade 运算。 p_prev: Any = H_j while cur_order <= max_order: p: dict[tuple[int, ...], Any] = poly_poisson(p_prev, W_temp) for kk in p: p[kk] = p[kk] / factorial(num) num += 1 if cur_order not in H_by_order: H_by_order[cur_order] = {} target = H_by_order[cur_order] for kk, v in p.items(): existing: Any = target.get(kk, 0.0) target[kk] = existing + v cur_order += order - 2 p_prev = p # 加 Ẇ 项 if order in H_by_order: target = H_by_order[order] for kk, v in Wd_temp.items(): # Wd 项为复值;与既有(可能复值)累加项相加。 cur: Any = target.get(kk, 0.0) target[kk] = cur + v # 按 delete_criterion 删除该阶残余/新生项(Code10 与 Code11 不同) if order in H_by_order: H_by_order[order] = { k: v for k, v in H_by_order[order].items() if delete_criterion(k, eliminated) } if not H_by_order[order]: H_by_order[order] = {(0, 0, 0, 0, 0, 0): np.zeros(N, dtype=complex)} # 化简各阶 H_by_order = {o: polylist_simplify(v) for o, v in H_by_order.items() if v} return H_by_order, W_series def _delete_invariant(pow_tuple, eliminated: list[tuple[int, ...]]) -> bool: """Code10 删除判据:保留双曲平衡项(``pow(1)==pow(4)``)。 qiao ``Code10`` 第 280–287 行:``if pow(1) == pow(4) continue`` (``pow(2)==pow(5) && pow(3)==pow(6)`` 在 qiao 源码中被注释掉)—— 只删双曲方向不平衡的项,中心方向不平衡项**保留** 给 Step 2 (``center``)处理。 """ return int(pow_tuple[0]) == int(pow_tuple[3]) def _delete_center(pow_tuple, eliminated: list[tuple[int, ...]]) -> bool: """Code11 删除判据:保留非 eliminated 的项。 qiao ``Code11`` 第 146–148 行:只删 ``list_iseliminate`` (即本阶求 过 W 的项),其他项(包括 Poisson cascade 新生成的)保留。Code11 的 ``keep_criterion`` 已是三对全平衡,故非 eliminated 项天然是作用量项。 """ return pow_tuple not in eliminated # --------------------------------------------------------------------------- # Reducer # ---------------------------------------------------------------------------
[文档] @dataclass(frozen=True) class CenterManifoldReducer: """中心流形化简 reducer(上下文绑定)。 通过 :meth:`reduce` 在 quasi-Floquet 化简后的 Hamiltonian 上执行 一步或两步 Lie 变换,消去双曲-中心耦合与中心方向间非共振耦合, 输出 :class:`CenterManifoldResult`。 默认 ``backend="rust"`` 走完整 Rust 数值内核(#466); ``backend="python"`` 仅作显式等价性对照,绝不自动回退(ADR 0020)。 Args: context: 归一化上下文(提供频率 ``ω_p``/``ω_v``、特征指数 λ)。 max_order: Lie 变换截断阶数,默认 ``10`` (与 qiao 一致)。 backend: 数值后端,``"rust"``(默认)或 ``"python"``。 """ context: NormalFormContext max_order: int = DEFAULT_MAX_ORDER backend: Literal["rust", "python"] = "rust"
[文档] def reduce( self, qf_result: QuasiFloquetResult, hamiltonian_terms: Mapping[tuple[int, ...], npt.ArrayLike] | None = None, steps: tuple[str, ...] | None = None, ) -> CenterManifoldResult: """对 ``qf_result`` 执行中心流形化简。 Args: qf_result: quasi-Floquet 变换结果(切片 #172),提供实标准形 ``D`` (频率 ``λ``/``ω_p``/``ω_v``)与采样时间 ``tlist``。 hamiltonian_terms: 高阶 Hamiltonian 系数表 ``{pow_tuple: coef_array}`` (对应 qiao ``Code09`` 的 ``L?_QF_Hamilton.npz``)。``None`` 时 reducer 只用二阶实 标准形项(平凡情形,仅供 smoke)。系数为时间序列 ``ndarray``,长度应与 ``qf_result.tlist`` 一致。 steps: 要执行的步骤元组,按顺序;默认 ``("invariant", "center")``。可选单步 ``("invariant",)`` 或 ``("center",)``。 Returns: :class:`CenterManifoldResult`。 Raises: ValueError: ``steps`` 含非法名;``max_order`` 非正; ``hamiltonian_terms`` 系数长度与 ``tlist`` 不一致; ``backend`` 非法。 """ if self.backend not in ("rust", "python"): raise ValueError(f"backend 须为 'rust' 或 'python',得到 {self.backend!r}") valid_steps = {"invariant", "center"} steps_resolved = tuple(steps) if steps is not None else ("invariant", "center") bad = [s for s in steps_resolved if s not in valid_steps] if bad: raise ValueError(f"steps 只能含 {sorted(valid_steps)},得到非法值:{bad}") if self.max_order < 1: raise ValueError(f"max_order 必须为正,得到 {self.max_order}") # 频率:从 qf_result 的实标准形 D 提取(与 QF 变换 B 严格自洽)。 # constant 方法(CR3BP)的 D 是 V⁻¹MV 的数值(M 特征值),与 # context 固化频率可有 0.3% 失谐;用 D 提取保证同调方程的谱 # k 与 H₂ 精确匹配。 D_qf = np.asarray(qf_result.D, dtype=float).reshape(6, 6) lam = float(D_qf[0, 0]) wp = float(abs(D_qf[1, 4])) wv = float(abs(D_qf[2, 5])) tlist = np.asarray(qf_result.tlist, dtype=float).ravel() N = tlist.size if self.backend == "rust": return self._reduce_rust( qf_result, hamiltonian_terms, steps_resolved, tlist, N, lam, wp, wv, ) return self._reduce_python( qf_result, hamiltonian_terms, steps_resolved, tlist, N, lam, wp, wv, )
def _reduce_rust( self, qf_result: QuasiFloquetResult, hamiltonian_terms: Mapping[tuple[int, ...], npt.ArrayLike] | None, steps_resolved: tuple[str, ...], tlist: npt.NDArray[np.floating], N: int, lam: float, wp: float, wv: float, ) -> CenterManifoldResult: """Rust 完整 reduce 路径(#466)。""" from e2m2e.integrators import center_manifold_reduce_py, require_rust_extension require_rust_extension("center_manifold_reduce_py") higher_pows: list[list[int]] = [] higher_coefs: list[list[float]] = [] if hamiltonian_terms: for pow_tuple, coef in hamiltonian_terms.items(): pow_list = [int(p) for p in pow_tuple] deg = sum(pow_list) if deg < 1 or deg > self.max_order: continue arr = np.asarray(coef, dtype=float).ravel() if arr.size == 1: arr = np.full(N, float(arr[0])) if arr.size != N: raise ValueError( f"hamiltonian_terms 系数长度 {arr.size} 与 tlist " f"长度 {N} 不一致(pow={tuple(pow_list)})" ) higher_pows.append(pow_list) higher_coefs.append(arr.tolist()) w_entries, h_pows, h_coefs, pre_coupling, steps_done = center_manifold_reduce_py( tlist.tolist(), lam, wp, wv, int(self.max_order), list(steps_resolved), higher_pows, higher_coefs, ) W_all: dict[str, dict[int, dict[tuple[int, ...], npt.NDArray[np.complex128]]]] = {} for step_name, order, pow_vec, re, im in w_entries: pow_t = tuple(int(p) for p in pow_vec) coef = np.asarray(re, dtype=np.float64) + 1j * np.asarray(im, dtype=np.float64) W_all.setdefault(step_name, {}).setdefault(int(order), {})[pow_t] = coef final_terms: dict[tuple[int, ...], npt.NDArray[np.floating]] = {} for pow_vec, coef in zip(h_pows, h_coefs, strict=True): pow_key: tuple[int, ...] = tuple(int(p) for p in pow_vec) final_terms[pow_key] = np.asarray(coef, dtype=float) return CenterManifoldResult( context=self.context, order=int(self.max_order), W_series=W_all, hamiltonian_terms=final_terms, steps_performed=tuple(steps_done), metadata={ "lambda": lam, "wp": wp, "wv": wv, "n_samples": N, "pre_hyperbolic_center_coupling": float(pre_coupling), "max_order": int(self.max_order), "backend": "rust", }, ) def _reduce_python( self, qf_result: QuasiFloquetResult, hamiltonian_terms: Mapping[tuple[int, ...], npt.ArrayLike] | None, steps_resolved: tuple[str, ...], tlist: npt.NDArray[np.floating], N: int, lam: float, wp: float, wv: float, ) -> CenterManifoldResult: """Python 参照路径(显式 ``backend='python'``)。""" # 组装初始 Hamiltonian 多项式表 {order: {pow: coef}} # 系数在化简循环中在实/复坐标间切换(虚变换 → 复,取实部 → 实), # 故此处用 Any(numpy dtype 跨域,静态类型无法追踪)。 H_by_order: Any = self._assemble_hamiltonian( qf_result, hamiltonian_terms, tlist, N, lam, wp, wv ) W_all: dict[str, dict[int, dict[tuple[int, ...], npt.NDArray[np.complex128]]]] = {} steps_done: list[str] = [] for step in steps_resolved: # 虚变换:实坐标 → 复坐标。二阶部分化为复对角形 # λ·y1·y4 + i·ω_p·y2·y5 + i·ω_v·y3·y6 后,同调方程的特征 # 频率 k(_characteristic_freq 的复值公式)才与泊松谱匹配。 # 对应 qiao Code10/Code11 的 ``X = D·Y`` 虚变换。 H_by_order = _linear_basis_change(H_by_order, _D) if step == "invariant": H_by_order, W_step = _lie_transform_step( H_by_order, max_order=self.max_order, tlist=tlist, lam=lam, wp=wp, wv=wv, keep_criterion=_is_invariant_term, delete_criterion=_delete_invariant, use_imag_solver=False, ) else: # center H_by_order, W_step = _lie_transform_step( H_by_order, max_order=self.max_order, tlist=tlist, lam=lam, wp=wp, wv=wv, keep_criterion=_is_center_term, delete_criterion=_delete_center, use_imag_solver=True, ) # 实变换:复坐标 → 实坐标,取实部吸收数值虚部噪声。 # 对应 qiao Code10/Code11 末尾的 ``Y = D⁻¹·X`` 实变换。 H_by_order = _linear_basis_change(H_by_order, _D_INV) H_by_order = { o: {k: np.real(np.asarray(v, dtype=complex)) for k, v in poly.items()} for o, poly in H_by_order.items() } # W 保留复坐标:Step 1(invariant)的双曲特征频率 λ 为实数,W # 天然实值;Step 2(center)的中心频率为纯虚 iω,同调方程特解 # W 为纯虚(实部≈0)。coord_trans 的 QF↔CM Lie 流在复域操作, # 必须拿到完整复值 W,故此处不取实部。对应 qiao Code10/Code11 # 输出复值 ``L?_InvarManifold.npz`` / ``L?_CenterManifold.npz``。 W_all[step] = {o: _polylist_to_complex(w) for o, w in W_step.items()} steps_done.append(step) # 汇总化简后 Hamiltonian(所有阶合并成一个 pow→coef 表) final_terms: dict[tuple[int, ...], npt.NDArray[np.floating]] = {} for o in sorted(H_by_order.keys()): for k, v in H_by_order[o].items(): v_arr = np.real(np.asarray(v, dtype=complex)) if k in final_terms: final_terms[k] = final_terms[k] + v_arr else: final_terms[k] = v_arr final_terms = polylist_simplify(final_terms) # 化简前最大双曲-中心耦合(诊断:用原始 hamiltonian_terms) pre_coupling = _max_hyperbolic_center_coupling(self._flat_terms(hamiltonian_terms, N)) return CenterManifoldResult( context=self.context, order=int(self.max_order), W_series=W_all, hamiltonian_terms=final_terms, steps_performed=tuple(steps_done), metadata={ "lambda": lam, "wp": wp, "wv": wv, "n_samples": N, "pre_hyperbolic_center_coupling": pre_coupling, "max_order": int(self.max_order), "backend": "python", }, ) # ------------------------------------------------------------------ # 内部辅助 # ------------------------------------------------------------------ def _assemble_hamiltonian( self, qf_result: QuasiFloquetResult, hamiltonian_terms: Mapping[tuple[int, ...], npt.ArrayLike] | None, tlist: npt.NDArray[np.floating], N: int, lam: float, wp: float, wv: float, ) -> dict[int, dict[tuple[int, ...], npt.NDArray[np.floating]]]: """组装 Lie 变换输入的 Hamiltonian 多项式表。 二阶项来自实标准形(``λ·q_1·p_1 + (ω_p/2)(q_2²+p_2²) + (ω_v/2)(q_3²+p_3²)``,对应 qiao ``Code09`` 的 ``NormalForm_poly``); 高阶项来自 ``hamiltonian_terms`` (若提供)。按总阶数分组。 """ H_by_order: dict[int, dict[tuple[int, ...], npt.NDArray[np.floating]]] = {} def _put(deg: int, key: tuple[int, ...], val: np.ndarray) -> None: H_by_order.setdefault(deg, {}) if key in H_by_order[deg]: H_by_order[deg][key] = H_by_order[deg][key] + val else: H_by_order[deg][key] = val # 二阶实标准形(qiao Code09 NormalForm_poly) _put(2, (1, 0, 0, 1, 0, 0), lam * np.ones(N)) _put(2, (0, 2, 0, 0, 0, 0), (wp / 2) * np.ones(N)) _put(2, (0, 0, 0, 0, 2, 0), (wp / 2) * np.ones(N)) _put(2, (0, 0, 2, 0, 0, 0), (wv / 2) * np.ones(N)) _put(2, (0, 0, 0, 0, 0, 2), (wv / 2) * np.ones(N)) # 高阶项(Code09 输出) if hamiltonian_terms: for pow_tuple, coef in hamiltonian_terms.items(): key = tuple(int(p) for p in pow_tuple) deg = sum(key) if deg < 1 or deg > self.max_order: continue arr = np.asarray(coef, dtype=float).ravel() if arr.size == 1: arr = np.full(N, float(arr[0])) if arr.size != N: raise ValueError( f"hamiltonian_terms 系数长度 {arr.size} 与 tlist " f"长度 {N} 不一致(pow={key})" ) _put(deg, key, arr) return H_by_order @staticmethod def _flat_terms( hamiltonian_terms: Mapping[tuple[int, ...], npt.ArrayLike] | None, N: int, ) -> dict[tuple[int, ...], npt.NDArray[np.floating]]: """把注入的高阶项规整为扁平 ``{pow: coef}`` 表(诊断用)。""" out: dict[tuple[int, ...], npt.NDArray[np.floating]] = {} if hamiltonian_terms: for pow_tuple, coef in hamiltonian_terms.items(): arr = np.asarray(coef, dtype=float).ravel() if arr.size == 1: arr = np.full(N, float(arr[0])) out[tuple(int(p) for p in pow_tuple)] = arr return out
def _max_hyperbolic_center_coupling( terms: Mapping[tuple[int, ...], npt.ArrayLike], ) -> float: """计算 Hamiltonian 表中双曲-中心**耦合** 项系数的最大绝对值。 耦合指**双曲方向不平衡** (``pow[0] != pow[3]``)且涉及中心方向的 项——Step 1(``invariant``)的消去对象。双曲方向平衡的项(如 ``I₁³·I₂²``)只是作用量组合:在中心流形上 ``q₁=p₁=0`` 时自动为零, 不是耦合,不统计在内。 """ mx = 0.0 for pow_tuple, coef in terms.items(): if int(pow_tuple[0]) == int(pow_tuple[3]): continue # 双曲平衡:作用量项,非耦合 center = int(pow_tuple[1]) + int(pow_tuple[4]) + int(pow_tuple[2]) + int(pow_tuple[5]) if center > 0: arr = np.asarray(coef, dtype=float).ravel() m = float(np.max(np.abs(arr))) if arr.size else 0.0 if m > mx: mx = m return mx __all__ = [ "DEFAULT_MAX_ORDER", "CenterManifoldReducer", "CenterManifoldResult", "list_deriv", ]