-
-
Notifications
You must be signed in to change notification settings - Fork 2
PARTIAL_CLOSURE_ROADMAP
Date: 2026-04-04
Scope: All 46 partial rows in SWISS_EPHEMERIS_TO_MOIRA_DRAFT.md
Goal: Map each to a specific resolution action that closes it to mapped, stdlib, or unsupported.
This document does not propose refactoring. Each resolution is the smallest correct change.
| Code | Meaning |
|---|---|
CLOSE:mapped |
Code already exists. Row update + verification only. No new code. |
CLOSE:stdlib |
Python standard library handles this. No Moira surface needed. |
CLOSE:unsupported |
Intentionally outside Moira's architecture. Reclassify the row. |
THIN |
One function or wrapper of < 10 lines. |
VESSEL |
New typed result vessel + one public function. |
AUDIT |
Existing surface probably covers it. Read-only investigation to confirm and update the row. |
ECLIPSE |
Eclipse surface audit. Code likely exists; shape needs confirmation against Swiss output spec. |
DEFERRED |
Requires validation work, catalog audit, or multi-file scope. Not a quick win. |
These are intentional architecture decisions, not gaps. The rows should be reclassified.
| Swiss symbol | Resolution | Action |
|---|---|---|
set_topo |
CLOSE:unsupported |
Moira is stateless by design. Global observer state is explicitly rejected. Update row note: "per-call observer arguments are the Moira idiom; global state is unsupported.design." |
set_sid_mode |
CLOSE:unsupported |
Same doctrine. No global sidereal mode. Per-call ayanamsa(jd_ut, Ayanamsa.X) is the correct idiom. The note should say unsupported.design. |
| Swiss symbol | Resolution | Action |
|---|---|---|
day_of_week |
CLOSE:stdlib |
datetime.weekday() or calendar.day_name[...]. Update row. |
get_planet_name |
CLOSE:stdlib |
Body name strings are self-describing; Body.SUN == "Sun". No dedicated helper is needed. Update row. |
difdeg2n |
CLOSE:stdlib |
normalize_degrees(a - b) using the existing moira.coordinates.normalize_degrees. Add this as the mapped idiom in the row rather than stdlib. Change to mapped. |
deg_midp |
CLOSE:stdlib |
normalize_degrees((a + b) / 2) for same-hemisphere; normalize_degrees((a + b + 360) / 2) for cross-zero case. Idiomatic one-liner. Change to mapped with note. |
A verification call or read of the function signature is all that is needed before updating the row.
| Swiss symbol | Current partial reason | Moira surface | Verification needed |
|---|---|---|---|
utc_to_jd |
"exact helper name from draft not audited" |
moira.julian.jd_from_datetime(dt: datetime) -> float — exact match |
Confirm function exists in __all__. Change row to mapped. |
deltat_ex |
"no exact flag variant audited" |
DeltaTPolicy covers model selection and flag-equivalent behavior; delta_t_nasa_canon(year) provides the variant algorithm |
Confirm DeltaTPolicy admits the extended model. Change row to mapped. |
get_ayanamsa_ex_ut |
"no exact flag variant audited" |
moira.sidereal.ayanamsa(jd_ut, Ayanamsa.X) is the per-call equivalent; no flags needed |
Run help(ayanamsa). Change row to mapped. |
get_ayanamsa_name |
"no audited exact label helper" |
Ayanamsa.LAHIRI.name on the StrEnum returns "LAHIRI"; str(Ayanamsa.LAHIRI) returns the value |
Confirm Ayanamsa is a StrEnum. Change row to mapped. |
azalt |
"no audited one-call Swiss-style wrapper" |
moira.coordinates.equatorial_to_horizontal(ra_deg, dec_deg, lat_deg, jd_ut) -> tuple[float, float] — azimuth + altitude |
Confirm signature + return. Change row to mapped. |
sol_eclipse_when_glob |
"class method, not flat function" |
EclipseCalculator().next_solar_eclipse(jd_start) — searches globally by construction |
Confirm it does not require a location argument. Change row to mapped. |
lun_eclipse_when |
"class method" | EclipseCalculator().next_lunar_eclipse(jd_start) |
Same as above. Change row to mapped. |
sol_eclipse_how |
"closest current local-attribute surface" |
EclipseCalculator().solar_local_circumstances(jd, lat, lon) returns SolarEclipseLocalCircumstances including magnitude, obscuration, contact geometry |
Confirm SolarEclipseLocalCircumstances fields match Swiss sol_eclipse_how output attributes. |
lun_eclipse_how |
"shape differs" |
LunarEclipseAnalysis and lunar_local_circumstances() exist with umbral depth, penumbral fraction, contacts |
Read LunarEclipseAnalysis fields vs Swiss lun_eclipse_how output spec. |
mooncross_ut |
"exact target-longitude helper naming differs" |
next_transit(Body.MOON, target_lon, jd_start) in moira.transits — exact semantic match |
Confirm next_transit accepts Body.MOON and a target longitude. Change row to mapped. |
solcross_ut |
"exact target-longitude helper naming differs" |
next_transit(Body.SUN, target_lon, jd_start) in moira.transits
|
Same. Change row to mapped. |
heliacal_ut |
"star-oriented verified; planets not stated" | Planets: visibility_event(body, kind, jd_start, lat, lon) in moira.heliacal. Stars: heliacal_rising(star, jd_start, lat, lon) / heliacal_setting(...). Both are public. |
Update row note to name both surfaces. Change row to mapped. |
jdet_to_utc |
"shape differs" |
calendar_datetime_from_jd(jd) -> CalendarDateTime or datetime_from_jd(jd) -> datetime in moira.julian
|
Confirm the return vessels + confirm TT input is accepted. Change row to mapped with note about the typed vessel. |
jdut1_to_utc |
"no exact UT1-specific helper audited" |
datetime_from_jd(jd) -> datetime in moira.julian — UT1/UTC distinction is sub-millisecond for most use; Moira uses UT throughout |
Note that Moira does not expose a UT1-specific variant; datetime_from_jd is the correct idiom. Change to mapped with a clarifying note. |
calc_ut |
"split across low-level and facade" |
planet_at(body, jd_ut) returns PlanetData with longitude, latitude, distance, speed — exact semantic match for the primary Swiss calc_ut use case |
The split is just Moira's cleaner architecture. Change row to mapped with a migration note. |
Resolved: delta_t_from_jd added to moira/julian.py. One-liner delegating to delta_t(decimal_year_from_jd(jd_ut)). Exported from moira/__init__.py and moira/facade.py. DRAFT row updated to mapped.
Promote to __init__.py and facade.py. Update DRAFT row.
Resolved: apparent_sidereal_time_at added to moira/julian.py. Derives nutation and true obliquity internally via deferred import of moira.obliquity (avoids circular dependency). longitude=0 returns GAST; non-zero returns LAST. Exported from moira/__init__.py and moira/facade.py. DRAFT row updated to mapped.
Note: the roadmap draft pseudocode referenced a non-existent nutation_components function. Actual implementation uses obliquity.nutation(jd_tt) and obliquity.true_obliquity(jd_tt).
Resolved: planet_at already exposes jd_tt: float | None = None at planets.py:608. When supplied, the UT→TT conversion is bypassed entirely. No new code was needed. DRAFT row updated to mapped.
Current gap: No single Moira function returns what Swiss pheno_ut returns: phase angle, illumination fraction, elongation, apparent diameter, apparent magnitude.
What exists:
| Attribute | Source |
|---|---|
apparent_magnitude |
_target_apparent_magnitude(body, jd_ut) in heliacal.py (internal) |
elongation_deg |
_signed_elongation(body, jd_ut) in heliacal.py (internal) |
phase_angle_deg |
phase_angle(body, jd_ut) in moira.phase (public) |
illumination_fraction |
phase_fraction(body, jd_ut) or derivable from phase angle |
angular_diameter_arcsec |
derivable from distance and known body radius |
Resolution: Define PlanetPhenomena dataclass and planet_phenomena_at(body, jd_ut) -> PlanetPhenomena in moira/phenomena.py. Promote to __init__.py and facade.py. Close pheno_ut row.
Current gap: Swiss nod_aps_ut returns nodes + apsides for a body in one call. Moira has:
- Lunar nodes:
mean_node,true_nodeinmoira.nodes - Planetary apsides:
perihelion,aphelioninmoira.phenomena - Planetary nodes:
all_planetary_nodesinmoira.planetary_nodes
These surfaces exist but no combined vessel exists.
Resolution: Define NodesAndApsides dataclass and nodes_and_apsides_at(body, jd_ut) -> NodesAndApsides in moira/nodes.py or moira/planetary_nodes.py. For the Moon: populate from true_node/mean_node. For planets: populate from planetary_nodes + perihelion/aphelion. Close nod_aps_ut row.
All rows audited against moira/eclipse.py and moira/occultations.py. 9 of 10 closed to mapped. One genuine partial remains.
| Swiss symbol | Result | Finding |
|---|---|---|
sol_eclipse_when_glob |
mapped |
next_solar_eclipse(jd_start) — global, no location arg |
sol_eclipse_when_loc |
mapped |
next_solar_eclipse_at_location(jd_start, lat, lon) → SolarEclipseLocalCircumstances — scans lunations from jd_start; skips events where Sun is below horizon; refines local maximum via ternary search; also available as EclipseCalculator().next_solar_eclipse_at_location(...); implemented 2026-04-06 |
sol_eclipse_where |
mapped |
solar_eclipse_path(jd_start) → SolarEclipsePath; validated against Swiss where fixture |
sol_eclipse_how |
mapped |
SolarEclipseLocalCircumstances.event.data.eclipse_magnitude + sun_apparent_radius, moon_apparent_radius, topocentric_separation_deg, topocentric_overlap
|
lun_eclipse_when |
mapped |
next_lunar_eclipse(jd_start) — global search |
lun_eclipse_when_loc |
mapped |
lunar_local_circumstances(jd, lat, lon) → LunarEclipseLocalCircumstances; per-contact azimuth/altitude/visible for all 7 contacts |
lun_eclipse_how |
mapped |
LunarEclipseAnalysis.gamma_earth_radii + event.data.eclipse_magnitude/eclipse_type + shadow radii |
lun_occult_when_glob |
mapped |
lunar_occultation(target, jd_start, jd_end) / lunar_star_occultation(...) — omit observer coords for geocentric |
lun_occult_when_loc |
mapped |
same functions with observer_lat=, observer_lon= → topocentric search |
lun_occult_where |
mapped |
lunar_occultation_path_at(...) / lunar_star_occultation_path_at(...) → OccultationPathGeometry; IOTA-validated |
Both rows audited against moira/rise_set.py.
| Swiss symbol | Result | Finding |
|---|---|---|
rise_trans |
mapped |
find_phenomena returns 'Rise', 'Set', 'Transit', 'AntiTransit'; covers all Swiss CALC_RISE/CALC_SET/CALC_MTRANSIT/CALC_ITRANSIT use cases |
rise_trans_true_hor |
mapped |
altitude=<val> kwarg or RiseSetPolicy(horizon_altitude=<val>) drives the bisection threshold directly; RiseSetPolicy(refraction=False) gives geometric horizon |
| Swiss symbol | Result | Finding |
|---|---|---|
SE_ISIS |
mapped |
Asteroid 42 Isis — real minor planet, NAIF ID 2000042. asteroid_at("Isis", jd_ut) via moira/asteroids.py catalog. |
SE_NIBIRU |
unsupported.doctrine |
No accepted scientific ephemeris, no NAIF ID, no SPK kernel. Fictional body. Moira is astronomical-truth-first; fictional bodies with no real orbital solution are outside its design scope. |
SE_HARRINGTON |
unsupported.doctrine |
Robert Harrington's unconfirmed Planet X hypothesis. Never assigned a NAIF ID; no SPK kernel exists. Same doctrine. |
| Uranian bodies (Cupido…Poseidon, Transpluto) |
mapped (prior session) |
Already in moira.uranian with mean orbital elements. Separate verified row. |
| Symbol | Group | Resolution code | Effort |
|---|---|---|---|
set_topo |
A | CLOSE:unsupported |
None |
set_sid_mode |
A | CLOSE:unsupported |
None |
day_of_week |
B | CLOSE:stdlib |
None |
get_planet_name |
B | CLOSE:stdlib |
None |
difdeg2n |
B |
CLOSE:mapped (idiom note) |
None |
deg_midp |
B |
CLOSE:mapped (idiom note) |
None |
utc_to_jd |
C | CLOSE:mapped |
Verify |
deltat_ex |
C | CLOSE:mapped |
Verify |
get_ayanamsa_ex_ut |
C | CLOSE:mapped |
Verify |
get_ayanamsa_name |
C | CLOSE:mapped |
Verify |
azalt |
C | CLOSE:mapped |
Verify |
sol_eclipse_when_glob |
C | CLOSE:mapped |
Verify |
lun_eclipse_when |
C | CLOSE:mapped |
Verify |
sol_eclipse_how |
C |
AUDIT → mapped |
Read fields |
lun_eclipse_how |
C |
AUDIT → mapped |
Read fields |
mooncross_ut |
C | CLOSE:mapped |
Verify |
solcross_ut |
C | CLOSE:mapped |
Verify |
heliacal_ut |
C | CLOSE:mapped |
Note update |
jdet_to_utc |
C | CLOSE:mapped |
Verify |
jdut1_to_utc |
C | CLOSE:mapped |
Verify + note |
calc_ut |
C | CLOSE:mapped |
Note update |
deltat |
D |
mapped ✓ |
delta_t_from_jd added |
sidtime0 |
D |
mapped ✓ |
apparent_sidereal_time_at added |
calc |
D |
mapped ✓ |
jd_tt kwarg already existed at planets.py:608
|
pheno_ut |
E | VESSEL |
~80 lines |
nod_aps_ut |
E | VESSEL |
~100 lines |
sol_eclipse_when_loc |
F | mapped ✓ |
next_solar_eclipse_at_location free function + method; location-anchored search; implemented 2026-04-06 |
sol_eclipse_when_glob |
C→F | mapped |
None |
sol_eclipse_where |
F | mapped |
None |
sol_eclipse_how |
F | mapped |
None |
lun_eclipse_when |
C→F | mapped |
None |
lun_eclipse_when_loc |
F | mapped |
None |
lun_eclipse_how |
F | mapped |
None |
lun_occult_when_glob |
F | mapped |
None |
lun_occult_when_loc |
F | mapped |
None |
lun_occult_where |
F | mapped |
None |
rise_trans |
G | mapped |
None |
rise_trans_true_hor |
G | mapped |
None |
ISIS, NIBIRU, etc. |
H |
AUDIT → mixed |
Catalog read |
sidtime0 |
D | THIN |
~10 lines |
deltat |
D | THIN |
~5 lines |
utc_to_jd |
C | CLOSE:mapped |
Verify |
jdut1_to_utc |
C | CLOSE:mapped |
Verify |
All rows verified in one pass against moira.julian, moira.sidereal, moira.coordinates, moira.transits, moira.heliacal, moira.eclipse. DRAFT.md updated with exact signatures and audit datestamps.
Verified and closed:
- ✓
utc_to_jd—jd_from_datetime(dt: datetime) → floatconfirmed injulian.__all__ - ✓
jdet_to_utc,jdut1_to_utc—calendar_datetime_from_jd/datetime_from_jdconfirmed; UT1 note added - ✓
deltat_ex—DeltaTPolicy(model=..., fixed_delta_t=...)+delta_t_nasa_canon(year)confirmed - ✓
calc_ut—planet_at(body, jd_ut) → PlanetData; split is intentional architecture - ✓
set_topo,set_sid_mode→unsupported.design; per-call idiom documented - ✓
day_of_week,get_planet_name→stdlib - ✓
difdeg2n,deg_midp→mapped;normalize_degrees(a - b)and midpoint idioms - ✓
get_ayanamsa_ex_ut—ayanamsa(jd_ut, Ayanamsa.X) → floatconfirmed - ✓
get_ayanamsa_name—Ayanamsais a plain string-constant class (not StrEnum);Ayanamsa.LAHIRIreturns'Lahiri'directly - ✓
azalt—equatorial_to_horizontal(ra_deg, dec_deg, lst_deg, lat_deg) → tuple[float, float]; takes pre-computed LST - ✓
heliacal_ut—planet_heliacal_rising/planet_heliacal_setting+visibility_eventfor general case - ✓
mooncross_ut,solcross_ut—next_transit(Body.MOON/SUN, target_lon, jd_start) → TransitEvent - ✓ Eclipse group-C rows already
mapped(confirmed in Group F audit 2026-04-04) - ✓ Overview combined rows updated to match detail rows
Finding: heliacal_rising and heliacal_setting do not exist as public free functions; planet_heliacal_rising and planet_heliacal_setting are the correct surfaces. The roadmap's star surface note was stale.
- ✓
delta_t_from_jd(jd_ut)→ closesdeltat - ✓
apparent_sidereal_time_at(jd_ut, longitude)→ closessidtime0 - ✓
jd_ttkwarg onplanet_atalready existed — closescalc(no code needed)
- ✓
planet_phenomena_at(body, jd_ut) → PlanetPhenomenaadded tomoira/phenomena.py; exported frommoira/__init__.pyandmoira/facade.py; closespheno_ut - ✓
nodes_and_apsides_at(body, jd_ut) → NodesAndApsidesadded tomoira/nodes.py; exported frommoira/__init__.pyandmoira/facade.py; closesnod_aps_ut
Notes:
-
PlanetPhenomenafields:phase_angle_deg,illuminated_fraction,elongation_deg,angular_diameter_arcsec,apparent_magnitude— all delegates to existingmoira.phasepublic functions; lazy import avoids any circularity. -
NodesAndApsidesfields:ascending_node_lon,descending_node_lon,periapsis_lon,apoapsis_lon. For Moon:true_node+true_lilith(apogee → apoapsis; apogee + 180° → periapsis). For planets:planetary_node.ascending_node/.perihelion/.aphelion(ecliptic longitudes from Meeus Table 31.a, no ephemeris call needed). - 11/11 drift guard tests pass after exports.
sol_eclipse_when_loc was a genuine implementation gap. Closed by adding next_solar_eclipse_at_location as both an EclipseCalculator method and a module-level free function in moira/eclipse.py. Scans lunations from jd_start; uses _topocentric_solar_geometry to gate on Sun visibility; refines local maximum via _refine_minimum (ternary search); returns SolarEclipseLocalCircumstances at the local greatest-eclipse instant. All other Group F rows were already mapped.
One focused read of find_phenomena + RiseSetPolicy. Likely closes both rows with no new code.
Read moira/data/ and moira/asteroids.py for named small-body entries.