Skip to main content

astroceleste_engine/
fixed_stars.rs

1//! Fixed stars conjunct chart points (`charts/calc/fixed_stars.py`).
2
3use serde::Serialize;
4
5use crate::aspects::{Aspect, Point};
6use crate::catalog::FIXED_STARS;
7use crate::pyfloat;
8use crate::symbolic::symbolic_degree_number;
9use crate::zodiac::{determine_house, longitude_to_zodiac, PRECESSION_RATE_ARCSEC_YEAR};
10
11/// A fixed star conjunct a chart point.
12#[derive(Debug, Clone, PartialEq, Serialize)]
13pub struct FixedStarPosition {
14    /// Star name, e.g. "Regulus".
15    pub name: &'static str,
16    /// Star glyph.
17    pub symbol: &'static str,
18    /// Zodiac sign name, e.g. "Taurus".
19    pub sign: &'static str,
20    /// Zodiac sign glyph, e.g. "♉".
21    pub sign_symbol: &'static str,
22    /// Whole degrees within the sign (0-29).
23    pub degree: i64,
24    /// Arc minutes past `degree` (0-59).
25    pub minute: i64,
26    /// Ecliptic longitude in degrees [0, 360), tropical or sidereal as requested.
27    pub ecliptic_longitude: f64,
28    /// Ecliptic latitude at J2000, in degrees.
29    pub ecliptic_latitude: f64,
30    /// House (1-12) the point falls in.
31    pub house: u8,
32    /// Apparent visual magnitude.
33    pub magnitude: f64,
34    /// Symbolic degree (1-30) within the sign, as used by degree symbolism.
35    pub symbolic_degree: i64,
36}
37
38/// Stars within `max_orb` of a chart point, and those conjunctions. Positions are the
39/// J2000 longitudes precessed at the mean rate; a star is listed only when conjunct.
40pub fn fixed_stars(
41    jd: f64,
42    cusps: &[f64],
43    points: &[Point],
44    max_orb: f64,
45    sidereal_shift: Option<f64>,
46) -> (Vec<FixedStarPosition>, Vec<Aspect>) {
47    let years = (jd - 2_451_545.0) / 365.25;
48    let precession_deg = (years * PRECESSION_RATE_ARCSEC_YEAR) / 3600.0;
49    let mut stars = Vec::new();
50    let mut aspects = Vec::new();
51
52    for star in &FIXED_STARS {
53        let mut lon = pyfloat::rem(star.j2000_lon + precession_deg, 360.0);
54        if let Some(ayanamsa) = sidereal_shift {
55            lon = pyfloat::rem(pyfloat::rem(lon - ayanamsa, 360.0) + 360.0, 360.0);
56        }
57        let mut conjunct = false;
58        for body in points {
59            let mut diff = pyfloat::rem((lon - body.longitude).abs(), 360.0);
60            if diff > 180.0 {
61                diff = 360.0 - diff;
62            }
63            let orb = (diff - 0.0).abs();
64            if orb <= max_orb {
65                conjunct = true;
66                aspects.push(Aspect {
67                    body1: star.name.to_string(),
68                    body2: body.name.to_string(),
69                    aspect_type: "Conjunction",
70                    symbol: "☌",
71                    angle: 0.0,
72                    orb: pyfloat::round(orb, 2),
73                    max_orb: pyfloat::round(max_orb, 2),
74                    is_major: None,
75                    is_applying: true,
76                });
77            }
78        }
79        if conjunct {
80            let z = longitude_to_zodiac(lon);
81            stars.push(FixedStarPosition {
82                name: star.name,
83                symbol: star.symbol,
84                sign: z.sign.name,
85                sign_symbol: z.sign.symbol,
86                degree: z.degree,
87                minute: z.minute,
88                ecliptic_longitude: lon,
89                ecliptic_latitude: star.j2000_lat,
90                house: determine_house(lon, cusps),
91                magnitude: star.magnitude,
92                symbolic_degree: symbolic_degree_number(z.degree, z.minute),
93            });
94        }
95    }
96    (stars, aspects)
97}