Files
astro/basic/lunar_limb_test.go
T

490 lines
22 KiB
Go
Raw Normal View History

package basic
import (
"math"
"testing"
"time"
"b612.me/astro/internal/geodata"
"b612.me/astro/moon/limb"
)
// recordingLunarLimb 记录最后一次查询参数,返回固定半径。
type recordingLunarLimb struct {
lon, lat, angle float64
radiusKM float64
}
func (limb *recordingLunarLimb) LimbRadiusKM(lon, lat, angle float64) (float64, bool) {
limb.lon, limb.lat, limb.angle = lon, lat, angle
return limb.radiusKM, true
}
func lunarLimbAngleDiff(a, b float64) float64 {
d := math.Mod(a-b, 360)
if d > 180 {
d -= 360
}
if d < -180 {
d += 360
}
return d
}
// 地心观测者所见的月面经纬必须还原该时刻的总天平动;目标位于月球北极投影方向时位置角为 0,
// 向天东转 90° 时为 90°。
func TestLunarLimbFrameRoundTrip(t *testing.T) {
for _, jde := range []float64{2460409.25, 2451545.0, 2462000.7} {
_, moon := solarEclipseSunMoonEquatorial(jde)
moonXYZ := solarEclipseLLRToXYZ(moon[0], moon[1], moon[2])
limb := &recordingLunarLimb{radiusKM: 1737.4}
frame := newLunarLimbFrame(limb, jde, moonXYZ)
if !frame.valid {
t.Fatalf("jde %v: frame invalid", jde)
}
physical := MoonPhysical(jde)
moonUnit, _ := lunarLimbNormalize(moonXYZ)
north, east, _ := lunarLimbSkyBasis(moonUnit)
for _, offset := range []float64{0, 90, 200} {
pa := (physical.PositionAngle + offset) * rad
// 目标放在月心旁 0.5° 处、位置角为 P + offset。
sky := lunarLimbCombine(math.Cos(pa), north, math.Sin(pa), east)
target := lunarLimbCombine(math.Cos(0.5*rad), moonUnit, math.Sin(0.5*rad), sky)
target = [3]float64{target[0] * 1.5e8, target[1] * 1.5e8, target[2] * 1.5e8}
if _, ok := frame.radiusKM(moonXYZ, target, false); !ok {
t.Fatalf("jde %v: radiusKM failed", jde)
}
if d := lunarLimbAngleDiff(limb.lon, physical.LibrationLongitude); math.Abs(d) > 1e-6 {
t.Errorf("jde %v: libration longitude %.9f, want %.9f", jde, limb.lon, physical.LibrationLongitude)
}
if d := limb.lat - physical.LibrationLatitude; math.Abs(d) > 1e-6 {
t.Errorf("jde %v: libration latitude %.9f, want %.9f", jde, limb.lat, physical.LibrationLatitude)
}
if d := lunarLimbAngleDiff(limb.angle, -offset); math.Abs(d) > 1e-6 {
t.Errorf("jde %v offset %v: position angle %.9f", jde, offset, limb.angle)
}
if _, ok := frame.radiusKM(moonXYZ, target, true); !ok {
t.Fatalf("jde %v: away radiusKM failed", jde)
}
if d := lunarLimbAngleDiff(limb.angle, 180-offset); math.Abs(d) > 1e-6 {
t.Errorf("jde %v offset %v: away position angle %.9f", jde, offset, limb.angle)
}
}
}
}
// 参考球半径等于 k·R⊕ 的常数月缘只能让结果在末位附近变化,用来确认替换路径本身不引入偏差。
func TestLocalSolarEclipseConstantLimbMatchesCircular(t *testing.T) {
const seed, lon, lat = 2460409.25, -96.8, 32.8 // 2024-04-08,达拉斯附近
options := SolarEclipseOptions{RadiusModel: SolarEclipseModelIAUSingleK}
circular := LocalSolarEclipseWithOptionsWithInstance(seed, lon, lat, 0, options, nil)
if !circular.HasTotal {
t.Fatalf("expected total eclipse, got %v", circular.Type)
}
options.LunarLimb = &recordingLunarLimb{
radiusKM: geodata.EarthEquatorialRadiusKM * solarEclipseIAUSingleRadiusK * localSolarMoonRadiusScale,
}
limbed := LocalSolarEclipseWithOptionsWithInstance(seed, lon, lat, 0, options, nil)
for _, pair := range [][2]float64{
{circular.PartialStart, limbed.PartialStart},
{circular.CentralStart, limbed.CentralStart},
{circular.CentralEnd, limbed.CentralEnd},
{circular.PartialEnd, limbed.PartialEnd},
} {
if d := math.Abs(pair[0]-pair[1]) * 86400; d > 1e-3 {
t.Errorf("contact moved by %.6f s with a constant limb", d)
}
}
}
func TestLunarEclipseConstantLimbMatchesCircular(t *testing.T) {
limb := &recordingLunarLimb{radiusKM: lunarMoonRadiusScale}
for _, chauvenet := range []bool{false, true} {
circular := LunarEclipseDanjonWithInstance(2460926.0, nil)
if chauvenet {
circular = LunarEclipseChauvenetWithInstance(2460926.0, nil)
}
limbed := LunarEclipseWithLunarLimb(circular, limb, nil)
for _, pair := range [][2]float64{
{circular.PenumbralStart, limbed.PenumbralStart},
{circular.PartialStart, limbed.PartialStart},
{circular.TotalStart, limbed.TotalStart},
{circular.TotalEnd, limbed.TotalEnd},
{circular.PartialEnd, limbed.PartialEnd},
{circular.PenumbralEnd, limbed.PenumbralEnd},
} {
if d := math.Abs(pair[0]-pair[1]) * 86400; d > 0.05 {
t.Errorf("chauvenet=%v: contact moved by %.4f s with a constant limb", chauvenet, d)
}
}
if limbed.Maximum != circular.Maximum || limbed.Magnitude != circular.Magnitude {
t.Error("maximum or magnitude changed")
}
}
if got := LunarEclipseWithLunarLimb(LunarEclipseDanjonWithInstance(2460926.0, nil), nil, nil); got != LunarEclipseDanjonWithInstance(2460926.0, nil) {
t.Error("nil limb must return the input unchanged")
}
}
func TestOccultationConstantLimbMatchesCircular(t *testing.T) {
limb := &recordingLunarLimb{radiusKM: moonEquatorialRadiusKM}
start := time.Date(2024, time.August, 21, 1, 30, 0, 0, time.UTC)
end := time.Date(2024, time.August, 21, 4, 0, 0, 0, time.UTC)
observer := Observer{Longitude: -30.072, Latitude: 16.21}
plain, err := FindPlanetOccultationsWithInstance(start, end, OccultationSaturn,
observer.Longitude, observer.Latitude, 0, OccultationSearchOptions{}, nil)
if err != nil || len(plain) != 1 {
t.Fatalf("circular planet search: %v, %d events", err, len(plain))
}
limbed, err := FindPlanetOccultationsWithInstance(start, end, OccultationSaturn,
observer.Longitude, observer.Latitude, 0, OccultationSearchOptions{LunarLimb: limb}, nil)
if err != nil || len(limbed) != 1 {
t.Fatalf("limb planet search: %v, %d events", err, len(limbed))
}
a, b := plain[0], limbed[0]
for i, pair := range [][2]time.Time{
{a.ExternalImmersion, b.ExternalImmersion}, {a.InternalImmersion, b.InternalImmersion},
{a.InternalEmersion, b.InternalEmersion}, {a.ExternalEmersion, b.ExternalEmersion},
} {
if d := math.Abs(pair[0].Sub(pair[1]).Seconds()); d > 0.01 {
t.Errorf("planet contact %d moved by %.4f s with a constant limb", i, d)
}
}
if !a.Greatest.Equal(b.Greatest) || a.Type != b.Type {
t.Error("greatest or type changed")
}
}
func TestGlobalSolarEclipseConstantLimbMatchesCircular(t *testing.T) {
// IAU Single-K 的内外 k 相同,常数月缘可同时复现外切与内切半径。
limb := &recordingLunarLimb{radiusKM: geodata.EarthEquatorialRadiusKM * solarEclipseIAUSingleRadiusK * localSolarMoonRadiusScale}
options := SolarEclipseOptions{RadiusModel: SolarEclipseModelIAUSingleK}
for _, seed := range []float64{2460409.0, 2460232.0} {
circular := SolarEclipseWithOptionsWithInstance(seed, options, nil)
withLimb := options
withLimb.LunarLimb = limb
limbed := SolarEclipseWithOptionsWithInstance(seed, withLimb, nil)
for i, pair := range [][2]float64{
{circular.PartialBeginOnEarth, limbed.PartialBeginOnEarth},
{circular.PartialEndOnEarth, limbed.PartialEndOnEarth},
} {
if d := math.Abs(pair[0]-pair[1]) * 86400; d > 0.05 {
t.Errorf("seed %.0f contact %d moved by %.4f s with a constant limb", seed, i, d)
}
}
if limbed.GreatestEclipse != circular.GreatestEclipse || limbed.Type != circular.Type ||
limbed.CentralBeginOnEarth != circular.CentralBeginOnEarth || limbed.PathWidthKM != circular.PathWidthKM {
t.Errorf("seed %.0f: non-contact fields changed", seed)
}
if d := math.Abs(limbed.CentralDurationDays-circular.CentralDurationDays) * 86400; d > 0.05 {
t.Errorf("seed %.0f: central duration moved by %.4f s with a constant limb", seed, d)
}
}
}
func TestCentralPathConstantLimbKeepsLimits(t *testing.T) {
limb := &recordingLunarLimb{radiusKM: geodata.EarthEquatorialRadiusKM * solarEclipseUmbralK * localSolarMoonRadiusScale}
for _, seed := range []float64{2460409.0, 2460232.0} {
circular := SolarEclipseCentralPathNASABulletinSplitKWithInstance(seed, SolarEclipsePathOptions{}, nil)
limbed := SolarEclipseCentralPathNASABulletinSplitKWithInstance(seed, SolarEclipsePathOptions{LunarLimb: limb}, nil)
if len(circular.NorthernLimit) == 0 || len(limbed.NorthernLimit) != len(circular.NorthernLimit) ||
len(limbed.SouthernLimit) != len(circular.SouthernLimit) {
t.Fatalf("seed %.0f: limit lengths %d/%d vs %d/%d", seed,
len(limbed.NorthernLimit), len(limbed.SouthernLimit), len(circular.NorthernLimit), len(circular.SouthernLimit))
}
worst := 0.0
for _, pair := range [][2][]SolarEclipsePathPoint{{circular.NorthernLimit, limbed.NorthernLimit}, {circular.SouthernLimit, limbed.SouthernLimit}} {
for i := range pair[0] {
d := math.Hypot((pair[0][i].Longitude-pair[1][i].Longitude)*math.Cos(pair[0][i].Latitude*rad), pair[0][i].Latitude-pair[1][i].Latitude) * 111.2
worst = math.Max(worst, d)
}
}
if worst > 0.05 {
t.Errorf("seed %.0f: constant limb moved a limit by %.3f km", seed, worst)
}
}
}
func TestOccultationPathConstantLimbKeepsLimits(t *testing.T) {
limb := &recordingLunarLimb{radiusKM: moonEquatorialRadiusKM}
star := 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,
}
start := time.Date(2025, 6, 4, 16, 0, 0, 0, time.UTC)
end := time.Date(2025, 6, 5, 16, 0, 0, 0, time.UTC)
options := OccultationPathOptions{Step: 5 * time.Minute, TargetSpacingKM: 200, DisableFootprints: true, DisableRiseSet: true}
circular, err := FindStarOccultationPathsWithInstance(start, end, star, options, nil)
if err != nil || len(circular) != 1 {
t.Fatalf("circular: %v %d", err, len(circular))
}
options.LunarLimb = limb
limbed, err := FindStarOccultationPathsWithInstance(start, end, star, options, nil)
if err != nil || len(limbed) != 1 {
t.Fatalf("limbed: %v %d", err, len(limbed))
}
worst := 0.0
for _, pair := range [][2][]OccultationPathPoint{{circular[0].NorthernLimit, limbed[0].NorthernLimit}, {circular[0].SouthernLimit, limbed[0].SouthernLimit}} {
if len(pair[0]) == 0 || len(pair[0]) != len(pair[1]) {
t.Fatalf("limit lengths %d vs %d", len(pair[0]), len(pair[1]))
}
for i := range pair[0] {
d := math.Hypot((pair[0][i].Longitude-pair[1][i].Longitude)*math.Cos(pair[0][i].Latitude*rad), pair[0][i].Latitude-pair[1][i].Latitude) * 111.2
worst = math.Max(worst, d)
}
}
if worst > 0.05 {
t.Errorf("constant limb moved a graze limit by %.3f km", worst)
}
if limbed[0].LunarLimbApplied {
t.Fatal("constant circular limb set LunarLimbApplied = true")
}
}
func TestCentralPathConstantLimbKeepsWidthsAndBand(t *testing.T) {
limb := &recordingLunarLimb{radiusKM: geodata.EarthEquatorialRadiusKM * solarEclipseUmbralK * localSolarMoonRadiusScale}
for _, seed := range []float64{2460409.0, 2460232.0} {
circular := SolarEclipseCentralPathNASABulletinSplitKWithInstance(seed, SolarEclipsePathOptions{}, nil)
limbed := SolarEclipseCentralPathNASABulletinSplitKWithInstance(seed, SolarEclipsePathOptions{LunarLimb: limb}, nil)
if len(limbed.CenterLine) != len(circular.CenterLine) || len(limbed.CentralBandSegments) != len(circular.CentralBandSegments) {
t.Fatalf("seed %.0f: sample counts changed", seed)
}
worstWidth := 0.0
for i := range circular.CenterLine {
worstWidth = math.Max(worstWidth, math.Abs(limbed.CenterLine[i].WidthKM-circular.CenterLine[i].WidthKM))
}
if worstWidth > 0.1 {
t.Errorf("seed %.0f: constant limb moved a width by %.3f km", seed, worstWidth)
}
if d := math.Abs(limbed.Greatest.WidthKM - circular.Greatest.WidthKM); d > 0.1 {
t.Errorf("seed %.0f: greatest width moved by %.3f km", seed, d)
}
if d := math.Abs(limbed.MaxCentralDurationDays-circular.MaxCentralDurationDays) * 86400; d > 0.05 {
t.Errorf("seed %.0f: max duration moved by %.4f s", seed, d)
}
worstBand := 0.0
for s := range circular.CentralBandSegments {
if len(limbed.CentralBandSegments[s]) != len(circular.CentralBandSegments[s]) {
t.Fatalf("seed %.0f: band segment %d length changed", seed, s)
}
for i, a := range circular.CentralBandSegments[s] {
b := limbed.CentralBandSegments[s][i]
d := math.Hypot((a.Longitude-b.Longitude)*math.Cos(a.Latitude*rad), a.Latitude-b.Latitude) * 111.2
worstBand = math.Max(worstBand, d)
}
}
if worstBand > 0.1 {
t.Errorf("seed %.0f: constant limb moved a band vertex by %.3f km", seed, worstBand)
}
}
}
// failingLunarLimb 处处无值:月缘修正必须整体退回圆月面且不置 LunarLimbApplied。
type failingLunarLimb struct{}
func (failingLunarLimb) LimbRadiusKM(_, _, _ float64) (float64, bool) { return 0, false }
func TestFailingLimbKeepsEclipsesCircular(t *testing.T) {
const seed, lon, lat = 2460409.25, -96.8, 32.8 // 2024-04-08,达拉斯附近
limb := failingLunarLimb{}
for _, model := range []SolarEclipseRadiusModel{SolarEclipseModelNASABulletinSplitK, SolarEclipseModelIAUSingleK} {
options := SolarEclipseOptions{RadiusModel: model}
withLimb := options
withLimb.LunarLimb = limb
circularLocal := LocalSolarEclipseWithOptionsWithInstance(seed, lon, lat, 0, options, nil)
limbedLocal := LocalSolarEclipseWithOptionsWithInstance(seed, lon, lat, 0, withLimb, nil)
if limbedLocal != circularLocal {
t.Errorf("model %v: failing limb changed the local result", model)
}
circularGlobal := SolarEclipseWithOptionsWithInstance(seed, options, nil)
limbedGlobal := SolarEclipseWithOptionsWithInstance(seed, withLimb, nil)
if limbedGlobal.LunarLimbApplied ||
limbedGlobal.PartialBeginOnEarth != circularGlobal.PartialBeginOnEarth ||
limbedGlobal.PartialEndOnEarth != circularGlobal.PartialEndOnEarth ||
limbedGlobal.CentralDurationDays != circularGlobal.CentralDurationDays {
t.Errorf("model %v: failing limb changed the global result or set LunarLimbApplied", model)
}
}
circularLunar := LunarEclipseDanjonWithInstance(2460926.0, nil)
if got := LunarEclipseWithLunarLimb(circularLunar, limb, nil); got != circularLunar {
t.Error("failing limb changed the lunar eclipse result")
}
}
func TestPartialBoundaryLimbCorrection(t *testing.T) {
const seed = 2460409.25 // 2024-04-08
circularOptions := SolarEclipsePartialFootprintOptions{}
circular := solarEclipsePartialFootprints(seed, SolarEclipseModelIAUSingleK, circularOptions, nil)
if len(circular.PartialBandContours) == 0 || len(circular.RiseSetCurves) == 0 {
t.Fatal("circular partial boundary missing")
}
samePoints := func(a, b []SolarEclipsePathPoint, toleranceKM float64) float64 {
worst := 0.0
for index := range a {
km := math.Hypot(signedAngleDifference(a[index].Longitude, b[index].Longitude)*111.19*math.Cos(a[index].Latitude*rad),
(a[index].Latitude-b[index].Latitude)*111.19)
worst = math.Max(worst, km)
}
return worst
}
compare := func(name string, limb LunarLimb, wantApplied bool, maxShiftKM float64) {
options := circularOptions
options.LunarLimb = limb
limbed := solarEclipsePartialFootprints(seed, SolarEclipseModelIAUSingleK, options, nil)
if limbed.PartialBoundaryLimbApplied != wantApplied {
t.Errorf("%s: PartialBoundaryLimbApplied = %v, want %v", name, limbed.PartialBoundaryLimbApplied, wantApplied)
}
if limbed.RiseSetTopologyDegraded {
t.Errorf("%s: rise/set topology degraded", name)
}
if len(limbed.PartialBandContours) != len(circular.PartialBandContours) || len(limbed.RiseSetCurves) != len(circular.RiseSetCurves) {
t.Fatalf("%s: boundary structure changed", name)
}
worst := 0.0
for index := range circular.PartialBandContours {
worst = math.Max(worst, samePoints(circular.PartialBandContours[index], limbed.PartialBandContours[index], 0))
}
for index, curve := range circular.RiseSetCurves {
for segment := range curve.Segments {
shift := samePoints(curve.Segments[segment], limbed.RiseSetCurves[index].Segments[segment], 0)
if curve.Phase == RiseSetPhaseGreatest && shift != 0 {
t.Errorf("%s: greatest rise/set curve moved by %.3f km", name, shift)
}
worst = math.Max(worst, shift)
}
}
if worst > maxShiftKM {
t.Errorf("%s: boundary moved by %.3f km, want <= %.3f km", name, worst, maxShiftKM)
}
if wantApplied && worst < 0.5 {
t.Errorf("%s: boundary moved only %.3f km", name, worst)
}
}
compare("failing", failingLunarLimb{}, false, 0)
compare("constant", &recordingLunarLimb{
radiusKM: geodata.EarthEquatorialRadiusKM * solarEclipseIAUSingleRadiusK * localSolarMoonRadiusScale,
}, false, 0.05)
compare("profile", sawtoothLunarLimb{}, true, limbLimitMaxShiftKM)
}
// sawtoothLunarLimb 以位置角 10° 为周期在参考半径上叠加 ±3 km 起伏,模拟月缘山谷。
type sawtoothLunarLimb struct{}
func (sawtoothLunarLimb) LimbRadiusKM(_, _, angle float64) (float64, bool) {
phase := math.Mod(angle, 10) / 10
return geodata.EarthEquatorialRadiusKM*solarEclipseIAUSingleRadiusK*localSolarMoonRadiusScale + 6*math.Abs(phase-0.5) - 1.5, true
}
// TestGlobalSolarEclipseLimbContactMatchesTime 检查月缘修正后的偏食始末接触点与修正时刻同源:
// 接触点必须落在该时刻重解的半影外切点上,而不是沿地球边缘扫描网格上的样点。
func TestGlobalSolarEclipseLimbContactMatchesTime(t *testing.T) {
for _, seed := range []float64{2460409.25, 2460232.25, 2461275.25} {
options := SolarEclipseOptions{RadiusModel: SolarEclipseModelNASABulletinSplitK, LunarLimb: limb.Default()}
result := SolarEclipseWithOptionsWithInstance(seed, options, nil)
if !result.LunarLimbApplied {
t.Fatalf("seed %.2f: the default limb profile changed nothing", seed)
}
solver := newSolarEclipseSolverWithOptions(CalcMoonSHByJDE(seed, 0), options, nil)
for _, pair := range []struct {
name string
jde float64
contact SolarEclipseLimbContact
}{
{"P1", result.PartialBeginOnEarth, result.PartialBeginContact},
{"P4", result.PartialEndOnEarth, result.PartialEndContact},
} {
if !pair.contact.Defined {
t.Errorf("seed %.2f %s: the corrected contact location is missing", seed, pair.name)
continue
}
point, ok := solver.shadowContactPointAt(pair.jde, solarEclipsePenumbralShadow, false)
if !ok {
t.Errorf("seed %.2f %s: no penumbral tangent point at the corrected time", seed, pair.name)
continue
}
longitude := signedAngleDifference(pair.contact.Longitude, point.Longitude) * math.Cos(point.Latitude*rad)
if d := math.Hypot(longitude, pair.contact.Latitude-point.Latitude) * geodata.EarthKMPerDegree; d > 0.05 {
t.Errorf("seed %.2f %s: contact location is %.3f km from the tangent point at its own time", seed, pair.name, d)
}
}
}
}
// TestLimbFlagsIgnoreNoiseWithConstantLimb 检查常数月缘等价于圆月面时,各层月缘标志都不因离散残差误报。
func TestLimbFlagsIgnoreNoiseWithConstantLimb(t *testing.T) {
iau := &recordingLunarLimb{radiusKM: geodata.EarthEquatorialRadiusKM * solarEclipseIAUSingleRadiusK * localSolarMoonRadiusScale}
umbral := &recordingLunarLimb{radiusKM: geodata.EarthEquatorialRadiusKM * solarEclipseUmbralK * localSolarMoonRadiusScale}
for _, seed := range []float64{2460409.25, 2460232.25, 2461275.25} {
global := SolarEclipseWithOptionsWithInstance(seed, SolarEclipseOptions{RadiusModel: SolarEclipseModelIAUSingleK, LunarLimb: iau}, nil)
if global.LunarLimbApplied || global.PartialBeginContact.Defined || global.PartialEndContact.Defined {
t.Errorf("seed %.2f: constant limb reported a global correction", seed)
}
central := SolarEclipseCentralPathNASABulletinSplitKWithInstance(seed, SolarEclipsePathOptions{LunarLimb: umbral}, nil)
if central.CentralLimitsLimbApplied {
t.Errorf("seed %.2f: constant limb reported moved central limits", seed)
}
local := LocalSolarEclipseWithOptionsWithInstance(seed, -96.8, 32.8, 0, SolarEclipseOptions{RadiusModel: SolarEclipseModelIAUSingleK, LunarLimb: iau}, nil)
if local.LunarLimbApplied {
t.Errorf("seed %.2f: constant limb reported a local correction", seed)
}
}
real := SolarEclipseCentralPathNASABulletinSplitKWithInstance(2460409.25, SolarEclipsePathOptions{LunarLimb: limb.Default()}, nil)
if !real.CentralLimitsLimbApplied {
t.Error("default limb profile must move the central limits")
}
}
// TestLocalSolarEclipseLimbFlagCoversMagnitude 检查只改变食分、遮蔽率而不改变接触时刻时站心标志仍为真。
func TestLocalSolarEclipseLimbFlagCoversMagnitude(t *testing.T) {
circular := LocalSolarEclipseResult{Type: SolarEclipsePartial, PartialStart: 1, PartialEnd: 2, Magnitude: 0.5, Obscuration: 0.4}
limbed := circular
if localSolarEclipseLimbChanged(limbed, circular) {
t.Fatal("identical results reported a change")
}
limbed.Magnitude += 1e-12
if localSolarEclipseLimbChanged(limbed, circular) {
t.Error("floating-point noise in magnitude reported a change")
}
limbed.Magnitude = circular.Magnitude + 1e-6
if !localSolarEclipseLimbChanged(limbed, circular) {
t.Error("a visible magnitude change was not reported")
}
limbed = circular
limbed.Obscuration += 1e-6
if !localSolarEclipseLimbChanged(limbed, circular) {
t.Error("a visible obscuration change was not reported")
}
limbed = circular
limbed.Type = SolarEclipseAnnular
if !localSolarEclipseLimbChanged(limbed, circular) {
t.Error("a type change was not reported")
}
}
// steppedLunarLimb 报告指定网格步长的常半径剖面。
type steppedLunarLimb struct{ step float64 }
func (limb steppedLunarLimb) LimbRadiusKM(lon, lat, angle float64) (float64, bool) {
return 1737.4, true
}
func (limb steppedLunarLimb) RadiusRangeKM() (float64, float64) { return 1737.4, 1737.4 }
func (limb steppedLunarLimb) AngleStepDeg() float64 { return limb.step }
func TestLunarLimbFrameRejectsTinyStep(t *testing.T) {
_, moon := solarEclipseSunMoonEquatorial(2460409.25)
moonXYZ := solarEclipseLLRToXYZ(moon[0], moon[1], moon[2])
for _, c := range []struct{ step, want float64 }{
{1e-300, lunarLimbDefaultStepDeg}, {1e-4, lunarLimbDefaultStepDeg}, {0.001, 0.001}, {0.5, 0.5}, {2, lunarLimbDefaultStepDeg},
} {
frame := newLunarLimbFrame(steppedLunarLimb{c.step}, 2460409.25, moonXYZ)
if !frame.valid || frame.stepDeg != c.want {
t.Errorf("step %g: got %g valid=%v, want %g", c.step, frame.stepDeg, frame.valid, c.want)
}
}
}