Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ jobs:
cache-dependency-path: tools/jpl/requirements.lock
- run: python -m pip install --require-hashes -r tools/jpl/requirements.lock
- run: python tools/verification/test_compare.py
- run: python tools/verification/test_chart_migration.py
- run: python tools/verification/test_acquisition.py
- run: python tools/verification/test_generation.py
worker:
Expand Down
147 changes: 79 additions & 68 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,49 +1,66 @@
# Astrology Engine

A Rust library for calculating astrology charts: planetary positions, chart
angles, and house cusps.

For a Fagan–Bradley sidereal chart, use `calculate_sidereal_chart` with the same
`ChartInput`. It returns the date's ayanamsa, sidereal positions, twelve house
cusps, and the actual house method (including Porphyry fallback). The tropical
`calculate_chart` API remains available. The sidereal model covers 1800–2200 TT;
chart dates must also lie within the supplied dataset.

For explicit zodiac and sign-division choices, use `calculate_configured_chart`.
It adds Tropical, Fagan–Bradley and Chimenti True Sky configuration, optional
Ophiuchus merging, sign assignments and zodiac-aware houses. See the
[convention contract](docs/chart-conventions.md) and the standalone
[browser/Wasm example](examples/configurable-chart/README.md).
High-precision, Wasm-first astrology engine in Rust. Calculates planetary
positions, angles, and houses for the browser, Cloudflare Workers, and server
runtimes.

## What it calculates

- Planetary and derived body positions, longitudinal speeds, and declinations,
plus the Ascendant, Midheaven, Descendant, and Imum Coeli.
- Tropical, Fagan–Bradley sidereal, and Chimenti True Sky references, with sign
assignments and equal or True Sky constellation divisions.
- Equal, Whole Sign, and Placidus houses, with twelve cusps and explicit reporting
when Placidus falls back to Porphyry.
- Sun-longitude crossings, including the design-date search used in Human Design.

One `calculate_chart` function takes explicit zodiac and house settings and
returns positions, signs, cusps, and calculation metadata. See the
[chart conventions](docs/chart-conventions.md) for reference models, supported
combinations, speed conventions, and polar-house safeguards. Fagan–Bradley's
model covers 1800–2200 TT; every chart also requires coverage in its dataset.

## Use it in your runtime

The runtime accepts caller-supplied ephemeris bytes. It performs no network or
filesystem access; your application loads the data and resolves place names and
local birth times into coordinates and an unambiguous instant.

- **Browser/Wasm:** the [standalone chart example](examples/configurable-chart/README.md)
builds Rust to Wasm and provides interactive zodiac and house settings.
- **Cloudflare Workers:** the [HTTP example](examples/cloudflare-worker/) loads
data from R2. Follow the [Worker runbook](docs/worker-runbook.md) for local
testing and deployment. Its HTTP interface uses tropical charts.
- **Native/server Rust:** parse the dataset once with `Ephemeris::parse` and reuse
it across calls, as in the quickstart below.

## Calculate a natal chart

Supply a birth instant, latitude and longitude, and a house system.
`calculate_chart` returns the chart's body positions, angles, and optional house
cusps using a reusable ephemeris dataset.

To run the example, use the Rust toolchain specified in
[`rust-toolchain.toml`](rust-toolchain.toml). You also need an ephemeris dataset:
a binary file containing the data the library uses to calculate celestial
positions. The file must use the library's `HDCHEB01` format; the examples call
it `cheb.bin`.

