astro_chart 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 203914237dcef72fb6ebc852749cf78811321155c0a509c0c8f711c1646cf7da
4
- data.tar.gz: 87661219e2c6f29aa8b9330a5af0a4408ce65d342899a3e085ea3dee9a9f43f2
3
+ metadata.gz: 1ddcb896268ae30564a7041611a7811b707a7547affa849de1d4498bf2e1553f
4
+ data.tar.gz: 7773d2a228e636ad928e90f408fa36375e87bc0fc1a7745d6534a861269ea121
5
5
  SHA512:
6
- metadata.gz: 81b2fd730e85708ecb9a9264bbfbd7bee4d6ea848c3554b12f4f98303a6f1f9432e7713e071191193035facb0a46f3c886efc1d7767c27711eb9384990766c6c
7
- data.tar.gz: bd6534cf3b77dcd81e9b800a343cc594a08e48bfbc8419024b897e59af9f0beef41c4c6d3bdc01d1011c7568659c8322c9b71ff3f2f1cefb8c2931eb4e17aca3
6
+ metadata.gz: cee34349b951431356fb2a776d40e0ede18cfeafbdb6564221aed0e80374a623a4ed58fab8de96bd6b99b98a7593518fd7f6a9737a508dd0aba0be12a128a749
7
+ data.tar.gz: dff024c7b6f9dcfa20f5882eecc95ad99fcf0fc6381f187f2c072485694caabd094f107fd2a09865086700c24cfc72a7326b4b11dfffb800fd2ae3b03e284376
data/CHANGELOG.md CHANGED
@@ -1,5 +1,102 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0 (2026-08-06)
4
+
5
+ **Traditional dignities, transit timing, two more house systems, minor
6
+ aspects, four new configurations, antiscia, draconic charts, solar arc,
7
+ lunar & annual predictive tools.** All pure Ruby, additive and
8
+ backward-compatible.
9
+
10
+ - **Minor aspects**: `Aspects.calculate(a, b, minor: true)` also matches
11
+ 十二分相 (30°), 半四分相 (45°), 補八分相 (135°) and 補十二分相 (quincunx,
12
+ 150°). Default stays major-only; minor aspects carry tighter orbs and never
13
+ shadow a major one.
14
+ - **New aspect patterns** in `Patterns.detect` (and every `Chart#generate`
15
+ output): 上帝之指 (Yod: sextile base + quincunx apex), 風箏 (Kite: a 大三角
16
+ plus an opposing/sextiling focal body, reported alongside the trine), 神祕
17
+ 矩形 (Mystic Rectangle: two oppositions joined by sextiles and trines) and
18
+ 星群 (Stellium: 3+ bodies in one sign). Existing 大三角/T三角/大十字 output is
19
+ unchanged; the node-axis exclusion rules extend to the new configurations.
20
+ - **Antiscia**: `Points.antiscion` (reflection across the 巨蟹–摩羯 solstice
21
+ axis) and `Points.contra_antiscion` (across the 牡羊–天秤 equinox axis).
22
+ - **Draconic charts (龍盤)**: new `AstroChart::Draconic` — `.positions` shifts
23
+ every longitude so the natal 北交點 sits at 0° 牡羊; `.chart` adds draconic
24
+ signs, in-sign degrees and inter-aspects.
25
+ - **Two more house systems**: Equal (`"E"`, from the ascendant) and Porphyry
26
+ (`"O"`, quadrant trisection), joining Placidus (`"P"`) and Whole Sign
27
+ (`"W"`). Both are pure geometry off the ASC/MC and are defined at every
28
+ latitude |lat| < 90° (including inside the polar circle, where Placidus
29
+ raises). Ord aliases 69/79 mirror the C-extension int argument.
30
+ - **Essential dignities (必然尊貴)**: new `AstroChart::Dignities` — 廟/旺/三分性/
31
+ 界/外觀 with the traditional rulerships and Dorothean triplicities.
32
+ `.of(planet, longitude, sect:)` reports a planet's dignities/debilities and
33
+ Lilly-weighted score (+5/+4/+3/+2/+1); `.almuten(longitude, sect:)` returns
34
+ the winning planet at a degree; plus `.domicile_ruler`/`detriment_ruler`/
35
+ `exaltation_ruler`/`fall_ruler`/`triplicity_ruler`/`term_ruler`/`face_ruler`.
36
+ 界 ships both 埃及界 (Egyptian, `scheme: :egyptian`, the default) and 托勒密界
37
+ (Ptolemaic, `scheme: :ptolemaic`); both are cross-validated against Astrolog
38
+ and the Egyptian table matches several independent references.
39
+ - **Annual profection (小限法)**: new `AstroChart::Profection` — `.annual(asc,
40
+ age)` and `.at(asc, birth_date, target_date)` give the profected house/sign
41
+ and Lord of the Year (年主星).
42
+ - **Transit timing (行運精確時點)**: new `AstroChart::TransitTiming.events` —
43
+ the exact UTC instants in a date range when a transiting body perfects an
44
+ aspect to a natal point, found by bracket-and-bisect on the ephemeris
45
+ (robust through retrograde stations, so all three passes of an outer-planet
46
+ contact are caught). `minor:`, `step_days:` and `bodies:` options.
47
+ - **Solar arc directions (太陽弧推運)**: new `AstroChart::SolarArc.directions`
48
+ — directs the whole chart rigidly by the secondary-progressed Sun's arc and
49
+ reports directed-to-natal aspects.
50
+ - **Lunar returns (月亮回歸)**: new `AstroChart::LunarReturn.for_date` — the
51
+ chart at the Moon's monthly return to its natal longitude, relocatable like
52
+ the solar return.
53
+
54
+ ## 0.3.0 (2026-07-24)
55
+
56
+ **Predictive & comparison modules, whole-sign houses, retrograde flags,
57
+ derived points, pattern detection.** All pure Ruby, MIT.
58
+
59
+ - **Whole-sign houses**: `Chart.new(..., house_system: "W")` ("P" Placidus
60
+ remains the default; anything else raises `ArgumentError`). Whole-sign
61
+ works at all latitudes |lat| < 90° (including polar latitudes, where
62
+ Placidus still raises); Placidus output is byte-identical to 0.2.0.
63
+ `Ephemeris.houses` / `Pure.houses` accept `"W"` too.
64
+ - **Retrograde flags**: every planet entry in `Chart#generate` gains
65
+ `"retrograde" => true/false` (central-difference daily motion via the new
66
+ `Ephemeris.speed(jd, id)` / `Ephemeris.retrograde?(jd, id)`). 太陽/月亮
67
+ are hard-false, 南交點 mirrors 北交點, derived/ruler points are false.
68
+ Within half a day of a body's valid ephemeris window edge (e.g. the
69
+ Pluto series' 1885–2099 range) the speed stencil falls back to a
70
+ one-sided difference, so edge-of-range charts still generate.
71
+ - **Derived points** (`AstroChart::Points`): 福點 (Part of Fortune,
72
+ day/night formula; sect judged from the horizon — ASC–DSC axis — so it
73
+ is independent of the display house system) and 莉莉絲 (mean Black Moon
74
+ Lilith, Meeus mean lunar apogee + 180°, verified < 0.13° vs Swiss
75
+ Ephemeris across 1900–2050). Both appended to `Chart#generate` planets,
76
+ before the ruler points.
77
+ - **Transits** (`AstroChart::Transits`): `.at(jd)` sky snapshot;
78
+ `.against(natal, jd, orb_limit: 3.0)` places transiting bodies in natal
79
+ houses and lists transit-to-natal aspects sorted by orb.
80
+ - **Secondary progressions** (`AstroChart::Progressions`):
81
+ `.secondary(natal, "YYYY-MM-DD", orb_limit: 1.0)` — day-for-a-year
82
+ progressed positions, natal house placement, aspects to natal.
83
+ - **Composite charts** (`AstroChart::Composite`): `.between(a, b)` —
84
+ shorter-arc midpoint chart of two natal charts, with internal aspects.
85
+ - **Solar returns** (`AstroChart::SolarReturn`): `.for_year(natal, year,
86
+ latitude:, longitude:, timezone:)` — Newton-iterated exact return
87
+ instant, full relocated chart at that moment.
88
+ - **Aspect patterns** (`AstroChart::Patterns.detect`): 大三角 (with
89
+ element), T三角 (with apex), 大十字; node-axis oppositions excluded
90
+ from opposition legs; 大十字 subsumes its own T三角s; an opposition
91
+ squared by the node axis is reported as a single T三角 (apex 北交點)
92
+ rather than mirrored 北/南交點 twins. `Chart#generate` now includes
93
+ `"patterns"`.
94
+ - **Element statistics** (`AstroChart::Stats.elements`): 火土風水 element
95
+ and 基本固定變動 modality counts over the 10 classical planets.
96
+ `Chart#generate` now includes `"element_stats"` and `"house_system"`.
97
+ - `Houses.calculate` now takes the house system as a keyword
98
+ (`system: "P"`).
99
+
3
100
  ## 0.2.0 (2026-07-24)
