kerykeion 3.1.1__py3-none-any.whl → 5.1.9__py3-none-any.whl
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.
Potentially problematic release.
This version of kerykeion might be problematic. Click here for more details.
- kerykeion/__init__.py +58 -141
- kerykeion/aspects/__init__.py +14 -0
- kerykeion/aspects/aspects_factory.py +568 -0
- kerykeion/aspects/aspects_utils.py +164 -0
- kerykeion/astrological_subject_factory.py +1901 -0
- kerykeion/backword.py +820 -0
- kerykeion/chart_data_factory.py +552 -0
- kerykeion/charts/__init__.py +5 -0
- kerykeion/charts/chart_drawer.py +2794 -0
- kerykeion/charts/charts_utils.py +1840 -0
- kerykeion/charts/draw_planets.py +658 -0
- kerykeion/charts/templates/aspect_grid_only.xml +596 -0
- kerykeion/charts/templates/chart.xml +741 -0
- kerykeion/charts/templates/wheel_only.xml +653 -0
- kerykeion/charts/themes/black-and-white.css +148 -0
- kerykeion/charts/themes/classic.css +113 -0
- kerykeion/charts/themes/dark-high-contrast.css +159 -0
- kerykeion/charts/themes/dark.css +160 -0
- kerykeion/charts/themes/light.css +160 -0
- kerykeion/charts/themes/strawberry.css +158 -0
- kerykeion/composite_subject_factory.py +408 -0
- kerykeion/ephemeris_data_factory.py +443 -0
- kerykeion/fetch_geonames.py +105 -61
- kerykeion/house_comparison/__init__.py +6 -0
- kerykeion/house_comparison/house_comparison_factory.py +103 -0
- kerykeion/house_comparison/house_comparison_utils.py +126 -0
- kerykeion/kr_types/__init__.py +70 -0
- kerykeion/kr_types/chart_template_model.py +20 -0
- kerykeion/kr_types/kerykeion_exception.py +20 -0
- kerykeion/kr_types/kr_literals.py +20 -0
- kerykeion/kr_types/kr_models.py +20 -0
- kerykeion/kr_types/settings_models.py +20 -0
- kerykeion/planetary_return_factory.py +805 -0
- kerykeion/relationship_score_factory.py +301 -0
- kerykeion/report.py +779 -0
- kerykeion/schemas/__init__.py +106 -0
- kerykeion/schemas/chart_template_model.py +367 -0
- kerykeion/schemas/kerykeion_exception.py +20 -0
- kerykeion/schemas/kr_literals.py +181 -0
- kerykeion/schemas/kr_models.py +603 -0
- kerykeion/schemas/settings_models.py +188 -0
- kerykeion/settings/__init__.py +20 -0
- kerykeion/settings/chart_defaults.py +444 -0
- kerykeion/settings/config_constants.py +152 -0
- kerykeion/settings/kerykeion_settings.py +51 -0
- kerykeion/settings/translation_strings.py +1499 -0
- kerykeion/settings/translations.py +74 -0
- kerykeion/sweph/README.md +3 -0
- kerykeion/sweph/ast136/s136108s.se1 +0 -0
- kerykeion/sweph/ast136/s136199s.se1 +0 -0
- kerykeion/sweph/ast136/s136472s.se1 +0 -0
- kerykeion/sweph/ast28/se28978s.se1 +0 -0
- kerykeion/sweph/ast50/se50000s.se1 +0 -0
- kerykeion/sweph/ast90/se90377s.se1 +0 -0
- kerykeion/sweph/ast90/se90482s.se1 +0 -0
- kerykeion/sweph/seas_18.se1 +0 -0
- kerykeion/sweph/sefstars.txt +1602 -0
- kerykeion/transits_time_range_factory.py +302 -0
- kerykeion/utilities.py +762 -130
- kerykeion-5.1.9.dist-info/METADATA +1793 -0
- kerykeion-5.1.9.dist-info/RECORD +63 -0
- {kerykeion-3.1.1.dist-info → kerykeion-5.1.9.dist-info}/WHEEL +1 -2
- kerykeion-5.1.9.dist-info/licenses/LICENSE +661 -0
- kerykeion/aspects.py +0 -331
- kerykeion/charts/charts_svg.py +0 -1607
- kerykeion/charts/templates/basic.xml +0 -285
- kerykeion/charts/templates/extended.xml +0 -294
- kerykeion/kr.config.json +0 -464
- kerykeion/main.py +0 -595
- kerykeion/print_all_data.py +0 -44
- kerykeion/relationship_score.py +0 -219
- kerykeion/types.py +0 -190
- kerykeion-3.1.1.dist-info/METADATA +0 -204
- kerykeion-3.1.1.dist-info/RECORD +0 -17
- kerykeion-3.1.1.dist-info/top_level.txt +0 -1
|
@@ -0,0 +1,1901 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Astrological Subject Factory Module
|
|
4
|
+
|
|
5
|
+
This module provides factory classes for creating astrological subjects with comprehensive
|
|
6
|
+
astrological calculations including planetary positions, house cusps, aspects, and various
|
|
7
|
+
astrological points.
|
|
8
|
+
|
|
9
|
+
The main factory class AstrologicalSubjectFactory offers multiple creation methods for
|
|
10
|
+
different initialization scenarios, supporting both online and offline calculation modes,
|
|
11
|
+
various zodiac systems (Tropical/Sidereal), house systems, and coordinate perspectives.
|
|
12
|
+
|
|
13
|
+
Key Features:
|
|
14
|
+
- Planetary position calculations for all traditional and modern planets
|
|
15
|
+
- House cusp calculations with multiple house systems
|
|
16
|
+
- Lunar nodes, Lilith points, asteroids, and trans-Neptunian objects
|
|
17
|
+
- Arabic parts (lots) calculations
|
|
18
|
+
- Fixed star positions
|
|
19
|
+
- Automatic location data fetching via GeoNames API
|
|
20
|
+
- Comprehensive timezone and coordinate handling
|
|
21
|
+
- Flexible point selection for performance optimization
|
|
22
|
+
|
|
23
|
+
Classes:
|
|
24
|
+
ChartConfiguration: Configuration settings for astrological calculations
|
|
25
|
+
LocationData: Geographical location information and utilities
|
|
26
|
+
AstrologicalSubjectFactory: Main factory for creating astrological subjects
|
|
27
|
+
|
|
28
|
+
Author: Giacomo Battaglia
|
|
29
|
+
Copyright: (C) 2025 Kerykeion Project
|
|
30
|
+
License: AGPL-3.0
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import pytz
|
|
34
|
+
import swisseph as swe
|
|
35
|
+
import logging
|
|
36
|
+
import math
|
|
37
|
+
from datetime import datetime
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
from typing import Optional, List, Dict, Any, get_args, cast
|
|
40
|
+
from dataclasses import dataclass, field
|
|
41
|
+
from contextlib import contextmanager
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
from kerykeion.fetch_geonames import FetchGeonames
|
|
45
|
+
from kerykeion.schemas import (
|
|
46
|
+
KerykeionException,
|
|
47
|
+
ZodiacType,
|
|
48
|
+
AstrologicalSubjectModel,
|
|
49
|
+
PointType,
|
|
50
|
+
SiderealMode,
|
|
51
|
+
HousesSystemIdentifier,
|
|
52
|
+
PerspectiveType,
|
|
53
|
+
AstrologicalPoint,
|
|
54
|
+
Houses,
|
|
55
|
+
)
|
|
56
|
+
from kerykeion.utilities import (
|
|
57
|
+
get_kerykeion_point_from_degree,
|
|
58
|
+
get_planet_house,
|
|
59
|
+
check_and_adjust_polar_latitude,
|
|
60
|
+
calculate_moon_phase,
|
|
61
|
+
datetime_to_julian,
|
|
62
|
+
get_house_number,
|
|
63
|
+
normalize_zodiac_type,
|
|
64
|
+
)
|
|
65
|
+
from kerykeion.settings.config_constants import DEFAULT_ACTIVE_POINTS
|
|
66
|
+
|
|
67
|
+
# Default configuration values
|
|
68
|
+
DEFAULT_GEONAMES_USERNAME = "century.boy"
|
|
69
|
+
DEFAULT_SIDEREAL_MODE: SiderealMode = "FAGAN_BRADLEY"
|
|
70
|
+
DEFAULT_HOUSES_SYSTEM_IDENTIFIER: HousesSystemIdentifier = "P"
|
|
71
|
+
DEFAULT_ZODIAC_TYPE: ZodiacType = "Tropical"
|
|
72
|
+
DEFAULT_PERSPECTIVE_TYPE: PerspectiveType = "Apparent Geocentric"
|
|
73
|
+
DEFAULT_GEONAMES_CACHE_EXPIRE_AFTER_DAYS = 30
|
|
74
|
+
|
|
75
|
+
# Warning messages
|
|
76
|
+
GEONAMES_DEFAULT_USERNAME_WARNING = (
|
|
77
|
+
"\n********\n"
|
|
78
|
+
"NO GEONAMES USERNAME SET!\n"
|
|
79
|
+
"Using the default geonames username is not recommended, please set a custom one!\n"
|
|
80
|
+
"You can get one for free here:\n"
|
|
81
|
+
"https://www.geonames.org/login\n"
|
|
82
|
+
"Keep in mind that the default username is limited to 2000 requests per hour and is shared with everyone else using this library.\n"
|
|
83
|
+
"********"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
@contextmanager
|
|
87
|
+
def ephemeris_context(
|
|
88
|
+
ephe_path: str,
|
|
89
|
+
config: "ChartConfiguration",
|
|
90
|
+
lng: float,
|
|
91
|
+
lat: float,
|
|
92
|
+
alt: Optional[float] = None,
|
|
93
|
+
):
|
|
94
|
+
"""Context manager that isolates Swiss Ephemeris configuration.
|
|
95
|
+
|
|
96
|
+
Responsibilities:
|
|
97
|
+
- Set ephemeris path and calculation flags
|
|
98
|
+
- Configure perspective (true geo / helio / topo)
|
|
99
|
+
- Configure sidereal mode when needed
|
|
100
|
+
- Apply topocentric observer only inside the with-block
|
|
101
|
+
- Yield iflag for calculations
|
|
102
|
+
- Reset topocentric coordinates afterward (defensive)
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
ephe_path: Path containing Swiss Ephemeris data files.
|
|
106
|
+
config: Validated chart configuration.
|
|
107
|
+
lng: Observer longitude (used for topocentric charts).
|
|
108
|
+
lat: Observer latitude (used for topocentric charts).
|
|
109
|
+
alt: Observer altitude (meters) for topocentric charts.
|
|
110
|
+
|
|
111
|
+
Yields:
|
|
112
|
+
int: iflag to be passed to swe.calc_ut / swe.fixstar_ut.
|
|
113
|
+
"""
|
|
114
|
+
swe.set_ephe_path(ephe_path)
|
|
115
|
+
iflag = swe.FLG_SWIEPH | swe.FLG_SPEED
|
|
116
|
+
|
|
117
|
+
topo_used = False
|
|
118
|
+
|
|
119
|
+
# Perspective configuration
|
|
120
|
+
if config.perspective_type == "True Geocentric":
|
|
121
|
+
iflag |= swe.FLG_TRUEPOS
|
|
122
|
+
elif config.perspective_type == "Heliocentric":
|
|
123
|
+
iflag |= swe.FLG_HELCTR
|
|
124
|
+
elif config.perspective_type == "Topocentric":
|
|
125
|
+
iflag |= swe.FLG_TOPOCTR
|
|
126
|
+
swe.set_topo(lng, lat, alt or 0.0)
|
|
127
|
+
topo_used = True
|
|
128
|
+
|
|
129
|
+
# Sidereal configuration
|
|
130
|
+
if config.zodiac_type == "Sidereal":
|
|
131
|
+
iflag |= swe.FLG_SIDEREAL
|
|
132
|
+
swe.set_sid_mode(getattr(swe, f"SIDM_{config.sidereal_mode}"))
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
yield iflag
|
|
136
|
+
finally:
|
|
137
|
+
# Defensive cleanup: reset topo if it was set
|
|
138
|
+
if topo_used:
|
|
139
|
+
swe.set_topo(0.0, 0.0, 0.0)
|
|
140
|
+
|
|
141
|
+
@dataclass
|
|
142
|
+
class ChartConfiguration:
|
|
143
|
+
"""
|
|
144
|
+
Configuration settings for astrological chart calculations.
|
|
145
|
+
|
|
146
|
+
This class encapsulates all the configuration parameters needed for astrological
|
|
147
|
+
calculations, including zodiac type, coordinate systems, house systems, and
|
|
148
|
+
calculation perspectives. It provides validation to ensure compatible settings
|
|
149
|
+
combinations.
|
|
150
|
+
|
|
151
|
+
Attributes:
|
|
152
|
+
zodiac_type (ZodiacType): The zodiac system to use ('Tropical' or 'Sidereal').
|
|
153
|
+
Defaults to 'Tropical'.
|
|
154
|
+
sidereal_mode (Optional[SiderealMode]): The sidereal calculation mode when using
|
|
155
|
+
sidereal zodiac. Only required/used when zodiac_type is 'Sidereal'.
|
|
156
|
+
Defaults to None (auto-set to FAGAN_BRADLEY for sidereal).
|
|
157
|
+
houses_system_identifier (HousesSystemIdentifier): The house system to use for
|
|
158
|
+
house cusp calculations. Defaults to 'P' (Placidus).
|
|
159
|
+
perspective_type (PerspectiveType): The coordinate perspective for calculations.
|
|
160
|
+
Options include 'Apparent Geocentric', 'True Geocentric', 'Heliocentric',
|
|
161
|
+
or 'Topocentric'. Defaults to 'Apparent Geocentric'.
|
|
162
|
+
|
|
163
|
+
Raises:
|
|
164
|
+
KerykeionException: When invalid configuration combinations are detected,
|
|
165
|
+
such as setting sidereal_mode with tropical zodiac, or using invalid
|
|
166
|
+
enumeration values.
|
|
167
|
+
|
|
168
|
+
Example:
|
|
169
|
+
>>> config = ChartConfiguration(
|
|
170
|
+
... zodiac_type="Sidereal",
|
|
171
|
+
... sidereal_mode="LAHIRI",
|
|
172
|
+
... houses_system_identifier="K",
|
|
173
|
+
... perspective_type="Topocentric"
|
|
174
|
+
... )
|
|
175
|
+
"""
|
|
176
|
+
zodiac_type: ZodiacType = DEFAULT_ZODIAC_TYPE
|
|
177
|
+
sidereal_mode: Optional[SiderealMode] = None
|
|
178
|
+
houses_system_identifier: HousesSystemIdentifier = DEFAULT_HOUSES_SYSTEM_IDENTIFIER
|
|
179
|
+
perspective_type: PerspectiveType = DEFAULT_PERSPECTIVE_TYPE
|
|
180
|
+
|
|
181
|
+
def __post_init__(self) -> None:
|
|
182
|
+
self.validate()
|
|
183
|
+
|
|
184
|
+
def validate(self) -> None:
|
|
185
|
+
"""
|
|
186
|
+
Validate configuration settings for internal consistency.
|
|
187
|
+
|
|
188
|
+
Performs comprehensive validation of all configuration parameters to ensure
|
|
189
|
+
they form a valid, compatible combination. This includes checking enumeration
|
|
190
|
+
values, zodiac/sidereal mode compatibility, and setting defaults where needed.
|
|
191
|
+
|
|
192
|
+
Raises:
|
|
193
|
+
KerykeionException: If any configuration parameter is invalid or if
|
|
194
|
+
incompatible parameter combinations are detected.
|
|
195
|
+
|
|
196
|
+
Side Effects:
|
|
197
|
+
- Sets default sidereal_mode to FAGAN_BRADLEY if zodiac_type is Sidereal
|
|
198
|
+
and no sidereal_mode is specified
|
|
199
|
+
- Logs informational message when setting default sidereal mode
|
|
200
|
+
"""
|
|
201
|
+
# Validate zodiac type
|
|
202
|
+
try:
|
|
203
|
+
normalized_zodiac_type = normalize_zodiac_type(self.zodiac_type)
|
|
204
|
+
except ValueError as exc:
|
|
205
|
+
raise KerykeionException(str(exc)) from exc
|
|
206
|
+
else:
|
|
207
|
+
if normalized_zodiac_type != self.zodiac_type:
|
|
208
|
+
self.zodiac_type = normalized_zodiac_type
|
|
209
|
+
|
|
210
|
+
# Validate sidereal mode settings
|
|
211
|
+
if self.sidereal_mode and self.zodiac_type == "Tropical":
|
|
212
|
+
raise KerykeionException("You can't set a sidereal mode with a Tropical zodiac type!")
|
|
213
|
+
|
|
214
|
+
if self.zodiac_type == "Sidereal":
|
|
215
|
+
if not self.sidereal_mode:
|
|
216
|
+
self.sidereal_mode = DEFAULT_SIDEREAL_MODE
|
|
217
|
+
logging.info("No sidereal mode set, using default FAGAN_BRADLEY")
|
|
218
|
+
elif self.sidereal_mode not in get_args(SiderealMode):
|
|
219
|
+
raise KerykeionException(
|
|
220
|
+
f"'{self.sidereal_mode}' is not a valid sidereal mode! Available modes are: {get_args(SiderealMode)}"
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Validate houses system
|
|
224
|
+
if self.houses_system_identifier not in get_args(HousesSystemIdentifier):
|
|
225
|
+
raise KerykeionException(
|
|
226
|
+
f"'{self.houses_system_identifier}' is not a valid house system! Available systems are: {get_args(HousesSystemIdentifier)}"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Validate perspective type
|
|
230
|
+
if self.perspective_type not in get_args(PerspectiveType):
|
|
231
|
+
raise KerykeionException(
|
|
232
|
+
f"'{self.perspective_type}' is not a valid chart perspective! Available perspectives are: {get_args(PerspectiveType)}"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@dataclass
|
|
237
|
+
class LocationData:
|
|
238
|
+
"""
|
|
239
|
+
Information about a geographical location for astrological calculations.
|
|
240
|
+
|
|
241
|
+
This class handles all location-related data including coordinates, timezone
|
|
242
|
+
information, and interaction with the GeoNames API for automatic location
|
|
243
|
+
data retrieval. It provides methods for fetching location data online and
|
|
244
|
+
preparing coordinates for astrological calculations.
|
|
245
|
+
|
|
246
|
+
Attributes:
|
|
247
|
+
city (str): Name of the city or location. Defaults to "Greenwich".
|
|
248
|
+
nation (str): ISO country code (2-letter). Defaults to "GB" (United Kingdom).
|
|
249
|
+
lat (float): Latitude in decimal degrees. Positive for North, negative for South.
|
|
250
|
+
Defaults to 51.5074 (Greenwich).
|
|
251
|
+
lng (float): Longitude in decimal degrees. Positive for East, negative for West.
|
|
252
|
+
Defaults to 0.0 (Greenwich).
|
|
253
|
+
tz_str (str): IANA timezone identifier (e.g., 'Europe/London', 'America/New_York').
|
|
254
|
+
Defaults to "Etc/GMT".
|
|
255
|
+
altitude (Optional[float]): Altitude above sea level in meters. Used for
|
|
256
|
+
topocentric calculations. Defaults to None (sea level assumed).
|
|
257
|
+
city_data (Dict[str, str]): Raw data retrieved from GeoNames API. Used internally
|
|
258
|
+
for caching and validation. Defaults to empty dictionary.
|
|
259
|
+
|
|
260
|
+
Note:
|
|
261
|
+
When using online mode, the initial coordinate and timezone values may be
|
|
262
|
+
overridden by data fetched from the GeoNames API based on city and nation.
|
|
263
|
+
For polar regions, latitude values are automatically adjusted to prevent
|
|
264
|
+
calculation errors.
|
|
265
|
+
|
|
266
|
+
Example:
|
|
267
|
+
>>> location = LocationData(city="Rome", nation="IT")
|
|
268
|
+
>>> location.fetch_from_geonames("your_username", 30)
|
|
269
|
+
>>> location.prepare_for_calculation()
|
|
270
|
+
"""
|
|
271
|
+
city: str = "Greenwich"
|
|
272
|
+
nation: str = "GB"
|
|
273
|
+
lat: float = 51.5074
|
|
274
|
+
lng: float = 0.0
|
|
275
|
+
tz_str: str = "Etc/GMT"
|
|
276
|
+
altitude: Optional[float] = None
|
|
277
|
+
|
|
278
|
+
# Storage for city data fetched from geonames
|
|
279
|
+
city_data: Dict[str, str] = field(default_factory=dict)
|
|
280
|
+
|
|
281
|
+
def fetch_from_geonames(self, username: str, cache_expire_after_days: int) -> None:
|
|
282
|
+
"""
|
|
283
|
+
Fetch location data from GeoNames API.
|
|
284
|
+
|
|
285
|
+
Retrieves accurate coordinates, timezone, and country code information
|
|
286
|
+
for the specified city and country from the GeoNames web service.
|
|
287
|
+
Updates the instance attributes with the fetched data.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
username (str): GeoNames API username. Must be registered at geonames.org.
|
|
291
|
+
Free accounts are limited to 2000 requests per hour.
|
|
292
|
+
cache_expire_after_days (int): Number of days to cache the location data
|
|
293
|
+
locally before refreshing from the API.
|
|
294
|
+
|
|
295
|
+
Raises:
|
|
296
|
+
KerykeionException: If required data is missing from the API response,
|
|
297
|
+
typically due to network issues, invalid location names, or API limits.
|
|
298
|
+
|
|
299
|
+
Side Effects:
|
|
300
|
+
- Updates city_data with raw API response
|
|
301
|
+
- Updates nation, lng, lat, and tz_str with fetched values
|
|
302
|
+
- May create or update local cache files
|
|
303
|
+
|
|
304
|
+
Note:
|
|
305
|
+
The method validates that all required fields (countryCode, timezonestr,
|
|
306
|
+
lat, lng) are present in the API response before updating instance attributes.
|
|
307
|
+
"""
|
|
308
|
+
logging.info(f"Fetching timezone/coordinates for {self.city}, {self.nation} from geonames")
|
|
309
|
+
|
|
310
|
+
geonames = FetchGeonames(
|
|
311
|
+
self.city,
|
|
312
|
+
self.nation,
|
|
313
|
+
username=username,
|
|
314
|
+
cache_expire_after_days=cache_expire_after_days
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
self.city_data = geonames.get_serialized_data()
|
|
318
|
+
|
|
319
|
+
# Validate data
|
|
320
|
+
required_fields = ["countryCode", "timezonestr", "lat", "lng"]
|
|
321
|
+
missing_fields = [field for field in required_fields if field not in self.city_data]
|
|
322
|
+
|
|
323
|
+
if missing_fields:
|
|
324
|
+
raise KerykeionException(
|
|
325
|
+
f"Missing data from geonames: {', '.join(missing_fields)}. "
|
|
326
|
+
"Check your connection or try a different location."
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# Update location data
|
|
330
|
+
self.nation = self.city_data["countryCode"]
|
|
331
|
+
self.lng = float(self.city_data["lng"])
|
|
332
|
+
self.lat = float(self.city_data["lat"])
|
|
333
|
+
self.tz_str = self.city_data["timezonestr"]
|
|
334
|
+
|
|
335
|
+
def prepare_for_calculation(self) -> None:
|
|
336
|
+
"""
|
|
337
|
+
Prepare location data for astrological calculations.
|
|
338
|
+
|
|
339
|
+
Performs final adjustments to location data to ensure compatibility
|
|
340
|
+
with Swiss Ephemeris calculations. This includes handling special cases
|
|
341
|
+
like polar regions where extreme latitudes can cause calculation errors.
|
|
342
|
+
|
|
343
|
+
Side Effects:
|
|
344
|
+
- Adjusts latitude values for polar regions (beyond ±66.5°) to
|
|
345
|
+
prevent Swiss Ephemeris calculation failures
|
|
346
|
+
- May log warnings about latitude adjustments
|
|
347
|
+
|
|
348
|
+
Note:
|
|
349
|
+
This method should be called after all location data has been set,
|
|
350
|
+
either manually or via fetch_from_geonames(), and before performing
|
|
351
|
+
any astrological calculations.
|
|
352
|
+
"""
|
|
353
|
+
# Adjust latitude for polar regions
|
|
354
|
+
self.lat = check_and_adjust_polar_latitude(self.lat)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class AstrologicalSubjectFactory:
|
|
358
|
+
"""
|
|
359
|
+
Factory class for creating comprehensive astrological subjects.
|
|
360
|
+
|
|
361
|
+
This factory creates AstrologicalSubjectModel instances with complete astrological
|
|
362
|
+
information including planetary positions, house cusps, aspects, lunar phases, and
|
|
363
|
+
various specialized astrological points. It provides multiple class methods for
|
|
364
|
+
different initialization scenarios and supports both online and offline calculation modes.
|
|
365
|
+
|
|
366
|
+
The factory handles complex astrological calculations using the Swiss Ephemeris library,
|
|
367
|
+
supports multiple coordinate systems and house systems, and can automatically fetch
|
|
368
|
+
location data from online sources.
|
|
369
|
+
|
|
370
|
+
Supported Astrological Points:
|
|
371
|
+
- Traditional Planets: Sun through Pluto
|
|
372
|
+
- Lunar Nodes: Mean and True North/South Nodes
|
|
373
|
+
- Lilith Points: Mean and True Black Moon
|
|
374
|
+
- Asteroids: Ceres, Pallas, Juno, Vesta
|
|
375
|
+
- Centaurs: Chiron, Pholus
|
|
376
|
+
- Trans-Neptunian Objects: Eris, Sedna, Haumea, Makemake, Ixion, Orcus, Quaoar
|
|
377
|
+
- Fixed Stars: Regulus, Spica (extensible)
|
|
378
|
+
- Arabic Parts: Pars Fortunae, Pars Spiritus, Pars Amoris, Pars Fidei
|
|
379
|
+
- Special Points: Vertex, Anti-Vertex, Earth (for heliocentric charts)
|
|
380
|
+
- House Cusps: All 12 houses with configurable house systems
|
|
381
|
+
- Angles: Ascendant, Medium Coeli, Descendant, Imum Coeli
|
|
382
|
+
|
|
383
|
+
Supported Features:
|
|
384
|
+
- Multiple zodiac systems (Tropical/Sidereal with various ayanamshas)
|
|
385
|
+
- Multiple house systems (Placidus, Koch, Equal, Whole Sign, etc.)
|
|
386
|
+
- Multiple coordinate perspectives (Geocentric, Heliocentric, Topocentric)
|
|
387
|
+
- Automatic timezone and coordinate resolution via GeoNames API
|
|
388
|
+
- Lunar phase calculations
|
|
389
|
+
- Day/night chart detection for Arabic parts
|
|
390
|
+
- Performance optimization through selective point calculation
|
|
391
|
+
- Comprehensive error handling and validation
|
|
392
|
+
|
|
393
|
+
Class Methods:
|
|
394
|
+
from_birth_data: Create subject from standard birth data (most flexible)
|
|
395
|
+
from_iso_utc_time: Create subject from ISO UTC timestamp
|
|
396
|
+
from_current_time: Create subject for current moment
|
|
397
|
+
|
|
398
|
+
Example:
|
|
399
|
+
>>> # Create natal chart
|
|
400
|
+
>>> subject = AstrologicalSubjectFactory.from_birth_data(
|
|
401
|
+
... name="John Doe",
|
|
402
|
+
... year=1990, month=6, day=15,
|
|
403
|
+
... hour=14, minute=30,
|
|
404
|
+
... city="Rome", nation="IT",
|
|
405
|
+
... online=True
|
|
406
|
+
... )
|
|
407
|
+
>>> print(f"Sun: {subject.sun.sign} {subject.sun.abs_pos}°")
|
|
408
|
+
>>> print(f"Active points: {len(subject.active_points)}")
|
|
409
|
+
|
|
410
|
+
>>> # Create chart for current time
|
|
411
|
+
>>> now_subject = AstrologicalSubjectFactory.from_current_time(
|
|
412
|
+
... name="Current Moment",
|
|
413
|
+
... city="London", nation="GB"
|
|
414
|
+
... )
|
|
415
|
+
|
|
416
|
+
Thread Safety:
|
|
417
|
+
This factory is not thread-safe due to its use of the Swiss Ephemeris library
|
|
418
|
+
which maintains global state. Use separate instances in multi-threaded applications
|
|
419
|
+
or implement appropriate locking mechanisms.
|
|
420
|
+
"""
|
|
421
|
+
|
|
422
|
+
@classmethod
|
|
423
|
+
def from_birth_data(
|
|
424
|
+
cls,
|
|
425
|
+
name: str = "Now",
|
|
426
|
+
year: Optional[int] = None,
|
|
427
|
+
month: Optional[int] = None,
|
|
428
|
+
day: Optional[int] = None,
|
|
429
|
+
hour: Optional[int] = None,
|
|
430
|
+
minute: Optional[int] = None,
|
|
431
|
+
city: Optional[str] = None,
|
|
432
|
+
nation: Optional[str] = None,
|
|
433
|
+
lng: Optional[float] = None,
|
|
434
|
+
lat: Optional[float] = None,
|
|
435
|
+
tz_str: Optional[str] = None,
|
|
436
|
+
geonames_username: Optional[str] = None,
|
|
437
|
+
online: bool = True,
|
|
438
|
+
zodiac_type: ZodiacType = DEFAULT_ZODIAC_TYPE,
|
|
439
|
+
sidereal_mode: Optional[SiderealMode] = None,
|
|
440
|
+
houses_system_identifier: HousesSystemIdentifier = DEFAULT_HOUSES_SYSTEM_IDENTIFIER,
|
|
441
|
+
perspective_type: PerspectiveType = DEFAULT_PERSPECTIVE_TYPE,
|
|
442
|
+
cache_expire_after_days: int = DEFAULT_GEONAMES_CACHE_EXPIRE_AFTER_DAYS,
|
|
443
|
+
is_dst: Optional[bool] = None,
|
|
444
|
+
altitude: Optional[float] = None,
|
|
445
|
+
active_points: Optional[List[AstrologicalPoint]] = None,
|
|
446
|
+
calculate_lunar_phase: bool = True,
|
|
447
|
+
*,
|
|
448
|
+
seconds: int = 0,
|
|
449
|
+
suppress_geonames_warning: bool = False,
|
|
450
|
+
|
|
451
|
+
) -> AstrologicalSubjectModel:
|
|
452
|
+
"""
|
|
453
|
+
Create an astrological subject from standard birth or event data.
|
|
454
|
+
|
|
455
|
+
This is the most flexible and commonly used factory method. It creates a complete
|
|
456
|
+
astrological subject with planetary positions, house cusps, and specialized points
|
|
457
|
+
for a specific date, time, and location. Supports both online location resolution
|
|
458
|
+
and offline calculation modes.
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
name (str, optional): Name or identifier for the subject. Defaults to "Now".
|
|
462
|
+
year (int, optional): Year of birth/event. Defaults to current year.
|
|
463
|
+
month (int, optional): Month of birth/event (1-12). Defaults to current month.
|
|
464
|
+
day (int, optional): Day of birth/event (1-31). Defaults to current day.
|
|
465
|
+
hour (int, optional): Hour of birth/event (0-23). Defaults to current hour.
|
|
466
|
+
minute (int, optional): Minute of birth/event (0-59). Defaults to current minute.
|
|
467
|
+
seconds (int, optional): Seconds of birth/event (0-59). Defaults to 0.
|
|
468
|
+
city (str, optional): City name for location lookup. Used with online=True.
|
|
469
|
+
Defaults to None (Greenwich if not specified).
|
|
470
|
+
nation (str, optional): ISO country code (e.g., 'US', 'GB', 'IT'). Used with
|
|
471
|
+
online=True. Defaults to None ('GB' if not specified).
|
|
472
|
+
lng (float, optional): Longitude in decimal degrees. East is positive, West
|
|
473
|
+
is negative. If not provided and online=True, fetched from GeoNames.
|
|
474
|
+
lat (float, optional): Latitude in decimal degrees. North is positive, South
|
|
475
|
+
is negative. If not provided and online=True, fetched from GeoNames.
|
|
476
|
+
tz_str (str, optional): IANA timezone identifier (e.g., 'Europe/London').
|
|
477
|
+
If not provided and online=True, fetched from GeoNames.
|
|
478
|
+
geonames_username (str, optional): Username for GeoNames API. Required for
|
|
479
|
+
online location lookup. Get one free at geonames.org.
|
|
480
|
+
online (bool, optional): Whether to fetch location data online. If False,
|
|
481
|
+
lng, lat, and tz_str must be provided. Defaults to True.
|
|
482
|
+
zodiac_type (ZodiacType, optional): Zodiac system - 'Tropical' or 'Sidereal'.
|
|
483
|
+
Defaults to 'Tropical'.
|
|
484
|
+
sidereal_mode (SiderealMode, optional): Sidereal calculation mode (e.g.,
|
|
485
|
+
'FAGAN_BRADLEY', 'LAHIRI'). Only used with zodiac_type='Sidereal'.
|
|
486
|
+
houses_system_identifier (HousesSystemIdentifier, optional): House system
|
|
487
|
+
for cusp calculations (e.g., 'P'=Placidus, 'K'=Koch, 'E'=Equal).
|
|
488
|
+
Defaults to 'P' (Placidus).
|
|
489
|
+
perspective_type (PerspectiveType, optional): Calculation perspective:
|
|
490
|
+
- 'Apparent Geocentric': Standard geocentric with light-time correction
|
|
491
|
+
- 'True Geocentric': Geometric geocentric positions
|
|
492
|
+
- 'Heliocentric': Sun-centered coordinates
|
|
493
|
+
- 'Topocentric': Earth surface perspective (requires altitude)
|
|
494
|
+
Defaults to 'Apparent Geocentric'.
|
|
495
|
+
cache_expire_after_days (int, optional): Days to cache GeoNames data locally.
|
|
496
|
+
Defaults to 30.
|
|
497
|
+
is_dst (bool, optional): Daylight Saving Time flag for ambiguous times.
|
|
498
|
+
If None, pytz attempts automatic detection. Set explicitly for
|
|
499
|
+
times during DST transitions.
|
|
500
|
+
altitude (float, optional): Altitude above sea level in meters. Used for
|
|
501
|
+
topocentric calculations and atmospheric corrections. Defaults to None
|
|
502
|
+
(sea level assumed).
|
|
503
|
+
active_points (Optional[List[AstrologicalPoint]], optional): List of astrological
|
|
504
|
+
points to calculate. Omitting points can improve performance for
|
|
505
|
+
specialized applications. If None, uses DEFAULT_ACTIVE_POINTS.
|
|
506
|
+
calculate_lunar_phase (bool, optional): Whether to calculate lunar phase.
|
|
507
|
+
Requires Sun and Moon in active_points. Defaults to True.
|
|
508
|
+
suppress_geonames_warning (bool, optional): If True, suppresses the warning
|
|
509
|
+
message when using the default GeoNames username. Useful for testing
|
|
510
|
+
or automated processes. Defaults to False.
|
|
511
|
+
|
|
512
|
+
Returns:
|
|
513
|
+
AstrologicalSubjectModel: Complete astrological subject with calculated
|
|
514
|
+
positions, houses, and metadata. Access planetary positions via
|
|
515
|
+
attributes like .sun, .moon, .mercury, etc.
|
|
516
|
+
|
|
517
|
+
Raises:
|
|
518
|
+
KerykeionException:
|
|
519
|
+
- If offline mode is used without required location data
|
|
520
|
+
- If invalid zodiac/sidereal mode combinations are specified
|
|
521
|
+
- If GeoNames data is missing or invalid
|
|
522
|
+
- If timezone localization fails (ambiguous DST times)
|
|
523
|
+
|
|
524
|
+
Examples:
|
|
525
|
+
>>> # Basic natal chart with online location lookup
|
|
526
|
+
>>> chart = AstrologicalSubjectFactory.from_birth_data(
|
|
527
|
+
... name="Jane Doe",
|
|
528
|
+
... year=1985, month=3, day=21,
|
|
529
|
+
... hour=15, minute=30,
|
|
530
|
+
... city="Paris", nation="FR",
|
|
531
|
+
... geonames_username="your_username"
|
|
532
|
+
... )
|
|
533
|
+
|
|
534
|
+
>>> # Offline calculation with manual coordinates
|
|
535
|
+
>>> chart = AstrologicalSubjectFactory.from_birth_data(
|
|
536
|
+
... name="John Smith",
|
|
537
|
+
... year=1990, month=12, day=25,
|
|
538
|
+
... hour=0, minute=0,
|
|
539
|
+
... lng=-74.006, lat=40.7128, tz_str="America/New_York",
|
|
540
|
+
... online=False
|
|
541
|
+
... )
|
|
542
|
+
|
|
543
|
+
>>> # Sidereal chart with specific points
|
|
544
|
+
>>> chart = AstrologicalSubjectFactory.from_birth_data(
|
|
545
|
+
... name="Vedic Chart",
|
|
546
|
+
... year=2000, month=6, day=15, hour=12,
|
|
547
|
+
... city="Mumbai", nation="IN",
|
|
548
|
+
... zodiac_type="Sidereal",
|
|
549
|
+
... sidereal_mode="LAHIRI",
|
|
550
|
+
... active_points=["Sun", "Moon", "Mercury", "Venus", "Mars",
|
|
551
|
+
... "Jupiter", "Saturn", "Ascendant"]
|
|
552
|
+
... )
|
|
553
|
+
|
|
554
|
+
Note:
|
|
555
|
+
- For high-precision calculations, consider providing seconds parameter
|
|
556
|
+
- Use topocentric perspective for observer-specific calculations
|
|
557
|
+
- Some Arabic parts automatically activate required base points
|
|
558
|
+
- The method handles polar regions by adjusting extreme latitudes
|
|
559
|
+
- Time zones are handled with full DST awareness via pytz
|
|
560
|
+
"""
|
|
561
|
+
# Resolve time defaults using current time
|
|
562
|
+
if year is None or month is None or day is None or hour is None or minute is None or seconds is None:
|
|
563
|
+
now = datetime.now()
|
|
564
|
+
year = year if year is not None else now.year
|
|
565
|
+
month = month if month is not None else now.month
|
|
566
|
+
day = day if day is not None else now.day
|
|
567
|
+
hour = hour if hour is not None else now.hour
|
|
568
|
+
minute = minute if minute is not None else now.minute
|
|
569
|
+
seconds = seconds if seconds is not None else now.second
|
|
570
|
+
|
|
571
|
+
# Create a calculation data container
|
|
572
|
+
calc_data: Dict[str, Any] = {}
|
|
573
|
+
|
|
574
|
+
# Basic identity
|
|
575
|
+
calc_data["name"] = name
|
|
576
|
+
calc_data["json_dir"] = str(Path.home())
|
|
577
|
+
|
|
578
|
+
# Create a deep copy of active points to avoid modifying the original list
|
|
579
|
+
if active_points is None:
|
|
580
|
+
active_points_list: List[AstrologicalPoint] = list(DEFAULT_ACTIVE_POINTS)
|
|
581
|
+
else:
|
|
582
|
+
active_points_list = list(active_points)
|
|
583
|
+
|
|
584
|
+
calc_data["active_points"] = active_points_list
|
|
585
|
+
|
|
586
|
+
# Initialize configuration
|
|
587
|
+
config = ChartConfiguration(
|
|
588
|
+
zodiac_type=zodiac_type,
|
|
589
|
+
sidereal_mode=sidereal_mode,
|
|
590
|
+
houses_system_identifier=houses_system_identifier,
|
|
591
|
+
perspective_type=perspective_type,
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
# Add configuration data to calculation data
|
|
595
|
+
calc_data["zodiac_type"] = config.zodiac_type
|
|
596
|
+
calc_data["sidereal_mode"] = config.sidereal_mode
|
|
597
|
+
calc_data["houses_system_identifier"] = config.houses_system_identifier
|
|
598
|
+
calc_data["perspective_type"] = config.perspective_type
|
|
599
|
+
|
|
600
|
+
# Set up geonames username if needed
|
|
601
|
+
if geonames_username is None and online and (not lat or not lng or not tz_str):
|
|
602
|
+
if not suppress_geonames_warning:
|
|
603
|
+
logging.warning(GEONAMES_DEFAULT_USERNAME_WARNING)
|
|
604
|
+
geonames_username = DEFAULT_GEONAMES_USERNAME
|
|
605
|
+
|
|
606
|
+
# Initialize location data
|
|
607
|
+
location = LocationData(
|
|
608
|
+
city=city or "Greenwich",
|
|
609
|
+
nation=nation or "GB",
|
|
610
|
+
lat=lat if lat is not None else 51.5074,
|
|
611
|
+
lng=lng if lng is not None else 0.0,
|
|
612
|
+
tz_str=tz_str or "Etc/GMT",
|
|
613
|
+
altitude=altitude
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
# If offline mode is requested but required data is missing, raise error
|
|
617
|
+
if not online and (not tz_str or lat is None or lng is None):
|
|
618
|
+
raise KerykeionException(
|
|
619
|
+
"For offline mode, you must provide timezone (tz_str) and coordinates (lat, lng)"
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
# Fetch location data if needed
|
|
623
|
+
if online and (not tz_str or lat is None or lng is None):
|
|
624
|
+
location.fetch_from_geonames(
|
|
625
|
+
username=geonames_username or DEFAULT_GEONAMES_USERNAME,
|
|
626
|
+
cache_expire_after_days=cache_expire_after_days
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
# Prepare location for calculations
|
|
630
|
+
location.prepare_for_calculation()
|
|
631
|
+
|
|
632
|
+
# Add location data to calculation data
|
|
633
|
+
calc_data["city"] = location.city
|
|
634
|
+
calc_data["nation"] = location.nation
|
|
635
|
+
calc_data["lat"] = location.lat
|
|
636
|
+
calc_data["lng"] = location.lng
|
|
637
|
+
calc_data["tz_str"] = location.tz_str
|
|
638
|
+
calc_data["altitude"] = location.altitude
|
|
639
|
+
|
|
640
|
+
# Store calculation parameters
|
|
641
|
+
calc_data["year"] = year
|
|
642
|
+
calc_data["month"] = month
|
|
643
|
+
calc_data["day"] = day
|
|
644
|
+
calc_data["hour"] = hour
|
|
645
|
+
calc_data["minute"] = minute
|
|
646
|
+
calc_data["seconds"] = seconds
|
|
647
|
+
calc_data["is_dst"] = is_dst
|
|
648
|
+
|
|
649
|
+
# Calculate time conversions
|
|
650
|
+
AstrologicalSubjectFactory._calculate_time_conversions(calc_data, location)
|
|
651
|
+
# Initialize Swiss Ephemeris and calculate houses and planets with context manager
|
|
652
|
+
ephe_path = str(Path(__file__).parent.absolute() / "sweph")
|
|
653
|
+
with ephemeris_context(
|
|
654
|
+
ephe_path=ephe_path,
|
|
655
|
+
config=config,
|
|
656
|
+
lng=calc_data["lng"],
|
|
657
|
+
lat=calc_data["lat"],
|
|
658
|
+
alt=calc_data["altitude"],
|
|
659
|
+
) as iflag:
|
|
660
|
+
calc_data["_iflag"] = iflag
|
|
661
|
+
# House system name (previously set in _setup_ephemeris)
|
|
662
|
+
calc_data["houses_system_name"] = swe.house_name(
|
|
663
|
+
config.houses_system_identifier.encode("ascii")
|
|
664
|
+
)
|
|
665
|
+
calculated_axial_cusps = AstrologicalSubjectFactory._calculate_houses(
|
|
666
|
+
calc_data, active_points_list
|
|
667
|
+
)
|
|
668
|
+
AstrologicalSubjectFactory._calculate_planets(
|
|
669
|
+
calc_data, active_points_list, calculated_axial_cusps
|
|
670
|
+
)
|
|
671
|
+
AstrologicalSubjectFactory._calculate_day_of_week(calc_data)
|
|
672
|
+
|
|
673
|
+
# Calculate lunar phase (optional - only if requested and Sun and Moon are available)
|
|
674
|
+
if calculate_lunar_phase and "moon" in calc_data and "sun" in calc_data:
|
|
675
|
+
calc_data["lunar_phase"] = calculate_moon_phase(
|
|
676
|
+
calc_data["moon"].abs_pos, # type: ignore[attr-defined,union-attr]
|
|
677
|
+
calc_data["sun"].abs_pos # type: ignore[attr-defined,union-attr]
|
|
678
|
+
)
|
|
679
|
+
else:
|
|
680
|
+
calc_data["lunar_phase"] = None
|
|
681
|
+
|
|
682
|
+
# Create and return the AstrologicalSubjectModel
|
|
683
|
+
return AstrologicalSubjectModel(**calc_data)
|
|
684
|
+
|
|
685
|
+
@classmethod
|
|
686
|
+
def from_iso_utc_time(
|
|
687
|
+
cls,
|
|
688
|
+
name: str,
|
|
689
|
+
iso_utc_time: str,
|
|
690
|
+
city: str = "Greenwich",
|
|
691
|
+
nation: str = "GB",
|
|
692
|
+
tz_str: str = "Etc/GMT",
|
|
693
|
+
online: bool = True,
|
|
694
|
+
lng: float = 0.0,
|
|
695
|
+
lat: float = 51.5074,
|
|
696
|
+
geonames_username: str = DEFAULT_GEONAMES_USERNAME,
|
|
697
|
+
zodiac_type: ZodiacType = DEFAULT_ZODIAC_TYPE,
|
|
698
|
+
sidereal_mode: Optional[SiderealMode] = None,
|
|
699
|
+
houses_system_identifier: HousesSystemIdentifier = DEFAULT_HOUSES_SYSTEM_IDENTIFIER,
|
|
700
|
+
perspective_type: PerspectiveType = DEFAULT_PERSPECTIVE_TYPE,
|
|
701
|
+
altitude: Optional[float] = None,
|
|
702
|
+
active_points: Optional[List[AstrologicalPoint]] = None,
|
|
703
|
+
calculate_lunar_phase: bool = True,
|
|
704
|
+
suppress_geonames_warning: bool = False
|
|
705
|
+
) -> AstrologicalSubjectModel:
|
|
706
|
+
"""
|
|
707
|
+
Create an astrological subject from an ISO formatted UTC timestamp.
|
|
708
|
+
|
|
709
|
+
This method is ideal for creating astrological subjects from standardized
|
|
710
|
+
time formats, such as those stored in databases or received from APIs.
|
|
711
|
+
It automatically handles timezone conversion from UTC to the specified
|
|
712
|
+
local timezone.
|
|
713
|
+
|
|
714
|
+
Args:
|
|
715
|
+
name (str): Name or identifier for the subject.
|
|
716
|
+
iso_utc_time (str): ISO 8601 formatted UTC timestamp. Supported formats:
|
|
717
|
+
- "2023-06-15T14:30:00Z" (with Z suffix)
|
|
718
|
+
- "2023-06-15T14:30:00+00:00" (with UTC offset)
|
|
719
|
+
- "2023-06-15T14:30:00.123Z" (with milliseconds)
|
|
720
|
+
city (str, optional): City name for location. Defaults to "Greenwich".
|
|
721
|
+
nation (str, optional): ISO country code. Defaults to "GB".
|
|
722
|
+
tz_str (str, optional): IANA timezone identifier for result conversion.
|
|
723
|
+
The ISO time is assumed to be in UTC and will be converted to this
|
|
724
|
+
timezone. Defaults to "Etc/GMT".
|
|
725
|
+
online (bool, optional): Whether to fetch coordinates online. If True,
|
|
726
|
+
coordinates are fetched via GeoNames API. Defaults to True.
|
|
727
|
+
lng (float, optional): Longitude in decimal degrees. Used when online=False
|
|
728
|
+
or as fallback. Defaults to 0.0 (Greenwich).
|
|
729
|
+
lat (float, optional): Latitude in decimal degrees. Used when online=False
|
|
730
|
+
or as fallback. Defaults to 51.5074 (Greenwich).
|
|
731
|
+
geonames_username (str, optional): GeoNames API username. Required when
|
|
732
|
+
online=True. Defaults to DEFAULT_GEONAMES_USERNAME.
|
|
733
|
+
zodiac_type (ZodiacType, optional): Zodiac system. Defaults to 'Tropical'.
|
|
734
|
+
sidereal_mode (SiderealMode, optional): Sidereal mode when zodiac_type
|
|
735
|
+
is 'Sidereal'. Defaults to None.
|
|
736
|
+
houses_system_identifier (HousesSystemIdentifier, optional): House system.
|
|
737
|
+
Defaults to 'P' (Placidus).
|
|
738
|
+
perspective_type (PerspectiveType, optional): Calculation perspective.
|
|
739
|
+
Defaults to 'Apparent Geocentric'.
|
|
740
|
+
altitude (float, optional): Altitude in meters for topocentric calculations.
|
|
741
|
+
Defaults to None (sea level).
|
|
742
|
+
active_points (Optional[List[AstrologicalPoint]], optional): Points to calculate.
|
|
743
|
+
If None, uses DEFAULT_ACTIVE_POINTS.
|
|
744
|
+
calculate_lunar_phase (bool, optional): Whether to calculate lunar phase.
|
|
745
|
+
Defaults to True.
|
|
746
|
+
|
|
747
|
+
Returns:
|
|
748
|
+
AstrologicalSubjectModel: Astrological subject with positions calculated
|
|
749
|
+
for the specified UTC time converted to local timezone.
|
|
750
|
+
|
|
751
|
+
Raises:
|
|
752
|
+
ValueError: If the ISO timestamp format is invalid or cannot be parsed.
|
|
753
|
+
KerykeionException: If location data cannot be fetched or is invalid.
|
|
754
|
+
|
|
755
|
+
Examples:
|
|
756
|
+
>>> # From API timestamp with online location lookup
|
|
757
|
+
>>> subject = AstrologicalSubjectFactory.from_iso_utc_time(
|
|
758
|
+
... name="Event Chart",
|
|
759
|
+
... iso_utc_time="2023-12-25T12:00:00Z",
|
|
760
|
+
... city="Tokyo", nation="JP",
|
|
761
|
+
... tz_str="Asia/Tokyo",
|
|
762
|
+
... geonames_username="your_username"
|
|
763
|
+
... )
|
|
764
|
+
|
|
765
|
+
>>> # From database timestamp with manual coordinates
|
|
766
|
+
>>> subject = AstrologicalSubjectFactory.from_iso_utc_time(
|
|
767
|
+
... name="Historical Event",
|
|
768
|
+
... iso_utc_time="1969-07-20T20:17:00Z",
|
|
769
|
+
... lng=-95.0969, lat=37.4419, # Houston
|
|
770
|
+
... tz_str="America/Chicago",
|
|
771
|
+
... online=False
|
|
772
|
+
... )
|
|
773
|
+
|
|
774
|
+
Note:
|
|
775
|
+
- The method assumes the input timestamp is in UTC
|
|
776
|
+
- Local time conversion respects DST rules for the target timezone
|
|
777
|
+
- Milliseconds in the timestamp are supported but truncated to seconds
|
|
778
|
+
- When online=True, the city/nation parameters override lng/lat
|
|
779
|
+
"""
|
|
780
|
+
# Parse the ISO time
|
|
781
|
+
dt = datetime.fromisoformat(iso_utc_time.replace('Z', '+00:00'))
|
|
782
|
+
|
|
783
|
+
# Get location data if online mode is enabled
|
|
784
|
+
if online:
|
|
785
|
+
if geonames_username == DEFAULT_GEONAMES_USERNAME:
|
|
786
|
+
if not suppress_geonames_warning:
|
|
787
|
+
logging.warning(GEONAMES_DEFAULT_USERNAME_WARNING)
|
|
788
|
+
|
|
789
|
+
geonames = FetchGeonames(
|
|
790
|
+
city,
|
|
791
|
+
nation,
|
|
792
|
+
username=geonames_username,
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
city_data = geonames.get_serialized_data()
|
|
796
|
+
lng = float(city_data["lng"])
|
|
797
|
+
lat = float(city_data["lat"])
|
|
798
|
+
|
|
799
|
+
# Convert UTC to local time
|
|
800
|
+
local_time = pytz.timezone(tz_str)
|
|
801
|
+
local_datetime = dt.astimezone(local_time)
|
|
802
|
+
|
|
803
|
+
# Create the subject with local time
|
|
804
|
+
return cls.from_birth_data(
|
|
805
|
+
name=name,
|
|
806
|
+
year=local_datetime.year,
|
|
807
|
+
month=local_datetime.month,
|
|
808
|
+
day=local_datetime.day,
|
|
809
|
+
hour=local_datetime.hour,
|
|
810
|
+
minute=local_datetime.minute,
|
|
811
|
+
seconds=local_datetime.second,
|
|
812
|
+
city=city,
|
|
813
|
+
nation=nation,
|
|
814
|
+
lng=lng,
|
|
815
|
+
lat=lat,
|
|
816
|
+
tz_str=tz_str,
|
|
817
|
+
online=False, # Already fetched data if needed
|
|
818
|
+
geonames_username=geonames_username,
|
|
819
|
+
zodiac_type=zodiac_type,
|
|
820
|
+
sidereal_mode=sidereal_mode,
|
|
821
|
+
houses_system_identifier=houses_system_identifier,
|
|
822
|
+
perspective_type=perspective_type,
|
|
823
|
+
altitude=altitude,
|
|
824
|
+
active_points=active_points,
|
|
825
|
+
calculate_lunar_phase=calculate_lunar_phase,
|
|
826
|
+
suppress_geonames_warning=suppress_geonames_warning
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
@classmethod
|
|
830
|
+
def from_current_time(
|
|
831
|
+
cls,
|
|
832
|
+
name: str = "Now",
|
|
833
|
+
city: Optional[str] = None,
|
|
834
|
+
nation: Optional[str] = None,
|
|
835
|
+
lng: Optional[float] = None,
|
|
836
|
+
lat: Optional[float] = None,
|
|
837
|
+
tz_str: Optional[str] = None,
|
|
838
|
+
geonames_username: Optional[str] = None,
|
|
839
|
+
online: bool = True,
|
|
840
|
+
zodiac_type: ZodiacType = DEFAULT_ZODIAC_TYPE,
|
|
841
|
+
sidereal_mode: Optional[SiderealMode] = None,
|
|
842
|
+
houses_system_identifier: HousesSystemIdentifier = DEFAULT_HOUSES_SYSTEM_IDENTIFIER,
|
|
843
|
+
perspective_type: PerspectiveType = DEFAULT_PERSPECTIVE_TYPE,
|
|
844
|
+
active_points: Optional[List[AstrologicalPoint]] = None,
|
|
845
|
+
calculate_lunar_phase: bool = True,
|
|
846
|
+
suppress_geonames_warning: bool = False
|
|
847
|
+
) -> AstrologicalSubjectModel:
|
|
848
|
+
"""
|
|
849
|
+
Create an astrological subject for the current moment in time.
|
|
850
|
+
|
|
851
|
+
This convenience method creates a "now" chart, capturing the current
|
|
852
|
+
astrological conditions at the moment of execution. Useful for horary
|
|
853
|
+
astrology, electional astrology, or real-time astrological monitoring.
|
|
854
|
+
|
|
855
|
+
Args:
|
|
856
|
+
name (str, optional): Name for the current moment chart.
|
|
857
|
+
Defaults to "Now".
|
|
858
|
+
city (str, optional): City name for location lookup. If not provided
|
|
859
|
+
and online=True, defaults to Greenwich.
|
|
860
|
+
nation (str, optional): ISO country code. If not provided and
|
|
861
|
+
online=True, defaults to 'GB'.
|
|
862
|
+
lng (float, optional): Longitude in decimal degrees. If not provided
|
|
863
|
+
and online=True, fetched from GeoNames API.
|
|
864
|
+
lat (float, optional): Latitude in decimal degrees. If not provided
|
|
865
|
+
and online=True, fetched from GeoNames API.
|
|
866
|
+
tz_str (str, optional): IANA timezone identifier. If not provided
|
|
867
|
+
and online=True, fetched from GeoNames API.
|
|
868
|
+
geonames_username (str, optional): GeoNames API username for location
|
|
869
|
+
lookup. Required when online=True and location is not fully specified.
|
|
870
|
+
online (bool, optional): Whether to fetch location data online.
|
|
871
|
+
Defaults to True.
|
|
872
|
+
zodiac_type (ZodiacType, optional): Zodiac system to use.
|
|
873
|
+
Defaults to 'Tropical'.
|
|
874
|
+
sidereal_mode (SiderealMode, optional): Sidereal calculation mode.
|
|
875
|
+
Only used when zodiac_type is 'Sidereal'. Defaults to None.
|
|
876
|
+
houses_system_identifier (HousesSystemIdentifier, optional): House
|
|
877
|
+
system for calculations. Defaults to 'P' (Placidus).
|
|
878
|
+
perspective_type (PerspectiveType, optional): Calculation perspective.
|
|
879
|
+
Defaults to 'Apparent Geocentric'.
|
|
880
|
+
active_points (Optional[List[AstrologicalPoint]], optional): Astrological points
|
|
881
|
+
to calculate. If None, uses DEFAULT_ACTIVE_POINTS.
|
|
882
|
+
calculate_lunar_phase (bool, optional): Whether to calculate lunar phase.
|
|
883
|
+
Defaults to True.
|
|
884
|
+
|
|
885
|
+
Returns:
|
|
886
|
+
AstrologicalSubjectModel: Astrological subject representing current
|
|
887
|
+
astrological conditions at the specified or default location.
|
|
888
|
+
|
|
889
|
+
Raises:
|
|
890
|
+
KerykeionException: If online location lookup fails or if offline mode
|
|
891
|
+
is used without sufficient location data.
|
|
892
|
+
|
|
893
|
+
Examples:
|
|
894
|
+
>>> # Current moment for your location
|
|
895
|
+
>>> now_chart = AstrologicalSubjectFactory.from_current_time(
|
|
896
|
+
... name="Current Transits",
|
|
897
|
+
... city="New York", nation="US",
|
|
898
|
+
... geonames_username="your_username"
|
|
899
|
+
... )
|
|
900
|
+
|
|
901
|
+
>>> # Horary chart with specific coordinates
|
|
902
|
+
>>> horary = AstrologicalSubjectFactory.from_current_time(
|
|
903
|
+
... name="Horary Question",
|
|
904
|
+
... lng=-0.1278, lat=51.5074, # London
|
|
905
|
+
... tz_str="Europe/London",
|
|
906
|
+
... online=False
|
|
907
|
+
... )
|
|
908
|
+
|
|
909
|
+
>>> # Current sidereal positions
|
|
910
|
+
>>> sidereal_now = AstrologicalSubjectFactory.from_current_time(
|
|
911
|
+
... name="Sidereal Now",
|
|
912
|
+
... city="Mumbai", nation="IN",
|
|
913
|
+
... zodiac_type="Sidereal",
|
|
914
|
+
... sidereal_mode="LAHIRI"
|
|
915
|
+
... )
|
|
916
|
+
|
|
917
|
+
Note:
|
|
918
|
+
- The exact time is captured at method execution, including seconds
|
|
919
|
+
- For horary astrology, consider the moment of understanding the question
|
|
920
|
+
- System clock accuracy affects precision; ensure accurate system time
|
|
921
|
+
- Time zone detection is automatic when using online location lookup
|
|
922
|
+
"""
|
|
923
|
+
now = datetime.now()
|
|
924
|
+
|
|
925
|
+
return cls.from_birth_data(
|
|
926
|
+
name=name,
|
|
927
|
+
year=now.year,
|
|
928
|
+
month=now.month,
|
|
929
|
+
day=now.day,
|
|
930
|
+
hour=now.hour,
|
|
931
|
+
minute=now.minute,
|
|
932
|
+
seconds=now.second,
|
|
933
|
+
city=city,
|
|
934
|
+
nation=nation,
|
|
935
|
+
lng=lng,
|
|
936
|
+
lat=lat,
|
|
937
|
+
tz_str=tz_str,
|
|
938
|
+
geonames_username=geonames_username,
|
|
939
|
+
online=online,
|
|
940
|
+
zodiac_type=zodiac_type,
|
|
941
|
+
sidereal_mode=sidereal_mode,
|
|
942
|
+
houses_system_identifier=houses_system_identifier,
|
|
943
|
+
perspective_type=perspective_type,
|
|
944
|
+
active_points=active_points,
|
|
945
|
+
calculate_lunar_phase=calculate_lunar_phase,
|
|
946
|
+
suppress_geonames_warning=suppress_geonames_warning
|
|
947
|
+
)
|
|
948
|
+
|
|
949
|
+
@staticmethod
|
|
950
|
+
def _calculate_time_conversions(data: Dict[str, Any], location: LocationData) -> None:
|
|
951
|
+
"""
|
|
952
|
+
Calculate time conversions between local time, UTC, and Julian Day Number.
|
|
953
|
+
|
|
954
|
+
Handles timezone-aware conversion from local civil time to UTC and astronomical
|
|
955
|
+
Julian Day Number, including proper DST handling and timezone localization.
|
|
956
|
+
|
|
957
|
+
Args:
|
|
958
|
+
data (Dict[str, Any]): Calculation data dictionary containing time components
|
|
959
|
+
(year, month, day, hour, minute, seconds) and optional DST flag.
|
|
960
|
+
location (LocationData): Location data containing timezone information.
|
|
961
|
+
|
|
962
|
+
Raises:
|
|
963
|
+
KerykeionException: If DST ambiguity occurs during timezone transitions
|
|
964
|
+
and is_dst parameter is not explicitly set to resolve the ambiguity.
|
|
965
|
+
|
|
966
|
+
Side Effects:
|
|
967
|
+
Updates data dictionary with:
|
|
968
|
+
- iso_formatted_utc_datetime: ISO 8601 UTC timestamp
|
|
969
|
+
- iso_formatted_local_datetime: ISO 8601 local timestamp
|
|
970
|
+
- julian_day: Julian Day Number for astronomical calculations
|
|
971
|
+
|
|
972
|
+
Note:
|
|
973
|
+
During DST transitions, times may be ambiguous (fall back) or non-existent
|
|
974
|
+
(spring forward). The method raises an exception for ambiguous times unless
|
|
975
|
+
the is_dst parameter is explicitly set to True or False.
|
|
976
|
+
"""
|
|
977
|
+
# Convert local time to UTC
|
|
978
|
+
local_timezone = pytz.timezone(location.tz_str)
|
|
979
|
+
naive_datetime = datetime(
|
|
980
|
+
data["year"], data["month"], data["day"],
|
|
981
|
+
data["hour"], data["minute"], data["seconds"]
|
|
982
|
+
)
|
|
983
|
+
|
|
984
|
+
try:
|
|
985
|
+
local_datetime = local_timezone.localize(naive_datetime, is_dst=data.get("is_dst"))
|
|
986
|
+
except pytz.exceptions.AmbiguousTimeError:
|
|
987
|
+
raise KerykeionException(
|
|
988
|
+
"Ambiguous time error! The time falls during a DST transition. "
|
|
989
|
+
"Please specify is_dst=True or is_dst=False to clarify."
|
|
990
|
+
)
|
|
991
|
+
except pytz.exceptions.NonExistentTimeError:
|
|
992
|
+
raise KerykeionException(
|
|
993
|
+
"Non-existent time error! The time does not exist due to DST transition (spring forward). "
|
|
994
|
+
"Please specify a valid time."
|
|
995
|
+
)
|
|
996
|
+
|
|
997
|
+
# Store formatted times
|
|
998
|
+
utc_datetime = local_datetime.astimezone(pytz.utc)
|
|
999
|
+
data["iso_formatted_utc_datetime"] = utc_datetime.isoformat()
|
|
1000
|
+
data["iso_formatted_local_datetime"] = local_datetime.isoformat()
|
|
1001
|
+
|
|
1002
|
+
# Calculate Julian day
|
|
1003
|
+
data["julian_day"] = datetime_to_julian(utc_datetime)
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
@staticmethod
|
|
1007
|
+
def _calculate_houses(data: Dict[str, Any], active_points: Optional[List[AstrologicalPoint]]) -> List[AstrologicalPoint]:
|
|
1008
|
+
"""
|
|
1009
|
+
Calculate house cusps and angular points (Ascendant, MC, etc.).
|
|
1010
|
+
|
|
1011
|
+
Computes the 12 house cusps using the specified house system and calculates
|
|
1012
|
+
the four main angles of the chart. Only calculates angular points that are
|
|
1013
|
+
included in the active_points list for performance optimization.
|
|
1014
|
+
|
|
1015
|
+
Args:
|
|
1016
|
+
data (Dict[str, Any]): Calculation data dictionary containing configuration
|
|
1017
|
+
and location information. Updated with calculated house and angle data.
|
|
1018
|
+
active_points (Optional[List[AstrologicalPoint]]): List of points to calculate.
|
|
1019
|
+
If None, all points are calculated. Angular points not in this list
|
|
1020
|
+
are skipped for performance.
|
|
1021
|
+
|
|
1022
|
+
Side Effects:
|
|
1023
|
+
Updates data dictionary with:
|
|
1024
|
+
- House cusp objects: first_house through twelfth_house
|
|
1025
|
+
- Angular points: ascendant, medium_coeli, descendant, imum_coeli
|
|
1026
|
+
- houses_names_list: List of all house names
|
|
1027
|
+
- _houses_degree_ut: Raw house cusp degrees for internal use
|
|
1028
|
+
|
|
1029
|
+
House Systems Supported:
|
|
1030
|
+
All systems supported by Swiss Ephemeris including Placidus, Koch,
|
|
1031
|
+
Equal House, Whole Sign, Regiomontanus, Campanus, Topocentric, etc.
|
|
1032
|
+
|
|
1033
|
+
Angular Points Calculated:
|
|
1034
|
+
- Ascendant: Eastern horizon point (1st house cusp)
|
|
1035
|
+
- Medium Coeli (Midheaven): Southern meridian point (10th house cusp)
|
|
1036
|
+
- Descendant: Western horizon point (opposite Ascendant)
|
|
1037
|
+
- Imum Coeli: Northern meridian point (opposite Medium Coeli)
|
|
1038
|
+
|
|
1039
|
+
Note:
|
|
1040
|
+
House calculations respect the zodiac type (Tropical/Sidereal) and use
|
|
1041
|
+
the appropriate Swiss Ephemeris function. Angular points include house
|
|
1042
|
+
position and retrograde status (always False for angles).
|
|
1043
|
+
"""
|
|
1044
|
+
# Skip calculation if point is not in active_points
|
|
1045
|
+
def should_calculate(point: AstrologicalPoint) -> bool:
|
|
1046
|
+
return not active_points or point in active_points
|
|
1047
|
+
# Track which axial cusps are actually calculated
|
|
1048
|
+
calculated_axial_cusps: List[AstrologicalPoint] = []
|
|
1049
|
+
|
|
1050
|
+
# Calculate houses based on zodiac type
|
|
1051
|
+
if data["zodiac_type"] == "Sidereal":
|
|
1052
|
+
cusps, ascmc = swe.houses_ex(
|
|
1053
|
+
tjdut=data["julian_day"],
|
|
1054
|
+
lat=data["lat"],
|
|
1055
|
+
lon=data["lng"],
|
|
1056
|
+
hsys=str.encode(data["houses_system_identifier"]),
|
|
1057
|
+
flags=swe.FLG_SIDEREAL
|
|
1058
|
+
)
|
|
1059
|
+
else: # Tropical zodiac
|
|
1060
|
+
cusps, ascmc = swe.houses(
|
|
1061
|
+
tjdut=data["julian_day"],
|
|
1062
|
+
lat=data["lat"],
|
|
1063
|
+
lon=data["lng"],
|
|
1064
|
+
hsys=str.encode(data["houses_system_identifier"])
|
|
1065
|
+
)
|
|
1066
|
+
|
|
1067
|
+
# Store house degrees
|
|
1068
|
+
data["_houses_degree_ut"] = cusps
|
|
1069
|
+
|
|
1070
|
+
# Create house objects
|
|
1071
|
+
point_type: PointType = "House"
|
|
1072
|
+
data["first_house"] = get_kerykeion_point_from_degree(cusps[0], "First_House", point_type=point_type)
|
|
1073
|
+
data["second_house"] = get_kerykeion_point_from_degree(cusps[1], "Second_House", point_type=point_type)
|
|
1074
|
+
data["third_house"] = get_kerykeion_point_from_degree(cusps[2], "Third_House", point_type=point_type)
|
|
1075
|
+
data["fourth_house"] = get_kerykeion_point_from_degree(cusps[3], "Fourth_House", point_type=point_type)
|
|
1076
|
+
data["fifth_house"] = get_kerykeion_point_from_degree(cusps[4], "Fifth_House", point_type=point_type)
|
|
1077
|
+
data["sixth_house"] = get_kerykeion_point_from_degree(cusps[5], "Sixth_House", point_type=point_type)
|
|
1078
|
+
data["seventh_house"] = get_kerykeion_point_from_degree(cusps[6], "Seventh_House", point_type=point_type)
|
|
1079
|
+
data["eighth_house"] = get_kerykeion_point_from_degree(cusps[7], "Eighth_House", point_type=point_type)
|
|
1080
|
+
data["ninth_house"] = get_kerykeion_point_from_degree(cusps[8], "Ninth_House", point_type=point_type)
|
|
1081
|
+
data["tenth_house"] = get_kerykeion_point_from_degree(cusps[9], "Tenth_House", point_type=point_type)
|
|
1082
|
+
data["eleventh_house"] = get_kerykeion_point_from_degree(cusps[10], "Eleventh_House", point_type=point_type)
|
|
1083
|
+
data["twelfth_house"] = get_kerykeion_point_from_degree(cusps[11], "Twelfth_House", point_type=point_type)
|
|
1084
|
+
|
|
1085
|
+
# Store house names
|
|
1086
|
+
data["houses_names_list"] = list(get_args(Houses))
|
|
1087
|
+
|
|
1088
|
+
# Calculate axis points
|
|
1089
|
+
point_type = "AstrologicalPoint"
|
|
1090
|
+
|
|
1091
|
+
# Calculate Ascendant if needed
|
|
1092
|
+
if should_calculate("Ascendant"):
|
|
1093
|
+
data["ascendant"] = get_kerykeion_point_from_degree(ascmc[0], "Ascendant", point_type=point_type)
|
|
1094
|
+
data["ascendant"].house = get_planet_house(data["ascendant"].abs_pos, data["_houses_degree_ut"])
|
|
1095
|
+
data["ascendant"].retrograde = False
|
|
1096
|
+
calculated_axial_cusps.append("Ascendant")
|
|
1097
|
+
|
|
1098
|
+
# Calculate Medium Coeli if needed
|
|
1099
|
+
if should_calculate("Medium_Coeli"):
|
|
1100
|
+
data["medium_coeli"] = get_kerykeion_point_from_degree(ascmc[1], "Medium_Coeli", point_type=point_type)
|
|
1101
|
+
data["medium_coeli"].house = get_planet_house(data["medium_coeli"].abs_pos, data["_houses_degree_ut"])
|
|
1102
|
+
data["medium_coeli"].retrograde = False
|
|
1103
|
+
calculated_axial_cusps.append("Medium_Coeli")
|
|
1104
|
+
|
|
1105
|
+
# Calculate Descendant if needed
|
|
1106
|
+
if should_calculate("Descendant"):
|
|
1107
|
+
dsc_deg = math.fmod(ascmc[0] + 180, 360)
|
|
1108
|
+
data["descendant"] = get_kerykeion_point_from_degree(dsc_deg, "Descendant", point_type=point_type)
|
|
1109
|
+
data["descendant"].house = get_planet_house(data["descendant"].abs_pos, data["_houses_degree_ut"])
|
|
1110
|
+
data["descendant"].retrograde = False
|
|
1111
|
+
calculated_axial_cusps.append("Descendant")
|
|
1112
|
+
|
|
1113
|
+
# Calculate Imum Coeli if needed
|
|
1114
|
+
if should_calculate("Imum_Coeli"):
|
|
1115
|
+
ic_deg = math.fmod(ascmc[1] + 180, 360)
|
|
1116
|
+
data["imum_coeli"] = get_kerykeion_point_from_degree(ic_deg, "Imum_Coeli", point_type=point_type)
|
|
1117
|
+
data["imum_coeli"].house = get_planet_house(data["imum_coeli"].abs_pos, data["_houses_degree_ut"])
|
|
1118
|
+
data["imum_coeli"].retrograde = False
|
|
1119
|
+
calculated_axial_cusps.append("Imum_Coeli")
|
|
1120
|
+
|
|
1121
|
+
return calculated_axial_cusps
|
|
1122
|
+
|
|
1123
|
+
@staticmethod
|
|
1124
|
+
def _calculate_single_planet(
|
|
1125
|
+
data: Dict[str, Any],
|
|
1126
|
+
planet_name: AstrologicalPoint,
|
|
1127
|
+
planet_id: int,
|
|
1128
|
+
julian_day: float,
|
|
1129
|
+
iflag: int,
|
|
1130
|
+
houses_degree_ut: List[float],
|
|
1131
|
+
point_type: PointType,
|
|
1132
|
+
calculated_planets: List[AstrologicalPoint],
|
|
1133
|
+
active_points: List[AstrologicalPoint]
|
|
1134
|
+
) -> None:
|
|
1135
|
+
"""
|
|
1136
|
+
Calculate a single celestial body's position with comprehensive error handling.
|
|
1137
|
+
|
|
1138
|
+
Computes the position of a single planet, asteroid, or other celestial object
|
|
1139
|
+
using Swiss Ephemeris, creates a Kerykeion point object, determines house
|
|
1140
|
+
position, and assesses retrograde status. Handles calculation errors gracefully
|
|
1141
|
+
by logging and removing failed points from the active list.
|
|
1142
|
+
|
|
1143
|
+
Args:
|
|
1144
|
+
data (Dict[str, Any]): Main calculation data dictionary to store results.
|
|
1145
|
+
planet_name (AstrologicalPoint): Name identifier for the celestial body.
|
|
1146
|
+
planet_id (int): Swiss Ephemeris numerical identifier for the object.
|
|
1147
|
+
julian_day (float): Julian Day Number for the calculation moment.
|
|
1148
|
+
iflag (int): Swiss Ephemeris calculation flags (perspective, zodiac, etc.).
|
|
1149
|
+
houses_degree_ut (List[float]): House cusp degrees for house determination.
|
|
1150
|
+
point_type (PointType): Classification of the point type for the object.
|
|
1151
|
+
calculated_planets (List[str]): Running list of successfully calculated objects.
|
|
1152
|
+
active_points (List[AstrologicalPoint]): Active points list (modified on error).
|
|
1153
|
+
|
|
1154
|
+
Side Effects:
|
|
1155
|
+
- Adds calculated object to data dictionary using lowercase planet_name as key
|
|
1156
|
+
- Appends planet_name to calculated_planets list on success
|
|
1157
|
+
- Removes planet_name from active_points list on calculation failure
|
|
1158
|
+
- Logs error messages for calculation failures
|
|
1159
|
+
|
|
1160
|
+
Calculated Properties:
|
|
1161
|
+
- Zodiacal position (longitude) in degrees
|
|
1162
|
+
- House position based on house cusp positions
|
|
1163
|
+
- Retrograde status based on velocity (negative = retrograde)
|
|
1164
|
+
- Sign, degree, and minute components
|
|
1165
|
+
|
|
1166
|
+
Error Handling:
|
|
1167
|
+
If Swiss Ephemeris calculation fails (e.g., for distant asteroids outside
|
|
1168
|
+
ephemeris range), the method logs the error and removes the object from
|
|
1169
|
+
active_points to prevent cascade failures.
|
|
1170
|
+
|
|
1171
|
+
Note:
|
|
1172
|
+
The method uses the Swiss Ephemeris calc_ut function which returns position
|
|
1173
|
+
and velocity data. Retrograde determination is based on the velocity
|
|
1174
|
+
component being negative (element index 3).
|
|
1175
|
+
"""
|
|
1176
|
+
try:
|
|
1177
|
+
# Calculate planet position using Swiss Ephemeris (ecliptic coordinates)
|
|
1178
|
+
planet_calc = swe.calc_ut(julian_day, planet_id, iflag)[0]
|
|
1179
|
+
|
|
1180
|
+
# Get declination from equatorial coordinates
|
|
1181
|
+
planet_eq = swe.calc_ut(julian_day, planet_id, iflag | swe.FLG_EQUATORIAL)[0]
|
|
1182
|
+
declination = planet_eq[1] # Declination from equatorial coordinates
|
|
1183
|
+
|
|
1184
|
+
# Create Kerykeion point from degree
|
|
1185
|
+
data[planet_name.lower()] = get_kerykeion_point_from_degree(
|
|
1186
|
+
planet_calc[0], planet_name, point_type=point_type, speed=planet_calc[3], declination=declination
|
|
1187
|
+
)
|
|
1188
|
+
|
|
1189
|
+
# Calculate house position
|
|
1190
|
+
data[planet_name.lower()].house = get_planet_house(planet_calc[0], houses_degree_ut)
|
|
1191
|
+
|
|
1192
|
+
# Determine if planet is retrograde
|
|
1193
|
+
data[planet_name.lower()].retrograde = planet_calc[3] < 0
|
|
1194
|
+
|
|
1195
|
+
# Track calculated planet
|
|
1196
|
+
calculated_planets.append(planet_name)
|
|
1197
|
+
|
|
1198
|
+
except Exception as e:
|
|
1199
|
+
logging.error(f"Error calculating {planet_name}: {e}")
|
|
1200
|
+
if planet_name in active_points:
|
|
1201
|
+
active_points.remove(planet_name)
|
|
1202
|
+
|
|
1203
|
+
@staticmethod
|
|
1204
|
+
def _calculate_planets(data: Dict[str, Any], active_points: List[AstrologicalPoint], calculated_axial_cusps: Optional[List[AstrologicalPoint]] = None) -> None:
|
|
1205
|
+
"""
|
|
1206
|
+
Calculate positions for all requested celestial bodies and special points.
|
|
1207
|
+
|
|
1208
|
+
This comprehensive method calculates positions for a wide range of astrological
|
|
1209
|
+
points including traditional planets, lunar nodes, asteroids, trans-Neptunian
|
|
1210
|
+
objects, fixed stars, Arabic parts, and specialized points like Vertex.
|
|
1211
|
+
|
|
1212
|
+
The calculation is performed selectively based on the active_points list for
|
|
1213
|
+
performance optimization. Some Arabic parts automatically activate their
|
|
1214
|
+
prerequisite points if needed.
|
|
1215
|
+
|
|
1216
|
+
Args:
|
|
1217
|
+
data (Dict[str, Any]): Main calculation data dictionary. Updated with all
|
|
1218
|
+
calculated planetary positions and related metadata.
|
|
1219
|
+
active_points (List[AstrologicalPoint]): Mutable list of points to calculate.
|
|
1220
|
+
Modified during execution to remove failed calculations and add
|
|
1221
|
+
automatically required points for Arabic parts.
|
|
1222
|
+
|
|
1223
|
+
Celestial Bodies Calculated:
|
|
1224
|
+
Traditional Planets:
|
|
1225
|
+
- Sun, Moon, Mercury, Venus, Mars, Jupiter, Saturn
|
|
1226
|
+
- Uranus, Neptune, Pluto
|
|
1227
|
+
|
|
1228
|
+
Lunar Nodes:
|
|
1229
|
+
- Mean Node, True Node (North nodes)
|
|
1230
|
+
- Mean South Node, True South Node (calculated as opposites)
|
|
1231
|
+
|
|
1232
|
+
Lilith Points:
|
|
1233
|
+
- Mean Lilith (Mean Black Moon Lilith)
|
|
1234
|
+
- True Lilith (Osculating Black Moon Lilith)
|
|
1235
|
+
|
|
1236
|
+
Asteroids:
|
|
1237
|
+
- Ceres, Pallas, Juno, Vesta (main belt asteroids)
|
|
1238
|
+
|
|
1239
|
+
Centaurs:
|
|
1240
|
+
- Chiron, Pholus
|
|
1241
|
+
|
|
1242
|
+
Trans-Neptunian Objects:
|
|
1243
|
+
- Eris, Sedna, Haumea, Makemake
|
|
1244
|
+
- Ixion, Orcus, Quaoar
|
|
1245
|
+
|
|
1246
|
+
Fixed Stars:
|
|
1247
|
+
- Regulus, Spica (examples, extensible)
|
|
1248
|
+
|
|
1249
|
+
Arabic Parts (Lots):
|
|
1250
|
+
- Pars Fortunae (Part of Fortune)
|
|
1251
|
+
- Pars Spiritus (Part of Spirit)
|
|
1252
|
+
- Pars Amoris (Part of Love/Eros)
|
|
1253
|
+
- Pars Fidei (Part of Faith)
|
|
1254
|
+
|
|
1255
|
+
Special Points:
|
|
1256
|
+
- Earth (for heliocentric perspectives)
|
|
1257
|
+
- Vertex and Anti-Vertex
|
|
1258
|
+
|
|
1259
|
+
Side Effects:
|
|
1260
|
+
- Updates data dictionary with all calculated positions
|
|
1261
|
+
- Modifies active_points list by removing failed calculations
|
|
1262
|
+
- Adds prerequisite points for Arabic parts calculations
|
|
1263
|
+
- Updates data["active_points"] with successfully calculated objects
|
|
1264
|
+
|
|
1265
|
+
Error Handling:
|
|
1266
|
+
Individual calculation failures (e.g., asteroids outside ephemeris range)
|
|
1267
|
+
are handled gracefully with logging and removal from active_points.
|
|
1268
|
+
This prevents cascade failures while maintaining calculation integrity.
|
|
1269
|
+
|
|
1270
|
+
Arabic Parts Logic:
|
|
1271
|
+
- Day/night birth detection based on Sun's house position
|
|
1272
|
+
- Automatic activation of required base points (Sun, Moon, Ascendant, etc.)
|
|
1273
|
+
- Classical formulae with day/night variations where applicable
|
|
1274
|
+
- All parts marked as non-retrograde (conceptual points)
|
|
1275
|
+
|
|
1276
|
+
Performance Notes:
|
|
1277
|
+
- Only points in active_points are calculated (selective computation)
|
|
1278
|
+
- Failed calculations are removed to prevent repeated attempts
|
|
1279
|
+
- Some expensive calculations (like distant TNOs) may timeout
|
|
1280
|
+
- Fixed stars use different calculation methods than planets
|
|
1281
|
+
|
|
1282
|
+
Note:
|
|
1283
|
+
The method maintains a running list of successfully calculated planets
|
|
1284
|
+
and updates the active_points list to reflect actual availability.
|
|
1285
|
+
This ensures that dependent calculations and aspects only use valid data.
|
|
1286
|
+
"""
|
|
1287
|
+
# Skip calculation if point is not in active_points
|
|
1288
|
+
def should_calculate(point: AstrologicalPoint) -> bool:
|
|
1289
|
+
return not active_points or point in active_points
|
|
1290
|
+
|
|
1291
|
+
point_type: PointType = "AstrologicalPoint"
|
|
1292
|
+
julian_day = data["julian_day"]
|
|
1293
|
+
iflag = data["_iflag"]
|
|
1294
|
+
houses_degree_ut = data["_houses_degree_ut"]
|
|
1295
|
+
|
|
1296
|
+
# Track which planets are actually calculated
|
|
1297
|
+
calculated_planets: List[AstrologicalPoint] = []
|
|
1298
|
+
|
|
1299
|
+
# ==================
|
|
1300
|
+
# MAIN PLANETS
|
|
1301
|
+
# ==================
|
|
1302
|
+
|
|
1303
|
+
# Calculate Sun
|
|
1304
|
+
if should_calculate("Sun"):
|
|
1305
|
+
AstrologicalSubjectFactory._calculate_single_planet(data, "Sun", 0, julian_day, iflag, houses_degree_ut, point_type, calculated_planets, active_points)
|
|
1306
|
+
|
|
1307
|
+
# Calculate Moon
|
|
1308
|
+
if should_calculate("Moon"):
|
|
1309
|
+
AstrologicalSubjectFactory._calculate_single_planet(data, "Moon", 1, julian_day, iflag, houses_degree_ut, point_type, calculated_planets, active_points)
|
|
1310
|
+
|
|
1311
|
+
# Calculate Mercury
|
|
1312
|
+
if should_calculate("Mercury"):
|
|
1313
|
+
AstrologicalSubjectFactory._calculate_single_planet(data, "Mercury", 2, julian_day, iflag, houses_degree_ut, point_type, calculated_planets, active_points)
|
|
1314
|
+
|
|
1315
|
+
# Calculate Venus
|
|
1316
|
+
if should_calculate("Venus"):
|
|
1317
|
+
AstrologicalSubjectFactory._calculate_single_planet(data, "Venus", 3, julian_day, iflag, houses_degree_ut, point_type, calculated_planets, active_points)
|
|
1318
|
+
|
|
1319
|
+
# Calculate Mars
|
|
1320
|
+
if should_calculate("Mars"):
|
|
1321
|
+
AstrologicalSubjectFactory._calculate_single_planet(data, "Mars", 4, julian_day, iflag, houses_degree_ut, point_type, calculated_planets, active_points)
|
|
1322
|
+
|
|
1323
|
+
# Calculate Jupiter
|
|
1324
|
+
if should_calculate("Jupiter"):
|
|
1325
|
+
AstrologicalSubjectFactory._calculate_single_planet(data, "Jupiter", 5, julian_day, iflag, houses_degree_ut, point_type, calculated_planets, active_points)
|
|
1326
|
+
|
|
1327
|
+
# Calculate Saturn
|
|
1328
|
+
if should_calculate("Saturn"):
|
|
1329
|
+
AstrologicalSubjectFactory._calculate_single_planet(data, "Saturn", 6, julian_day, iflag, houses_degree_ut, point_type, calculated_planets, active_points)
|
|
1330
|
+
|
|
1331
|
+
# Calculate Uranus
|
|
1332
|
+
if should_calculate("Uranus"):
|
|
1333
|
+
AstrologicalSubjectFactory._calculate_single_planet(data, "Uranus", 7, julian_day, iflag, houses_degree_ut, point_type, calculated_planets, active_points)
|
|
1334
|
+
|
|
1335
|
+
# Calculate Neptune
|
|
1336
|
+
if should_calculate("Neptune"):
|
|
1337
|
+
AstrologicalSubjectFactory._calculate_single_planet(data, "Neptune", 8, julian_day, iflag, houses_degree_ut, point_type, calculated_planets, active_points)
|
|
1338
|
+
|
|
1339
|
+
# Calculate Pluto
|
|
1340
|
+
if should_calculate("Pluto"):
|
|
1341
|
+
AstrologicalSubjectFactory._calculate_single_planet(data, "Pluto", 9, julian_day, iflag, houses_degree_ut, point_type, calculated_planets, active_points)
|
|
1342
|
+
|
|
1343
|
+
# ==================
|
|
1344
|
+
# LUNAR NODES
|
|
1345
|
+
# ==================
|
|
1346
|
+
|
|
1347
|
+
# Calculate Mean North Lunar Node
|
|
1348
|
+
if should_calculate("Mean_North_Lunar_Node"):
|
|
1349
|
+
AstrologicalSubjectFactory._calculate_single_planet(data, "Mean_North_Lunar_Node", 10, julian_day, iflag, houses_degree_ut, point_type, calculated_planets, active_points)
|
|
1350
|
+
# Get correct declination using equatorial coordinates
|
|
1351
|
+
if "mean_north_lunar_node" in data:
|
|
1352
|
+
mean_north_lunar_node_eq = swe.calc_ut(julian_day, 10, iflag | swe.FLG_EQUATORIAL)[0]
|
|
1353
|
+
data["mean_north_lunar_node"].declination = mean_north_lunar_node_eq[1] # Declination from equatorial coordinates
|
|
1354
|
+
|
|
1355
|
+
# Calculate True North Lunar Node
|
|
1356
|
+
if should_calculate("True_North_Lunar_Node"):
|
|
1357
|
+
AstrologicalSubjectFactory._calculate_single_planet(data, "True_North_Lunar_Node", 11, julian_day, iflag, houses_degree_ut, point_type, calculated_planets, active_points)
|
|
1358
|
+
# Get correct declination using equatorial coordinates
|
|
1359
|
+
if "true_north_lunar_node" in data:
|
|
1360
|
+
true_north_lunar_node_eq = swe.calc_ut(julian_day, 11, iflag | swe.FLG_EQUATORIAL)[0]
|
|
1361
|
+
data["true_north_lunar_node"].declination = true_north_lunar_node_eq[1] # Declination from equatorial coordinates
|
|
1362
|
+
|
|
1363
|
+
# Calculate Mean South Lunar Node (opposite to Mean North Lunar Node)
|
|
1364
|
+
if should_calculate("Mean_South_Lunar_Node") and "mean_north_lunar_node" in data:
|
|
1365
|
+
mean_south_lunar_node_deg = math.fmod(data["mean_north_lunar_node"].abs_pos + 180, 360)
|
|
1366
|
+
data["mean_south_lunar_node"] = get_kerykeion_point_from_degree(
|
|
1367
|
+
mean_south_lunar_node_deg, "Mean_South_Lunar_Node", point_type=point_type,
|
|
1368
|
+
speed=-data["mean_north_lunar_node"].speed if data["mean_north_lunar_node"].speed is not None else None,
|
|
1369
|
+
declination=-data["mean_north_lunar_node"].declination if data["mean_north_lunar_node"].declination is not None else None
|
|
1370
|
+
)
|
|
1371
|
+
data["mean_south_lunar_node"].house = get_planet_house(mean_south_lunar_node_deg, houses_degree_ut)
|
|
1372
|
+
data["mean_south_lunar_node"].retrograde = data["mean_north_lunar_node"].retrograde
|
|
1373
|
+
calculated_planets.append("Mean_South_Lunar_Node")
|
|
1374
|
+
|
|
1375
|
+
# Calculate True South Lunar Node (opposite to True North Lunar Node)
|
|
1376
|
+
if should_calculate("True_South_Lunar_Node") and "true_north_lunar_node" in data:
|
|
1377
|
+
true_south_lunar_node_deg = math.fmod(data["true_north_lunar_node"].abs_pos + 180, 360)
|
|
1378
|
+
data["true_south_lunar_node"] = get_kerykeion_point_from_degree(
|
|
1379
|
+
true_south_lunar_node_deg, "True_South_Lunar_Node", point_type=point_type,
|
|
1380
|
+
speed=-data["true_north_lunar_node"].speed if data["true_north_lunar_node"].speed is not None else None,
|
|
1381
|
+
declination=-data["true_north_lunar_node"].declination if data["true_north_lunar_node"].declination is not None else None
|
|
1382
|
+
)
|
|
1383
|
+
data["true_south_lunar_node"].house = get_planet_house(true_south_lunar_node_deg, houses_degree_ut)
|
|
1384
|
+
data["true_south_lunar_node"].retrograde = data["true_north_lunar_node"].retrograde
|
|
1385
|
+
calculated_planets.append("True_South_Lunar_Node")
|
|
1386
|
+
|
|
1387
|
+
# ==================
|
|
1388
|
+
# LILITH POINTS
|
|
1389
|
+
# ==================
|
|
1390
|
+
|
|
1391
|
+
# Calculate Mean Lilith (Mean Black Moon)
|
|
1392
|
+
if should_calculate("Mean_Lilith"):
|
|
1393
|
+
AstrologicalSubjectFactory._calculate_single_planet(
|
|
1394
|
+
data, "Mean_Lilith", 12, julian_day, iflag, houses_degree_ut,
|
|
1395
|
+
point_type, calculated_planets, active_points
|
|
1396
|
+
)
|
|
1397
|
+
|
|
1398
|
+
# Calculate True Lilith (Osculating Black Moon)
|
|
1399
|
+
if should_calculate("True_Lilith"):
|
|
1400
|
+
AstrologicalSubjectFactory._calculate_single_planet(
|
|
1401
|
+
data, "True_Lilith", 13, julian_day, iflag, houses_degree_ut,
|
|
1402
|
+
point_type, calculated_planets, active_points
|
|
1403
|
+
)
|
|
1404
|
+
|
|
1405
|
+
# ==================
|
|
1406
|
+
# SPECIAL POINTS
|
|
1407
|
+
# ==================
|
|
1408
|
+
|
|
1409
|
+
# Calculate Earth - useful for heliocentric charts
|
|
1410
|
+
if should_calculate("Earth"):
|
|
1411
|
+
AstrologicalSubjectFactory._calculate_single_planet(
|
|
1412
|
+
data, "Earth", 14, julian_day, iflag, houses_degree_ut,
|
|
1413
|
+
point_type, calculated_planets, active_points
|
|
1414
|
+
)
|
|
1415
|
+
|
|
1416
|
+
# Calculate Chiron
|
|
1417
|
+
if should_calculate("Chiron"):
|
|
1418
|
+
AstrologicalSubjectFactory._calculate_single_planet(
|
|
1419
|
+
data, "Chiron", 15, julian_day, iflag, houses_degree_ut,
|
|
1420
|
+
point_type, calculated_planets, active_points
|
|
1421
|
+
)
|
|
1422
|
+
|
|
1423
|
+
# Calculate Pholus
|
|
1424
|
+
if should_calculate("Pholus"):
|
|
1425
|
+
AstrologicalSubjectFactory._calculate_single_planet(
|
|
1426
|
+
data, "Pholus", 16, julian_day, iflag, houses_degree_ut,
|
|
1427
|
+
point_type, calculated_planets, active_points
|
|
1428
|
+
)
|
|
1429
|
+
|
|
1430
|
+
# ==================
|
|
1431
|
+
# ASTEROIDS
|
|
1432
|
+
# ==================
|
|
1433
|
+
|
|
1434
|
+
# Calculate Ceres
|
|
1435
|
+
if should_calculate("Ceres"):
|
|
1436
|
+
AstrologicalSubjectFactory._calculate_single_planet(
|
|
1437
|
+
data, "Ceres", 17, julian_day, iflag, houses_degree_ut,
|
|
1438
|
+
point_type, calculated_planets, active_points
|
|
1439
|
+
)
|
|
1440
|
+
|
|
1441
|
+
# Calculate Pallas
|
|
1442
|
+
if should_calculate("Pallas"):
|
|
1443
|
+
AstrologicalSubjectFactory._calculate_single_planet(
|
|
1444
|
+
data, "Pallas", 18, julian_day, iflag, houses_degree_ut,
|
|
1445
|
+
point_type, calculated_planets, active_points
|
|
1446
|
+
)
|
|
1447
|
+
|
|
1448
|
+
# Calculate Juno
|
|
1449
|
+
if should_calculate("Juno"):
|
|
1450
|
+
AstrologicalSubjectFactory._calculate_single_planet(
|
|
1451
|
+
data, "Juno", 19, julian_day, iflag, houses_degree_ut,
|
|
1452
|
+
point_type, calculated_planets, active_points
|
|
1453
|
+
)
|
|
1454
|
+
|
|
1455
|
+
# Calculate Vesta
|
|
1456
|
+
if should_calculate("Vesta"):
|
|
1457
|
+
AstrologicalSubjectFactory._calculate_single_planet(
|
|
1458
|
+
data, "Vesta", 20, julian_day, iflag, houses_degree_ut,
|
|
1459
|
+
point_type, calculated_planets, active_points
|
|
1460
|
+
)
|
|
1461
|
+
|
|
1462
|
+
# ==================
|
|
1463
|
+
# TRANS-NEPTUNIAN OBJECTS
|
|
1464
|
+
# ==================
|
|
1465
|
+
|
|
1466
|
+
# Calculate Eris
|
|
1467
|
+
if should_calculate("Eris"):
|
|
1468
|
+
try:
|
|
1469
|
+
eris_calc = swe.calc_ut(julian_day, swe.AST_OFFSET + 136199, iflag)[0]
|
|
1470
|
+
data["eris"] = get_kerykeion_point_from_degree(eris_calc[0], "Eris", point_type=point_type, speed=eris_calc[3], declination=eris_calc[1])
|
|
1471
|
+
data["eris"].house = get_planet_house(eris_calc[0], houses_degree_ut)
|
|
1472
|
+
data["eris"].retrograde = eris_calc[3] < 0
|
|
1473
|
+
calculated_planets.append("Eris")
|
|
1474
|
+
except Exception as e:
|
|
1475
|
+
logging.warning(f"Could not calculate Eris position: {e}")
|
|
1476
|
+
active_points.remove("Eris") # Remove if not calculated
|
|
1477
|
+
|
|
1478
|
+
# Calculate Sedna
|
|
1479
|
+
if should_calculate("Sedna"):
|
|
1480
|
+
try:
|
|
1481
|
+
sedna_calc = swe.calc_ut(julian_day, swe.AST_OFFSET + 90377, iflag)[0]
|
|
1482
|
+
data["sedna"] = get_kerykeion_point_from_degree(sedna_calc[0], "Sedna", point_type=point_type, speed=sedna_calc[3], declination=sedna_calc[1])
|
|
1483
|
+
data["sedna"].house = get_planet_house(sedna_calc[0], houses_degree_ut)
|
|
1484
|
+
data["sedna"].retrograde = sedna_calc[3] < 0
|
|
1485
|
+
calculated_planets.append("Sedna")
|
|
1486
|
+
except Exception as e:
|
|
1487
|
+
logging.warning(f"Could not calculate Sedna position: {e}")
|
|
1488
|
+
active_points.remove("Sedna")
|
|
1489
|
+
|
|
1490
|
+
# Calculate Haumea
|
|
1491
|
+
if should_calculate("Haumea"):
|
|
1492
|
+
try:
|
|
1493
|
+
haumea_calc = swe.calc_ut(julian_day, swe.AST_OFFSET + 136108, iflag)[0]
|
|
1494
|
+
data["haumea"] = get_kerykeion_point_from_degree(haumea_calc[0], "Haumea", point_type=point_type, speed=haumea_calc[3], declination=haumea_calc[1])
|
|
1495
|
+
data["haumea"].house = get_planet_house(haumea_calc[0], houses_degree_ut)
|
|
1496
|
+
data["haumea"].retrograde = haumea_calc[3] < 0
|
|
1497
|
+
calculated_planets.append("Haumea")
|
|
1498
|
+
except Exception as e:
|
|
1499
|
+
logging.warning(f"Could not calculate Haumea position: {e}")
|
|
1500
|
+
active_points.remove("Haumea") # Remove if not calculated
|
|
1501
|
+
|
|
1502
|
+
# Calculate Makemake
|
|
1503
|
+
if should_calculate("Makemake"):
|
|
1504
|
+
try:
|
|
1505
|
+
makemake_calc = swe.calc_ut(julian_day, swe.AST_OFFSET + 136472, iflag)[0]
|
|
1506
|
+
data["makemake"] = get_kerykeion_point_from_degree(makemake_calc[0], "Makemake", point_type=point_type, speed=makemake_calc[3], declination=makemake_calc[1])
|
|
1507
|
+
data["makemake"].house = get_planet_house(makemake_calc[0], houses_degree_ut)
|
|
1508
|
+
data["makemake"].retrograde = makemake_calc[3] < 0
|
|
1509
|
+
calculated_planets.append("Makemake")
|
|
1510
|
+
except Exception as e:
|
|
1511
|
+
logging.warning(f"Could not calculate Makemake position: {e}")
|
|
1512
|
+
active_points.remove("Makemake") # Remove if not calculated
|
|
1513
|
+
|
|
1514
|
+
# Calculate Ixion
|
|
1515
|
+
if should_calculate("Ixion"):
|
|
1516
|
+
try:
|
|
1517
|
+
ixion_calc = swe.calc_ut(julian_day, swe.AST_OFFSET + 28978, iflag)[0]
|
|
1518
|
+
data["ixion"] = get_kerykeion_point_from_degree(ixion_calc[0], "Ixion", point_type=point_type, speed=ixion_calc[3], declination=ixion_calc[1])
|
|
1519
|
+
data["ixion"].house = get_planet_house(ixion_calc[0], houses_degree_ut)
|
|
1520
|
+
data["ixion"].retrograde = ixion_calc[3] < 0
|
|
1521
|
+
calculated_planets.append("Ixion")
|
|
1522
|
+
except Exception as e:
|
|
1523
|
+
logging.warning(f"Could not calculate Ixion position: {e}")
|
|
1524
|
+
active_points.remove("Ixion") # Remove if not calculated
|
|
1525
|
+
|
|
1526
|
+
# Calculate Orcus
|
|
1527
|
+
if should_calculate("Orcus"):
|
|
1528
|
+
try:
|
|
1529
|
+
orcus_calc = swe.calc_ut(julian_day, swe.AST_OFFSET + 90482, iflag)[0]
|
|
1530
|
+
data["orcus"] = get_kerykeion_point_from_degree(orcus_calc[0], "Orcus", point_type=point_type, speed=orcus_calc[3], declination=orcus_calc[1])
|
|
1531
|
+
data["orcus"].house = get_planet_house(orcus_calc[0], houses_degree_ut)
|
|
1532
|
+
data["orcus"].retrograde = orcus_calc[3] < 0
|
|
1533
|
+
calculated_planets.append("Orcus")
|
|
1534
|
+
except Exception as e:
|
|
1535
|
+
logging.warning(f"Could not calculate Orcus position: {e}")
|
|
1536
|
+
active_points.remove("Orcus") # Remove if not calculated
|
|
1537
|
+
|
|
1538
|
+
# Calculate Quaoar
|
|
1539
|
+
if should_calculate("Quaoar"):
|
|
1540
|
+
try:
|
|
1541
|
+
quaoar_calc = swe.calc_ut(julian_day, swe.AST_OFFSET + 50000, iflag)[0]
|
|
1542
|
+
data["quaoar"] = get_kerykeion_point_from_degree(quaoar_calc[0], "Quaoar", point_type=point_type, speed=quaoar_calc[3], declination=quaoar_calc[1])
|
|
1543
|
+
data["quaoar"].house = get_planet_house(quaoar_calc[0], houses_degree_ut)
|
|
1544
|
+
data["quaoar"].retrograde = quaoar_calc[3] < 0
|
|
1545
|
+
calculated_planets.append("Quaoar")
|
|
1546
|
+
except Exception as e:
|
|
1547
|
+
logging.warning(f"Could not calculate Quaoar position: {e}")
|
|
1548
|
+
active_points.remove("Quaoar") # Remove if not calculated
|
|
1549
|
+
|
|
1550
|
+
# ==================
|
|
1551
|
+
# FIXED STARS
|
|
1552
|
+
# ==================
|
|
1553
|
+
|
|
1554
|
+
# Calculate Regulus (example fixed star)
|
|
1555
|
+
if should_calculate("Regulus"):
|
|
1556
|
+
try:
|
|
1557
|
+
star_name = "Regulus"
|
|
1558
|
+
pos = swe.fixstar_ut(star_name, julian_day, iflag)[0]
|
|
1559
|
+
regulus_deg = pos[0]
|
|
1560
|
+
regulus_speed = pos[3] if len(pos) > 3 else 0.0 # Fixed stars have very slow speed
|
|
1561
|
+
regulus_dec = pos[1] if len(pos) > 1 else None # Declination
|
|
1562
|
+
data["regulus"] = get_kerykeion_point_from_degree(regulus_deg, "Regulus", point_type=point_type, speed=regulus_speed, declination=regulus_dec)
|
|
1563
|
+
data["regulus"].house = get_planet_house(regulus_deg, houses_degree_ut)
|
|
1564
|
+
data["regulus"].retrograde = False # Fixed stars are never retrograde
|
|
1565
|
+
calculated_planets.append("Regulus")
|
|
1566
|
+
except Exception as e:
|
|
1567
|
+
logging.warning(f"Could not calculate Regulus position: {e}")
|
|
1568
|
+
active_points.remove("Regulus") # Remove if not calculated
|
|
1569
|
+
|
|
1570
|
+
# Calculate Spica (example fixed star)
|
|
1571
|
+
if should_calculate("Spica"):
|
|
1572
|
+
try:
|
|
1573
|
+
star_name = "Spica"
|
|
1574
|
+
pos = swe.fixstar_ut(star_name, julian_day, iflag)[0]
|
|
1575
|
+
spica_deg = pos[0]
|
|
1576
|
+
spica_speed = pos[3] if len(pos) > 3 else 0.0 # Fixed stars have very slow speed
|
|
1577
|
+
spica_dec = pos[1] if len(pos) > 1 else None # Declination
|
|
1578
|
+
data["spica"] = get_kerykeion_point_from_degree(spica_deg, "Spica", point_type=point_type, speed=spica_speed, declination=spica_dec)
|
|
1579
|
+
data["spica"].house = get_planet_house(spica_deg, houses_degree_ut)
|
|
1580
|
+
data["spica"].retrograde = False # Fixed stars are never retrograde
|
|
1581
|
+
calculated_planets.append("Spica")
|
|
1582
|
+
except Exception as e:
|
|
1583
|
+
logging.warning(f"Could not calculate Spica position: {e}")
|
|
1584
|
+
active_points.remove("Spica") # Remove if not calculated
|
|
1585
|
+
|
|
1586
|
+
# ==================
|
|
1587
|
+
# ARABIC PARTS / LOTS
|
|
1588
|
+
# ==================
|
|
1589
|
+
|
|
1590
|
+
# Calculate Pars Fortunae (Part of Fortune)
|
|
1591
|
+
if should_calculate("Pars_Fortunae"):
|
|
1592
|
+
# Auto-activate required points with notification
|
|
1593
|
+
pars_fortunae_required_points: List[AstrologicalPoint] = ["Ascendant", "Sun", "Moon"]
|
|
1594
|
+
missing_points = [point for point in pars_fortunae_required_points if point not in active_points]
|
|
1595
|
+
if missing_points:
|
|
1596
|
+
logging.info(f"Automatically adding required points for Pars_Fortunae: {missing_points}")
|
|
1597
|
+
active_points.extend(cast(List[AstrologicalPoint], missing_points))
|
|
1598
|
+
# Recalculate the missing points
|
|
1599
|
+
for point in missing_points:
|
|
1600
|
+
if point == "Ascendant" and "ascendant" not in data:
|
|
1601
|
+
# Calculate Ascendant from houses data if needed
|
|
1602
|
+
if data["zodiac_type"] == "Sidereal":
|
|
1603
|
+
cusps, ascmc = swe.houses_ex(
|
|
1604
|
+
tjdut=data["julian_day"],
|
|
1605
|
+
lat=data["lat"],
|
|
1606
|
+
lon=data["lng"],
|
|
1607
|
+
hsys=str.encode(data["houses_system_identifier"]),
|
|
1608
|
+
flags=swe.FLG_SIDEREAL
|
|
1609
|
+
)
|
|
1610
|
+
else:
|
|
1611
|
+
cusps, ascmc = swe.houses(
|
|
1612
|
+
tjdut=data["julian_day"],
|
|
1613
|
+
lat=data["lat"],
|
|
1614
|
+
lon=data["lng"],
|
|
1615
|
+
hsys=str.encode(data["houses_system_identifier"])
|
|
1616
|
+
)
|
|
1617
|
+
data["ascendant"] = get_kerykeion_point_from_degree(ascmc[0], "Ascendant", point_type=point_type)
|
|
1618
|
+
data["ascendant"].house = get_planet_house(ascmc[0], houses_degree_ut)
|
|
1619
|
+
data["ascendant"].retrograde = False
|
|
1620
|
+
elif point == "Sun" and "sun" not in data:
|
|
1621
|
+
sun_calc = swe.calc_ut(julian_day, 0, iflag)[0]
|
|
1622
|
+
data["sun"] = get_kerykeion_point_from_degree(sun_calc[0], "Sun", point_type=point_type, speed=sun_calc[3], declination=sun_calc[1])
|
|
1623
|
+
data["sun"].house = get_planet_house(sun_calc[0], houses_degree_ut)
|
|
1624
|
+
data["sun"].retrograde = sun_calc[3] < 0
|
|
1625
|
+
elif point == "Moon" and "moon" not in data:
|
|
1626
|
+
moon_calc = swe.calc_ut(julian_day, 1, iflag)[0]
|
|
1627
|
+
data["moon"] = get_kerykeion_point_from_degree(moon_calc[0], "Moon", point_type=point_type, speed=moon_calc[3], declination=moon_calc[1])
|
|
1628
|
+
data["moon"].house = get_planet_house(moon_calc[0], houses_degree_ut)
|
|
1629
|
+
data["moon"].retrograde = moon_calc[3] < 0
|
|
1630
|
+
|
|
1631
|
+
# Check if required points are available
|
|
1632
|
+
if all(k in data for k in ["ascendant", "sun", "moon"]):
|
|
1633
|
+
# Different calculation for day and night charts
|
|
1634
|
+
# Day birth (Sun above horizon): ASC + Moon - Sun
|
|
1635
|
+
# Night birth (Sun below horizon): ASC + Sun - Moon
|
|
1636
|
+
if data["sun"].house:
|
|
1637
|
+
is_day_chart = get_house_number(data["sun"].house) < 7 # Houses 1-6 are above horizon
|
|
1638
|
+
else:
|
|
1639
|
+
is_day_chart = True # Default to day chart if house is None
|
|
1640
|
+
|
|
1641
|
+
if is_day_chart:
|
|
1642
|
+
fortune_deg = math.fmod(data["ascendant"].abs_pos + data["moon"].abs_pos - data["sun"].abs_pos, 360)
|
|
1643
|
+
else:
|
|
1644
|
+
fortune_deg = math.fmod(data["ascendant"].abs_pos + data["sun"].abs_pos - data["moon"].abs_pos, 360)
|
|
1645
|
+
|
|
1646
|
+
data["pars_fortunae"] = get_kerykeion_point_from_degree(fortune_deg, "Pars_Fortunae", point_type=point_type)
|
|
1647
|
+
data["pars_fortunae"].house = get_planet_house(fortune_deg, houses_degree_ut)
|
|
1648
|
+
data["pars_fortunae"].retrograde = False # Parts are never retrograde
|
|
1649
|
+
calculated_planets.append("Pars_Fortunae")
|
|
1650
|
+
|
|
1651
|
+
# Calculate Pars Spiritus (Part of Spirit)
|
|
1652
|
+
if should_calculate("Pars_Spiritus"):
|
|
1653
|
+
# Auto-activate required points with notification
|
|
1654
|
+
pars_spiritus_required_points: List[AstrologicalPoint] = ["Ascendant", "Sun", "Moon"]
|
|
1655
|
+
missing_points = [point for point in pars_spiritus_required_points if point not in active_points]
|
|
1656
|
+
if missing_points:
|
|
1657
|
+
logging.info(f"Automatically adding required points for Pars_Spiritus: {missing_points}")
|
|
1658
|
+
active_points.extend(cast(List[AstrologicalPoint], missing_points))
|
|
1659
|
+
# Recalculate the missing points
|
|
1660
|
+
for point in missing_points:
|
|
1661
|
+
if point == "Ascendant" and "ascendant" not in data:
|
|
1662
|
+
# Calculate Ascendant from houses data if needed
|
|
1663
|
+
if data["zodiac_type"] == "Sidereal":
|
|
1664
|
+
cusps, ascmc = swe.houses_ex(
|
|
1665
|
+
tjdut=data["julian_day"],
|
|
1666
|
+
lat=data["lat"],
|
|
1667
|
+
lon=data["lng"],
|
|
1668
|
+
hsys=str.encode(data["houses_system_identifier"]),
|
|
1669
|
+
flags=swe.FLG_SIDEREAL
|
|
1670
|
+
)
|
|
1671
|
+
else:
|
|
1672
|
+
cusps, ascmc = swe.houses(
|
|
1673
|
+
tjdut=data["julian_day"],
|
|
1674
|
+
lat=data["lat"],
|
|
1675
|
+
lon=data["lng"],
|
|
1676
|
+
hsys=str.encode(data["houses_system_identifier"])
|
|
1677
|
+
)
|
|
1678
|
+
data["ascendant"] = get_kerykeion_point_from_degree(ascmc[0], "Ascendant", point_type=point_type)
|
|
1679
|
+
data["ascendant"].house = get_planet_house(ascmc[0], houses_degree_ut)
|
|
1680
|
+
data["ascendant"].retrograde = False
|
|
1681
|
+
elif point == "Sun" and "sun" not in data:
|
|
1682
|
+
sun_calc = swe.calc_ut(julian_day, 0, iflag)[0]
|
|
1683
|
+
data["sun"] = get_kerykeion_point_from_degree(sun_calc[0], "Sun", point_type=point_type, speed=sun_calc[3], declination=sun_calc[1])
|
|
1684
|
+
data["sun"].house = get_planet_house(sun_calc[0], houses_degree_ut)
|
|
1685
|
+
data["sun"].retrograde = sun_calc[3] < 0
|
|
1686
|
+
elif point == "Moon" and "moon" not in data:
|
|
1687
|
+
moon_calc = swe.calc_ut(julian_day, 1, iflag)[0]
|
|
1688
|
+
data["moon"] = get_kerykeion_point_from_degree(moon_calc[0], "Moon", point_type=point_type, speed=moon_calc[3], declination=moon_calc[1])
|
|
1689
|
+
data["moon"].house = get_planet_house(moon_calc[0], houses_degree_ut)
|
|
1690
|
+
data["moon"].retrograde = moon_calc[3] < 0
|
|
1691
|
+
|
|
1692
|
+
# Check if required points are available
|
|
1693
|
+
if all(k in data for k in ["ascendant", "sun", "moon"]):
|
|
1694
|
+
# Day birth: ASC + Sun - Moon
|
|
1695
|
+
# Night birth: ASC + Moon - Sun
|
|
1696
|
+
if data["sun"].house:
|
|
1697
|
+
is_day_chart = get_house_number(data["sun"].house) < 7
|
|
1698
|
+
else:
|
|
1699
|
+
is_day_chart = True # Default to day chart if house is None
|
|
1700
|
+
|
|
1701
|
+
if is_day_chart:
|
|
1702
|
+
spirit_deg = math.fmod(data["ascendant"].abs_pos + data["sun"].abs_pos - data["moon"].abs_pos, 360)
|
|
1703
|
+
else:
|
|
1704
|
+
spirit_deg = math.fmod(data["ascendant"].abs_pos + data["moon"].abs_pos - data["sun"].abs_pos, 360)
|
|
1705
|
+
|
|
1706
|
+
data["pars_spiritus"] = get_kerykeion_point_from_degree(spirit_deg, "Pars_Spiritus", point_type=point_type)
|
|
1707
|
+
data["pars_spiritus"].house = get_planet_house(spirit_deg, houses_degree_ut)
|
|
1708
|
+
data["pars_spiritus"].retrograde = False
|
|
1709
|
+
calculated_planets.append("Pars_Spiritus")
|
|
1710
|
+
|
|
1711
|
+
# Calculate Pars Amoris (Part of Eros/Love)
|
|
1712
|
+
if should_calculate("Pars_Amoris"):
|
|
1713
|
+
# Auto-activate required points with notification
|
|
1714
|
+
pars_amoris_required_points: List[AstrologicalPoint] = ["Ascendant", "Venus", "Sun"]
|
|
1715
|
+
missing_points = [point for point in pars_amoris_required_points if point not in active_points]
|
|
1716
|
+
if missing_points:
|
|
1717
|
+
logging.info(f"Automatically adding required points for Pars_Amoris: {missing_points}")
|
|
1718
|
+
active_points.extend(cast(List[AstrologicalPoint], missing_points))
|
|
1719
|
+
# Recalculate the missing points
|
|
1720
|
+
for point in missing_points:
|
|
1721
|
+
if point == "Ascendant" and "ascendant" not in data:
|
|
1722
|
+
# Calculate Ascendant from houses data if needed
|
|
1723
|
+
if data["zodiac_type"] == "Sidereal":
|
|
1724
|
+
cusps, ascmc = swe.houses_ex(
|
|
1725
|
+
tjdut=data["julian_day"],
|
|
1726
|
+
lat=data["lat"],
|
|
1727
|
+
lon=data["lng"],
|
|
1728
|
+
hsys=str.encode(data["houses_system_identifier"]),
|
|
1729
|
+
flags=swe.FLG_SIDEREAL
|
|
1730
|
+
)
|
|
1731
|
+
else:
|
|
1732
|
+
cusps, ascmc = swe.houses(
|
|
1733
|
+
tjdut=data["julian_day"],
|
|
1734
|
+
lat=data["lat"],
|
|
1735
|
+
lon=data["lng"],
|
|
1736
|
+
hsys=str.encode(data["houses_system_identifier"])
|
|
1737
|
+
)
|
|
1738
|
+
data["ascendant"] = get_kerykeion_point_from_degree(ascmc[0], "Ascendant", point_type=point_type)
|
|
1739
|
+
data["ascendant"].house = get_planet_house(ascmc[0], houses_degree_ut)
|
|
1740
|
+
data["ascendant"].retrograde = False
|
|
1741
|
+
elif point == "Sun" and "sun" not in data:
|
|
1742
|
+
sun_calc = swe.calc_ut(julian_day, 0, iflag)[0]
|
|
1743
|
+
data["sun"] = get_kerykeion_point_from_degree(sun_calc[0], "Sun", point_type=point_type, speed=sun_calc[3], declination=sun_calc[1])
|
|
1744
|
+
data["sun"].house = get_planet_house(sun_calc[0], houses_degree_ut)
|
|
1745
|
+
data["sun"].retrograde = sun_calc[3] < 0
|
|
1746
|
+
elif point == "Venus" and "venus" not in data:
|
|
1747
|
+
venus_calc = swe.calc_ut(julian_day, 3, iflag)[0]
|
|
1748
|
+
data["venus"] = get_kerykeion_point_from_degree(venus_calc[0], "Venus", point_type=point_type, speed=venus_calc[3], declination=venus_calc[1])
|
|
1749
|
+
data["venus"].house = get_planet_house(venus_calc[0], houses_degree_ut)
|
|
1750
|
+
data["venus"].retrograde = venus_calc[3] < 0
|
|
1751
|
+
|
|
1752
|
+
# Check if required points are available
|
|
1753
|
+
if all(k in data for k in ["ascendant", "venus", "sun"]):
|
|
1754
|
+
# ASC + Venus - Sun
|
|
1755
|
+
amoris_deg = math.fmod(data["ascendant"].abs_pos + data["venus"].abs_pos - data["sun"].abs_pos, 360)
|
|
1756
|
+
|
|
1757
|
+
data["pars_amoris"] = get_kerykeion_point_from_degree(amoris_deg, "Pars_Amoris", point_type=point_type)
|
|
1758
|
+
data["pars_amoris"].house = get_planet_house(amoris_deg, houses_degree_ut)
|
|
1759
|
+
data["pars_amoris"].retrograde = False
|
|
1760
|
+
calculated_planets.append("Pars_Amoris")
|
|
1761
|
+
|
|
1762
|
+
# Calculate Pars Fidei (Part of Faith)
|
|
1763
|
+
if should_calculate("Pars_Fidei"):
|
|
1764
|
+
# Auto-activate required points with notification
|
|
1765
|
+
pars_fidei_required_points: List[AstrologicalPoint] = ["Ascendant", "Jupiter", "Saturn"]
|
|
1766
|
+
missing_points = [point for point in pars_fidei_required_points if point not in active_points]
|
|
1767
|
+
if missing_points:
|
|
1768
|
+
logging.info(f"Automatically adding required points for Pars_Fidei: {missing_points}")
|
|
1769
|
+
active_points.extend(cast(List[AstrologicalPoint], missing_points))
|
|
1770
|
+
# Recalculate the missing points
|
|
1771
|
+
for point in missing_points:
|
|
1772
|
+
if point == "Ascendant" and "ascendant" not in data:
|
|
1773
|
+
# Calculate Ascendant from houses data if needed
|
|
1774
|
+
if data["zodiac_type"] == "Sidereal":
|
|
1775
|
+
cusps, ascmc = swe.houses_ex(
|
|
1776
|
+
tjdut=data["julian_day"],
|
|
1777
|
+
lat=data["lat"],
|
|
1778
|
+
lon=data["lng"],
|
|
1779
|
+
hsys=str.encode(data["houses_system_identifier"]),
|
|
1780
|
+
flags=swe.FLG_SIDEREAL
|
|
1781
|
+
)
|
|
1782
|
+
else:
|
|
1783
|
+
cusps, ascmc = swe.houses(
|
|
1784
|
+
tjdut=data["julian_day"],
|
|
1785
|
+
lat=data["lat"],
|
|
1786
|
+
lon=data["lng"],
|
|
1787
|
+
hsys=str.encode(data["houses_system_identifier"])
|
|
1788
|
+
)
|
|
1789
|
+
data["ascendant"] = get_kerykeion_point_from_degree(ascmc[0], "Ascendant", point_type=point_type)
|
|
1790
|
+
data["ascendant"].house = get_planet_house(ascmc[0], houses_degree_ut)
|
|
1791
|
+
data["ascendant"].retrograde = False
|
|
1792
|
+
elif point == "Jupiter" and "jupiter" not in data:
|
|
1793
|
+
jupiter_calc = swe.calc_ut(julian_day, 5, iflag)[0]
|
|
1794
|
+
data["jupiter"] = get_kerykeion_point_from_degree(jupiter_calc[0], "Jupiter", point_type=point_type, speed=jupiter_calc[3], declination=jupiter_calc[1])
|
|
1795
|
+
data["jupiter"].house = get_planet_house(jupiter_calc[0], houses_degree_ut)
|
|
1796
|
+
data["jupiter"].retrograde = jupiter_calc[3] < 0
|
|
1797
|
+
elif point == "Saturn" and "saturn" not in data:
|
|
1798
|
+
saturn_calc = swe.calc_ut(julian_day, 6, iflag)[0]
|
|
1799
|
+
data["saturn"] = get_kerykeion_point_from_degree(saturn_calc[0], "Saturn", point_type=point_type, speed=saturn_calc[3], declination=saturn_calc[1])
|
|
1800
|
+
data["saturn"].house = get_planet_house(saturn_calc[0], houses_degree_ut)
|
|
1801
|
+
data["saturn"].retrograde = saturn_calc[3] < 0
|
|
1802
|
+
|
|
1803
|
+
# Check if required points are available
|
|
1804
|
+
if all(k in data for k in ["ascendant", "jupiter", "saturn"]):
|
|
1805
|
+
# ASC + Jupiter - Saturn
|
|
1806
|
+
fidei_deg = math.fmod(data["ascendant"].abs_pos + data["jupiter"].abs_pos - data["saturn"].abs_pos, 360)
|
|
1807
|
+
|
|
1808
|
+
data["pars_fidei"] = get_kerykeion_point_from_degree(fidei_deg, "Pars_Fidei", point_type=point_type)
|
|
1809
|
+
data["pars_fidei"].house = get_planet_house(fidei_deg, houses_degree_ut)
|
|
1810
|
+
data["pars_fidei"].retrograde = False
|
|
1811
|
+
calculated_planets.append("Pars_Fidei")
|
|
1812
|
+
|
|
1813
|
+
# Calculate Vertex and/or Anti-Vertex
|
|
1814
|
+
if should_calculate("Vertex") or should_calculate("Anti_Vertex"):
|
|
1815
|
+
try:
|
|
1816
|
+
# Vertex is at ascmc[3] in Swiss Ephemeris
|
|
1817
|
+
if data["zodiac_type"] == "Sidereal":
|
|
1818
|
+
_, ascmc = swe.houses_ex(
|
|
1819
|
+
tjdut=data["julian_day"],
|
|
1820
|
+
lat=data["lat"],
|
|
1821
|
+
lon=data["lng"],
|
|
1822
|
+
hsys=str.encode("V"), # Vertex works best with Vehlow system
|
|
1823
|
+
flags=swe.FLG_SIDEREAL
|
|
1824
|
+
)
|
|
1825
|
+
else:
|
|
1826
|
+
_, ascmc = swe.houses(
|
|
1827
|
+
tjdut=data["julian_day"],
|
|
1828
|
+
lat=data["lat"],
|
|
1829
|
+
lon=data["lng"],
|
|
1830
|
+
hsys=str.encode("V")
|
|
1831
|
+
)
|
|
1832
|
+
|
|
1833
|
+
vertex_deg = ascmc[3]
|
|
1834
|
+
|
|
1835
|
+
# Calculate Vertex if requested
|
|
1836
|
+
if should_calculate("Vertex"):
|
|
1837
|
+
data["vertex"] = get_kerykeion_point_from_degree(vertex_deg, "Vertex", point_type=point_type)
|
|
1838
|
+
data["vertex"].house = get_planet_house(vertex_deg, houses_degree_ut)
|
|
1839
|
+
data["vertex"].retrograde = False
|
|
1840
|
+
calculated_planets.append("Vertex")
|
|
1841
|
+
|
|
1842
|
+
# Calculate Anti-Vertex if requested
|
|
1843
|
+
if should_calculate("Anti_Vertex"):
|
|
1844
|
+
anti_vertex_deg = math.fmod(vertex_deg + 180, 360)
|
|
1845
|
+
data["anti_vertex"] = get_kerykeion_point_from_degree(anti_vertex_deg, "Anti_Vertex", point_type=point_type)
|
|
1846
|
+
data["anti_vertex"].house = get_planet_house(anti_vertex_deg, houses_degree_ut)
|
|
1847
|
+
data["anti_vertex"].retrograde = False
|
|
1848
|
+
calculated_planets.append("Anti_Vertex")
|
|
1849
|
+
|
|
1850
|
+
except Exception as e:
|
|
1851
|
+
logging.warning("Could not calculate Vertex/Anti-Vertex position, error: %s", e)
|
|
1852
|
+
if "Vertex" in active_points:
|
|
1853
|
+
active_points.remove("Vertex")
|
|
1854
|
+
if "Anti_Vertex" in active_points:
|
|
1855
|
+
active_points.remove("Anti_Vertex")
|
|
1856
|
+
|
|
1857
|
+
# Store only the planets that were actually calculated
|
|
1858
|
+
all_calculated_points = calculated_planets.copy()
|
|
1859
|
+
if calculated_axial_cusps:
|
|
1860
|
+
all_calculated_points.extend(calculated_axial_cusps)
|
|
1861
|
+
data["active_points"] = all_calculated_points
|
|
1862
|
+
|
|
1863
|
+
@staticmethod
|
|
1864
|
+
def _calculate_day_of_week(data: Dict[str, Any]) -> None:
|
|
1865
|
+
"""
|
|
1866
|
+
Calculate the day of the week for the given astronomical event.
|
|
1867
|
+
|
|
1868
|
+
Determines the day of the week corresponding to the local datetime
|
|
1869
|
+
using the standard library for consistency.
|
|
1870
|
+
|
|
1871
|
+
Args:
|
|
1872
|
+
data (Dict[str, Any]): Calculation data dictionary containing
|
|
1873
|
+
iso_formatted_local_datetime. Updated with the calculated day_of_week string.
|
|
1874
|
+
|
|
1875
|
+
Side Effects:
|
|
1876
|
+
Updates data dictionary with:
|
|
1877
|
+
- day_of_week: Human-readable day name (e.g., "Monday", "Tuesday")
|
|
1878
|
+
"""
|
|
1879
|
+
dt = datetime.fromisoformat(data["iso_formatted_local_datetime"])
|
|
1880
|
+
data["day_of_week"] = dt.strftime("%A")
|
|
1881
|
+
|
|
1882
|
+
if __name__ == "__main__":
|
|
1883
|
+
from kerykeion.schemas.kr_literals import AstrologicalPoint
|
|
1884
|
+
|
|
1885
|
+
# Example usage
|
|
1886
|
+
new_active_points: List[AstrologicalPoint] = ['Sun', 'Moon', 'Mercury', 'Venus', 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune', 'Pluto', 'Mean_North_Lunar_Node', 'True_North_Lunar_Node', 'Mean_South_Lunar_Node', 'True_South_Lunar_Node', 'Chiron', 'Mean_Lilith', 'True_Lilith', 'Earth', 'Pholus', 'Ceres', 'Pallas', 'Juno', 'Vesta', 'Eris', 'Sedna', 'Haumea', 'Makemake', 'Ixion', 'Orcus', 'Quaoar', 'Regulus', 'Spica', 'Pars_Fortunae', 'Pars_Spiritus', 'Pars_Amoris', 'Pars_Fidei', 'Vertex', 'Anti_Vertex', 'Ascendant', 'Medium_Coeli', 'Descendant', 'Imum_Coeli']
|
|
1887
|
+
subject = AstrologicalSubjectFactory.from_current_time(name="Test Subject", active_points=new_active_points)
|
|
1888
|
+
print(subject.sun)
|
|
1889
|
+
print(subject.pars_amoris)
|
|
1890
|
+
print(subject.eris)
|
|
1891
|
+
print(subject.active_points)
|
|
1892
|
+
print(subject.pars_fidei)
|
|
1893
|
+
print("----")
|
|
1894
|
+
print(subject.anti_vertex)
|
|
1895
|
+
|
|
1896
|
+
# Create JSON output
|
|
1897
|
+
json_string = subject.model_dump_json(exclude_none=True, indent=2)
|
|
1898
|
+
|
|
1899
|
+
# Write JSON to home
|
|
1900
|
+
with open(Path.home() / "kerykeion_subject_example.json", "w", encoding="utf-8") as f:
|
|
1901
|
+
f.write(json_string)
|