// Package ephemeris computes tropical ecliptic longitudes. // Default backend: Swiss Ephemeris Moshier (no external ephe files). // NOTE: go-swisseph is AGPL-3.0; commercial deployment needs Astrodienst SE license or AGPL compliance. package ephemeris import ( "fmt" "math" "os" "sync" "time" swe "github.com/tejzpr/go-swisseph" ) // Body keys match natal chart keys. const ( BodySun = "sun" BodyMoon = "moon" BodyMercury = "mercury" BodyVenus = "venus" BodyMars = "mars" BodyJupiter = "jupiter" BodySaturn = "saturn" BodyUranus = "uranus" BodyNeptune = "neptune" BodyPluto = "pluto" ) var bodyID = map[string]int32{ BodySun: swe.Sun, BodyMoon: swe.Moon, BodyMercury: swe.Mercury, BodyVenus: swe.Venus, BodyMars: swe.Mars, BodyJupiter: swe.Jupiter, BodySaturn: swe.Saturn, BodyUranus: swe.Uranus, BodyNeptune: swe.Neptune, BodyPluto: swe.Pluto, } // PlanetOrder is the standard body sequence (excluding ASC). var PlanetOrder = []string{ BodySun, BodyMoon, BodyMercury, BodyVenus, BodyMars, BodyJupiter, BodySaturn, BodyUranus, BodyNeptune, BodyPluto, } var ( once sync.Once iflag int32 = swe.FlagMoseph epheOK bool ) func initSE() { once.Do(func() { path := os.Getenv("SE_EPHE_PATH") if path == "" { path = os.Getenv("EPHEMERIS_PATH") } if path != "" { swe.SetEphePath(path) iflag = swe.FlagSwieph epheOK = true } else { // Moshier: no files required iflag = swe.FlagMoseph epheOK = true } }) } // Backend returns "moshier" or "swiss" for diagnostics. func Backend() string { initSE() if iflag == swe.FlagSwieph { return "swiss" } return "moshier" } // JulianDayUT converts a UTC instant to Julian Day (UT). func JulianDayUT(utc time.Time) float64 { initSE() utc = utc.UTC() sec := float64(utc.Second()) + float64(utc.Nanosecond())/1e9 dret, err := swe.UtcToJd( int32(utc.Year()), int32(utc.Month()), int32(utc.Day()), int32(utc.Hour()), int32(utc.Minute()), sec, swe.GregCal, ) if err != nil { // Fallback Meeus-style JD return julianDayFallback(utc) } return dret[1] // UT } // PlanetLon returns tropical ecliptic longitude in degrees [0,360). func PlanetLon(jdUt float64, body string) (float64, error) { initSE() ipl, ok := bodyID[body] if !ok { return 0, fmt.Errorf("unknown body %q", body) } r := swe.CalcUT(jdUt, ipl, iflag) if r.Flag < 0 { return 0, fmt.Errorf("swe calc %s: %s", body, r.Error) } return norm360(r.Data[0]), nil } // Ascendant returns tropical ASC longitude degrees using Placidus cusps (Points[0]). // Callers apply Whole Sign houses from this ASC. func Ascendant(jdUt, lat, lng float64) (float64, error) { initSE() h := swe.Houses(jdUt, lat, lng, 'P') if h.Flag < 0 || len(h.Points) < 1 { return 0, fmt.Errorf("swe houses failed flag=%d", h.Flag) } return norm360(h.Points[0]), nil } // AllPlanetLons returns longitudes for PlanetOrder. func AllPlanetLons(jdUt float64) (map[string]float64, error) { out := make(map[string]float64, len(PlanetOrder)) for _, k := range PlanetOrder { lon, err := PlanetLon(jdUt, k) if err != nil { return nil, err } out[k] = lon } return out, nil } func norm360(x float64) float64 { x = math.Mod(x, 360) if x < 0 { x += 360 } return x } func julianDayFallback(t time.Time) float64 { y := t.Year() m := int(t.Month()) d := float64(t.Day()) + float64(t.Hour())/24 + float64(t.Minute())/1440 + float64(t.Second())/86400 if m <= 2 { y-- m += 12 } A := y / 100 B := 2 - A + A/4 return math.Floor(365.25*float64(y+4716)) + math.Floor(30.6001*float64(m+1)) + d + float64(B) - 1524.5 } // Ready reports whether ephemeris init succeeded (always true for Moshier). func Ready() bool { initSE() return epheOK }