197 lines
8.1 KiB
Go
197 lines
8.1 KiB
Go
|
|
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,
|
||
|
|
}
|
||
|
|
}
|