kerykeion 4.26.2__py3-none-any.whl → 5.0.0a2__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 +9 -7
- kerykeion/aspects/aspects_utils.py +14 -8
- kerykeion/aspects/natal_aspects.py +26 -17
- kerykeion/aspects/synastry_aspects.py +32 -15
- kerykeion/aspects/transits_time_range.py +2 -2
- kerykeion/astrological_subject_factory.py +1132 -0
- kerykeion/charts/charts_utils.py +676 -146
- kerykeion/charts/draw_planets.py +9 -8
- kerykeion/charts/draw_planets_v2.py +639 -0
- kerykeion/charts/kerykeion_chart_svg.py +1334 -601
- kerykeion/charts/templates/chart.xml +184 -78
- kerykeion/charts/templates/wheel_only.xml +13 -12
- kerykeion/charts/themes/classic.css +91 -76
- kerykeion/charts/themes/dark-high-contrast.css +129 -107
- kerykeion/charts/themes/dark.css +130 -107
- kerykeion/charts/themes/light.css +130 -103
- kerykeion/charts/themes/strawberry.css +143 -0
- kerykeion/composite_subject_factory.py +26 -43
- kerykeion/ephemeris_data.py +6 -10
- kerykeion/house_comparison/__init__.py +3 -0
- kerykeion/house_comparison/house_comparison_factory.py +70 -0
- kerykeion/house_comparison/house_comparison_models.py +38 -0
- kerykeion/house_comparison/house_comparison_utils.py +98 -0
- kerykeion/kr_types/chart_types.py +13 -5
- kerykeion/kr_types/kr_literals.py +34 -6
- kerykeion/kr_types/kr_models.py +122 -160
- kerykeion/kr_types/settings_models.py +107 -143
- kerykeion/planetary_return_factory.py +299 -0
- kerykeion/{relationship_score/relationship_score_factory.py → relationship_score_factory.py} +10 -13
- kerykeion/report.py +4 -4
- kerykeion/settings/config_constants.py +35 -6
- kerykeion/settings/kerykeion_settings.py +1 -0
- kerykeion/settings/kr.config.json +1301 -1255
- kerykeion/settings/legacy/__init__.py +0 -0
- kerykeion/settings/legacy/legacy_celestial_points_settings.py +299 -0
- kerykeion/settings/legacy/legacy_chart_aspects_settings.py +71 -0
- kerykeion/settings/legacy/legacy_color_settings.py +42 -0
- kerykeion/transits_time_range.py +13 -9
- kerykeion/utilities.py +228 -31
- {kerykeion-4.26.2.dist-info → kerykeion-5.0.0a2.dist-info}/METADATA +119 -107
- kerykeion-5.0.0a2.dist-info/RECORD +54 -0
- {kerykeion-4.26.2.dist-info → kerykeion-5.0.0a2.dist-info}/WHEEL +1 -1
- kerykeion/astrological_subject.py +0 -841
- kerykeion/relationship_score/__init__.py +0 -2
- kerykeion/relationship_score/relationship_score.py +0 -175
- kerykeion-4.26.2.dist-info/LICENSE +0 -661
- kerykeion-4.26.2.dist-info/RECORD +0 -46
- /LICENSE → /kerykeion-5.0.0a2.dist-info/LICENSE +0 -0
- {kerykeion-4.26.2.dist-info → kerykeion-5.0.0a2.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,1132 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
This is part of Kerykeion (C) 2025 Giacomo Battaglia
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import pytz
|
|
7
|
+
import swisseph as swe
|
|
8
|
+
import logging
|
|
9
|
+
import warnings
|
|
10
|
+
import math
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Union, Optional, List, Dict, Any, Literal, get_args, cast, TypedDict, Set
|
|
14
|
+
from functools import cached_property, lru_cache
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Callable
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
from kerykeion.fetch_geonames import FetchGeonames
|
|
20
|
+
from kerykeion.kr_types import (
|
|
21
|
+
KerykeionException,
|
|
22
|
+
ZodiacType,
|
|
23
|
+
AstrologicalSubjectModel,
|
|
24
|
+
LunarPhaseModel,
|
|
25
|
+
KerykeionPointModel,
|
|
26
|
+
PointType,
|
|
27
|
+
SiderealMode,
|
|
28
|
+
HousesSystemIdentifier,
|
|
29
|
+
PerspectiveType,
|
|
30
|
+
AstrologicalPoint,
|
|
31
|
+
Houses,
|
|
32
|
+
)
|
|
33
|
+
from kerykeion.utilities import (
|
|
34
|
+
get_number_from_name,
|
|
35
|
+
get_kerykeion_point_from_degree,
|
|
36
|
+
get_planet_house,
|
|
37
|
+
check_and_adjust_polar_latitude,
|
|
38
|
+
calculate_moon_phase,
|
|
39
|
+
datetime_to_julian,
|
|
40
|
+
get_house_number
|
|
41
|
+
)
|
|
42
|
+
from kerykeion.settings.config_constants import DEFAULT_ACTIVE_POINTS
|
|
43
|
+
|
|
44
|
+
# Default configuration values
|
|
45
|
+
DEFAULT_GEONAMES_USERNAME = "century.boy"
|
|
46
|
+
DEFAULT_SIDEREAL_MODE: SiderealMode = "FAGAN_BRADLEY"
|
|
47
|
+
DEFAULT_HOUSES_SYSTEM_IDENTIFIER: HousesSystemIdentifier = "P"
|
|
48
|
+
DEFAULT_ZODIAC_TYPE: ZodiacType = "Tropic"
|
|
49
|
+
DEFAULT_PERSPECTIVE_TYPE: PerspectiveType = "Apparent Geocentric"
|
|
50
|
+
DEFAULT_GEONAMES_CACHE_EXPIRE_AFTER_DAYS = 30
|
|
51
|
+
|
|
52
|
+
# Warning messages
|
|
53
|
+
GEONAMES_DEFAULT_USERNAME_WARNING = (
|
|
54
|
+
"\n********\n"
|
|
55
|
+
"NO GEONAMES USERNAME SET!\n"
|
|
56
|
+
"Using the default geonames username is not recommended, please set a custom one!\n"
|
|
57
|
+
"You can get one for free here:\n"
|
|
58
|
+
"https://www.geonames.org/login\n"
|
|
59
|
+
"Keep in mind that the default username is limited to 2000 requests per hour and is shared with everyone else using this library.\n"
|
|
60
|
+
"********"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
NOW = datetime.now()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class ChartConfiguration:
|
|
68
|
+
"""Configuration settings for astrological chart calculations"""
|
|
69
|
+
zodiac_type: ZodiacType = DEFAULT_ZODIAC_TYPE
|
|
70
|
+
sidereal_mode: Optional[SiderealMode] = None
|
|
71
|
+
houses_system_identifier: HousesSystemIdentifier = DEFAULT_HOUSES_SYSTEM_IDENTIFIER
|
|
72
|
+
perspective_type: PerspectiveType = DEFAULT_PERSPECTIVE_TYPE
|
|
73
|
+
|
|
74
|
+
def validate(self) -> None:
|
|
75
|
+
"""Validate configuration settings"""
|
|
76
|
+
# Validate zodiac type
|
|
77
|
+
if self.zodiac_type not in get_args(ZodiacType):
|
|
78
|
+
raise KerykeionException(
|
|
79
|
+
f"'{self.zodiac_type}' is not a valid zodiac type! Available types are: {get_args(ZodiacType)}"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Validate sidereal mode settings
|
|
83
|
+
if self.sidereal_mode and self.zodiac_type == "Tropic":
|
|
84
|
+
raise KerykeionException("You can't set a sidereal mode with a Tropic zodiac type!")
|
|
85
|
+
|
|
86
|
+
if self.zodiac_type == "Sidereal":
|
|
87
|
+
if not self.sidereal_mode:
|
|
88
|
+
self.sidereal_mode = DEFAULT_SIDEREAL_MODE
|
|
89
|
+
logging.info("No sidereal mode set, using default FAGAN_BRADLEY")
|
|
90
|
+
elif self.sidereal_mode not in get_args(SiderealMode):
|
|
91
|
+
raise KerykeionException(
|
|
92
|
+
f"'{self.sidereal_mode}' is not a valid sidereal mode! Available modes are: {get_args(SiderealMode)}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Validate houses system
|
|
96
|
+
if self.houses_system_identifier not in get_args(HousesSystemIdentifier):
|
|
97
|
+
raise KerykeionException(
|
|
98
|
+
f"'{self.houses_system_identifier}' is not a valid house system! Available systems are: {get_args(HousesSystemIdentifier)}"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Validate perspective type
|
|
102
|
+
if self.perspective_type not in get_args(PerspectiveType):
|
|
103
|
+
raise KerykeionException(
|
|
104
|
+
f"'{self.perspective_type}' is not a valid chart perspective! Available perspectives are: {get_args(PerspectiveType)}"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass
|
|
109
|
+
class LocationData:
|
|
110
|
+
"""Information about a geographical location"""
|
|
111
|
+
city: str = "Greenwich"
|
|
112
|
+
nation: str = "GB"
|
|
113
|
+
lat: float = 51.5074
|
|
114
|
+
lng: float = 0.0
|
|
115
|
+
tz_str: str = "Etc/GMT"
|
|
116
|
+
altitude: Optional[float] = None
|
|
117
|
+
|
|
118
|
+
# Storage for city data fetched from geonames
|
|
119
|
+
city_data: Dict[str, str] = field(default_factory=dict)
|
|
120
|
+
|
|
121
|
+
def fetch_from_geonames(self, username: str, cache_expire_after_days: int) -> None:
|
|
122
|
+
"""Fetch location data from geonames API"""
|
|
123
|
+
logging.info(f"Fetching timezone/coordinates for {self.city}, {self.nation} from geonames")
|
|
124
|
+
|
|
125
|
+
geonames = FetchGeonames(
|
|
126
|
+
self.city,
|
|
127
|
+
self.nation,
|
|
128
|
+
username=username,
|
|
129
|
+
cache_expire_after_days=cache_expire_after_days
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
self.city_data = geonames.get_serialized_data()
|
|
133
|
+
|
|
134
|
+
# Validate data
|
|
135
|
+
required_fields = ["countryCode", "timezonestr", "lat", "lng"]
|
|
136
|
+
missing_fields = [field for field in required_fields if field not in self.city_data]
|
|
137
|
+
|
|
138
|
+
if missing_fields:
|
|
139
|
+
raise KerykeionException(
|
|
140
|
+
f"Missing data from geonames: {', '.join(missing_fields)}. "
|
|
141
|
+
"Check your connection or try a different location."
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Update location data
|
|
145
|
+
self.nation = self.city_data["countryCode"]
|
|
146
|
+
self.lng = float(self.city_data["lng"])
|
|
147
|
+
self.lat = float(self.city_data["lat"])
|
|
148
|
+
self.tz_str = self.city_data["timezonestr"]
|
|
149
|
+
|
|
150
|
+
def prepare_for_calculation(self) -> None:
|
|
151
|
+
"""Prepare location data for astrological calculations"""
|
|
152
|
+
# Adjust latitude for polar regions
|
|
153
|
+
self.lat = check_and_adjust_polar_latitude(self.lat)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class AstrologicalSubjectFactory:
|
|
157
|
+
"""
|
|
158
|
+
Factory class for creating astrological subjects with planetary positions,
|
|
159
|
+
houses, and other astrological information for a specific time and location.
|
|
160
|
+
|
|
161
|
+
This factory creates and returns AstrologicalSubjectModel instances and provides
|
|
162
|
+
multiple creation methods for different initialization scenarios.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
@classmethod
|
|
166
|
+
def from_birth_data(
|
|
167
|
+
cls,
|
|
168
|
+
name: str = "Now",
|
|
169
|
+
year: int = NOW.year,
|
|
170
|
+
month: int = NOW.month,
|
|
171
|
+
day: int = NOW.day,
|
|
172
|
+
hour: int = NOW.hour,
|
|
173
|
+
minute: int = NOW.minute,
|
|
174
|
+
city: Optional[str] = None,
|
|
175
|
+
nation: Optional[str] = None,
|
|
176
|
+
lng: Optional[float] = None,
|
|
177
|
+
lat: Optional[float] = None,
|
|
178
|
+
tz_str: Optional[str] = None,
|
|
179
|
+
geonames_username: Optional[str] = None,
|
|
180
|
+
online: bool = True,
|
|
181
|
+
zodiac_type: ZodiacType = DEFAULT_ZODIAC_TYPE,
|
|
182
|
+
sidereal_mode: Optional[SiderealMode] = None,
|
|
183
|
+
houses_system_identifier: HousesSystemIdentifier = DEFAULT_HOUSES_SYSTEM_IDENTIFIER,
|
|
184
|
+
perspective_type: PerspectiveType = DEFAULT_PERSPECTIVE_TYPE,
|
|
185
|
+
cache_expire_after_days: int = DEFAULT_GEONAMES_CACHE_EXPIRE_AFTER_DAYS,
|
|
186
|
+
is_dst: Optional[bool] = None,
|
|
187
|
+
altitude: Optional[float] = None,
|
|
188
|
+
active_points: List[AstrologicalPoint] = DEFAULT_ACTIVE_POINTS,
|
|
189
|
+
*,
|
|
190
|
+
seconds: int = 0,
|
|
191
|
+
|
|
192
|
+
) -> AstrologicalSubjectModel:
|
|
193
|
+
"""
|
|
194
|
+
Create an astrological subject from standard birth/event details.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
name: Subject name
|
|
198
|
+
year, month, day, hour, minute, seconds: Time components
|
|
199
|
+
city: Location name
|
|
200
|
+
nation: Country code
|
|
201
|
+
lng, lat: Coordinates (optional if online=True)
|
|
202
|
+
tz_str: Timezone string (optional if online=True)
|
|
203
|
+
geonames_username: Username for geonames API
|
|
204
|
+
online: Whether to fetch location data online
|
|
205
|
+
zodiac_type: Type of zodiac (Tropical or Sidereal)
|
|
206
|
+
sidereal_mode: Mode for sidereal calculations
|
|
207
|
+
houses_system_identifier: House system for calculations
|
|
208
|
+
perspective_type: Perspective type for calculations
|
|
209
|
+
cache_expire_after_days: Cache duration for geonames data
|
|
210
|
+
is_dst: Daylight saving time flag
|
|
211
|
+
altitude: Location altitude for topocentric calculations
|
|
212
|
+
active_points: Set of points to calculate (optimization)
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
An AstrologicalSubjectModel with calculated data
|
|
216
|
+
"""
|
|
217
|
+
logging.debug("Starting Kerykeion calculation")
|
|
218
|
+
|
|
219
|
+
if "Sun" not in active_points:
|
|
220
|
+
logging.info("Automatically adding 'Sun' to active points")
|
|
221
|
+
active_points.append("Sun")
|
|
222
|
+
|
|
223
|
+
if "Moon" not in active_points:
|
|
224
|
+
logging.info("Automatically adding 'Moon' to active points")
|
|
225
|
+
active_points.append("Moon")
|
|
226
|
+
|
|
227
|
+
if "Ascendant" not in active_points:
|
|
228
|
+
logging.info("Automatically adding 'Ascendant' to active points")
|
|
229
|
+
active_points.append("Ascendant")
|
|
230
|
+
|
|
231
|
+
if "Medium_Coeli" not in active_points:
|
|
232
|
+
logging.info("Automatically adding 'Medium_Coeli' to active points")
|
|
233
|
+
active_points.append("Medium_Coeli")
|
|
234
|
+
|
|
235
|
+
if "Mercury" not in active_points:
|
|
236
|
+
logging.info("Automatically adding 'Mercury' to active points")
|
|
237
|
+
active_points.append("Mercury")
|
|
238
|
+
|
|
239
|
+
if "Venus" not in active_points:
|
|
240
|
+
logging.info("Automatically adding 'Venus' to active points")
|
|
241
|
+
active_points.append("Venus")
|
|
242
|
+
|
|
243
|
+
if "Mars" not in active_points:
|
|
244
|
+
logging.info("Automatically adding 'Mars' to active points")
|
|
245
|
+
active_points.append("Mars")
|
|
246
|
+
|
|
247
|
+
if "Jupiter" not in active_points:
|
|
248
|
+
logging.info("Automatically adding 'Jupiter' to active points")
|
|
249
|
+
active_points.append("Jupiter")
|
|
250
|
+
|
|
251
|
+
if "Saturn" not in active_points:
|
|
252
|
+
logging.info("Automatically adding 'Saturn' to active points")
|
|
253
|
+
active_points.append("Saturn")
|
|
254
|
+
|
|
255
|
+
# Create a calculation data container
|
|
256
|
+
calc_data = {}
|
|
257
|
+
|
|
258
|
+
# Basic identity
|
|
259
|
+
calc_data["name"] = name
|
|
260
|
+
calc_data["json_dir"] = str(Path.home())
|
|
261
|
+
|
|
262
|
+
# Initialize configuration
|
|
263
|
+
config = ChartConfiguration(
|
|
264
|
+
zodiac_type=zodiac_type,
|
|
265
|
+
sidereal_mode=sidereal_mode,
|
|
266
|
+
houses_system_identifier=houses_system_identifier,
|
|
267
|
+
perspective_type=perspective_type,
|
|
268
|
+
)
|
|
269
|
+
config.validate()
|
|
270
|
+
|
|
271
|
+
# Add configuration data to calculation data
|
|
272
|
+
calc_data["zodiac_type"] = config.zodiac_type
|
|
273
|
+
calc_data["sidereal_mode"] = config.sidereal_mode
|
|
274
|
+
calc_data["houses_system_identifier"] = config.houses_system_identifier
|
|
275
|
+
calc_data["perspective_type"] = config.perspective_type
|
|
276
|
+
|
|
277
|
+
# Set up geonames username if needed
|
|
278
|
+
if geonames_username is None and online and (not lat or not lng or not tz_str):
|
|
279
|
+
logging.warning(GEONAMES_DEFAULT_USERNAME_WARNING)
|
|
280
|
+
geonames_username = DEFAULT_GEONAMES_USERNAME
|
|
281
|
+
|
|
282
|
+
# Initialize location data
|
|
283
|
+
location = LocationData(
|
|
284
|
+
city=city or "Greenwich",
|
|
285
|
+
nation=nation or "GB",
|
|
286
|
+
lat=lat if lat is not None else 51.5074,
|
|
287
|
+
lng=lng if lng is not None else 0.0,
|
|
288
|
+
tz_str=tz_str or "Etc/GMT",
|
|
289
|
+
altitude=altitude
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# If offline mode is requested but required data is missing, raise error
|
|
293
|
+
if not online and (not tz_str or not lat or not lng):
|
|
294
|
+
raise KerykeionException(
|
|
295
|
+
"For offline mode, you must provide timezone (tz_str) and coordinates (lat, lng)"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Fetch location data if needed
|
|
299
|
+
if online and (not tz_str or not lat or not lng):
|
|
300
|
+
location.fetch_from_geonames(
|
|
301
|
+
username=geonames_username or DEFAULT_GEONAMES_USERNAME,
|
|
302
|
+
cache_expire_after_days=cache_expire_after_days
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# Prepare location for calculations
|
|
306
|
+
location.prepare_for_calculation()
|
|
307
|
+
|
|
308
|
+
# Add location data to calculation data
|
|
309
|
+
calc_data["city"] = location.city
|
|
310
|
+
calc_data["nation"] = location.nation
|
|
311
|
+
calc_data["lat"] = location.lat
|
|
312
|
+
calc_data["lng"] = location.lng
|
|
313
|
+
calc_data["tz_str"] = location.tz_str
|
|
314
|
+
calc_data["altitude"] = location.altitude
|
|
315
|
+
|
|
316
|
+
# Store calculation parameters
|
|
317
|
+
calc_data["year"] = year
|
|
318
|
+
calc_data["month"] = month
|
|
319
|
+
calc_data["day"] = day
|
|
320
|
+
calc_data["hour"] = hour
|
|
321
|
+
calc_data["minute"] = minute
|
|
322
|
+
calc_data["seconds"] = seconds
|
|
323
|
+
calc_data["is_dst"] = is_dst
|
|
324
|
+
calc_data["active_points"] = active_points
|
|
325
|
+
|
|
326
|
+
# Calculate time conversions
|
|
327
|
+
cls._calculate_time_conversions(calc_data, location)
|
|
328
|
+
|
|
329
|
+
# Initialize Swiss Ephemeris and calculate houses and planets
|
|
330
|
+
cls._setup_ephemeris(calc_data, config)
|
|
331
|
+
cls._calculate_houses(calc_data, active_points)
|
|
332
|
+
cls._calculate_planets(calc_data, active_points)
|
|
333
|
+
cls._calculate_day_of_week(calc_data)
|
|
334
|
+
|
|
335
|
+
# Calculate lunar phase
|
|
336
|
+
calc_data["lunar_phase"] = calculate_moon_phase(
|
|
337
|
+
calc_data["moon"].abs_pos,
|
|
338
|
+
calc_data["sun"].abs_pos
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
# Create and return the AstrologicalSubjectModel
|
|
342
|
+
return AstrologicalSubjectModel(**calc_data)
|
|
343
|
+
|
|
344
|
+
@classmethod
|
|
345
|
+
def from_iso_utc_time(
|
|
346
|
+
cls,
|
|
347
|
+
name: str,
|
|
348
|
+
iso_utc_time: str,
|
|
349
|
+
city: str = "Greenwich",
|
|
350
|
+
nation: str = "GB",
|
|
351
|
+
tz_str: str = "Etc/GMT",
|
|
352
|
+
online: bool = False,
|
|
353
|
+
lng: float = 0.0,
|
|
354
|
+
lat: float = 51.5074,
|
|
355
|
+
geonames_username: str = DEFAULT_GEONAMES_USERNAME,
|
|
356
|
+
zodiac_type: ZodiacType = DEFAULT_ZODIAC_TYPE,
|
|
357
|
+
sidereal_mode: Optional[SiderealMode] = None,
|
|
358
|
+
houses_system_identifier: HousesSystemIdentifier = DEFAULT_HOUSES_SYSTEM_IDENTIFIER,
|
|
359
|
+
perspective_type: PerspectiveType = DEFAULT_PERSPECTIVE_TYPE,
|
|
360
|
+
altitude: Optional[float] = None,
|
|
361
|
+
active_points: List[AstrologicalPoint] = DEFAULT_ACTIVE_POINTS
|
|
362
|
+
) -> AstrologicalSubjectModel:
|
|
363
|
+
"""
|
|
364
|
+
Create an astrological subject from an ISO formatted UTC time.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
name: Subject name
|
|
368
|
+
iso_utc_time: ISO formatted UTC time string
|
|
369
|
+
city: Location name
|
|
370
|
+
nation: Country code
|
|
371
|
+
tz_str: Timezone string
|
|
372
|
+
online: Whether to fetch location data online
|
|
373
|
+
lng, lat: Coordinates
|
|
374
|
+
geonames_username: Username for geonames API
|
|
375
|
+
zodiac_type: Type of zodiac
|
|
376
|
+
sidereal_mode: Mode for sidereal calculations
|
|
377
|
+
houses_system_identifier: House system
|
|
378
|
+
perspective_type: Perspective for calculations
|
|
379
|
+
altitude: Location altitude
|
|
380
|
+
active_points: Set of points to calculate
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
AstrologicalSubjectModel instance
|
|
384
|
+
"""
|
|
385
|
+
# Parse the ISO time
|
|
386
|
+
dt = datetime.fromisoformat(iso_utc_time.replace('Z', '+00:00'))
|
|
387
|
+
|
|
388
|
+
# Get location data if online mode is enabled
|
|
389
|
+
if online:
|
|
390
|
+
if geonames_username == DEFAULT_GEONAMES_USERNAME:
|
|
391
|
+
logging.warning(GEONAMES_DEFAULT_USERNAME_WARNING)
|
|
392
|
+
|
|
393
|
+
geonames = FetchGeonames(
|
|
394
|
+
city,
|
|
395
|
+
nation,
|
|
396
|
+
username=geonames_username,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
city_data = geonames.get_serialized_data()
|
|
400
|
+
lng = float(city_data["lng"])
|
|
401
|
+
lat = float(city_data["lat"])
|
|
402
|
+
|
|
403
|
+
# Convert UTC to local time
|
|
404
|
+
local_time = pytz.timezone(tz_str)
|
|
405
|
+
local_datetime = dt.astimezone(local_time)
|
|
406
|
+
|
|
407
|
+
# Create the subject with local time
|
|
408
|
+
return cls.from_birth_data(
|
|
409
|
+
name=name,
|
|
410
|
+
year=local_datetime.year,
|
|
411
|
+
month=local_datetime.month,
|
|
412
|
+
day=local_datetime.day,
|
|
413
|
+
hour=local_datetime.hour,
|
|
414
|
+
minute=local_datetime.minute,
|
|
415
|
+
seconds=local_datetime.second,
|
|
416
|
+
city=city,
|
|
417
|
+
nation=nation,
|
|
418
|
+
lng=lng,
|
|
419
|
+
lat=lat,
|
|
420
|
+
tz_str=tz_str,
|
|
421
|
+
online=False, # Already fetched data if needed
|
|
422
|
+
geonames_username=geonames_username,
|
|
423
|
+
zodiac_type=zodiac_type,
|
|
424
|
+
sidereal_mode=sidereal_mode,
|
|
425
|
+
houses_system_identifier=houses_system_identifier,
|
|
426
|
+
perspective_type=perspective_type,
|
|
427
|
+
altitude=altitude,
|
|
428
|
+
active_points=active_points
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
@classmethod
|
|
432
|
+
def from_current_time(
|
|
433
|
+
cls,
|
|
434
|
+
name: str = "Now",
|
|
435
|
+
city: Optional[str] = None,
|
|
436
|
+
nation: Optional[str] = None,
|
|
437
|
+
lng: Optional[float] = None,
|
|
438
|
+
lat: Optional[float] = None,
|
|
439
|
+
tz_str: Optional[str] = None,
|
|
440
|
+
geonames_username: Optional[str] = None,
|
|
441
|
+
online: bool = True,
|
|
442
|
+
zodiac_type: ZodiacType = DEFAULT_ZODIAC_TYPE,
|
|
443
|
+
sidereal_mode: Optional[SiderealMode] = None,
|
|
444
|
+
houses_system_identifier: HousesSystemIdentifier = DEFAULT_HOUSES_SYSTEM_IDENTIFIER,
|
|
445
|
+
perspective_type: PerspectiveType = DEFAULT_PERSPECTIVE_TYPE,
|
|
446
|
+
active_points: List[AstrologicalPoint] = DEFAULT_ACTIVE_POINTS
|
|
447
|
+
) -> AstrologicalSubjectModel:
|
|
448
|
+
"""
|
|
449
|
+
Create an astrological subject for the current time.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
name: Subject name
|
|
453
|
+
city: Location name
|
|
454
|
+
nation: Country code
|
|
455
|
+
lng, lat: Coordinates
|
|
456
|
+
tz_str: Timezone string
|
|
457
|
+
geonames_username: Username for geonames API
|
|
458
|
+
online: Whether to fetch location data online
|
|
459
|
+
zodiac_type: Type of zodiac
|
|
460
|
+
sidereal_mode: Mode for sidereal calculations
|
|
461
|
+
houses_system_identifier: House system
|
|
462
|
+
perspective_type: Perspective for calculations
|
|
463
|
+
active_points: Set of points to calculate
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
AstrologicalSubjectModel for current time
|
|
467
|
+
"""
|
|
468
|
+
now = datetime.now()
|
|
469
|
+
|
|
470
|
+
return cls.from_birth_data(
|
|
471
|
+
name=name,
|
|
472
|
+
year=now.year,
|
|
473
|
+
month=now.month,
|
|
474
|
+
day=now.day,
|
|
475
|
+
hour=now.hour,
|
|
476
|
+
minute=now.minute,
|
|
477
|
+
seconds=now.second,
|
|
478
|
+
city=city,
|
|
479
|
+
nation=nation,
|
|
480
|
+
lng=lng,
|
|
481
|
+
lat=lat,
|
|
482
|
+
tz_str=tz_str,
|
|
483
|
+
geonames_username=geonames_username,
|
|
484
|
+
online=online,
|
|
485
|
+
zodiac_type=zodiac_type,
|
|
486
|
+
sidereal_mode=sidereal_mode,
|
|
487
|
+
houses_system_identifier=houses_system_identifier,
|
|
488
|
+
perspective_type=perspective_type,
|
|
489
|
+
active_points=active_points
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
@classmethod
|
|
493
|
+
def _calculate_time_conversions(cls, data: Dict[str, Any], location: LocationData) -> None:
|
|
494
|
+
"""Calculate time conversions between local time, UTC and Julian day"""
|
|
495
|
+
# Convert local time to UTC
|
|
496
|
+
local_timezone = pytz.timezone(location.tz_str)
|
|
497
|
+
naive_datetime = datetime(
|
|
498
|
+
data["year"], data["month"], data["day"],
|
|
499
|
+
data["hour"], data["minute"], data["seconds"]
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
try:
|
|
503
|
+
local_datetime = local_timezone.localize(naive_datetime, is_dst=data.get("is_dst"))
|
|
504
|
+
except pytz.exceptions.AmbiguousTimeError:
|
|
505
|
+
raise KerykeionException(
|
|
506
|
+
"Ambiguous time error! The time falls during a DST transition. "
|
|
507
|
+
"Please specify is_dst=True or is_dst=False to clarify."
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
# Store formatted times
|
|
511
|
+
utc_datetime = local_datetime.astimezone(pytz.utc)
|
|
512
|
+
data["iso_formatted_utc_datetime"] = utc_datetime.isoformat()
|
|
513
|
+
data["iso_formatted_local_datetime"] = local_datetime.isoformat()
|
|
514
|
+
|
|
515
|
+
# Calculate Julian day
|
|
516
|
+
data["julian_day"] = datetime_to_julian(utc_datetime)
|
|
517
|
+
|
|
518
|
+
@classmethod
|
|
519
|
+
def _setup_ephemeris(cls, data: Dict[str, Any], config: ChartConfiguration) -> None:
|
|
520
|
+
"""Set up Swiss Ephemeris with appropriate flags"""
|
|
521
|
+
# Set ephemeris path
|
|
522
|
+
swe.set_ephe_path(str(Path(__file__).parent.absolute() / "sweph"))
|
|
523
|
+
|
|
524
|
+
# Base flags
|
|
525
|
+
iflag = swe.FLG_SWIEPH + swe.FLG_SPEED
|
|
526
|
+
|
|
527
|
+
# Add perspective flags
|
|
528
|
+
if config.perspective_type == "True Geocentric":
|
|
529
|
+
iflag += swe.FLG_TRUEPOS
|
|
530
|
+
elif config.perspective_type == "Heliocentric":
|
|
531
|
+
iflag += swe.FLG_HELCTR
|
|
532
|
+
elif config.perspective_type == "Topocentric":
|
|
533
|
+
iflag += swe.FLG_TOPOCTR
|
|
534
|
+
# Set topocentric coordinates
|
|
535
|
+
swe.set_topo(data["lng"], data["lat"], data["altitude"] or 0)
|
|
536
|
+
|
|
537
|
+
# Add sidereal flag if needed
|
|
538
|
+
if config.zodiac_type == "Sidereal":
|
|
539
|
+
iflag += swe.FLG_SIDEREAL
|
|
540
|
+
# Set sidereal mode
|
|
541
|
+
mode = f"SIDM_{config.sidereal_mode}"
|
|
542
|
+
swe.set_sid_mode(getattr(swe, mode))
|
|
543
|
+
logging.debug(f"Using sidereal mode: {mode}")
|
|
544
|
+
|
|
545
|
+
# Save house system name and iflag for later use
|
|
546
|
+
data["houses_system_name"] = swe.house_name(
|
|
547
|
+
config.houses_system_identifier.encode('ascii')
|
|
548
|
+
)
|
|
549
|
+
data["_iflag"] = iflag
|
|
550
|
+
|
|
551
|
+
@classmethod
|
|
552
|
+
def _calculate_houses(cls, data: Dict[str, Any], active_points: Optional[List[AstrologicalPoint]]) -> None:
|
|
553
|
+
"""Calculate house cusps and axis points"""
|
|
554
|
+
# Skip calculation if point is not in active_points
|
|
555
|
+
should_calculate: Callable[[AstrologicalPoint], bool] = lambda point: not active_points or point in active_points
|
|
556
|
+
# Track which axial cusps are actually calculated
|
|
557
|
+
calculated_axial_cusps = []
|
|
558
|
+
|
|
559
|
+
# Calculate houses based on zodiac type
|
|
560
|
+
if data["zodiac_type"] == "Sidereal":
|
|
561
|
+
cusps, ascmc = swe.houses_ex(
|
|
562
|
+
tjdut=data["julian_day"],
|
|
563
|
+
lat=data["lat"],
|
|
564
|
+
lon=data["lng"],
|
|
565
|
+
hsys=str.encode(data["houses_system_identifier"]),
|
|
566
|
+
flags=swe.FLG_SIDEREAL
|
|
567
|
+
)
|
|
568
|
+
else: # Tropical zodiac
|
|
569
|
+
cusps, ascmc = swe.houses(
|
|
570
|
+
tjdut=data["julian_day"],
|
|
571
|
+
lat=data["lat"],
|
|
572
|
+
lon=data["lng"],
|
|
573
|
+
hsys=str.encode(data["houses_system_identifier"])
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
# Store house degrees
|
|
577
|
+
data["_houses_degree_ut"] = cusps
|
|
578
|
+
|
|
579
|
+
# Create house objects
|
|
580
|
+
point_type: PointType = "House"
|
|
581
|
+
data["first_house"] = get_kerykeion_point_from_degree(cusps[0], "First_House", point_type=point_type)
|
|
582
|
+
data["second_house"] = get_kerykeion_point_from_degree(cusps[1], "Second_House", point_type=point_type)
|
|
583
|
+
data["third_house"] = get_kerykeion_point_from_degree(cusps[2], "Third_House", point_type=point_type)
|
|
584
|
+
data["fourth_house"] = get_kerykeion_point_from_degree(cusps[3], "Fourth_House", point_type=point_type)
|
|
585
|
+
data["fifth_house"] = get_kerykeion_point_from_degree(cusps[4], "Fifth_House", point_type=point_type)
|
|
586
|
+
data["sixth_house"] = get_kerykeion_point_from_degree(cusps[5], "Sixth_House", point_type=point_type)
|
|
587
|
+
data["seventh_house"] = get_kerykeion_point_from_degree(cusps[6], "Seventh_House", point_type=point_type)
|
|
588
|
+
data["eighth_house"] = get_kerykeion_point_from_degree(cusps[7], "Eighth_House", point_type=point_type)
|
|
589
|
+
data["ninth_house"] = get_kerykeion_point_from_degree(cusps[8], "Ninth_House", point_type=point_type)
|
|
590
|
+
data["tenth_house"] = get_kerykeion_point_from_degree(cusps[9], "Tenth_House", point_type=point_type)
|
|
591
|
+
data["eleventh_house"] = get_kerykeion_point_from_degree(cusps[10], "Eleventh_House", point_type=point_type)
|
|
592
|
+
data["twelfth_house"] = get_kerykeion_point_from_degree(cusps[11], "Twelfth_House", point_type=point_type)
|
|
593
|
+
|
|
594
|
+
# Store house names
|
|
595
|
+
data["houses_names_list"] = list(get_args(Houses))
|
|
596
|
+
|
|
597
|
+
# Calculate axis points
|
|
598
|
+
point_type = "AstrologicalPoint"
|
|
599
|
+
|
|
600
|
+
# Calculate Ascendant if needed
|
|
601
|
+
if should_calculate("Ascendant"):
|
|
602
|
+
data["ascendant"] = get_kerykeion_point_from_degree(ascmc[0], "Ascendant", point_type=point_type)
|
|
603
|
+
data["ascendant"].house = get_planet_house(data["ascendant"].abs_pos, data["_houses_degree_ut"])
|
|
604
|
+
data["ascendant"].retrograde = False
|
|
605
|
+
calculated_axial_cusps.append("Ascendant")
|
|
606
|
+
|
|
607
|
+
# Calculate Medium Coeli if needed
|
|
608
|
+
if should_calculate("Medium_Coeli"):
|
|
609
|
+
data["medium_coeli"] = get_kerykeion_point_from_degree(ascmc[1], "Medium_Coeli", point_type=point_type)
|
|
610
|
+
data["medium_coeli"].house = get_planet_house(data["medium_coeli"].abs_pos, data["_houses_degree_ut"])
|
|
611
|
+
data["medium_coeli"].retrograde = False
|
|
612
|
+
calculated_axial_cusps.append("Medium_Coeli")
|
|
613
|
+
|
|
614
|
+
# Calculate Descendant if needed
|
|
615
|
+
if should_calculate("Descendant"):
|
|
616
|
+
dsc_deg = math.fmod(ascmc[0] + 180, 360)
|
|
617
|
+
data["descendant"] = get_kerykeion_point_from_degree(dsc_deg, "Descendant", point_type=point_type)
|
|
618
|
+
data["descendant"].house = get_planet_house(data["descendant"].abs_pos, data["_houses_degree_ut"])
|
|
619
|
+
data["descendant"].retrograde = False
|
|
620
|
+
calculated_axial_cusps.append("Descendant")
|
|
621
|
+
|
|
622
|
+
# Calculate Imum Coeli if needed
|
|
623
|
+
if should_calculate("Imum_Coeli"):
|
|
624
|
+
ic_deg = math.fmod(ascmc[1] + 180, 360)
|
|
625
|
+
data["imum_coeli"] = get_kerykeion_point_from_degree(ic_deg, "Imum_Coeli", point_type=point_type)
|
|
626
|
+
data["imum_coeli"].house = get_planet_house(data["imum_coeli"].abs_pos, data["_houses_degree_ut"])
|
|
627
|
+
data["imum_coeli"].retrograde = False
|
|
628
|
+
calculated_axial_cusps.append("Imum_Coeli")
|
|
629
|
+
|
|
630
|
+
@classmethod
|
|
631
|
+
def _calculate_planets(cls, data: Dict[str, Any], active_points: List[AstrologicalPoint]) -> None:
|
|
632
|
+
"""Calculate planetary positions and related information"""
|
|
633
|
+
# Skip calculation if point is not in active_points
|
|
634
|
+
should_calculate: Callable[[AstrologicalPoint], bool] = lambda point: not active_points or point in active_points
|
|
635
|
+
|
|
636
|
+
point_type: PointType = "AstrologicalPoint"
|
|
637
|
+
julian_day = data["julian_day"]
|
|
638
|
+
iflag = data["_iflag"]
|
|
639
|
+
houses_degree_ut = data["_houses_degree_ut"]
|
|
640
|
+
|
|
641
|
+
# Track which planets are actually calculated
|
|
642
|
+
calculated_planets = []
|
|
643
|
+
|
|
644
|
+
# ==================
|
|
645
|
+
# MAIN PLANETS
|
|
646
|
+
# ==================
|
|
647
|
+
|
|
648
|
+
# Calculate Sun
|
|
649
|
+
if should_calculate("Sun"):
|
|
650
|
+
sun_deg = swe.calc_ut(julian_day, 0, iflag)[0][0]
|
|
651
|
+
data["sun"] = get_kerykeion_point_from_degree(sun_deg, "Sun", point_type=point_type)
|
|
652
|
+
data["sun"].house = get_planet_house(sun_deg, houses_degree_ut)
|
|
653
|
+
data["sun"].retrograde = swe.calc_ut(julian_day, 0, iflag)[0][3] < 0
|
|
654
|
+
calculated_planets.append("Sun")
|
|
655
|
+
|
|
656
|
+
# Calculate Moon
|
|
657
|
+
if should_calculate("Moon"):
|
|
658
|
+
moon_deg = swe.calc_ut(julian_day, 1, iflag)[0][0]
|
|
659
|
+
data["moon"] = get_kerykeion_point_from_degree(moon_deg, "Moon", point_type=point_type)
|
|
660
|
+
data["moon"].house = get_planet_house(moon_deg, houses_degree_ut)
|
|
661
|
+
data["moon"].retrograde = swe.calc_ut(julian_day, 1, iflag)[0][3] < 0
|
|
662
|
+
calculated_planets.append("Moon")
|
|
663
|
+
|
|
664
|
+
# Calculate Mercury
|
|
665
|
+
if should_calculate("Mercury"):
|
|
666
|
+
mercury_deg = swe.calc_ut(julian_day, 2, iflag)[0][0]
|
|
667
|
+
data["mercury"] = get_kerykeion_point_from_degree(mercury_deg, "Mercury", point_type=point_type)
|
|
668
|
+
data["mercury"].house = get_planet_house(mercury_deg, houses_degree_ut)
|
|
669
|
+
data["mercury"].retrograde = swe.calc_ut(julian_day, 2, iflag)[0][3] < 0
|
|
670
|
+
calculated_planets.append("Mercury")
|
|
671
|
+
|
|
672
|
+
# Calculate Venus
|
|
673
|
+
if should_calculate("Venus"):
|
|
674
|
+
venus_deg = swe.calc_ut(julian_day, 3, iflag)[0][0]
|
|
675
|
+
data["venus"] = get_kerykeion_point_from_degree(venus_deg, "Venus", point_type=point_type)
|
|
676
|
+
data["venus"].house = get_planet_house(venus_deg, houses_degree_ut)
|
|
677
|
+
data["venus"].retrograde = swe.calc_ut(julian_day, 3, iflag)[0][3] < 0
|
|
678
|
+
calculated_planets.append("Venus")
|
|
679
|
+
|
|
680
|
+
# Calculate Mars
|
|
681
|
+
if should_calculate("Mars"):
|
|
682
|
+
mars_deg = swe.calc_ut(julian_day, 4, iflag)[0][0]
|
|
683
|
+
data["mars"] = get_kerykeion_point_from_degree(mars_deg, "Mars", point_type=point_type)
|
|
684
|
+
data["mars"].house = get_planet_house(mars_deg, houses_degree_ut)
|
|
685
|
+
data["mars"].retrograde = swe.calc_ut(julian_day, 4, iflag)[0][3] < 0
|
|
686
|
+
calculated_planets.append("Mars")
|
|
687
|
+
|
|
688
|
+
# Calculate Jupiter
|
|
689
|
+
if should_calculate("Jupiter"):
|
|
690
|
+
jupiter_deg = swe.calc_ut(julian_day, 5, iflag)[0][0]
|
|
691
|
+
data["jupiter"] = get_kerykeion_point_from_degree(jupiter_deg, "Jupiter", point_type=point_type)
|
|
692
|
+
data["jupiter"].house = get_planet_house(jupiter_deg, houses_degree_ut)
|
|
693
|
+
data["jupiter"].retrograde = swe.calc_ut(julian_day, 5, iflag)[0][3] < 0
|
|
694
|
+
calculated_planets.append("Jupiter")
|
|
695
|
+
|
|
696
|
+
# Calculate Saturn
|
|
697
|
+
if should_calculate("Saturn"):
|
|
698
|
+
saturn_deg = swe.calc_ut(julian_day, 6, iflag)[0][0]
|
|
699
|
+
data["saturn"] = get_kerykeion_point_from_degree(saturn_deg, "Saturn", point_type=point_type)
|
|
700
|
+
data["saturn"].house = get_planet_house(saturn_deg, houses_degree_ut)
|
|
701
|
+
data["saturn"].retrograde = swe.calc_ut(julian_day, 6, iflag)[0][3] < 0
|
|
702
|
+
calculated_planets.append("Saturn")
|
|
703
|
+
|
|
704
|
+
# Calculate Uranus
|
|
705
|
+
if should_calculate("Uranus"):
|
|
706
|
+
uranus_deg = swe.calc_ut(julian_day, 7, iflag)[0][0]
|
|
707
|
+
data["uranus"] = get_kerykeion_point_from_degree(uranus_deg, "Uranus", point_type=point_type)
|
|
708
|
+
data["uranus"].house = get_planet_house(uranus_deg, houses_degree_ut)
|
|
709
|
+
data["uranus"].retrograde = swe.calc_ut(julian_day, 7, iflag)[0][3] < 0
|
|
710
|
+
calculated_planets.append("Uranus")
|
|
711
|
+
|
|
712
|
+
# Calculate Neptune
|
|
713
|
+
if should_calculate("Neptune"):
|
|
714
|
+
neptune_deg = swe.calc_ut(julian_day, 8, iflag)[0][0]
|
|
715
|
+
data["neptune"] = get_kerykeion_point_from_degree(neptune_deg, "Neptune", point_type=point_type)
|
|
716
|
+
data["neptune"].house = get_planet_house(neptune_deg, houses_degree_ut)
|
|
717
|
+
data["neptune"].retrograde = swe.calc_ut(julian_day, 8, iflag)[0][3] < 0
|
|
718
|
+
calculated_planets.append("Neptune")
|
|
719
|
+
|
|
720
|
+
# Calculate Pluto
|
|
721
|
+
if should_calculate("Pluto"):
|
|
722
|
+
pluto_deg = swe.calc_ut(julian_day, 9, iflag)[0][0]
|
|
723
|
+
data["pluto"] = get_kerykeion_point_from_degree(pluto_deg, "Pluto", point_type=point_type)
|
|
724
|
+
data["pluto"].house = get_planet_house(pluto_deg, houses_degree_ut)
|
|
725
|
+
data["pluto"].retrograde = swe.calc_ut(julian_day, 9, iflag)[0][3] < 0
|
|
726
|
+
calculated_planets.append("Pluto")
|
|
727
|
+
|
|
728
|
+
# ==================
|
|
729
|
+
# LUNAR NODES
|
|
730
|
+
# ==================
|
|
731
|
+
|
|
732
|
+
# Calculate Mean Lunar Node
|
|
733
|
+
if should_calculate("Mean_Node"):
|
|
734
|
+
mean_node_deg = swe.calc_ut(julian_day, 10, iflag)[0][0]
|
|
735
|
+
data["mean_node"] = get_kerykeion_point_from_degree(mean_node_deg, "Mean_Node", point_type=point_type)
|
|
736
|
+
data["mean_node"].house = get_planet_house(mean_node_deg, houses_degree_ut)
|
|
737
|
+
data["mean_node"].retrograde = swe.calc_ut(julian_day, 10, iflag)[0][3] < 0
|
|
738
|
+
calculated_planets.append("Mean_Node")
|
|
739
|
+
|
|
740
|
+
# Calculate True Lunar Node
|
|
741
|
+
if should_calculate("True_Node"):
|
|
742
|
+
true_node_deg = swe.calc_ut(julian_day, 11, iflag)[0][0]
|
|
743
|
+
data["true_node"] = get_kerykeion_point_from_degree(true_node_deg, "True_Node", point_type=point_type)
|
|
744
|
+
data["true_node"].house = get_planet_house(true_node_deg, houses_degree_ut)
|
|
745
|
+
data["true_node"].retrograde = swe.calc_ut(julian_day, 11, iflag)[0][3] < 0
|
|
746
|
+
calculated_planets.append("True_Node")
|
|
747
|
+
|
|
748
|
+
# Calculate Mean South Node (opposite to Mean North Node)
|
|
749
|
+
if should_calculate("Mean_South_Node") and "mean_node" in data:
|
|
750
|
+
mean_south_node_deg = math.fmod(data["mean_node"].abs_pos + 180, 360)
|
|
751
|
+
data["mean_south_node"] = get_kerykeion_point_from_degree(
|
|
752
|
+
mean_south_node_deg, "Mean_South_Node", point_type=point_type
|
|
753
|
+
)
|
|
754
|
+
data["mean_south_node"].house = get_planet_house(mean_south_node_deg, houses_degree_ut)
|
|
755
|
+
data["mean_south_node"].retrograde = data["mean_node"].retrograde
|
|
756
|
+
calculated_planets.append("Mean_South_Node")
|
|
757
|
+
|
|
758
|
+
# Calculate True South Node (opposite to True North Node)
|
|
759
|
+
if should_calculate("True_South_Node") and "true_node" in data:
|
|
760
|
+
true_south_node_deg = math.fmod(data["true_node"].abs_pos + 180, 360)
|
|
761
|
+
data["true_south_node"] = get_kerykeion_point_from_degree(
|
|
762
|
+
true_south_node_deg, "True_South_Node", point_type=point_type
|
|
763
|
+
)
|
|
764
|
+
data["true_south_node"].house = get_planet_house(true_south_node_deg, houses_degree_ut)
|
|
765
|
+
data["true_south_node"].retrograde = data["true_node"].retrograde
|
|
766
|
+
calculated_planets.append("True_South_Node")
|
|
767
|
+
|
|
768
|
+
# ==================
|
|
769
|
+
# LILITH POINTS
|
|
770
|
+
# ==================
|
|
771
|
+
|
|
772
|
+
# Calculate Mean Lilith (Mean Black Moon)
|
|
773
|
+
if should_calculate("Mean_Lilith"):
|
|
774
|
+
try:
|
|
775
|
+
mean_lilith_deg = swe.calc_ut(julian_day, 12, iflag)[0][0]
|
|
776
|
+
data["mean_lilith"] = get_kerykeion_point_from_degree(mean_lilith_deg, "Mean_Lilith", point_type=point_type)
|
|
777
|
+
data["mean_lilith"].house = get_planet_house(mean_lilith_deg, houses_degree_ut)
|
|
778
|
+
data["mean_lilith"].retrograde = swe.calc_ut(julian_day, 12, iflag)[0][3] < 0
|
|
779
|
+
calculated_planets.append("Mean_Lilith")
|
|
780
|
+
except Exception as e:
|
|
781
|
+
logging.error(f"Error calculating Mean Lilith: {e}")
|
|
782
|
+
active_points.remove("Mean_Lilith")
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
# Calculate True Lilith (Osculating Black Moon)
|
|
786
|
+
if should_calculate("True_Lilith"):
|
|
787
|
+
try:
|
|
788
|
+
true_lilith_deg = swe.calc_ut(julian_day, 13, iflag)[0][0]
|
|
789
|
+
data["true_lilith"] = get_kerykeion_point_from_degree(true_lilith_deg, "True_Lilith", point_type=point_type)
|
|
790
|
+
data["true_lilith"].house = get_planet_house(true_lilith_deg, houses_degree_ut)
|
|
791
|
+
data["true_lilith"].retrograde = swe.calc_ut(julian_day, 13, iflag)[0][3] < 0
|
|
792
|
+
calculated_planets.append("True_Lilith")
|
|
793
|
+
except Exception as e:
|
|
794
|
+
logging.error(f"Error calculating True Lilith: {e}")
|
|
795
|
+
active_points.remove("True_Lilith")
|
|
796
|
+
|
|
797
|
+
# ==================
|
|
798
|
+
# SPECIAL POINTS
|
|
799
|
+
# ==================
|
|
800
|
+
|
|
801
|
+
# Calculate Earth - useful for heliocentric charts
|
|
802
|
+
if should_calculate("Earth"):
|
|
803
|
+
try:
|
|
804
|
+
earth_deg = swe.calc_ut(julian_day, 14, iflag)[0][0]
|
|
805
|
+
data["earth"] = get_kerykeion_point_from_degree(earth_deg, "Earth", point_type=point_type)
|
|
806
|
+
data["earth"].house = get_planet_house(earth_deg, houses_degree_ut)
|
|
807
|
+
data["earth"].retrograde = swe.calc_ut(julian_day, 14, iflag)[0][3] < 0
|
|
808
|
+
calculated_planets.append("Earth")
|
|
809
|
+
except Exception as e:
|
|
810
|
+
logging.error(f"Error calculating Earth position: {e}")
|
|
811
|
+
active_points.remove("Earth")
|
|
812
|
+
|
|
813
|
+
# Calculate Chiron
|
|
814
|
+
if should_calculate("Chiron"):
|
|
815
|
+
try:
|
|
816
|
+
chiron_deg = swe.calc_ut(julian_day, 15, iflag)[0][0]
|
|
817
|
+
data["chiron"] = get_kerykeion_point_from_degree(chiron_deg, "Chiron", point_type=point_type)
|
|
818
|
+
data["chiron"].house = get_planet_house(chiron_deg, houses_degree_ut)
|
|
819
|
+
data["chiron"].retrograde = swe.calc_ut(julian_day, 15, iflag)[0][3] < 0
|
|
820
|
+
calculated_planets.append("Chiron")
|
|
821
|
+
except Exception as e:
|
|
822
|
+
logging.error(f"Error calculating Chiron position: {e}")
|
|
823
|
+
active_points.remove("Chiron")
|
|
824
|
+
|
|
825
|
+
# Calculate Pholus
|
|
826
|
+
if should_calculate("Pholus"):
|
|
827
|
+
try:
|
|
828
|
+
pholus_deg = swe.calc_ut(julian_day, 16, iflag)[0][0]
|
|
829
|
+
data["pholus"] = get_kerykeion_point_from_degree(pholus_deg, "Pholus", point_type=point_type)
|
|
830
|
+
data["pholus"].house = get_planet_house(pholus_deg, houses_degree_ut)
|
|
831
|
+
data["pholus"].retrograde = swe.calc_ut(julian_day, 16, iflag)[0][3] < 0
|
|
832
|
+
calculated_planets.append("Pholus")
|
|
833
|
+
except Exception as e:
|
|
834
|
+
logging.error(f"Error calculating Pholus position: {e}")
|
|
835
|
+
active_points.remove("Pholus")
|
|
836
|
+
|
|
837
|
+
# ==================
|
|
838
|
+
# ASTEROIDS
|
|
839
|
+
# ==================
|
|
840
|
+
|
|
841
|
+
# Calculate Ceres
|
|
842
|
+
if should_calculate("Ceres"):
|
|
843
|
+
try:
|
|
844
|
+
ceres_deg = swe.calc_ut(julian_day, 17, iflag)[0][0]
|
|
845
|
+
data["ceres"] = get_kerykeion_point_from_degree(ceres_deg, "Ceres", point_type=point_type)
|
|
846
|
+
data["ceres"].house = get_planet_house(ceres_deg, houses_degree_ut)
|
|
847
|
+
data["ceres"].retrograde = swe.calc_ut(julian_day, 17, iflag)[0][3] < 0
|
|
848
|
+
calculated_planets.append("Ceres")
|
|
849
|
+
except Exception as e:
|
|
850
|
+
logging.error(f"Error calculating Ceres position: {e}")
|
|
851
|
+
active_points.remove("Ceres")
|
|
852
|
+
|
|
853
|
+
# Calculate Pallas
|
|
854
|
+
if should_calculate("Pallas"):
|
|
855
|
+
try:
|
|
856
|
+
pallas_deg = swe.calc_ut(julian_day, 18, iflag)[0][0]
|
|
857
|
+
data["pallas"] = get_kerykeion_point_from_degree(pallas_deg, "Pallas", point_type=point_type)
|
|
858
|
+
data["pallas"].house = get_planet_house(pallas_deg, houses_degree_ut)
|
|
859
|
+
data["pallas"].retrograde = swe.calc_ut(julian_day, 18, iflag)[0][3] < 0
|
|
860
|
+
calculated_planets.append("Pallas")
|
|
861
|
+
except Exception as e:
|
|
862
|
+
logging.error(f"Error calculating Pallas position: {e}")
|
|
863
|
+
active_points.remove("Pallas")
|
|
864
|
+
|
|
865
|
+
# Calculate Juno
|
|
866
|
+
if should_calculate("Juno"):
|
|
867
|
+
try:
|
|
868
|
+
juno_deg = swe.calc_ut(julian_day, 19, iflag)[0][0]
|
|
869
|
+
data["juno"] = get_kerykeion_point_from_degree(juno_deg, "Juno", point_type=point_type)
|
|
870
|
+
data["juno"].house = get_planet_house(juno_deg, houses_degree_ut)
|
|
871
|
+
data["juno"].retrograde = swe.calc_ut(julian_day, 19, iflag)[0][3] < 0
|
|
872
|
+
calculated_planets.append("Juno")
|
|
873
|
+
except Exception as e:
|
|
874
|
+
logging.error(f"Error calculating Juno position: {e}")
|
|
875
|
+
active_points.remove("Juno")
|
|
876
|
+
|
|
877
|
+
# Calculate Vesta
|
|
878
|
+
if should_calculate("Vesta"):
|
|
879
|
+
try:
|
|
880
|
+
vesta_deg = swe.calc_ut(julian_day, 20, iflag)[0][0]
|
|
881
|
+
data["vesta"] = get_kerykeion_point_from_degree(vesta_deg, "Vesta", point_type=point_type)
|
|
882
|
+
data["vesta"].house = get_planet_house(vesta_deg, houses_degree_ut)
|
|
883
|
+
data["vesta"].retrograde = swe.calc_ut(julian_day, 20, iflag)[0][3] < 0
|
|
884
|
+
calculated_planets.append("Vesta")
|
|
885
|
+
except Exception as e:
|
|
886
|
+
logging.error(f"Error calculating Vesta position: {e}")
|
|
887
|
+
active_points.remove("Vesta")
|
|
888
|
+
|
|
889
|
+
# ==================
|
|
890
|
+
# TRANS-NEPTUNIAN OBJECTS
|
|
891
|
+
# ==================
|
|
892
|
+
|
|
893
|
+
# Calculate Eris
|
|
894
|
+
if should_calculate("Eris"):
|
|
895
|
+
try:
|
|
896
|
+
eris_deg = swe.calc_ut(julian_day, swe.AST_OFFSET + 136199, iflag)[0][0]
|
|
897
|
+
data["eris"] = get_kerykeion_point_from_degree(eris_deg, "Eris", point_type=point_type)
|
|
898
|
+
data["eris"].house = get_planet_house(eris_deg, houses_degree_ut)
|
|
899
|
+
data["eris"].retrograde = swe.calc_ut(julian_day, swe.AST_OFFSET + 136199, iflag)[0][3] < 0
|
|
900
|
+
calculated_planets.append("Eris")
|
|
901
|
+
except Exception as e:
|
|
902
|
+
logging.warning(f"Could not calculate Eris position: {e}")
|
|
903
|
+
active_points.remove("Eris") # Remove if not calculated
|
|
904
|
+
|
|
905
|
+
# Calculate Sedna
|
|
906
|
+
if should_calculate("Sedna"):
|
|
907
|
+
try:
|
|
908
|
+
sedna_deg = swe.calc_ut(julian_day, swe.AST_OFFSET + 90377, iflag)[0][0]
|
|
909
|
+
data["sedna"] = get_kerykeion_point_from_degree(sedna_deg, "Sedna", point_type=point_type)
|
|
910
|
+
data["sedna"].house = get_planet_house(sedna_deg, houses_degree_ut)
|
|
911
|
+
data["sedna"].retrograde = swe.calc_ut(julian_day, swe.AST_OFFSET + 90377, iflag)[0][3] < 0
|
|
912
|
+
calculated_planets.append("Sedna")
|
|
913
|
+
except Exception as e:
|
|
914
|
+
logging.warning(f"Could not calculate Sedna position: {e}")
|
|
915
|
+
active_points.remove("Sedna")
|
|
916
|
+
|
|
917
|
+
# Calculate Haumea
|
|
918
|
+
if should_calculate("Haumea"):
|
|
919
|
+
try:
|
|
920
|
+
haumea_deg = swe.calc_ut(julian_day, swe.AST_OFFSET + 136108, iflag)[0][0]
|
|
921
|
+
data["haumea"] = get_kerykeion_point_from_degree(haumea_deg, "Haumea", point_type=point_type)
|
|
922
|
+
data["haumea"].house = get_planet_house(haumea_deg, houses_degree_ut)
|
|
923
|
+
data["haumea"].retrograde = swe.calc_ut(julian_day, swe.AST_OFFSET + 136108, iflag)[0][3] < 0
|
|
924
|
+
calculated_planets.append("Haumea")
|
|
925
|
+
except Exception as e:
|
|
926
|
+
logging.warning(f"Could not calculate Haumea position: {e}")
|
|
927
|
+
active_points.remove("Haumea") # Remove if not calculated
|
|
928
|
+
|
|
929
|
+
# Calculate Makemake
|
|
930
|
+
if should_calculate("Makemake"):
|
|
931
|
+
try:
|
|
932
|
+
makemake_deg = swe.calc_ut(julian_day, swe.AST_OFFSET + 136472, iflag)[0][0]
|
|
933
|
+
data["makemake"] = get_kerykeion_point_from_degree(makemake_deg, "Makemake", point_type=point_type)
|
|
934
|
+
data["makemake"].house = get_planet_house(makemake_deg, houses_degree_ut)
|
|
935
|
+
data["makemake"].retrograde = swe.calc_ut(julian_day, swe.AST_OFFSET + 136472, iflag)[0][3] < 0
|
|
936
|
+
calculated_planets.append("Makemake")
|
|
937
|
+
except Exception as e:
|
|
938
|
+
logging.warning(f"Could not calculate Makemake position: {e}")
|
|
939
|
+
active_points.remove("Makemake") # Remove if not calculated
|
|
940
|
+
|
|
941
|
+
# Calculate Ixion
|
|
942
|
+
if should_calculate("Ixion"):
|
|
943
|
+
try:
|
|
944
|
+
ixion_deg = swe.calc_ut(julian_day, swe.AST_OFFSET + 28978, iflag)[0][0]
|
|
945
|
+
data["ixion"] = get_kerykeion_point_from_degree(ixion_deg, "Ixion", point_type=point_type)
|
|
946
|
+
data["ixion"].house = get_planet_house(ixion_deg, houses_degree_ut)
|
|
947
|
+
data["ixion"].retrograde = swe.calc_ut(julian_day, swe.AST_OFFSET + 28978, iflag)[0][3] < 0
|
|
948
|
+
calculated_planets.append("Ixion")
|
|
949
|
+
except Exception as e:
|
|
950
|
+
logging.warning(f"Could not calculate Ixion position: {e}")
|
|
951
|
+
active_points.remove("Ixion") # Remove if not calculated
|
|
952
|
+
|
|
953
|
+
# Calculate Orcus
|
|
954
|
+
if should_calculate("Orcus"):
|
|
955
|
+
try:
|
|
956
|
+
orcus_deg = swe.calc_ut(julian_day, swe.AST_OFFSET + 90482, iflag)[0][0]
|
|
957
|
+
data["orcus"] = get_kerykeion_point_from_degree(orcus_deg, "Orcus", point_type=point_type)
|
|
958
|
+
data["orcus"].house = get_planet_house(orcus_deg, houses_degree_ut)
|
|
959
|
+
data["orcus"].retrograde = swe.calc_ut(julian_day, swe.AST_OFFSET + 90482, iflag)[0][3] < 0
|
|
960
|
+
calculated_planets.append("Orcus")
|
|
961
|
+
except Exception as e:
|
|
962
|
+
logging.warning(f"Could not calculate Orcus position: {e}")
|
|
963
|
+
active_points.remove("Orcus") # Remove if not calculated
|
|
964
|
+
|
|
965
|
+
# Calculate Quaoar
|
|
966
|
+
if should_calculate("Quaoar"):
|
|
967
|
+
try:
|
|
968
|
+
quaoar_deg = swe.calc_ut(julian_day, swe.AST_OFFSET + 50000, iflag)[0][0]
|
|
969
|
+
data["quaoar"] = get_kerykeion_point_from_degree(quaoar_deg, "Quaoar", point_type=point_type)
|
|
970
|
+
data["quaoar"].house = get_planet_house(quaoar_deg, houses_degree_ut)
|
|
971
|
+
data["quaoar"].retrograde = swe.calc_ut(julian_day, swe.AST_OFFSET + 50000, iflag)[0][3] < 0
|
|
972
|
+
calculated_planets.append("Quaoar")
|
|
973
|
+
except Exception as e:
|
|
974
|
+
logging.warning(f"Could not calculate Quaoar position: {e}")
|
|
975
|
+
active_points.remove("Quaoar") # Remove if not calculated
|
|
976
|
+
|
|
977
|
+
# ==================
|
|
978
|
+
# FIXED STARS
|
|
979
|
+
# ==================
|
|
980
|
+
|
|
981
|
+
# Calculate Regulus (example fixed star)
|
|
982
|
+
if should_calculate("Regulus"):
|
|
983
|
+
try:
|
|
984
|
+
star_name = b"Regulus"
|
|
985
|
+
swe.fixstar_ut(star_name, julian_day, iflag)
|
|
986
|
+
regulus_deg = swe.fixstar_ut(star_name, julian_day, iflag)[0][0]
|
|
987
|
+
data["regulus"] = get_kerykeion_point_from_degree(regulus_deg, "Regulus", point_type=point_type)
|
|
988
|
+
data["regulus"].house = get_planet_house(regulus_deg, houses_degree_ut)
|
|
989
|
+
data["regulus"].retrograde = False # Fixed stars are never retrograde
|
|
990
|
+
calculated_planets.append("Regulus")
|
|
991
|
+
except Exception as e:
|
|
992
|
+
logging.warning(f"Could not calculate Regulus position: {e}")
|
|
993
|
+
active_points.remove("Regulus") # Remove if not calculated
|
|
994
|
+
|
|
995
|
+
# Calculate Spica (example fixed star)
|
|
996
|
+
if should_calculate("Spica"):
|
|
997
|
+
try:
|
|
998
|
+
star_name = b"Spica"
|
|
999
|
+
swe.fixstar_ut(star_name, julian_day, iflag)
|
|
1000
|
+
spica_deg = swe.fixstar_ut(star_name, julian_day, iflag)[0][0]
|
|
1001
|
+
data["spica"] = get_kerykeion_point_from_degree(spica_deg, "Spica", point_type=point_type)
|
|
1002
|
+
data["spica"].house = get_planet_house(spica_deg, houses_degree_ut)
|
|
1003
|
+
data["spica"].retrograde = False # Fixed stars are never retrograde
|
|
1004
|
+
calculated_planets.append("Spica")
|
|
1005
|
+
except Exception as e:
|
|
1006
|
+
logging.warning(f"Could not calculate Spica position: {e}")
|
|
1007
|
+
active_points.remove("Spica") # Remove if not calculated
|
|
1008
|
+
|
|
1009
|
+
# ==================
|
|
1010
|
+
# ARABIC PARTS / LOTS
|
|
1011
|
+
# ==================
|
|
1012
|
+
|
|
1013
|
+
# Calculate Pars Fortunae (Part of Fortune)
|
|
1014
|
+
if should_calculate("Pars_Fortunae"):
|
|
1015
|
+
# Check if required points are available
|
|
1016
|
+
if all(k in data for k in ["ascendant", "sun", "moon"]):
|
|
1017
|
+
# Different calculation for day and night charts
|
|
1018
|
+
# Day birth (Sun above horizon): ASC + Moon - Sun
|
|
1019
|
+
# Night birth (Sun below horizon): ASC + Sun - Moon
|
|
1020
|
+
is_day_chart = get_house_number(data["sun"].house) < 7 # Houses 1-6 are above horizon
|
|
1021
|
+
|
|
1022
|
+
if is_day_chart:
|
|
1023
|
+
fortune_deg = math.fmod(data["ascendant"].abs_pos + data["moon"].abs_pos - data["sun"].abs_pos, 360)
|
|
1024
|
+
else:
|
|
1025
|
+
fortune_deg = math.fmod(data["ascendant"].abs_pos + data["sun"].abs_pos - data["moon"].abs_pos, 360)
|
|
1026
|
+
|
|
1027
|
+
data["pars_fortunae"] = get_kerykeion_point_from_degree(fortune_deg, "Pars_Fortunae", point_type=point_type)
|
|
1028
|
+
data["pars_fortunae"].house = get_planet_house(fortune_deg, houses_degree_ut)
|
|
1029
|
+
data["pars_fortunae"].retrograde = False # Parts are never retrograde
|
|
1030
|
+
calculated_planets.append("Pars_Fortunae")
|
|
1031
|
+
|
|
1032
|
+
# Calculate Pars Spiritus (Part of Spirit)
|
|
1033
|
+
if should_calculate("Pars_Spiritus"):
|
|
1034
|
+
# Check if required points are available
|
|
1035
|
+
if all(k in data for k in ["ascendant", "sun", "moon"]):
|
|
1036
|
+
# Day birth: ASC + Sun - Moon
|
|
1037
|
+
# Night birth: ASC + Moon - Sun
|
|
1038
|
+
is_day_chart = get_house_number(data["sun"].house) < 7
|
|
1039
|
+
|
|
1040
|
+
if is_day_chart:
|
|
1041
|
+
spirit_deg = math.fmod(data["ascendant"].abs_pos + data["sun"].abs_pos - data["moon"].abs_pos, 360)
|
|
1042
|
+
else:
|
|
1043
|
+
spirit_deg = math.fmod(data["ascendant"].abs_pos + data["moon"].abs_pos - data["sun"].abs_pos, 360)
|
|
1044
|
+
|
|
1045
|
+
data["pars_spiritus"] = get_kerykeion_point_from_degree(spirit_deg, "Pars_Spiritus", point_type=point_type)
|
|
1046
|
+
data["pars_spiritus"].house = get_planet_house(spirit_deg, houses_degree_ut)
|
|
1047
|
+
data["pars_spiritus"].retrograde = False
|
|
1048
|
+
calculated_planets.append("Pars_Spiritus")
|
|
1049
|
+
|
|
1050
|
+
# Calculate Pars Amoris (Part of Eros/Love)
|
|
1051
|
+
if should_calculate("Pars_Amoris"):
|
|
1052
|
+
# Check if required points are available
|
|
1053
|
+
if all(k in data for k in ["ascendant", "venus"]):
|
|
1054
|
+
# ASC + Venus - Sun
|
|
1055
|
+
if "sun" in data:
|
|
1056
|
+
amoris_deg = math.fmod(data["ascendant"].abs_pos + data["venus"].abs_pos - data["sun"].abs_pos, 360)
|
|
1057
|
+
|
|
1058
|
+
data["pars_amoris"] = get_kerykeion_point_from_degree(amoris_deg, "Pars_Amoris", point_type=point_type)
|
|
1059
|
+
data["pars_amoris"].house = get_planet_house(amoris_deg, houses_degree_ut)
|
|
1060
|
+
data["pars_amoris"].retrograde = False
|
|
1061
|
+
calculated_planets.append("Pars_Amoris")
|
|
1062
|
+
|
|
1063
|
+
# Calculate Pars Fidei (Part of Faith)
|
|
1064
|
+
if should_calculate("Pars_Fidei"):
|
|
1065
|
+
# Check if required points are available
|
|
1066
|
+
if all(k in data for k in ["ascendant", "jupiter", "saturn"]):
|
|
1067
|
+
# ASC + Jupiter - Saturn
|
|
1068
|
+
fidei_deg = math.fmod(data["ascendant"].abs_pos + data["jupiter"].abs_pos - data["saturn"].abs_pos, 360)
|
|
1069
|
+
|
|
1070
|
+
data["pars_fidei"] = get_kerykeion_point_from_degree(fidei_deg, "Pars_Fidei", point_type=point_type)
|
|
1071
|
+
data["pars_fidei"].house = get_planet_house(fidei_deg, houses_degree_ut)
|
|
1072
|
+
data["pars_fidei"].retrograde = False
|
|
1073
|
+
calculated_planets.append("Pars_Fidei")
|
|
1074
|
+
|
|
1075
|
+
# Calculate Vertex (a sort of auxiliary Descendant)
|
|
1076
|
+
if should_calculate("Vertex"):
|
|
1077
|
+
try:
|
|
1078
|
+
# Vertex is at ascmc[3] in Swiss Ephemeris
|
|
1079
|
+
if data["zodiac_type"] == "Sidereal":
|
|
1080
|
+
_, ascmc = swe.houses_ex(
|
|
1081
|
+
tjdut=data["julian_day"],
|
|
1082
|
+
lat=data["lat"],
|
|
1083
|
+
lon=data["lng"],
|
|
1084
|
+
hsys=str.encode("V"), # Vertex works best with Vehlow system
|
|
1085
|
+
flags=swe.FLG_SIDEREAL
|
|
1086
|
+
)
|
|
1087
|
+
else:
|
|
1088
|
+
_, ascmc = swe.houses(
|
|
1089
|
+
tjdut=data["julian_day"],
|
|
1090
|
+
lat=data["lat"],
|
|
1091
|
+
lon=data["lng"],
|
|
1092
|
+
hsys=str.encode("V")
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
vertex_deg = ascmc[3]
|
|
1096
|
+
data["vertex"] = get_kerykeion_point_from_degree(vertex_deg, "Vertex", point_type=point_type)
|
|
1097
|
+
data["vertex"].house = get_planet_house(vertex_deg, houses_degree_ut)
|
|
1098
|
+
data["vertex"].retrograde = False
|
|
1099
|
+
calculated_planets.append("Vertex")
|
|
1100
|
+
|
|
1101
|
+
# Calculate Anti-Vertex (opposite to Vertex)
|
|
1102
|
+
anti_vertex_deg = math.fmod(vertex_deg + 180, 360)
|
|
1103
|
+
data["anti_vertex"] = get_kerykeion_point_from_degree(anti_vertex_deg, "Anti_Vertex", point_type=point_type)
|
|
1104
|
+
data["anti_vertex"].house = get_planet_house(anti_vertex_deg, houses_degree_ut)
|
|
1105
|
+
data["anti_vertex"].retrograde = False
|
|
1106
|
+
calculated_planets.append("Anti_Vertex")
|
|
1107
|
+
except Exception as e:
|
|
1108
|
+
logging.warning("Could not calculate Vertex position, error: %s", e)
|
|
1109
|
+
active_points.remove("Vertex")
|
|
1110
|
+
|
|
1111
|
+
# Store only the planets that were actually calculated
|
|
1112
|
+
data["active_points"] = calculated_planets
|
|
1113
|
+
|
|
1114
|
+
@classmethod
|
|
1115
|
+
def _calculate_day_of_week(cls, data: Dict[str, Any]) -> None:
|
|
1116
|
+
"""Calculate the day of the week for the given Julian Day"""
|
|
1117
|
+
# Calculate the day of the week (0=Sunday, 1=Monday, ..., 6=Saturday)
|
|
1118
|
+
day_of_week = swe.day_of_week(data["julian_day"])
|
|
1119
|
+
# Map to human-readable names
|
|
1120
|
+
days_of_week = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
|
1121
|
+
data["day_of_week"] = days_of_week[day_of_week]
|
|
1122
|
+
|
|
1123
|
+
if __name__ == "__main__":
|
|
1124
|
+
# Example usage
|
|
1125
|
+
subject = AstrologicalSubjectFactory.from_current_time(name="Test Subject")
|
|
1126
|
+
print(subject.sun)
|
|
1127
|
+
print(subject.pars_amoris)
|
|
1128
|
+
print(subject.eris)
|
|
1129
|
+
print(subject.active_points)
|
|
1130
|
+
|
|
1131
|
+
# Create JSON output
|
|
1132
|
+
json_string = subject.model_dump_json(exclude_none=True, indent=2)
|