Dataset files are not included in the repository. Follow the
[dataset generation guide](tools/README.md) to create one, then run this command
from the repository root, replacing `/path/to/cheb.bin` with your file's path:
Use the Rust toolchain specified in [`rust-toolchain.toml`](rust-toolchain.toml).
You also need an ephemeris dataset: a binary file of prepared celestial-position
data in the engine's `HDCHEB01` format. The examples call this file `cheb.bin`.
Dataset files are supplied separately and are not included in the repository.
Follow [Build a dataset](tools/README.md#build-a-dataset), then run:

```sh
cargo run --locked --example natal_chart -- /path/to/cheb.bin
cargo run --locked --example natal_chart -- /absolute/path/to/cheb.bin
```

The example uses a birth instant of **1 January 2000 at 12:00 UTC**, a London
location, and equal houses. Convert the person's local birth date and time to
UTC before calling the library. The dataset must cover the birth time and the
times 12 hours before and after it, which the library uses to calculate speeds.
This example calculates a tropical chart for **1 January 2000 at 12:00 UTC**,
London coordinates, and Equal houses. Convert local birth times to UTC before
calling the engine. The dataset must support the instant and samples 12 hours
before and after it for speed calculations.

The complete source is [`examples/natal_chart.rs`](examples/natal_chart.rs):

```rust
use astrology_engine::{calculate_chart, ChartInput, Ephemeris, Epoch, HouseSystem, Location};
// Copyright (c) Jack Asher
// SPDX-License-Identifier: MPL-2.0

use astrology_engine::{
calculate_chart, ChartInput, Ephemeris, Epoch, HouseSystem, Location, SignDivisions,
ZodiacConfiguration, ZodiacReference,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
let dataset = std::env::args_os()
Expand All @@ -62,64 +79,58 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
longitude: -0.1278,
},
house_system: HouseSystem::Equal,
zodiac: ZodiacConfiguration {
reference: ZodiacReference::Tropical,
divisions: SignDivisions::Equal,
ophiuchus: None,
},
},
)?;

println!("Birth moment: {birth_moment}");
println!("Longitudes in degrees:");
for body in &chart.bodies {
println!("{:<20} {:>12.6}", body.name, body.longitude);
}
if let Some(cusps) = &chart.cusps {
println!("House cusps in degrees: {cusps:?}");
println!("{:<20} {:>12.6}", body.values.name, body.values.longitude);
}
println!("House cusps in degrees: {:?}", chart.cusps);

Ok(())
}
```

The example prints the body and angle longitudes and the twelve equal-house
cusps. Each body entry also contains longitudinal speed and declination. The
returned `ChartValues` contains the numerical results your application can use
to draw a chart.

Load and parse the ephemeris once, then reuse it for subsequent charts.
`ChartValues` contains bodies with numerical values and sign assignments,
twelve house cusps, effective zodiac settings, and the requested and actual
house methods. Body longitudes, declinations, and cusps use degrees; speeds use
degrees per day. Load and parse the ephemeris once, then reuse it for later charts.

## Human Design design-date search

The library can also find the design instant used in Human Design: the time
before birth when the Sun's longitude was 88° less than its longitude at birth.
Starting with a natal chart, subtract 88° from the birth Sun's longitude, adjust
the result to the range from 0° inclusive to 360° exclusive, and search backward
with `find_sun_crossing` and `Direction::Backward`.

The offset is an angle; the search determines the corresponding date and time.
You can then call `calculate_chart` at that instant to obtain its planetary
positions, using the same ephemeris.
The design instant is the time before birth when the tropical Sun's longitude
was 88° less than its birth longitude. Calculate a tropical birth chart, subtract
88° from the Sun's longitude, wrap into [0°, 360°), and call `find_sun_crossing`
with `Direction::Backward`. Calculate another chart at the returned instant.

The [birth and design example](examples/birth_and_design.rs) demonstrates the
complete workflow:
The [birth and design example](examples/birth_and_design.rs) demonstrates this
workflow:

```sh
cargo run --locked --example birth_and_design -- /path/to/cheb.bin
cargo run --locked --example birth_and_design -- /absolute/path/to/cheb.bin
```

This search requires at least 89 days of dataset coverage before birth. The
dataset must also cover the returned design instant and the times 12 hours
before and after it, which the library uses to calculate speeds.
The search needs at least 89 days of dataset coverage before birth. Calculating
the design chart also requires speed-sampling support around the returned instant.

## Architecture and entry points
## Architecture and verification

Read [From JPL data to a natal chart](docs/architecture.md) to learn how NASA JPL
data becomes an ephemeris dataset and how the library uses that dataset to
calculate charts.
[From JPL data to a natal chart](docs/architecture.md) explains the separation
between native dataset preparation and the immutable Rust/Wasm runtime.
[The public Rust API](src/lib.rs) exports chart calculation, ephemeris evaluation,
and solar-longitude search.

- [Public Rust API](src/lib.rs): chart calculation, ephemeris evaluation, and
solar longitude search.
- [Example Cloudflare Worker](examples/cloudflare-worker/): an HTTP adapter that
loads the dataset from R2 and calls the library.
- [Deploy the Worker demo](docs/worker-runbook.md): generate a dataset, test
locally, deploy a public demo in your Cloudflare account, and clean up.
[Verification guidance](tools/README.md#run-focused-checks) distinguishes
structural checks, local Wasm execution, frozen regression parity, and dataset
regeneration. Recorded regression outputs preserve previous engine behavior;
these checks do not establish independent astronomical accuracy.

## License and distribution

Expand Down
8 changes: 4 additions & 4 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ parsed ephemeris across calculations. File access, storage, and checking the
artifact's SHA-256 belong to the caller; the library accepts the bytes.

For a natal chart, `calculate_chart` takes that ephemeris, an unambiguous birth
instant, latitude and longitude, and a house system. The caller resolves local
instant, latitude and longitude, a house system, and explicit zodiac settings. The caller resolves local
time and timezone ambiguity before passing the instant.

For each stored body coordinate, the runtime selects the segment containing the
Expand All @@ -82,12 +82,12 @@ The runtime combines those body values with angles and houses calculated from
the birth instant, birthplace, and selected house system. Birthplace affects this
chart geometry; it does not alter the stored geocentric body positions.

The result is `ChartValues`: a list of body and angle entries, plus optional
house cusps. These are the numerical values an application can use to display a
The result is `ChartValues`: body and angle entries with sign assignments, twelve
house cusps, and metadata describing the effective zodiac and actual house method. These are the numerical values an application can use to display a
chart. The current result contains 15 body/node entries and four chart angles.

The same pipeline supports the Human Design design chart's planetary positions.
The caller first calculates the birth chart, subtracts 88° from the birth Sun's
The caller first calculates a tropical birth chart, subtracts 88° from the birth Sun's
longitude, and asks `find_sun_crossing` to search backward. It then calls
`calculate_chart` at the returned instant, reusing the same ephemeris. Human
Design gate calculations remain outside this library. The
Expand Down
58 changes: 43 additions & 15 deletions docs/chart-conventions.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
# Configurable natal charts

`calculate_configured_chart` accepts the existing `ChartInput` and an explicit
`ZodiacConfiguration`. Its references are Tropical, Fagan–Bradley and Chimenti
True Sky. Sign divisions can be equal or, for True Sky, constellation midpoints.
The original `calculate_chart` and `calculate_sidereal_chart` contracts remain
available unchanged, including absent tropical Whole Sign cusps.
`calculate_chart` accepts an explicit `ChartInput`, including its `zodiac` field
of type `ZodiacConfiguration`. References are Tropical, Fagan–Bradley and Chimenti True
Sky. Sign divisions can be equal or, for True Sky, constellation midpoints.
There is one chart entry point and one `ChartValues` result contract.

```rust
use astrology_engine::{
calculate_configured_chart, ChartInput, Ephemeris, Ophiuchus,
calculate_chart, ChartInput, Ephemeris, Ophiuchus,
SignDivisions, ZodiacConfiguration, ZodiacReference,
};

fn chart(ephemeris: &Ephemeris, input: ChartInput) -> Result<(), Box<dyn std::error::Error>> {
let values = calculate_configured_chart(ephemeris, input, ZodiacConfiguration {
reference: ZodiacReference::TrueSky,
divisions: SignDivisions::Constellation,
ophiuchus: Some(Ophiuchus::Enabled),
})?;
let values = calculate_chart(
ephemeris,
ChartInput {
zodiac: ZodiacConfiguration {
reference: ZodiacReference::TrueSky,
divisions: SignDivisions::Constellation,
ophiuchus: Some(Ophiuchus::Enabled),
},
..input
},
)?;
println!("{}: {:?}", values.convention_revision, values.cusps);
Ok(())
}
Expand Down Expand Up @@ -53,8 +58,8 @@ retains the original instant's precision.

True Sky preserves the source reading layer's tropical longitudinal speeds and
equatorial declinations. Consequently `speed_reference` is `tropical`, even
though the returned longitudes use True Sky. Fagan–Bradley delegates to the
existing engine implementation and returns `selected-reference` speeds. The four
though the returned longitudes use True Sky. Fagan–Bradley applies the engine's
ayanamsa and its daily rate, returning `selected-reference` speeds. The four
angle speeds remain zero, meaning not calculated. Speeds are degrees per day;
longitudes, declinations, offsets, sector starts and widths are degrees.

Expand All @@ -78,6 +83,29 @@ starts in the merged Scorpio sector at 210.1972°. The twelve house starts are:
With Ophiuchus disabled, the same longitude is Scorpio, 17.8028° into its
25.5846° sector. The house starts are unchanged.

Tropical preserves the underlying numerical positions. At extreme latitudes,
floating-point angle calculations can round to 360° (equivalent to 0°); sign
lookup wraps those longitudes into [0°, 360°).

## Migrating the chart API

Replace `calculate_sidereal_chart` and `calculate_configured_chart` calls with
`calculate_chart`, and put the chosen `ZodiacConfiguration` in `ChartInput.zodiac`.
Former tropical callers must explicitly choose Tropical with Equal divisions and
`ophiuchus: None`. The runtime supplies no defaults or legacy wrappers.

`ChartValues` replaces the former configured and sidereal result types. Each
body is a `ChartEntry` with `values: ChartBody` and `sign: SignPosition`; serialized
body fields remain flattened. Use `reference_offset_degrees` for the reference
offset (the ayanamsa for Fagan–Bradley), and the typed `house_system` for the actual
method. Cusps are always `[f64; 12]`, including tropical Whole Sign houses.
Overlapping polar house sets now fail for every reference.

The Worker example continues to use its existing tropical HTTP request and
response fields, including omitted Whole Sign cusps. Overlapping polar charts
follow its existing calculation-error mapping: public HTTP 500 with `internal`,
and the explicit overlap message in verification captures.

## Browser and host integration

The [standalone browser/Wasm example](../examples/configurable-chart/README.md)
Expand All @@ -91,8 +119,8 @@ remains the host's responsibility.

[Regression replay instructions](../tools/verification/conventions.md) describe
how to compare current outputs with the committed fixtures. Structural checks
also cover invalid settings, non-finite input, coverage admission and existing
API compatibility.
also cover invalid settings, non-finite input, coverage admission and unified
chart behavior.

Boundary tables and assignments match the recorded outputs exactly.
Chart comparisons use a 1e-12 absolute tolerance for native/Wasm floating-point
Expand Down
13 changes: 9 additions & 4 deletions examples/birth_and_design.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

use astrology_engine::{
calculate_chart, find_sun_crossing, Body, ChartInput, Direction, Ephemeris, Epoch, HouseSystem,
Location, SunSearchInput,
Location, SignDivisions, SunSearchInput, ZodiacConfiguration, ZodiacReference,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
Expand All @@ -25,6 +25,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
epoch,
location,
house_system: HouseSystem::Equal,
zodiac: ZodiacConfiguration {
reference: ZodiacReference::Tropical,
divisions: SignDivisions::Equal,
ophiuchus: None,
},
},
)
};
Expand All @@ -33,9 +38,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let birth_sun = birth_chart
.bodies
.iter()
.find(|body| body.name == Body::Sun.wire_name())
.find(|body| body.values.name == Body::Sun.wire_name())
.ok_or("Birth chart has no Sun")?;
let target_longitude = (birth_sun.longitude - 88.0).rem_euclid(360.0);
let target_longitude = (birth_sun.values.longitude - 88.0).rem_euclid(360.0);
let design_moment = find_sun_crossing(
&ephemeris,
SunSearchInput {
Expand All @@ -54,7 +59,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
for (birth, design) in birth_chart.bodies.iter().zip(&design_chart.bodies) {
println!(
"{:<20} {:>12.6} {:>12.6}",
birth.name, birth.longitude, design.longitude
birth.values.name, birth.values.longitude, design.values.longitude
);
}

Expand Down
6 changes: 3 additions & 3 deletions examples/cloudflare-worker/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ requests, deployment and cleanup procedures.
only after loading, hashing and parsing succeed so failed initialization can
recover. In the [local harness](verify.mjs), cold/warm requests and retry after
invalid storage exercise this lifecycle across the R2 and Rust boundaries.
- HTTP representation is distinct from library values: absent Whole Sign
cusps are omitted in JSON, and public search results discard sub-minute
precision. The [capture adapter](src/verification.rs) retains floating-point
- HTTP representation is distinct from library values: Whole Sign
cusps are omitted in JSON even though the runtime returns twelve cusps. Public
search results discard sub-minute precision. The [capture adapter](src/verification.rs) retains floating-point
bits and precise instants for parity. Keep public formatting separate from
capture transport, including lossless large-integer handling in `verify.mjs`.

Expand Down
Loading
Loading