Module: AstroChart::LunarReturn
- Defined in:
- lib/astro_chart/lunar_return.rb
Overview
Lunar return chart (月亮回歸盤): the chart cast for the exact UTC instant the transiting Moon returns to its natal longitude — a monthly (~27.32-day) cycle, the lunar analogue of the solar return.
natal = AstroChart::Chart.new(...).generate
result = AstroChart::LunarReturn.for_date(natal, "2026-07-24")
result["return_jd"] # Julian Day (UT) of the nearest return
result["return_time_utc"] # ISO8601 UTC
result["chart"] # full chart at the return instant
The instant is found by Newton iteration on the Moon's longitude starting from the target date, so it converges to the return nearest that date (within ~±½ cycle). Chart building and JD↔UTC reuse SolarReturn.
Defined Under Namespace
Classes: ConvergenceError
Constant Summary collapse
- MOON_ID =
Ephemeris::PLANETS["月亮"]
- CONVERGENCE_DEG =
1e-4- MAX_ITERATIONS =
more than the Sun: the Moon's speed varies ~11-15°/day
40
Class Method Summary collapse
-
.find_return_jd(target_deg, target_date) ⇒ Object
Newton iteration on the Moon's longitude from the target date.
-
.for_date(natal_chart, target_date, latitude: nil, longitude: nil, timezone: nil) ⇒ Object
natal_chart: a Chart#generate result hash.
-
.moon_speed(jd, step = 0.02) ⇒ Object
Moon's longitudinal speed (deg/day) via central difference (~13.2).
Class Method Details
.find_return_jd(target_deg, target_date) ⇒ Object
Newton iteration on the Moon's longitude from the target date.
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/astro_chart/lunar_return.rb', line 53 def self.find_return_jd(target_deg, target_date) y, m, d = target_date.split("-").map(&:to_i) raise ArgumentError, "invalid date: #{target_date.inspect}" if y.nil? || m.nil? || d.nil? jd = Ephemeris.julday(y, m, d, 0.0) MAX_ITERATIONS.times do delta = SolarReturn.angle_delta(target_deg - Ephemeris.calc_ut(jd, MOON_ID)) return jd if delta.abs < CONVERGENCE_DEG jd += delta / moon_speed(jd) end raise ConvergenceError, "lunar return did not converge within #{MAX_ITERATIONS} iterations " \ "(date=#{target_date}, target=#{target_deg})" end |
.for_date(natal_chart, target_date, latitude: nil, longitude: nil, timezone: nil) ⇒ Object
natal_chart: a Chart#generate result hash. target_date: "YYYY-MM-DD" — the returned instant is the lunar return nearest this date. Location defaults to the natal coordinates; pass latitude:/longitude: (and timezone:, informational) to relocate.
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
# File 'lib/astro_chart/lunar_return.rb', line 30 def self.for_date(natal_chart, target_date, latitude: nil, longitude: nil, timezone: nil) natal_moon = natal_moon_degree(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 jd = find_return_jd(natal_moon, target_date) { "return_jd" => jd, "return_time_utc" => SolarReturn.jd_to_utc_iso8601(jd), "location" => { "latitude" => lat.to_f, "longitude" => lng.to_f, "timezone" => tz }, "chart" => SolarReturn.build_chart_at(jd, lat.to_f, lng.to_f), } end |
.moon_speed(jd, step = 0.02) ⇒ Object
Moon's longitudinal speed (deg/day) via central difference (~13.2).
72 73 74 75 76 |
# File 'lib/astro_chart/lunar_return.rb', line 72 def self.moon_speed(jd, step = 0.02) SolarReturn.angle_delta( Ephemeris.calc_ut(jd + step, MOON_ID) - Ephemeris.calc_ut(jd - step, MOON_ID) ) / (2.0 * step) end |