feat: 完善时标与天象几何计算并扩展输出接口
- 新增时标、ΔT 模型、质心时间与 UT1 支持 - 改进日月食、月掩、行星事件及路径边界计算 - 完善恒星三维自行与动态距离传播 - 扩展 SVG、GeoJSON、KML 输出与底层距离换算工具 - 整理中英文手册、示例资源及回归测试
This commit is contained in:
+24
-21
@@ -31,51 +31,55 @@ type Horizontal struct {
|
||||
HourAngle float64 // 时角,单位度 / hour angle in degrees.
|
||||
}
|
||||
|
||||
func jdeUTC(date time.Time) float64 {
|
||||
return basic.Date2JDE(date.UTC())
|
||||
// jdUTC 绝对时刻换算成 UTC 民用儒略日,本包所有入口共用这一个口径。
|
||||
//
|
||||
// 黄赤交角、章动与岁差按 TT 取用,这里给的是 UTC 民用 JD:两者的差比本包例程约 0.1″
|
||||
// 的精度低三个数量级,故不叠加 UTC2TT。
|
||||
func jdUTC(date time.Time) float64 {
|
||||
return basic.Date2JD(date.UTC())
|
||||
}
|
||||
|
||||
// EclipticToEquatorial 黄道坐标转赤道坐标 / converts ecliptic to equatorial coordinates.
|
||||
func EclipticToEquatorial(date time.Time, lon, lat float64) Equatorial {
|
||||
ra, dec := basic.LoBoToRaDec(jdeUTC(date), lon, lat)
|
||||
ra, dec := basic.LoBoToRaDec(jdUTC(date), lon, lat)
|
||||
return Equatorial{RA: ra, Dec: dec}
|
||||
}
|
||||
|
||||
// EquatorialToEcliptic 赤道坐标转黄道坐标 / converts equatorial to ecliptic coordinates.
|
||||
func EquatorialToEcliptic(date time.Time, ra, dec float64) Ecliptic {
|
||||
lon, lat := basic.RaDecToLoBo(jdeUTC(date), ra, dec)
|
||||
lon, lat := basic.RaDecToLoBo(jdUTC(date), ra, dec)
|
||||
return Ecliptic{Lon: lon, Lat: lat}
|
||||
}
|
||||
|
||||
// Precess 岁差修正 / precesses equatorial coordinates from one date to another.
|
||||
func Precess(from, to time.Time, ra, dec float64) Equatorial {
|
||||
nextRA, nextDec := basic.Precess(ra, dec, jdeUTC(from), jdeUTC(to))
|
||||
nextRA, nextDec := basic.Precess(ra, dec, jdUTC(from), jdUTC(to))
|
||||
return Equatorial{RA: nextRA, Dec: nextDec}
|
||||
}
|
||||
|
||||
// EclipticObliquity 黄赤交角 / ecliptic obliquity.
|
||||
func EclipticObliquity(date time.Time, nutation bool) float64 {
|
||||
return basic.EclipticObliquity(jdeUTC(date), nutation)
|
||||
return basic.EclipticObliquity(jdUTC(date), nutation)
|
||||
}
|
||||
|
||||
// Nutation2000B IAU 2000B 章动 / IAU 2000B nutation.
|
||||
func Nutation2000B(date time.Time) (longitude, obliquity float64) {
|
||||
return basic.Nutation2000B(jdeUTC(date))
|
||||
return basic.Nutation2000B(jdUTC(date))
|
||||
}
|
||||
|
||||
// Nutation1980 IAU 1980 章动 / IAU 1980 nutation.
|
||||
func Nutation1980(date time.Time) (longitude, obliquity float64) {
|
||||
return basic.Nutation1980(jdeUTC(date))
|
||||
return basic.Nutation1980(jdUTC(date))
|
||||
}
|
||||
|
||||
// MeanSiderealTime 平恒星时,单位小时 / mean sidereal time in hours.
|
||||
func MeanSiderealTime(date time.Time) float64 {
|
||||
return basic.MeanSiderealTime(jdeUTC(date))
|
||||
return basic.MeanSiderealTime(basic.UTC2UT1(jdUTC(date)))
|
||||
}
|
||||
|
||||
// ApparentSiderealTime 真恒星时,单位小时 / apparent sidereal time in hours.
|
||||
func ApparentSiderealTime(date time.Time) float64 {
|
||||
return basic.ApparentSiderealTime(jdeUTC(date))
|
||||
return basic.ApparentSiderealTime(basic.UTC2UT1(jdUTC(date)))
|
||||
}
|
||||
|
||||
// HourAngle 时角 / hour angle.
|
||||
@@ -83,7 +87,7 @@ func ApparentSiderealTime(date time.Time) float64 {
|
||||
// ra 为瞬时赤经;observerLon 为观测者经度,东正西负。
|
||||
// ra is apparent right ascension; observerLon is east-positive longitude.
|
||||
func HourAngle(date time.Time, ra, observerLon float64) float64 {
|
||||
return basic.StarHourAngle(jdeUTC(date), ra, observerLon, 0)
|
||||
return basic.StarHourAngle(jdUTC(date), ra, observerLon, 0)
|
||||
}
|
||||
|
||||
// EquatorialToHorizontal 赤道坐标转地平坐标 / converts equatorial to horizontal coordinates.
|
||||
@@ -91,33 +95,32 @@ func HourAngle(date time.Time, ra, observerLon float64) float64 {
|
||||
// ra/dec 为瞬时赤经赤纬;observerLon/observerLat 为观测者经纬度,东正西负、北正南负。
|
||||
// ra/dec are apparent coordinates; observerLon/observerLat are east-positive and north-positive.
|
||||
func EquatorialToHorizontal(date time.Time, ra, dec, observerLon, observerLat float64) Horizontal {
|
||||
jde := jdeUTC(date)
|
||||
altitude := basic.StarHeight(jde, ra, dec, observerLon, observerLat, 0)
|
||||
jd := jdUTC(date)
|
||||
altitude := basic.StarHeight(jd, ra, dec, observerLon, observerLat, 0)
|
||||
return Horizontal{
|
||||
Azimuth: basic.StarAzimuth(jde, ra, dec, observerLon, observerLat, 0),
|
||||
Azimuth: basic.StarAzimuth(jd, ra, dec, observerLon, observerLat, 0),
|
||||
Altitude: altitude,
|
||||
Zenith: 90 - altitude,
|
||||
HourAngle: basic.StarHourAngle(jde, ra, observerLon, 0),
|
||||
HourAngle: basic.StarHourAngle(jd, ra, observerLon, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// TopocentricEquatorial 地心赤道坐标转站心赤道坐标 / converts geocentric to topocentric equatorial coordinates.
|
||||
//
|
||||
// distanceAU 为目标天体到地心距离,单位 AU;height 为观测者海拔,单位米。
|
||||
// distanceAU 为目标天体到地心距离,单位 AU;height 为观测者椭球高(大地高),单位米。
|
||||
// distanceAU is geocentric distance in AU; height is observer elevation in meters.
|
||||
func TopocentricEquatorial(date time.Time, ra, dec, observerLon, observerLat, distanceAU, height float64) Equatorial {
|
||||
topRA, topDec := basic.TopocentricRaDec(ra, dec, observerLat, observerLon, jdeUTC(date), distanceAU, height)
|
||||
topRA, topDec := basic.TopocentricRaDec(ra, dec, observerLat, observerLon, jdUTC(date), distanceAU, height)
|
||||
return Equatorial{RA: topRA, Dec: topDec}
|
||||
}
|
||||
|
||||
// TopocentricEcliptic 地心黄道坐标转站心黄道坐标 / converts geocentric to topocentric ecliptic coordinates.
|
||||
//
|
||||
// distanceAU 为目标天体到地心距离,单位 AU;height 为观测者海拔,单位米。
|
||||
// distanceAU 为目标天体到地心距离,单位 AU;height 为观测者椭球高(大地高),单位米。
|
||||
// distanceAU is geocentric distance in AU; height is observer elevation in meters.
|
||||
func TopocentricEcliptic(date time.Time, lon, lat, observerLon, observerLat, distanceAU, height float64) Ecliptic {
|
||||
jde := jdeUTC(date)
|
||||
topLon := basic.TopocentricLo(lon, lat, observerLat, observerLon, jde, distanceAU, height)
|
||||
topLat := basic.TopocentricBo(lon, lat, observerLat, observerLon, jde, distanceAU, height)
|
||||
jd := jdUTC(date)
|
||||
topLon, topLat := basic.TopocentricLoBo(lon, lat, observerLat, observerLon, jd, distanceAU, height)
|
||||
return Ecliptic{Lon: topLon, Lat: topLat}
|
||||
}
|
||||
|
||||
|
||||
+39
-8
@@ -18,7 +18,7 @@ func assertClose(t *testing.T, name string, got, want, tolerance float64) {
|
||||
|
||||
func TestEclipticEquatorialWrappers(t *testing.T) {
|
||||
date := time.Date(2026, 4, 27, 10, 30, 45, 0, time.FixedZone("CST", 8*3600))
|
||||
jde := basic.Date2JDE(date.UTC())
|
||||
jde := basic.Date2JD(date.UTC())
|
||||
lon := 139.686111
|
||||
lat := 4.875278
|
||||
|
||||
@@ -35,10 +35,10 @@ func TestEclipticEquatorialWrappers(t *testing.T) {
|
||||
func TestTimeAndPrecessionWrappers(t *testing.T) {
|
||||
date := time.Date(2026, 4, 27, 2, 30, 45, 0, time.UTC)
|
||||
to := time.Date(2050, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
jde := basic.Date2JDE(date.UTC())
|
||||
jde := basic.Date2JD(date.UTC())
|
||||
|
||||
assertClose(t, "mean sidereal time", MeanSiderealTime(date), basic.MeanSiderealTime(jde), 1e-12)
|
||||
assertClose(t, "apparent sidereal time", ApparentSiderealTime(date), basic.ApparentSiderealTime(jde), 1e-12)
|
||||
assertClose(t, "mean sidereal time", MeanSiderealTime(date), basic.MeanSiderealTime(basic.UTC2UT1(jde)), 1e-12)
|
||||
assertClose(t, "apparent sidereal time", ApparentSiderealTime(date), basic.ApparentSiderealTime(basic.UTC2UT1(jde)), 1e-12)
|
||||
assertClose(t, "obliquity", EclipticObliquity(date, true), basic.EclipticObliquity(jde, true), 1e-12)
|
||||
|
||||
gotLon, gotObl := Nutation2000B(date)
|
||||
@@ -47,14 +47,14 @@ func TestTimeAndPrecessionWrappers(t *testing.T) {
|
||||
assertClose(t, "nutation obliquity", gotObl, wantObl, 1e-12)
|
||||
|
||||
got := Precess(date, to, 101.28715533, -16.71611586)
|
||||
wantRA, wantDec := basic.Precess(101.28715533, -16.71611586, jde, basic.Date2JDE(to.UTC()))
|
||||
wantRA, wantDec := basic.Precess(101.28715533, -16.71611586, jde, basic.Date2JD(to.UTC()))
|
||||
assertClose(t, "precess ra", got.RA, wantRA, 1e-12)
|
||||
assertClose(t, "precess dec", got.Dec, wantDec, 1e-12)
|
||||
}
|
||||
|
||||
func TestHorizontalAndTopocentricWrappers(t *testing.T) {
|
||||
date := time.Date(2026, 4, 27, 2, 30, 45, 0, time.UTC)
|
||||
jde := basic.Date2JDE(date.UTC())
|
||||
jde := basic.Date2JD(date.UTC())
|
||||
ra := 101.28715533
|
||||
dec := -16.71611586
|
||||
observerLon := 115.0
|
||||
@@ -84,6 +84,37 @@ func TestAngularSeparationWrapper(t *testing.T) {
|
||||
assertClose(t, "angular separation", got, want, 1e-12)
|
||||
}
|
||||
|
||||
// 站心黄道纬度必须留在 [-90,90],且与"站心赤道坐标再转黄道"这条独立路径一致。
|
||||
// 旧实现把黄经的分母复用到纬度的 Atan2:黄经落在 90°–270° 时纬度会被切到对顶象限
|
||||
// (物理 +4.7° 报成约 -176°),且大视差目标(月球)量值也偏。
|
||||
func TestTopocentricEclipticLatitudeStaysPhysical(t *testing.T) {
|
||||
date := time.Date(2026, 1, 15, 4, 0, 0, 0, time.UTC)
|
||||
const (
|
||||
observerLon = 115.0
|
||||
observerLat = 40.0
|
||||
height = 53.0
|
||||
distanceAU = 0.00257
|
||||
)
|
||||
cases := []struct{ lon, lat float64 }{
|
||||
{254.978443, -5.093420}, // 黄经落在 90°–270°,旧实现翻象限
|
||||
{109.000000, 4.681960}, // 黄经落在 0°–90°,作对照
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got := TopocentricEcliptic(date, tc.lon, tc.lat, observerLon, observerLat, distanceAU, height)
|
||||
if got.Lat < -90 || got.Lat > 90 {
|
||||
t.Fatalf("黄经 %.6f: 站心黄纬 %.6f 越出 [-90,90]", tc.lon, got.Lat)
|
||||
}
|
||||
eq := EclipticToEquatorial(date, tc.lon, tc.lat)
|
||||
top := TopocentricEquatorial(date, eq.RA, eq.Dec, observerLon, observerLat, distanceAU, height)
|
||||
want := EquatorialToEcliptic(date, top.RA, top.Dec)
|
||||
assertClose(t, "topocentric lon", got.Lon, want.Lon, 1e-9)
|
||||
assertClose(t, "topocentric lat", got.Lat, want.Lat, 1e-9)
|
||||
if math.Abs(got.Lat-tc.lat) > 1.5 {
|
||||
t.Fatalf("黄经 %.6f: 站心黄纬 %.6f 与地心黄纬 %.6f 的差超过月球视差量级", tc.lon, got.Lat, tc.lat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TopocentricEcliptic 对同一时刻只求一次儒略日:参考实现按旧口径重复求值,逐位对照。
|
||||
func TestTopocentricEclipticMatchesDuplicatedJDE(t *testing.T) {
|
||||
type sample struct {
|
||||
@@ -154,8 +185,8 @@ func TestTopocentricEclipticMatchesDuplicatedJDE(t *testing.T) {
|
||||
|
||||
for _, tc := range cases {
|
||||
got := TopocentricEcliptic(tc.date, tc.lon, tc.lat, tc.obsLon, tc.obsLat, tc.distanceAU, tc.height)
|
||||
wantLon := basic.TopocentricLo(tc.lon, tc.lat, tc.obsLat, tc.obsLon, jdeUTC(tc.date), tc.distanceAU, tc.height)
|
||||
wantLat := basic.TopocentricBo(tc.lon, tc.lat, tc.obsLat, tc.obsLon, jdeUTC(tc.date), tc.distanceAU, tc.height)
|
||||
wantLon := basic.TopocentricLo(tc.lon, tc.lat, tc.obsLat, tc.obsLon, jdUTC(tc.date), tc.distanceAU, tc.height)
|
||||
wantLat := basic.TopocentricBo(tc.lon, tc.lat, tc.obsLat, tc.obsLon, jdUTC(tc.date), tc.distanceAU, tc.height)
|
||||
if got.Lon != wantLon || got.Lat != wantLat {
|
||||
t.Fatalf("%s %s: got (%.17g, %.17g) want (%.17g, %.17g)", tc.label, tc.date.Format(time.RFC3339Nano), got.Lon, got.Lat, wantLon, wantLat)
|
||||
}
|
||||
|
||||
@@ -19,5 +19,5 @@ func ParallacticAngleByHourAngle(hourAngle, dec, observerLat float64) float64 {
|
||||
// Returns the signed parallactic angle for the apparent equatorial coordinates
|
||||
// at the observing instant.
|
||||
func ParallacticAngle(date time.Time, ra, dec, observerLon, observerLat float64) float64 {
|
||||
return basic.StarParallacticAngle(jdeUTC(date), ra, dec, observerLon, observerLat, 0)
|
||||
return basic.StarParallacticAngle(jdUTC(date), ra, dec, observerLon, observerLat, 0)
|
||||
}
|
||||
|
||||
@@ -24,15 +24,16 @@ func BenchmarkTopocentricEcliptic(b *testing.B) {
|
||||
benchmarkEclipticSink = sink
|
||||
}
|
||||
|
||||
func BenchmarkTopocentricEclipticLegacy(b *testing.B) {
|
||||
// BenchmarkTopocentricEclipticTwoWrappers 量的是分别调用 Lo/Bo 两个包装(各解一次)与组合入口的差距。
|
||||
func BenchmarkTopocentricEclipticTwoWrappers(b *testing.B) {
|
||||
date, lon, lat, observerLon, observerLat, distanceAU, height := benchmarkEclipticInputs()
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
sink := Ecliptic{}
|
||||
for i := 0; i < b.N; i++ {
|
||||
sink = Ecliptic{
|
||||
Lon: basic.TopocentricLo(lon, lat, observerLat, observerLon, jdeUTC(date), distanceAU, height),
|
||||
Lat: basic.TopocentricBo(lon, lat, observerLat, observerLon, jdeUTC(date), distanceAU, height),
|
||||
Lon: basic.TopocentricLo(lon, lat, observerLat, observerLon, jdUTC(date), distanceAU, height),
|
||||
Lat: basic.TopocentricBo(lon, lat, observerLat, observerLon, jdUTC(date), distanceAU, height),
|
||||
}
|
||||
}
|
||||
benchmarkEclipticSink = sink
|
||||
|
||||
Reference in New Issue
Block a user