astro_chart 0.1.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +122 -0
  3. data/LICENSE +17 -16
  4. data/README.md +417 -0
  5. data/astro_chart.gemspec +11 -6
  6. data/lib/astro_chart/aspects.rb +31 -2
  7. data/lib/astro_chart/chart.rb +94 -12
  8. data/lib/astro_chart/composite.rb +101 -0
  9. data/lib/astro_chart/dignities.rb +220 -0
  10. data/lib/astro_chart/draconic.rb +64 -0
  11. data/lib/astro_chart/ephemeris.rb +125 -18
  12. data/lib/astro_chart/houses.rb +2 -1
  13. data/lib/astro_chart/lunar_return.rb +90 -0
  14. data/lib/astro_chart/patterns.rb +248 -0
  15. data/lib/astro_chart/points.rb +82 -0
  16. data/lib/astro_chart/profection.rb +56 -0
  17. data/lib/astro_chart/progressions.rb +68 -0
  18. data/lib/astro_chart/pure/core.rb +209 -0
  19. data/lib/astro_chart/pure/houses.rb +207 -0
  20. data/lib/astro_chart/pure/moon.rb +341 -0
  21. data/lib/astro_chart/pure/moon_elp.rb +931 -0
  22. data/lib/astro_chart/pure/pluto.rb +220 -0
  23. data/lib/astro_chart/pure/vsop87.rb +152 -0
  24. data/lib/astro_chart/pure/vsop87_data.rb +210 -0
  25. data/lib/astro_chart/pure.rb +68 -0
  26. data/lib/astro_chart/solar_arc.rb +59 -0
  27. data/lib/astro_chart/solar_return.rb +193 -0
  28. data/lib/astro_chart/stats.rb +34 -0
  29. data/lib/astro_chart/synastry.rb +118 -0
  30. data/lib/astro_chart/transit_timing.rb +186 -0
  31. data/lib/astro_chart/transits.rb +108 -0
  32. data/lib/astro_chart/version.rb +1 -1
  33. data/lib/astro_chart.rb +19 -1
  34. metadata +36 -45
  35. data/ext/astro_chart/astro_chart_ext.c +0 -99
  36. data/ext/astro_chart/extconf.rb +0 -8
  37. data/ext/astro_chart/swecl.c +0 -6428
  38. data/ext/astro_chart/swedate.c +0 -588
  39. data/ext/astro_chart/swedate.h +0 -81
  40. data/ext/astro_chart/swedll.h +0 -403
  41. data/ext/astro_chart/sweephe4.c +0 -702
  42. data/ext/astro_chart/sweephe4.h +0 -239
  43. data/ext/astro_chart/swehel.c +0 -3511
  44. data/ext/astro_chart/swehouse.c +0 -3143
  45. data/ext/astro_chart/swehouse.h +0 -98
  46. data/ext/astro_chart/swejpl.c +0 -958
  47. data/ext/astro_chart/swejpl.h +0 -103
  48. data/ext/astro_chart/swemmoon.c +0 -1930
  49. data/ext/astro_chart/swemplan.c +0 -967
  50. data/ext/astro_chart/swemptab.h +0 -10640
  51. data/ext/astro_chart/swenut2000a.h +0 -2819
  52. data/ext/astro_chart/sweodef.h +0 -326
  53. data/ext/astro_chart/sweph.c +0 -8614
  54. data/ext/astro_chart/sweph.h +0 -849
  55. data/ext/astro_chart/swephexp.h +0 -1020
  56. data/ext/astro_chart/swephlib.c +0 -4634
  57. data/ext/astro_chart/swephlib.h +0 -189
