feat: 扩展天文计算能力
- 新增日食、月食、本地可见性、中心线、半影区域、SVG 图示与沙罗周期信息 - 新增行星冲合、留、方照、物理星历、视直径、相位、亮肢角、轨道节点等计算 - 新增木星伽利略卫星位置、现象与接触事件计算 - 新增恒星星表、星座判定、自行修正与观测辅助能力 - 新增 coord、formula、orbit、sundial、lite/sun、lite/moon 等扩展包 - 完善农历年号、月相英文别名、视差角、大气质量、折射、日晷与双星计算 - 增加 NASA、JPL Horizons、IMCCE 等回归测试数据与基线测试 - 重构基础算法文件组织,补充大量公开 API 注释和语义回归测试 - 更新中文和英文 README,补充示例、精度说明、SVG 配图
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
package coord
|
||||
|
||||
import "b612.me/astro/formula"
|
||||
|
||||
// AirmassPlaneParallelFromTrueAltitude 平行平板大气质量 / plane-parallel airmass from true altitude.
|
||||
//
|
||||
// 输入为真高度角,单位度。适合中高空几何近似,接近地平线时会发散。
|
||||
func AirmassPlaneParallelFromTrueAltitude(trueAltitude float64) float64 {
|
||||
return formula.AirmassPlaneParallel(trueAltitude)
|
||||
}
|
||||
|
||||
// AirmassKastenYoungFromApparentAltitude Kasten-Young 大气质量 / Kasten-Young airmass from apparent altitude.
|
||||
//
|
||||
// 输入为视高度角,单位度。
|
||||
func AirmassKastenYoungFromApparentAltitude(apparentAltitude float64) float64 {
|
||||
return formula.AirmassKastenYoung(apparentAltitude)
|
||||
}
|
||||
|
||||
// AirmassPickeringFromApparentAltitude Pickering 大气质量 / Pickering airmass from apparent altitude.
|
||||
//
|
||||
// 输入为视高度角,单位度。
|
||||
func AirmassPickeringFromApparentAltitude(apparentAltitude float64) float64 {
|
||||
return formula.AirmassPickering(apparentAltitude)
|
||||
}
|
||||
|
||||
// AirmassKastenYoungFromTrueAltitude Kasten-Young 大气质量 / Kasten-Young airmass from true altitude.
|
||||
//
|
||||
// 先用 pressureHPa / temperatureC 估算大气折射,将真高度角换算为视高度角,再代入经验公式。
|
||||
func AirmassKastenYoungFromTrueAltitude(trueAltitude, pressureHPa, temperatureC float64) float64 {
|
||||
return formula.AirmassKastenYoung(ApparentAltitude(trueAltitude, pressureHPa, temperatureC))
|
||||
}
|
||||
|
||||
// AirmassPickeringFromTrueAltitude Pickering 大气质量 / Pickering airmass from true altitude.
|
||||
//
|
||||
// 先用 pressureHPa / temperatureC 估算大气折射,将真高度角换算为视高度角,再代入经验公式。
|
||||
func AirmassPickeringFromTrueAltitude(trueAltitude, pressureHPa, temperatureC float64) float64 {
|
||||
return formula.AirmassPickering(ApparentAltitude(trueAltitude, pressureHPa, temperatureC))
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package coord
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"b612.me/astro/formula"
|
||||
)
|
||||
|
||||
func TestAirmassWrappers(t *testing.T) {
|
||||
assertClose(t, "plane-parallel wrapper", AirmassPlaneParallelFromTrueAltitude(30), formula.AirmassPlaneParallel(30), 1e-15)
|
||||
assertClose(t, "kasten-young apparent wrapper", AirmassKastenYoungFromApparentAltitude(30), formula.AirmassKastenYoung(30), 1e-15)
|
||||
assertClose(t, "pickering apparent wrapper", AirmassPickeringFromApparentAltitude(30), formula.AirmassPickering(30), 1e-15)
|
||||
}
|
||||
|
||||
func TestAirmassTrueAltitudeWrappers(t *testing.T) {
|
||||
trueAltitude := 5.0
|
||||
pressureHPa := 1013.25
|
||||
temperatureC := 10.0
|
||||
apparentAltitude := ApparentAltitude(trueAltitude, pressureHPa, temperatureC)
|
||||
|
||||
gotKastenYoung := AirmassKastenYoungFromTrueAltitude(trueAltitude, pressureHPa, temperatureC)
|
||||
wantKastenYoung := formula.AirmassKastenYoung(apparentAltitude)
|
||||
assertClose(t, "kasten-young true-alt wrapper", gotKastenYoung, wantKastenYoung, 1e-12)
|
||||
|
||||
gotPickering := AirmassPickeringFromTrueAltitude(trueAltitude, pressureHPa, temperatureC)
|
||||
wantPickering := formula.AirmassPickering(apparentAltitude)
|
||||
assertClose(t, "pickering true-alt wrapper", gotPickering, wantPickering, 1e-12)
|
||||
|
||||
if math.IsNaN(gotKastenYoung) || math.IsNaN(gotPickering) {
|
||||
t.Fatal("expected finite airmass values for low but above-horizon altitude")
|
||||
}
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
// Package coord 坐标工具包 / coordinate utility package.
|
||||
//
|
||||
// 本包面向用户提供常用坐标变换、恒星时、岁差、站心和地平坐标封装。
|
||||
// 所有角度输入和输出默认使用度;恒星时输出使用小时。
|
||||
// date 按绝对时刻使用,内部会转换为 UTC 后计算。
|
||||
package coord
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"b612.me/astro/basic"
|
||||
)
|
||||
|
||||
// Ecliptic 黄道坐标 / ecliptic coordinates.
|
||||
type Ecliptic struct {
|
||||
Lon float64 // 黄经,单位度 / ecliptic longitude in degrees.
|
||||
Lat float64 // 黄纬,单位度 / ecliptic latitude in degrees.
|
||||
}
|
||||
|
||||
// Equatorial 赤道坐标 / equatorial coordinates.
|
||||
type Equatorial struct {
|
||||
RA float64 // 赤经,单位度 / right ascension in degrees.
|
||||
Dec float64 // 赤纬,单位度 / declination in degrees.
|
||||
}
|
||||
|
||||
// Horizontal 地平坐标 / horizontal coordinates.
|
||||
type Horizontal struct {
|
||||
Azimuth float64 // 方位角,正北为0,顺时针增加 / azimuth from north clockwise, degrees.
|
||||
Altitude float64 // 高度角,单位度 / altitude in degrees.
|
||||
Zenith float64 // 天顶距,单位度 / zenith distance in degrees.
|
||||
HourAngle float64 // 时角,单位度 / hour angle in degrees.
|
||||
}
|
||||
|
||||
func jdeUTC(date time.Time) float64 {
|
||||
return basic.Date2JDE(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)
|
||||
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)
|
||||
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))
|
||||
return Equatorial{RA: nextRA, Dec: nextDec}
|
||||
}
|
||||
|
||||
// EclipticObliquity 黄赤交角 / ecliptic obliquity.
|
||||
func EclipticObliquity(date time.Time, nutation bool) float64 {
|
||||
return basic.EclipticObliquity(jdeUTC(date), nutation)
|
||||
}
|
||||
|
||||
// Nutation2000B IAU 2000B 章动 / IAU 2000B nutation.
|
||||
func Nutation2000B(date time.Time) (longitude, obliquity float64) {
|
||||
return basic.Nutation2000B(jdeUTC(date))
|
||||
}
|
||||
|
||||
// Nutation1980 IAU 1980 章动 / IAU 1980 nutation.
|
||||
func Nutation1980(date time.Time) (longitude, obliquity float64) {
|
||||
return basic.Nutation1980(jdeUTC(date))
|
||||
}
|
||||
|
||||
// MeanSiderealTime 平恒星时,单位小时 / mean sidereal time in hours.
|
||||
func MeanSiderealTime(date time.Time) float64 {
|
||||
return basic.MeanSiderealTime(jdeUTC(date))
|
||||
}
|
||||
|
||||
// ApparentSiderealTime 真恒星时,单位小时 / apparent sidereal time in hours.
|
||||
func ApparentSiderealTime(date time.Time) float64 {
|
||||
return basic.ApparentSiderealTime(jdeUTC(date))
|
||||
}
|
||||
|
||||
// HourAngle 时角 / hour angle.
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
|
||||
// EquatorialToHorizontal 赤道坐标转地平坐标 / converts equatorial to horizontal coordinates.
|
||||
//
|
||||
// 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)
|
||||
return Horizontal{
|
||||
Azimuth: basic.StarAzimuth(jde, ra, dec, observerLon, observerLat, 0),
|
||||
Altitude: altitude,
|
||||
Zenith: 90 - altitude,
|
||||
HourAngle: basic.StarHourAngle(jde, ra, observerLon, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// TopocentricEquatorial 地心赤道坐标转站心赤道坐标 / converts geocentric to topocentric equatorial coordinates.
|
||||
//
|
||||
// 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)
|
||||
return Equatorial{RA: topRA, Dec: topDec}
|
||||
}
|
||||
|
||||
// TopocentricEcliptic 地心黄道坐标转站心黄道坐标 / converts geocentric to topocentric ecliptic coordinates.
|
||||
//
|
||||
// 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 {
|
||||
topLon := basic.TopocentricLo(lon, lat, observerLat, observerLon, jdeUTC(date), distanceAU, height)
|
||||
topLat := basic.TopocentricBo(lon, lat, observerLat, observerLon, jdeUTC(date), distanceAU, height)
|
||||
return Ecliptic{Lon: topLon, Lat: topLat}
|
||||
}
|
||||
|
||||
// AngularSeparation 角距离 / angular separation.
|
||||
//
|
||||
// 输入为两组赤道坐标,单位度;返回角距离,单位度。
|
||||
// Inputs are two equatorial coordinates in degrees; return value is in degrees.
|
||||
func AngularSeparation(ra1, dec1, ra2, dec2 float64) float64 {
|
||||
return basic.StarAngularSeparation(ra1, dec1, ra2, dec2)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package coord
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"b612.me/astro/basic"
|
||||
)
|
||||
|
||||
func assertClose(t *testing.T, name string, got, want, tolerance float64) {
|
||||
t.Helper()
|
||||
if math.Abs(got-want) > tolerance {
|
||||
t.Fatalf("%s mismatch: got %.15f want %.15f", name, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
lon := 139.686111
|
||||
lat := 4.875278
|
||||
|
||||
got := EclipticToEquatorial(date, lon, lat)
|
||||
wantRA, wantDec := basic.LoBoToRaDec(jde, lon, lat)
|
||||
assertClose(t, "ra", got.RA, wantRA, 1e-12)
|
||||
assertClose(t, "dec", got.Dec, wantDec, 1e-12)
|
||||
|
||||
back := EquatorialToEcliptic(date, got.RA, got.Dec)
|
||||
assertClose(t, "lon", back.Lon, lon, 1e-10)
|
||||
assertClose(t, "lat", back.Lat, lat, 1e-10)
|
||||
}
|
||||
|
||||
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())
|
||||
|
||||
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, "obliquity", EclipticObliquity(date, true), basic.EclipticObliquity(jde, true), 1e-12)
|
||||
|
||||
gotLon, gotObl := Nutation2000B(date)
|
||||
wantLon, wantObl := basic.Nutation2000B(jde)
|
||||
assertClose(t, "nutation longitude", gotLon, wantLon, 1e-12)
|
||||
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()))
|
||||
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())
|
||||
ra := 101.28715533
|
||||
dec := -16.71611586
|
||||
observerLon := 115.0
|
||||
observerLat := 40.0
|
||||
|
||||
hz := EquatorialToHorizontal(date, ra, dec, observerLon, observerLat)
|
||||
wantAltitude := basic.StarHeight(jde, ra, dec, observerLon, observerLat, 0)
|
||||
assertClose(t, "altitude", hz.Altitude, wantAltitude, 1e-12)
|
||||
assertClose(t, "zenith", hz.Zenith, 90-wantAltitude, 1e-12)
|
||||
assertClose(t, "azimuth", hz.Azimuth, basic.StarAzimuth(jde, ra, dec, observerLon, observerLat, 0), 1e-12)
|
||||
assertClose(t, "hour angle", hz.HourAngle, basic.StarHourAngle(jde, ra, observerLon, 0), 1e-12)
|
||||
assertClose(t, "hour angle func", HourAngle(date, ra, observerLon), hz.HourAngle, 1e-12)
|
||||
|
||||
top := TopocentricEquatorial(date, ra, dec, observerLon, observerLat, 0.00257, 53)
|
||||
wantRA, wantDec := basic.TopocentricRaDec(ra, dec, observerLat, observerLon, jde, 0.00257, 53)
|
||||
assertClose(t, "topocentric ra", top.RA, wantRA, 1e-12)
|
||||
assertClose(t, "topocentric dec", top.Dec, wantDec, 1e-12)
|
||||
|
||||
ecl := TopocentricEcliptic(date, 139.686111, 4.875278, observerLon, observerLat, 0.00257, 53)
|
||||
assertClose(t, "topocentric lon", ecl.Lon, basic.TopocentricLo(139.686111, 4.875278, observerLat, observerLon, jde, 0.00257, 53), 1e-12)
|
||||
assertClose(t, "topocentric lat", ecl.Lat, basic.TopocentricBo(139.686111, 4.875278, observerLat, observerLon, jde, 0.00257, 53), 1e-12)
|
||||
}
|
||||
|
||||
func TestAngularSeparationWrapper(t *testing.T) {
|
||||
got := AngularSeparation(101.28715533, -16.71611586, 95.9879578, -52.6956611)
|
||||
want := basic.StarAngularSeparation(101.28715533, -16.71611586, 95.9879578, -52.6956611)
|
||||
assertClose(t, "angular separation", got, want, 1e-12)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package coord
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"b612.me/astro/basic"
|
||||
)
|
||||
|
||||
// ParallacticAngleByHourAngle 由时角计算视差角(天顶方向角) / parallactic angle from hour angle.
|
||||
//
|
||||
// hourAngle/dec/observerLat 单位均为度,返回值通常落在 [-180, 180] 度。
|
||||
func ParallacticAngleByHourAngle(hourAngle, dec, observerLat float64) float64 {
|
||||
return basic.ParallacticAngleByHourAngle(hourAngle, dec, observerLat)
|
||||
}
|
||||
|
||||
// ParallacticAngle 由赤经赤纬计算视差角(天顶方向角) / parallactic angle from right ascension and declination.
|
||||
//
|
||||
// ra/dec 为瞬时赤经赤纬;observerLon/observerLat 为观测者经纬度,东正西负、北正南负。
|
||||
// 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)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package coord
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"b612.me/astro/basic"
|
||||
)
|
||||
|
||||
func TestParallacticAngleWrappers(t *testing.T) {
|
||||
date := time.Date(2026, 4, 29, 13, 15, 0, 0, time.UTC)
|
||||
ra := 101.28715533
|
||||
dec := -16.71611586
|
||||
observerLon := 115.0
|
||||
observerLat := 40.0
|
||||
|
||||
got := ParallacticAngle(date, ra, dec, observerLon, observerLat)
|
||||
want := basic.ParallacticAngleByHourAngle(HourAngle(date, ra, observerLon), dec, observerLat)
|
||||
if math.Abs(got-want) > 1e-12 {
|
||||
t.Fatalf("parallactic angle mismatch: got %.15f want %.15f", got, want)
|
||||
}
|
||||
|
||||
if direct := ParallacticAngleByHourAngle(30, 0, 0); math.Abs(direct-90) > 1e-12 {
|
||||
t.Fatalf("direct formula mismatch: got %.15f want %.15f", direct, 90.0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package coord
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"b612.me/astro/basic"
|
||||
)
|
||||
|
||||
// AtmosphericRefractionFromTrueAltitude 真高度角折射修正 / atmospheric refraction from true altitude.
|
||||
//
|
||||
// 输入真高度角,返回应加到真高度角上的大气折射修正量,单位度。
|
||||
func AtmosphericRefractionFromTrueAltitude(trueAltitude, pressureHPa, temperatureC float64) float64 {
|
||||
return basic.RefractionFromTrueAltitude(trueAltitude, pressureHPa, temperatureC)
|
||||
}
|
||||
|
||||
// AtmosphericRefractionFromApparentAltitude 视高度角折射修正 / atmospheric refraction from apparent altitude.
|
||||
//
|
||||
// 输入视高度角,返回对应的大气折射修正量,单位度。
|
||||
func AtmosphericRefractionFromApparentAltitude(apparentAltitude, pressureHPa, temperatureC float64) float64 {
|
||||
return basic.RefractionFromApparentAltitude(apparentAltitude, pressureHPa, temperatureC)
|
||||
}
|
||||
|
||||
// ApparentAltitude 真高度角转视高度角 / apparent altitude from true altitude.
|
||||
//
|
||||
// 输入真高度角,返回加入标准大气折射后的视高度角,单位度。
|
||||
func ApparentAltitude(trueAltitude, pressureHPa, temperatureC float64) float64 {
|
||||
return basic.ApparentAltitude(trueAltitude, pressureHPa, temperatureC)
|
||||
}
|
||||
|
||||
// TrueAltitude 视高度角转真高度角 / true altitude from apparent altitude.
|
||||
//
|
||||
// 输入视高度角,返回去除大气折射后的真高度角,单位度。
|
||||
func TrueAltitude(apparentAltitude, pressureHPa, temperatureC float64) float64 {
|
||||
return basic.TrueAltitude(apparentAltitude, pressureHPa, temperatureC)
|
||||
}
|
||||
|
||||
// EquatorialToApparentHorizontal 赤道坐标转视地平坐标 / converts equatorial coordinates to apparent horizontal coordinates.
|
||||
func EquatorialToApparentHorizontal(date time.Time, ra, dec, observerLon, observerLat, pressureHPa, temperatureC float64) Horizontal {
|
||||
horizontal := EquatorialToHorizontal(date, ra, dec, observerLon, observerLat)
|
||||
horizontal.Altitude = basic.ApparentAltitude(horizontal.Altitude, pressureHPa, temperatureC)
|
||||
horizontal.Zenith = 90 - horizontal.Altitude
|
||||
return horizontal
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package coord
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"b612.me/astro/basic"
|
||||
)
|
||||
|
||||
func TestRefractionWrappers(t *testing.T) {
|
||||
assertClose(t, "AtmosphericRefractionFromTrueAltitude", AtmosphericRefractionFromTrueAltitude(10, 1010, 10), basic.RefractionFromTrueAltitude(10, 1010, 10), 1e-12)
|
||||
assertClose(t, "AtmosphericRefractionFromApparentAltitude", AtmosphericRefractionFromApparentAltitude(10, 1010, 10), basic.RefractionFromApparentAltitude(10, 1010, 10), 1e-12)
|
||||
assertClose(t, "ApparentAltitude", ApparentAltitude(10, 1010, 10), basic.ApparentAltitude(10, 1010, 10), 1e-12)
|
||||
assertClose(t, "TrueAltitude", TrueAltitude(10, 1010, 10), basic.TrueAltitude(10, 1010, 10), 1e-12)
|
||||
}
|
||||
|
||||
func TestEquatorialToApparentHorizontal(t *testing.T) {
|
||||
date := time.Date(2026, 4, 27, 2, 30, 45, 0, time.UTC)
|
||||
ra := 101.28715533
|
||||
dec := -16.71611586
|
||||
observerLon := 115.0
|
||||
observerLat := 40.0
|
||||
pressureHPa := 1010.0
|
||||
temperatureC := 10.0
|
||||
|
||||
trueHorizontal := EquatorialToHorizontal(date, ra, dec, observerLon, observerLat)
|
||||
apparentHorizontal := EquatorialToApparentHorizontal(date, ra, dec, observerLon, observerLat, pressureHPa, temperatureC)
|
||||
|
||||
assertClose(t, "apparent altitude", apparentHorizontal.Altitude, basic.ApparentAltitude(trueHorizontal.Altitude, pressureHPa, temperatureC), 1e-12)
|
||||
assertClose(t, "apparent zenith", apparentHorizontal.Zenith, 90-apparentHorizontal.Altitude, 1e-12)
|
||||
assertClose(t, "apparent azimuth unchanged", apparentHorizontal.Azimuth, trueHorizontal.Azimuth, 1e-12)
|
||||
assertClose(t, "apparent hour angle unchanged", apparentHorizontal.HourAngle, trueHorizontal.HourAngle, 1e-12)
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package coord
|
||||
|
||||
import "math"
|
||||
|
||||
// Galactic 银道坐标 / galactic coordinates.
|
||||
type Galactic struct {
|
||||
Lon float64 // 银经,单位度 / galactic longitude in degrees.
|
||||
Lat float64 // 银纬,单位度 / galactic latitude in degrees.
|
||||
}
|
||||
|
||||
// SOFA ICRS2G/G2ICRS fixed rotation matrix.
|
||||
// Source: IAU SOFA 2023-10-11, iauIcrs2g/iauG2icrs.
|
||||
var icrsToGalacticMatrix = [3][3]float64{
|
||||
{-0.054875560416215368492398900454, -0.873437090234885048760383168409, -0.483835015548713226831774175116},
|
||||
{+0.494109427875583673525222371358, -0.444829629960011178146614061616, +0.746982244497218890527388004556},
|
||||
{-0.867666149019004701181616534570, -0.198076373431201528180486091412, +0.455983776175066922272100478348},
|
||||
}
|
||||
|
||||
// EclipticToEquatorialByObliquity 黄道转赤道坐标(指定黄赤交角) / ecliptic to equatorial by obliquity.
|
||||
//
|
||||
// lon: 黄经,单位度
|
||||
// lat: 黄纬,单位度
|
||||
// obliquity: 黄赤交角,单位度
|
||||
//
|
||||
// 返回:
|
||||
//
|
||||
// 赤经 RA,单位度;赤纬 Dec,单位度
|
||||
func EclipticToEquatorialByObliquity(lon, lat, obliquity float64) Equatorial {
|
||||
sinLon, cosLon := sinCosDeg(lon)
|
||||
sinLat, cosLat := sinCosDeg(lat)
|
||||
sinObliquity, cosObliquity := sinCosDeg(obliquity)
|
||||
|
||||
ra := normalize360(math.Atan2(sinLon*cosObliquity-math.Tan(lat*math.Pi/180)*sinObliquity, cosLon) * 180 / math.Pi)
|
||||
dec := math.Asin(clampUnit(sinLat*cosObliquity+cosLat*sinObliquity*sinLon)) * 180 / math.Pi
|
||||
return Equatorial{RA: ra, Dec: dec}
|
||||
}
|
||||
|
||||
// EquatorialToEclipticByObliquity 赤道转黄道坐标(指定黄赤交角) / equatorial to ecliptic by obliquity.
|
||||
//
|
||||
// ra: 赤经,单位度
|
||||
// dec: 赤纬,单位度
|
||||
// obliquity: 黄赤交角,单位度
|
||||
//
|
||||
// 返回:
|
||||
//
|
||||
// 黄经 Lon,单位度;黄纬 Lat,单位度
|
||||
func EquatorialToEclipticByObliquity(ra, dec, obliquity float64) Ecliptic {
|
||||
sinRA, cosRA := sinCosDeg(ra)
|
||||
sinDec, cosDec := sinCosDeg(dec)
|
||||
sinObliquity, cosObliquity := sinCosDeg(obliquity)
|
||||
|
||||
lon := normalize360(math.Atan2(sinRA*cosObliquity+math.Tan(dec*math.Pi/180)*sinObliquity, cosRA) * 180 / math.Pi)
|
||||
lat := math.Asin(clampUnit(sinDec*cosObliquity-cosDec*sinObliquity*sinRA)) * 180 / math.Pi
|
||||
return Ecliptic{Lon: lon, Lat: lat}
|
||||
}
|
||||
|
||||
// HourAngleDeclinationToHorizontal 时角赤纬转地平坐标 / horizontal coordinates from hour angle and declination.
|
||||
//
|
||||
// hourAngle: 时角,单位度
|
||||
// declination: 赤纬,单位度
|
||||
// latitude: 观测者地理纬度,单位度,北正南负
|
||||
//
|
||||
// 返回:
|
||||
//
|
||||
// 方位角 Azimuth(正北为0,顺时针增加)、高度角 Altitude、天顶距 Zenith,均为度
|
||||
func HourAngleDeclinationToHorizontal(hourAngle, declination, latitude float64) Horizontal {
|
||||
sinLatitude, cosLatitude := sinCosDeg(latitude)
|
||||
sinDeclination, cosDeclination := sinCosDeg(declination)
|
||||
sinHourAngle, cosHourAngle := sinCosDeg(hourAngle)
|
||||
|
||||
altitude := math.Asin(clampUnit(sinLatitude*sinDeclination+cosLatitude*cosDeclination*cosHourAngle)) * 180 / math.Pi
|
||||
azimuth := normalize360(math.Atan2(-cosDeclination*sinHourAngle, cosLatitude*sinDeclination-sinLatitude*cosDeclination*cosHourAngle) * 180 / math.Pi)
|
||||
return Horizontal{
|
||||
Azimuth: azimuth,
|
||||
Altitude: altitude,
|
||||
Zenith: 90 - altitude,
|
||||
HourAngle: normalize360(hourAngle),
|
||||
}
|
||||
}
|
||||
|
||||
// HorizontalToHourAngleDeclination 地平坐标转时角赤纬 / hour angle and declination from horizontal coordinates.
|
||||
//
|
||||
// azimuth: 方位角,单位度,正北为0,顺时针增加
|
||||
// altitude: 高度角,单位度
|
||||
// latitude: 观测者地理纬度,单位度,北正南负
|
||||
//
|
||||
// 返回:
|
||||
//
|
||||
// 时角 HourAngle,单位度;赤纬 Declination,单位度
|
||||
func HorizontalToHourAngleDeclination(azimuth, altitude, latitude float64) (hourAngle, declination float64) {
|
||||
sinLatitude, cosLatitude := sinCosDeg(latitude)
|
||||
sinAltitude, cosAltitude := sinCosDeg(altitude)
|
||||
sinAzimuth, cosAzimuth := sinCosDeg(azimuth)
|
||||
|
||||
declination = math.Asin(clampUnit(sinLatitude*sinAltitude+cosLatitude*cosAltitude*cosAzimuth)) * 180 / math.Pi
|
||||
sinHourAngle := -cosAltitude * sinAzimuth
|
||||
cosHourAngle := sinAltitude*cosLatitude - cosAltitude*sinLatitude*cosAzimuth
|
||||
hourAngle = normalize360(math.Atan2(sinHourAngle, cosHourAngle) * 180 / math.Pi)
|
||||
return hourAngle, declination
|
||||
}
|
||||
|
||||
// EquatorialToHorizontalByLocalSiderealTime 赤道转地平坐标(指定地方恒星时) / equatorial to horizontal by local sidereal time.
|
||||
//
|
||||
// localSiderealTimeHours: 站点本地恒星时,单位小时
|
||||
// ra: 赤经,单位度
|
||||
// dec: 赤纬,单位度
|
||||
// latitude: 观测者地理纬度,单位度,北正南负
|
||||
//
|
||||
// 返回:
|
||||
//
|
||||
// 方位角 Azimuth(正北为0,顺时针增加)、高度角 Altitude、天顶距 Zenith,均为度;
|
||||
// 附带返回对应的时角 HourAngle,单位度
|
||||
//
|
||||
// 例:
|
||||
//
|
||||
// hz := coord.EquatorialToHorizontalByLocalSiderealTime(10.5, 83.6331, 22.0145, 31.2)
|
||||
func EquatorialToHorizontalByLocalSiderealTime(localSiderealTimeHours, ra, dec, latitude float64) Horizontal {
|
||||
hourAngle := normalize360(localSiderealTimeHours*15 - ra)
|
||||
return HourAngleDeclinationToHorizontal(hourAngle, dec, latitude)
|
||||
}
|
||||
|
||||
// HorizontalToEquatorialByLocalSiderealTime 地平转赤道坐标(指定地方恒星时) / horizontal to equatorial by local sidereal time.
|
||||
//
|
||||
// localSiderealTimeHours: 站点本地恒星时,单位小时
|
||||
// azimuth: 方位角,单位度,正北为0,顺时针增加
|
||||
// altitude: 高度角,单位度
|
||||
// latitude: 观测者地理纬度,单位度,北正南负
|
||||
//
|
||||
// 返回:
|
||||
//
|
||||
// 赤经 RA,单位度;赤纬 Dec,单位度
|
||||
//
|
||||
// 例:
|
||||
//
|
||||
// eq := coord.HorizontalToEquatorialByLocalSiderealTime(10.5, 128.2, 37.6, 31.2)
|
||||
func HorizontalToEquatorialByLocalSiderealTime(localSiderealTimeHours, azimuth, altitude, latitude float64) Equatorial {
|
||||
hourAngle, declination := HorizontalToHourAngleDeclination(azimuth, altitude, latitude)
|
||||
ra := normalize360(localSiderealTimeHours*15 - hourAngle)
|
||||
return Equatorial{RA: ra, Dec: declination}
|
||||
}
|
||||
|
||||
// EquatorialToGalactic 赤道转银道坐标 / equatorial to galactic coordinates.
|
||||
//
|
||||
// ra: ICRS 赤经,单位度
|
||||
// dec: ICRS 赤纬,单位度
|
||||
//
|
||||
// 返回:
|
||||
//
|
||||
// 银经 Lon,单位度;银纬 Lat,单位度
|
||||
func EquatorialToGalactic(ra, dec float64) Galactic {
|
||||
vector := sphericalToVector(ra, dec)
|
||||
rotated := matrixVectorMul(icrsToGalacticMatrix, vector)
|
||||
lon, lat := vectorToSpherical(rotated)
|
||||
return Galactic{Lon: lon, Lat: lat}
|
||||
}
|
||||
|
||||
// GalacticToEquatorial 银道转赤道坐标 / galactic to equatorial coordinates.
|
||||
//
|
||||
// lon: 银经,单位度
|
||||
// lat: 银纬,单位度
|
||||
//
|
||||
// 返回:
|
||||
//
|
||||
// ICRS 赤经 RA,单位度;ICRS 赤纬 Dec,单位度
|
||||
func GalacticToEquatorial(lon, lat float64) Equatorial {
|
||||
vector := sphericalToVector(lon, lat)
|
||||
rotated := matrixTransposeVectorMul(icrsToGalacticMatrix, vector)
|
||||
ra, dec := vectorToSpherical(rotated)
|
||||
return Equatorial{RA: ra, Dec: dec}
|
||||
}
|
||||
|
||||
func sinCosDeg(angle float64) (sinValue, cosValue float64) {
|
||||
return math.Sincos(angle * math.Pi / 180)
|
||||
}
|
||||
|
||||
func normalize360(angle float64) float64 {
|
||||
angle = math.Mod(angle, 360)
|
||||
if angle < 0 {
|
||||
angle += 360
|
||||
}
|
||||
return angle
|
||||
}
|
||||
|
||||
func clampUnit(value float64) float64 {
|
||||
if value > 1 {
|
||||
return 1
|
||||
}
|
||||
if value < -1 {
|
||||
return -1
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func sphericalToVector(lon, lat float64) [3]float64 {
|
||||
sinLon, cosLon := sinCosDeg(lon)
|
||||
sinLat, cosLat := sinCosDeg(lat)
|
||||
return [3]float64{cosLat * cosLon, cosLat * sinLon, sinLat}
|
||||
}
|
||||
|
||||
func vectorToSpherical(vector [3]float64) (lon, lat float64) {
|
||||
lon = normalize360(math.Atan2(vector[1], vector[0]) * 180 / math.Pi)
|
||||
lat = math.Asin(clampUnit(vector[2])) * 180 / math.Pi
|
||||
return lon, lat
|
||||
}
|
||||
|
||||
func matrixVectorMul(matrix [3][3]float64, vector [3]float64) [3]float64 {
|
||||
return [3]float64{
|
||||
matrix[0][0]*vector[0] + matrix[0][1]*vector[1] + matrix[0][2]*vector[2],
|
||||
matrix[1][0]*vector[0] + matrix[1][1]*vector[1] + matrix[1][2]*vector[2],
|
||||
matrix[2][0]*vector[0] + matrix[2][1]*vector[1] + matrix[2][2]*vector[2],
|
||||
}
|
||||
}
|
||||
|
||||
func matrixTransposeVectorMul(matrix [3][3]float64, vector [3]float64) [3]float64 {
|
||||
return [3]float64{
|
||||
matrix[0][0]*vector[0] + matrix[1][0]*vector[1] + matrix[2][0]*vector[2],
|
||||
matrix[0][1]*vector[0] + matrix[1][1]*vector[1] + matrix[2][1]*vector[2],
|
||||
matrix[0][2]*vector[0] + matrix[1][2]*vector[1] + matrix[2][2]*vector[2],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package coord
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestObliquityDrivenCoordinateConversions(t *testing.T) {
|
||||
date := time.Date(2026, 4, 27, 10, 30, 45, 0, time.FixedZone("CST", 8*3600))
|
||||
lon := 139.686111
|
||||
lat := 4.875278
|
||||
obliquity := EclipticObliquity(date, true)
|
||||
|
||||
got := EclipticToEquatorialByObliquity(lon, lat, obliquity)
|
||||
want := EclipticToEquatorial(date, lon, lat)
|
||||
assertClose(t, "manual obliquity ra", got.RA, want.RA, 1e-12)
|
||||
assertClose(t, "manual obliquity dec", got.Dec, want.Dec, 1e-12)
|
||||
|
||||
back := EquatorialToEclipticByObliquity(got.RA, got.Dec, obliquity)
|
||||
assertClose(t, "manual obliquity lon", back.Lon, lon, 1e-10)
|
||||
assertClose(t, "manual obliquity lat", back.Lat, lat, 1e-10)
|
||||
}
|
||||
|
||||
func TestHourAngleDrivenHorizontalConversions(t *testing.T) {
|
||||
date := time.Date(2026, 4, 27, 2, 30, 45, 0, time.UTC)
|
||||
ra := 101.28715533
|
||||
dec := -16.71611586
|
||||
observerLon := 115.0
|
||||
observerLat := 40.0
|
||||
hourAngle := HourAngle(date, ra, observerLon)
|
||||
|
||||
got := HourAngleDeclinationToHorizontal(hourAngle, dec, observerLat)
|
||||
want := EquatorialToHorizontal(date, ra, dec, observerLon, observerLat)
|
||||
assertClose(t, "manual hour angle azimuth", got.Azimuth, want.Azimuth, 1e-12)
|
||||
assertClose(t, "manual hour angle altitude", got.Altitude, want.Altitude, 1e-12)
|
||||
assertClose(t, "manual hour angle zenith", got.Zenith, want.Zenith, 1e-12)
|
||||
assertClose(t, "manual hour angle value", got.HourAngle, want.HourAngle, 1e-12)
|
||||
|
||||
roundTripHourAngle, roundTripDeclination := HorizontalToHourAngleDeclination(got.Azimuth, got.Altitude, observerLat)
|
||||
assertClose(t, "round trip hour angle", roundTripHourAngle, normalize360(hourAngle), 1e-10)
|
||||
assertClose(t, "round trip declination", roundTripDeclination, dec, 1e-10)
|
||||
}
|
||||
|
||||
func TestLocalSiderealTimeDrivenHorizontalConversions(t *testing.T) {
|
||||
date := time.Date(2026, 4, 27, 2, 30, 45, 0, time.UTC)
|
||||
ra := 101.28715533
|
||||
dec := -16.71611586
|
||||
observerLon := 115.0
|
||||
observerLat := 40.0
|
||||
localSiderealTimeHours := math.Mod(ApparentSiderealTime(date)+observerLon/15, 24)
|
||||
if localSiderealTimeHours < 0 {
|
||||
localSiderealTimeHours += 24
|
||||
}
|
||||
|
||||
got := EquatorialToHorizontalByLocalSiderealTime(localSiderealTimeHours, ra, dec, observerLat)
|
||||
want := EquatorialToHorizontal(date, ra, dec, observerLon, observerLat)
|
||||
assertClose(t, "LST azimuth", got.Azimuth, want.Azimuth, 1e-12)
|
||||
assertClose(t, "LST altitude", got.Altitude, want.Altitude, 1e-12)
|
||||
assertClose(t, "LST zenith", got.Zenith, want.Zenith, 1e-12)
|
||||
assertClose(t, "LST hour angle", got.HourAngle, want.HourAngle, 1e-12)
|
||||
|
||||
back := HorizontalToEquatorialByLocalSiderealTime(localSiderealTimeHours, got.Azimuth, got.Altitude, observerLat)
|
||||
assertClose(t, "LST round trip ra", back.RA, ra, 1e-10)
|
||||
assertClose(t, "LST round trip dec", back.Dec, dec, 1e-10)
|
||||
}
|
||||
|
||||
func TestGalacticCoordinateConversions(t *testing.T) {
|
||||
galacticCenter := EquatorialToGalactic(266.4051, -28.936175)
|
||||
assertClose(t, "galactic center lon", galacticCenter.Lon, 0, 5e-4)
|
||||
assertClose(t, "galactic center lat", galacticCenter.Lat, 0, 5e-4)
|
||||
|
||||
pole := GalacticToEquatorial(0, 90)
|
||||
assertClose(t, "north galactic pole ra", pole.RA, 192.85948, 1e-5)
|
||||
assertClose(t, "north galactic pole dec", pole.Dec, 27.12825, 1e-5)
|
||||
|
||||
sample := EquatorialToGalactic(83.6331, 22.0145)
|
||||
back := GalacticToEquatorial(sample.Lon, sample.Lat)
|
||||
assertClose(t, "galactic round trip ra", back.RA, 83.6331, 1e-10)
|
||||
assertClose(t, "galactic round trip dec", back.Dec, 22.0145, 1e-10)
|
||||
}
|
||||
|
||||
func TestHorizontalRoundTripAcrossQuadrants(t *testing.T) {
|
||||
samples := []struct {
|
||||
hourAngle float64
|
||||
declination float64
|
||||
latitude float64
|
||||
}{
|
||||
{15, 20, 35},
|
||||
{95, -10, 52},
|
||||
{210, 45, -20},
|
||||
{315, -35, 10},
|
||||
}
|
||||
|
||||
for _, sample := range samples {
|
||||
hz := HourAngleDeclinationToHorizontal(sample.hourAngle, sample.declination, sample.latitude)
|
||||
hourAngle, declination := HorizontalToHourAngleDeclination(hz.Azimuth, hz.Altitude, sample.latitude)
|
||||
if math.Abs(hourAngle-normalize360(sample.hourAngle)) > 1e-10 {
|
||||
t.Fatalf("hour angle round trip mismatch: got %.15f want %.15f", hourAngle, normalize360(sample.hourAngle))
|
||||
}
|
||||
if math.Abs(declination-sample.declination) > 1e-10 {
|
||||
t.Fatalf("declination round trip mismatch: got %.15f want %.15f", declination, sample.declination)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user