Skip to main content

mingli_core/
quantizer.rs

1//! S2 圆 S¹ 角度 → Z_n 分段(家族 B 的代数石)。
2//!
3//! 把一个连续角度(来自 L1 星历的黄经,或任何 [0,360) 量)量化进 n 个桶。
4//! 西洋占星/Jyotish/七政四余共用:唯一差别在喂进来的角度(回归 vs 恒星黄经)。
5
6/// 角度归一到 `[0,360)`。
7#[inline]
8#[must_use]
9pub fn norm360(deg: f64) -> f64 {
10    deg.rem_euclid(360.0)
11}
12
13/// 角度差归一到 `(-180, 180]`(用于求两角的有向最短差,如黄经收敛迭代)。
14#[inline]
15#[must_use]
16pub fn norm180(deg: f64) -> f64 {
17    let a = deg.rem_euclid(360.0);
18    if a > 180.0 {
19        a - 360.0
20    } else {
21        a
22    }
23}
24
25/// 等分量化:360° 分 `n` 桶,返回桶号 `0..n`。
26/// 例:`sector(λ,12)` → 星座;`sector(λ,24)` → 节气见下。
27#[inline]
28#[must_use]
29#[allow(
30    clippy::cast_possible_truncation,
31    clippy::cast_sign_loss,
32    reason = "floor 后的值恒在 [0,n) 内,截断与符号丢失不会发生"
33)]
34pub fn sector(deg: f64, n: u32) -> u32 {
35    let w = 360.0 / f64::from(n);
36    (norm360(deg) / w).floor() as u32 % n
37}
38
39/// 桶内剩余角度(如「星座内度数」)。
40#[inline]
41#[must_use]
42pub fn within(deg: f64, n: u32) -> f64 {
43    let w = 360.0 / f64::from(n);
44    norm360(deg).rem_euclid(w)
45}
46
47/// 二十四节气序号:太阳黄经每 15° 一个,0°=春分(序 0)。返回 `0..24`。
48#[inline]
49#[must_use]
50pub fn solar_term_index(sun_longitude: f64) -> u32 {
51    sector(sun_longitude, 24)
52}
53
54/// 不等分量化:给定升序边界 `bounds`(如占星 terms 每宫 5 段),返回落入的段号。
55/// `bounds` 为各段**起点**(首段起点应为 0),长度 = 段数。
56#[must_use]
57pub fn unequal_sector(x: f64, span: f64, bounds: &[f64]) -> usize {
58    let v = x.rem_euclid(span);
59    let mut idx = 0;
60    for (i, &b) in bounds.iter().enumerate() {
61        if v >= b {
62            idx = i;
63        } else {
64            break;
65        }
66    }
67    idx
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn norm_ranges() {
76        assert!((norm360(370.0) - 10.0).abs() < 1e-9);
77        assert!((norm360(-10.0) - 350.0).abs() < 1e-9);
78        assert!((norm180(190.0) + 170.0).abs() < 1e-9);
79        assert!((norm180(180.0) - 180.0).abs() < 1e-9);
80        assert!((norm180(-10.0) + 10.0).abs() < 1e-9);
81    }
82
83    #[test]
84    fn zodiac_sectors() {
85        assert_eq!(sector(0.0, 12), 0); // 0°白羊
86        assert_eq!(sector(35.0, 12), 1); // 金牛
87        assert_eq!(sector(359.9, 12), 11); // 双鱼
88        assert!((within(35.0, 12) - 5.0).abs() < 1e-9);
89    }
90
91    #[test]
92    fn solar_terms() {
93        assert_eq!(solar_term_index(0.0), 0); // 春分
94        assert_eq!(solar_term_index(90.0), 6); // 夏至(第6个15°)
95        assert_eq!(solar_term_index(315.0), 21); // 立春(315/15=21)
96        assert_eq!(solar_term_index(360.0), 0); // wrap
97    }
98
99    #[test]
100    fn egyptian_terms_of_aries() {
101        // 白羊界:木0–6 金6–12 水12–20 火20–25 土25–30(起点)。
102        let bounds = [0.0, 6.0, 12.0, 20.0, 25.0];
103        assert_eq!(unequal_sector(3.0, 30.0, &bounds), 0); // 木
104        assert_eq!(unequal_sector(15.0, 30.0, &bounds), 2); // 水
105        assert_eq!(unequal_sector(28.0, 30.0, &bounds), 4); // 土
106    }
107
108    use proptest::prelude::*;
109    proptest! {
110        #[test]
111        fn prop_norm360_in_range(d in -1e7f64..1e7) {
112            prop_assert!((0.0..360.0).contains(&norm360(d)));
113        }
114        #[test]
115        fn prop_norm180_in_range(d in -1e7f64..1e7) {
116            let n = norm180(d); // 值域 (-180, 180]
117            prop_assert!(n > -180.0 && n <= 180.0);
118        }
119        #[test]
120        fn prop_sector_in_range(d in -1e7f64..1e7, n in 1u32..360) {
121            prop_assert!(sector(d, n) < n);
122        }
123        #[test]
124        fn prop_within_below_sector_width(d in -1e7f64..1e7, n in 1u32..360) {
125            prop_assert!(within(d, n) < 360.0 / f64::from(n) + 1e-9);
126        }
127    }
128}