@@ -0,0 +1,68 @@
1
+ require_relative "time_conversion"
2
+ require_relative "transits"
3
+
4
+ module AstroChart
5
+ # Secondary progressions (二次推運): "a day for a year".
6
+ #
7
+ # For a target date N years after birth, the progressed chart is the sky
8
+ # N *days* after birth: jd_prog = jd_natal + years_elapsed, where
9
+ # years_elapsed = (jd_target - jd_natal) / 365.2425.
10
+ #
11
+ # Typical usage with a Chart#generate result:
12
+ #
13
+ # AstroChart::Progressions.secondary(natal_chart, "2026-07-24")
14
+ module Progressions
15
+ # Mean Gregorian year length in days (matches the calendar's 400-year cycle).
16
+ DAYS_PER_YEAR = 365.2425
17
+
18
+ # Secondary-progressed positions for a natal chart at a target date.
19
+ #
20
+ # natal_chart: a Chart#generate result hash (its "input" block supplies
21
+ # birth date/time/timezone, so jd_natal is reconstructed exactly).
22
+ # target_date: "YYYY-MM-DD". The target moment is taken at the same
23
+ # local birth time + timezone, so whole calendar years elapse cleanly.
24
+ # orb_limit: keep only progressed-to-natal aspects with orb <= limit
25
+ # (default 1.0 — progressions move slowly, only exact contacts matter).
26
+ #
27
+ # Returns:
28
+ # {
29
+ # "progressed_jd" => 2447926.72,
30
+ # "years_elapsed" => 30.0,
31
+ # "planets" => [ { "planet" => "太陽", "zodiac" => "獅子座",
32
+ # "degree" => 9.87, "total_degree" => 129.87,
33
+ # "natal_house" => 3 }, ... 12 entries ],
34
+ # "aspects_to_natal" => [ { "progressed_planet" => "月亮",
35
+ # "natal_planet" => "金星",
36
+ # "aspect_type" => "三分相", "orb" => 0.4 }, ... ]
37
+ # }
38
+ #
39
+ # aspects_to_natal are sorted by orb (tightest first).
40
+ def self.secondary(natal_chart, target_date, orb_limit: 1.0)
41
+ input = natal_chart&.dig("input")
42
+ if input.nil? || input["birth_date"].nil? || input["birth_time"].nil? || input["timezone"].nil?
43
+ raise ArgumentError, "chart has no input data (birth_date/birth_time/timezone required)"
44
+ end
45
+
46
+ jd_natal = TimeConversion.to_julian_day(input["birth_date"], input["birth_time"], input["timezone"])
47
+ jd_target = TimeConversion.to_julian_day(target_date, input["birth_time"], input["timezone"])
48
+
49
+ years_elapsed = (jd_target - jd_natal) / DAYS_PER_YEAR
50
+ jd_prog = jd_natal + years_elapsed # a day for a year
51
+
52
+ progressed_positions = Planets.calculate_positions(jd_prog)
53
+ natal_positions = Synastry.positions_from_chart(natal_chart)
54
+ natal_cusps = Synastry.cusps_from_chart(natal_chart)
55
+
56
+ {
57
+ "progressed_jd" => jd_prog,
58
+ "years_elapsed" => years_elapsed.round(2),
59
+ "planets" => Transits.planet_details(progressed_positions, natal_cusps),
60
+ "aspects_to_natal" => Transits.aspects_to_natal(
61
+ progressed_positions, natal_positions,
62
+ orb_limit: orb_limit,
63
+ keys: %w[progressed_planet natal_planet]
64
+ ),
65
+ }
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AstroChart
4
+ module Pure
5
+ # Core — MIT-safe 純 Ruby 天文基建(零外部依賴)。
6
+ #
7
+ # 只使用公開文獻公式,不參考 Swiss Ephemeris C 原始碼:
8
+ # - 儒略日:Jean Meeus, "Astronomical Algorithms" 2nd ed., Ch. 7 (eq. 7.1)
9
+ # - ΔT(TT−UT):Espenak & Meeus 多項式擬合
10
+ # (NASA Eclipse website, "Polynomial Expressions for Delta T (ΔT)",
11
+ # Five Millennium Canon of Solar Eclipses: -1999 to +3000)
12
+ # - 章動(Δψ / Δε):Meeus Ch. 22(IAU 1980 序列截斷,振幅最大 13 項)
13
+ # - 平黃赤交角:Meeus eq. 22.2
14
+ # - 平恆星時 GMST:Meeus Ch. 12 (eq. 12.4,等同 IAU 1982 GMST)
15
+ # - 視恆星時 = GMST + Δψ·cos(ε_true)(Meeus Ch. 12)
16
+ #
17
+ # 時間尺度慣例(呼叫者必須遵守,弄錯會差 ~ΔT ≈ 70s → 月亮 ~0.01°):
18
+ # - julday / gmst_deg / apparent_sidereal_deg 吃 **JD(UT)**
19
+ # - nutation / mean_obliquity / true_obliquity 吃 **JD(TT)**(先過 jd_tt 換算)
20
+ #
21
+ # 公開 API(module_function,皆回傳 Float 或 [Float, Float]):
22
+ # Core.julday(year, month, day, hour) -> JD(UT);hour 為十進位 UT 小時
23
+ # Core.delta_t(jd_ut) -> ΔT 秒(TT − UT)
24
+ # Core.jd_tt(jd_ut) -> JD(TT) = jd_ut + ΔT/86400
25
+ # Core.nutation(jd_tt) -> [Δψ_deg, Δε_deg](度)
26
+ # Core.mean_obliquity(jd_tt) -> ε₀(度)
27
+ # Core.true_obliquity(jd_tt) -> ε = ε₀ + Δε(度)
28
+ # Core.gmst_deg(jd_ut) -> 格林威治平恆星時(度,[0,360))
29
+ # Core.apparent_sidereal_deg(jd_ut) -> 格林威治視恆星時(度,[0,360))
30
+ # Core.norm360(deg) -> 正規化到 [0, 360)
31
+ # Core::DomainError -> 超出適用範圍時 raise(顯性失敗)
32
+ module Core
33
+ DEG2RAD = Math::PI / 180.0
34
+ RAD2DEG = 180.0 / Math::PI
35
+
36
+ # 超出公式適用範圍或演算法未收斂時丟出——失敗必須顯性,
37
+ # 不可靜默回傳一個看似正常、實則錯誤的值。
38
+ class DomainError < StandardError; end
39
+
40
+ module_function
41
+
42
+ # --- 儒略日(Meeus 7.1,Gregorian) ---
43
+ # hour 為十進位 UT 小時(例 1.5 = 01:30 UT)
44
+ def julday(year, month, day, hour)
45
+ y = year
46
+ m = month
47
+ if m <= 2
48
+ y -= 1
49
+ m += 12
50
+ end
51
+ a = (y / 100.0).floor
52
+ b = 2 - a + (a / 4.0).floor
53
+ (365.25 * (y + 4716)).floor + (30.6001 * (m + 1)).floor +
54
+ day + hour / 24.0 + b - 1524.5
55
+ end
56
+
57
+ # --- ΔT = TT − UT(秒) ---
58
+ # Espenak & Meeus 分段多項式(NASA Eclipse website,涵蓋 -1999..+3000)。
59
+ # 決策年 y 用儒略年近似(y = 2000 + (JD-J2000)/365.25),與 E&M 定義的
60
+ # y = year + (month-0.5)/12 差 < 半個月,對逐年平滑的 ΔT 曲線影響 < 0.01s。
61
+ # 注意:2005 之後為預測值,與實測(IERS)可差數秒 → 月亮 ~0.001°,
62
+ # 在本專案 0.01° 精度預算內。
63
+ def delta_t(jd_ut)
64
+ y = 2000.0 + (jd_ut - 2451545.0) / 365.25
65
+
66
+ if y < -500.0
67
+ u = (y - 1820.0) / 100.0
68
+ -20.0 + 32.0 * u * u
69
+ elsif y < 500.0
70
+ u = y / 100.0
71
+ 10583.6 - 1014.41 * u + 33.78311 * u**2 - 5.952053 * u**3 -
72
+ 0.1798452 * u**4 + 0.022174192 * u**5 + 0.0090316521 * u**6
73
+ elsif y < 1600.0
74
+ u = (y - 1000.0) / 100.0
75
+ 1574.2 - 556.01 * u + 71.23472 * u**2 + 0.319781 * u**3 -
76
+ 0.8503463 * u**4 - 0.005050998 * u**5 + 0.0083572073 * u**6
77
+ elsif y < 1700.0
78
+ t = y - 1600.0
79
+ 120.0 - 0.9808 * t - 0.01532 * t**2 + t**3 / 7129.0
80
+ elsif y < 1800.0
81
+ t = y - 1700.0
82
+ 8.83 + 0.1603 * t - 0.0059285 * t**2 + 0.00013336 * t**3 - t**4 / 1_174_000.0
83
+ elsif y < 1860.0
84
+ t = y - 1800.0
85
+ 13.72 - 0.332447 * t + 0.0068612 * t**2 + 0.0041116 * t**3 -
86
+ 0.00037436 * t**4 + 0.0000121272 * t**5 - 0.0000001699 * t**6 +
87
+ 0.000000000875 * t**7
88
+ elsif y < 1900.0
89
+ t = y - 1860.0
90
+ 7.62 + 0.5737 * t - 0.251754 * t**2 + 0.01680668 * t**3 -
91
+ 0.0004473624 * t**4 + t**5 / 233_174.0
92
+ elsif y < 1920.0
93
+ t = y - 1900.0
94
+ -2.79 + 1.494119 * t - 0.0598939 * t**2 + 0.0061966 * t**3 - 0.000197 * t**4
95
+ elsif y < 1941.0
96
+ t = y - 1920.0
97
+ 21.20 + 0.84493 * t - 0.076100 * t**2 + 0.0020936 * t**3
98
+ elsif y < 1961.0
99
+ t = y - 1950.0
100
+ 29.07 + 0.407 * t - t**2 / 233.0 + t**3 / 2547.0
101
+ elsif y < 1986.0
102
+ t = y - 1975.0
103
+ 45.45 + 1.067 * t - t**2 / 260.0 - t**3 / 718.0
104
+ elsif y < 2005.0
105
+ t = y - 2000.0
106
+ 63.86 + 0.3345 * t - 0.060374 * t**2 + 0.0017275 * t**3 +
107
+ 0.000651814 * t**4 + 0.00002373599 * t**5
108
+ elsif y < 2050.0
109
+ t = y - 2000.0
110
+ 62.92 + 0.32217 * t + 0.005589 * t**2
111
+ elsif y < 2150.0
112
+ -20.0 + 32.0 * ((y - 1820.0) / 100.0)**2 - 0.5628 * (2150.0 - y)
113
+ else
114
+ u = (y - 1820.0) / 100.0
115
+ -20.0 + 32.0 * u * u
116
+ end
117
+ end
118
+
119
+ # --- JD(TT) = JD(UT) + ΔT/86400 ---
120
+ def jd_tt(jd_ut)
121
+ jd_ut + delta_t(jd_ut) / 86_400.0
122
+ end
123
+
124
+ # --- 章動(Meeus Ch. 22,IAU 1980 截斷序列) ---
125
+ # 係數單位:0.0001 角秒。取振幅最大的 13 項(Δψ 主項 -17.1996" sinΩ 起),
126
+ # 截斷誤差 < 0.0004",遠小於 0.01° 目標。
127
+ # 各項引數為 [D, M, M', F, Ω] 的整數倍組合(Meeus Table 22.A 前段)。
128
+ NUTATION_TERMS = [
129
+ # [D, M, Mp, F, Om, psi_sin, psi_sin_t, eps_cos, eps_cos_t]
130
+ [0, 0, 0, 0, 1, -171_996.0, -174.2, 92_025.0, 8.9],
131
+ [-2, 0, 0, 2, 2, -13_187.0, -1.6, 5736.0, -3.1],
132
+ [0, 0, 0, 2, 2, -2274.0, -0.2, 977.0, -0.5],
133
+ [0, 0, 0, 0, 2, 2062.0, 0.2, -895.0, 0.5],
134
+ [0, 1, 0, 0, 0, 1426.0, -3.4, 54.0, -0.1],
135
+ [0, 0, 1, 0, 0, 712.0, 0.1, -7.0, 0.0],
136
+ [-2, 1, 0, 2, 2, -517.0, 1.2, 224.0, -0.6],
137
+ [0, 0, 0, 2, 1, -386.0, -0.4, 200.0, 0.0],
138
+ [0, 0, 1, 2, 2, -301.0, 0.0, 129.0, -0.1],
139
+ [-2, -1, 0, 2, 2, 217.0, -0.5, -95.0, 0.3],
140
+ [-2, 0, 1, 0, 0, -158.0, 0.0, 0.0, 0.0],
141
+ [-2, 0, 0, 2, 1, 129.0, 0.1, -70.0, 0.0],
142
+ [0, 0, -1, 2, 2, 123.0, 0.0, -53.0, 0.0]
143
+ ].freeze
144
+
145
+ # 回傳 [Δψ_deg, Δε_deg](度)。引數時間尺度為 **TT**。
146
+ def nutation(jd_tt)
147
+ t = (jd_tt - 2451545.0) / 36525.0
148
+
149
+ # 基本引數(Meeus Ch. 22,度)
150
+ d = 297.85036 + 445_267.111480 * t - 0.0019142 * t * t + t**3 / 189_474.0
151
+ m = 357.52772 + 35_999.050340 * t - 0.0001603 * t * t - t**3 / 300_000.0
152
+ mp = 134.96298 + 477_198.867398 * t + 0.0086972 * t * t + t**3 / 56_250.0
153
+ f = 93.27191 + 483_202.017538 * t - 0.0036825 * t * t + t**3 / 327_270.0
154
+ om = 125.04452 - 1934.136261 * t + 0.0020708 * t * t + t**3 / 450_000.0
155
+
156
+ dpsi_01as = 0.0 # 單位 0.0001"
157
+ deps_01as = 0.0
158
+ NUTATION_TERMS.each do |cd, cm, cmp, cf, com, ps, pst, ec, ect|
159
+ arg = (cd * d + cm * m + cmp * mp + cf * f + com * om) * DEG2RAD
160
+ dpsi_01as += (ps + pst * t) * Math.sin(arg)
161
+ deps_01as += (ec + ect * t) * Math.cos(arg)
162
+ end
163
+
164
+ [dpsi_01as * 0.0001 / 3600.0, deps_01as * 0.0001 / 3600.0]
165
+ end
166
+
167
+ # --- 平黃赤交角(Meeus 22.2,度)。引數時間尺度為 **TT**。 ---
168
+ # 23°26'21.448" - 46.8150"T - 0.00059"T² + 0.001813"T³
169
+ def mean_obliquity(jd_tt)
170
+ t = (jd_tt - 2451545.0) / 36525.0
171
+ 23.0 + 26.0 / 60.0 + 21.448 / 3600.0 -
172
+ (46.8150 * t + 0.00059 * t * t - 0.001813 * t**3) / 3600.0
173
+ end
174
+
175
+ # --- 真黃赤交角 ε = ε₀ + Δε(度)。引數時間尺度為 **TT**。 ---
176
+ def true_obliquity(jd_tt)
177
+ mean_obliquity(jd_tt) + nutation(jd_tt)[1]
178
+ end
179
+
180
+ # --- 平恆星時 GMST(Meeus 12.4 / IAU 1982),回傳度。引數為 **UT**。 ---
181
+ def gmst_deg(jd_ut)
182
+ t = (jd_ut - 2451545.0) / 36525.0
183
+ norm360(
184
+ 280.46061837 +
185
+ 360.98564736629 * (jd_ut - 2451545.0) +
186
+ 0.000387933 * t * t -
187
+ t * t * t / 38_710_000.0
188
+ )
189
+ end
190
+
191
+ # --- 視恆星時 = GMST + Δψ·cos(ε_true)(度)。引數為 **UT**。 ---
192
+ # 章動與交角的引數在內部換算為 TT。
193
+ def apparent_sidereal_deg(jd_ut)
194
+ tt = jd_tt(jd_ut)
195
+ dpsi_deg, = nutation(tt)
196
+ eps_rad = true_obliquity(tt) * DEG2RAD
197
+ norm360(gmst_deg(jd_ut) + dpsi_deg * Math.cos(eps_rad))
198
+ end
199
+
200
+ # --- 正規化到 [0, 360) ---
201
+ def norm360(deg)
202
+ deg %= 360.0
203
+ # Ruby 的 float % 對正模數永不為負,但極小負數(如 -1e-16 % 360.0)
204
+ # 會因浮點捨入得到恰好 360.0,違反 [0, 360) 契約 → 下游 floor(deg/30) 會得 12。
205
+ deg >= 360.0 ? 0.0 : deg
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "core"
4
+
5
+ module AstroChart
6
+ module Pure
7
+ # Houses — MIT-safe 純 Ruby Placidus 宮位(ASC / MC / 12 宮頭)。
8
+ #
9
+ # 零外部依賴、不參考 Swiss Ephemeris C 原始碼,僅使用公開文獻公式:
10
+ # - 視恆星時 / 章動 / 黃赤交角:見 Core(Meeus Ch. 12 / 22)
11
+ # - ASC / MC:標準球面天文公式(Meeus Ch. 13 座標轉換推得)
12
+ # - Placidus 宮頭:古典半弧迭代法(半日弧 SA = arccos(-tanφ·tanδ),
13
+ # 宮頭 11/12 位於 RAMC 東側 1/3、2/3 半日弧處;宮頭 2/3 以半夜弧對應)
14
+ #
15
+ # 公開 API:
16
+ # Houses.calc(jd_ut, lat, lon, hsys = "P")
17
+ # -> { "cusps" => [12 floats], "ascendant" => Float, "mc" => Float }
18
+ # 欄位與 AstroChart::Ext.houses(jd, lat, lon, hsys.ord) 相同。
19
+ #
20
+ # 宮位制:
21
+ # "P" — Placidus(預設)。適用範圍 |lat| < ~66°(極圈內無定義),
22
+ # 超出範圍或迭代未收斂時 raise Core::DomainError(顯性失敗,不靜默 clamp)。
23
+ # "W" — 整宮制(Whole Sign)。宮頭 1 = ASC 所在星座起點(floor(asc/30)*30),
24
+ # 其後每宮 +30°。ASC / MC 計算與 Placidus 相同(整宮制只改宮頭),
25
+ # 唯極圈內(|lat| > 90° − ε ≈ 66.56°)另套用東昇點修正(見
26
+ # east_ascendant)——標準公式在該區可能傳回差 180° 的西方交點。
27
+ # 於 |lat| < 90° 皆有定義(lat = ±90° 時 tanφ 發散,ASC 本身無定義)。
28
+ module Houses
29
+ DEG2RAD = Core::DEG2RAD
30
+ RAD2DEG = Core::RAD2DEG
31
+
32
+ module_function
33
+
34
+ # jd_ut: UT 儒略日;lat: 地理緯度(北正);lon: 地理經度(東正)
35
+ # hsys: "P"(Placidus,預設)或 "W"(整宮制)
36
+ def calc(jd_ut, lat, lon, hsys = "P")
37
+ tt = Core.jd_tt(jd_ut)
38
+ eps = Core.true_obliquity(tt) * DEG2RAD # 真黃赤交角(弧度)
39
+ ast_deg = Core.apparent_sidereal_deg(jd_ut) # 視恆星時(度)
40
+ armc = Core.norm360(ast_deg + lon) # 天頂赤經 RAMC
41
+
42
+ phi = lat * DEG2RAD
43
+ mc = mc_longitude(armc, eps)
44
+ asc = asc_longitude(armc, eps, phi)
45
+
46
+ cusps =
47
+ case hsys
48
+ when "W"
49
+ # 極圈內(|lat| > 90° − ε ≈ 66.56°)標準 atan2 公式可能傳回西方
50
+ # 交點(差 180°);整宮制在極圈內仍有定義,故此處修正為東昇點。
51
+ # 極圈外此修正恆為 no-op;Placidus 路徑不套用(維持原輸出)。
52
+ asc = east_ascendant(asc, mc)
53
+ whole_sign_cusps(asc)
54
+ when "E"
55
+ # 等宮制:宮頭 1 = ASC,其後每宮 +30°。與整宮制同樣在 |lat| < 90°
56
+ # 皆有定義,故一併套用東昇點修正。
57
+ asc = east_ascendant(asc, mc)
58
+ equal_cusps(asc)
59
+ when "O"
60
+ # Porphyry:四大軸(ASC/IC/DSC/MC)之間的黃道弧各三等分。純幾何,
61
+ # 不需半弧迭代,於 |lat| < 90° 皆有定義。
62
+ asc = east_ascendant(asc, mc)
63
+ porphyry_cusps(asc, mc)
64
+ else
65
+ placidus_cusps(armc, eps, phi, asc, mc)
66
+ end
67
+
68
+ { "cusps" => cusps, "ascendant" => asc, "mc" => mc }
69
+ end
70
+
71
+ # --- Placidus 12 宮頭 ---
72
+ def placidus_cusps(armc, eps, phi, asc, mc)
73
+ # Placidus 半弧迭代:宮頭 11、12(地平上,RAMC 東側)與 2、3(地平下)
74
+ c11 = placidus_cusp(armc, eps, phi, 30.0, 1.0 / 3.0, :diurnal)
75
+ c12 = placidus_cusp(armc, eps, phi, 60.0, 2.0 / 3.0, :diurnal)
76
+ c2 = placidus_cusp(armc, eps, phi, 120.0, 2.0 / 3.0, :nocturnal)
77
+ c3 = placidus_cusp(armc, eps, phi, 150.0, 1.0 / 3.0, :nocturnal)
78
+
79
+ cusps = Array.new(12)
80
+ cusps[0] = asc # 第 1 宮 = ASC
81
+ cusps[1] = c2
82
+ cusps[2] = c3
83
+ cusps[3] = Core.norm360(mc + 180.0) # 第 4 宮 = MC 對沖
84
+ cusps[4] = Core.norm360(c11 + 180.0)
85
+ cusps[5] = Core.norm360(c12 + 180.0)
86
+ cusps[6] = Core.norm360(asc + 180.0) # 第 7 宮 = ASC 對沖
87
+ cusps[7] = Core.norm360(c2 + 180.0)
88
+ cusps[8] = Core.norm360(c3 + 180.0)
89
+ cusps[9] = mc # 第 10 宮 = MC
90
+ cusps[10] = c11
91
+ cusps[11] = c12
92
+ cusps
93
+ end
94
+
95
+ # --- 東昇點修正 ---
96
+ # ASC 依定義位於 MC 東側半圓:(asc − mc) mod 360 ∈ (0°, 180°)。
97
+ # 極圈內原始公式可能落在西側半圓(實為降點),此時加 180°。
98
+ # 與 Swiss Ephemeris 極區行為一致(tmp/explore_polar_asc.rb 驗證 400 例全符)。
99
+ def east_ascendant(asc, mc)
100
+ ((asc - mc) % 360.0) < 180.0 ? asc : Core.norm360(asc + 180.0)
101
+ end
102
+
103
+ # --- 整宮制 12 宮頭 ---
104
+ # 宮頭 1 = ASC 所在星座 0°,其後每宮 +30°(模 360)。
105
+ def whole_sign_cusps(asc)
106
+ base = (asc / 30.0).floor * 30.0
107
+ Array.new(12) { |i| (base + 30.0 * i) % 360.0 }
108
+ end
109
+
110
+ # --- 等宮制 12 宮頭 ---
111
+ # 宮頭 1 = ASC 本身,其後每宮 +30°(模 360)。MC 仍照常算出並回傳,
112
+ # 但等宮制的第 10 宮頭為 ASC+270°,不等於 MC(MC 在宮內浮動)。
113
+ def equal_cusps(asc)
114
+ Array.new(12) { |i| (asc + 30.0 * i) % 360.0 }
115
+ end
116
+
117
+ # --- Porphyry 12 宮頭 ---
118
+ # 依黃道經度遞增,四大軸順序為 ASC(1) → IC(4) → DSC(7) → MC(10)。每個
119
+ # 象限的黃道弧三等分,得中間兩個宮頭:
120
+ # 1→4:ASC..IC 弧 → 宮頭 2、3 4→7:IC..DSC 弧 → 宮頭 5、6
121
+ # 7→10:DSC..MC 弧 → 宮頭 8、9 10→1:MC..ASC 弧 → 宮頭 11、12
122
+ def porphyry_cusps(asc, mc)
123
+ ic = Core.norm360(mc + 180.0) # 第 4 宮 = MC 對沖
124
+ dsc = Core.norm360(asc + 180.0) # 第 7 宮 = ASC 對沖
125
+
126
+ q1 = (ic - asc) % 360.0 # ASC → IC
127
+ q2 = (dsc - ic) % 360.0 # IC → DSC
128
+ q3 = (mc - dsc) % 360.0 # DSC → MC
129
+ q4 = (asc - mc) % 360.0 # MC → ASC
130
+
131
+ cusps = Array.new(12)
132
+ cusps[0] = asc
133
+ cusps[1] = Core.norm360(asc + q1 / 3.0)
134
+ cusps[2] = Core.norm360(asc + 2.0 * q1 / 3.0)
135
+ cusps[3] = ic
136
+ cusps[4] = Core.norm360(ic + q2 / 3.0)
137
+ cusps[5] = Core.norm360(ic + 2.0 * q2 / 3.0)
138
+ cusps[6] = dsc
139
+ cusps[7] = Core.norm360(dsc + q3 / 3.0)
140
+ cusps[8] = Core.norm360(dsc + 2.0 * q3 / 3.0)
141
+ cusps[9] = mc
142
+ cusps[10] = Core.norm360(mc + q4 / 3.0)
143
+ cusps[11] = Core.norm360(mc + 2.0 * q4 / 3.0)
144
+ cusps
145
+ end
146
+
147
+ # --- MC:天頂赤經 → 黃道經度 ---
148
+ # tan λ_MC = tan(ARMC) / cos ε,以 atan2 處理象限
149
+ def mc_longitude(armc_deg, eps)
150
+ ra = armc_deg * DEG2RAD
151
+ Core.norm360(Math.atan2(Math.sin(ra), Math.cos(ra) * Math.cos(eps)) * RAD2DEG)
152
+ end
153
+
154
+ # --- ASC:標準公式 ---
155
+ # λ_ASC = atan2( cos ARMC, -(sin ARMC · cos ε + tan φ · sin ε) )
156
+ def asc_longitude(armc_deg, eps, phi)
157
+ ra = armc_deg * DEG2RAD
158
+ Core.norm360(
159
+ Math.atan2(
160
+ Math.cos(ra),
161
+ -(Math.sin(ra) * Math.cos(eps) + Math.tan(phi) * Math.sin(eps))
162
+ ) * RAD2DEG
163
+ )
164
+ end
165
+
166
+ # --- Placidus 宮頭半弧迭代 ---
167
+ # 定義:黃道上一點 (α, δ),半日弧 SA_d = arccos(-tanφ·tanδ)、半夜弧 SA_n = arccos(tanφ·tanδ)。
168
+ # 宮頭 11:α = RAMC + (1/3)·SA_d 宮頭 12:α = RAMC + (2/3)·SA_d
169
+ # 宮頭 2 :α = RAMC + 180 - (2/3)·SA_n 宮頭 3 :α = RAMC + 180 - (1/3)·SA_n
170
+ # 黃道點的赤緯由 tan δ = tan ε · sin α 得出,固定點迭代至收斂。
171
+ # offset_deg:初值偏移(赤道情形的精確解);frac:半弧比例;mode:地平上/下。
172
+ def placidus_cusp(armc_deg, eps, phi, offset_deg, frac, mode)
173
+ tan_phi = Math.tan(phi)
174
+ tan_eps = Math.tan(eps)
175
+ ra = (armc_deg + offset_deg) * DEG2RAD
176
+ armc_rad = armc_deg * DEG2RAD
177
+
178
+ converged = false
179
+ 60.times do
180
+ dec = Math.atan(tan_eps * Math.sin(ra)) # 黃道點赤緯
181
+ x = tan_phi * Math.tan(dec)
182
+ if x.abs > 1.0
183
+ # 極圈內該點無升落,Placidus 無定義。顯性失敗,不靜默 clamp。
184
+ raise Core::DomainError,
185
+ "Placidus 在此緯度無定義(|tanφ·tanδ| = #{x.abs.round(4)} > 1,lat=#{(phi * RAD2DEG).round(4)}°)"
186
+ end
187
+ new_ra =
188
+ if mode == :diurnal
189
+ armc_rad + frac * Math.acos(-x)
190
+ else
191
+ armc_rad + Math::PI - frac * Math.acos(x)
192
+ end
193
+ step = (new_ra - ra).abs
194
+ ra = new_ra # 先採納本次結果,收斂時回傳的才是最終值
195
+ if step < 1e-9 * DEG2RAD
196
+ converged = true
197
+ break
198
+ end
199
+ end
200
+ raise Core::DomainError, "Placidus 宮頭迭代 60 次未收斂" unless converged
201
+
202
+ # 赤經 → 黃道經度:tan λ = tan α / cos ε
203
+ Core.norm360(Math.atan2(Math.sin(ra), Math.cos(ra) * Math.cos(eps)) * RAD2DEG)
204
+ end
205
+ end
206
+ end
207
+ end