DEV Community

Cover image for When 359 Becomes 2 : Testing Boundary Sensitivity in Circular Features with Python
AETHERCORE / Sora Attilas
AETHERCORE / Sora Attilas

Posted on

When 359 Becomes 2 : Testing Boundary Sensitivity in Circular Features with Python

Suppose a feature changes from 359.060 degrees to 2.794 degrees.

A naive subtraction reports -356.266. The actual forward movement is +3.734 degrees. If those values are converted into 30-degree categories, the label changes as well—even though the underlying motion is smooth.

This boundary problem appears in headings, clock time, seasonal phase, hue, and any feature defined on a circle. I encountered it while testing how a ten-minute birth-time shift propagates through an astrological calculation pipeline.

The astrology context supplies a useful real example. The engineering problem is broader: how should a system preserve continuous circular data, categorical labels, and uncertainty at the same time?

Measured change from the 12:00 baseline

The experiment

I held the synthetic date and public Tokyo coordinates constant and changed only local time.

date: 2000-01-01
timezone: Asia/Tokyo
latitude: 35.6762
longitude: 139.6503
times: 12:00, 12:10, 13:00, 14:00
sidereal mode: Lahiri
house system: Whole Sign
Enter fullscreen mode Exit fullscreen mode

No personal birth data are involved.

The current GenesisCore calculation path produced these values:

Local time Sun Moon Ascendant
12:00 256.130° 194.948° 359.060°
12:10 256.137° 195.032° 2.794°
13:00 256.172° 195.451° 20.340°
14:00 256.215° 195.954° 38.752°

The Sun moved about 0.085 degrees over two hours, the Moon about 1.006 degrees, and the Ascendant about 39.692 degrees around the circle.

Why ordinary subtraction fails

For values normalized to [0, 360), a forward angular difference can be calculated with modulo arithmetic:

def forward_delta(base: float, current: float) -> float:
    return (current - base) % 360.0


print(forward_delta(359.060, 2.794))
# 3.734
Enter fullscreen mode Exit fullscreen mode

If direction can be positive or negative, use the shortest signed distance:

def signed_circular_delta(base: float, current: float) -> float:
    return ((current - base + 180.0) % 360.0) - 180.0
Enter fullscreen mode Exit fullscreen mode

The right choice depends on the process. In this experiment, time only moves forward and the sampled Ascendant advances around the circle, so the forward delta is the relevant quantity.

Reproduce the source angles

The following function uses Swiss Ephemeris. It converts the local timestamp through an IANA timezone, calculates tropical Sun, Moon, and Ascendant values, and then applies Lahiri ayanāṃśa.

python -m pip install pyswisseph matplotlib
Enter fullscreen mode Exit fullscreen mode
from datetime import datetime, timezone
from zoneinfo import ZoneInfo

import swisseph as swe

LAT, LON = 35.6762, 139.6503
TZ = ZoneInfo("Asia/Tokyo")

swe.set_sid_mode(swe.SIDM_LAHIRI)


def sample(hour: int, minute: int) -> dict:
    local_dt = datetime(2000, 1, 1, hour, minute, tzinfo=TZ)
    utc_dt = local_dt.astimezone(timezone.utc)
    utc_hour = utc_dt.hour + utc_dt.minute / 60 + utc_dt.second / 3600
    jd = swe.julday(
        utc_dt.year, utc_dt.month, utc_dt.day, utc_hour, swe.GREG_CAL
    )

    sun = swe.calc_ut(jd, swe.SUN, swe.FLG_SPEED)[0][0]
    moon = swe.calc_ut(jd, swe.MOON, swe.FLG_SPEED)[0][0]
    ayanamsa = swe.get_ayanamsa(jd)
    _cusps, ascmc = swe.houses_ex(jd, LAT, LON, b"W", swe.FLG_SWIEPH)

    return {
        "minute": (hour - 12) * 60 + minute,
        "sun": (sun - ayanamsa) % 360.0,
        "moon": (moon - ayanamsa) % 360.0,
        "ascendant": (ascmc[0] - ayanamsa) % 360.0,
    }


rows = [sample(*time) for time in [(12, 0), (12, 10), (13, 0), (14, 0)]]
Enter fullscreen mode Exit fullscreen mode

Plot change, not wrapped position

Plotting the raw Ascendant values would draw a large downward line between 359 and 2 degrees. That line is a coordinate artifact. Convert each series to forward change from the baseline first.

import matplotlib.pyplot as plt


def forward_delta(base: float, current: float) -> float:
    return (current - base) % 360.0


minutes = [row["minute"] for row in rows]

for key, label in [
    ("sun", "Sun"),
    ("moon", "Moon"),
    ("ascendant", "Ascendant"),
]:
    baseline = rows[0][key]
    changes = [forward_delta(baseline, row[key]) for row in rows]
    plt.plot(minutes, changes, marker="o", label=label)

plt.xlabel("Minutes after 12:00")
plt.ylabel("Forward angular change (degrees)")
plt.title("Input-time sensitivity of calculated chart features")
plt.grid(alpha=0.25)
plt.legend()
plt.tight_layout()
plt.show()
Enter fullscreen mode Exit fullscreen mode

This representation preserves the process being measured. It also makes the different sensitivities immediately visible.

Feature engineering near a circular boundary

The example suggests four design rules.

Preserve the original angle

A category such as a zodiac sign or compass sector loses distance information. Store the normalized angle beside the label. If later analysis uses only categories, that should be an explicit modeling choice.

Add distance to the nearest boundary

For 30-degree segments:

def boundary_distance(angle: float, width: float = 30.0) -> float:
    offset = angle % width
    return min(offset, width - offset)
Enter fullscreen mode Exit fullscreen mode

This makes boundary sensitivity measurable. A label derived from an angle 0.1 degrees from a boundary should not carry the same stability assumption as one 15 degrees from it.

Consider sine/cosine encoding

Many statistical and machine-learning models will treat 359 and 2 as far apart. Circular encoding keeps neighboring angles close:

import math


def encode_angle(angle: float) -> tuple[float, float]:
    radians = math.radians(angle)
    return math.sin(radians), math.cos(radians)
Enter fullscreen mode Exit fullscreen mode

This does not solve every domain question, but it removes the artificial discontinuity at zero.

Propagate input uncertainty

When the timestamp is uncertain, sample the defensible time interval. A result can then be labeled:

  • stable throughout the interval;
  • continuous but boundary-sensitive;
  • category-changing;
  • unavailable because the input is insufficient.

A precise-looking category should not hide an imprecise timestamp.

The role of astrology in this example

An astronomical position can be reproducible while an astrological interpretation remains unverified. Keeping those layers separate is central to the design.

AETHERCORE develops GenesisCore with separate Western and Jyotish calculation paths. Shared birth records pass through different declared coordinate and rule systems. The public route-research project then asks whether frozen chart-derived features are associated with documented social classifications.

The circular-data lesson affects that research directly. A sign-membership feature, a degree-based feature, and a boundary-distance feature are three different hypotheses. Reproduction requires the code and configuration to say which one was tested.

Project and source links

The next useful test is to apply the same stability labels to every time-sensitive output and show them directly in the result UI.

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.