Architecture (Agent Orientation)¶
🤖 Primarily for coding agents. Hello, Claude! Read this before re-deriving the architecture from source. If it disagrees with the code, the code wins — please update the doc.
Part of the developer docs. Hub:
/CLAUDE.md· Index:docs/DOCS_INDEX.md
This is the map an agent should read first. It explains how the pieces fit so you can find the right file fast. For per-subsystem API detail, follow the links to the other spokes.
The one-paragraph mental model¶
Stellium turns birth data into an immutable result object and then renders
it. You configure a ChartBuilder (fluent, lazy), call .calculate(), and
get back a frozen CalculatedChart. Calculation is performed by pluggable
engines (ephemeris, houses, aspects, orbs) selected via Protocols;
optional components and analyzers add extra data (Arabic Parts,
midpoints, dignities, patterns). The chart then feeds visualization
(chart.draw(...) → SVG), presentation (ReportBuilder → terminal /
markdown / PDF), export (to_dict, analysis DataFrames), and timing
transforms (profections, zodiacal releasing, returns, progressions).
Native/Notable ─► ChartBuilder ──.calculate()──► CalculatedChart ─┬─► chart.draw(...) (SVG)
(birth data) (config, lazy) (engines + (frozen) ├─► ReportBuilder (report)
components + ├─► chart.to_dict() (JSON)
analyzers) ├─► analysis.* / io.* (DataFrames)
└─► profection()/zodiacal_releasing()/returns
Three principles (these are load-bearing, not slogans)¶
Protocols over inheritance. Engines/components implement a
Protocolfromcore/protocols.pyby matching method signatures — no base class. To add behavior you write a class with the right methods and inject it.Immutability. Every result model is a
@dataclass(frozen=True). Mutate withdataclasses.replace(obj, field=...), never in place.Composability + lazy evaluation.
ChartBuilder.with_*()/add_*()just record config and returnself. Nothing is computed until.calculate().
Dependency direction (do not violate)¶
core/ ─► (depends on nothing internal)
engines/, components/, electional/, returns/, io/, analysis/, chinese/ ─► core
presentation/, visualization/, planner/ ─► core, engines, components
core/ must never import from engines/, components/, etc.
core/chart_utils.py deliberately uses duck-typing to inspect chart types
without importing the higher layers — follow that pattern if you need it.
Layer-by-layer (where to look)¶
Layer |
Package |
What lives here |
Deep-dive doc |
|---|---|---|---|
Core models |
|
Frozen dataclasses: |
|
Interfaces |
|
All |
|
Builder / API |
|
|
|
Birth data |
|
|
|
Config |
|
|
|
Registries |
|
|
|
Multi-chart |
|
Synastry/transits/composite/Davison containers |
|
Engines |
|
Calculation: ephemeris, houses, aspects, orbs, dignities, patterns, dispositors, profections, releasing, directions, voc, fixed_stars, search |
|
Components |
|
Optional add-ons: arabic_parts, midpoints, dignity, fixed_stars, antiscia |
|
Analysis / IO |
|
Batch, DataFrames, stats, queries, vectors; CSV/AAF/DataFrame import |
|
Timing |
|
Returns, electional search, profections, ZR, progressions |
|
Visualization |
|
SVG: builder→composer→layers; themes/palettes; dial, vedic, atlas |
|
Presentation |
|
|
|
Planner |
|
Personalized PDF planners (Typst) |
|
Chinese |
|
BaZi (Four Pillars); Zi Wei (planned) |
|
CLI |
|
|
|
Utils |
|
Caching, time/JD, chart shape, chart ruler, progressions, crossings |
What .calculate() actually does¶
Reading this once saves you from guessing why a field is empty. From
core/builder.py::calculate():
Resolve the object list from
CalculationConfig(heliocentric mode removes Sun/nodes/apogees and adds Earth).Ephemeris →
list[CelestialPosition](ecliptic + equatorial for declinationphase data).
For each configured house system: compute cusps and angles (ASC/MC/DSC/IC/Vertex). Angles are appended to the positions list.
Assign house placements for every system.
Run components in order — each returns extra
CelestialPositions that are appended (or writes tometadata).Compute aspects, then declination aspects (if enabled).
Run analyzers against a provisional chart — they populate
metadata(e.g. aspect patterns, zodiacal releasing).Build the final frozen
CalculatedChart(with component manifest, ayanamsa value if sidereal, tags).
Implications:
Components add positions; analyzers add metadata. Access them via
chart.get_component_result(name)and the dedicated getters.Unknown-time charts (
with_unknown_time()) skip houses/angles and return anUnknownTimeChartwith amoon_rangeinstead.
Conventions worth memorizing¶
Longitudes are 0–360°, always normalized; wraparound at 0°/360° is handled explicitly (see
utils/houses.py::find_house_for_longitude).All internal time is UTC Julian Day; convert to local only at display. Helpers in
utils/time.py.Retrograde =
speed_longitude < 0; |speed| below a small epsilon is treated as stationary (no applying/separating verdict).Sect-aware logic (Arabic Parts, some dignities) flips by day/night chart; see
chart.sectandcomponents/dignity.py::determine_sect.One aspect per pair; axis pairs (ASC/DSC, MC/IC, Node/South Node) are excluded from normal aspects.
Whole Sign is preferred for profections; Placidus is the default house system everywhere else.
See EXTENDING.md for how to plug in new engines, components, analyzers, layers, themes, palettes, and report sections.