4
101
 
5
102
  **License change: AGPL-3.0 → MIT.**
data/README.md ADDED
@@ -0,0 +1,417 @@
1
+ <p align="center">
2
+ <img src="logo.png" alt="AstroChart — Pure-Ruby astrology chart calculation" width="600">
3
+ </p>
4
+
5
+ # AstroChart
6
+
7
+ Pure-Ruby gem for astrology chart calculation: apparent planetary
8
+ longitudes, four house systems (Placidus, whole-sign, equal, Porphyry),
9
+ retrograde flags, major & minor aspects, derived points (福點/莉莉絲/映點),
10
+ aspect patterns, element statistics, essential dignities & almuten,
11
+ synastry, transits, transit timing, secondary progressions, solar arc
12
+ directions, composite & draconic charts, solar & lunar returns, and annual
13
+ profection.
14
+
15
+ **No C extension, no external data files, MIT licensed.** Implemented from
16
+ public formulas (Jean Meeus, *Astronomical Algorithms* 2nd ed.; VSOP87D;
17
+ ELP-2000/82B), verified against Swiss Ephemeris to < 0.014° (most bodies
18
+ < 0.001°) across 1900–2026 with zero zodiac sign flips.
19
+
20
+ ## Hosted Service(線上服務)
21
+
22
+ A live web UI and open JSON API built on this gem:
23
+
24
+ - **Web 星盤查詢**: https://astro-chart-api.fly.dev/ (本命盤、合盤、行運、二次推運、組合盤、太陽回歸,繁體中文)
25
+ - **API 文件**: https://astro-chart-api.fly.dev/docs
26
+ - Currently in open beta: no API key, no rate limit, free. CORS enabled.
27
+
28
+ ```bash
29
+ curl -X POST https://astro-chart-api.fly.dev/api/v1/charts \
30
+ -H "Content-Type: application/json" \
31
+ -d '{"birth_date":"1990-01-01","birth_time":"12:00",
32
+ "latitude":25.033,"longitude":121.5654,"timezone":"Asia/Taipei"}'
33
+ ```
34
+
35
+ Endpoints: `POST /api/v1/charts` (natal chart), `POST /api/v1/synastry`
36
+ (合盤), `POST /api/v1/transits` (行運), `POST /api/v1/progressions`
37
+ (二次推運), `POST /api/v1/composite` (組合盤), `POST /api/v1/solar-return`
38
+ (太陽回歸), `GET /api/v1/cities` (城市搜尋), `GET /api/v1/health`.
39
+ The hosted web app and MCP server live in a separate repository and depend
40
+ on `astro_chart` as a normal gem.
41
+
42
+ ## Installation
43
+
44
+ ```ruby
45
+ gem "astro_chart", "~> 0.3"
46
+ ```
47
+
48
+ Then `bundle install`. Nothing to compile.
49
+
50
+ ## Usage
51
+
52
+ ```ruby
53
+ require "astro_chart"
54
+
55
+ chart = AstroChart::Chart.new(
56
+ birth_date: "1990-01-01",
57
+ birth_time: "12:00",
58
+ latitude: 25.0330,
59
+ longitude: 121.5654,
60
+ timezone: "Asia/Taipei",
61
+ house_system: "P" # "P" Placidus (default) or "W" Whole Sign
62
+ )
63
+
64
+ result = chart.generate
65
+ ```
66
+
67
+ ### Return Value
68
+
69
+ `generate` returns a Hash with string keys:
70
+
71
+ ```ruby
72
+ {
73
+ "input" => {
74
+ "birth_date" => "1990-01-01",
75
+ "birth_time" => "12:00",
76
+ "coordinates" => { "latitude" => 25.033, "longitude" => 121.5654 },
77
+ "timezone" => "Asia/Taipei"
78
+ },
79
+ "chart" => {
80
+ "ascendant" => {
81
+ "zodiac" => "牡羊座",
82
+ "degree" => 16.4422,
83
+ "total_degree" => 16.4422
84
+ },
85
+ "house_system" => "P",
86
+ "planets" => [
87
+ {
88
+ "planet" => "太陽",
89
+ "zodiac" => "摩羯座",
90
+ "house" => 9,
91
+ "degree" => 10.4744,
92
+ "total_degree" => 280.4744,
93
+ "retrograde" => false,
94
+ "aspects" => [
95
+ { "planet" => "土星", "aspect_type" => "合相", "orb" => 5.14 }
96
+ ]
97
+ },
98
+ # ... 月亮, 水星, 金星, 火星, 木星, 土星, 天王星, 海王星, 冥王星,
99
+ # 北交點, 南交點, 福點, 莉莉絲,
100
+ # 北交點定位星, 南交點定位星, 上升星座定位星 (17 entries)
101
+ ],
102
+ "houses" => [
103
+ { "house_number" => 1, "degree" => 16.4422, "zodiac" => "牡羊座" },
104
+ # ... 2-12
105
+ ],
106
+ "patterns" => [
107
+ { "pattern_type" => "T三角", "planets" => ["太陽", "月亮", "火星"],
108
+ "apex" => "火星" }
109
+ # 大三角 (with "element"), T三角/上帝之指/風箏 (with "apex"), 大十字,
110
+ # 神祕矩形, 星群 (with "zodiac")
111
+ ],
112
+ "element_stats" => {
113
+ "elements" => { "火" => 1, "土" => 5, "風" => 2, "水" => 2 },
114
+ "modalities" => { "基本" => 6, "固定" => 3, "變動" => 1 }
115
+ }
116
+ }
117
+ }
118
+ ```
119
+
120
+ ### Planets Included
121
+
122
+ 太陽, 月亮, 水星, 金星, 火星, 木星, 土星, 天王星, 海王星, 冥王星, 北交點(真交點), 南交點
123
+
124
+ Plus derived points 福點 (Part of Fortune) and 莉莉絲 (mean Black Moon
125
+ Lilith), and three ruler points: 北交點定位星, 南交點定位星, 上升星座定位星
126
+ — 17 entries total, in that order.
127
+
128
+ Every entry carries `"retrograde"` (central-difference daily motion;
129
+ 太陽/月亮 and derived/ruler points are always `false`, 南交點 mirrors
130
+ 北交點).
131
+
132
+ ### House Systems
133
+
134
+ Four systems, via `Chart.new(..., house_system: ...)`:
135
+
136
+ | Code | System | Notes |
137
+ |------|--------|-------|
138
+ | `"P"` | Placidus (default) | time-based; undefined inside the polar circle (raises) |
139
+ | `"W"` | Whole Sign | cusps on sign boundaries from the ascendant's sign |
140
+ | `"E"` | Equal | cusp 1 = ASC, then +30°; the true MC is still reported |
141
+ | `"O"` | Porphyry | quadrant trisection between the four angles |
142
+
143
+ Any other value raises `ArgumentError`. `W`/`E`/`O` are pure ASC/MC geometry
144
+ and are defined at every latitude |lat| < 90° — including polar latitudes,
145
+ where Placidus is undefined. (`Ephemeris.houses` also accepts the ord aliases
146
+ `80`/`87`/`69`/`79`.)
147
+
148
+ ### Aspects
149
+
150
+ Major: 合相 (0°, orb 15°), 六分相 (60°, orb 6°), 四分相 (90°, orb 8°), 三分相 (120°, orb 8°), 對分相 (180°, orb 10°).
151
+
152
+ Minor (opt-in via `Aspects.calculate(a, b, minor: true)`): 十二分相 (30°), 半四分相 (45°), 補八分相 (135°), 補十二分相/quincunx (150°) — tighter orbs, and they never shadow a major aspect.
153
+
154
+ ### Synastry (合盤)
155
+
156
+ ```ruby
157
+ result = AstroChart::Synastry.between(chart_a, chart_b, orb_limit: 6.0)
158
+
159
+ result["aspects"]
160
+ #=> [{ "a_planet" => "太陽", "b_planet" => "月亮",
161
+ # "aspect_type" => "三分相", "orb" => 1.23 }, ...] # sorted by orb
162
+
163
+ result["a_planets_in_b_houses"] #=> { "太陽" => 7, ... } (house overlay)
164
+ result["b_planets_in_a_houses"] #=> { "月亮" => 12, ... }
165
+ ```
166
+
167
+ Lower-level: `Synastry.cross_aspects(positions_a, positions_b)` and
168
+ `Synastry.house_overlay(positions, cusps)` work on raw longitude hashes.
169
+
170
+ ### Transits (行運)
171
+
172
+ ```ruby
173
+ jd = AstroChart::TimeConversion.to_julian_day("2026-07-24", "12:00", "Asia/Taipei")
174
+
175
+ AstroChart::Transits.at(jd)
176
+ #=> { "太陽" => 121.9, ... } # sky snapshot, 12 bodies
177
+
178
+ result = AstroChart::Transits.against(natal_chart, jd, orb_limit: 3.0)
179
+ result["planets"] # 12 transiting bodies with "natal_house"
180
+ result["aspects"] # [{ "transit_planet" => "土星", "natal_planet" => "太陽",
181
+ # "aspect_type" => "四分相", "orb" => 1.23 }, ...] sorted by orb
182
+ ```
183
+
184
+ ### Secondary Progressions (二次推運)
185
+
186
+ ```ruby
187
+ result = AstroChart::Progressions.secondary(natal_chart, "2026-07-24", orb_limit: 1.0)
188
+ result["progressed_jd"] # day-for-a-year progressed Julian Day
189
+ result["years_elapsed"] # e.g. 36.56
190
+ result["planets"] # progressed positions + natal house placement
191
+ result["aspects_to_natal"] # [{ "progressed_planet" => ..., "natal_planet" => ... }]
192
+ ```
193
+
194
+ ### Composite Charts (組合盤)
195
+
196
+ ```ruby
197
+ result = AstroChart::Composite.between(chart_a, chart_b)
198
+ result["planets"] # shorter-arc midpoints of the 12 bodies
199
+ result["aspects"] # aspects among the composite positions, sorted by orb
200
+ ```
201
+
202
+ (No composite houses — midpoint houses are not well-defined.)
203
+
204
+ ### Solar Returns (太陽回歸)
205
+
206
+ ```ruby
207
+ result = AstroChart::SolarReturn.for_year(natal_chart, 2026)
208
+ result["return_jd"] # exact JD (UT) the Sun returns to its natal longitude
209
+ result["return_time_utc"] # "2026-07-03T05:12:34Z"
210
+ result["chart"] # full chart at that instant (relocate with
211
+ # latitude:/longitude:/timezone: overrides)
212
+ ```
213
+
214
+ ### Transit Timing (行運精確時點)
215
+
216
+ The exact UTC instants, within a date range, when a transiting body perfects
217
+ an aspect to a natal point — the "when" that `Transits.against` (a snapshot)
218
+ can't give:
219
+
220
+ ```ruby
221
+ events = AstroChart::TransitTiming.events(natal_chart, "2026-01-01", "2026-12-31")
222
+ events.first
223
+ #=> { "transit_planet" => "土星", "natal_planet" => "太陽",
224
+ # "aspect_type" => "四分相", "jd" => 2461...,
225
+ # "time_utc" => "2026-03-14T07:22:10Z", "transit_zodiac" => "牡羊座",
226
+ # "retrograde" => false }
227
+ ```
228
+
229
+ Found by bracket-and-bisect on the ephemeris, so it is robust through
230
+ retrograde stations — all three passes of an outer-planet contact are caught.
231
+ Options: `minor:` (also time the minor aspects), `step_days:` (sampling
232
+ stride, default 1.0), `bodies:` (restrict transiting bodies, e.g. drop `"月亮"`
233
+ to avoid the Moon's monthly hits). Events are sorted by time.
234
+
235
+ ### Solar Arc Directions (太陽弧推運)
236
+
237
+ ```ruby
238
+ result = AstroChart::SolarArc.directions(natal_chart, "2026-07-24")
239
+ result["arc"] # degrees the chart is directed (~1° per year of life)
240
+ result["planets"] # every natal point advanced rigidly by the arc
241
+ result["aspects_to_natal"] # directed→natal aspects, sorted by orb
242
+ ```
243
+
244
+ The arc is the secondary-progressed Sun's travel since birth; unlike secondary
245
+ progressions, the whole chart moves at that single rate.
246
+
247
+ ### Lunar Returns (月亮回歸)
248
+
249
+ ```ruby
250
+ result = AstroChart::LunarReturn.for_date(natal_chart, "2026-07-24")
251
+ result["return_jd"] # JD (UT) of the lunar return nearest the date
252
+ result["return_time_utc"] # "2026-07-31T09:54:22Z"
253
+ result["chart"] # full chart at that instant (relocatable)
254
+ ```
255
+
256
+ The monthly (~27.32-day) analogue of the solar return.
257
+
258
+ ### Draconic Charts (龍盤)
259
+
260
+ ```ruby
261
+ positions = AstroChart::Planets.calculate_positions(jd) # or any { name => longitude }
262
+ AstroChart::Draconic.positions(positions, north_node) # shift so 北交點 = 0° 牡羊
263
+ AstroChart::Draconic.chart(positions, north_node)
264
+ #=> { "planets" => [{ "planet" =>, "zodiac" =>, "degree" =>, "total_degree" => }...],
265
+ # "aspects" => [...] } # draconic signs + inter-aspects
266
+ ```
267
+
268
+ ### Essential Dignities (必然尊貴)
269
+
270
+ Traditional 廟/旺/三分性/界/外觀 with the traditional rulerships, Dorothean
271
+ triplicities, and both Egyptian and Ptolemaic terms. Pure lookup tables.
272
+
273
+ ```ruby
274
+ D = AstroChart::Dignities
275
+
276
+ D.of("火星", 5.0) # 火星 at 5° 牡羊
277
+ #=> { "planet" => "火星", "dignities" => ["廟", "外觀"],
278
+ # "debilities" => [], "score" => 6 }
279
+
280
+ D.almuten(5.0, sect: :day) #=> { "planet" => "太陽", "score" => 7, "tied" => ["太陽"] }
281
+
282
+ D.term_ruler(13.0, scheme: :ptolemaic) #=> "金星" (:egyptian is the default)
283
+ D.domicile_ruler(215) #=> "火星" D.triplicity_ruler(5, sect: :night) #=> "木星"
284
+ D.face_ruler(120) #=> "土星" D.exaltation_ruler(5) #=> "太陽"
285
+ ```
286
+
287
+ Dignities score +5/+4/+3/+2/+1 (廟/旺/三分性/界/外觀); debilities 陷 (detriment),
288
+ 弱 (fall). Rulers are the seven traditional planets (dignity theory predates
289
+ the outer planets), so these differ from `Zodiac.ruler`'s modern rulerships.
290
+
291
+ ### Annual Profection (小限法)
292
+
293
+ ```ruby
294
+ AstroChart::Profection.annual(ascendant_longitude, 36)
295
+ #=> { "age" => 36, "profected_house" => 1, "profected_sign" => "牡羊座",
296
+ # "year_lord" => "火星" }
297
+
298
+ AstroChart::Profection.at(ascendant_longitude, "1990-01-01", "2026-08-05")
299
+ # derives the age from the two dates, then profects
300
+ ```
301
+
302
+ Each completed year advances one whole sign from the ascendant; the
303
+ traditional ruler of the profected sign is the Lord of the Year (年主星).
304
+
305
+ ### Patterns & Element Stats
306
+
307
+ Included in `Chart#generate` output, or usable standalone on any
308
+ `{ name => longitude }` hash:
309
+
310
+ ```ruby
311
+ AstroChart::Patterns.detect(positions)
312
+ #=> [{ "pattern_type" => "大三角", "planets" => [...], "element" => "火" },
313
+ # { "pattern_type" => "T三角", "planets" => [...], "apex" => "火星" },
314
+ # { "pattern_type" => "大十字", "planets" => [...] },
315
+ # { "pattern_type" => "上帝之指", "planets" => [...], "apex" => "水星" }, # Yod
316
+ # { "pattern_type" => "風箏", "planets" => [...], "apex" => "月亮" }, # Kite
317
+ # { "pattern_type" => "神祕矩形", "planets" => [...] }, # Mystic Rectangle
318
+ # { "pattern_type" => "星群", "planets" => [...], "zodiac" => "牡羊座" }] # Stellium
319
+
320
+ AstroChart::Stats.elements(positions)
321
+ #=> { "elements" => { "火" => 3, "土" => 2, "風" => 3, "水" => 2 },
322
+ # "modalities" => { "基本" => 4, "固定" => 3, "變動" => 3 } }
323
+ ```
324
+
325
+ ### Derived Points
326
+
327
+ ```ruby
328
+ AstroChart::Points.lilith(jd) #=> 216.47 (mean Black Moon Lilith, 0-360)
329
+ AstroChart::Points.fortune(asc: asc, sun: sun, moon: moon, day_chart: true)
330
+ AstroChart::Points.day_chart?(sun_longitude, cusps) #=> true / false / nil
331
+ AstroChart::Points.day_chart_from_horizon?(sun_longitude, ascendant)
332
+ # Chart#generate determines 福點 sect from the horizon (ASC-DSC axis),
333
+ # so the same birth instant yields the same 福點 under "P" and "W".
334
+
335
+ # 映點 / 反映點 (antiscia / contra-antiscia)
336
+ AstroChart::Points.antiscion(15.0) #=> 165.0 (across the 巨蟹–摩羯 axis)
337
+ AstroChart::Points.contra_antiscion(15.0) #=> 345.0 (across the 牡羊–天秤 axis)
338
+ ```
339
+
340
+ ### Individual Modules
341
+
342
+ ```ruby
343
+ # Zodiac sign from ecliptic longitude
344
+ AstroChart::Zodiac.sign_name(280.5) #=> "摩羯座"
345
+ AstroChart::Zodiac.ruler("摩羯座") #=> "土星"
346
+
347
+ # Aspect between two positions
348
+ AstroChart::Aspects.calculate(0, 90) #=> ["四分相", 0.0]
349
+
350
+ # Julian Day conversion
351
+ jd = AstroChart::TimeConversion.to_julian_day("1990-01-01", "12:00", "Asia/Taipei")
352
+
353
+ # Raw ephemeris access (planet ids follow the SE convention)
354
+ AstroChart::Ephemeris.julday(2000, 1, 1, 12.0)
355
+ AstroChart::Ephemeris.calc_ut(jd, AstroChart::Ephemeris::PLANETS["太陽"])
356
+ AstroChart::Ephemeris.houses(jd, 25.033, 121.565) # Placidus
357
+ AstroChart::Ephemeris.houses(jd, 25.033, 121.565, "W") # Whole Sign
358
+ AstroChart::Ephemeris.houses(jd, 25.033, 121.565, "E") # Equal
359
+ AstroChart::Ephemeris.houses(jd, 25.033, 121.565, "O") # Porphyry
360
+
361
+ # Daily motion / retrograde detection
362
+ AstroChart::Ephemeris.speed(jd, 2) #=> degrees/day (negative = retrograde)
363
+ AstroChart::Ephemeris.retrograde?(jd, 2) #=> true / false
364
+ ```
365
+
366
+ ## Backends
367
+
368
+ The default backend is `:pure` (pure Ruby, always available). The legacy
369
+ Swiss Ephemeris C extension backend can still be selected **if you compile
370
+ and provide the extension yourself** — it is no longer shipped with this gem
371
+ (it is AGPL-licensed, see 0.1.x):
372
+
373
+ ```ruby
374
+ AstroChart.backend #=> :pure
375
+ AstroChart.backend = :swiss # raises LoadError unless the extension is present
376
+ ```
377
+
378
+ ## Accuracy & Limits
379
+
380
+ - Verified against Swiss Ephemeris (Moshier), 1900–2026, 400 samples per
381
+ body: Sun ≤ 0.0002°, Moon ≤ 0.0013°, planets ≤ 0.0006°, Pluto ≤ 0.0005°,
382
+ true node ≤ 0.014°, house cusps ≤ 0.0003°. Zero zodiac sign flips.
383
+ - 莉莉絲 (mean Black Moon Lilith) uses the Meeus mean lunar perigee
384
+ polynomial + 180°; verified ≤ 0.13° vs Swiss Ephemeris across 1900–2050.
385
+ - Whole-sign cusps verified identical to Swiss Ephemeris, including inside
386
+ the polar circle. Retrograde flags verified against Swiss central-difference
387
+ speeds (Mercury 2020–2024, every sample matches).
388
+ - Transit / progression / solar-return positions verified within 0.003° of
389
+ Swiss Ephemeris at sampled instants (1962–2033).
390
+ - Pluto series is valid 1885–2099 (raises outside this range).
391
+ - **House systems: Placidus (`"P"`), Whole Sign (`"W"`), Equal (`"E"`) and
392
+ Porphyry (`"O"`)** on the pure backend. Other systems raise `ArgumentError`.
393
+ Polar latitudes (|lat| ≳ 66.5°) raise `AstroChart::Pure::Core::DomainError`
394
+ on Placidus — it is undefined there; use whole-sign, equal or Porphyry for
395
+ polar charts. Porphyry shares the four angles with Placidus and trisects
396
+ each quadrant; verified against those invariants.
397
+
398
+ ## Geocoding
399
+
400
+ This gem does **not** handle geocoding. Pass latitude, longitude, and
401
+ timezone directly. For city-to-coordinate conversion, use the
402
+ [geocoder](https://github.com/alexreisner/geocoder) gem or your own lookup
403
+ table.
404
+
405
+ ## Development
406
+
407
+ ```bash
408
+ bundle install
409
+ rake spec
410
+ ```
411
+
412
+ The hosted web app and MCP server that build on this gem live in a
413
+ separate repository and depend on `astro_chart` as a normal gem.
414
+
415
+ ## License
416
+
417
+ MIT
data/astro_chart.gemspec CHANGED
@@ -4,16 +4,20 @@ Gem::Specification.new do |spec|
4
4
  spec.name = "astro_chart"
5
5
  spec.version = AstroChart::VERSION
6
6
  spec.authors = ["Huang Yudi"]
7
- spec.summary = "Pure-Ruby natal chart calculation (planets, Placidus houses, aspects, synastry)"
8
- spec.description = "Natal astrology chart calculation in pure Ruby: apparent planetary longitudes " \
9
- "(VSOP87D, ELP-2000/82B moon, Meeus Pluto), Placidus houses, aspects and synastry. " \
7
+ spec.summary = "Pure-Ruby astrology chart calculation (planets, Placidus/whole-sign houses, " \
8
+ "aspects, synastry, transits, progressions, composite, solar returns)"
9
+ spec.description = "Astrology chart calculation in pure Ruby: apparent planetary longitudes " \
10
+ "(VSOP87D, ELP-2000/82B moon, Meeus Pluto), Placidus and whole-sign houses, " \
11
+ "retrograde flags, aspects, derived points (Part of Fortune, mean Lilith), " \
12
+ "aspect-pattern detection and element statistics, plus synastry, transits, " \
13
+ "secondary progressions, composite charts and solar returns. " \
10
14
  "No C extension, no external data files. Implemented from public formulas " \
11
15
  "(Meeus, Astronomical Algorithms 2nd ed.)."
12
16
  spec.homepage = "https://github.com/morriedig/astro_chart"
13
17
  spec.license = "MIT"
14
18
  spec.required_ruby_version = ">= 3.0"
15
19
 
16
- spec.files = Dir["lib/**/*.rb", "LICENSE", "CHANGELOG.md", "astro_chart.gemspec"]
20
+ spec.files = Dir["lib/**/*.rb", "README.md", "LICENSE", "CHANGELOG.md", "astro_chart.gemspec"]
17
21
  spec.require_paths = ["lib"]
18
22
 
19
23
  spec.add_dependency "tzinfo", "~> 2.0"
@@ -1,6 +1,6 @@
1
1
  module AstroChart
2
2
  module Aspects
3
- # Major aspects: angle => [name, max_orb]
3
+ # Major aspects (托勒密相位): angle => [name, max_orb]
4
4
  MAJOR = {
5
5
  60 => ["六分相", 6], # Sextile
6
6
  90 => ["四分相", 8], # Square
@@ -8,11 +8,31 @@ module AstroChart
8
8
  180 => ["對分相", 10], # Opposition
9
9
  }.freeze
10
10
 
11
+ # Minor aspects (次要相位): angle => [name, max_orb]. Opt-in via
12
+ # `minor: true` — they carry tighter orbs and are checked only after the
13
+ # major set, so no minor aspect can shadow a major one (the angle bands
14
+ # do not overlap given these orbs).
15
+ #
16
+ # 30° 十二分相 (semi-sextile)
17
+ # 45° 半四分相 (semi-square)
18
+ # 135° 補八分相 (sesquiquadrate)
19
+ # 150° 補十二分相 (quincunx / inconjunct)
20
+ MINOR = {
21
+ 30 => ["十二分相", 2],
22
+ 45 => ["半四分相", 2],
23
+ 135 => ["補八分相", 2],
24
+ 150 => ["補十二分相", 3],
25
+ }.freeze
26
+
11
27
  CONJUNCTION_ORB = 15
12
28
 
13
29
  # Calculate the aspect between two ecliptic positions.
30
+ #
31
+ # By default only conjunction + the four major aspects are considered
32
+ # (backward-compatible). Pass `minor: true` to also match the minor set.
33
+ #
14
34
  # Returns [aspect_name, orb] or [nil, nil].
15
- def self.calculate(pos1, pos2)
35
+ def self.calculate(pos1, pos2, minor: false)
16
36
  return [nil, nil] if pos1.nil? || pos2.nil?
17
37
 
18
38
  p1 = pos1 % 360.0
@@ -34,6 +54,15 @@ module AstroChart
34
54
  end
35
55
  end
36
56
 
57
+ if minor
58
+ MINOR.each do |angle, (name, max_orb)|
59
+ orb = (diff - angle).abs
60
+ if orb <= max_orb
61
+ return [name, orb.round(2)]
62
+ end
63
+ end
64
+ end
65
+
37
66
  [nil, nil]
38
67
  end
39
68
  end