astro_chart 0.2.0 → 0.5.1

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: 373643f3dc987aeb880aad6a15a5dd6a509367683c7ee1463ce7086b3e06980f
4
+ data.tar.gz: ec28b6b542ae7aaaa03997e744718b62dcf6a69c4372e1fcdb5196cb87110e4e
5
5
  SHA512:
6
- metadata.gz: 81b2fd730e85708ecb9a9264bbfbd7bee4d6ea848c3554b12f4f98303a6f1f9432e7713e071191193035facb0a46f3c886efc1d7767c27711eb9384990766c6c
7
- data.tar.gz: bd6534cf3b77dcd81e9b800a343cc594a08e48bfbc8419024b897e59af9f0beef41c4c6d3bdc01d1011c7568659c8322c9b71ff3f2f1cefb8c2931eb4e17aca3
6
+ metadata.gz: e5a812f264e71b82bbe09489d7098254b29f0e7fb648c2eb84aa244091d7e62cebab9cfaa7420da59b44392227bc3dc1b110c1aaa6f6dbe02de60ab6a80e5a3a
7
+ data.tar.gz: 9c8e709366b24a5c28db55223c68edbfe1e705f9b9ec4d95f1e5dae1a6a72e6d8419597d900c80f832126b2bb53c930ddf636d4e7db93bc4ca7208346f8e8905
data/CHANGELOG.md CHANGED
@@ -1,5 +1,127 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.1 (2026-08-07)
4
+
5
+ **Ecliptic latitude → accurate astrocartography.** Additive.
6
+
7
+ - New `AstroChart::Ephemeris.ecliptic_latlon(jd, id)` — apparent geocentric
8
+ ecliptic [longitude, latitude] for the Sun, Moon, VSOP87 planets and Pluto
9
+ (the geocentric vector was already computed; latitude was simply discarded).
10
+ - `Astrocartography.lines` now forms RA/Dec from each body's **true** ecliptic
11
+ latitude instead of β = 0, so the Moon and Pluto lines (β up to ~5° / ~17°)
12
+ are placed correctly. The longitude methods delegate to the new latitude-
13
+ aware ones, so planetary longitudes remain byte-identical (Swiss oracle spec
14
+ still passes).
15
+
16
+ ## 0.5.0 (2026-08-07)
17
+
18
+ **Astrocartography (星象地圖).** All pure Ruby, additive.
19
+
20
+ - New `AstroChart::Astrocartography.lines(jd)` — the relocational map lines for
21
+ the ten bodies at a birth instant: the MC and IC meridian longitudes, and
22
+ the ASC (rising) and DSC (setting) horizon curves as latitude/longitude
23
+ segments (broken where a body is circumpolar). Built from the body's RA/Dec
24
+ and Greenwich apparent sidereal time; verified independently (body altitude
25
+ ≈ 0 along every ASC/DSC point, LST = RA on the MC). Ecliptic latitude is
26
+ approximated as 0 (exact for the Sun; largest error for the Moon/Pluto).
27
+
28
+ ## 0.4.0 (2026-08-06)
29
+
30
+ **Traditional dignities, transit timing, two more house systems, minor
31
+ aspects, four new configurations, antiscia, draconic charts, solar arc,
32
+ lunar & annual predictive tools.** All pure Ruby, additive and
33
+ backward-compatible.
34
+
35
+ - **Minor aspects**: `Aspects.calculate(a, b, minor: true)` also matches
36
+ 十二分相 (30°), 半四分相 (45°), 補八分相 (135°) and 補十二分相 (quincunx,
37
+ 150°). Default stays major-only; minor aspects carry tighter orbs and never
38
+ shadow a major one.
39
+ - **New aspect patterns** in `Patterns.detect` (and every `Chart#generate`
40
+ output): 上帝之指 (Yod: sextile base + quincunx apex), 風箏 (Kite: a 大三角
41
+ plus an opposing/sextiling focal body, reported alongside the trine), 神祕
42
+ 矩形 (Mystic Rectangle: two oppositions joined by sextiles and trines) and
43
+ 星群 (Stellium: 3+ bodies in one sign). Existing 大三角/T三角/大十字 output is
44
+ unchanged; the node-axis exclusion rules extend to the new configurations.
45
+ - **Antiscia**: `Points.antiscion` (reflection across the 巨蟹–摩羯 solstice
46
+ axis) and `Points.contra_antiscion` (across the 牡羊–天秤 equinox axis).
47
+ - **Draconic charts (龍盤)**: new `AstroChart::Draconic` — `.positions` shifts
48
+ every longitude so the natal 北交點 sits at 0° 牡羊; `.chart` adds draconic
49
+ signs, in-sign degrees and inter-aspects.
50
+ - **Two more house systems**: Equal (`"E"`, from the ascendant) and Porphyry
51
+ (`"O"`, quadrant trisection), joining Placidus (`"P"`) and Whole Sign
52
+ (`"W"`). Both are pure geometry off the ASC/MC and are defined at every
53
+ latitude |lat| < 90° (including inside the polar circle, where Placidus
54
+ raises). Ord aliases 69/79 mirror the C-extension int argument.
55
+ - **Essential dignities (必然尊貴)**: new `AstroChart::Dignities` — 廟/旺/三分性/
56
+ 界/外觀 with the traditional rulerships and Dorothean triplicities.
57
+ `.of(planet, longitude, sect:)` reports a planet's dignities/debilities and
58
+ Lilly-weighted score (+5/+4/+3/+2/+1); `.almuten(longitude, sect:)` returns
59
+ the winning planet at a degree; plus `.domicile_ruler`/`detriment_ruler`/
60
+ `exaltation_ruler`/`fall_ruler`/`triplicity_ruler`/`term_ruler`/`face_ruler`.
61
+ 界 ships both 埃及界 (Egyptian, `scheme: :egyptian`, the default) and 托勒密界
62
+ (Ptolemaic, `scheme: :ptolemaic`); both are cross-validated against Astrolog
63
+ and the Egyptian table matches several independent references.
64
+ - **Annual profection (小限法)**: new `AstroChart::Profection` — `.annual(asc,
65
+ age)` and `.at(asc, birth_date, target_date)` give the profected house/sign
66
+ and Lord of the Year (年主星).
67
+ - **Transit timing (行運精確時點)**: new `AstroChart::TransitTiming.events` —
68
+ the exact UTC instants in a date range when a transiting body perfects an
69
+ aspect to a natal point, found by bracket-and-bisect on the ephemeris
70
+ (robust through retrograde stations, so all three passes of an outer-planet
71
+ contact are caught). `minor:`, `step_days:` and `bodies:` options.
72
+ - **Solar arc directions (太陽弧推運)**: new `AstroChart::SolarArc.directions`
73
+ — directs the whole chart rigidly by the secondary-progressed Sun's arc and
74
+ reports directed-to-natal aspects.
75
+ - **Lunar returns (月亮回歸)**: new `AstroChart::LunarReturn.for_date` — the
76
+ chart at the Moon's monthly return to its natal longitude, relocatable like
77
+ the solar return.
78
+
79
+ ## 0.3.0 (2026-07-24)
80
+
81
+ **Predictive & comparison modules, whole-sign houses, retrograde flags,
82
+ derived points, pattern detection.** All pure Ruby, MIT.
83
+
84
+ - **Whole-sign houses**: `Chart.new(..., house_system: "W")` ("P" Placidus
85
+ remains the default; anything else raises `ArgumentError`). Whole-sign
86
+ works at all latitudes |lat| < 90° (including polar latitudes, where
87
+ Placidus still raises); Placidus output is byte-identical to 0.2.0.
88
+ `Ephemeris.houses` / `Pure.houses` accept `"W"` too.
89
+ - **Retrograde flags**: every planet entry in `Chart#generate` gains
90
+ `"retrograde" => true/false` (central-difference daily motion via the new
91
+ `Ephemeris.speed(jd, id)` / `Ephemeris.retrograde?(jd, id)`). 太陽/月亮
92
+ are hard-false, 南交點 mirrors 北交點, derived/ruler points are false.
93
+ Within half a day of a body's valid ephemeris window edge (e.g. the
94
+ Pluto series' 1885–2099 range) the speed stencil falls back to a
95
+ one-sided difference, so edge-of-range charts still generate.
96
+ - **Derived points** (`AstroChart::Points`): 福點 (Part of Fortune,
97
+ day/night formula; sect judged from the horizon — ASC–DSC axis — so it
98
+ is independent of the display house system) and 莉莉絲 (mean Black Moon
99
+ Lilith, Meeus mean lunar apogee + 180°, verified < 0.13° vs Swiss
100
+ Ephemeris across 1900–2050). Both appended to `Chart#generate` planets,
101
+ before the ruler points.
102
+ - **Transits** (`AstroChart::Transits`): `.at(jd)` sky snapshot;
103
+ `.against(natal, jd, orb_limit: 3.0)` places transiting bodies in natal
104
+ houses and lists transit-to-natal aspects sorted by orb.
105
+ - **Secondary progressions** (`AstroChart::Progressions`):
106
+ `.secondary(natal, "YYYY-MM-DD", orb_limit: 1.0)` — day-for-a-year
107
+ progressed positions, natal house placement, aspects to natal.
108
+ - **Composite charts** (`AstroChart::Composite`): `.between(a, b)` —
109
+ shorter-arc midpoint chart of two natal charts, with internal aspects.
110
+ - **Solar returns** (`AstroChart::SolarReturn`): `.for_year(natal, year,
111
+ latitude:, longitude:, timezone:)` — Newton-iterated exact return
112
+ instant, full relocated chart at that moment.
113
+ - **Aspect patterns** (`AstroChart::Patterns.detect`): 大三角 (with
114
+ element), T三角 (with apex), 大十字; node-axis oppositions excluded
115
+ from opposition legs; 大十字 subsumes its own T三角s; an opposition
116
+ squared by the node axis is reported as a single T三角 (apex 北交點)
117
+ rather than mirrored 北/南交點 twins. `Chart#generate` now includes
118
+ `"patterns"`.
119
+ - **Element statistics** (`AstroChart::Stats.elements`): 火土風水 element
120
+ and 基本固定變動 modality counts over the 10 classical planets.
121
+ `Chart#generate` now includes `"element_stats"` and `"house_system"`.
122
+ - `Houses.calculate` now takes the house system as a keyword
123
+ (`system: "P"`).
124
+
3
125
  ## 0.2.0 (2026-07-24)
4
126
 
5
127
  **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