natal-chart 0.1.0__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.
- natal_chart-0.1.0/.gitignore +8 -0
- natal_chart-0.1.0/CHANGELOG.md +7 -0
- natal_chart-0.1.0/LICENSE +21 -0
- natal_chart-0.1.0/PKG-INFO +206 -0
- natal_chart-0.1.0/README.md +174 -0
- natal_chart-0.1.0/pyproject.toml +58 -0
- natal_chart-0.1.0/src/natal_chart/__init__.py +42 -0
- natal_chart-0.1.0/src/natal_chart/_angles.py +69 -0
- natal_chart-0.1.0/src/natal_chart/_ephemeris.py +61 -0
- natal_chart-0.1.0/src/natal_chart/aspects.py +136 -0
- natal_chart-0.1.0/src/natal_chart/chart.py +256 -0
- natal_chart-0.1.0/src/natal_chart/cli.py +130 -0
- natal_chart-0.1.0/src/natal_chart/houses.py +202 -0
- natal_chart-0.1.0/src/natal_chart/points.py +76 -0
- natal_chart-0.1.0/src/natal_chart/py.typed +0 -0
- natal_chart-0.1.0/tests/conftest.py +16 -0
- natal_chart-0.1.0/tests/test_chart.py +118 -0
- natal_chart-0.1.0/tests/test_houses.py +83 -0
- natal_chart-0.1.0/tests/test_points_and_aspects.py +75 -0
- natal_chart-0.1.0/tests/test_vs_swisseph.py +137 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 - unreleased
|
|
4
|
+
|
|
5
|
+
First release: planets Sun-Pluto (JPL DE440s via Skyfield), mean node, mean
|
|
6
|
+
Lilith, Placidus/Koch/Porphyry/Equal/Whole Sign houses with a polar-latitude
|
|
7
|
+
error or fallback, major aspects with applying/separating, transits, CLI.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Victor Balakirev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: natal-chart
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Tropical natal chart math (planets, Placidus/Koch/whole-sign houses, aspects, transits) on NASA JPL ephemerides via Skyfield. No Swiss Ephemeris.
|
|
5
|
+
Project-URL: Homepage, https://astralread.com
|
|
6
|
+
Project-URL: Repository, https://github.com/VictorMachiavelli/natal-chart
|
|
7
|
+
Project-URL: Issues, https://github.com/VictorMachiavelli/natal-chart/issues
|
|
8
|
+
Author-email: Victor Balakirev <balakirevvictor@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: aspects,astrology,ephemeris,horoscope,houses,jpl,natal chart,placidus,skyfield,transits
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Astronomy
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: skyfield>=1.45
|
|
27
|
+
Requires-Dist: tzdata; sys_platform == 'win32'
|
|
28
|
+
Provides-Extra: test
|
|
29
|
+
Requires-Dist: pyswisseph>=2.10; extra == 'test'
|
|
30
|
+
Requires-Dist: pytest>=7; extra == 'test'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# natal-chart
|
|
34
|
+
|
|
35
|
+
Tropical natal chart math for Python: planet positions, houses
|
|
36
|
+
(Placidus, Koch, Porphyry, Equal, Whole Sign), Ascendant/MC, mean lunar node,
|
|
37
|
+
mean Black Moon Lilith, major aspects and transits.
|
|
38
|
+
|
|
39
|
+
Positions come from NASA JPL ephemerides through
|
|
40
|
+
[Skyfield](https://rhodesmill.org/skyfield/). The package does **not** use or
|
|
41
|
+
contain the Swiss Ephemeris, so it can be released under the MIT license.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install natal-chart
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Requires Python 3.10+. The only dependency is Skyfield (which brings NumPy).
|
|
50
|
+
|
|
51
|
+
The first call downloads the JPL DE440s ephemeris (~32 MB, covers
|
|
52
|
+
1849-2150) into `~/.cache/natal-chart` (override with the `NATAL_CHART_DATA`
|
|
53
|
+
environment variable). It is never bundled with the package. To use a
|
|
54
|
+
different file, e.g. the smaller DE421 (1900-2050) or DE440 (1550-2650):
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import natal_chart
|
|
58
|
+
natal_chart.set_ephemeris("de421.bsp") # or NATAL_CHART_EPHEMERIS=de421.bsp
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Quickstart
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from natal_chart import chart, aspects, transits
|
|
65
|
+
|
|
66
|
+
# Fictional birth data: local time + IANA time zone
|
|
67
|
+
c = chart("1990-05-17T14:30", lat=55.75, lon=37.62, tz="Europe/Moscow")
|
|
68
|
+
|
|
69
|
+
c["Sun"].sign, round(c["Sun"].degree, 2) # ('Taurus', 26.26)
|
|
70
|
+
c["Saturn"].retrograde # True
|
|
71
|
+
c["Moon"].house # 5
|
|
72
|
+
c.ascendant.longitude, c.mc.longitude # 166.69..., 71.52...
|
|
73
|
+
c.cusps # 12 cusp longitudes, cusp 1 first
|
|
74
|
+
|
|
75
|
+
for a in aspects(c)[:3]:
|
|
76
|
+
print(a.first, a.aspect, a.second, round(a.orb, 2),
|
|
77
|
+
"applying" if a.applying else "separating")
|
|
78
|
+
|
|
79
|
+
for a in transits(c, "2026-11-10T12:00:00+00:00"):
|
|
80
|
+
print("transiting", a.first, a.aspect, "natal", a.second, round(a.orb, 2))
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`chart()` accepts a timezone-aware `datetime`, or a naive one plus `tz=`.
|
|
84
|
+
A naive time without `tz` is rejected rather than silently treated as UTC.
|
|
85
|
+
|
|
86
|
+
Points: Sun, Moon, Mercury, Venus, Mars, Jupiter, Saturn, Uranus, Neptune,
|
|
87
|
+
Pluto, `"Mean Node"`, `"Lilith"`, `"Ascendant"`, `"MC"`. Each `Point` has
|
|
88
|
+
`longitude`, `latitude`, `speed` (°/day), `sign`, `degree` (within sign),
|
|
89
|
+
`retrograde` and `house`.
|
|
90
|
+
|
|
91
|
+
### Aspects
|
|
92
|
+
|
|
93
|
+
`aspects(chart, orbs=None, points=None)` finds conjunction, sextile, square,
|
|
94
|
+
trine and opposition, tightest first. Default orbs:
|
|
95
|
+
conjunction/opposition 8°, trine/square 7°, sextile 5°. Pass your own
|
|
96
|
+
mapping, e.g. `orbs={"conjunction": 10, "square": 6}` (aspects left out are
|
|
97
|
+
not searched). `applying` is decided from the current speeds of both points.
|
|
98
|
+
|
|
99
|
+
`transits(natal, when, orbs=None)` compares the transiting planets with every
|
|
100
|
+
natal point (default orb 1°). The natal point is treated as fixed.
|
|
101
|
+
|
|
102
|
+
### Command line
|
|
103
|
+
|
|
104
|
+
```text
|
|
105
|
+
$ natal-chart 1990-05-17T14:30 --tz Europe/Moscow --lat 55.75 --lon 37.62 --aspects
|
|
106
|
+
1990-05-17 10:30:00 UTC lat 55.7500 lon 37.6200 houses: placidus
|
|
107
|
+
|
|
108
|
+
Point Sign Degree Longitude Speed R House
|
|
109
|
+
Sun Taurus 26°15'49" 56.2636 0.9636 9
|
|
110
|
+
Moon Aquarius 21°37'13" 321.6204 12.9387 5
|
|
111
|
+
...
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Options: `--houses {placidus,koch,porphyry,equal,whole_sign}`,
|
|
115
|
+
`--polar-fallback {porphyry,equal,whole_sign}`, `--aspects`,
|
|
116
|
+
`--transits DATETIME`, `--json`. Without `--tz` the time is taken as UTC.
|
|
117
|
+
|
|
118
|
+
## What exactly is computed
|
|
119
|
+
|
|
120
|
+
* **Planets**: geocentric *apparent* positions (light-time, aberration,
|
|
121
|
+
gravitational deflection), ecliptic longitude and latitude referred to the
|
|
122
|
+
**true equinox and ecliptic of date** (IAU 2006 precession + IAU 2000A
|
|
123
|
+
nutation), tropical zodiac. This is the same convention as the default
|
|
124
|
+
output of most astrology software. Mars-Pluto use the planetary-system
|
|
125
|
+
barycenter segments of the JPL file; the offset from the planet's centre
|
|
126
|
+
is below 0.1" as seen from Earth.
|
|
127
|
+
* **Speed**: central difference over ±0.01 day, degrees per day.
|
|
128
|
+
* **Mean node / mean Lilith**: ELP-2000/82 mean-element polynomials
|
|
129
|
+
(Meeus, *Astronomical Algorithms*, ch. 47), plus nutation in longitude.
|
|
130
|
+
Lilith (mean lunar apogee) is projected from the inclined lunar orbit onto
|
|
131
|
+
the ecliptic, so it also has a small ecliptic latitude.
|
|
132
|
+
* **Houses**: RAMC from Skyfield's Greenwich apparent sidereal time plus the
|
|
133
|
+
east longitude; true obliquity of date. Placidus is computed by iterating
|
|
134
|
+
each cusp on its own diurnal/nocturnal semi-arc; Koch from Ascendants at
|
|
135
|
+
the times that trisect the MC degree's semi-arc.
|
|
136
|
+
* **Time**: from 1972 the input is UTC with leap seconds. Before 1972 the
|
|
137
|
+
given UTC is treated as UT1 (civil time followed UT then); Delta T comes
|
|
138
|
+
from Skyfield's built-in model. Ambiguous or non-existent local times at
|
|
139
|
+
DST changes follow Python's `datetime.fold` rule (`fold=0` by default).
|
|
140
|
+
|
|
141
|
+
### High latitudes
|
|
142
|
+
|
|
143
|
+
Placidus and Koch are undefined when part of the ecliptic never rises or sets,
|
|
144
|
+
i.e. for |latitude| ≥ 90° − obliquity (about 66.56°). There `chart()` raises
|
|
145
|
+
`PolarHousesError` with an explanation. Pass
|
|
146
|
+
`polar_fallback="porphyry"` (or `"equal"`, `"whole_sign"`) to get a chart
|
|
147
|
+
anyway; then `chart.house_system` names the system actually used and
|
|
148
|
+
`chart.polar_fallback_used` is `True`. Equal, Whole Sign and Porphyry work at
|
|
149
|
+
any latitude short of the poles.
|
|
150
|
+
|
|
151
|
+
## Accuracy
|
|
152
|
+
|
|
153
|
+
Measured against the Swiss Ephemeris 2.10 (pyswisseph, with its
|
|
154
|
+
`sepl_18`/`semo_18` data files) on 300 random fictional dates 1900-2100 and
|
|
155
|
+
latitudes within ±66°. The comparison is run at identical instants (same TT
|
|
156
|
+
for bodies, same UT1 for houses) so that what is measured is ephemeris,
|
|
157
|
+
reduction and house math, not the two libraries' Delta T models.
|
|
158
|
+
|
|
159
|
+
| Quantity | Max difference |
|
|
160
|
+
| --- | --- |
|
|
161
|
+
| Sun ... Pluto, longitude | 0.007" |
|
|
162
|
+
| Sun ... Pluto, latitude | 0.003" |
|
|
163
|
+
| Speeds | 0.00008 °/day |
|
|
164
|
+
| Mean Node | 0.11" |
|
|
165
|
+
| Mean Lilith (longitude / latitude) | 0.54" / 0.04" |
|
|
166
|
+
| Ascendant, MC | 0.000003° |
|
|
167
|
+
| Placidus, Koch, Porphyry, Equal cusps | 0.000006° |
|
|
168
|
+
|
|
169
|
+
Things that differ between libraries and are *not* in the table:
|
|
170
|
+
|
|
171
|
+
* **Delta T for future dates.** Neither library can know future Earth
|
|
172
|
+
rotation; their predictions differ by a few seconds after 2026, which moves
|
|
173
|
+
the Moon (0.5"/s) by up to ~13" at the same civil time (we measured 0.003"
|
|
174
|
+
for 1972-2025 and 0.4" for 1900-1971).
|
|
175
|
+
* **Sidereal time after 2050.** From 2050-01-01 the Swiss Ephemeris switches
|
|
176
|
+
to its own long-term sidereal-time formula, which differs from the IERS
|
|
177
|
+
expression used here by ~1.9" (0.13 s of time).
|
|
178
|
+
|
|
179
|
+
The comparison test (`tests/test_vs_swisseph.py`) runs only when the
|
|
180
|
+
optional `pyswisseph` dev dependency is installed; point `SE_EPHE_PATH` at the
|
|
181
|
+
Swiss data files to compare against the full Swiss Ephemeris instead of its
|
|
182
|
+
built-in Moshier fallback.
|
|
183
|
+
|
|
184
|
+
## Ephemeris data
|
|
185
|
+
|
|
186
|
+
NASA JPL DE440 (public domain) via Skyfield. Default file DE440s; the
|
|
187
|
+
file is downloaded on first use and is not part of this package.
|
|
188
|
+
|
|
189
|
+
## Development
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
python -m venv .venv && .venv/bin/pip install -e ".[test]"
|
|
193
|
+
.venv/bin/pytest -s
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
All birth data in the tests is fictional. `pyswisseph` (AGPL) appears only in
|
|
197
|
+
the optional test extra and is never imported by the package.
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
MIT. See [LICENSE](LICENSE).
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
Built by the team behind [AstralRead](https://astralread.com) — free natal
|
|
206
|
+
chart & AI astrologer.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# natal-chart
|
|
2
|
+
|
|
3
|
+
Tropical natal chart math for Python: planet positions, houses
|
|
4
|
+
(Placidus, Koch, Porphyry, Equal, Whole Sign), Ascendant/MC, mean lunar node,
|
|
5
|
+
mean Black Moon Lilith, major aspects and transits.
|
|
6
|
+
|
|
7
|
+
Positions come from NASA JPL ephemerides through
|
|
8
|
+
[Skyfield](https://rhodesmill.org/skyfield/). The package does **not** use or
|
|
9
|
+
contain the Swiss Ephemeris, so it can be released under the MIT license.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install natal-chart
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Requires Python 3.10+. The only dependency is Skyfield (which brings NumPy).
|
|
18
|
+
|
|
19
|
+
The first call downloads the JPL DE440s ephemeris (~32 MB, covers
|
|
20
|
+
1849-2150) into `~/.cache/natal-chart` (override with the `NATAL_CHART_DATA`
|
|
21
|
+
environment variable). It is never bundled with the package. To use a
|
|
22
|
+
different file, e.g. the smaller DE421 (1900-2050) or DE440 (1550-2650):
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import natal_chart
|
|
26
|
+
natal_chart.set_ephemeris("de421.bsp") # or NATAL_CHART_EPHEMERIS=de421.bsp
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quickstart
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from natal_chart import chart, aspects, transits
|
|
33
|
+
|
|
34
|
+
# Fictional birth data: local time + IANA time zone
|
|
35
|
+
c = chart("1990-05-17T14:30", lat=55.75, lon=37.62, tz="Europe/Moscow")
|
|
36
|
+
|
|
37
|
+
c["Sun"].sign, round(c["Sun"].degree, 2) # ('Taurus', 26.26)
|
|
38
|
+
c["Saturn"].retrograde # True
|
|
39
|
+
c["Moon"].house # 5
|
|
40
|
+
c.ascendant.longitude, c.mc.longitude # 166.69..., 71.52...
|
|
41
|
+
c.cusps # 12 cusp longitudes, cusp 1 first
|
|
42
|
+
|
|
43
|
+
for a in aspects(c)[:3]:
|
|
44
|
+
print(a.first, a.aspect, a.second, round(a.orb, 2),
|
|
45
|
+
"applying" if a.applying else "separating")
|
|
46
|
+
|
|
47
|
+
for a in transits(c, "2026-11-10T12:00:00+00:00"):
|
|
48
|
+
print("transiting", a.first, a.aspect, "natal", a.second, round(a.orb, 2))
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`chart()` accepts a timezone-aware `datetime`, or a naive one plus `tz=`.
|
|
52
|
+
A naive time without `tz` is rejected rather than silently treated as UTC.
|
|
53
|
+
|
|
54
|
+
Points: Sun, Moon, Mercury, Venus, Mars, Jupiter, Saturn, Uranus, Neptune,
|
|
55
|
+
Pluto, `"Mean Node"`, `"Lilith"`, `"Ascendant"`, `"MC"`. Each `Point` has
|
|
56
|
+
`longitude`, `latitude`, `speed` (°/day), `sign`, `degree` (within sign),
|
|
57
|
+
`retrograde` and `house`.
|
|
58
|
+
|
|
59
|
+
### Aspects
|
|
60
|
+
|
|
61
|
+
`aspects(chart, orbs=None, points=None)` finds conjunction, sextile, square,
|
|
62
|
+
trine and opposition, tightest first. Default orbs:
|
|
63
|
+
conjunction/opposition 8°, trine/square 7°, sextile 5°. Pass your own
|
|
64
|
+
mapping, e.g. `orbs={"conjunction": 10, "square": 6}` (aspects left out are
|
|
65
|
+
not searched). `applying` is decided from the current speeds of both points.
|
|
66
|
+
|
|
67
|
+
`transits(natal, when, orbs=None)` compares the transiting planets with every
|
|
68
|
+
natal point (default orb 1°). The natal point is treated as fixed.
|
|
69
|
+
|
|
70
|
+
### Command line
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
$ natal-chart 1990-05-17T14:30 --tz Europe/Moscow --lat 55.75 --lon 37.62 --aspects
|
|
74
|
+
1990-05-17 10:30:00 UTC lat 55.7500 lon 37.6200 houses: placidus
|
|
75
|
+
|
|
76
|
+
Point Sign Degree Longitude Speed R House
|
|
77
|
+
Sun Taurus 26°15'49" 56.2636 0.9636 9
|
|
78
|
+
Moon Aquarius 21°37'13" 321.6204 12.9387 5
|
|
79
|
+
...
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Options: `--houses {placidus,koch,porphyry,equal,whole_sign}`,
|
|
83
|
+
`--polar-fallback {porphyry,equal,whole_sign}`, `--aspects`,
|
|
84
|
+
`--transits DATETIME`, `--json`. Without `--tz` the time is taken as UTC.
|
|
85
|
+
|
|
86
|
+
## What exactly is computed
|
|
87
|
+
|
|
88
|
+
* **Planets**: geocentric *apparent* positions (light-time, aberration,
|
|
89
|
+
gravitational deflection), ecliptic longitude and latitude referred to the
|
|
90
|
+
**true equinox and ecliptic of date** (IAU 2006 precession + IAU 2000A
|
|
91
|
+
nutation), tropical zodiac. This is the same convention as the default
|
|
92
|
+
output of most astrology software. Mars-Pluto use the planetary-system
|
|
93
|
+
barycenter segments of the JPL file; the offset from the planet's centre
|
|
94
|
+
is below 0.1" as seen from Earth.
|
|
95
|
+
* **Speed**: central difference over ±0.01 day, degrees per day.
|
|
96
|
+
* **Mean node / mean Lilith**: ELP-2000/82 mean-element polynomials
|
|
97
|
+
(Meeus, *Astronomical Algorithms*, ch. 47), plus nutation in longitude.
|
|
98
|
+
Lilith (mean lunar apogee) is projected from the inclined lunar orbit onto
|
|
99
|
+
the ecliptic, so it also has a small ecliptic latitude.
|
|
100
|
+
* **Houses**: RAMC from Skyfield's Greenwich apparent sidereal time plus the
|
|
101
|
+
east longitude; true obliquity of date. Placidus is computed by iterating
|
|
102
|
+
each cusp on its own diurnal/nocturnal semi-arc; Koch from Ascendants at
|
|
103
|
+
the times that trisect the MC degree's semi-arc.
|
|
104
|
+
* **Time**: from 1972 the input is UTC with leap seconds. Before 1972 the
|
|
105
|
+
given UTC is treated as UT1 (civil time followed UT then); Delta T comes
|
|
106
|
+
from Skyfield's built-in model. Ambiguous or non-existent local times at
|
|
107
|
+
DST changes follow Python's `datetime.fold` rule (`fold=0` by default).
|
|
108
|
+
|
|
109
|
+
### High latitudes
|
|
110
|
+
|
|
111
|
+
Placidus and Koch are undefined when part of the ecliptic never rises or sets,
|
|
112
|
+
i.e. for |latitude| ≥ 90° − obliquity (about 66.56°). There `chart()` raises
|
|
113
|
+
`PolarHousesError` with an explanation. Pass
|
|
114
|
+
`polar_fallback="porphyry"` (or `"equal"`, `"whole_sign"`) to get a chart
|
|
115
|
+
anyway; then `chart.house_system` names the system actually used and
|
|
116
|
+
`chart.polar_fallback_used` is `True`. Equal, Whole Sign and Porphyry work at
|
|
117
|
+
any latitude short of the poles.
|
|
118
|
+
|
|
119
|
+
## Accuracy
|
|
120
|
+
|
|
121
|
+
Measured against the Swiss Ephemeris 2.10 (pyswisseph, with its
|
|
122
|
+
`sepl_18`/`semo_18` data files) on 300 random fictional dates 1900-2100 and
|
|
123
|
+
latitudes within ±66°. The comparison is run at identical instants (same TT
|
|
124
|
+
for bodies, same UT1 for houses) so that what is measured is ephemeris,
|
|
125
|
+
reduction and house math, not the two libraries' Delta T models.
|
|
126
|
+
|
|
127
|
+
| Quantity | Max difference |
|
|
128
|
+
| --- | --- |
|
|
129
|
+
| Sun ... Pluto, longitude | 0.007" |
|
|
130
|
+
| Sun ... Pluto, latitude | 0.003" |
|
|
131
|
+
| Speeds | 0.00008 °/day |
|
|
132
|
+
| Mean Node | 0.11" |
|
|
133
|
+
| Mean Lilith (longitude / latitude) | 0.54" / 0.04" |
|
|
134
|
+
| Ascendant, MC | 0.000003° |
|
|
135
|
+
| Placidus, Koch, Porphyry, Equal cusps | 0.000006° |
|
|
136
|
+
|
|
137
|
+
Things that differ between libraries and are *not* in the table:
|
|
138
|
+
|
|
139
|
+
* **Delta T for future dates.** Neither library can know future Earth
|
|
140
|
+
rotation; their predictions differ by a few seconds after 2026, which moves
|
|
141
|
+
the Moon (0.5"/s) by up to ~13" at the same civil time (we measured 0.003"
|
|
142
|
+
for 1972-2025 and 0.4" for 1900-1971).
|
|
143
|
+
* **Sidereal time after 2050.** From 2050-01-01 the Swiss Ephemeris switches
|
|
144
|
+
to its own long-term sidereal-time formula, which differs from the IERS
|
|
145
|
+
expression used here by ~1.9" (0.13 s of time).
|
|
146
|
+
|
|
147
|
+
The comparison test (`tests/test_vs_swisseph.py`) runs only when the
|
|
148
|
+
optional `pyswisseph` dev dependency is installed; point `SE_EPHE_PATH` at the
|
|
149
|
+
Swiss data files to compare against the full Swiss Ephemeris instead of its
|
|
150
|
+
built-in Moshier fallback.
|
|
151
|
+
|
|
152
|
+
## Ephemeris data
|
|
153
|
+
|
|
154
|
+
NASA JPL DE440 (public domain) via Skyfield. Default file DE440s; the
|
|
155
|
+
file is downloaded on first use and is not part of this package.
|
|
156
|
+
|
|
157
|
+
## Development
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
python -m venv .venv && .venv/bin/pip install -e ".[test]"
|
|
161
|
+
.venv/bin/pytest -s
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
All birth data in the tests is fictional. `pyswisseph` (AGPL) appears only in
|
|
165
|
+
the optional test extra and is never imported by the package.
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
|
|
169
|
+
MIT. See [LICENSE](LICENSE).
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
Built by the team behind [AstralRead](https://astralread.com) — free natal
|
|
174
|
+
chart & AI astrologer.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.21"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "natal-chart"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Tropical natal chart math (planets, Placidus/Koch/whole-sign houses, aspects, transits) on NASA JPL ephemerides via Skyfield. No Swiss Ephemeris."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
authors = [{ name = "Victor Balakirev", email = "balakirevvictor@gmail.com" }]
|
|
14
|
+
keywords = ["astrology", "natal chart", "horoscope", "ephemeris", "placidus", "houses", "aspects", "transits", "skyfield", "jpl"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Programming Language :: Python :: 3.14",
|
|
27
|
+
"Topic :: Scientific/Engineering :: Astronomy",
|
|
28
|
+
"Typing :: Typed",
|
|
29
|
+
]
|
|
30
|
+
dependencies = [
|
|
31
|
+
"skyfield>=1.45",
|
|
32
|
+
"tzdata; sys_platform == 'win32'",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
# pyswisseph (AGPL) is used ONLY by the optional cross-check tests; it is
|
|
37
|
+
# never imported by the package itself.
|
|
38
|
+
test = ["pytest>=7", "pyswisseph>=2.10"]
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://astralread.com"
|
|
42
|
+
Repository = "https://github.com/VictorMachiavelli/natal-chart"
|
|
43
|
+
Issues = "https://github.com/VictorMachiavelli/natal-chart/issues"
|
|
44
|
+
|
|
45
|
+
[project.scripts]
|
|
46
|
+
natal-chart = "natal_chart.cli:main"
|
|
47
|
+
|
|
48
|
+
[tool.hatch.version]
|
|
49
|
+
path = "src/natal_chart/__init__.py"
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.wheel]
|
|
52
|
+
packages = ["src/natal_chart"]
|
|
53
|
+
|
|
54
|
+
[tool.hatch.build.targets.sdist]
|
|
55
|
+
include = ["src/natal_chart", "tests", "README.md", "LICENSE", "CHANGELOG.md"]
|
|
56
|
+
|
|
57
|
+
[tool.pytest.ini_options]
|
|
58
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""natal-chart: tropical natal chart math on NASA JPL ephemerides (via Skyfield).
|
|
2
|
+
|
|
3
|
+
>>> from natal_chart import chart, aspects
|
|
4
|
+
>>> c = chart("1990-05-17T14:30", lat=55.75, lon=37.62, tz="Europe/Moscow")
|
|
5
|
+
>>> c["Sun"].sign
|
|
6
|
+
'Taurus'
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.0"
|
|
12
|
+
|
|
13
|
+
from ._ephemeris import data_dir, set_ephemeris
|
|
14
|
+
from .aspects import (
|
|
15
|
+
ASPECT_ANGLES,
|
|
16
|
+
DEFAULT_ORBS,
|
|
17
|
+
DEFAULT_TRANSIT_ORBS,
|
|
18
|
+
Aspect,
|
|
19
|
+
aspects,
|
|
20
|
+
transits,
|
|
21
|
+
)
|
|
22
|
+
from .chart import PLANETS, Chart, Point, chart, positions
|
|
23
|
+
from .houses import HOUSE_SYSTEMS, PolarHousesError
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"ASPECT_ANGLES",
|
|
27
|
+
"DEFAULT_ORBS",
|
|
28
|
+
"DEFAULT_TRANSIT_ORBS",
|
|
29
|
+
"HOUSE_SYSTEMS",
|
|
30
|
+
"PLANETS",
|
|
31
|
+
"Aspect",
|
|
32
|
+
"Chart",
|
|
33
|
+
"Point",
|
|
34
|
+
"PolarHousesError",
|
|
35
|
+
"aspects",
|
|
36
|
+
"chart",
|
|
37
|
+
"data_dir",
|
|
38
|
+
"positions",
|
|
39
|
+
"set_ephemeris",
|
|
40
|
+
"transits",
|
|
41
|
+
"__version__",
|
|
42
|
+
]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Small angle helpers (degrees)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
|
|
7
|
+
SIGNS: tuple[str, ...] = (
|
|
8
|
+
"Aries",
|
|
9
|
+
"Taurus",
|
|
10
|
+
"Gemini",
|
|
11
|
+
"Cancer",
|
|
12
|
+
"Leo",
|
|
13
|
+
"Virgo",
|
|
14
|
+
"Libra",
|
|
15
|
+
"Scorpio",
|
|
16
|
+
"Sagittarius",
|
|
17
|
+
"Capricorn",
|
|
18
|
+
"Aquarius",
|
|
19
|
+
"Pisces",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def norm360(x: float) -> float:
|
|
24
|
+
"""Normalise an angle to [0, 360)."""
|
|
25
|
+
x = math.fmod(x, 360.0)
|
|
26
|
+
if x < 0:
|
|
27
|
+
x += 360.0
|
|
28
|
+
# fmod can return 360.0 for tiny negative inputs after the addition
|
|
29
|
+
return 0.0 if x >= 360.0 else x
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def norm180(x: float) -> float:
|
|
33
|
+
"""Normalise an angle to [-180, 180)."""
|
|
34
|
+
return norm360(x + 180.0) - 180.0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def sind(x: float) -> float:
|
|
38
|
+
return math.sin(math.radians(x))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def cosd(x: float) -> float:
|
|
42
|
+
return math.cos(math.radians(x))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def tand(x: float) -> float:
|
|
46
|
+
return math.tan(math.radians(x))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def asind(x: float) -> float:
|
|
50
|
+
return math.degrees(math.asin(max(-1.0, min(1.0, x))))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def atan2d(y: float, x: float) -> float:
|
|
54
|
+
return math.degrees(math.atan2(y, x))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def sign_of(longitude: float) -> tuple[str, float]:
|
|
58
|
+
"""Return (sign name, degree within sign) for an ecliptic longitude."""
|
|
59
|
+
lon = norm360(longitude)
|
|
60
|
+
idx = int(lon // 30.0)
|
|
61
|
+
return SIGNS[idx], lon - 30.0 * idx
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def format_dms(degree_in_sign: float) -> str:
|
|
65
|
+
"""Format a degree within a sign as ``DD°MM'SS"``."""
|
|
66
|
+
total = int(round(degree_in_sign * 3600.0))
|
|
67
|
+
d, rem = divmod(total, 3600)
|
|
68
|
+
m, s = divmod(rem, 60)
|
|
69
|
+
return f"{d:2d}°{m:02d}'{s:02d}\""
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Loading of the JPL ephemeris through Skyfield.
|
|
2
|
+
|
|
3
|
+
The ephemeris file is never shipped with the package. On first use Skyfield
|
|
4
|
+
downloads it into a cache directory:
|
|
5
|
+
|
|
6
|
+
* ``$NATAL_CHART_DATA`` if set, otherwise
|
|
7
|
+
* ``~/.cache/natal-chart``.
|
|
8
|
+
|
|
9
|
+
The default file is NASA JPL DE440s (1849-2150, ~32 MB). Any other
|
|
10
|
+
Skyfield-readable SPK file (for example ``de421.bsp``, 1900-2050, ~17 MB, or
|
|
11
|
+
``de440.bsp``, 1550-2650, ~114 MB) can be selected with
|
|
12
|
+
:func:`set_ephemeris` or the ``NATAL_CHART_EPHEMERIS`` environment variable.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
from functools import lru_cache
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from skyfield.api import Loader
|
|
23
|
+
|
|
24
|
+
DEFAULT_EPHEMERIS = "de440s.bsp"
|
|
25
|
+
|
|
26
|
+
_ephemeris_name: str = os.environ.get("NATAL_CHART_EPHEMERIS", DEFAULT_EPHEMERIS)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def data_dir() -> Path:
|
|
30
|
+
"""Directory where the ephemeris file is cached."""
|
|
31
|
+
env = os.environ.get("NATAL_CHART_DATA")
|
|
32
|
+
return Path(env).expanduser() if env else Path.home() / ".cache" / "natal-chart"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@lru_cache(maxsize=1)
|
|
36
|
+
def _loader() -> Loader:
|
|
37
|
+
path = data_dir()
|
|
38
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
return Loader(str(path), verbose=False)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@lru_cache(maxsize=1)
|
|
43
|
+
def timescale() -> Any:
|
|
44
|
+
"""Skyfield timescale (uses Skyfield's built-in Delta T / leap-second data)."""
|
|
45
|
+
return _loader().timescale()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@lru_cache(maxsize=4)
|
|
49
|
+
def _load(name: str) -> Any:
|
|
50
|
+
return _loader()(name)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def ephemeris() -> Any:
|
|
54
|
+
"""The currently selected JPL ephemeris, downloading it on first use."""
|
|
55
|
+
return _load(_ephemeris_name)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def set_ephemeris(name_or_path: str) -> None:
|
|
59
|
+
"""Select the SPK file to use, e.g. ``"de421.bsp"`` or a local path."""
|
|
60
|
+
global _ephemeris_name
|
|
61
|
+
_ephemeris_name = name_or_path
|