feat: 新增月掩与日月食地理绘图并提升观测计算精度
- 新增月掩恒星和行星:支持搜索、掩甚点、全球掩带及固定地点轨迹计算 - 支持恒星星表坐标转换、有限盘面行星接触事件和月掩 SVG 输出 - 新增日月食及月掩全球投影图、时间标记和 GeoJSON 地理数据接口 - 扩展日食中心线、南北界及偏食足迹采样,支持极区投影 - 修正站心时角、月出月落、月球视半径、折射和恒星自行计算 - 优化内外行星事件搜索、边界选择、极端输入处理和计算稳定性
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
package basic
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestStarOccultationLatitudeEnvelopePrefilter(t *testing.T) {
|
||||
start := occultationTimeToTT(time.Date(2026, 8, 1, 0, 0, 0, 0, time.UTC))
|
||||
end := start + starOccultationSiderealMonthDays
|
||||
polar := StarCoordinate{RA: 0, Dec: 89, Epoch: time.Date(2000, 1, 1, 12, 0, 0, 0, time.UTC), Frame: CoordinateFrameICRS}
|
||||
if starOccultationLatitudeEnvelopePass(start, end, polar, nil, 0) {
|
||||
t.Fatal("polar star should be rejected by global ecliptic-latitude envelope")
|
||||
}
|
||||
if !starOccultationLatitudeEnvelopePass(start, end, hr4799OccultationCoordinateForTest(), nil, 0) {
|
||||
t.Fatal("HR 4799 should pass the global ecliptic-latitude envelope")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStarOccultationScanCandidatesFindsMultipleLocalMinima(t *testing.T) {
|
||||
value := func(tt float64) float64 {
|
||||
return (tt-1)*(tt-1)*(tt-3)*(tt-3) + 0.001
|
||||
}
|
||||
got := starOccultationScanCandidates(0, 4, 0.5, value, func(float64) bool { return true })
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("candidate count = %d, want 2: %v", len(got), got)
|
||||
}
|
||||
if math.Abs(got[0]-1) > 1e-6 || math.Abs(got[1]-3) > 1e-6 {
|
||||
t.Fatalf("candidate minima = %v, want [1 3]", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStarOccultationScanCandidatesFindsNarrowWindowMidpoint(t *testing.T) {
|
||||
const (
|
||||
start = 10.0
|
||||
end = 10.1
|
||||
want = (start + end) / 2
|
||||
)
|
||||
value := func(tt float64) float64 { return (tt - want) * (tt - want) }
|
||||
got := starOccultationScanCandidates(start, end, starOccultationDefaultStepDays, value, func(float64) bool { return true })
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("candidate count = %d, want 1: %v", len(got), got)
|
||||
}
|
||||
if math.Abs(got[0]-want) > 1e-7 {
|
||||
t.Fatalf("candidate midpoint = %.12f, want %.12f", got[0], want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUniqueOccultationCandidateTimesFiltersSingleOutsideCandidate(t *testing.T) {
|
||||
if got := uniqueOccultationCandidateTimes([]float64{9.9}, 10, 11); len(got) != 0 {
|
||||
t.Fatalf("outside candidate was not filtered: %v", got)
|
||||
}
|
||||
got := uniqueOccultationCandidateTimes([]float64{10.5}, 10, 11)
|
||||
if len(got) != 1 || got[0] != 10.5 {
|
||||
t.Fatalf("inside candidate = %v, want [10.5]", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStarOccultationPropagatesApparentCoordinateProperMotion(t *testing.T) {
|
||||
epoch := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
target := epoch.Add(365*24*time.Hour + 6*time.Hour)
|
||||
star := StarCoordinate{
|
||||
RA: 10,
|
||||
Dec: 20,
|
||||
Epoch: epoch,
|
||||
Frame: CoordinateFrameApparentOfDate,
|
||||
ProperMotionRACosDecMasPerYear: 360000,
|
||||
ProperMotionDecMasPerYear: -720000,
|
||||
}
|
||||
baseline := star
|
||||
baseline.ProperMotionRACosDecMasPerYear = 0
|
||||
baseline.ProperMotionDecMasPerYear = 0
|
||||
baselineRA, baselineDec := starApparentRaDec(occultationTimeToTT(target), baseline, Observer{})
|
||||
ra, dec := starApparentRaDec(occultationTimeToTT(target), star, Observer{})
|
||||
raMotion := signedAngleDifference(ra, baselineRA) * math.Cos(baselineDec*math.Pi/180)
|
||||
decMotion := dec - baselineDec
|
||||
if math.Abs(raMotion-0.1) > 0.001 {
|
||||
t.Fatalf("propagated RA*cos(Dec) motion = %.10f deg, want 0.1", raMotion)
|
||||
}
|
||||
if math.Abs(decMotion-(-0.2)) > 0.001 {
|
||||
t.Fatalf("propagated Dec motion = %.10f deg, want -0.2", decMotion)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStarOccultationApparentOfDateRoundTripsAtEpoch(t *testing.T) {
|
||||
epoch := time.Date(2026, 8, 2, 12, 0, 0, 0, time.UTC)
|
||||
star := StarCoordinate{RA: 189.5, Dec: -6.2, Epoch: epoch, Frame: CoordinateFrameApparentOfDate, ParallaxMas: 100}
|
||||
ra, dec := starApparentRaDecGeocentric(occultationTimeToTT(epoch), star)
|
||||
if math.Abs(signedAngleDifference(ra, star.RA))*3600 > 1e-5 || math.Abs(dec-star.Dec)*3600 > 1e-5 {
|
||||
t.Fatalf("apparent coordinate did not round-trip at epoch: got %.12f %.12f", ra, dec)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStarOccultationApparentPlaceCorrections(t *testing.T) {
|
||||
location := time.FixedZone("CST", 8*3600)
|
||||
tt := occultationTimeToTT(time.Date(2025, 6, 5, 20, 2, 7, 700000000, location))
|
||||
star := hr4799OccultationCoordinateForTest()
|
||||
|
||||
gotRA, gotDec := starApparentRaDecGeocentric(tt, star)
|
||||
if math.Abs(signedAngleDifference(gotRA, 189.527817)) > 0.0002 || math.Abs(gotDec-(-5.973401)) > 0.0002 {
|
||||
t.Fatalf("apparent place = %.9f %.9f, want near 189.527817 -5.973401", gotRA, gotDec)
|
||||
}
|
||||
|
||||
years := (tt - Date2JDE(star.Epoch.UTC())) / 365.25
|
||||
meanRA := star.RA + years*star.ProperMotionRACosDecMasPerYear/(3600000*math.Cos(star.Dec*math.Pi/180))
|
||||
meanDec := star.Dec + years*star.ProperMotionDecMasPerYear/3600000
|
||||
meanRA, meanDec = Precess(meanRA, meanDec, 2451545, tt)
|
||||
correction := angularSeparationDegrees(meanRA, meanDec, gotRA, gotDec) * 3600
|
||||
if correction < 5 || correction > 30 {
|
||||
t.Fatalf("apparent-place correction = %.6f arcsec, want a plausible annual correction", correction)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStarOccultationApparentPlaceAppliesAnnualParallax(t *testing.T) {
|
||||
star := hr4799OccultationCoordinateForTest()
|
||||
tt := occultationTimeToTT(time.Date(2025, 6, 5, 12, 0, 0, 0, time.UTC))
|
||||
withoutRA, withoutDec := starApparentRaDecGeocentric(tt, star)
|
||||
star.ParallaxMas = 1000
|
||||
withRA, withDec := starApparentRaDecGeocentric(tt, star)
|
||||
shift := angularSeparationDegrees(withoutRA, withoutDec, withRA, withDec) * 3600
|
||||
if shift < 0.05 || shift > 1.1 {
|
||||
t.Fatalf("annual parallax shift = %.6f arcsec, want (0.05, 1.1]", shift)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStarOccultationICRSAppliesJ2000FrameBias(t *testing.T) {
|
||||
epoch := time.Date(2000, time.January, 1, 12, 0, 0, 0, time.UTC)
|
||||
icrs := StarCoordinate{RA: 0, Dec: 0, Epoch: epoch, Frame: CoordinateFrameICRS}
|
||||
j2000 := icrs
|
||||
j2000.Frame = CoordinateFrameJ2000
|
||||
tt := occultationTimeToTT(epoch)
|
||||
icrsRA, icrsDec := starApparentRaDecGeocentric(tt, icrs)
|
||||
j2000RA, j2000Dec := starApparentRaDecGeocentric(tt, j2000)
|
||||
raBiasMas := signedAngleDifference(icrsRA, j2000RA) * 3600000
|
||||
decBiasMas := (icrsDec - j2000Dec) * 3600000
|
||||
if math.Abs(raBiasMas-14.6) > 0.1 || math.Abs(decBiasMas-(-16.617)) > 0.1 {
|
||||
t.Fatalf("ICRS frame bias = %.6f %.6f mas, want about 14.6 -16.617", raBiasMas, decBiasMas)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefinedStarOccultationCenterLineRespectsWidthTolerance(t *testing.T) {
|
||||
star := hr4799OccultationCoordinateForTest()
|
||||
start := time.Date(2025, time.June, 5, 0, 0, 0, 0, time.UTC)
|
||||
paths, err := FindStarOccultationPaths(
|
||||
start, start.Add(24*time.Hour), star,
|
||||
OccultationPathOptions{Step: 5 * time.Minute, TargetSpacingKM: 50},
|
||||
)
|
||||
if err != nil || len(paths) != 1 {
|
||||
t.Fatalf("FindStarOccultationPaths() paths=%d err=%v, want one", len(paths), err)
|
||||
}
|
||||
for index, point := range paths[0].CenterLine {
|
||||
exact, ok := starOccultationPathCenterPoint(centerTimeTT(point.Time), star, time.UTC)
|
||||
if !ok {
|
||||
t.Fatalf("exact center point %d is unavailable", index)
|
||||
}
|
||||
if difference := math.Abs(point.WidthKM - exact.WidthKM); difference > occultationPathWidthToleranceKM {
|
||||
t.Fatalf("center point %d width differs from exact value by %.9f km: got %.9f want %.9f",
|
||||
index, difference, point.WidthKM, exact.WidthKM)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefineOccultationPathWidthsBoundsSmoothInterpolationError(t *testing.T) {
|
||||
start := time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||
startTT := occultationTimeToTT(start)
|
||||
widthAt := func(tt float64) (float64, bool) {
|
||||
seconds := (tt - startTT) * 86400
|
||||
return 3500 + 0.0002*(seconds-50)*(seconds-50), true
|
||||
}
|
||||
points := make([]OccultationPathPoint, 101)
|
||||
for index := range points {
|
||||
points[index].Time = start.Add(time.Duration(index) * time.Second)
|
||||
}
|
||||
points[0].WidthKM, _ = widthAt(centerTimeTT(points[0].Time))
|
||||
points[len(points)-1].WidthKM, _ = widthAt(centerTimeTT(points[len(points)-1].Time))
|
||||
refineOccultationPathWidths(points, widthAt)
|
||||
for index, point := range points {
|
||||
exact, _ := widthAt(centerTimeTT(point.Time))
|
||||
if difference := math.Abs(point.WidthKM - exact); difference > occultationPathWidthToleranceKM {
|
||||
t.Fatalf("interpolated width %d differs by %.9f km, tolerance %.9f", index, difference, occultationPathWidthToleranceKM)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hr4799OccultationCoordinateForTest() StarCoordinate {
|
||||
return StarCoordinate{
|
||||
ID: "HR 4799",
|
||||
RA: 189.1975,
|
||||
Dec: -5.831944444444,
|
||||
Epoch: time.Date(2000, 1, 1, 12, 0, 0, 0, time.UTC),
|
||||
Frame: CoordinateFrameJ2000,
|
||||
ProperMotionRACosDecMasPerYear: -28,
|
||||
ProperMotionDecMasPerYear: -18,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user