本文定义 @epheon/phenomena 的第二阶段最小模型。
它回答:
Epheon 如何从连续天体运动中求出离散事件?
二十四节气的目标黄经如何定义?
朔望事件的角度条件是什么?
求解时使用什么时间输入、什么时间输出?
默认求解算法是什么,哪些优化暂缓?
哪些能力属于 phenomena,哪些不属于?本文目标不是一次性覆盖日月食、出没、站心观测或所有天象事件,而是先锁定:
Solar Terms
New Moon
Full Moon
最小事件搜索边界
最小求根策略这样第二阶段可以先完成节气求解,再为第三阶段的朔望求解留出稳定接口。
@epheon/phenomena 负责:
SolarTermName
SolarTermEvent
LunarPhaseKind
LunarPhaseEvent
基于连续角度函数的事件搜索
二分法求根与可选 Newton refinement 策略
事件定义与返回值模型@epheon/phenomena 不负责:
太阳或月亮位置算法
VSOP87 / ELP2000 / JPL 实现
Delta-T 数据快照
闰秒数据快照
时区数据库
农历日期、闰月、干支
八字、紫微斗数等术数解释其中:
Instant / DeltaTProvider / LeapSecondProvider 属于 @epheon/temporal。
Body / Position / ReferenceFrame 属于 @epheon/reference。
EphemerisProvider 属于 @epheon/ephemerides。
太阳与月亮位置实现属于 @epheon/ephemerides-*。
历法规则属于 @epheon/calendars 和 @epheon/calendar-chinese。天象事件求解必须遵守:
先定义事件条件,再选算法。
公共 API 使用 Instant 输入输出,不暴露裸 Julian 数值。
事件搜索不依赖系统时区、当前时间、文件系统或网络。
节气和朔望只依赖注入的 ephemeris 与时间尺度 provider。
默认算法优先选择稳健的二分法,不先暴露通用求根 DSL。
测试 tolerance 明确写在测试中,不把精度承诺偷塞进运行时常量。
phenomena 只知道天象,不知道历法。这些约束是为了让节气和朔望求解保持可替换、可验证、可组合,而不把历法规则或具体星历算法耦合进求解层。
第二阶段最小事件都属于:
存在一个连续角度函数 f(t)
目标是找到某个时刻 t,使得 f(t) = 0对应关系:
节气:太阳黄经 - 目标黄经 = 0
朔:月亮黄经 - 太阳黄经 = 0
望:月亮黄经 - 太阳黄经 = 180°这里的“连续”是指数值求解语义上的连续。实现可以在离散采样后找到包围区间,再用求根法细化。
@epheon/phenomena 不在第二阶段定义通用 EventCondition 抽象。
原因:
当前只需要节气与朔望。
过早设计通用事件 DSL 会把简单问题变复杂。
等出没、食、合、冲真的进入实现后再评估是否需要统一抽象。二十四节气按太阳地心黄经定义。
第二阶段固定使用:
太阳地心视黄经
ReferenceFrame.TrueOfDateEcliptic
Origin.Geocentric原因:
D1 当前 bootstrap fixture 使用 JPL Horizons 的 ObsEcLon(地心视黄经)。
standards/solar/terms.json 和 standards/solar/longitudes.json 已按这一语义生成。
phenomena 不应依赖 ephemeris provider 的默认 frame,而应显式请求目标 frame。二十四节气目标黄经如下:
春分 = 0°
清明 = 15°
谷雨 = 30°
立夏 = 45°
小满 = 60°
芒种 = 75°
夏至 = 90°
小暑 = 105°
大暑 = 120°
立秋 = 135°
处暑 = 150°
白露 = 165°
秋分 = 180°
寒露 = 195°
霜降 = 210°
立冬 = 225°
小雪 = 240°
大雪 = 255°
冬至 = 270°
小寒 = 285°
大寒 = 300°
立春 = 315°
雨水 = 330°
惊蛰 = 345°排序语义:
ofYear(year) 返回该 UTC Gregorian year 内发生的 24 个事件。
返回顺序按 Instant 从早到晚排序。
year 表示事件 instant 的 UTC 年,不引入时区换年语义。第二阶段不接受:
按中国时区或任意地区时区定义节气归属年
按历法月令解释节气用途
按地方历法规则重排节气边界这些都属于历法层或应用层。
第二阶段先锁定朔望的角度条件。
固定使用:
月亮地心视黄经
太阳地心视黄经
ReferenceFrame.TrueOfDateEcliptic
Origin.Geocentric事件条件:
朔:normalizeSigned(moonLongitude - sunLongitude - 0°) = 0
望:normalizeSigned(moonLongitude - sunLongitude - 180°) = 0其中 normalizeSigned 表示把角度差归一化到 [-180°, 180°),避免 359.999° 与 0.001° 被误判为跨越巨大差值。
本 RFC 不在第二阶段要求实现:
上弦(90°)
下弦(270°)
食相关事件
月球纬度条件
站心修正这些能力有真实实现需求时再扩展。
月亮事件进入实现前,最小 provider 需求是:
ephemeris provider 必须支持 Body.Moon
并能在 TrueOfDateEcliptic / Geocentric 语义下返回月亮位置第二阶段 @epheon/phenomena 的公共输入输出统一使用:
输入:Instant / year / 搜索时间窗
输出:Instant原因:
Instant 已经是项目稳定的时间点值对象。
节气和朔望的上层调用方最终需要的是“发生在什么时候”。
公共 API 不应让调用方直接持有裸 JDE 或 UT1 数值。时间尺度策略:
公共边界只暴露 Instant。
ephemeris 实现可以自行从 Instant 派生 TT / JDE / UT1。
phenomena 内部若需要更均匀的求解尺度,可以使用 provider 与 temporal 能力在实现中转换,但不把该细节推到公共 API。Delta-T 的进入方式:
phenomena 上层 context 必须显式持有 DeltaTProvider。
需要从 UTC 搜索窗转换到 UT1 / TT 推理时,由实现使用该 provider。
如果具体事件实现只通过 ephemerisProvider.position(instant) 评估事件函数,也可以暂时不直接调用 DeltaTProvider,但 context 仍保持显式。Leap second 的进入方式:
需要从 year 或原始 UTC 字面量生成 Instant 时,调用方必须提供 LeapSecondProvider。
原因是当前 ephemeris 实现可能通过 Instant 派生 TT / JDE,这一步需要 leap second 数据。因此第二阶段最小求解 context 应包含:
type PhenomenaContext = {
readonly ephemeris: EphemerisProvider;
readonly leapSeconds: LeapSecondProvider;
readonly deltaT: DeltaTProvider;
};这一定义属于 @epheon/phenomena,不回推到 @epheon/temporal 或 @epheon/ephemerides。
第二阶段默认算法固定为:
先粗采样找到变号区间
再用二分法收敛原因:
二分法实现简单。
二分法只要求区间内存在单根且函数符号可判定。
对 bootstrap 阶段的节气与朔望,比 Newton 法更稳。默认流程:
1. 根据 year 或调用方提供的搜索窗生成一组离散 Instant 样本。
2. 对相邻样本计算事件函数值。
3. 找到包含目标根的区间。
4. 在该区间上用二分法收敛。
5. 当区间宽度达到测试要求的时间 tolerance 后,返回 Instant。第二阶段不要求把“粗采样步长”做成公共可配置项。
原因:
节气与朔望当前都属于项目内部已知场景。
步长应先由实现根据 fixture 与稳定性决定。
如果未来出现性能压力,再在实现包 README 或独立 RFC 中引入配置。Newton 法是允许的优化,不是默认路径。
第二阶段策略:
必须先有已知包围区间。
Newton 只能作为二分法之上的 refinement。
若导数过小、步进离开包围区间或出现 NaN,必须回退到二分法。原因:
当前最小链路优先要稳,不优先追求最快。
节气与朔望函数都具有周期性;没有包围区间时,Newton 容易跳到错误根。因此 @epheon/phenomena 第二阶段不导出:
通用 NewtonSolver
通用 RootFinder 接口
多算法可插拔框架这些都等多个事件实现稳定后再考虑。
第二阶段最小结果建议如下:
export const SolarTermName = {
ChunFen: "春分",
QingMing: "清明",
// ...
JingZhe: "惊蛰"
} as const;
export type SolarTermName = (typeof SolarTermName)[keyof typeof SolarTermName];
export type SolarTermEvent = {
readonly name: SolarTermName;
readonly targetLongitude: Angle;
readonly instant: Instant;
};
export const LunarPhaseKind = {
NewMoon: "NEW_MOON",
FullMoon: "FULL_MOON"
} as const;
export type LunarPhaseKind = (typeof LunarPhaseKind)[keyof typeof LunarPhaseKind];
export type LunarPhaseEvent = {
readonly kind: LunarPhaseKind;
readonly targetLongitudeDifference: Angle;
readonly instant: Instant;
};约束:
结果对象只表达“是什么”和“发生在什么时候”。
不在结果对象中挂 provider、算法实例或大段调试状态。
误差说明、采样步长、迭代次数等实现细节可以先留给测试与 README,不急着做成公共字段。第二阶段最小 API 草案:
export type PhenomenaContext = {
readonly ephemeris: EphemerisProvider;
readonly leapSeconds: LeapSecondProvider;
readonly deltaT: DeltaTProvider;
};
export declare function solarTermsOfYear(
year: number,
context: PhenomenaContext
): readonly SolarTermEvent[];
export declare function findLunarPhaseBetween(
kind: LunarPhaseKind,
start: Instant,
end: Instant,
context: PhenomenaContext
): LunarPhaseEvent;说明:
solarTermsOfYear(year, ...) 先服务 C5。
findLunarPhaseBetween(...) 先服务 C6。
先用函数,不引入 class、builder 或 service 容器。第二阶段不导出:
通用 Event 类型
通用 RootFinder 类型
timezone 选项
calendar 选项
moonrise / sunrise API
eclipse API@epheon/phenomena 可以依赖:
Instant
DeltaTProvider
LeapSecondProvider
EphemerisProvider
Body
ReferenceFrame
Position
Angle@epheon/phenomena 不得反向依赖:
calendar
calendar-chinese
术数层
具体 dataset 包的内部数据表
其他 package 的 src/internal/*特别约束:
phenomena 可以依赖 ephemerides 协议,但不直接依赖具体 VSOP87 或 ELP2000 实现细节。
具体实现包由调用方注入。第二阶段验证遵循:
先 standards,再实现。
节气测试读取 standards/solar/terms.json。
朔望测试读取未来的 standards/lunar/ 对应 fixture。
每个测试必须显式写 tolerance。当前 tolerance 策略:
RFC 不写死单一秒数承诺。
D1 当前 terms fixture 来自 JPL 小时采样后线性插值,是 bootstrap 外部参考,不应在 RFC 中伪装成终极真值。
C5/C6 落地时,测试应根据 fixture 生成方法与当前实现误差显式声明 tolerance。这也是为什么第二阶段先完成 D1 和 C4,再写本 RFC。
第二阶段最小路径:
C4: 太阳黄经最小 provider
B3: RFC 0007 天象事件求解
C5: 二十四节气求解第三阶段继续:
月亮黄经 provider
C6: 朔望求解也就是说:
本 RFC 先为 C5 固定事件定义与求解策略。
朔望部分先固定边界,不要求第二阶段立刻实现。第二阶段暂缓:
上弦 / 下弦
日出日落 / 月出月落
食相关事件
通用事件 DSL
公开的可插拔求根框架
timezone-aware 的 ofYear 语义
基于站心或地平坐标的事件
调试元数据进入公共结果对象这些能力等 @epheon/phenomena 出现第二个、第三个稳定事件族后再决定是否进入公共接口。
第二阶段 RFC 0007 完成时,应证明:
明确了二十四节气对应的 0°、15°、30° 直到 345° 目标黄经。
明确了节气使用太阳地心视黄经、TrueOfDateEcliptic 与 Geocentric 语义。
明确了朔与望的角度条件。
明确了公共输入输出使用 Instant。
明确了 Delta-T 与 leap second 的进入方式。
明确了默认算法是先找包围区间再用二分法。
明确了 Newton 法只作为可选 refinement。
明确了月亮事件依赖支持 Body.Moon 的 ephemeris provider。
明确了 phenomena 不负责历法判断和时区数据库。
通过 pnpm format:check。这些约束固定后,C5 可以在稳定边界内实现二十四节气求解,C6 可以在月亮 provider 准备好后继续实现朔望。