kerykeion 5.0.0a11__py3-none-any.whl → 5.0.0a12__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 CHANGED
@@ -6,7 +6,7 @@ This is part of Kerykeion (C) 2025 Giacomo Battaglia
6
6
  """
7
7
 
8
8
  # Local
9
- from .aspects import SynastryAspectsFactory, NatalAspectsFactory
9
+ from .aspects import AspectsFactory
10
10
  from .astrological_subject_factory import AstrologicalSubjectFactory
11
11
  from .charts.kerykeion_chart_svg import KerykeionChartSVG
12
12
  from .composite_subject_factory import CompositeSubjectFactory
@@ -21,8 +21,7 @@ from .settings import KerykeionSettingsModel, get_settings
21
21
  from .transits_time_range_factory import TransitsTimeRangeFactory
22
22
 
23
23
  __all__ = [
24
- "SynastryAspectsFactory",
25
- "NatalAspectsFactory",
24
+ "AspectsFactory",
26
25
  "AstrologicalSubjectFactory",
27
26
  "KerykeionChartSVG",
28
27
  "CompositeSubjectFactory",
@@ -7,10 +7,8 @@ The aspects module contains the classes and functions for calculating
7
7
  """
8
8
 
9
9
 
10
- from .synastry_aspects_factory import SynastryAspectsFactory
11
- from .natal_aspects_factory import NatalAspectsFactory
10
+ from .aspects_factory import AspectsFactory
12
11
 
13
12
  __all__ = [
14
- "SynastryAspectsFactory",
15
- "NatalAspectsFactory",
13
+ "AspectsFactory",
16
14
  ]
