Astrology Engine

Locate a crossing interval

We do not yet know the event time, but we can look for two nearby times that lie on opposite sides of the target. Such an interval is a bracket. For a direction on a circle, finding it requires more care than checking whether ordinary subtraction changes sign.

Before this lesson

Read “Define the event to find” and “Measure angular motion.” We reuse the shortest signed angular difference. The examples are synthetic and describe the coarse scan; backward Sun searches can try a seed before reaching this fallback.

Read the preceding lesson.

What you will learn

You will calculate signed differences, reject the opposite-angle discontinuity, and order a daily scan bracket for refinement.

Dotted-underlined terms open a definition beside the text. Select one to read more, then close it to continue.

Each step has its own check. Pass every step to complete the lesson. Your answers and checked steps are saved in this browser, so you can continue after leaving or reloading.

1. Recognize which sign change crosses the target

Suppose our target is 0°. A sample of 359° is one degree behind it and a sample of 1° is one degree ahead. Their shortest signed differences from the target are −1° and +1°. If the intervening fitted direction moves continuously through the target, there is a zero difference between the samples. Finding that zero turns the event question into a root-finding problem.

difference(d) = signed_delta(sample(d), target), in [−180°, 180°)

For finite angles, the helper removes complete turns and selects the shortest signed separation, with a half-turn tie of −180°. Near the opposite direction, however, this representation jumps: 179° gives +179°, while 181° gives −179°. The signs differ even though the path passes 180°, not our target at 0°. This is a discontinuity in the difference representation rather than a target crossing.

The engine requires both different signs and a sum of absolute differences strictly below 180°. For −1° and +1°, the sum is 2° and the pair passes. For +179° and −179°, the sum is 358° and the pair fails. Equality at 180° also fails. The absolute value removes the minus sign when measuring magnitude; it does not change the signed value used for comparison.

real_crossing = signs differ AND |previous difference| + |current difference| < 180°

This test is a local safeguard, not a general proof for arbitrary sampling callbacks. Its interpretation needs continuous, finite samples and sufficiently resolved motion. Two endpoints cannot reveal extra turns, two crossings between samples, or a touch of the target without a sign change. The public API searches the fitted Sun; the generic internal helper is not a universal event detector.

Root intervals and the circular complication

The National Institute of Standards and Technology describes bisection of intervals containing a zero. The current engine adapts that numerical idea to wrapped angular differences with its strict 180° safeguard. The source documents this local predicate; no historical inventor is assigned to its exact threshold or expression.

See the teaching TypeScript
const wrap = (value: number): number => { const remainder = value % 360; return remainder < 0 ? remainder + 360 : remainder; };
const signedDelta = (angle: number, target: number): number => wrap(wrap(angle - target) + 540) - 180;
const rustSign = (value: number): number => Number.isNaN(value) ? NaN : value < 0 || Object.is(value, -0) ? -1 : 1;
const isRealCrossing = (previous: number, current: number): boolean => rustSign(previous) !== rustSign(current) && Math.abs(previous) + Math.abs(current) < 180;

Finite, correctly labeled inputs are assumed. This demonstrates the arithmetic; it does not fetch data or replace the engine.

Connect this step to the source

src/astro/find_moment.rs

signed_delta; is_real_crossing

Paths refer to the astrology-engine repository. Examples use invented inputs; a successful exercise is not an astronomical-accuracy test.

Sources for this section

Apply this step

Answer every part, then check. You can retry as often as you like.

Enter a number without units; tolerance ±0.00000001. Accepted tolerance: ±1e-8 degrees. Omit units and commas.

2. Differences change from +179° to −179°. Does the predicate accept this pair?
3. Differences are −90° and +90°. Does the pair pass?
4. A synthetic curve touches zero between two positive daily samples. Must this scan detect the touch?

2. Walk through time and retain the right interval

The coarse scan samples offset zero first. If its difference is exactly zero, it immediately returns offset zero. Otherwise it samples +1, +2, …, +730 days for forward searches, or −1, −2, …, −730 for backward searches. Each new sample is compared with the immediately preceding one. The first pair accepted by the predicate is sent to bisection; exhausting the scan returns None.

Take a forward toy model whose difference is d − 2.25 degrees, where d is the day offset. At offsets 0, 1 and 2, the differences are −2.25°, −1.25° and −0.25°. At offset 3 the difference is +0.75°. The accepted pair therefore brackets the event in [2, 3] days. The scan has located an interval, not yet the fractional day 2.25.

For a backward toy model with difference d + 2.25 degrees, offsets 0, −1 and −2 are positive; offset −3 is negative. The pair arrives in decreasing time order, but the solver rearranges it to lo = −3 and hi = −2 days. It must also carry the negative difference at −3 as lo_diff. Bisection assumes lo < hi, whatever direction produced the bracket.

forward offsets: 0, 1, …, 730 days; backward offsets: 0, −1, …, −730 days

Endpoint zeros after the start are handled through the crossing predicate rather than a separate exact-zero return. Rust’s floating-point signum treats +0 as +1 and −0 as −1; JavaScript Math.sign instead returns signed zero. The teaching snippet above reproduces the Rust convention explicitly. Exact zero at a bisection midpoint has its own early return, explained next.

The one-day cadence and 730-day bound are implementation policies. With no early exit, the coarse scan samples 731 times including the start. Missing samples are carried as NaN; pairs containing them fail the magnitude comparison. A later finite pair can still qualify, but no result is guaranteed. The backward seed branch can perform additional samples before this scan.

Connect this step to the source

src/astro/find_moment.rs

coarse_scan

Paths refer to the astrology-engine repository. Examples use invented inputs; a successful exercise is not an astronomical-accuracy test.

Sources for this section

Apply this step

Answer every part, then check. You can retry as often as you like.

Enter a number without units; tolerance ±0.00000001. Accepted tolerance: ±1e-8 days. Omit units and commas.

Enter a number without units; tolerance ±0.00000001. Accepted tolerance: ±1e-8 days. Omit units and commas.

Enter a number without units; tolerance ±0.00000001. Accepted tolerance: ±1e-8 samples. Omit units and commas.

4. The initial coarse-scan sample exactly equals the target. What is returned?

Your lesson checks

0 of 2 steps passed.

Use the feedback beside each check to retry any unfinished step.