astroceleste-engine 0.0.2__tar.gz → 0.0.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/Cargo.lock +3 -3
  2. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/Cargo.toml +1 -1
  3. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/PKG-INFO +4 -1
  4. {astroceleste_engine-0.0.2/crates/astroceleste-engine-py → astroceleste_engine-0.0.4}/README.md +3 -0
  5. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/README.md +41 -60
  6. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/src/almanac.rs +49 -1
  7. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/src/chart.rs +57 -11
  8. astroceleste_engine-0.0.4/crates/astroceleste-engine/src/election.rs +1688 -0
  9. astroceleste_engine-0.0.4/crates/astroceleste-engine/src/ephemeris/mod.rs +19 -0
  10. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/src/horary.rs +77 -20
  11. astroceleste_engine-0.0.4/crates/astroceleste-engine/src/houses.rs +385 -0
  12. astroceleste_engine-0.0.4/crates/astroceleste-engine/src/lib.rs +157 -0
  13. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/src/planets.rs +10 -1
  14. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine-py/Cargo.toml +1 -1
  15. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4/crates/astroceleste-engine-py}/README.md +3 -0
  16. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine-py/astroceleste_engine.pyi +24 -0
  17. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine-py/src/lib.rs +74 -2
  18. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine-py/tests/test_engine.py +26 -0
  19. astroceleste_engine-0.0.2/crates/astroceleste-engine/src/ephemeris/mod.rs +0 -10
  20. astroceleste_engine-0.0.2/crates/astroceleste-engine/src/houses.rs +0 -139
  21. astroceleste_engine-0.0.2/crates/astroceleste-engine/src/lib.rs +0 -68
  22. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/LICENSE-APACHE +0 -0
  23. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/LICENSE-MIT +0 -0
  24. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/NOTICE +0 -0
  25. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/Cargo.toml +0 -0
  26. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/LICENSE-APACHE +0 -0
  27. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/LICENSE-MIT +0 -0
  28. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/NOTICE +0 -0
  29. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/src/aspects.rs +0 -0
  30. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/src/catalog/derived_meanings.rs +0 -0
  31. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/src/catalog/fixed_stars.rs +0 -0
  32. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/src/catalog/mansions.rs +0 -0
  33. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/src/catalog/mod.rs +0 -0
  34. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/src/chiron.rs +0 -0
  35. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/src/constants.rs +0 -0
  36. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/src/data/chiron.bin +0 -0
  37. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/src/data/deltat.bin +0 -0
  38. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/src/derived.rs +0 -0
  39. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/src/ephemeris/kernels.rs +0 -0
  40. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/src/ephemeris/observe.rs +0 -0
  41. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/src/ephemeris/spk.rs +0 -0
  42. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/src/error.rs +0 -0
  43. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/src/fixed_stars.rs +0 -0
  44. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/src/frames/mod.rs +0 -0
  45. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/src/frames/nutation.rs +0 -0
  46. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/src/frames/nutation_data.rs +0 -0
  47. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/src/frames/precession.rs +0 -0
  48. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/src/frames/sidereal.rs +0 -0
  49. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/src/instant.rs +0 -0
  50. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/src/lots.rs +0 -0
  51. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/src/lunar.rs +0 -0
  52. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/src/pyfloat.rs +0 -0
  53. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/src/symbolic.rs +0 -0
  54. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/src/temperament.rs +0 -0
  55. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/src/time.rs +0 -0
  56. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine/src/zodiac.rs +0 -0
  57. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine-py/LICENSE-APACHE +0 -0
  58. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine-py/LICENSE-MIT +0 -0
  59. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/crates/astroceleste-engine-py/NOTICE +0 -0
  60. {astroceleste_engine-0.0.2 → astroceleste_engine-0.0.4}/pyproject.toml +0 -0
@@ -4,7 +4,7 @@ version = 4
4
4
 
5
5
  [[package]]
6
6
  name = "astroceleste-engine"