@@ -0,0 +1,514 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ This is part of Kerykeion (C) 2025 Giacomo Battaglia
4
+ """
5
+
6
+ import logging
7
+ from typing import Union, List, Optional
8
+
9
+ from kerykeion.astrological_subject_factory import AstrologicalSubjectFactory
10
+ from kerykeion.aspects.aspects_utils import get_aspect_from_two_points, get_active_points_list
11
+ from kerykeion.kr_types.kr_models import (
12
+ AstrologicalSubjectModel,
13
+ AspectModel,
14
+ ActiveAspect,
15
+ CompositeSubjectModel,
16
+ PlanetReturnModel,
17
+ SingleChartAspectsModel,
18
+ DualChartAspectsModel,
19
+ # Legacy aliases for backward compatibility
20
+ NatalAspectsModel,
21
+ SynastryAspectsModel
22
+ )
23
+ from kerykeion.kr_types.kr_literals import AstrologicalPoint
24
+ from kerykeion.settings.config_constants import DEFAULT_ACTIVE_ASPECTS, DEFAULT_AXIS_ORBIT
25
+ from kerykeion.settings.legacy.legacy_celestial_points_settings import DEFAULT_CELESTIAL_POINTS_SETTINGS
26
+ from kerykeion.settings.legacy.legacy_chart_aspects_settings import DEFAULT_CHART_ASPECTS_SETTINGS
27
+ from kerykeion.utilities import find_common_active_points
28
+
29
+ # Axes constants for orb filtering
30
+ AXES_LIST = [
31
+ "Ascendant",
32
+ "Medium_Coeli",
33
+ "Descendant",
34
+ "Imum_Coeli",
35
+ ]
36
+
37
+
38
+ class AspectsFactory:
39
+ """
40
+ Unified factory class for creating both single chart and dual chart aspects analysis.
41
+
42
+ This factory provides methods to calculate all aspects within a single chart or
43
+ between two charts. It consolidates the common functionality between different
44
+ types of aspect calculations while providing specialized methods for each type.
45
+
46
+ The factory provides both comprehensive and filtered aspect lists based on orb settings
47
+ and relevance criteria.
48
+
49
+ Key Features:
50
+ - Calculates aspects within a single chart (natal, returns, composite, etc.)
51
+ - Calculates aspects between two charts (synastry, transits, comparisons, etc.)
52
+ - Filters aspects based on orb thresholds
53
+ - Applies stricter orb limits for chart axes (ASC, MC, DSC, IC)
54
+ - Supports multiple subject types (natal, composite, planetary returns)
55
+
56
+ Example:
57
+ >>> # For single chart aspects (natal, returns, etc.)
58
+ >>> johnny = AstrologicalSubjectFactory.from_birth_data("Johnny", 1963, 6, 9, 0, 0, "Owensboro", "US")
59
+ >>> single_chart_aspects = AspectsFactory.single_chart_aspects(johnny)
60
+ >>>
61
+ >>> # For dual chart aspects (synastry, comparisons, etc.)
62
+ >>> john = AstrologicalSubjectFactory.from_birth_data("John", 1990, 1, 1, 12, 0, "London", "GB")
63
+ >>> jane = AstrologicalSubjectFactory.from_birth_data("Jane", 1992, 6, 15, 14, 30, "Paris", "FR")
64
+ >>> dual_chart_aspects = AspectsFactory.dual_chart_aspects(john, jane)
65
+ """
66
+
67
+ @staticmethod
68
+ def single_chart_aspects(
69
+ subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
70
+ *,
71
+ active_points: Optional[List[AstrologicalPoint]] = None,
72
+ active_aspects: Optional[List[ActiveAspect]] = None,
73
+ ) -> SingleChartAspectsModel:
74
+ """
75
+ Create aspects analysis for a single astrological chart.
76
+
77
+ This method calculates all astrological aspects (angular relationships)
78
+ within a single chart. Can be used for any type of chart including:
79
+ - Natal charts
80
+ - Planetary return charts
81
+ - Composite charts
82
+ - Any other single chart type
83
+
84
+ Args:
85
+ subject: The astrological subject for aspect calculation
86
+
87
+ Kwargs:
88
+ active_points: List of points to include in calculations
89
+ active_aspects: List of aspects with their orb settings
90
+
91
+ Returns:
92
+ SingleChartAspectsModel containing all calculated aspects data
93
+
94
+ Example:
95
+ >>> johnny = AstrologicalSubjectFactory.from_birth_data("Johnny", 1963, 6, 9, 0, 0, "Owensboro", "US")
96
+ >>> chart_aspects = AspectsFactory.single_chart_aspects(johnny)
97
+ >>> print(f"Found {len(chart_aspects.relevant_aspects)} relevant aspects")
98
+ """
99
+ # Initialize settings and configurations
100
+ celestial_points = DEFAULT_CELESTIAL_POINTS_SETTINGS
101
+ aspects_settings = DEFAULT_CHART_ASPECTS_SETTINGS
102
+ axes_orbit_settings = DEFAULT_AXIS_ORBIT
103
+
104
+ # Set active aspects with default fallback
105
+ active_aspects_resolved = active_aspects if active_aspects is not None else DEFAULT_ACTIVE_ASPECTS
106
+
107
+ # Determine active points to use
108
+ if active_points is None:
109
+ active_points_resolved = subject.active_points
110
+ else:
111
+ active_points_resolved = find_common_active_points(
112
+ subject.active_points,
113
+ active_points,
114
+ )
115
+
116
+ return AspectsFactory._create_single_chart_aspects_model(
117
+ subject, active_points_resolved, active_aspects_resolved,
118
+ aspects_settings, axes_orbit_settings, celestial_points
119
+ )
120
+
121
+ @staticmethod
122
+ def dual_chart_aspects(
123
+ first_subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
124
+ second_subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
125
+ *,
126
+ active_points: Optional[List[AstrologicalPoint]] = None,
127
+ active_aspects: Optional[List[ActiveAspect]] = None,
128
+ ) -> DualChartAspectsModel:
129
+ """
130
+ Create aspects analysis between two astrological charts.
131
+
132
+ This method calculates all astrological aspects (angular relationships)
133
+ between planets and points in two different charts. Can be used for:
134
+ - Synastry (relationship compatibility)
135
+ - Transit comparisons
136
+ - Composite vs natal comparisons
137
+ - Any other dual chart analysis
138
+
139
+ Args:
140
+ first_subject: The first astrological subject
141
+ second_subject: The second astrological subject to compare with the first
142
+
143
+ Kwargs:
144
+ active_points: Optional list of celestial points to include in calculations.
145
+ If None, uses common points between both subjects.
146
+ active_aspects: Optional list of aspect types with their orb settings.
147
+ If None, uses default aspect configuration.
148
+
149
+ Returns:
150
+ DualChartAspectsModel: Complete model containing all calculated aspects data,
151
+ including both comprehensive and filtered relevant aspects.
152
+
153
+ Example:
154
+ >>> john = AstrologicalSubjectFactory.from_birth_data("John", 1990, 1, 1, 12, 0, "London", "GB")
155
+ >>> jane = AstrologicalSubjectFactory.from_birth_data("Jane", 1992, 6, 15, 14, 30, "Paris", "FR")
156
+ >>> synastry = AspectsFactory.dual_chart_aspects(john, jane)
157
+ >>> print(f"Found {len(synastry.relevant_aspects)} relevant aspects")
158
+ """
159
+ # Initialize settings and configurations
160
+ celestial_points = DEFAULT_CELESTIAL_POINTS_SETTINGS
161
+ aspects_settings = DEFAULT_CHART_ASPECTS_SETTINGS
162
+ axes_orbit_settings = DEFAULT_AXIS_ORBIT
163
+
164
+ # Set active aspects with default fallback
165
+ active_aspects_resolved = active_aspects if active_aspects is not None else DEFAULT_ACTIVE_ASPECTS
166
+
167
+ # Determine active points to use - find common points between both subjects
168
+ if active_points is None:
169
+ active_points_resolved = first_subject.active_points
170
+ else:
171
+ active_points_resolved = find_common_active_points(
172
+ first_subject.active_points,
173
+ active_points,
174
+ )
175
+
176
+ # Further filter with second subject's active points
177
+ active_points_resolved = find_common_active_points(
178
+ second_subject.active_points,
179
+ active_points_resolved,
180
+ )
181
+
182
+ return AspectsFactory._create_dual_chart_aspects_model(
183
+ first_subject, second_subject, active_points_resolved, active_aspects_resolved,
184
+ aspects_settings, axes_orbit_settings, celestial_points
185
+ )
186
+
187
+ @staticmethod
188
+ def _create_single_chart_aspects_model(
189
+ subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
190
+ active_points_resolved: List[AstrologicalPoint],
191
+ active_aspects_resolved: List[ActiveAspect],
192
+ aspects_settings: List[dict],
193
+ axes_orbit_settings: float,
194
+ celestial_points: List[dict]
195
+ ) -> SingleChartAspectsModel:
196
+ """
197
+ Create the complete single chart aspects model with all calculations.
198
+
199
+ Returns:
200
+ SingleChartAspectsModel containing all aspects data
201
+ """
202
+ all_aspects = AspectsFactory._calculate_single_chart_aspects(
203
+ subject, active_points_resolved, active_aspects_resolved, aspects_settings, celestial_points
204
+ )
205
+ relevant_aspects = AspectsFactory._filter_relevant_aspects(all_aspects, axes_orbit_settings)
206
+
207
+ return SingleChartAspectsModel(
208
+ subject=subject,
209
+ all_aspects=all_aspects,
210
+ relevant_aspects=relevant_aspects,
211
+ active_points=active_points_resolved,
212
+ active_aspects=active_aspects_resolved,
213
+ )
214
+
215
+ @staticmethod
216
+ def _create_dual_chart_aspects_model(
217
+ first_subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
218
+ second_subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
219
+ active_points_resolved: List[AstrologicalPoint],
220
+ active_aspects_resolved: List[ActiveAspect],
221
+ aspects_settings: List[dict],
222
+ axes_orbit_settings: float,
223
+ celestial_points: List[dict]
224
+ ) -> DualChartAspectsModel:
225
+ """
226
+ Create the complete dual chart aspects model with all calculations.
227
+
228
+ Args:
229
+ first_subject: First astrological subject
230
+ second_subject: Second astrological subject
231
+ active_points_resolved: Resolved list of active celestial points
232
+ active_aspects_resolved: Resolved list of active aspects with orbs
233
+ aspects_settings: Chart aspect configuration settings
234
+ axes_orbit_settings: Orb threshold for chart axes
235
+ celestial_points: Celestial points configuration
236
+
237
+ Returns:
238
+ DualChartAspectsModel: Complete model containing all aspects data
239
+ """
240
+ all_aspects = AspectsFactory._calculate_dual_chart_aspects(
241
+ first_subject, second_subject, active_points_resolved, active_aspects_resolved,
242
+ aspects_settings, celestial_points
243
+ )
244
+ relevant_aspects = AspectsFactory._filter_relevant_aspects(all_aspects, axes_orbit_settings)
245
+
246
+ return DualChartAspectsModel(
247
+ first_subject=first_subject,
248
+ second_subject=second_subject,
249
+ all_aspects=all_aspects,
250
+ relevant_aspects=relevant_aspects,
251
+ active_points=active_points_resolved,
252
+ active_aspects=active_aspects_resolved,
253
+ )
254
+
255
+ @staticmethod
256
+ def _calculate_single_chart_aspects(
257
+ subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
258
+ active_points: List[AstrologicalPoint],
259
+ active_aspects: List[ActiveAspect],
260
+ aspects_settings: List[dict],
261
+ celestial_points: List[dict]
262
+ ) -> List[AspectModel]:
263
+ """
264
+ Calculate all aspects within a single chart.
265
+
266
+ This method handles all aspect calculations including settings updates,
267
+ opposite pair filtering, and planet ID resolution for single charts.
268
+ Works with any chart type (natal, return, composite, etc.).
269
+
270
+ Returns:
271
+ List of all calculated AspectModel instances
272
+ """
273
+ active_points_list = get_active_points_list(subject, active_points)
274
+
275
+ # Update aspects settings with active aspects orbs
276
+ filtered_settings = AspectsFactory._update_aspect_settings(aspects_settings, active_aspects)
277
+
278
+ # Create a lookup dictionary for planet IDs to optimize performance
279
+ planet_id_lookup = {planet["name"]: planet["id"] for planet in celestial_points}
280
+
281
+ # Define opposite pairs that should be skipped for single chart aspects
282
+ opposite_pairs = {
283
+ ("Ascendant", "Descendant"),
284
+ ("Descendant", "Ascendant"),
285
+ ("Medium_Coeli", "Imum_Coeli"),
286
+ ("Imum_Coeli", "Medium_Coeli"),
287
+ ("True_Node", "True_South_Node"),
288
+ ("Mean_Node", "Mean_South_Node"),
289
+ ("True_South_Node", "True_Node"),
290
+ ("Mean_South_Node", "Mean_Node"),
291
+ }
292
+
293
+ all_aspects_list = []
294
+
295
+ for first in range(len(active_points_list)):
296
+ # Generate aspects list without repetitions (single chart - same chart)
297
+ for second in range(first + 1, len(active_points_list)):
298
+ # Skip predefined opposite pairs (AC/DC, MC/IC, North/South nodes)
299
+ first_name = active_points_list[first]["name"]
300
+ second_name = active_points_list[second]["name"]
301
+
302
+ if (first_name, second_name) in opposite_pairs:
303
+ continue
304
+
305
+ aspect = get_aspect_from_two_points(
306
+ filtered_settings,
307
+ active_points_list[first]["abs_pos"],
308
+ active_points_list[second]["abs_pos"]
309
+ )
310
+
311
+ if aspect["verdict"]:
312
+ # Get planet IDs using lookup dictionary for better performance
313
+ first_planet_id = planet_id_lookup.get(first_name, 0)
314
+ second_planet_id = planet_id_lookup.get(second_name, 0)
315
+
316
+ aspect_model = AspectModel(
317
+ p1_name=first_name,
318
+ p1_owner=subject.name,
319
+ p1_abs_pos=active_points_list[first]["abs_pos"],
320
+ p2_name=second_name,
321
+ p2_owner=subject.name,
322
+ p2_abs_pos=active_points_list[second]["abs_pos"],
323
+ aspect=aspect["name"],
324
+ orbit=aspect["orbit"],
325
+ aspect_degrees=aspect["aspect_degrees"],
326
+ diff=aspect["diff"],
327
+ p1=first_planet_id,
328
+ p2=second_planet_id,
329
+ )
330
+ all_aspects_list.append(aspect_model)
331
+
332
+ return all_aspects_list
333
+
334
+ @staticmethod
335
+ def _calculate_dual_chart_aspects(
336
+ first_subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
337
+ second_subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
338
+ active_points: List[AstrologicalPoint],
339
+ active_aspects: List[ActiveAspect],
340
+ aspects_settings: List[dict],
341
+ celestial_points: List[dict]
342
+ ) -> List[AspectModel]:
343
+ """
344
+ Calculate all aspects between two charts.
345
+
346
+ This method performs comprehensive aspect calculations between all active points
347
+ of both subjects, applying the specified orb settings and creating detailed
348
+ aspect models with planet IDs and positional information.
349
+ Works with any chart types (synastry, transits, comparisons, etc.).
350
+
351
+ Args:
352
+ first_subject: First astrological subject
353
+ second_subject: Second astrological subject
354
+ active_points: List of celestial points to include in calculations
355
+ active_aspects: List of aspect types with their orb settings
356
+ aspects_settings: Base aspect configuration settings
357
+ celestial_points: Celestial points configuration with IDs
358
+
359
+ Returns:
360
+ List[AspectModel]: Complete list of all calculated aspect instances
361
+ """
362
+ # Get active points lists for both subjects
363
+ first_active_points_list = get_active_points_list(first_subject, active_points)
364
+ second_active_points_list = get_active_points_list(second_subject, active_points)
365
+
366
+ # Create a lookup dictionary for planet IDs to optimize performance
367
+ planet_id_lookup = {planet["name"]: planet["id"] for planet in celestial_points}
368
+
369
+ # Update aspects settings with active aspects orbs
370
+ filtered_settings = AspectsFactory._update_aspect_settings(aspects_settings, active_aspects)
371
+
372
+ all_aspects_list = []
373
+ for first in range(len(first_active_points_list)):
374
+ # Generate aspects list between all points of first and second subjects
375
+ for second in range(len(second_active_points_list)):
376
+ aspect = get_aspect_from_two_points(
377
+ filtered_settings,
378
+ first_active_points_list[first]["abs_pos"],
379
+ second_active_points_list[second]["abs_pos"],
380
+ )
381
+
382
+ if aspect["verdict"]:
383
+ first_name = first_active_points_list[first]["name"]
384
+ second_name = second_active_points_list[second]["name"]
385
+
386
+ # Get planet IDs using lookup dictionary for better performance
387
+ first_planet_id = planet_id_lookup.get(first_name, 0)
388
+ second_planet_id = planet_id_lookup.get(second_name, 0)
389
+
390
+ aspect_model = AspectModel(
391
+ p1_name=first_name,
392
+ p1_owner=first_subject.name,
393
+ p1_abs_pos=first_active_points_list[first]["abs_pos"],
394
+ p2_name=second_name,
395
+ p2_owner=second_subject.name,
396
+ p2_abs_pos=second_active_points_list[second]["abs_pos"],
397
+ aspect=aspect["name"],
398
+ orbit=aspect["orbit"],
399
+ aspect_degrees=aspect["aspect_degrees"],
400
+ diff=aspect["diff"],
401
+ p1=first_planet_id,
402
+ p2=second_planet_id,
403
+ )
404
+ all_aspects_list.append(aspect_model)
405
+
406
+ return all_aspects_list
407
+
408
+ @staticmethod
409
+ def _update_aspect_settings(
410
+ aspects_settings: List[dict],
411
+ active_aspects: List[ActiveAspect]
412
+ ) -> List[dict]:
413
+ """
414
+ Update aspects settings with active aspects orbs.
415
+
416
+ This is a common utility method used by both single chart and dual chart calculations.
417
+
418
+ Args:
419
+ aspects_settings: Base aspect settings
420
+ active_aspects: Active aspects with their orb configurations
421
+
422
+ Returns:
423
+ List of filtered and updated aspect settings
424
+ """
425
+ filtered_settings = []
426
+ for aspect_setting in aspects_settings:
427
+ for active_aspect in active_aspects:
428
+ if aspect_setting["name"] == active_aspect["name"]:
429
+ aspect_setting = aspect_setting.copy() # Don't modify original
430
+ aspect_setting["orb"] = active_aspect["orb"]
431
+ filtered_settings.append(aspect_setting)
432
+ break
433
+ return filtered_settings
434
+
435
+ @staticmethod
436
+ def _filter_relevant_aspects(all_aspects: List[AspectModel], axes_orbit_settings: float) -> List[AspectModel]:
437
+ """
438
+ Filter aspects based on orb thresholds for axes and comprehensive criteria.
439
+
440
+ This method consolidates all filtering logic including axes checks and orb thresholds
441
+ for both single chart and dual chart aspects in a single comprehensive filtering method.
442
+
443
+ Args:
444
+ all_aspects: Complete list of calculated aspects
445
+ axes_orbit_settings: Orb threshold for axes aspects
446
+
447
+ Returns:
448
+ Filtered list of relevant aspects
449
+ """
450
+ logging.debug("Calculating relevant aspects by filtering orbs...")
451
+
452
+ relevant_aspects = []
453
+
454
+ for aspect in all_aspects:
455
+ # Check if aspect involves any of the chart axes and apply stricter orb limits
456
+ aspect_involves_axes = (aspect.p1_name in AXES_LIST or aspect.p2_name in AXES_LIST)
457
+
458
+ if aspect_involves_axes and abs(aspect.orbit) >= axes_orbit_settings:
459
+ continue
460
+
461
+ relevant_aspects.append(aspect)
462
+
463
+ return relevant_aspects
464
+
465
+ # Legacy methods for temporary backward compatibility
466
+ @staticmethod
467
+ def natal_aspects(
468
+ subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
469
+ *,
470
+ active_points: Optional[List[AstrologicalPoint]] = None,
471
+ active_aspects: Optional[List[ActiveAspect]] = None,
472
+ ) -> NatalAspectsModel:
473
+ """
474
+ Legacy method - use single_chart_aspects() instead.
475
+
476
+ ⚠️ DEPRECATION WARNING ⚠️
477
+ This method is deprecated. Use AspectsFactory.single_chart_aspects() instead.
478
+ """
479
+ return AspectsFactory.single_chart_aspects(subject, active_points=active_points, active_aspects=active_aspects)
480
+
481
+ @staticmethod
482
+ def synastry_aspects(
483
+ first_subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
484
+ second_subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
485
+ *,
486
+ active_points: Optional[List[AstrologicalPoint]] = None,
487
+ active_aspects: Optional[List[ActiveAspect]] = None,
488
+ ) -> SynastryAspectsModel:
489
+ """
490
+ Legacy method - use dual_chart_aspects() instead.
491
+
492
+ ⚠️ DEPRECATION WARNING ⚠️
493
+ This method is deprecated. Use AspectsFactory.dual_chart_aspects() instead.
494
+ """
495
+ return AspectsFactory.dual_chart_aspects(
496
+ first_subject, second_subject, active_points=active_points, active_aspects=active_aspects
497
+ )
498
+
499
+
500
+ if __name__ == "__main__":
501
+ from kerykeion.utilities import setup_logging
502
+
503
+ setup_logging(level="debug")
504
+
505
+ # Test single chart aspects (replaces natal aspects)
506
+ johnny = AstrologicalSubjectFactory.from_birth_data("Johnny Depp", 1963, 6, 9, 0, 0, city="Owensboro", nation="US")
507
+ single_chart_aspects = AspectsFactory.single_chart_aspects(johnny)
508
+ print(f"Single chart aspects - All: {len(single_chart_aspects.all_aspects)}, Relevant: {len(single_chart_aspects.relevant_aspects)}")
509
+
510
+ # Test dual chart aspects (replaces synastry aspects)
511
+ john = AstrologicalSubjectFactory.from_birth_data("John", 1940, 10, 9, 10, 30, "Liverpool", "GB")
512
+ yoko = AstrologicalSubjectFactory.from_birth_data("Yoko", 1933, 2, 18, 10, 30, "Tokyo", "JP")
513
+ dual_chart_aspects = AspectsFactory.dual_chart_aspects(john, yoko)
514
+ print(f"Dual chart aspects - All: {len(dual_chart_aspects.all_aspects)}, Relevant: {len(dual_chart_aspects.relevant_aspects)}")
@@ -345,7 +345,7 @@ class AstrologicalSubjectFactory:
345
345
  ... city="Rome", nation="IT",
346
346
  ... online=True
347
347
  ... )
348
- >>> print(f"Sun: {subject.sun.sign} {subject.sun.degree_ut}°")
348
+ >>> print(f"Sun: {subject.sun.sign} {subject.sun.abs_pos}°")
349
349
  >>> print(f"Active points: {len(subject.active_points)}")
350
350
 
351
351
  >>> # Create chart for current time
@@ -1496,7 +1496,7 @@ class AstrologicalSubjectFactory:
1496
1496
  # Calculate Regulus (example fixed star)
1497
1497
  if should_calculate("Regulus"):
1498
1498
  try:
1499
- star_name = b"Regulus"
1499
+ star_name = "Regulus"
1500
1500
  swe.fixstar_ut(star_name, julian_day, iflag)
1501
1501
  regulus_deg = swe.fixstar_ut(star_name, julian_day, iflag)[0][0]
1502
1502
  data["regulus"] = get_kerykeion_point_from_degree(regulus_deg, "Regulus", point_type=point_type)
@@ -1510,7 +1510,7 @@ class AstrologicalSubjectFactory:
1510
1510
  # Calculate Spica (example fixed star)
1511
1511
  if should_calculate("Spica"):
1512
1512
  try:
1513
- star_name = b"Spica"
1513
+ star_name = "Spica"
1514
1514
  swe.fixstar_ut(star_name, julian_day, iflag)
1515
1515
  spica_deg = swe.fixstar_ut(star_name, julian_day, iflag)[0][0]
1516
1516
  data["spica"] = get_kerykeion_point_from_degree(spica_deg, "Spica", point_type=point_type)
@@ -9,8 +9,7 @@ import swisseph as swe
9
9
  from typing import get_args, Union, Optional
10
10
 
11
11
  from kerykeion.settings.kerykeion_settings import get_settings
12
- from kerykeion.aspects.synastry_aspects_factory import SynastryAspectsFactory
13
- from kerykeion.aspects.natal_aspects_factory import NatalAspectsFactory
12
+ from kerykeion.aspects import AspectsFactory
14
13
  from kerykeion.house_comparison.house_comparison_factory import HouseComparisonFactory
15
14
  from kerykeion.kr_types import (
16
15
  KerykeionException,
@@ -312,12 +311,12 @@ class KerykeionChartSVG:
312
311
  raise KerykeionException("First object must be an AstrologicalSubjectModel or AstrologicalSubject instance.")
313
312
 
314
313
  # Calculate aspects
315
- natal_aspects_instance = NatalAspectsFactory.from_subject(
314
+ aspects_instance = AspectsFactory.single_chart_aspects(
316
315
  self.first_obj,
317
316
  active_points=self.active_points,
318
317
  active_aspects=active_aspects,
319
318
  )
320
- self.aspects_list = natal_aspects_instance.relevant_aspects
319
+ self.aspects_list = aspects_instance.relevant_aspects
321
320
 
322
321
  # Screen size
323
322
  self.height = self._DEFAULT_HEIGHT
@@ -346,7 +345,7 @@ class KerykeionChartSVG:
346
345
  raise KerykeionException("First object must be a CompositeSubjectModel instance.")
347
346
 
348
347
  # Calculate aspects
349
- self.aspects_list = NatalAspectsFactory.from_subject(self.first_obj, active_points=self.active_points).relevant_aspects
348
+ self.aspects_list = AspectsFactory.single_chart_aspects(self.first_obj, active_points=self.active_points).relevant_aspects
350
349
 
351
350
  # Screen size
352
351
  self.height = self._DEFAULT_HEIGHT
@@ -377,7 +376,7 @@ class KerykeionChartSVG:
377
376
  self.second_obj = second_obj
378
377
 
379
378
  # Calculate aspects (transit to natal)
380
- synastry_aspects_instance = SynastryAspectsFactory.from_subjects(
379
+ synastry_aspects_instance = AspectsFactory.dual_chart_aspects(
381
380
  self.first_obj,
382
381
  self.second_obj,
383
382
  active_points=self.active_points,
@@ -421,7 +420,7 @@ class KerykeionChartSVG:
421
420
  self.second_obj = second_obj
422
421
 
423
422
  # Calculate aspects (natal to partner)
424
- synastry_aspects_instance = SynastryAspectsFactory.from_subjects(
423
+ synastry_aspects_instance = AspectsFactory.dual_chart_aspects(
425
424
  self.first_obj,
426
425
  self.second_obj,
427
426
  active_points=self.active_points,
@@ -461,7 +460,7 @@ class KerykeionChartSVG:
461
460
  self.second_obj = second_obj
462
461
 
463
462
  # Calculate aspects (natal to return)
464
- synastry_aspects_instance = SynastryAspectsFactory.from_subjects(
463
+ synastry_aspects_instance = AspectsFactory.dual_chart_aspects(
465
464
  self.first_obj,
466
465
  self.second_obj,
467
466
  active_points=self.active_points,
@@ -494,12 +493,12 @@ class KerykeionChartSVG:
494
493
  raise KerykeionException("First object must be an AstrologicalSubjectModel or AstrologicalSubject instance.")
495
494
 
496
495
  # Calculate aspects
497
- natal_aspects_instance = NatalAspectsFactory.from_subject(
496
+ aspects_instance = AspectsFactory.single_chart_aspects(
498
497
  self.first_obj,
499
498
  active_points=self.active_points,
500
499
  active_aspects=active_aspects,
501
500
  )
502
- self.aspects_list = natal_aspects_instance.relevant_aspects
501
+ self.aspects_list = aspects_instance.relevant_aspects
503
502
 
504
503
  # Screen size
505
504
  self.height = self._DEFAULT_HEIGHT