Repository navigation
pullen_sinusoidal_runtime_design_note
Implemented in the live houses runtime.
This note translates the governing doctrine in pullen_sinusoidal_admission_doctrine.md into a concrete runtime design for Moira's houses Pillar.
It is intentionally narrower than a roadmap and more executable than the admission note. Its role is to define:
- where SD/SR would enter the current house architecture
- what helpers are allowed
- what helpers are forbidden
- how the implementation must be staged to remain Moira-owned
The implementation must be explainable in this order:
- compute the ecliptic angle anchors already owned by the houses Pillar
- derive the four ecliptic quadrant arcs
- reduce those to the compressed / expanded quadrant pair
- derive the three house widths for each quadrant from the source law
- assemble the 12 cusp sequence by accumulation from the anchors
- finalize and validate via the existing cusp hardening path
If the code is best explained as "ported Pullen support from Swiss," the design has failed even before implementation begins.
This design assumes two new canonical codes will eventually be added in moira/constants.py:
HouseSystem.PULLEN_SDHouseSystem.PULLEN_SR
The exact string tokens should be chosen only once and then frozen.
Suggested naming:
-
PSDorSDfor Pullen Sinusoidal Delta -
PSRorSRfor Pullen Sinusoidal Ratio
Constraint:
- choose codes that do not collide with existing semantic uses elsewhere in the repository
- once chosen, update
HOUSE_SYSTEM_NAMES
Both systems should classify as:
- family:
HouseSystemFamily.QUADRANT - cusp basis:
HouseSystemCuspBasis.SINUSOIDAL - latitude-sensitive:
True - polar-capable:
True
That classification must be added to _CLASSIFICATIONS and _KNOWN_SYSTEMS.
They must not be added to _POLAR_SYSTEMS, because their governing law is
not blocked by the same singularity class as Koch / Regiomontanus /
Topocentric / Campanus.
The present extension seam in moira/houses.py already computes:
mc = _mc_from_armc(armc, obliquity, lat)asc = _asc_from_armc(armc, obliquity, lat)
inside houses_from_armc(...).
Those anchors are sufficient.
The Pullen systems must not introduce a parallel derivation of Ascendant or Midheaven.
They also do not need:
- equatorial-sector helpers
- semi-arc helpers
- pole-height helpers
- local-horizon vector families
- root-search engines
Their runtime implementation should sit near the other ecliptic/quadrant assembly helpers, not near Placidus/Koch/Regio families.
-
- add the two
HouseSystemcodes - add display names to
HOUSE_SYSTEM_NAMES
- add the two
-
- add
_CLASSIFICATIONSentries - add
_KNOWN_SYSTEMSentries - add two private computation helpers
- add dispatch branches inside
houses_from_armc(...)
- add
-
tests/unit/test_house_classification.py
- family, basis, latitude-sensitive, polar-capable assertions
-
new focused house tests
- Pullen law tests
- degeneracy tests
- ordered-figure / invariant tests
-
wiki/02_standards/HOUSES_BACKEND_STANDARD.md
- only after runtime admission is real
The implementation should be built from a very small helper surface.
Suggested helper:
_ecliptic_quadrant_arcs(asc: float, mc: float) -> tuple[float, float, float, float]
Responsibilities:
- compute the forward ecliptic arcs:
- Asc -> MC
- MC -> DSC
- DSC -> IC
- IC -> ASC
- preserve the actual directional arcs
- avoid hidden symmetry assumptions at the helper boundary
Rationale:
- the governing object is the actual quadrant arcs
- later helpers may reduce these to a compressed/expanded pair, but should not re-derive them ad hoc
Suggested helper:
_compressed_expanded_quadrants(asc: float, mc: float) -> tuple[float, float, bool]
Returns:
- compressed quadrant size
q - expanded quadrant size
180 - q - orientation flag indicating which half-figure is compressed
Responsibilities:
- choose the smaller of the complementary quadrants as the source-law
q - declare, not hide, which houses belong to the compressed side
This avoids Swiss-shaped "if quadrant < other quadrant then rotate slot fill" behavior.
Suggested helper:
_pullen_sd_widths(q: float) -> tuple[float, float, float, float, float, float]
Responsibilities:
- compute the six doctrinal widths from the source equations
- apply the explicit
q < 30°degeneracy branch if admitted - return widths in named doctrinal order:
- compressed flank
- compressed middle
- compressed flank
- expanded flank
- expanded middle
- expanded flank
This helper must not know anything about cusp indices.
Suggested helper:
_pullen_sr_widths(q: float) -> tuple[float, float, float, float, float, float]
Responsibilities:
- solve for
rdeterministically from the source relation - derive
x - return the six doctrinal widths in the same named order as SD
Solver doctrine:
- deterministic bracketed numeric solve
- fixed iteration budget or convergence tolerance
- no adaptive heuristic shaped by parity to an external engine
Suggested helper:
_assemble_pullen_cusps(asc: float, mc: float, widths: tuple[float, ...], compressed_is_upper: bool) -> list[float]
Responsibilities:
- map doctrinal widths onto the actual ecliptic circuit
- accumulate cusps from the anchor set in transparent order
- preserve anchor truth:
- cusp 1 opens at Asc
- cusp 10 opens at MC
- cusp 7 opens at DSC
- cusp 4 opens at IC
This helper is where implementation ownership matters most.
It must be written as named anchor-to-anchor accumulation, not legacy slot filling with offset tables.
At the system level, the intended private functions are:
_pullen_sd(asc: float, mc: float) -> list[float]_pullen_sr(asc: float, mc: float) -> list[float]
Each should do only this:
- derive compressed/expanded quadrant doctrine
- derive widths from the correct law
- assemble cusps from anchors
- return
_finalize_cusps(...)
They should not contain:
- policy logic
- fallback logic
- classification logic
- direct interaction with
HouseCusps
That remains with the existing runtime shell.
Implementation slot:
add two new branches in the direct system dispatch alongside other concrete systems, for example after the ecliptic equal-family helpers or near Porphyry.
Conceptual shape:
elif effective_system == HouseSystem.PULLEN_SD:
cusps = _pullen_sd(asc, mc)
elif effective_system == HouseSystem.PULLEN_SR:
cusps = _pullen_sr(asc, mc)
Important:
- no polar pre-emption branch
- no experimental-search detour
- no fallback semantics beyond existing unknown-system doctrine
If runtime implementation later reveals a real singularity beyond the source packet, admission must pause and doctrine must be revised first.
The cusp sequence must be assembled from the four cardinal anchors explicitly.
- House 1 cusp = Asc
- House 10 cusp = MC
- House 7 cusp = DSC = Asc + 180°
- House 4 cusp = IC = MC + 180°
The implementation must treat each quadrant as a three-house span:
- Asc -> MC contains houses 12, 11, 10 or 1, 12, 11 depending on the chosen accumulation convention
- MC -> DSC contains the next three houses
- DSC -> IC the next three
- IC -> ASC the final three
The convention may be whichever is most natural in current houses.py, but it
must be documented explicitly in code comments.
Do not:
- build a list of 12 placeholder slots and patch values by remembered positional choreography
- rotate a pre-filled array until the anchors appear correct
- apply index-based repairs after the fact
Those are exactly the lineage smells this design is meant to prevent.
When q >= 30°:
- use the exact closed-form source equations
When q < 30°:
- only the source-declared zero-middle-house extension is admissible
- if that branch is implemented, it must be visible in code comments and tests
r must be solved from the governing source relation.
Allowed:
- deterministic bisection
- deterministic Newton/bisection hybrid if bracket safety is preserved
Preferred:
- pure bisection first, because it is easiest to explain and smell-audit
Not allowed:
- heuristic seed tuned to match Swiss outputs
- branch logic justified only by software parity
The first implementation pass should be admitted only with focused tests that prove doctrine, not just snapshots.
- both systems classify as
QUADRANT - both systems classify as
SINUSOIDAL - both are latitude-sensitive
- both are polar-capable
For both systems:
- cusp 1 equals Asc
- cusp 10 equals MC
- cusp 7 equals Asc + 180°
- cusp 4 equals MC + 180°
For SD:
- when
q >= 30°, width differences satisfy the source delta law exactly
For SR:
- widths satisfy the declared ratio / power law within explicit numeric tolerance
For SD:
- one narrow-quadrant case with
q < 30° - prove the middle house width is
0° - prove the flanking widths bisect the remaining quadrant
For SR:
- one extreme narrow-quadrant case
- prove ordered, finite cusps
- all 12 cusps normalize into
[0, 360) - all forward spans are non-negative
- the total span sums to
360°
Before merge, the implementation must be reviewed against these questions:
- Is the code organized around ecliptic quadrants and source width laws?
- Are SD/SR widths computed from named doctrinal quantities rather than remembered software decomposition?
- Is cusp assembly anchor-first and transparent?
- Is the SD narrow-quadrant branch explicit doctrine rather than repair?
- Does the SR solver exist because the source requires it, rather than because Swiss-like code used a similar numeric staging?
- Would another engineer recognize this as Moira-native code on first read?
If any answer is "no", the implementation should be rewritten before admission.
When coding begins, the order should be:
- add codes and names in
constants.py - add classifications in
houses.py - write failing classification tests
- write failing doctrinal width-law tests
- implement shared quadrant helpers
- implement
_pullen_sd - implement
_pullen_sr - wire
houses_from_armc(...) - run correctness audit
- run smell audit
- only then update the frozen houses standard
This order preserves ownership and keeps the proof burden where it belongs.