feat: 引入可切换星历模型并扩展天象观测链路
- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算 - 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询 - 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出 - 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解 - 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤 - 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持 - 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理 - 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
本包内嵌的月缘剖面数据不适用本仓库的 Apache-2.0,遵循其来源条款。完整声明见仓库根目录 /NOTICE(第 1 节)。
|
||||
The lunar limb profile embedded in this package is NOT covered by this repository's Apache-2.0 licence; it follows the terms of its source. The full notice is section 1 of /NOTICE in the repository root.
|
||||
|
||||
SELENE (KAGUYA) lunar limb profile — kaguya_lalt.bin
|
||||
Source data: SELENE Global Grid Topographic Data of the Moon (LALT_GGT_NUM),
|
||||
dataset sln-l-lalt-5-topo-ggt-num-v2.0, version 2.0, published 2017.
|
||||
Origin of the data: ISAS/JAXA.
|
||||
Provided by the SELENE (KAGUYA) Data Archive (DARTS), ISAS/JAXA:
|
||||
https://darts.isas.jaxa.jp/datasets/darts:sln-l-lalt-5-topo-ggt-num-v2.0
|
||||
|
||||
Acknowledgement, as requested by the SELENE Data Archive:
|
||||
"We thank the SELENE(KAGUYA) LALT team and the SELENE Data Archive for
|
||||
providing the SELENE(KAGUYA) data. SELENE is a Japanese mission developed
|
||||
and operated by JAXA."
|
||||
|
||||
Modifications: the original PDS3 fixed-width table (1/16 degree global grid,
|
||||
2880 x 5760 samples) was converted to a float32 elevation grid and resampled
|
||||
into a limb-radius profile table on a 1 degree grid of selenographic
|
||||
(libration) longitude and latitude and a 0.1 degree grid of position angle,
|
||||
taking max (R + h) cos(alpha) over a +/-9 degree arc per direction; the
|
||||
result is stored in this project's own zlib-compressed LLB1 container.
|
||||
|
||||
Licence: ISAS Data Policy,
|
||||
https://www.isas.jaxa.jp/en/researchers/data-policy/
|
||||
which states that its rules are compatible with Creative Commons Attribution
|
||||
4.0 International, https://creativecommons.org/licenses/by/4.0/
|
||||
Provided "as is", without warranties of any kind. ISAS/JAXA is not
|
||||
responsible for the actions of data users and does not endorse this project.
|
||||
Binary file not shown.
@@ -0,0 +1,37 @@
|
||||
// Package limb provides the embedded Kaguya LALT lunar limb profile.
|
||||
package limb
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"sync"
|
||||
|
||||
"b612.me/astro/moon/limbtable"
|
||||
)
|
||||
|
||||
//go:embed kaguya_lalt.bin
|
||||
var kaguyaTable []byte
|
||||
|
||||
// Profile is the decoded Kaguya lunar limb profile.
|
||||
type Profile = limbtable.Profile
|
||||
|
||||
var (
|
||||
defaultOnce sync.Once
|
||||
defaultProfile *Profile
|
||||
)
|
||||
|
||||
// Default returns the embedded Kaguya LALT profile, decoded on first use.
|
||||
func Default() *Profile {
|
||||
defaultOnce.Do(func() {
|
||||
profile, err := Parse(kaguyaTable)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defaultProfile = profile
|
||||
})
|
||||
return defaultProfile
|
||||
}
|
||||
|
||||
// Parse decodes an LLB1 profile table.
|
||||
func Parse(data []byte) (*Profile, error) { return limbtable.Parse(data) }
|
||||
|
||||
const headerSize = limbtable.HeaderSize
|
||||
@@ -0,0 +1,110 @@
|
||||
package limb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/zlib"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 内置表的 sha256;换表时须同步更新。
|
||||
const kaguyaTableSHA256 = "7d49dc75497c404b78b5b259129d0f5ab17b56522128478591ea33c8cb97d072"
|
||||
|
||||
func TestEmbeddedTableChecksum(t *testing.T) {
|
||||
sum := sha256.Sum256(kaguyaTable)
|
||||
if got := hex.EncodeToString(sum[:]); got != kaguyaTableSHA256 {
|
||||
t.Fatalf("embedded table sha256 = %s, want %s", got, kaguyaTableSHA256)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLimbRadiusMatchesGenerator(t *testing.T) {
|
||||
cases := []struct{ lon, lat, angle, radiusKM float64 }{
|
||||
{0, 0, 0, 1737.432000000},
|
||||
{3.3, -2.7, 123.45, 1735.688760000},
|
||||
{-8.6, 7.9, 359.97, 1738.466256000},
|
||||
{5.5, 1.25, 270.05, 1738.604000000},
|
||||
{-1.0, -6.0, 45.0, 1737.796000000},
|
||||
}
|
||||
profile := Default()
|
||||
for _, c := range cases {
|
||||
got, ok := profile.LimbRadiusKM(c.lon, c.lat, c.angle)
|
||||
if !ok {
|
||||
t.Fatalf("LimbRadiusKM(%v, %v, %v) not ok", c.lon, c.lat, c.angle)
|
||||
}
|
||||
if math.Abs(got-c.radiusKM) > 1e-6 {
|
||||
t.Errorf("LimbRadiusKM(%v, %v, %v) = %.9f, want %.9f", c.lon, c.lat, c.angle, got, c.radiusKM)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLimbRadiusWrapsAndClamps(t *testing.T) {
|
||||
profile := Default()
|
||||
base, _ := profile.LimbRadiusKM(2, 3, 10)
|
||||
for _, angle := range []float64{370, -350} {
|
||||
if got, _ := profile.LimbRadiusKM(2, 3, angle); math.Abs(got-base) > 1e-9 {
|
||||
t.Errorf("angle %v = %.9f, want %.9f", angle, got, base)
|
||||
}
|
||||
}
|
||||
edge, _ := profile.LimbRadiusKM(9, -8, 10)
|
||||
if got, _ := profile.LimbRadiusKM(12, -11, 10); got != edge {
|
||||
t.Errorf("out-of-range libration = %.9f, want clamped %.9f", got, edge)
|
||||
}
|
||||
if _, ok := profile.LimbRadiusKM(math.NaN(), 0, 0); ok {
|
||||
t.Error("NaN libration must fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRejectsBadInput(t *testing.T) {
|
||||
if _, err := Parse([]byte("nope")); err == nil {
|
||||
t.Error("short input must fail")
|
||||
}
|
||||
broken := append([]byte(nil), kaguyaTable[:headerSize+16]...)
|
||||
if _, err := Parse(broken); err == nil {
|
||||
t.Error("truncated payload must fail")
|
||||
}
|
||||
huge := append([]byte(nil), kaguyaTable...)
|
||||
binary.LittleEndian.PutUint32(huge[4:], 1<<30)
|
||||
binary.LittleEndian.PutUint32(huge[8:], 1<<30)
|
||||
if _, err := Parse(huge); err == nil {
|
||||
t.Error("oversized header must fail before allocating")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRejectsNonFiniteHeader(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
angleStep, ref float64
|
||||
}{
|
||||
{name: "NaN angle step", angleStep: math.NaN(), ref: 1737.4},
|
||||
{name: "Inf angle step", angleStep: math.Inf(1), ref: 1737.4},
|
||||
{name: "NaN reference radius", angleStep: 180, ref: math.NaN()},
|
||||
{name: "zero reference radius", angleStep: 180, ref: 0},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if _, err := Parse(syntheticTable(c.angleStep, c.ref)); err == nil {
|
||||
t.Errorf("%s: Parse err = nil, want an error", c.name)
|
||||
}
|
||||
}
|
||||
if _, err := Parse(syntheticTable(180, 1737.4)); err != nil {
|
||||
t.Fatalf("valid header: Parse err = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// syntheticTable 最小合法 2×2×2 LLB1 表,仅表头两项可变。
|
||||
func syntheticTable(angleStep, referenceRadiusKM float64) []byte {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString("LLB1")
|
||||
for _, count := range []uint32{2, 2, 2} {
|
||||
binary.Write(&buf, binary.LittleEndian, count)
|
||||
}
|
||||
for _, value := range []float64{0, 1, 0, 1, angleStep, referenceRadiusKM, 1} {
|
||||
binary.Write(&buf, binary.LittleEndian, math.Float64bits(value))
|
||||
}
|
||||
writer := zlib.NewWriter(&buf)
|
||||
writer.Write(make([]byte, 2*8))
|
||||
writer.Close()
|
||||
return buf.Bytes()
|
||||
}
|
||||
Reference in New Issue
Block a user