Module: AstroChart::SolarReturn
- Defined in:
- lib/astro_chart/solar_return.rb
Overview
Solar return chart (太陽回歸盤): the chart cast for the exact UTC instant the transiting Sun returns to its natal longitude in a given year.
natal = AstroChart::Chart.new(...).generate
result = SolarReturn.for_year(natal, 2026)
result["return_jd"] # Julian Day (UT) of the return instant
result["return_time_utc"] # ISO8601 UTC string, e.g. "2026-07-03T05:12:34Z"
result["chart"] # full chart structure at the return instant
The return instant is found by Newton iteration on the Sun's longitude, then the chart is computed directly at that Julian Day (no lossy round-trip through date strings).
Defined Under Namespace
Classes: ConvergenceError
Constant Summary collapse
- SUN_ID =
Ephemeris::PLANETS["太陽"]
- CONVERGENCE_DEG =
Convergence threshold in degrees (~0.36 arcsec, i.e. under 10 seconds of clock time at the Sun's mean speed).
1e-4- MAX_ITERATIONS =
20- ASPECT_MAP =
Same planet => aspect-list wiring as Chart#generate.
{ "太陽" => "sun_aspects", "月亮" => "moon_aspects", "土星" => "saturn_aspects", "金星" => "venus_aspects", "北交點" => "north_node_aspects", "南交點" => "south_node_aspects", }.freeze
Class Method Summary collapse
-
.angle_delta(deg) ⇒ Object
Signed shortest angular difference, mapped into [-180, 180).
-
.build_chart_at(jd, latitude, longitude) ⇒ Object
Chart structure at an exact JD — mirrors the "chart" section of Chart#generate, computed directly with Planets/Houses (Chart itself only accepts date strings, which would lose sub-minute precision).
-
.find_return_jd(target_deg, year, month, day) ⇒ Object
Newton iteration: find the JD(UT) nearest the birthday in
yearwhere the Sun's longitude equals target_deg. -
.for_year(natal_chart, year, latitude: nil, longitude: nil, timezone: nil) ⇒ Object
Build the solar return for a natal chart (a Chart#generate hash) in the given year.
-
.jd_to_utc(jd) ⇒ Object
Inverse Julian Day (Meeus, Astronomical Algorithms ch. 7): JD(UT) -> [year, month, day, hour, minute, second] in UTC.
- .jd_to_utc_iso8601(jd) ⇒ Object
-
.sun_speed(jd, step = 0.05) ⇒ Object
Sun's longitudinal speed (deg/day) via central difference (~0.9856).
Class Method Details
.angle_delta(deg) ⇒ Object
Signed shortest angular difference, mapped into [-180, 180).
95 96 97 |
# File 'lib/astro_chart/solar_return.rb', line 95 def self.angle_delta(deg) (deg + 540.0) % 360.0 - 180.0 end |
.build_chart_at(jd, latitude, longitude) ⇒ Object
Chart structure at an exact JD — mirrors the "chart" section of Chart#generate, computed directly with Planets/Houses (Chart itself only accepts date strings, which would lose sub-minute precision).
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
# File 'lib/astro_chart/solar_return.rb', line 135 def self.build_chart_at(jd, latitude, longitude) cusps, ascendant = Houses.calculate(jd, latitude, longitude) positions = Planets.calculate_positions(jd) planet_details = Planets.build_details(positions, cusps) kp = Planets.key_points_data(positions, cusps, ascendant) planet_details.each do |planet| key = ASPECT_MAP[planet["planet"]] planet["aspects"] = kp[key] if key end planet_details.concat(kp["additional_points"]) houses_data = cusps.each_with_index.map do |deg, i| { "house_number" => i + 1, "degree" => deg.round(4), "zodiac" => Zodiac.sign_name(deg), } end { "ascendant" => { "zodiac" => Zodiac.sign_name(ascendant), "degree" => (ascendant % 30).round(4), "total_degree" => ascendant.round(4), }, "planets" => planet_details, "houses" => houses_data, } end |
.find_return_jd(target_deg, year, month, day) ⇒ Object
Newton iteration: find the JD(UT) nearest the birthday in year where
the Sun's longitude equals target_deg.
71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/astro_chart/solar_return.rb', line 71 def self.find_return_jd(target_deg, year, month, day) jd = Ephemeris.julday(year, month, day, 12.0) MAX_ITERATIONS.times do delta = angle_delta(target_deg - Ephemeris.calc_ut(jd, SUN_ID)) return jd if delta.abs < CONVERGENCE_DEG jd += delta / sun_speed(jd) end raise ConvergenceError, "solar return did not converge within #{MAX_ITERATIONS} iterations " \ "(year=#{year}, target=#{target_deg})" end |
.for_year(natal_chart, year, latitude: nil, longitude: nil, timezone: nil) ⇒ Object
Build the solar return for a natal chart (a Chart#generate hash) in the given year. Location defaults to the natal chart's coordinates/timezone; pass latitude:/longitude: (and timezone:, informational) to relocate.
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/astro_chart/solar_return.rb', line 39 def self.for_year(natal_chart, year, latitude: nil, longitude: nil, timezone: nil) natal_sun = natal_sun_degree(natal_chart) month, day = natal_month_day(natal_chart) input = natal_chart["input"] || {} coords = input["coordinates"] || {} lat = latitude || coords["latitude"] lng = longitude || coords["longitude"] tz = timezone || input["timezone"] if lat.nil? || lng.nil? raise ArgumentError, "no coordinates: natal chart input has none and none were given" end lat = lat.to_f lng = lng.to_f jd = find_return_jd(natal_sun, year, month, day) { "return_jd" => jd, "return_time_utc" => jd_to_utc_iso8601(jd), "location" => { "latitude" => lat, "longitude" => lng, "timezone" => tz, }, "chart" => build_chart_at(jd, lat, lng), } end |
.jd_to_utc(jd) ⇒ Object
Inverse Julian Day (Meeus, Astronomical Algorithms ch. 7): JD(UT) -> [year, month, day, hour, minute, second] in UTC. Rounded to the nearest whole second before decomposition, so 23:59:59.6 rolls over to 00:00:00 of the next day correctly.
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/astro_chart/solar_return.rb', line 103 def self.jd_to_utc(jd) total_seconds = ((jd + 0.5) * 86_400.0).round z = total_seconds / 86_400 sec = total_seconds % 86_400 if z < 2_299_161 # before the Gregorian reform (1582-10-15) a = z else alpha = ((z - 1_867_216.25) / 36_524.25).floor a = z + 1 + alpha - (alpha / 4) end b = a + 1524 c = ((b - 122.1) / 365.25).floor d = (365.25 * c).floor e = ((b - d) / 30.6001).floor day = b - d - (30.6001 * e).floor month = e < 14 ? e - 1 : e - 13 year = month > 2 ? c - 4716 : c - 4715 [year, month, day, sec / 3600, (sec % 3600) / 60, sec % 60] end |
.jd_to_utc_iso8601(jd) ⇒ Object
127 128 129 130 |
# File 'lib/astro_chart/solar_return.rb', line 127 def self.jd_to_utc_iso8601(jd) y, mo, d, h, mi, s = jd_to_utc(jd) format("%04d-%02d-%02dT%02d:%02d:%02dZ", y, mo, d, h, mi, s) end |
.sun_speed(jd, step = 0.05) ⇒ Object
Sun's longitudinal speed (deg/day) via central difference (~0.9856).
87 88 89 90 91 92 |
# File 'lib/astro_chart/solar_return.rb', line 87 def self.sun_speed(jd, step = 0.05) diff = angle_delta( Ephemeris.calc_ut(jd + step, SUN_ID) - Ephemeris.calc_ut(jd - step, SUN_ID) ) diff / (2.0 * step) end |