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 +4 -4
- data/CHANGELOG.md +97 -0
- data/README.md +417 -0
- data/astro_chart.gemspec +8 -4
- data/lib/astro_chart/aspects.rb +31 -2
- data/lib/astro_chart/chart.rb +94 -12
- data/lib/astro_chart/composite.rb +101 -0
- data/lib/astro_chart/dignities.rb +220 -0
- data/lib/astro_chart/draconic.rb +64 -0
- data/lib/astro_chart/ephemeris.rb +78 -29
- data/lib/astro_chart/houses.rb +2 -1
- data/lib/astro_chart/lunar_return.rb +90 -0
- data/lib/astro_chart/patterns.rb +248 -0
- data/lib/astro_chart/points.rb +82 -0
- data/lib/astro_chart/profection.rb +56 -0
- data/lib/astro_chart/progressions.rb +68 -0
- data/lib/astro_chart/pure/houses.rb +91 -6
- data/lib/astro_chart/pure.rb +11 -6
- data/lib/astro_chart/solar_arc.rb +59 -0
- data/lib/astro_chart/solar_return.rb +193 -0
- data/lib/astro_chart/stats.rb +34 -0
- data/lib/astro_chart/transit_timing.rb +186 -0
- data/lib/astro_chart/transits.rb +108 -0
- data/lib/astro_chart/version.rb +1 -1
- data/lib/astro_chart.rb +13 -0
- metadata +24 -7
data/lib/astro_chart/chart.rb
CHANGED
|
@@ -1,17 +1,35 @@
|
|
|
1
1
|
module AstroChart
|
|
2
2
|
class Chart
|
|
3
|
-
|
|
3
|
+
# Supported house systems: "P" (Placidus) / "W" (Whole Sign) /
|
|
4
|
+
# "E" (Equal, from the ascendant) / "O" (Porphyry).
|
|
5
|
+
HOUSE_SYSTEMS = ["P", "W", "E", "O"].freeze
|
|
6
|
+
|
|
7
|
+
# Bodies that can never be retrograde in geocentric longitude.
|
|
8
|
+
NEVER_RETROGRADE = ["太陽", "月亮"].freeze
|
|
9
|
+
|
|
10
|
+
attr_reader :birth_date, :birth_time, :latitude, :longitude, :timezone,
|
|
11
|
+
:house_system
|
|
4
12
|
|
|
5
13
|
# birth_date: "YYYY-MM-DD"
|
|
6
14
|
# birth_time: "HH:MM"
|
|
7
15
|
# latitude / longitude: Float
|
|
8
16
|
# timezone: IANA timezone string (e.g. "Asia/Taipei")
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
# house_system: "P" (Placidus, default), "W" (Whole Sign),
|
|
18
|
+
# "E" (Equal, from the ascendant) or "O" (Porphyry)
|
|
19
|
+
def initialize(birth_date:, birth_time:, latitude:, longitude:, timezone:,
|
|
20
|
+
house_system: "P")
|
|
21
|
+
unless HOUSE_SYSTEMS.include?(house_system)
|
|
22
|
+
raise ArgumentError,
|
|
23
|
+
"unknown house system #{house_system.inspect} " \
|
|
24
|
+
"(expected \"P\", \"W\", \"E\" or \"O\")"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
@birth_date = birth_date
|
|
28
|
+
@birth_time = birth_time
|
|
29
|
+
@latitude = latitude.to_f
|
|
30
|
+
@longitude = longitude.to_f
|
|
31
|
+
@timezone = timezone
|
|
32
|
+
@house_system = house_system
|
|
15
33
|
end
|
|
16
34
|
|
|
17
35
|
# Generate complete chart data.
|
|
@@ -19,12 +37,17 @@ module AstroChart
|
|
|
19
37
|
def generate
|
|
20
38
|
jd = TimeConversion.to_julian_day(birth_date, birth_time, timezone)
|
|
21
39
|
|
|
22
|
-
cusps, ascendant = Houses.calculate(jd, latitude, longitude)
|
|
40
|
+
cusps, ascendant = Houses.calculate(jd, latitude, longitude, system: house_system)
|
|
23
41
|
asc_zodiac = Zodiac.sign_name(ascendant)
|
|
24
42
|
|
|
25
43
|
positions = Planets.calculate_positions(jd)
|
|
26
44
|
planet_details = Planets.build_details(positions, cusps)
|
|
27
45
|
|
|
46
|
+
# Retrograde flags for the 12 physical bodies (太陽/月亮 hard-false,
|
|
47
|
+
# 南交點 mirrors 北交點 — they share the same axis).
|
|
48
|
+
retro = retrograde_flags(jd)
|
|
49
|
+
planet_details.each { |p| p["retrograde"] = retro.fetch(p["planet"], false) }
|
|
50
|
+
|
|
28
51
|
kp = Planets.key_points_data(positions, cusps, ascendant)
|
|
29
52
|
|
|
30
53
|
# Merge aspects into planet details
|
|
@@ -42,8 +65,12 @@ module AstroChart
|
|
|
42
65
|
planet["aspects"] = kp[key] if key
|
|
43
66
|
end
|
|
44
67
|
|
|
45
|
-
#
|
|
46
|
-
planet_details.concat(
|
|
68
|
+
# Derived points (福點 / 莉莉絲), placed before the ruler points
|
|
69
|
+
planet_details.concat(derived_points(jd, positions, cusps, ascendant))
|
|
70
|
+
|
|
71
|
+
# Append ruler points (定位星 carry no retrograde motion of their own)
|
|
72
|
+
ruler_points = kp["additional_points"].map { |p| p.merge("retrograde" => false) }
|
|
73
|
+
planet_details.concat(ruler_points)
|
|
47
74
|
|
|
48
75
|
houses_data = cusps.each_with_index.map do |deg, i|
|
|
49
76
|
{
|
|
@@ -53,6 +80,10 @@ module AstroChart
|
|
|
53
80
|
}
|
|
54
81
|
end
|
|
55
82
|
|
|
83
|
+
# 12-body positions feed pattern detection; the 10 classical planets
|
|
84
|
+
# (nodes excluded) feed the element/modality statistics.
|
|
85
|
+
classical = positions.reject { |name, _| name == "北交點" || name == "南交點" }
|
|
86
|
+
|
|
56
87
|
{
|
|
57
88
|
"input" => {
|
|
58
89
|
"birth_date" => birth_date,
|
|
@@ -69,10 +100,61 @@ module AstroChart
|
|
|
69
100
|
"degree" => (ascendant % 30).round(4),
|
|
70
101
|
"total_degree" => ascendant.round(4),
|
|
71
102
|
},
|
|
72
|
-
"
|
|
73
|
-
"
|
|
103
|
+
"house_system" => house_system,
|
|
104
|
+
"planets" => planet_details,
|
|
105
|
+
"houses" => houses_data,
|
|
106
|
+
"patterns" => Patterns.detect(positions),
|
|
107
|
+
"element_stats" => Stats.elements(classical),
|
|
74
108
|
},
|
|
75
109
|
}
|
|
76
110
|
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
# { "太陽" => false, ..., "南交點" => bool } — retrograde flags per body.
|
|
115
|
+
def retrograde_flags(jd)
|
|
116
|
+
flags = {}
|
|
117
|
+
Ephemeris::PLANETS.each do |name, planet_id|
|
|
118
|
+
flags[name] = if NEVER_RETROGRADE.include?(name)
|
|
119
|
+
false
|
|
120
|
+
else
|
|
121
|
+
Ephemeris.retrograde?(jd, planet_id)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
flags["南交點"] = flags["北交點"]
|
|
125
|
+
flags
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# 福點 (Part of Fortune) + 莉莉絲 (mean Black Moon Lilith) entries,
|
|
129
|
+
# shaped like regular planet entries. Both are hard-false retrograde:
|
|
130
|
+
# 福點 is a derived point with no motion of its own; 莉莉絲 is the mean
|
|
131
|
+
# lunar apogee, which always moves prograde.
|
|
132
|
+
def derived_points(jd, positions, cusps, ascendant)
|
|
133
|
+
# Sect (day/night) is an astronomical fact defined by the Sun relative
|
|
134
|
+
# to the ASC-DSC horizon axis, so it is judged from the horizon itself
|
|
135
|
+
# — never from the selected display house system's cusps (whole-sign
|
|
136
|
+
# cusps sit on sign boundaries, not on the horizon).
|
|
137
|
+
day_chart = Points.day_chart_from_horizon?(positions["太陽"], ascendant)
|
|
138
|
+
fortune = Points.fortune(
|
|
139
|
+
asc: ascendant,
|
|
140
|
+
sun: positions["太陽"],
|
|
141
|
+
moon: positions["月亮"],
|
|
142
|
+
day_chart: !!day_chart
|
|
143
|
+
)
|
|
144
|
+
lilith = Points.lilith(jd)
|
|
145
|
+
|
|
146
|
+
[point_entry("福點", fortune, cusps), point_entry("莉莉絲", lilith, cusps)]
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def point_entry(name, pos, cusps)
|
|
150
|
+
{
|
|
151
|
+
"planet" => name,
|
|
152
|
+
"zodiac" => Zodiac.sign_name(pos),
|
|
153
|
+
"house" => Houses.find_house(pos, cusps),
|
|
154
|
+
"degree" => (pos % 30).round(4),
|
|
155
|
+
"total_degree" => pos.round(4),
|
|
156
|
+
"retrograde" => false,
|
|
157
|
+
}
|
|
158
|
+
end
|
|
77
159
|
end
|
|
78
160
|
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
module AstroChart
|
|
2
|
+
# Composite chart (組合盤): the midpoint chart of two natal charts.
|
|
3
|
+
#
|
|
4
|
+
# For every body present in BOTH charts, the composite position is the
|
|
5
|
+
# shorter-arc midpoint of the two natal longitudes. Aspects are then
|
|
6
|
+
# computed among the composite positions themselves.
|
|
7
|
+
#
|
|
8
|
+
# Backend-independent: works purely on Chart#generate result hashes,
|
|
9
|
+
# no ephemeris call happens here.
|
|
10
|
+
#
|
|
11
|
+
# result = Composite.between(chart_a, chart_b)
|
|
12
|
+
# result["planets"] # 12 個組合盤中點位置
|
|
13
|
+
# result["aspects"] # 組合盤位置彼此之間的相位(依 orb 排序)
|
|
14
|
+
#
|
|
15
|
+
# Houses are intentionally out of scope: midpoint-composite house systems
|
|
16
|
+
# are convention-contested (midpoint of cusps vs. recomputed for a
|
|
17
|
+
# reference location/time), so no single answer is returned here.
|
|
18
|
+
module Composite
|
|
19
|
+
# The 12 bodies considered: the 11 ephemeris bodies + 南交點.
|
|
20
|
+
BODIES = (Ephemeris::PLANETS.keys + ["南交點"]).freeze
|
|
21
|
+
|
|
22
|
+
# Full composite between two Chart#generate result hashes.
|
|
23
|
+
#
|
|
24
|
+
# Returns:
|
|
25
|
+
# {
|
|
26
|
+
# "planets" => [{ "planet", "zodiac", "degree", "total_degree" }],
|
|
27
|
+
# "aspects" => [{ "planet_a", "planet_b", "aspect_type", "orb" }],
|
|
28
|
+
# }
|
|
29
|
+
def self.between(chart_a, chart_b)
|
|
30
|
+
pos_a = positions_from_chart(chart_a)
|
|
31
|
+
pos_b = positions_from_chart(chart_b)
|
|
32
|
+
|
|
33
|
+
composite_positions = {}
|
|
34
|
+
BODIES.each do |name|
|
|
35
|
+
a = pos_a[name]
|
|
36
|
+
b = pos_b[name]
|
|
37
|
+
next if a.nil? || b.nil?
|
|
38
|
+
|
|
39
|
+
composite_positions[name] = midpoint(a, b)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
{
|
|
43
|
+
"planets" => build_planets(composite_positions),
|
|
44
|
+
"aspects" => build_aspects(composite_positions),
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Shorter-arc midpoint of two ecliptic longitudes (degrees, [0, 360)).
|
|
49
|
+
# 350° & 10° => 0° (not 180°).
|
|
50
|
+
def self.midpoint(a, b)
|
|
51
|
+
(a + (((b - a + 540.0) % 360.0) - 180.0) / 2.0) % 360.0
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Extract { name => total_degree } for BODIES from a Chart#generate hash.
|
|
55
|
+
# Ruler points appended by key_points_data are ignored (not in BODIES).
|
|
56
|
+
def self.positions_from_chart(chart)
|
|
57
|
+
planets = chart&.dig("chart", "planets")
|
|
58
|
+
raise ArgumentError, "chart has no planets data" if planets.nil? || planets.empty?
|
|
59
|
+
|
|
60
|
+
planets.each_with_object({}) do |p, out|
|
|
61
|
+
name = p["planet"]
|
|
62
|
+
next unless BODIES.include?(name)
|
|
63
|
+
|
|
64
|
+
degree = p["total_degree"]
|
|
65
|
+
out[name] = degree if degree
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.build_planets(positions)
|
|
70
|
+
positions.map do |name, pos|
|
|
71
|
+
{
|
|
72
|
+
"planet" => name,
|
|
73
|
+
"zodiac" => Zodiac.sign_name(pos),
|
|
74
|
+
"degree" => (pos % 30).round(4),
|
|
75
|
+
"total_degree" => pos.round(4),
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.build_aspects(positions)
|
|
81
|
+
names = positions.keys
|
|
82
|
+
results = []
|
|
83
|
+
|
|
84
|
+
names.combination(2) do |name_a, name_b|
|
|
85
|
+
aspect_type, orb = Aspects.calculate(positions[name_a], positions[name_b])
|
|
86
|
+
next if aspect_type.nil?
|
|
87
|
+
|
|
88
|
+
results << {
|
|
89
|
+
"planet_a" => name_a,
|
|
90
|
+
"planet_b" => name_b,
|
|
91
|
+
"aspect_type" => aspect_type,
|
|
92
|
+
"orb" => orb,
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
results.sort_by { |r| r["orb"] }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private_class_method :build_planets, :build_aspects
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
require_relative "zodiac"
|
|
2
|
+
|
|
3
|
+
module AstroChart
|
|
4
|
+
# Essential dignities (必然尊貴) — the classical Hellenistic / medieval
|
|
5
|
+
# scheme of five dignities and their planetary rulers, plus the two major
|
|
6
|
+
# debilities. Pure lookup tables; no ephemeris, fully deterministic.
|
|
7
|
+
#
|
|
8
|
+
# Rulers are the SEVEN traditional planets (太陽 月亮 水星 金星 火星 木星 土星).
|
|
9
|
+
# These use the TRADITIONAL rulerships (天蠍→火星, 水瓶→土星, 雙魚→木星), which
|
|
10
|
+
# differ from the modern rulers in Zodiac.ruler (冥王星/天王星/海王星). Dignity
|
|
11
|
+
# theory predates the outer planets, so it is defined on the traditional set.
|
|
12
|
+
#
|
|
13
|
+
# The five dignities and their scores (Lilly / Ptolemaic weighting):
|
|
14
|
+
# 廟 domicile +5 planet rules the sign
|
|
15
|
+
# 旺 exaltation +4 planet is exalted in the sign (with an exact degree)
|
|
16
|
+
# 三分性 triplicity +3 planet rules the sign's element by sect
|
|
17
|
+
# 界 term/bound +2 planet rules the bound the degree falls in
|
|
18
|
+
# 外觀 face/decan +1 planet rules the 10° face the degree falls in
|
|
19
|
+
# Debilities: 陷 detriment (−5, opposite domicile), 弱 fall (−4, opposite
|
|
20
|
+
# exaltation).
|
|
21
|
+
#
|
|
22
|
+
# Terms: two schemes are bundled — 埃及界 (Egyptian, the default and most
|
|
23
|
+
# widely used) and 托勒密界 (Ptolemaic). Both are cross-validated against
|
|
24
|
+
# Astrolog's tables; the Egyptian table additionally matches several
|
|
25
|
+
# independent references degree-for-degree.
|
|
26
|
+
module Dignities
|
|
27
|
+
PLANETS = ["太陽", "月亮", "水星", "金星", "火星", "木星", "土星"].freeze
|
|
28
|
+
|
|
29
|
+
# 傳統守護星 (traditional domicile rulers), by sign.
|
|
30
|
+
DOMICILE = {
|
|
31
|
+
"牡羊座" => "火星", "金牛座" => "金星", "雙子座" => "水星", "巨蟹座" => "月亮",
|
|
32
|
+
"獅子座" => "太陽", "處女座" => "水星", "天秤座" => "金星", "天蠍座" => "火星",
|
|
33
|
+
"射手座" => "木星", "摩羯座" => "土星", "水瓶座" => "土星", "雙魚座" => "木星",
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
# 旺 (exaltation): sign => [planet, exact degree]. Signs without an
|
|
37
|
+
# exaltation ruler are absent.
|
|
38
|
+
EXALTATION = {
|
|
39
|
+
"牡羊座" => ["太陽", 19], "金牛座" => ["月亮", 3], "巨蟹座" => ["木星", 15],
|
|
40
|
+
"處女座" => ["水星", 15], "天秤座" => ["土星", 21], "摩羯座" => ["火星", 28],
|
|
41
|
+
"雙魚座" => ["金星", 27],
|
|
42
|
+
}.freeze
|
|
43
|
+
|
|
44
|
+
# 三分性 (triplicity), Dorothean scheme: element => day/night/participating
|
|
45
|
+
# rulers. Elements repeat every 4 signs from 牡羊 (火).
|
|
46
|
+
ELEMENTS = ["火", "土", "風", "水"].freeze
|
|
47
|
+
TRIPLICITY = {
|
|
48
|
+
"火" => { day: "太陽", night: "木星", participating: "土星" },
|
|
49
|
+
"土" => { day: "金星", night: "月亮", participating: "火星" },
|
|
50
|
+
"風" => { day: "土星", night: "水星", participating: "木星" },
|
|
51
|
+
"水" => { day: "金星", night: "火星", participating: "月亮" },
|
|
52
|
+
}.freeze
|
|
53
|
+
|
|
54
|
+
# 埃及界 (Egyptian terms): sign => [[ruler, ending_degree], ...5], the
|
|
55
|
+
# ending degree measured within the sign (0-30). Verified against multiple
|
|
56
|
+
# references.
|
|
57
|
+
EGYPTIAN_TERMS = {
|
|
58
|
+
"牡羊座" => [["木星", 6], ["金星", 12], ["水星", 20], ["火星", 25], ["土星", 30]],
|
|
59
|
+
"金牛座" => [["金星", 8], ["水星", 14], ["木星", 22], ["土星", 27], ["火星", 30]],
|
|
60
|
+
"雙子座" => [["水星", 6], ["木星", 12], ["金星", 17], ["火星", 24], ["土星", 30]],
|
|
61
|
+
"巨蟹座" => [["火星", 7], ["金星", 13], ["水星", 19], ["木星", 26], ["土星", 30]],
|
|
62
|
+
"獅子座" => [["木星", 6], ["金星", 11], ["土星", 18], ["水星", 24], ["火星", 30]],
|
|
63
|
+
"處女座" => [["水星", 7], ["金星", 17], ["木星", 21], ["火星", 28], ["土星", 30]],
|
|
64
|
+
"天秤座" => [["土星", 6], ["水星", 14], ["木星", 21], ["金星", 28], ["火星", 30]],
|
|
65
|
+
"天蠍座" => [["火星", 7], ["金星", 11], ["水星", 19], ["木星", 24], ["土星", 30]],
|
|
66
|
+
"射手座" => [["木星", 12], ["金星", 17], ["水星", 21], ["土星", 26], ["火星", 30]],
|
|
67
|
+
"摩羯座" => [["水星", 7], ["木星", 14], ["金星", 22], ["土星", 26], ["火星", 30]],
|
|
68
|
+
"水瓶座" => [["水星", 7], ["金星", 13], ["木星", 20], ["火星", 25], ["土星", 30]],
|
|
69
|
+
"雙魚座" => [["金星", 12], ["木星", 16], ["水星", 19], ["火星", 28], ["土星", 30]],
|
|
70
|
+
}.freeze
|
|
71
|
+
|
|
72
|
+
# 托勒密界 (Ptolemaic terms): Ptolemy's alternative bounds from the
|
|
73
|
+
# Tetrabiblos. Decoded from Astrolog's rgnTermPtolemy table (the same
|
|
74
|
+
# decoding reproduces Astrolog's Egyptian table identically to EGYPTIAN_TERMS
|
|
75
|
+
# above, which validates it). Resolves the classically disputed cells as
|
|
76
|
+
# 獅子 first term = 土星 and 天蠍 first term = 火星.
|
|
77
|
+
PTOLEMAIC_TERMS = {
|
|
78
|
+
"牡羊座" => [["木星", 6], ["金星", 14], ["水星", 21], ["火星", 26], ["土星", 30]],
|
|
79
|
+
"金牛座" => [["金星", 8], ["水星", 15], ["木星", 22], ["土星", 26], ["火星", 30]],
|
|
80
|
+
"雙子座" => [["水星", 7], ["木星", 14], ["金星", 21], ["土星", 25], ["火星", 30]],
|
|
81
|
+
"巨蟹座" => [["火星", 6], ["木星", 13], ["水星", 20], ["金星", 27], ["土星", 30]],
|
|
82
|
+
"獅子座" => [["土星", 6], ["水星", 13], ["金星", 19], ["木星", 25], ["火星", 30]],
|
|
83
|
+
"處女座" => [["水星", 7], ["金星", 13], ["木星", 18], ["土星", 24], ["火星", 30]],
|
|
84
|
+
"天秤座" => [["土星", 6], ["金星", 11], ["木星", 19], ["水星", 24], ["火星", 30]],
|
|
85
|
+
"天蠍座" => [["火星", 6], ["木星", 14], ["金星", 21], ["水星", 27], ["土星", 30]],
|
|
86
|
+
"射手座" => [["木星", 8], ["金星", 14], ["水星", 19], ["土星", 25], ["火星", 30]],
|
|
87
|
+
"摩羯座" => [["金星", 6], ["水星", 12], ["木星", 19], ["火星", 25], ["土星", 30]],
|
|
88
|
+
"水瓶座" => [["土星", 6], ["水星", 12], ["金星", 20], ["木星", 25], ["火星", 30]],
|
|
89
|
+
"雙魚座" => [["金星", 8], ["木星", 14], ["水星", 20], ["火星", 26], ["土星", 30]],
|
|
90
|
+
}.freeze
|
|
91
|
+
|
|
92
|
+
TERM_SCHEMES = { egyptian: EGYPTIAN_TERMS, ptolemaic: PTOLEMAIC_TERMS }.freeze
|
|
93
|
+
|
|
94
|
+
# 外觀 (faces / decans), Chaldean order: the 36 faces cycle the seven
|
|
95
|
+
# planets in Chaldean sequence (土木火日金水月) starting at 火星 for 牡羊 0°.
|
|
96
|
+
# Generated rather than tabulated — face n = CHALDEAN[(⌊lon/10⌋) mod 7].
|
|
97
|
+
CHALDEAN_FROM_MARS = ["火星", "太陽", "金星", "水星", "月亮", "土星", "木星"].freeze
|
|
98
|
+
|
|
99
|
+
DIGNITY_SCORES = { domicile: 5, exaltation: 4, triplicity: 3, term: 2, face: 1 }.freeze
|
|
100
|
+
|
|
101
|
+
module_function
|
|
102
|
+
|
|
103
|
+
def sign_of(longitude)
|
|
104
|
+
Zodiac.sign_name(longitude)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def element_of(longitude)
|
|
108
|
+
ELEMENTS[((longitude % 360).floor / 30) % 4]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Degree within the current sign (0-30).
|
|
112
|
+
def degree_in_sign(longitude)
|
|
113
|
+
(longitude % 360.0) % 30.0
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# +5 ruler of the sign (traditional).
|
|
117
|
+
def domicile_ruler(longitude)
|
|
118
|
+
DOMICILE[sign_of(longitude)]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# −5 planet in detriment here = ruler of the opposite sign.
|
|
122
|
+
def detriment_ruler(longitude)
|
|
123
|
+
DOMICILE[sign_of(longitude + 180.0)]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# +4 exalted planet in this sign, or nil.
|
|
127
|
+
def exaltation_ruler(longitude)
|
|
128
|
+
entry = EXALTATION[sign_of(longitude)]
|
|
129
|
+
entry && entry[0]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# −4 planet in fall here = the planet exalted in the opposite sign, or nil.
|
|
133
|
+
def fall_ruler(longitude)
|
|
134
|
+
entry = EXALTATION[sign_of(longitude + 180.0)]
|
|
135
|
+
entry && entry[0]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Triplicity rulers for the sign's element.
|
|
139
|
+
# Returns { day:, night:, participating: }.
|
|
140
|
+
def triplicity_rulers(longitude)
|
|
141
|
+
TRIPLICITY[element_of(longitude)]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# +3 triplicity ruler active for the given sect (:day / :night).
|
|
145
|
+
def triplicity_ruler(longitude, sect: :day)
|
|
146
|
+
trip = triplicity_rulers(longitude)
|
|
147
|
+
sect == :night ? trip[:night] : trip[:day]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# +2 term/bound ruler at this degree (Egyptian by default).
|
|
151
|
+
def term_ruler(longitude, scheme: :egyptian)
|
|
152
|
+
table = TERM_SCHEMES[scheme]
|
|
153
|
+
unless table
|
|
154
|
+
raise ArgumentError,
|
|
155
|
+
"unknown term scheme #{scheme.inspect} (available: #{TERM_SCHEMES.keys.inspect})"
|
|
156
|
+
end
|
|
157
|
+
deg = degree_in_sign(longitude)
|
|
158
|
+
table[sign_of(longitude)].each do |ruler, ending|
|
|
159
|
+
return ruler if deg < ending
|
|
160
|
+
end
|
|
161
|
+
table[sign_of(longitude)].last[0] # exactly 30.0 → last term
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# +1 face/decan ruler at this degree (Chaldean order).
|
|
165
|
+
def face_ruler(longitude)
|
|
166
|
+
CHALDEAN_FROM_MARS[((longitude % 360).floor / 10) % 7]
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Total essential-dignity score a planet holds at a longitude (sum of the
|
|
170
|
+
# positive dignities it rules). Debilities are reported separately by .of
|
|
171
|
+
# and are not subtracted here.
|
|
172
|
+
def score(planet, longitude, sect: :day, terms: :egyptian)
|
|
173
|
+
total = 0
|
|
174
|
+
total += DIGNITY_SCORES[:domicile] if domicile_ruler(longitude) == planet
|
|
175
|
+
total += DIGNITY_SCORES[:exaltation] if exaltation_ruler(longitude) == planet
|
|
176
|
+
total += DIGNITY_SCORES[:triplicity] if triplicity_ruler(longitude, sect: sect) == planet
|
|
177
|
+
total += DIGNITY_SCORES[:term] if term_ruler(longitude, scheme: terms) == planet
|
|
178
|
+
total += DIGNITY_SCORES[:face] if face_ruler(longitude) == planet
|
|
179
|
+
total
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Full dignity/debility report for a planet at a longitude.
|
|
183
|
+
#
|
|
184
|
+
# { "dignities" => ["廟", "界"...], "debilities" => ["陷"...],
|
|
185
|
+
# "score" => 7 }
|
|
186
|
+
#
|
|
187
|
+
# Exaltation/fall are placement dignities of the sign (the exact degree is
|
|
188
|
+
# the peak but the whole sign confers the dignity), matching Lilly's table.
|
|
189
|
+
def of(planet, longitude, sect: :day, terms: :egyptian)
|
|
190
|
+
dignities = []
|
|
191
|
+
dignities << "廟" if domicile_ruler(longitude) == planet
|
|
192
|
+
dignities << "旺" if exaltation_ruler(longitude) == planet
|
|
193
|
+
dignities << "三分性" if triplicity_ruler(longitude, sect: sect) == planet
|
|
194
|
+
dignities << "界" if term_ruler(longitude, scheme: terms) == planet
|
|
195
|
+
dignities << "外觀" if face_ruler(longitude) == planet
|
|
196
|
+
|
|
197
|
+
debilities = []
|
|
198
|
+
debilities << "陷" if detriment_ruler(longitude) == planet
|
|
199
|
+
debilities << "弱" if fall_ruler(longitude) == planet
|
|
200
|
+
|
|
201
|
+
{
|
|
202
|
+
"planet" => planet,
|
|
203
|
+
"dignities" => dignities,
|
|
204
|
+
"debilities" => debilities,
|
|
205
|
+
"score" => score(planet, longitude, sect: sect, terms: terms),
|
|
206
|
+
}
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Almuten of a degree (界主星/度數勝利星): the traditional planet with the
|
|
210
|
+
# highest total dignity score at that longitude. Returns
|
|
211
|
+
# { "planet" => "火星", "score" => 7, "tied" => ["火星", ...] }
|
|
212
|
+
# `tied` lists every planet sharing the top score (usually one).
|
|
213
|
+
def almuten(longitude, sect: :day, terms: :egyptian)
|
|
214
|
+
scored = PLANETS.map { |p| [p, score(p, longitude, sect: sect, terms: terms)] }
|
|
215
|
+
top = scored.map { |_p, s| s }.max
|
|
216
|
+
winners = scored.select { |_p, s| s == top }.map { |p, _s| p }
|
|
217
|
+
{ "planet" => winners.first, "score" => top, "tied" => winners }
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
require_relative "zodiac"
|
|
2
|
+
require_relative "aspects"
|
|
3
|
+
|
|
4
|
+
module AstroChart
|
|
5
|
+
# Draconic chart (龍盤): the tropical zodiac rotated so the natal True North
|
|
6
|
+
# Node sits at 0° 牡羊. Every ecliptic longitude is shifted by the node's
|
|
7
|
+
# longitude, which re-signs the whole chart around the lunar nodal axis.
|
|
8
|
+
# Widely read as the "soul level" overlay of the ordinary tropical chart.
|
|
9
|
+
#
|
|
10
|
+
# draconic_longitude = (L − 北交點) mod 360
|
|
11
|
+
#
|
|
12
|
+
# Pure composition: this only transforms longitudes. Feed it the raw
|
|
13
|
+
# positions hash from `Planets.calculate_positions` (or any { name => L }
|
|
14
|
+
# hash) together with the natal True North Node longitude.
|
|
15
|
+
module Draconic
|
|
16
|
+
# positions: { "太陽" => 123.45, ... } tropical ecliptic longitudes
|
|
17
|
+
# north_node: True North Node longitude (degrees, 0-360)
|
|
18
|
+
#
|
|
19
|
+
# Returns { name => draconic_longitude } with 北交點 mapped to 0.0.
|
|
20
|
+
def self.positions(positions, north_node)
|
|
21
|
+
positions.transform_values { |lon| (lon - north_node) % 360.0 }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Full draconic view: draconic longitude, zodiac sign and in-sign degree
|
|
25
|
+
# for every body, plus the aspects among them (major aspects only, via
|
|
26
|
+
# Aspects.calculate — draconic-to-draconic contacts are identical to the
|
|
27
|
+
# tropical ones, but the signs and houses they fall in differ).
|
|
28
|
+
#
|
|
29
|
+
# Returns:
|
|
30
|
+
# { "planets" => [ { "planet" =>, "zodiac" =>, "degree" =>,
|
|
31
|
+
# "total_degree" => }, ... ],
|
|
32
|
+
# "aspects" => [ { "a_planet" =>, "b_planet" =>,
|
|
33
|
+
# "aspect_type" =>, "orb" => }, ... ] } # sorted by orb
|
|
34
|
+
def self.chart(positions, north_node)
|
|
35
|
+
draconic = self.positions(positions, north_node)
|
|
36
|
+
|
|
37
|
+
planets = draconic.map do |name, lon|
|
|
38
|
+
{
|
|
39
|
+
"planet" => name,
|
|
40
|
+
"zodiac" => Zodiac.sign_name(lon),
|
|
41
|
+
"degree" => (lon % 30).round(4),
|
|
42
|
+
"total_degree" => lon.round(4),
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
names = draconic.keys
|
|
47
|
+
aspects = []
|
|
48
|
+
names.combination(2) do |a, b|
|
|
49
|
+
type, orb = Aspects.calculate(draconic[a], draconic[b])
|
|
50
|
+
next if type.nil?
|
|
51
|
+
|
|
52
|
+
aspects << {
|
|
53
|
+
"a_planet" => a,
|
|
54
|
+
"b_planet" => b,
|
|
55
|
+
"aspect_type" => type,
|
|
56
|
+
"orb" => orb,
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
aspects.sort_by! { |asp| asp["orb"] }
|
|
60
|
+
|
|
61
|
+
{ "planets" => planets, "aspects" => aspects }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|