7
- version = "0.0.2"
7
+ version = "0.0.4"
8
8
  dependencies = [
9
9
  "serde",
10
10
  "serde_json",
@@ -12,7 +12,7 @@ dependencies = [
12
12
 
13
13
  [[package]]
14
14
  name = "astroceleste-engine-py"
15
- version = "0.0.2"
15
+ version = "0.0.4"
16
16
  dependencies = [
17
17
  "astroceleste-engine",
18
18
  "pyo3",
@@ -22,7 +22,7 @@ dependencies = [
22
22
 
23
23
  [[package]]
24
24
  name = "astroceleste-engine-wasm"
25
- version = "0.0.2"
25
+ version = "0.0.4"
26
26
  dependencies = [
27
27
  "astroceleste-engine",
28
28
  "js-sys",
@@ -3,7 +3,7 @@ resolver = "2"
3
3
  members = ["crates/astroceleste-engine", "crates/astroceleste-engine-py"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.0.2"
6
+ version = "0.0.4"
7
7
  edition = "2021"
8
8
  rust-version = "1.85"
9
9
  license = "MIT OR Apache-2.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: astroceleste-engine
3
- Version: 0.0.2
3
+ Version: 0.0.4
4
4
  Classifier: Development Status :: 2 - Pre-Alpha
5
5
  Classifier: Programming Language :: Rust
6
6
  Classifier: Programming Language :: Python :: 3
@@ -28,6 +28,9 @@ Project-URL: Repository, https://github.com/ffalcinelli/astroceleste-engine
28
28
  Python bindings for [astroceleste-engine](https://github.com/ffalcinelli/astroceleste-engine),
29
29
  astrological chart calculation on NASA JPL ephemerides, written in Rust.
30
30
 
31
+ [Website and live demo](https://ffalcinelli.github.io/astroceleste-engine/) ·
32
+ [API guide](https://github.com/ffalcinelli/astroceleste-engine/blob/main/docs/api.md)
33
+
31
34
  ```python
32
35
  from datetime import datetime, timezone
33
36
  import astroceleste_engine as ace
@@ -7,6 +7,9 @@
7
7
  Python bindings for [astroceleste-engine](https://github.com/ffalcinelli/astroceleste-engine),
8
8
  astrological chart calculation on NASA JPL ephemerides, written in Rust.
9
9
 
10
+ [Website and live demo](https://ffalcinelli.github.io/astroceleste-engine/) ·
11
+ [API guide](https://github.com/ffalcinelli/astroceleste-engine/blob/main/docs/api.md)
12
+
10
13
  ```python
11
14
  from datetime import datetime, timezone
12
15
  import astroceleste_engine as ace
@@ -12,11 +12,15 @@ It is the calculation core of [Astroceleste](https://astroceleste.it). The same
12
12
  on the server (Python bindings), in the desktop and mobile apps (native) and in the browser
13
13
  (WASM), so every platform computes the same chart down to the arcsecond.
14
14
 
15
+ **[Website and live demo](https://ffalcinelli.github.io/astroceleste-engine/)** · [API docs](https://docs.rs/astroceleste-engine) ·
16
+ [Guides](https://github.com/ffalcinelli/astroceleste-engine/tree/main/docs)
17
+
15
18
  > **Experimental (0.0.x):** the API may change in any release. Pin an exact version.
16
19
 
17
20
  > Status: the complete chart pipeline is ported and matches the reference implementation
18
21
  > on every golden chart: planets, lunar nodes, Chiron, Lilith, houses (Placidus, Whole Sign,
19
- > Equal, Porphyry), aspects and orbs, fixed stars, Arabic parts, temperament, lunar status,
22
+ > Equal, Porphyry; Koch, Regiomontanus, Campanus, Topocentric, Alcabitius, Morinus and Vehlow
23
+ > are checked against Swiss Ephemeris), aspects and orbs, fixed stars, Arabic parts, temperament, lunar status,
20
24
  > horary charts (planetary hours from computed sunrise and sunset), transits, synastry and
21
25
  > derived charts, with Python and WebAssembly bindings.
22
26
 
@@ -35,21 +39,32 @@ println!("{}", serde_json::to_string_pretty(&chart)?);
35
39
  ```
36
40
 
37
41
  Charts serialize to the same JSON as the Astroceleste API. Other entry points:
38
- `calculate_horary_chart`, `calculate_transit_chart`, `calculate_synastry` and
39
- `calculate_derived_chart`. In the browser or on mobile, load the kernel from memory with
42
+ `calculate_horary_chart`, `calculate_transit_chart`, `calculate_synastry`,
43
+ `calculate_derived_chart`, and for electional astrology `calculate_election_chart` and
44
+ `search_elections` (the best moments over a span of time). In the browser or on mobile, load the kernel from memory with
40
45
  `Spk::from_bytes`.
41
46
 
42
47
  ## Bindings
43
48
 
44
49
  - **Python** (`pip install astroceleste-engine`, CPython ≥ 3.12): `Engine([kernel paths])`
45
- with `.chart()`, `.horary()`, `.transit()`, plus `synastry()` and `derived_chart()`,
46
- returning plain dicts. See [crates/astroceleste-engine-py](crates/astroceleste-engine-py).
50
+ with `.chart()`, `.horary()`, `.transit()`, `.election()`, `.elections()`, plus `synastry()` and `derived_chart()`,
51
+ returning plain dicts. See [crates/astroceleste-engine-py](https://github.com/ffalcinelli/astroceleste-engine/tree/main/crates/astroceleste-engine-py).
47
52
  - **JavaScript / WebAssembly** (`npm install astroceleste-engine`): the same API over
48
53
  kernels loaded from memory, for browsers and Node. See
49
- [crates/astroceleste-engine-wasm](crates/astroceleste-engine-wasm).
54
+ [crates/astroceleste-engine-wasm](https://github.com/ffalcinelli/astroceleste-engine/tree/main/crates/astroceleste-engine-wasm).
50
55
 
51
56
  Both are tested against the same golden fixtures as the Rust crate.
52
57
 
58
+ ## Documentation
59
+
60
+ - [API reference on docs.rs](https://docs.rs/astroceleste-engine)
61
+ - [API guide](https://github.com/ffalcinelli/astroceleste-engine/blob/main/docs/api.md): request options, the chart JSON, error codes in every
62
+ language
63
+ - [Ephemerides](https://github.com/ffalcinelli/astroceleste-engine/blob/main/docs/ephemerides.md): kernels, date coverage, loading from memory,
64
+ smaller excerpts for the web and mobile
65
+ - [Accuracy](https://github.com/ffalcinelli/astroceleste-engine/blob/main/docs/accuracy.md): how every number is verified
66
+ - [Architecture](https://github.com/ffalcinelli/astroceleste-engine/blob/main/docs/architecture.md): crates and calculation pipeline
67
+
53
68
  ## Layout
54
69
 
55
70
  | Path | Content |
@@ -63,78 +78,44 @@ Both are tested against the same golden fixtures as the Rust crate.
63
78
  | `scripts/make_spk_fixtures.py` | rebuilds the excerpt and the SPK reference states with `jplephem` |
64
79
  | `scripts/make_reduction_fixtures.py` | stage-by-stage reference values from Skyfield |
65
80
  | `scripts/gen_tables.py` | regenerates the embedded ΔT, nutation and Chiron tables |
81
+ | `scripts/build-site.sh` | builds the landing site and live demo into `target/site/` |
82
+ | `docs/` | guides |
83
+ | `site/` | landing site and live demo, deployed to GitHub Pages |
84
+
85
+ ## Contributing
66
86
 
67
- ## Development
87
+ Contributions are welcome: see [CONTRIBUTING.md](https://github.com/ffalcinelli/astroceleste-engine/blob/main/CONTRIBUTING.md) for setup, the test
88
+ suite and the rules that keep every platform identical. Report vulnerabilities privately,
89
+ as described in [SECURITY.md](https://github.com/ffalcinelli/astroceleste-engine/blob/main/SECURITY.md). Maintainers release by pushing a version
90
+ tag ([RELEASING.md](https://github.com/ffalcinelli/astroceleste-engine/blob/main/RELEASING.md)).
68
91
 
69
92
  ```bash
70
93
  cargo test --workspace --exclude astroceleste-engine-py # uses the committed excerpt
71
- scripts/fetch-kernels.sh # optional: full de440s (1849-2150), enables the full-range tests
94
+ scripts/fetch-kernels.sh # full de440s (1849-2150): enables the golden and full-range tests
72
95
  cargo clippy --workspace --all-targets -- -D warnings
73
-
74
- # Python bindings (in a virtualenv)
75
- pip install maturin pytest && maturin develop -m crates/astroceleste-engine-py/Cargo.toml
76
- pytest crates/astroceleste-engine-py/tests
77
-
78
- # WebAssembly bindings (needs wasm-pack and the wasm32-unknown-unknown target)
79
- wasm-pack build --target nodejs --out-dir pkg-node crates/astroceleste-engine-wasm
80
- node crates/astroceleste-engine-wasm/tests/golden.mjs
81
96
  ```
82
97
 
83
- ## Releasing
84
-
85
- Releases are trunk-based: there are no release branches or release PRs. Pushing a
86
- `vX.Y.Z` tag on `main` publishes that version.
87
-
88
- 1. Commits on `main` follow [Conventional Commits](https://www.conventionalcommits.org)
89
- (`feat:`, `fix:`, `perf:`, `refactor:`, … and `!` for breaking changes). While the
90
- version is 0.0.x, `feat`/`fix` bump the patch version and a breaking change bumps the
91
- minor version.
92
- 2. Prepare the release on `main`: run `release-plz update` (or edit by hand) to bump the
93
- workspace version, the bindings' `astroceleste-engine` dependency version and
94
- `CHANGELOG.md`, then commit as `chore: release vX.Y.Z` and push.
95
- 3. Once CI is green, tag that commit and push the tag:
96
- `git tag vX.Y.Z && git push origin vX.Y.Z`.
97
- 4. The `Release` workflow checks that the tag is on `main`, matches the version in
98
- `Cargo.toml` and has a `CHANGELOG.md` entry. It then publishes the crate to crates.io,
99
- creates the GitHub Release from the changelog entry, and publishes the Python wheels
100
- (Linux x86_64/aarch64 glibc and musl, macOS universal2, Windows x64, sdist) to PyPI
101
- and the WebAssembly package to npm.
102
-
103
- Protect `v*` tags with a tag ruleset so only maintainers can trigger a release.
104
-
105
- All three registries use Trusted Publishing (OIDC), so no long-lived token is stored.
106
- crates.io and npm can only trust a workflow for a package that already exists, so the
107
- first release uses short-lived tokens instead:
108
-
109
- 1. Create the `release`, `pypi` and `npm` environments in the repository settings
110
- (optionally with required reviewers).
111
- 2. **crates.io**: create an API token scoped to `publish-new` and `publish-update` for the
112
- crate `astroceleste-engine`, with a short expiry, and store it as the
113
- `CARGO_REGISTRY_TOKEN` secret of the `release` environment.
114
- 3. **npm**: create a granular access token with publish rights and a short expiry, and store
115
- it as the `NPM_TOKEN` secret of the `npm` environment.
116
- 4. **PyPI**: add a *pending* trusted publisher for project `astroceleste-engine` (workflow
117
- `release.yml`, environment `pypi`). No token is needed.
118
- 5. Push the `vX.Y.Z` tag for the version in `Cargo.toml`.
119
- 6. Once the packages exist, add the trusted publisher on crates.io (crate settings →
120
- *Trusted Publishing*: this repository, workflow `release.yml`, environment `release`)
121
- and on npm (package settings: workflow `release.yml`, environment `npm`). Then delete
122
- both secrets and revoke the tokens. The workflow prefers OIDC whenever it is available.
123
-
124
98
  ## Ephemerides
125
99
 
126
100
  The engine reads NASA JPL SPK kernels (DE440s by default, DE441 for dates outside 1849–2150).
127
- Dates outside the loaded kernels are reported as errors, never approximated.
101
+ Dates outside the loaded kernels are reported as errors, never approximated. See
102
+ [docs/ephemerides.md](https://github.com/ffalcinelli/astroceleste-engine/blob/main/docs/ephemerides.md) for coverage, sizes and excerpts.
128
103
 
129
104
  ## License
130
105
 
131
106
  Licensed under either of
132
107
 
133
- - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE))
134
- - MIT license ([LICENSE-MIT](LICENSE-MIT))
108
+ - Apache License, Version 2.0 ([LICENSE-APACHE](https://github.com/ffalcinelli/astroceleste-engine/blob/main/LICENSE-APACHE))
109
+ - MIT license ([LICENSE-MIT](https://github.com/ffalcinelli/astroceleste-engine/blob/main/LICENSE-MIT))
135
110
 
136
111
  at your option.
137
112
 
113
+ The astronomical reduction (ΔT, IAU 2000A nutation, IAU 2006 precession, light-time,
114
+ deflection and aberration) is ported from [Skyfield](https://github.com/skyfielders/python-skyfield)
115
+ by Brandon Rhodes, used under the MIT license: see
116
+ [NOTICE](https://github.com/ffalcinelli/astroceleste-engine/blob/main/NOTICE), which is shipped in
117
+ every package.
118
+
138
119
  Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion
139
120
  in this work, as defined in the Apache-2.0 license, shall be dual licensed as above, without
140
121
  any additional terms or conditions.
@@ -155,7 +155,55 @@ pub fn rise_set(
155
155
  let t_start = Time::from_ut1(jd_utc - 1.5);
156
156
  let t_end = Time::from_ut1(jd_utc + 1.5);
157
157
  let events = find_transitions(kernel, &site, t_start.tt(), t_end.tt())?;
158
+ Ok(select_rise_set(&events, jd_utc))
159
+ }
160
+
161
+ /// Sunrises and sunsets over a span, found once for all the days in it.
162
+ pub(crate) struct SunEvents {
163
+ /// (TT Julian date, whether it is a sunrise), in time order.
164
+ events: Vec<(f64, bool)>,
165
+ /// TT span searched.
166
+ span: (f64, f64),
167
+ }
168
+
169
+ impl SunEvents {
170
+ /// Every sunrise and sunset from UT1 Julian date `jd0` to `jd1`, when a single kernel
171
+ /// covers the whole span (`None` otherwise).
172
+ pub fn find(
173
+ kernels: &KernelSet,
174
+ jd0: f64,
175
+ jd1: f64,
176
+ latitude: f64,
177
+ longitude: f64,
178
+ ) -> Result<Option<Self>, EngineError> {
179
+ let kernel = require_kernel(kernels, jd0)?;
180
+ if !(kernel.start_jd..=kernel.end_jd).contains(&jd1) {
181
+ return Ok(None);
182
+ }
183
+ let site = Site::new(latitude, longitude);
184
+ let span = (Time::from_ut1(jd0).tt(), Time::from_ut1(jd1).tt());
185
+ let events = find_transitions(kernel, &site, span.0, span.1)?;
186
+ Ok(Some(SunEvents { events, span }))
187
+ }
188
+
189
+ /// [`rise_set`] for `jd_utc`, from the events of its ±1.5 day window; `None` when
190
+ /// the window is not inside the span searched.
191
+ pub fn rise_set(&self, jd_utc: f64) -> Option<(f64, f64, f64)> {
192
+ let (t0, t1) = (
193
+ Time::from_ut1(jd_utc - 1.5).tt(),
194
+ Time::from_ut1(jd_utc + 1.5).tt(),
195
+ );
196
+ if t0 < self.span.0 || t1 > self.span.1 {
197
+ return None;
198
+ }
199
+ let from = self.events.partition_point(|e| e.0 < t0);
200
+ let to = self.events.partition_point(|e| e.0 <= t1);
201
+ Some(select_rise_set(&self.events[from..to], jd_utc))
202
+ }
203
+ }
158
204
 
205
+ /// The day's sunrise, sunset and next sunrise among the events around `jd_utc`.
206
+ fn select_rise_set(events: &[(f64, bool)], jd_utc: f64) -> (f64, f64, f64) {
159
207
  let ut1 = |jd_tt: f64| Time::from_tt_jd(jd_tt).ut1();
160
208
  let sunrises: Vec<f64> = events.iter().filter(|e| e.1).map(|e| ut1(e.0)).collect();
161
209
  let sunsets: Vec<f64> = events.iter().filter(|e| !e.1).map(|e| ut1(e.0)).collect();
@@ -176,5 +224,5 @@ pub fn rise_set(
176
224
  .copied()
177
225
  .find(|&s| s > rise + 0.1)
178
226
  .unwrap_or(rise + 1.0);
179
- Ok((rise, set, next_rise))
227
+ (rise, set, next_rise)
180
228
  }
@@ -8,7 +8,7 @@ use crate::aspects::{natal_aspects, Aspect, OrbSettings, Point};
8
8
  use crate::ephemeris::KernelSet;
9
9
  use crate::error::EngineError;
10
10
  use crate::fixed_stars::{fixed_stars, FixedStarPosition};
11
- use crate::houses::{calculate_houses, HouseSystem};
11
+ use crate::houses::{calculate_houses, HouseSystem, Houses};
12
12
  use crate::instant::UtcInstant;
13
13
  use crate::lots::{arabic_parts, Lot};
14
14
  use crate::lunar::{lunar_status, LunarStatus};
@@ -16,7 +16,7 @@ use crate::planets::calculate_planets;
16
16
  use crate::symbolic::symbolic_degree_number;
17
17
  use crate::temperament::{temperament, Temperament};
18
18
  use crate::zodiac::{
19
- ayanamsa, ayanamsa_info, determine_house, longitude_to_zodiac, DEFAULT_AYANAMSA,
19
+ ayanamsa, ayanamsa_info, determine_house, longitude_to_zodiac, AyanamsaInfo, DEFAULT_AYANAMSA,
20
20
  };
21
21
 
22
22
  /// Every body a complete chart carries; missing ones are reported in
@@ -200,15 +200,28 @@ fn symbol_of(name: &str) -> &'static str {
200
200
  }
201
201
  }
202
202
 
203
- /// Compute a chart (`calculate_chart_data`).
204
- pub fn calculate_chart(kernels: &KernelSet, req: &ChartRequest) -> Result<Chart, EngineError> {
203
+ /// The positions part of a chart: placements and house cusps, without aspects, fixed
204
+ /// stars, lots or temperament. Electional searches evaluate thousands of these.
205
+ pub(crate) struct Sky {
206
+ pub zodiac_type: &'static str,
207
+ pub is_sidereal: bool,
208
+ pub ayanamsa_code: &'static str,
209
+ pub info: AyanamsaInfo,
210
+ pub jd: f64,
211
+ /// Cusps 1-12, degrees.
212
+ pub cusps: Vec<f64>,
213
+ pub house_cusps: Vec<HouseCusp>,
214
+ pub planets: Vec<Placement>,
215
+ }
216
+
217
+ /// Placements and houses of a request (the first half of [`calculate_chart`]).
218
+ pub(crate) fn sky(kernels: &KernelSet, req: &ChartRequest) -> Result<Sky, EngineError> {
205
219
  let zodiac_type = match req.zodiac_type.trim().to_lowercase().as_str() {
206
220
  "sidereal" => "sidereal",
207
221
  _ => "tropical",
208
222
  };
209
223
  let is_sidereal = zodiac_type == "sidereal";
210
224
  let ayanamsa_code = ayanamsa(req.ayanamsa).code;
211
- let orbs = OrbSettings::merge(req.orb_settings)?;
212
225
 
213
226
  let jd = req.instant.julian_day();
214
227
  let info = ayanamsa_info(jd, ayanamsa_code);
@@ -242,16 +255,34 @@ pub fn calculate_chart(kernels: &KernelSet, req: &ChartRequest) -> Result<Chart,
242
255
  })
243
256
  .collect();
244
257
 
245
- let mut raw: Vec<(&'static str, f64, f64, bool)> = engine
258
+ let raw: Vec<(&'static str, f64, f64, bool)> = engine
246
259
  .bodies
247
260
  .iter()
248
261
  .map(|b| (b.name, b.longitude, b.speed, b.is_retrograde))
249
262
  .collect();
263
+ let planets = placements(raw, &houses);
264
+
265
+ Ok(Sky {
266
+ zodiac_type,
267
+ is_sidereal,
268
+ ayanamsa_code,
269
+ info,
270
+ jd,
271
+ cusps,
272
+ house_cusps,
273
+ planets,
274
+ })
275
+ }
276
+
277
+ /// Placements of bodies given as (name, longitude, speed, retrograde), followed by the
278
+ /// Ascendant and the Midheaven, with their houses.
279
+ pub(crate) fn placements(
280
+ mut raw: Vec<(&'static str, f64, f64, bool)>,
281
+ houses: &Houses,
282
+ ) -> Vec<Placement> {
250
283
  raw.push(("Ascendant", houses.ascendant, 0.0, false));
251
284
  raw.push(("Midheaven", houses.midheaven, 0.0, false));
252
-
253
- let planets: Vec<Placement> = raw
254
- .iter()
285
+ raw.iter()
255
286
  .map(|&(name, lon, speed, is_retrograde)| {
256
287
  let z = longitude_to_zodiac(lon);
257
288
  Placement {
@@ -262,13 +293,28 @@ pub fn calculate_chart(kernels: &KernelSet, req: &ChartRequest) -> Result<Chart,
262
293
  degree: z.degree,
263
294
  minute: z.minute,
264
295
  ecliptic_longitude: lon,
265
- house: determine_house(lon, &cusps),
296
+ house: determine_house(lon, &houses.cusps),
266
297
  speed,
267
298
  is_retrograde,
268
299
  symbolic_degree: symbolic_degree_number(z.degree, z.minute),
269
300
  }
270
301
  })
271
- .collect();
302
+ .collect()
303
+ }
304
+
305
+ /// Compute a chart (`calculate_chart_data`).
306
+ pub fn calculate_chart(kernels: &KernelSet, req: &ChartRequest) -> Result<Chart, EngineError> {
307
+ let orbs = OrbSettings::merge(req.orb_settings)?;
308
+ let Sky {
309
+ zodiac_type,
310
+ is_sidereal,
311
+ ayanamsa_code,
312
+ info,
313
+ jd,
314
+ cusps,
315
+ house_cusps,
316
+ planets,
317
+ } = sky(kernels, req)?;
272
318
  let points: Vec<Point> = planets
273
319
  .iter()
274
320
  .map(|p| Point {