feat: 扩展天文计算能力

- 新增日食、月食、本地可见性、中心线、半影区域、SVG 图示与沙罗周期信息
- 新增行星冲合、留、方照、物理星历、视直径、相位、亮肢角、轨道节点等计算
- 新增木星伽利略卫星位置、现象与接触事件计算
- 新增恒星星表、星座判定、自行修正与观测辅助能力
- 新增 coord、formula、orbit、sundial、lite/sun、lite/moon 等扩展包
- 完善农历年号、月相英文别名、视差角、大气质量、折射、日晷与双星计算
- 增加 NASA、JPL Horizons、IMCCE 等回归测试数据与基线测试
- 重构基础算法文件组织,补充大量公开 API 注释和语义回归测试
- 更新中文和英文 README,补充示例、精度说明、SVG 配图
This commit is contained in:
2026-05-01 22:38:44 +08:00
parent 98ff574495
commit 3ffdbe0034
365 changed files with 63589 additions and 17508 deletions
+60
View File
@@ -0,0 +1,60 @@
package formula
import "math"
// AirmassPlaneParallel 平行平板大气模型 / plane-parallel airmass.
//
// altitude 为真高度角,单位度。该模型等价于 sec(z),其中 z 为天顶距。
// 中高空可作几何近似;接近地平线时会发散,不宜用于低空精细估算。
// altitude is true altitude in degrees. The model is sec(z) with zenith
// distance z = 90° - altitude. It is a geometric approximation that diverges
// near the horizon.
func AirmassPlaneParallel(altitude float64) float64 {
if !isFinitePhotometry(altitude) || altitude < 0 || altitude > 90 {
return math.NaN()
}
if altitude == 0 {
return math.Inf(1)
}
return 1 / math.Sin(altitude*math.Pi/180)
}
// AirmassPlaneParallelByZenithDistance 按天顶距的平行平板大气质量 / plane-parallel airmass by zenith distance.
func AirmassPlaneParallelByZenithDistance(zenithDistance float64) float64 {
if !isFinitePhotometry(zenithDistance) || zenithDistance < 0 || zenithDistance > 90 {
return math.NaN()
}
if zenithDistance == 90 {
return math.Inf(1)
}
return 1 / math.Cos(zenithDistance*math.Pi/180)
}
// AirmassKastenYoung Kasten-Young 1989 大气质量模型 / Kasten-Young 1989 airmass.
//
// apparentAltitude 为视高度角,单位度。该经验公式在低空通常比 sec(z) 更稳健。
// apparentAltitude is apparent altitude in degrees. This empirical model is
// generally more robust than sec(z) at low altitude.
func AirmassKastenYoung(apparentAltitude float64) float64 {
if !isFinitePhotometry(apparentAltitude) || apparentAltitude < 0 || apparentAltitude > 90 {
return math.NaN()
}
return 1 / (math.Sin(apparentAltitude*math.Pi/180) + 0.50572*math.Pow(apparentAltitude+6.07995, -1.6364))
}
// AirmassPickering Pickering 2002 大气质量模型 / Pickering 2002 airmass.
//
// apparentAltitude 为视高度角,单位度。该经验公式专门面向低空观测修正。
// apparentAltitude is apparent altitude in degrees. This empirical model is
// intended for low-altitude observational use.
func AirmassPickering(apparentAltitude float64) float64 {
if !isFinitePhotometry(apparentAltitude) || apparentAltitude < 0 || apparentAltitude > 90 {
return math.NaN()
}
correctedAltitude := apparentAltitude + 244/(165+47*math.Pow(apparentAltitude, 1.1))
return 1 / math.Sin(correctedAltitude*math.Pi/180)
}
func isFinitePhotometry(value float64) bool {
return !math.IsNaN(value) && !math.IsInf(value, 0)
}
+43
View File
@@ -0,0 +1,43 @@
package formula
import (
"math"
"testing"
)
func TestAirmassModels(t *testing.T) {
assertFormulaClose(t, "AirmassPlaneParallel(90)", AirmassPlaneParallel(90), 1, 1e-15)
assertFormulaClose(t, "AirmassPlaneParallel(30)", AirmassPlaneParallel(30), 2, 1e-15)
if !math.IsInf(AirmassPlaneParallel(0), 1) {
t.Fatal("expected plane-parallel airmass at horizon to be +Inf")
}
assertFormulaClose(t, "AirmassPlaneParallelByZenithDistance(0)", AirmassPlaneParallelByZenithDistance(0), 1, 1e-15)
assertFormulaClose(t, "AirmassPlaneParallelByZenithDistance(60)", AirmassPlaneParallelByZenithDistance(60), 2, 1e-12)
if !math.IsInf(AirmassPlaneParallelByZenithDistance(90), 1) {
t.Fatal("expected sec(z) at z=90 to be +Inf")
}
assertFormulaClose(t, "AirmassKastenYoung(90)", AirmassKastenYoung(90), 0.9997119918558381, 1e-15)
assertFormulaClose(t, "AirmassKastenYoung(30)", AirmassKastenYoung(30), 1.9942928525292503, 1e-15)
assertFormulaClose(t, "AirmassKastenYoung(0)", AirmassKastenYoung(0), 37.91960837783633, 1e-12)
assertFormulaClose(t, "AirmassPickering(90)", AirmassPickering(90), 1.000000196171337, 1e-15)
assertFormulaClose(t, "AirmassPickering(30)", AirmassPickering(30), 1.9931538464145713, 1e-15)
assertFormulaClose(t, "AirmassPickering(0)", AirmassPickering(0), 38.749398755780355, 1e-12)
}
func TestAirmassInvalidInput(t *testing.T) {
for name, value := range map[string]float64{
"plane-parallel negative altitude": AirmassPlaneParallel(-1),
"plane-parallel >90 altitude": AirmassPlaneParallel(91),
"plane-parallel zenith <0": AirmassPlaneParallelByZenithDistance(-1),
"plane-parallel zenith >90": AirmassPlaneParallelByZenithDistance(91),
"kasten-young negative altitude": AirmassKastenYoung(-1),
"pickering >90 altitude": AirmassPickering(91),
} {
if !math.IsNaN(value) {
t.Fatalf("%s should be NaN, got %.15f", name, value)
}
}
}
+67
View File
@@ -0,0 +1,67 @@
// Package formula 提供与具体时刻、星历表无关的研究型天文公式。
package formula
import "math"
const (
planckConstant = 6.62607015e-34
speedOfLight = 299792458.0
boltzmannConstant = 1.380649e-23
stefanBoltzmannConstant = 5.670374419e-8
wienDisplacementConstant = 2.897771955e-3
)
// WienPeakWavelength 维恩峰值波长 / Wien peak wavelength.
//
// temperatureK: 黑体温度,单位开尔文
//
// 返回:
//
// 峰值波长,单位米
func WienPeakWavelength(temperatureK float64) float64 {
if temperatureK <= 0 || math.IsNaN(temperatureK) || math.IsInf(temperatureK, 0) {
return math.NaN()
}
return wienDisplacementConstant / temperatureK
}
// StefanBoltzmannFlux 斯特藩-玻尔兹曼通量 / Stefan-Boltzmann flux.
//
// temperatureK: 黑体温度,单位开尔文
//
// 返回:
//
// 单位面积总出射度,单位 W/m^2
func StefanBoltzmannFlux(temperatureK float64) float64 {
if temperatureK < 0 || math.IsNaN(temperatureK) || math.IsInf(temperatureK, 0) {
return math.NaN()
}
return stefanBoltzmannConstant * math.Pow(temperatureK, 4)
}
// PlanckRadianceByWavelength 按波长的普朗克谱辐亮度 / Planck spectral radiance by wavelength.
//
// wavelengthM: 波长,单位米
// temperatureK: 黑体温度,单位开尔文
//
// 返回:
//
// 谱辐亮度,单位 W·sr^-1·m^-3
//
// 例:
//
// b := formula.PlanckRadianceByWavelength(500e-9, 5772)
func PlanckRadianceByWavelength(wavelengthM, temperatureK float64) float64 {
if wavelengthM <= 0 || temperatureK <= 0 ||
math.IsNaN(wavelengthM) || math.IsInf(wavelengthM, 0) ||
math.IsNaN(temperatureK) || math.IsInf(temperatureK, 0) {
return math.NaN()
}
exponent := planckConstant * speedOfLight / (wavelengthM * boltzmannConstant * temperatureK)
denominator := math.Expm1(exponent)
if denominator == 0 {
return math.Inf(1)
}
return 2 * planckConstant * speedOfLight * speedOfLight / math.Pow(wavelengthM, 5) / denominator
}
+63
View File
@@ -0,0 +1,63 @@
package formula
import (
"math"
"testing"
)
func assertFormulaClose(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 TestBlackbodyFormulas(t *testing.T) {
assertFormulaClose(t, "WienPeakWavelength", WienPeakWavelength(5772), 5.020394931568954e-07, 1e-15)
assertFormulaClose(t, "StefanBoltzmannFlux", StefanBoltzmannFlux(5772), 6.293859246828887e+07, 1e-6)
assertFormulaClose(t, "PlanckRadianceByWavelength", PlanckRadianceByWavelength(500e-9, 5772), 2.6238540568595848e+13, 1e-1)
if !math.IsNaN(WienPeakWavelength(0)) {
t.Fatal("expected WienPeakWavelength(0) to be NaN")
}
}
func TestPhotometryFormulas(t *testing.T) {
assertFormulaClose(t, "DistanceModulus(10pc)", DistanceModulus(10), 0, 1e-15)
assertFormulaClose(t, "DistanceModulus(100pc)", DistanceModulus(100), 5, 1e-15)
assertFormulaClose(t, "ApparentMagnitudeFromAbsolute", ApparentMagnitudeFromAbsolute(4.83, 100), 9.83, 1e-15)
assertFormulaClose(t, "AbsoluteMagnitudeFromApparent", AbsoluteMagnitudeFromApparent(9.83, 100), 4.83, 1e-12)
}
func TestStellarParameterFormulas(t *testing.T) {
luminosity := LuminosityFromRadiusTemperature(2.5*solarRadiusM, 9000)
radius := RadiusFromLuminosityTemperature(luminosity, 9000)
temperature := EffectiveTemperatureFromLuminosityRadius(luminosity, 2.5*solarRadiusM)
assertFormulaClose(t, "RadiusFromLuminosityTemperature", radius, 2.5*solarRadiusM, 1e-4)
assertFormulaClose(t, "EffectiveTemperatureFromLuminosityRadius", temperature, 9000, 1e-9)
luminositySolar := LuminositySolarFromRadiusTemperature(2.5, 9000)
radiusSolar := RadiusSolarFromLuminosityTemperature(luminositySolar, 9000)
temperatureSolar := EffectiveTemperatureFromLuminositySolarRadius(luminositySolar, 2.5)
assertFormulaClose(t, "RadiusSolarFromLuminosityTemperature", radiusSolar, 2.5, 1e-12)
assertFormulaClose(t, "EffectiveTemperatureFromLuminositySolarRadius", temperatureSolar, 9000, 1e-9)
assertFormulaClose(t, "SolarEffectiveTemperature", SolarEffectiveTemperature(), 5772, 1e-12)
}
func TestSynodicPeriod(t *testing.T) {
assertFormulaClose(t, "Earth-Venus synodic period", SynodicPeriod(365.25636, 224.70069), 583.9206352820089, 1e-9)
if !math.IsInf(SynodicPeriod(365.25636, 365.25636), 1) {
t.Fatal("expected equal periods to yield +Inf synodic period")
}
}
func TestTelescopeFormulas(t *testing.T) {
assertFormulaClose(t, "LightGatheringPowerRatio", LightGatheringPowerRatio(200, 100), 4, 1e-15)
assertFormulaClose(t, "DawesLimitArcsec", DawesLimitArcsec(100), 1.16, 1e-15)
assertFormulaClose(t, "RayleighLimitArcsec", RayleighLimitArcsec(100), 1.384, 1e-15)
assertFormulaClose(t, "LimitingMagnitudeEmpirical", LimitingMagnitudeEmpirical(70, 6), 11, 1e-15)
if !math.IsNaN(LightGatheringPowerRatio(0, 100)) {
t.Fatal("expected invalid aperture to produce NaN")
}
}
+29
View File
@@ -0,0 +1,29 @@
package formula
import "math"
// SynodicPeriod 会合周期 / synodic period.
//
// period1: 第一个天体的恒星周期或朔望周期,单位自定,但必须与 period2 一致
// period2: 第二个天体的周期,单位需与 period1 一致
//
// 返回:
//
// 会合周期,单位与输入相同
//
// 例:
//
// // 地球与金星的会合周期,单位天
// s := formula.SynodicPeriod(365.25636, 224.70069)
func SynodicPeriod(period1, period2 float64) float64 {
if period1 <= 0 || period2 <= 0 ||
math.IsNaN(period1) || math.IsInf(period1, 0) ||
math.IsNaN(period2) || math.IsInf(period2, 0) {
return math.NaN()
}
frequencyDiff := math.Abs(1/period1 - 1/period2)
if frequencyDiff == 0 {
return math.Inf(1)
}
return 1 / frequencyDiff
}
+49
View File
@@ -0,0 +1,49 @@
package formula
import "math"
// DistanceModulus 距离模数 / distance modulus.
//
// distanceParsec: 天体距离,单位秒差距 pc
//
// 返回:
//
// 距离模数 m-M
func DistanceModulus(distanceParsec float64) float64 {
if distanceParsec <= 0 || math.IsNaN(distanceParsec) || math.IsInf(distanceParsec, 0) {
return math.NaN()
}
return 5 * math.Log10(distanceParsec/10)
}
// ApparentMagnitudeFromAbsolute 由绝对星等求视星等 / apparent magnitude from absolute magnitude.
//
// absoluteMagnitude: 绝对星等 M
// distanceParsec: 天体距离,单位秒差距 pc
//
// 返回:
//
// 视星等 m
func ApparentMagnitudeFromAbsolute(absoluteMagnitude, distanceParsec float64) float64 {
modulus := DistanceModulus(distanceParsec)
if math.IsNaN(modulus) {
return math.NaN()
}
return absoluteMagnitude + modulus
}
// AbsoluteMagnitudeFromApparent 由视星等求绝对星等 / absolute magnitude from apparent magnitude.
//
// apparentMagnitude: 视星等 m
// distanceParsec: 天体距离,单位秒差距 pc
//
// 返回:
//
// 绝对星等 M
func AbsoluteMagnitudeFromApparent(apparentMagnitude, distanceParsec float64) float64 {
modulus := DistanceModulus(distanceParsec)
if math.IsNaN(modulus) {
return math.NaN()
}
return apparentMagnitude - modulus
}
+133
View File
@@ -0,0 +1,133 @@
package formula
import "math"
const (
solarLuminosityW = 3.828e26
solarRadiusM = 6.957e8
solarEffectiveTempK = 5772.0
)
// LuminosityFromRadiusTemperature 由半径和温度求光度 / luminosity from radius and temperature.
//
// radiusM: 恒星半径,单位米
// temperatureK: 恒星有效温度,单位开尔文
//
// 返回:
//
// 总光度,单位瓦特
func LuminosityFromRadiusTemperature(radiusM, temperatureK float64) float64 {
if radiusM <= 0 || temperatureK <= 0 ||
math.IsNaN(radiusM) || math.IsInf(radiusM, 0) ||
math.IsNaN(temperatureK) || math.IsInf(temperatureK, 0) {
return math.NaN()
}
return 4 * math.Pi * radiusM * radiusM * StefanBoltzmannFlux(temperatureK)
}
// RadiusFromLuminosityTemperature 由光度和温度求半径 / radius from luminosity and temperature.
//
// luminosityW: 恒星总光度,单位瓦特
// temperatureK: 恒星有效温度,单位开尔文
//
// 返回:
//
// 恒星半径,单位米
func RadiusFromLuminosityTemperature(luminosityW, temperatureK float64) float64 {
if luminosityW <= 0 || temperatureK <= 0 ||
math.IsNaN(luminosityW) || math.IsInf(luminosityW, 0) ||
math.IsNaN(temperatureK) || math.IsInf(temperatureK, 0) {
return math.NaN()
}
denominator := 4 * math.Pi * StefanBoltzmannFlux(temperatureK)
if denominator == 0 {
return math.NaN()
}
return math.Sqrt(luminosityW / denominator)
}
// EffectiveTemperatureFromLuminosityRadius 由光度和半径求温度 / effective temperature from luminosity and radius.
//
// luminosityW: 恒星总光度,单位瓦特
// radiusM: 恒星半径,单位米
//
// 返回:
//
// 恒星有效温度,单位开尔文
func EffectiveTemperatureFromLuminosityRadius(luminosityW, radiusM float64) float64 {
if luminosityW <= 0 || radiusM <= 0 ||
math.IsNaN(luminosityW) || math.IsInf(luminosityW, 0) ||
math.IsNaN(radiusM) || math.IsInf(radiusM, 0) {
return math.NaN()
}
denominator := 4 * math.Pi * radiusM * radiusM * stefanBoltzmannConstant
if denominator == 0 {
return math.NaN()
}
return math.Pow(luminosityW/denominator, 0.25)
}
// LuminositySolarFromRadiusTemperature 由太阳半径单位和温度求光度 / luminosity in solar units from radius and temperature.
//
// radiusSolar: 恒星半径,单位为太阳半径
// temperatureK: 恒星有效温度,单位开尔文
//
// 返回:
//
// 总光度,单位为太阳光度 L☉
func LuminositySolarFromRadiusTemperature(radiusSolar, temperatureK float64) float64 {
if radiusSolar <= 0 || temperatureK <= 0 ||
math.IsNaN(radiusSolar) || math.IsInf(radiusSolar, 0) ||
math.IsNaN(temperatureK) || math.IsInf(temperatureK, 0) {
return math.NaN()
}
return LuminosityFromRadiusTemperature(radiusSolar*solarRadiusM, temperatureK) / solarLuminosityW
}
// RadiusSolarFromLuminosityTemperature 由太阳光度单位和温度求半径 / radius in solar units from luminosity and temperature.
//
// luminositySolar: 恒星总光度,单位为太阳光度 L☉
// temperatureK: 恒星有效温度,单位开尔文
//
// 返回:
//
// 恒星半径,单位为太阳半径 R☉
func RadiusSolarFromLuminosityTemperature(luminositySolar, temperatureK float64) float64 {
if luminositySolar <= 0 || temperatureK <= 0 ||
math.IsNaN(luminositySolar) || math.IsInf(luminositySolar, 0) ||
math.IsNaN(temperatureK) || math.IsInf(temperatureK, 0) {
return math.NaN()
}
return RadiusFromLuminosityTemperature(luminositySolar*solarLuminosityW, temperatureK) / solarRadiusM
}
// EffectiveTemperatureFromLuminositySolarRadius 由太阳光度和半径单位求温度 / effective temperature from solar luminosity and radius.
//
// luminositySolar: 恒星总光度,单位为太阳光度 L☉
// radiusSolar: 恒星半径,单位为太阳半径 R☉
//
// 返回:
//
// 恒星有效温度,单位开尔文
//
// 例:
//
// // 半径 2.5 R☉、光度 20 L☉ 的主序星
// t := formula.EffectiveTemperatureFromLuminositySolarRadius(20, 2.5)
func EffectiveTemperatureFromLuminositySolarRadius(luminositySolar, radiusSolar float64) float64 {
if luminositySolar <= 0 || radiusSolar <= 0 ||
math.IsNaN(luminositySolar) || math.IsInf(luminositySolar, 0) ||
math.IsNaN(radiusSolar) || math.IsInf(radiusSolar, 0) {
return math.NaN()
}
return EffectiveTemperatureFromLuminosityRadius(luminositySolar*solarLuminosityW, radiusSolar*solarRadiusM)
}
// SolarEffectiveTemperature 太阳有效温度常数 / solar effective temperature constant.
//
// 返回:
//
// 太阳有效温度,单位开尔文
func SolarEffectiveTemperature() float64 {
return solarEffectiveTempK
}
+72
View File
@@ -0,0 +1,72 @@
package formula
import "math"
const darkAdaptedPupilDiameterMM = 7.0
// LightGatheringPowerRatio 集光力比值 / light-gathering power ratio.
//
// diameter1MM: 第一个望远镜口径,单位毫米
// diameter2MM: 第二个望远镜口径,单位毫米
//
// 返回:
//
// 集光力比值,等于 (diameter1MM / diameter2MM)^2
func LightGatheringPowerRatio(diameter1MM, diameter2MM float64) float64 {
if diameter1MM <= 0 || diameter2MM <= 0 ||
math.IsNaN(diameter1MM) || math.IsInf(diameter1MM, 0) ||
math.IsNaN(diameter2MM) || math.IsInf(diameter2MM, 0) {
return math.NaN()
}
return math.Pow(diameter1MM/diameter2MM, 2)
}
// DawesLimitArcsec Dawes 极限 / Dawes limit in arcseconds.
//
// diameterMM: 望远镜口径,单位毫米
//
// 返回:
//
// Dawes 极限,单位角秒
func DawesLimitArcsec(diameterMM float64) float64 {
if diameterMM <= 0 || math.IsNaN(diameterMM) || math.IsInf(diameterMM, 0) {
return math.NaN()
}
return 116 / diameterMM
}
// RayleighLimitArcsec Rayleigh 极限 / Rayleigh limit in arcseconds.
//
// diameterMM: 望远镜口径,单位毫米
//
// 返回:
//
// Rayleigh 极限,单位角秒
func RayleighLimitArcsec(diameterMM float64) float64 {
if diameterMM <= 0 || math.IsNaN(diameterMM) || math.IsInf(diameterMM, 0) {
return math.NaN()
}
return 138.4 / diameterMM
}
// LimitingMagnitudeEmpirical 经验极限星等 / empirical limiting magnitude.
//
// diameterMM: 望远镜有效口径,单位毫米
// nakedEyeLimit: 观测地裸眼极限星等,例如乡村暗空可近似取 6
//
// 返回:
//
// 经验极限星等;这是经验值,不包含天空背景、倍率、透过率和观测经验修正
//
// 例:
//
// // 70mm 小型折射镜,裸眼极限 6 等
// mag := formula.LimitingMagnitudeEmpirical(70, 6)
func LimitingMagnitudeEmpirical(diameterMM, nakedEyeLimit float64) float64 {
if diameterMM <= 0 ||
math.IsNaN(diameterMM) || math.IsInf(diameterMM, 0) ||
math.IsNaN(nakedEyeLimit) || math.IsInf(nakedEyeLimit, 0) {
return math.NaN()
}
return nakedEyeLimit + 5*math.Log10(diameterMM/darkAdaptedPupilDiameterMM)
}