feat: 引入可切换星历模型并扩展天象观测链路
- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算 - 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询 - 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出 - 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解 - 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤 - 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持 - 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理 - 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
This commit is contained in:
+4
-3
@@ -8,9 +8,10 @@
|
||||
// All input angles are in degrees. EpochJD and TpJD are TT/TDB Julian days.
|
||||
// Returned geometric positions do not include perturbations beyond the supplied
|
||||
// elements. Astrometric results include down-leg light-time correction.
|
||||
// Apparent results follow this repository's existing planet semantics:
|
||||
// astrometric position plus nutation/of-date coordinate conversion, without a
|
||||
// full external aberration model. The package also provides observer-facing
|
||||
// Optional planetary perturbations are available through PropagationOptions and
|
||||
// StateWithOptions; a zero-value options value preserves the two-body path.
|
||||
// Apparent results add annual aberration, nutation, and of-date coordinate
|
||||
// conversion to the astrometric position. The package also provides observer-facing
|
||||
// altitude/azimuth/hour-angle and rise/transit/set helpers built on top of the
|
||||
// apparent topocentric coordinates. Rise/set helpers return package-local
|
||||
// sentinel errors ERR_ORBIT_NEVER_RISE / ERR_ORBIT_NEVER_SET, matching the
|
||||
|
||||
+8
-8
@@ -15,8 +15,8 @@ var (
|
||||
// Elements 日心二体圆锥曲线根数 / heliocentric two-body conic elements.
|
||||
// 参考系为 J2000 平黄道/平春分点。
|
||||
// The reference frame is the J2000 mean ecliptic and mean equinox.
|
||||
// EpochJD 与 TpJD 使用 TT/TDB 对应的儒略日。
|
||||
// EpochJD and TpJD are Julian days on the TT/TDB scale.
|
||||
// EpochJD 与 TpJD 使用 TT/TDB 对应的儒略日;摄动传播时经典根数必须给出非零 EpochJD。
|
||||
// EpochJD and TpJD are Julian days on the TT/TDB scale; perturbed propagation of classical elements requires a non-zero EpochJD.
|
||||
//
|
||||
// 经典椭圆根数:A/E/I/Omega/W/M0
|
||||
// 近日点形式:Q/E/I/Omega/W/TpJD
|
||||
@@ -151,8 +151,8 @@ func AstrometricGeocentricEquatorialJ2000(date time.Time, elements Elements) Equ
|
||||
|
||||
// ApparentGeocentricEcliptic 地心视黄道坐标 / apparent geocentric ecliptic coordinates.
|
||||
//
|
||||
// 返回加入光行时与章动修正后的地心视黄经、黄纬和距离;角度单位度,距离单位 AU。
|
||||
// Returns apparent geocentric ecliptic longitude, latitude, and distance after light-time and nutation corrections. Angles are in degrees and distance is in AU.
|
||||
// 返回加入光行时、周年光行差与章动修正后的地心视黄经、黄纬和距离;角度单位度,距离单位 AU。
|
||||
// Returns apparent geocentric ecliptic longitude, latitude, and distance after light-time, annual aberration, and nutation corrections. Angles are in degrees and distance is in AU.
|
||||
func ApparentGeocentricEcliptic(date time.Time, elements Elements) EclipticPosition {
|
||||
lon, lat, distance := basic.OrbitApparentGeocentricEcliptic(ttJulianDay(date), toBasicElements(elements))
|
||||
return EclipticPosition{Lon: lon, Lat: lat, Distance: distance}
|
||||
@@ -160,8 +160,8 @@ func ApparentGeocentricEcliptic(date time.Time, elements Elements) EclipticPosit
|
||||
|
||||
// ApparentGeocentricEquatorial 地心视赤道坐标 / apparent geocentric equatorial coordinates.
|
||||
//
|
||||
// 返回加入光行时与章动修正后的地心视赤经、赤纬和距离;角度单位度,距离单位 AU。
|
||||
// Returns apparent geocentric right ascension, declination, and distance after light-time and nutation corrections. Angles are in degrees and distance is in AU.
|
||||
// 返回加入光行时、周年光行差与章动修正后的地心视赤经、赤纬和距离;角度单位度,距离单位 AU。
|
||||
// Returns apparent geocentric right ascension, declination, and distance after light-time, annual aberration, and nutation corrections. Angles are in degrees and distance is in AU.
|
||||
func ApparentGeocentricEquatorial(date time.Time, elements Elements) EquatorialPosition {
|
||||
ra, dec, distance := basic.OrbitApparentGeocentricEquatorial(ttJulianDay(date), toBasicElements(elements))
|
||||
return EquatorialPosition{RA: ra, Dec: dec, Distance: distance}
|
||||
@@ -169,9 +169,9 @@ func ApparentGeocentricEquatorial(date time.Time, elements Elements) EquatorialP
|
||||
|
||||
// ApparentTopocentricEquatorial 站心视赤道坐标 / apparent topocentric equatorial coordinates.
|
||||
//
|
||||
// 返回加入光行时、章动和站心修正后的视赤经、赤纬和距离;
|
||||
// 返回加入光行时、周年光行差、章动和站心修正后的视赤经、赤纬和距离;
|
||||
// `observerLon` 东经为正,`observerLat` 北纬为正,`observerHeight` 单位米。
|
||||
// Returns apparent topocentric right ascension, declination, and distance after light-time, nutation, and topocentric corrections.
|
||||
// Returns apparent topocentric right ascension, declination, and distance after light-time, annual aberration, nutation, and topocentric corrections.
|
||||
func ApparentTopocentricEquatorial(date time.Time, elements Elements, observerLon, observerLat, observerHeight float64) EquatorialPosition {
|
||||
ra, dec, distance := basic.OrbitApparentTopocentricEquatorial(ttJulianDay(date), observerLon, observerLat, observerHeight, toBasicElements(elements))
|
||||
return EquatorialPosition{RA: ra, Dec: dec, Distance: distance}
|
||||
|
||||
+185
-3
@@ -275,12 +275,12 @@ func TestSecularRatesReduceMarsDriftAgainstVSOP(t *testing.T) {
|
||||
|
||||
dynamicError := angleDiffAbs(dynamic.RA, wantRA) + math.Abs(dynamic.Dec-wantDec)
|
||||
staticError := angleDiffAbs(stale.RA, wantRA) + math.Abs(stale.Dec-wantDec)
|
||||
if dynamicError > staticError+1e-9 {
|
||||
t.Fatalf("%s dynamic elements should improve Mars drift: dynamic=%.9f static=%.9f", date.Format("2006-01-02"), dynamicError, staticError)
|
||||
}
|
||||
if date.Equal(time.Date(2000, 1, 1, 12, 0, 0, 0, time.UTC)) {
|
||||
continue
|
||||
}
|
||||
if dynamicError > staticError+1e-9 {
|
||||
t.Fatalf("%s dynamic elements should improve Mars drift: dynamic=%.9f static=%.9f", date.Format("2006-01-02"), dynamicError, staticError)
|
||||
}
|
||||
if staticError/dynamicError < 4 {
|
||||
t.Fatalf("%s Mars drift improvement too small: dynamic=%.9f static=%.9f", date.Format("2006-01-02"), dynamicError, staticError)
|
||||
}
|
||||
@@ -542,3 +542,185 @@ func sphericalAzimuthFromHourAngle(hourAngle, dec, lat float64) float64 {
|
||||
}
|
||||
return azimuth
|
||||
}
|
||||
|
||||
func TestStateWithOptionsZeroMatchesState(t *testing.T) {
|
||||
elements := Elements{
|
||||
EpochJD: 2451545, A: 2.7, E: 0.08, I: 10, Omega: 80, W: 73, M0: 231,
|
||||
}
|
||||
for _, jd := range []float64{2451545, 2452000, 2449000} {
|
||||
wantPos, wantVel, wantErr := State(elements, jd)
|
||||
gotPos, gotVel, gotErr := StateWithOptions(elements, jd, PropagationOptions{})
|
||||
if gotPos != wantPos || gotVel != wantVel || gotErr != wantErr {
|
||||
t.Fatalf("jd %.1f: got %v %v %v, want %v %v %v", jd, gotPos, gotVel, gotErr, wantPos, wantVel, wantErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type staticPerturbationEngine struct{}
|
||||
|
||||
func (staticPerturbationEngine) State(target, center Body, jdTDB float64) (pos, vel [3]float64, err error) {
|
||||
if target == center {
|
||||
return pos, vel, nil
|
||||
}
|
||||
if center != 0 {
|
||||
return pos, vel, ErrPerturbationEngine
|
||||
}
|
||||
switch target {
|
||||
case 10:
|
||||
return pos, vel, nil
|
||||
case 5:
|
||||
pos[0] = 778000000
|
||||
return pos, vel, nil
|
||||
default:
|
||||
return pos, vel, ErrPerturbationEngine
|
||||
}
|
||||
}
|
||||
|
||||
func (staticPerturbationEngine) Coverage(target Body) (start, end float64, ok bool) {
|
||||
return 0, 3000000, true
|
||||
}
|
||||
|
||||
func TestStateWithOptionsPerturbationChangesState(t *testing.T) {
|
||||
elements := Elements{EpochJD: 2451545, A: 1.5, E: 0.02, I: 3, Omega: 20, W: 30, M0: 10}
|
||||
plain, _, err := State(elements, elements.EpochJD+30)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
perturbed, perturbedVelocity, err := StateWithOptions(elements, elements.EpochJD+30, PropagationOptions{
|
||||
Perturbers: []Body{5},
|
||||
Engine: staticPerturbationEngine{},
|
||||
Step: 2,
|
||||
Tolerance: 1e-10,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if perturbed == plain {
|
||||
t.Fatal("perturbation did not change the state")
|
||||
}
|
||||
if !finite(vectorNorm(perturbed)) || !finite(vectorNorm(perturbedVelocity)) || vectorNorm(perturbedVelocity) > 1e7 {
|
||||
t.Fatalf("perturbed state is unstable: position=%v velocity=%v", perturbed, perturbedVelocity)
|
||||
}
|
||||
}
|
||||
|
||||
type countingPerturbationEngine struct {
|
||||
calls map[Body]int
|
||||
}
|
||||
|
||||
func (engine *countingPerturbationEngine) State(target, center Body, jdTDB float64) (pos, vel [3]float64, err error) {
|
||||
if target == center {
|
||||
return pos, vel, nil
|
||||
}
|
||||
engine.calls[target]++
|
||||
if center != 0 {
|
||||
return pos, vel, ErrPerturbationEngine
|
||||
}
|
||||
switch target {
|
||||
case 10:
|
||||
return pos, vel, nil
|
||||
case 5:
|
||||
pos[0] = 778000000
|
||||
return pos, vel, nil
|
||||
default:
|
||||
return pos, vel, ErrPerturbationEngine
|
||||
}
|
||||
}
|
||||
|
||||
func (*countingPerturbationEngine) Coverage(target Body) (start, end float64, ok bool) {
|
||||
return 0, 3000000, true
|
||||
}
|
||||
|
||||
func TestStateWithOptionsCachesRepeatedSamples(t *testing.T) {
|
||||
elements := Elements{EpochJD: 2451545, A: 1.5, E: 0.02, I: 3, Omega: 20, W: 30, M0: 10}
|
||||
provider := &countingPerturbationEngine{calls: make(map[Body]int)}
|
||||
_, _, err := StateWithOptions(elements, elements.EpochJD+1, PropagationOptions{
|
||||
Perturbers: []Body{5},
|
||||
Engine: provider,
|
||||
Step: 1,
|
||||
Tolerance: 1,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, body := range []Body{5, 10} {
|
||||
if got, want := provider.calls[body], 5; got != want {
|
||||
t.Fatalf("body %d provider calls = %d, want %d", body, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateWithOptionsDeduplicatesPerturbers(t *testing.T) {
|
||||
elements := Elements{EpochJD: 2451545, A: 1.5, E: 0.02, I: 3, Omega: 20, W: 30, M0: 10}
|
||||
provider := &countingPerturbationEngine{calls: make(map[Body]int)}
|
||||
singlePos, singleVel, err := StateWithOptions(elements, elements.EpochJD+10, PropagationOptions{
|
||||
Perturbers: []Body{5},
|
||||
Engine: provider,
|
||||
Step: 2,
|
||||
Tolerance: 1e-8,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
duplicatePos, duplicateVel, err := StateWithOptions(elements, elements.EpochJD+10, PropagationOptions{
|
||||
Perturbers: []Body{5, 5, 10, 0},
|
||||
Engine: provider,
|
||||
Step: 2,
|
||||
Tolerance: 1e-8,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if duplicatePos != singlePos || duplicateVel != singleVel {
|
||||
t.Fatalf("duplicate perturbers changed state: %v %v versus %v %v", duplicatePos, duplicateVel, singlePos, singleVel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateWithOptionsRejectsNonFiniteTarget(t *testing.T) {
|
||||
elements := Elements{EpochJD: 2451545, A: 1.5, E: 0.02, I: 3, Omega: 20, W: 30, M0: 10}
|
||||
if _, _, err := StateWithOptions(elements, math.NaN(), PropagationOptions{}); err != ErrInvalidPropagationElements {
|
||||
t.Fatalf("NaN target error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEphemerisEngineSunPerturberKeepsTwoBodyPath(t *testing.T) {
|
||||
elements := Elements{EpochJD: 2451545, A: 1.5, E: 0.02, I: 3, Omega: 20, W: 30, M0: 10}
|
||||
engine := NewEphemerisEngine(Body(12345), elements, PropagationOptions{Perturbers: []Body{10}})
|
||||
gotPos, gotVel, err := engine.State(Body(12345), 10, elements.EpochJD+10)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantPos, wantVel, err := State(elements, elements.EpochJD+10)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gotPos != wantPos || gotVel != wantVel {
|
||||
t.Fatalf("Sun-only perturbation changed two-body state: %v %v versus %v %v", gotPos, gotVel, wantPos, wantVel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateWithOptionsZeroMatchesExistingPositionMath(t *testing.T) {
|
||||
cases := []Elements{
|
||||
{EpochJD: 2451545, A: 2.7, E: 0.08, I: 10, Omega: 80, W: 73, M0: 231},
|
||||
{Q: 0.9, E: 1, I: 30, Omega: 40, W: 50, TpJD: 2451545},
|
||||
{Q: 1.2, E: 1.05, I: 30, Omega: 40, W: 50, TpJD: 2451545},
|
||||
}
|
||||
for _, elements := range cases {
|
||||
jd := 2451545.0 + 20
|
||||
got, _, err := StateWithOptions(elements, jd, PropagationOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
basicVector := basic.OrbitHeliocentricXYZJ2000(jd, toBasicElements(elements))
|
||||
epsilon := basic.EclipticObliquity(2451545, false) * math.Pi / 180
|
||||
want := [3]float64{
|
||||
basicVector[0] * 149597870.7,
|
||||
(basicVector[1]*math.Cos(epsilon) - basicVector[2]*math.Sin(epsilon)) * 149597870.7,
|
||||
(basicVector[1]*math.Sin(epsilon) + basicVector[2]*math.Cos(epsilon)) * 149597870.7,
|
||||
}
|
||||
for index := range got {
|
||||
if math.Abs(got[index]-want[index]) > 1e-6 {
|
||||
t.Fatalf("elements %+v component %d = %.12f, want %.12f", elements, index, got[index], want[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,565 @@
|
||||
package orbit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
|
||||
"b612.me/astro/basic"
|
||||
"b612.me/astro/internal/ephem"
|
||||
)
|
||||
|
||||
const (
|
||||
propagationAU = 149597870.7
|
||||
propagationGaussianK = 0.01720209895
|
||||
propagationSunGM = propagationGaussianK * propagationGaussianK * propagationAU * propagationAU * propagationAU
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidPropagationElements 根数不能用于传播 / orbital elements cannot be propagated.
|
||||
ErrInvalidPropagationElements = errors.New("orbit: invalid propagation elements")
|
||||
// ErrPerturbationEngine 摄动传播缺少天体星历 / perturbation propagation requires an ephemeris.
|
||||
ErrPerturbationEngine = errors.New("orbit: perturbation ephemeris is not configured")
|
||||
// ErrUnsupportedPerturber 摄动天体未知或重复计入 / perturbing body is unknown or counted twice.
|
||||
ErrUnsupportedPerturber = errors.New("orbit: unsupported perturbing body")
|
||||
)
|
||||
|
||||
// maxPropagationNodes 单次传播允许的网格步数上限 / upper bound on grid steps of one propagation.
|
||||
const maxPropagationNodes = 1 << 30
|
||||
|
||||
// Body NAIF 天体编号 / NAIF body identifier.
|
||||
type Body = ephem.Body
|
||||
|
||||
// StateProvider 提供摄动天体状态 / supplies perturbing-body states.
|
||||
type StateProvider = ephem.Engine
|
||||
|
||||
// PropagationOptions 可选摄动传播参数;零值保持纯二体 / optional perturbation propagation parameters; zero preserves two-body propagation.
|
||||
type PropagationOptions struct {
|
||||
Perturbers []Body // 摄动天体 / perturbing bodies.
|
||||
Engine StateProvider // 摄动星历 / perturbation ephemeris.
|
||||
Context StateProvider // 摄动星历别名 / alternate perturbation ephemeris.
|
||||
Step float64 // 最大步长,单位日 / maximum step in days.
|
||||
Tolerance float64 // 步长加倍的相对误差阈值 / relative step-doubling error threshold.
|
||||
}
|
||||
|
||||
// State 计算日心 J2000 平黄道根数的二体状态,输出 ICRF km 与 km/日 / computes a two-body state from heliocentric J2000 ecliptic elements, returning ICRF km and km/day.
|
||||
func State(elements Elements, jdTDB float64) (pos, vel [3]float64, err error) {
|
||||
return twoBodyState(elements, jdTDB)
|
||||
}
|
||||
|
||||
// StateWithOptions 按选项传播根数;零值与 State 完全一致 / propagates elements with optional perturbations; zero options exactly match State.
|
||||
func StateWithOptions(elements Elements, jdTDB float64, options PropagationOptions) (pos, vel [3]float64, err error) {
|
||||
if !finite(jdTDB) {
|
||||
return pos, vel, ErrInvalidPropagationElements
|
||||
}
|
||||
perturbers, err := normalizePerturbers(options.Perturbers)
|
||||
if err != nil {
|
||||
return pos, vel, err
|
||||
}
|
||||
if len(perturbers) == 0 {
|
||||
return twoBodyState(elements, jdTDB)
|
||||
}
|
||||
run, err := newPropagationRun(elements, perturbers, options)
|
||||
if err != nil {
|
||||
return pos, vel, err
|
||||
}
|
||||
return run.state(jdTDB, nil)
|
||||
}
|
||||
|
||||
// propagationRun 固定一次传播的根数、天体与步长网格。
|
||||
type propagationRun struct {
|
||||
elements Elements
|
||||
perturbers []Body
|
||||
provider StateProvider
|
||||
step float64
|
||||
tolerance float64
|
||||
epoch float64
|
||||
start propagationCheckpoint
|
||||
}
|
||||
|
||||
type propagationCheckpoint struct {
|
||||
pos [3]float64
|
||||
vel [3]float64
|
||||
}
|
||||
|
||||
// propagationCheckpointStride 每隔多少个网格点缓存一次状态。
|
||||
const propagationCheckpointStride = 32
|
||||
|
||||
// propagationCheckpoints 引擎缓存的网格状态,下标 j 对应 epoch±j·stride·step。
|
||||
type propagationCheckpoints struct {
|
||||
forward []propagationCheckpoint
|
||||
backward []propagationCheckpoint
|
||||
}
|
||||
|
||||
func newPropagationRun(elements Elements, perturbers []Body, options PropagationOptions) (*propagationRun, error) {
|
||||
provider := options.Engine
|
||||
if provider == nil {
|
||||
provider = options.Context
|
||||
}
|
||||
if provider == nil {
|
||||
return nil, ErrPerturbationEngine
|
||||
}
|
||||
epoch := propagationEpoch(elements)
|
||||
// 经典根数漏填 EpochJD 时为 0,会从 JD 0 起积分数千年,按非法根数拒绝。
|
||||
if !finite(epoch) || epoch == 0 {
|
||||
return nil, ErrInvalidPropagationElements
|
||||
}
|
||||
pos, vel, err := twoBodyState(elements, epoch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
step := options.Step
|
||||
if !(step > 0) || !finite(step) {
|
||||
step = 0.5
|
||||
}
|
||||
tolerance := options.Tolerance
|
||||
if !(tolerance > 0) || !finite(tolerance) {
|
||||
tolerance = 1e-6
|
||||
}
|
||||
return &propagationRun{
|
||||
elements: elements,
|
||||
perturbers: perturbers,
|
||||
provider: provider,
|
||||
step: step,
|
||||
tolerance: tolerance,
|
||||
epoch: epoch,
|
||||
start: propagationCheckpoint{pos: pos, vel: vel},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// propagationEpoch 积分起点:近日点形式给出 EpochJD 时取其为密切历元,否则用 TpJD / integration start: the osculating EpochJD when a perihelion-form set provides one, otherwise TpJD.
|
||||
func propagationEpoch(elements Elements) float64 {
|
||||
if usesPerihelionForm(elements) && (elements.EpochJD == 0 || !finite(elements.EpochJD)) {
|
||||
return elements.TpJD
|
||||
}
|
||||
return elements.EpochJD
|
||||
}
|
||||
|
||||
// state 先沿固定网格积分到不越过目标的最后一个格点,再积分余量;结果只取决于目标历元,与 cache 内容无关 / integrates along a fixed grid to the last node short of the target, then the remainder; the result depends only on the target, never on cache contents.
|
||||
func (run *propagationRun) state(target float64, cache *propagationCheckpoints) (pos, vel [3]float64, err error) {
|
||||
if target == run.epoch {
|
||||
return run.start.pos, run.start.vel, nil
|
||||
}
|
||||
direction := 1.0
|
||||
if target < run.epoch {
|
||||
direction = -1
|
||||
}
|
||||
span := math.Abs(target - run.epoch)
|
||||
count := math.Floor(span / run.step)
|
||||
if count > maxPropagationNodes {
|
||||
return pos, vel, ErrInvalidPropagationElements
|
||||
}
|
||||
index := int(count)
|
||||
for index > 0 && math.Abs(run.node(index, direction)-run.epoch) > span {
|
||||
index--
|
||||
}
|
||||
current, known := run.start, 0
|
||||
var stored *[]propagationCheckpoint
|
||||
if cache != nil {
|
||||
stored = &cache.forward
|
||||
if direction < 0 {
|
||||
stored = &cache.backward
|
||||
}
|
||||
if len(*stored) == 0 {
|
||||
*stored = append(*stored, run.start)
|
||||
}
|
||||
slot := index / propagationCheckpointStride
|
||||
if slot > len(*stored)-1 {
|
||||
slot = len(*stored) - 1
|
||||
}
|
||||
known = slot * propagationCheckpointStride
|
||||
current = (*stored)[slot]
|
||||
}
|
||||
session := newPropagationSession(run.provider)
|
||||
for k := known; k < index; k++ {
|
||||
current.pos, current.vel, err = run.integrate(current.pos, current.vel, run.node(k, direction), run.node(k+1, direction), session)
|
||||
if err != nil {
|
||||
return current.pos, current.vel, err
|
||||
}
|
||||
if stored != nil && (k+1)%propagationCheckpointStride == 0 && len(*stored) == (k+1)/propagationCheckpointStride {
|
||||
*stored = append(*stored, current)
|
||||
}
|
||||
}
|
||||
from := run.node(index, direction)
|
||||
if from == target {
|
||||
return current.pos, current.vel, nil
|
||||
}
|
||||
return run.integrate(current.pos, current.vel, from, target, session)
|
||||
}
|
||||
|
||||
func (run *propagationRun) node(index int, direction float64) float64 {
|
||||
return run.epoch + direction*float64(index)*run.step
|
||||
}
|
||||
|
||||
// integrate 自适应子步积分 [from, to],终点精确落在 to / adaptively integrates [from, to], landing exactly on to.
|
||||
func (run *propagationRun) integrate(pos, vel [3]float64, from, to float64, session *propagationSession) (outPos, outVel [3]float64, err error) {
|
||||
outPos, outVel = pos, vel
|
||||
current := from
|
||||
for current != to {
|
||||
dt := to - current
|
||||
var used float64
|
||||
outPos, outVel, used, err = propagationAdaptiveStep(outPos, outVel, current, dt, run.elements, run.perturbers, session, run.tolerance)
|
||||
if err != nil {
|
||||
return outPos, outVel, err
|
||||
}
|
||||
if used == dt {
|
||||
current = to
|
||||
} else {
|
||||
current += used
|
||||
}
|
||||
}
|
||||
return outPos, outVel, nil
|
||||
}
|
||||
|
||||
func propagationAdaptiveStep(pos, vel [3]float64, jd, dt float64, elements Elements, perturbers []Body, session *propagationSession, tolerance float64) (outPos, outVel [3]float64, used float64, err error) {
|
||||
trial := dt
|
||||
lastError := math.Inf(1)
|
||||
for attempts := 0; attempts < 20; attempts++ {
|
||||
fullPos, fullVel, fullErr := propagationRK4(pos, vel, jd, trial, elements, perturbers, session)
|
||||
if fullErr != nil {
|
||||
return pos, vel, trial, fullErr
|
||||
}
|
||||
halfPos, halfVel, halfErr := propagationRK4(pos, vel, jd, trial/2, elements, perturbers, session)
|
||||
if halfErr != nil {
|
||||
return pos, vel, trial, halfErr
|
||||
}
|
||||
halfPos, halfVel, halfErr = propagationRK4(halfPos, halfVel, jd+trial/2, trial/2, elements, perturbers, session)
|
||||
if halfErr != nil {
|
||||
return pos, vel, trial, halfErr
|
||||
}
|
||||
error := 0.0
|
||||
for index := range fullPos {
|
||||
positionScale := math.Max(1, math.Max(math.Abs(fullPos[index]), math.Abs(halfPos[index])))
|
||||
velocityScale := math.Max(1, math.Max(math.Abs(fullVel[index]), math.Abs(halfVel[index])))
|
||||
error = math.Max(error, math.Abs(fullPos[index]-halfPos[index])/(15*positionScale))
|
||||
error = math.Max(error, math.Abs(fullVel[index]-halfVel[index])/(15*velocityScale))
|
||||
}
|
||||
lastError = error
|
||||
if error <= tolerance || math.Abs(trial) <= 1e-8 {
|
||||
return halfPos, halfVel, trial, nil
|
||||
}
|
||||
trial /= 2
|
||||
}
|
||||
return pos, vel, trial, fmt.Errorf("orbit: propagation step did not converge (dt=%g error=%g tolerance=%g)", trial, lastError, tolerance)
|
||||
}
|
||||
|
||||
type propagationSession struct {
|
||||
provider StateProvider
|
||||
states map[propagationStateKey]propagationSample
|
||||
}
|
||||
|
||||
type propagationStateKey struct {
|
||||
body Body
|
||||
jd uint64
|
||||
}
|
||||
|
||||
type propagationSample struct {
|
||||
pos [3]float64
|
||||
vel [3]float64
|
||||
err error
|
||||
}
|
||||
|
||||
func newPropagationSession(provider StateProvider) *propagationSession {
|
||||
return &propagationSession{
|
||||
provider: provider,
|
||||
states: make(map[propagationStateKey]propagationSample),
|
||||
}
|
||||
}
|
||||
|
||||
func (session *propagationSession) state(body Body, jd float64) (pos, vel [3]float64, err error) {
|
||||
key := propagationStateKey{body: body, jd: math.Float64bits(jd)}
|
||||
if sample, ok := session.states[key]; ok {
|
||||
return sample.pos, sample.vel, sample.err
|
||||
}
|
||||
pos, vel, err = session.provider.State(body, ephem.SSB, jd)
|
||||
session.states[key] = propagationSample{pos: pos, vel: vel, err: err}
|
||||
return pos, vel, err
|
||||
}
|
||||
|
||||
// EphemerisEngine 根数传播的只读星历引擎,结果与 StateWithOptions 逐位一致且不受查询顺序影响 / read-only engine over propagated elements.
|
||||
type EphemerisEngine struct {
|
||||
target Body
|
||||
elements Elements
|
||||
options PropagationOptions
|
||||
setupErr error
|
||||
run *propagationRun
|
||||
mu sync.Mutex
|
||||
cache propagationCheckpoints
|
||||
}
|
||||
|
||||
// NewEphemerisEngine 创建根数传播星历引擎;选项无效时 State 返回对应错误 / creates an ephemeris engine backed by propagated elements; invalid options surface from State.
|
||||
func NewEphemerisEngine(target Body, elements Elements, options PropagationOptions) *EphemerisEngine {
|
||||
engine := &EphemerisEngine{target: target, elements: elements}
|
||||
options.Perturbers, engine.setupErr = normalizePerturbers(options.Perturbers)
|
||||
engine.options = options
|
||||
if engine.setupErr == nil && len(options.Perturbers) != 0 {
|
||||
engine.run, engine.setupErr = newPropagationRun(elements, options.Perturbers, options)
|
||||
}
|
||||
return engine
|
||||
}
|
||||
|
||||
// Target 返回传播目标 / returns the propagated target.
|
||||
func (engine *EphemerisEngine) Target() Body { return engine.target }
|
||||
|
||||
// Elements 返回传播根数 / returns the propagation elements.
|
||||
func (engine *EphemerisEngine) Elements() Elements { return engine.elements }
|
||||
|
||||
// State 返回传播目标相对指定中心的状态 / returns propagated target state relative to a center.
|
||||
func (engine *EphemerisEngine) State(target, center Body, jdTDB float64) (pos, vel [3]float64, err error) {
|
||||
if engine == nil || target != engine.target {
|
||||
if target == center {
|
||||
return pos, vel, nil
|
||||
}
|
||||
return pos, vel, ephem.ErrUnsupportedBody
|
||||
}
|
||||
if target == center {
|
||||
return pos, vel, nil
|
||||
}
|
||||
if center != ephem.Sun && center != ephem.SSB {
|
||||
return pos, vel, ephem.ErrUnsupportedBody
|
||||
}
|
||||
if engine.setupErr != nil {
|
||||
return pos, vel, engine.setupErr
|
||||
}
|
||||
if !finite(jdTDB) {
|
||||
return pos, vel, ErrInvalidPropagationElements
|
||||
}
|
||||
if engine.run == nil {
|
||||
pos, vel, err = twoBodyState(engine.elements, jdTDB)
|
||||
} else {
|
||||
engine.mu.Lock()
|
||||
pos, vel, err = engine.run.state(jdTDB, &engine.cache)
|
||||
engine.mu.Unlock()
|
||||
}
|
||||
if err != nil {
|
||||
return pos, vel, err
|
||||
}
|
||||
if center == ephem.Sun {
|
||||
return pos, vel, nil
|
||||
}
|
||||
provider := engine.options.Engine
|
||||
if provider == nil {
|
||||
provider = engine.options.Context
|
||||
}
|
||||
if provider == nil {
|
||||
return pos, vel, ErrPerturbationEngine
|
||||
}
|
||||
sunPos, sunVel, sunErr := provider.State(ephem.Sun, ephem.SSB, jdTDB)
|
||||
if sunErr != nil {
|
||||
return pos, vel, sunErr
|
||||
}
|
||||
for index := range pos {
|
||||
pos[index] += sunPos[index]
|
||||
vel[index] += sunVel[index]
|
||||
}
|
||||
return pos, vel, nil
|
||||
}
|
||||
|
||||
// Coverage 返回传播引擎的数学覆盖范围 / returns the mathematical coverage of the propagator.
|
||||
func (engine *EphemerisEngine) Coverage(target Body) (start, end float64, ok bool) {
|
||||
if engine == nil || target != engine.target {
|
||||
return 0, 0, false
|
||||
}
|
||||
return math.Inf(-1), math.Inf(1), true
|
||||
}
|
||||
|
||||
func propagationRK4(pos, vel [3]float64, jd, dt float64, elements Elements, perturbers []Body, session *propagationSession) ([3]float64, [3]float64, error) {
|
||||
p1, v1, err := propagationDerivative(pos, vel, jd, elements, perturbers, session)
|
||||
if err != nil {
|
||||
return pos, vel, err
|
||||
}
|
||||
p2 := vectorAdd(pos, vectorScale(p1, dt/2))
|
||||
v2 := vectorAdd(vel, vectorScale(v1, dt/2))
|
||||
p2d, v2d, err := propagationDerivative(p2, v2, jd+dt/2, elements, perturbers, session)
|
||||
if err != nil {
|
||||
return pos, vel, err
|
||||
}
|
||||
p3 := vectorAdd(pos, vectorScale(p2d, dt/2))
|
||||
v3 := vectorAdd(vel, vectorScale(v2d, dt/2))
|
||||
p3d, v3d, err := propagationDerivative(p3, v3, jd+dt/2, elements, perturbers, session)
|
||||
if err != nil {
|
||||
return pos, vel, err
|
||||
}
|
||||
p4 := vectorAdd(pos, vectorScale(p3d, dt))
|
||||
v4 := vectorAdd(vel, vectorScale(v3d, dt))
|
||||
p4d, v4d, err := propagationDerivative(p4, v4, jd+dt, elements, perturbers, session)
|
||||
if err != nil {
|
||||
return pos, vel, err
|
||||
}
|
||||
return vectorAdd(pos, vectorScale(vectorAdd(vectorAdd(p1, vectorScale(p2d, 2)), vectorAdd(vectorScale(p3d, 2), p4d)), dt/6)), vectorAdd(vel, vectorScale(vectorAdd(vectorAdd(v1, vectorScale(v2d, 2)), vectorAdd(vectorScale(v3d, 2), v4d)), dt/6)), nil
|
||||
}
|
||||
|
||||
func propagationDerivative(pos, vel [3]float64, jd float64, elements Elements, perturbers []Body, session *propagationSession) ([3]float64, [3]float64, error) {
|
||||
radius := vectorNorm(pos)
|
||||
if !(radius > 0) || !finite(radius) {
|
||||
return vel, [3]float64{}, ErrInvalidPropagationElements
|
||||
}
|
||||
acc := vectorScale(pos, -propagationSunGM/math.Pow(radius, 3))
|
||||
sunPos, _, err := session.state(ephem.Sun, jd)
|
||||
if err != nil {
|
||||
return vel, acc, err
|
||||
}
|
||||
for _, body := range perturbers {
|
||||
if body == ephem.Sun || body == ephem.SSB {
|
||||
continue
|
||||
}
|
||||
bodyPos, _, bodyErr := session.state(body, jd)
|
||||
if bodyErr != nil {
|
||||
return vel, acc, bodyErr
|
||||
}
|
||||
relBody := vectorSub(bodyPos, sunPos)
|
||||
toBody := vectorSub(relBody, pos)
|
||||
mu := perturbingGM(body)
|
||||
if vectorNorm(relBody) == 0 || vectorNorm(toBody) == 0 {
|
||||
return vel, acc, ErrInvalidPropagationElements
|
||||
}
|
||||
acc = vectorAdd(acc, vectorScale(toBody, mu/math.Pow(vectorNorm(toBody), 3)))
|
||||
acc = vectorAdd(acc, vectorScale(relBody, -mu/math.Pow(vectorNorm(relBody), 3)))
|
||||
}
|
||||
return vel, acc, nil
|
||||
}
|
||||
|
||||
func perturbingGM(body Body) float64 {
|
||||
const secondPerDay = 86400.0
|
||||
var gm float64
|
||||
switch body {
|
||||
case ephem.Mercury:
|
||||
gm = 22031.86855
|
||||
case ephem.Venus:
|
||||
gm = 324858.592
|
||||
case ephem.Earth:
|
||||
gm = 398600.435436
|
||||
case ephem.Moon:
|
||||
gm = 4902.800066
|
||||
case ephem.EMB:
|
||||
gm = 398600.435436 + 4902.800066
|
||||
case ephem.Mars:
|
||||
gm = 42828.375214
|
||||
case ephem.Jupiter:
|
||||
gm = 126686534.911
|
||||
case ephem.Saturn:
|
||||
gm = 37931207.8
|
||||
case ephem.Uranus:
|
||||
gm = 5793966.0
|
||||
case ephem.Neptune:
|
||||
gm = 6835100.0
|
||||
}
|
||||
return gm * secondPerDay * secondPerDay
|
||||
}
|
||||
|
||||
// normalizePerturbers 去重并跳过太阳与 SSB;未知天体或 EMB 与地球/月球同时出现时报错 / dedups and skips Sun and SSB; rejects unknown bodies and EMB combined with Earth or Moon.
|
||||
func normalizePerturbers(perturbers []Body) ([]Body, error) {
|
||||
if len(perturbers) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
seen := make(map[Body]struct{}, len(perturbers))
|
||||
result := make([]Body, 0, len(perturbers))
|
||||
for _, body := range perturbers {
|
||||
if body == ephem.Sun || body == ephem.SSB {
|
||||
continue
|
||||
}
|
||||
if perturbingGM(body) == 0 {
|
||||
return nil, fmt.Errorf("%w: %d", ErrUnsupportedPerturber, body)
|
||||
}
|
||||
if _, ok := seen[body]; ok {
|
||||
continue
|
||||
}
|
||||
seen[body] = struct{}{}
|
||||
result = append(result, body)
|
||||
}
|
||||
_, hasEMB := seen[ephem.EMB]
|
||||
_, hasEarth := seen[ephem.Earth]
|
||||
_, hasMoon := seen[ephem.Moon]
|
||||
if hasEMB && (hasEarth || hasMoon) {
|
||||
return nil, fmt.Errorf("%w: EMB already includes Earth and Moon", ErrUnsupportedPerturber)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func twoBodyState(elements Elements, jd float64) (pos, vel [3]float64, err error) {
|
||||
if !finite(jd) {
|
||||
return pos, vel, ErrInvalidPropagationElements
|
||||
}
|
||||
basicElements := toBasicElements(elements)
|
||||
trueAnomaly := basic.OrbitTrueAnomaly(jd, basicElements)
|
||||
if math.IsNaN(trueAnomaly) {
|
||||
return pos, vel, ErrInvalidPropagationElements
|
||||
}
|
||||
resolved := elementsAt(elements, jd)
|
||||
eccentricity := resolved.E
|
||||
semiLatus := resolved.A * (1 - eccentricity*eccentricity)
|
||||
if usesPerihelionForm(elements) {
|
||||
semiLatus = resolved.Q * (1 + eccentricity)
|
||||
}
|
||||
if !(semiLatus > 0) || !finite(semiLatus) {
|
||||
return pos, vel, ErrInvalidPropagationElements
|
||||
}
|
||||
nu := trueAnomaly * math.Pi / 180
|
||||
mu := propagationGaussianK * propagationGaussianK
|
||||
radius := semiLatus / (1 + eccentricity*math.Cos(nu))
|
||||
if !(radius > 0) || !finite(radius) {
|
||||
return pos, vel, ErrInvalidPropagationElements
|
||||
}
|
||||
x := radius * math.Cos(nu)
|
||||
y := radius * math.Sin(nu)
|
||||
speedScale := math.Sqrt(mu / semiLatus)
|
||||
vx := -speedScale * math.Sin(nu)
|
||||
vy := speedScale * (eccentricity + math.Cos(nu))
|
||||
posAU, velAU := rotatePerifocal(x, y, vx, vy, resolved)
|
||||
return rotateEquatorial(posAU, propagationAU), rotateEquatorial(velAU, propagationAU), nil
|
||||
}
|
||||
|
||||
func elementsAt(elements Elements, jd float64) Elements {
|
||||
if usesPerihelionForm(elements) || !finite(elements.EpochJD) {
|
||||
return elements
|
||||
}
|
||||
delta := jd - elements.EpochJD
|
||||
elements.A += elements.ADot * delta
|
||||
elements.E += elements.EDot * delta
|
||||
elements.I += elements.IDot * delta
|
||||
elements.Omega += elements.OmegaDot * delta
|
||||
elements.W += elements.WDot * delta
|
||||
return elements
|
||||
}
|
||||
|
||||
func usesPerihelionForm(elements Elements) bool { return elements.Q > 0 && finite(elements.TpJD) }
|
||||
|
||||
func rotatePerifocal(x, y, vx, vy float64, elements Elements) ([3]float64, [3]float64) {
|
||||
omega := elements.Omega * math.Pi / 180
|
||||
argument := elements.W * math.Pi / 180
|
||||
inclination := elements.I * math.Pi / 180
|
||||
sinOmega, cosOmega := math.Sincos(omega)
|
||||
sinArgument, cosArgument := math.Sincos(argument)
|
||||
sinInclination, cosInclination := math.Sincos(inclination)
|
||||
basisX := [3]float64{cosOmega*cosArgument - sinOmega*sinArgument*cosInclination, sinOmega*cosArgument + cosOmega*sinArgument*cosInclination, sinArgument * sinInclination}
|
||||
basisY := [3]float64{-cosOmega*sinArgument - sinOmega*cosArgument*cosInclination, -sinOmega*sinArgument + cosOmega*cosArgument*cosInclination, cosArgument * sinInclination}
|
||||
return vectorAdd(vectorScale(basisX, x), vectorScale(basisY, y)), vectorAdd(vectorScale(basisX, vx), vectorScale(basisY, vy))
|
||||
}
|
||||
|
||||
var sinJ2000Obliquity, cosJ2000Obliquity = math.Sincos(basic.EclipticObliquity(2451545.0, false) * math.Pi / 180)
|
||||
|
||||
func rotateEquatorial(value [3]float64, scale float64) [3]float64 {
|
||||
sinEps, cosEps := sinJ2000Obliquity, cosJ2000Obliquity
|
||||
return [3]float64{value[0] * scale, (value[1]*cosEps - value[2]*sinEps) * scale, (value[1]*sinEps + value[2]*cosEps) * scale}
|
||||
}
|
||||
|
||||
func vectorAdd(left, right [3]float64) [3]float64 {
|
||||
return [3]float64{left[0] + right[0], left[1] + right[1], left[2] + right[2]}
|
||||
}
|
||||
|
||||
func vectorSub(left, right [3]float64) [3]float64 {
|
||||
return [3]float64{left[0] - right[0], left[1] - right[1], left[2] - right[2]}
|
||||
}
|
||||
|
||||
func vectorScale(value [3]float64, scale float64) [3]float64 {
|
||||
return [3]float64{value[0] * scale, value[1] * scale, value[2] * scale}
|
||||
}
|
||||
|
||||
func vectorNorm(value [3]float64) float64 {
|
||||
return math.Sqrt(value[0]*value[0] + value[1]*value[1] + value[2]*value[2])
|
||||
}
|
||||
|
||||
func finite(value float64) bool { return !math.IsNaN(value) && !math.IsInf(value, 0) }
|
||||
|
||||
var _ StateProvider = (*EphemerisEngine)(nil)
|
||||
@@ -0,0 +1,147 @@
|
||||
package orbit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// movingPerturbationEngine 提供随时间变化的太阳与行星位置 / supplies time-varying Sun and planet positions.
|
||||
type movingPerturbationEngine struct{}
|
||||
|
||||
func (movingPerturbationEngine) State(target, center Body, jdTDB float64) (pos, vel [3]float64, err error) {
|
||||
if target == center {
|
||||
return pos, vel, nil
|
||||
}
|
||||
if center != 0 {
|
||||
return pos, vel, ErrPerturbationEngine
|
||||
}
|
||||
angle := func(period float64) float64 { return 2 * math.Pi * (jdTDB - 2451545) / period }
|
||||
switch target {
|
||||
case 10:
|
||||
phase := angle(4332.6)
|
||||
pos = [3]float64{-742000 * math.Cos(phase), -742000 * math.Sin(phase), 0}
|
||||
case 3:
|
||||
phase := angle(365.25)
|
||||
pos = [3]float64{149597870 * math.Cos(phase), 149597870 * math.Sin(phase), 0}
|
||||
case 5:
|
||||
phase := angle(4332.6)
|
||||
pos = [3]float64{778000000 * math.Cos(phase), 778000000 * math.Sin(phase), 0}
|
||||
default:
|
||||
return pos, vel, ErrPerturbationEngine
|
||||
}
|
||||
return pos, vel, nil
|
||||
}
|
||||
|
||||
func (movingPerturbationEngine) Coverage(target Body) (start, end float64, ok bool) {
|
||||
return 0, 3000000, true
|
||||
}
|
||||
|
||||
func TestEphemerisEngineMatchesFreshPropagationInAnyOrder(t *testing.T) {
|
||||
elements := Elements{EpochJD: 2451545, A: 1.5, E: 0.02, I: 3, Omega: 20, W: 30, M0: 10}
|
||||
options := PropagationOptions{Perturbers: []Body{5, 3}, Engine: movingPerturbationEngine{}, Step: 0.7, Tolerance: 1e-9}
|
||||
queries := []float64{2451545 + 400.3, 2451545 + 12.25, 2451545 - 90.1, 2451545 + 400.3, 2451545 + 33.6, 2451545 - 3.5, 2451545, 2451545 + 1000}
|
||||
engine := NewEphemerisEngine(Body(12345), elements, options)
|
||||
for _, jd := range queries {
|
||||
gotPos, gotVel, err := engine.State(Body(12345), 10, jd)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantPos, wantVel, err := StateWithOptions(elements, jd, options)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gotPos != wantPos || gotVel != wantVel {
|
||||
t.Fatalf("jd %.2f: engine %v %v, fresh %v %v", jd, gotPos, gotVel, wantPos, wantVel)
|
||||
}
|
||||
}
|
||||
// 逆序查询同样逐位一致 / reverse order also matches bit for bit.
|
||||
reverse := NewEphemerisEngine(Body(12345), elements, options)
|
||||
for index := len(queries) - 1; index >= 0; index-- {
|
||||
gotPos, _, err := reverse.State(Body(12345), 10, queries[index])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantPos, _, _ := StateWithOptions(elements, queries[index], options)
|
||||
if gotPos != wantPos {
|
||||
t.Fatalf("reverse jd %.2f: engine %v, fresh %v", queries[index], gotPos, wantPos)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEphemerisEngineKeepsConstructionElements(t *testing.T) {
|
||||
elements := Elements{EpochJD: 2451545, A: 1.5, E: 0.02, I: 3, Omega: 20, W: 30, M0: 10}
|
||||
options := PropagationOptions{Perturbers: []Body{5}, Engine: movingPerturbationEngine{}, Step: 1}
|
||||
engine := NewEphemerisEngine(Body(12345), elements, options)
|
||||
if _, _, err := engine.State(Body(12345), 10, 2451545+50); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
options.Perturbers[0] = 3
|
||||
got := engine.Elements()
|
||||
got.A = 9
|
||||
gotPos, _, err := engine.State(Body(12345), 10, 2451545+80)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantPos, _, _ := StateWithOptions(elements, 2451545+80, PropagationOptions{Perturbers: []Body{5}, Engine: movingPerturbationEngine{}, Step: 1})
|
||||
if gotPos != wantPos || engine.Elements() != elements || engine.Target() != Body(12345) {
|
||||
t.Fatalf("engine state changed after caller mutation: %v versus %v", gotPos, wantPos)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateWithOptionsPerihelionFormStartsAtOsculatingEpoch(t *testing.T) {
|
||||
elements := Elements{EpochJD: 2451545 + 120, Q: 0.6, E: 0.9, I: 30, Omega: 40, W: 50, TpJD: 2451545}
|
||||
options := PropagationOptions{Perturbers: []Body{5}, Engine: movingPerturbationEngine{}, Step: 1, Tolerance: 1e-10}
|
||||
gotPos, gotVel, err := StateWithOptions(elements, elements.EpochJD, options)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantPos, wantVel, err := State(elements, elements.EpochJD)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gotPos != wantPos || gotVel != wantVel {
|
||||
t.Fatalf("osculating epoch state %v %v, want two-body %v %v", gotPos, gotVel, wantPos, wantVel)
|
||||
}
|
||||
// 未给 EpochJD 时仍以 TpJD 为起点 / without EpochJD the integration still starts at TpJD.
|
||||
noEpoch := elements
|
||||
noEpoch.EpochJD = 0
|
||||
gotPos, _, err = StateWithOptions(noEpoch, noEpoch.TpJD, options)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantPos, _, _ = State(noEpoch, noEpoch.TpJD)
|
||||
if gotPos != wantPos {
|
||||
t.Fatalf("TpJD start state %v, want %v", gotPos, wantPos)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateWithOptionsRejectsUnsupportedPerturbers(t *testing.T) {
|
||||
elements := Elements{EpochJD: 2451545, A: 1.5, E: 0.02, I: 3, Omega: 20, W: 30, M0: 10}
|
||||
for _, perturbers := range [][]Body{{5, 9}, {2000001}, {3, 399}, {301, 3}} {
|
||||
_, _, err := StateWithOptions(elements, elements.EpochJD+5, PropagationOptions{Perturbers: perturbers, Engine: movingPerturbationEngine{}})
|
||||
if !errors.Is(err, ErrUnsupportedPerturber) {
|
||||
t.Fatalf("perturbers %v error = %v, want ErrUnsupportedPerturber", perturbers, err)
|
||||
}
|
||||
engine := NewEphemerisEngine(Body(12345), elements, PropagationOptions{Perturbers: perturbers, Engine: movingPerturbationEngine{}})
|
||||
if _, _, err := engine.State(Body(12345), 10, elements.EpochJD+5); !errors.Is(err, ErrUnsupportedPerturber) {
|
||||
t.Fatalf("engine perturbers %v error = %v, want ErrUnsupportedPerturber", perturbers, err)
|
||||
}
|
||||
}
|
||||
plain, _, _ := State(elements, elements.EpochJD+30)
|
||||
withEMB, _, err := StateWithOptions(elements, elements.EpochJD+30, PropagationOptions{Perturbers: []Body{3}, Engine: movingPerturbationEngine{}, Tolerance: 1e-10})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if withEMB == plain {
|
||||
t.Fatal("EMB perturbation did not change the state")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateWithOptionsRejectsMissingClassicalEpoch(t *testing.T) {
|
||||
elements := Elements{A: 1.5, E: 0.02, I: 3, Omega: 20, W: 30, M0: 10}
|
||||
_, _, err := StateWithOptions(elements, 2451545, PropagationOptions{Perturbers: []Body{3}, Engine: movingPerturbationEngine{}})
|
||||
if !errors.Is(err, ErrInvalidPropagationElements) {
|
||||
t.Fatalf("error = %v, want ErrInvalidPropagationElements", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user