Immanuel v1.6.0
The largest release since v1.0.0 sees settings rebuilt from the ground up as per-chart config objects, charts generating roughly 5-10x faster, and aspect patterns for all charts. General chart output keeps the same shape apart from the additions listed below, so if you only ever read natal.objects and co, the upgrade path is very small.
Breaking changes
Settings are now a per-chart Config object
The global settings singleton is finally gone, and in its place is a per-chart Config class that you instantiate, customize, and pass to a chart instance.
Before:
from immanuel import charts
from immanuel.const import chart
from immanuel.setup import settings
settings.house_system = chart.CAMPANUS
settings.objects.append(chart.CERES)
native = charts.Subject("2000-01-01 10:00", "32n43", "117w09")
natal = charts.Natal(native)After:
from immanuel import charts
from immanuel.const import chart
config = charts.Config()
config.house_system = chart.CAMPANUS
config.objects.append(chart.CERES)
native = charts.Subject("2000-01-01 10:00", "32n43", "117w09")
natal = charts.Natal(native, config=config)Every chart class takes the same optional config keyword argument, which replaces the old settings argument. If you never touched settings at all then nothing changes, since charts fall back to a shared default config.
Two convenience methods from the old singleton have gone too. settings.set({...}) is replaced by plain attribute assignment, and settings.reset() can be replicated by simply creating a fresh Config(). Config.copy() is also available, and returns a deep copy for when you want minor variations on a base config.
Reasons for these changes
Immanuel is now safe in a long-running process. Under the old singleton, settings were global. A web request that switched the house system switched it for every other request in that worker, and there was no way to build two charts with different settings without mutating a shared state between them. Configs are now per-chart, so a request can build its own and throw it away.
Charts snapshot their config. A chart instance freezes the config it was handed at construction time, so mutating that config afterwards cannot retroactively alter another chart instance you have already generated with it. One config can be shared safely across as many charts as you like.
Locales are no longer global. Translation used to call locale.setlocale(), which changed the whole process and depended on the target locale being installed on the host OS. This was largely for date formatting, but weekday and month names now come from Immanuel's own translation files, so several locales can be live in the same process at once:
for lcid in ("en_US", "pt_BR", "es_ES", "de_DE"):
config = charts.Config()
config.locale = lcid
print(list(charts.Natal(native, config=config).objects.values())[0])
# Asc 05°36'38" in Pisces, 1st House
# Ascendente 05°36'38" em Peixes, Casa 1
# Ascendente 05°36'38" en Piscis, Casa 1
# Aszendent 05°36'38" in Fische, 1. HausCascading settings finally behave. The cascading aspect rules and orbs used to be rebuilt from scratch on every read, which made them unsubscriptable - assigning to a single chart object's rules silently did nothing. They are now backed by chain maps, so both subscripting and in-place edits work, and still cascade:
config.aspects.remove(calc.CONJUNCTION)
config.aspect_rules[chart.SUN]["initiate"] # [120.0]Ephemeris file paths
Since these should be global rather than per-chart, they remain in the settings module as plain functions. settings.add_filepath() is now settings.add_swe_filepath(), joined by set_swe_filepath() and reset_swe_filepath().
from immanuel import settings
settings.add_swe_filepath("/path/to/your/ephemeris")Import paths
ToJSON and Config are both re-exported from charts, which is the shortest path for most code:
print(json.dumps(natal, cls=charts.ToJSON, indent=4))If you ever imported from Immanuel's internals, the classes package has become support and setup has become settings:
| v1.5.4 | v1.6.0 |
|---|---|
immanuel.setup |
immanuel.settings |
immanuel.classes.serialize |
immanuel.support.serialize |
immanuel.classes.wrap |
immanuel.support.wrap |
immanuel.classes.localize |
immanuel.support.localize |
immanuel.classes.types |
immanuel.support.localize (Stringable) |
immanuel.classes.cache |
removed |
immanuel.const.genders |
immanuel.const.contexts |
Dependency change
Immanuel now depends on pysweph rather than pyswisseph. It still provides the same swisseph module, so nothing changes for your own code, but it is a different package on PyPI and pip will install it for you.
Performance
The settings overhaul turned out to be the biggest performance win. Because the old cascading settings were @property-driven getters that rebuilt their dicts on every read, a single natal chart ended up spending most of its life reconstructing aspect rules and orbs that it had already built. Configs are now computed once and frozen, aspects are calculated in one pass, and heavier third-party imports are deferred until something actually needs them.
Rough avg timings for a full chart on my local machine:
| Chart | v1.5.4 | v1.6.0 |
|---|---|---|
| Natal | 42.9 ms | 4.2 ms |
| Natal with synastry aspects | 45.2 ms | 4.6 ms |
| Solar return | 46.1 ms | 6.6 ms |
| Progressed | 49.2 ms | 8.8 ms |
| Composite | 42.7 ms | 4.5 ms |
| Transits | 50.6 ms | 8.8 ms |
from immanuel import charts |
99 ms | 10 ms |
The v1.6.0 figures include the new aspect pattern search, so the same work is being done in a tenth of the time, with more output at the end.
New
Aspect patterns
Charts now report the major patterns their aspects form, under a new aspect_patterns property. Nine are supported: T-square, grand trine, yod, grand cross, kite, mystic rectangle, cradle, grand sextile and grand quintile.
print(natal.aspect_patterns)
# T-Square: 1
print(json.dumps(natal.aspect_patterns, cls=charts.ToJSON))
# {"t_square": [[[4000002], [4000009], [4000008]]], "grand_trine": [], ...}Each vertex is itself a tuple of chart object indices. Conjunct objects are folded into a single vertex, so a stellium sitting on the corner of a T-square reports one pattern rather than several near-identical ones, and a kite does not also report the grand trine inside it.
Note: quintiles and biquintiles are not calculated by default, so the grand quintile pattern will never appear without those aspects being added to the config.
Custom aspects
Aspects you add to config.aspects can now carry a name, and you can declare which aspects count as major so that yours wins when orbs overlap:
config.aspects.append(54.3)
config.aspect_names[54.3] = "Wrongle"
config.major_aspects.append(54.3)Other additions
- Chart objects gained a
movement.typicalflag, indicating whether an object is moving as it normally would. - The bundled ephemeris now covers a continuous range from 1200 CE to 4799 CE; the 3000 to 3599 block was previously missing, and charts in those years raised a file-not-found error.
Fixes
Custom aspect rules were largely ignored. Narrowing a chart object's entry in aspect_rules to a subset of aspects would silently wipe out every aspect for that object, because the search gave up at the first disallowed aspect in the list rather than skipping past it. Restricting the Sun to trines and sextiles now finds its trines and sextiles instead of nothing at all:
config = charts.Config()
config.aspect_rules = {
chart.SUN: {"initiate": [calc.TRINE, calc.SEXTILE], "receive": [calc.TRINE, calc.SEXTILE]},
}
natal = charts.Natal(native, config=config)
# v1.5.4: no aspects for the Sun.
# v1.6.0:
# Moon Sun Sextile within -05°42'03" (Separative, Associate)
# Sun Saturn Trine within -00°13'59" (Exact, Associate)The other fixes:
- Two objects of identical speed could be assigned active and passive roles inconsistently, depending on the order they happened to be iterated in. This assignment is now based on object index, which is arbitrary but consistent.
- Where several aspects competed within overlapping orbs, which one won was undefined. Major aspects now take precedence, and
major_aspectslets you decide what counts as one. - Post-natal lunar eclipses were checked against the solar eclipse constant, so their movement could be misreported.
- Negative values that rounded down to zero kept their negative sign, giving output such as
-00°00'for an angle or0S0.0for a latitude.
Under the hood
The package was reorganized into clean layers that only import downwards: tools for the astronomy, reports for interpretation on top of it, charts for the user-facing classes, and support for presentation, serialization and shared helpers. The old ephemeris module was split into a low-level sweph engine that talks to pysweph directly and a thinner ephemeris dispatcher over it, with condition, orbit and part extracted alongside. This is invisible if you only use the chart classes, but it makes the submodules far more friendly and less spaghettified if you want to explore.
Full documentation is in the docs.