Module: AstroChart::TransitTiming

Defined in:
lib/astro_chart/transit_timing.rb

Overview

Transit timing (行運精確時點): the exact UTC instants, within a date range, when a transiting body forms an exact aspect to a natal point.

Where Transits.against is a single snapshot ("what aspects hold right now?"), this answers the question people actually ask — "when does transiting 土星 exactly square my natal 太陽?".

natal  = AstroChart::Chart.new(...).generate
events = AstroChart::TransitTiming.events(natal, "2026-01-01", "2026-12-31")
events.first
#=> { "transit_planet" => "土星", "natal_planet" => "太陽",
#     "aspect_type" => "四分相", "jd" => 2461...,
#     "time_utc" => "2026-03-14T07:22:10Z", "transit_zodiac" => "牡羊座",
#     "retrograde" => false }

Method: each transiting body's longitude is sampled across the range at step_days; for every natal point and aspect angle the signed separation is bracketed where it crosses exactness, then refined by bisection (robust through retrograde stations, unlike a pure Newton step). A step where the body moves < 90° guarantees no aliasing — 1 day is safe for all bodies (the Moon moves ~13°/day).

Constant Summary collapse

BODIES =

Transiting bodies considered (12-body set minus 南交點, mirroring Synastry/Transits — a south-node hit merely mirrors a north-node one).

Synastry::BODIES
MAJOR =

Major aspect angles. Each non-zero, non-opposition aspect is exact at two signed separations (applying from either side); 合相/對分相 have one.

{ "合相" => 0, "六分相" => 60, "四分相" => 90, "三分相" => 120, "對分相" => 180 }.freeze
MINOR =
{ "十二分相" => 30, "半四分相" => 45, "補八分相" => 135, "補十二分相" => 150 }.freeze
CONVERGENCE_DAYS =

~0.1 second of clock time

1e-6
MAX_BISECT =
60

Class Method Summary collapse

Class Method Details

.events(natal_chart, start_date, end_date, minor: false, step_days: 1.0, bodies: nil) ⇒ Object

natal_chart: a Chart#generate result hash. start_date / end_date: "YYYY-MM-DD" (interpreted at 00:00 UT). minor: also time the minor aspects (30/45/135/150). step_days: sampling stride (default 1.0 — safe for every body). bodies: restrict transiting bodies (default all BODIES; e.g. drop "月亮" to avoid the Moon's ~monthly hits flooding the list).

Returns an Array of event hashes sorted by time (jd ascending).



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/astro_chart/transit_timing.rb', line 50

def self.events(natal_chart, start_date, end_date, minor: false,
                step_days: 1.0, bodies: nil)
  natal = Synastry.positions_from_chart(natal_chart)
  moving = (bodies || BODIES).select { |b| Ephemeris::PLANETS.key?(b) }
  targets = signed_targets(minor)

  jd_start = date_to_jd(start_date)
  jd_end   = date_to_jd(end_date)

  events = []
  moving.each do |body|
    id = Ephemeris::PLANETS[body]
    # Sample this body's longitude ONCE across the grid, then reuse the
    # cached samples for every natal point × aspect target (calc_ut is the
    # cost; the scan phase does zero extra ephemeris calls, only bisection
    # refinement recomputes).
    grid = sample_grid(id, jd_start, jd_end, step_days)

    natal.each do |n_name, n_pos|
      targets.each do |aspect_type, sep|
        scan_grid(grid, n_pos, sep).each do |jd0, h0, jd1, h1|
          jd = bisect(id, n_pos, sep, jd0, h0, jd1, h1)
          events << build_event(body, id, n_name, aspect_type, jd)
        end
      end
    end
  end

  dedupe(events).sort_by { |e| e["jd"] }
end