kerykeion 5.1.9__py3-none-any.whl → 5.1.12__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.

@@ -330,12 +330,25 @@ class AspectsFactory:
330
330
  first_planet_id = planet_id_lookup.get(first_name, 0)
331
331
  second_planet_id = planet_id_lookup.get(second_name, 0)
332
332
 
333
- # Calculate aspect movement (applying/separating/exact)
334
- aspect_movement = calculate_aspect_movement(
335
- active_points_list[first]["abs_pos"],
336
- active_points_list[second]["abs_pos"],
337
- aspect["aspect_degrees"]
338
- )
333
+ # Determine aspect movement.
334
+ # If both points are chart axes, there is no meaningful
335
+ # dynamic movement between them, so we mark the aspect as
336
+ # "Fixed" regardless of any synthetic speeds.
337
+ if first_name in AXES_LIST and second_name in AXES_LIST:
338
+ aspect_movement = "Fixed"
339
+ else:
340
+ # Get speeds, fall back to 0.0 only if missing/None
341
+ first_speed = active_points_list[first].get("speed") or 0.0
342
+ second_speed = active_points_list[second].get("speed") or 0.0
343
+
344
+ # Calculate aspect movement (applying/separating/fixed)
345
+ aspect_movement = calculate_aspect_movement(
346
+ active_points_list[first]["abs_pos"],
347
+ active_points_list[second]["abs_pos"],
348
+ aspect["aspect_degrees"],
349
+ first_speed,
350
+ second_speed
351
+ )
339
352
 
340
353
  aspect_model = AspectModel(
341
354
  p1_name=first_name,
@@ -412,12 +425,24 @@ class AspectsFactory:
412
425
  first_planet_id = planet_id_lookup.get(first_name, 0)
413
426
  second_planet_id = planet_id_lookup.get(second_name, 0)
414
427
 
415
- # Calculate aspect movement (applying/separating/exact)
416
- aspect_movement = calculate_aspect_movement(
417
- first_active_points_list[first]["abs_pos"],
418
- second_active_points_list[second]["abs_pos"],
419
- aspect["aspect_degrees"]
420
- )
428
+ # For aspects between axes (ASC, MC, DSC, IC) in different charts
429
+ # there is no meaningful dynamic movement between two house systems,
430
+ # so we mark the movement as "Fixed".
431
+ if first_name in AXES_LIST and second_name in AXES_LIST:
432
+ aspect_movement = "Fixed"
433
+ else:
434
+ # Get speeds, fall back to 0.0 only if missing/None
435
+ first_speed = first_active_points_list[first].get("speed") or 0.0
436
+ second_speed = second_active_points_list[second].get("speed") or 0.0
437
+
438
+ # Calculate aspect movement (applying/separating/fixed)
439
+ aspect_movement = calculate_aspect_movement(
440
+ first_active_points_list[first]["abs_pos"],
441
+ second_active_points_list[second]["abs_pos"],
442
+ aspect["aspect_degrees"],
443
+ first_speed,
444
+ second_speed
445
+ )
421
446
 
422
447
  aspect_model = AspectModel(
423
448
  p1_name=first_name,
@@ -565,4 +590,3 @@ if __name__ == "__main__":
565
590
  yoko = AstrologicalSubjectFactory.from_birth_data("Yoko", 1933, 2, 18, 10, 30, "Tokyo", "JP")
566
591
  dual_chart_aspects = AspectsFactory.dual_chart_aspects(john, yoko)
567
592
  print(f"Dual chart aspects: {len(dual_chart_aspects.aspects)}")
568
-
@@ -56,75 +56,182 @@ def get_aspect_from_two_points(
56
56
  }
57
57
 
58
58
 
59
+ def _sign(x: float) -> int:
60
+ """
61
+ Return the sign of ``x``.
62
+
63
+ Returns +1 if ``x`` > 0, -1 if ``x`` < 0, and 0 if ``x`` == 0.
64
+ """
65
+ if x > 0:
66
+ return 1
67
+ elif x < 0:
68
+ return -1
69
+ else:
70
+ return 0
71
+
72
+
59
73
  def calculate_aspect_movement(
60
74
  point_one_abs_pos: float,
61
75
  point_two_abs_pos: float,
62
- aspect_degrees: int,
63
- exact_orb_threshold: float = 0.05
76
+ aspect_degrees: float,
77
+ point_one_speed: float,
78
+ point_two_speed: float,
64
79
  ) -> AspectMovementType:
65
80
  """
66
- Calculate whether an aspect is applying, separating, or exact.
81
+ Determine whether an aspect is applying, separating, or fixed.
67
82
 
68
- An aspect is:
69
- - "Exact": When the orb is very tight (default < 0.17°)
70
- - "Applying": When the faster planet is moving toward the exact aspect
71
- - "Separating": When the faster planet is moving away from the exact aspect
83
+ This implementation uses a dynamic definition based on the time evolution
84
+ of the orb:
72
85
 
73
- For simplicity, we assume the first planet (p1) is faster than the second (p2).
74
- This is generally true for:
75
- - Moon vs outer planets
76
- - Inner planets vs outer planets
77
- - Transits (transiting planet vs natal planet)
86
+ - "Applying": the orb is decreasing with time (the aspect is moving toward
87
+ exactness).
88
+ - "Separating": the orb is increasing with time (the aspect has already
89
+ perfected in the past, or will not perfect given the current motion).
90
+ - "Fixed": both points are effectively fixed so the orb does not change.
78
91
 
79
- Args:
80
- point_one_abs_pos: Absolute position of first point (0-360°)
81
- point_two_abs_pos: Absolute position of second point (0-360°)
82
- aspect_degrees: The exact degree of the aspect (0, 60, 90, 120, 180, etc.)
83
- exact_orb_threshold: Maximum orb to consider aspect "exact" (default 0.17°)
92
+ Motion direction (direct or retrograde) is taken from the sign of the
93
+ speed values:
84
94
 
85
- Returns:
86
- "Applying", "Separating", or "Exact"
87
-
88
- Example:
89
- >>> # Moon at 45° applying to Sun at 50° (conjunction at 0°/360°)
90
- >>> calculate_aspect_movement(45, 50, 0)
91
- 'Applying'
92
- >>> # Moon at 55° separating from Sun at 50° (conjunction)
93
- >>> calculate_aspect_movement(55, 50, 0)
94
- 'Separating'
95
+ - speed > 0: direct motion (increasing longitude)
96
+ - speed < 0: retrograde motion (decreasing longitude)
97
+
98
+ The algorithm does not assume which point is "faster" in absolute terms;
99
+ it uses the relative motion to determine whether the orb is growing or
100
+ shrinking.
101
+
102
+ Definitions:
103
+
104
+ - Longitudes are in degrees, 0 <= λ < 360 (normalized internally).
105
+ - ``aspect_degrees`` is the nominal angle of the aspect; values > 180
106
+ are made symmetric (e.g. 240° becomes 120°).
107
+ - The orb is defined as::
108
+
109
+ orb = abs(abs(separation) - aspect)
110
+
111
+ where ``separation`` is the minimal angular distance between the two
112
+ points in [-180, 180).
113
+
114
+ Logical steps:
115
+
116
+ 1. Normalize longitudes to [0, 360) and the aspect to [0, 180].
117
+ 2. Compute the signed separation ``sep`` between the two points using
118
+ ``difdeg2n``.
119
+ 3. Compute the current orb::
120
+
121
+ sep_abs = abs(sep)
122
+ orb = abs(sep_abs - aspect)
123
+
124
+ 4. Compute the relevant speed:
125
+ - if both points move: ``moving_speed = point_two_speed - point_one_speed``
126
+ - if one point is fixed (speed == 0): use the moving point speed
127
+ against the fixed point.
128
+ 5. The qualitative sign of the orb derivative is::
129
+
130
+ sign_d_orb = sign(sep_abs - aspect) * sign(sep) * sign(moving_speed)
131
+
132
+ If ``sign_d_orb < 0`` the orb decreases (applying), if
133
+ ``sign_d_orb > 0`` the orb increases (separating).
134
+ 6. If the relevant speed is exactly zero, the orb does not change over
135
+ time; if it is not exact, it is considered separating by convention.
136
+
137
+ Args:
138
+ point_one_abs_pos: Absolute longitude of the first point in degrees.
139
+ point_two_abs_pos: Absolute longitude of the second point in degrees.
140
+ aspect_degrees: Nominal aspect angle (e.g. 0, 60, 90, 120, 180).
141
+ point_one_speed: Speed of the first point in degrees/day (signed).
142
+ point_two_speed: Speed of the second point in degrees/day (signed).
143
+
144
+ Returns:
145
+ AspectMovementType: ``"Applying"``, ``"Separating"`` or ``"Fixed"``.
146
+
147
+ Raises:
148
+ ValueError: If any of the speeds is ``None``.
149
+
150
+ Examples:
151
+ >>> # Fast Moon at 45° approaching Sun at 50° (conjunction)
152
+ >>> calculate_aspect_movement(45, 50, 0, 12.5, 1.0)
153
+ 'Applying'
154
+
155
+ >>> # Moon at 55° moving away from Sun at 50° (conjunction)
156
+ >>> calculate_aspect_movement(55, 50, 0, 12.5, 1.0)
157
+ 'Separating'
158
+
159
+ >>> # Mercury (fast) square Mars
160
+ >>> calculate_aspect_movement(5, 100, 90, 1.5, 0.5)
161
+ 'Applying'
162
+
163
+ >>> # Venus separating from a trine to Jupiter
164
+ >>> calculate_aspect_movement(5, 127, 120, 0.1, 1.2)
165
+ 'Separating'
166
+
167
+ >>> # Retrograde Mars applying to a conjunction with direct Jupiter
168
+ >>> calculate_aspect_movement(110, 100, 0, -0.8, 0.1)
169
+ 'Applying'
95
170
  """
96
171
 
97
- # Calculate the angular distance
98
- distance = abs(difdeg2n(point_one_abs_pos, point_two_abs_pos))
99
- orbit = abs(distance - aspect_degrees)
172
+ if point_one_speed is None or point_two_speed is None:
173
+ raise ValueError(
174
+ "Speed values for both points are required to compute aspect "
175
+ "movement correctly. point_one_speed and point_two_speed "
176
+ "cannot be None."
177
+ )
100
178
 
101
- # Check if aspect is exact (within tight orb)
102
- if orbit <= exact_orb_threshold:
103
- return "Exact"
179
+ # Normalize longitudes to [0, 360)
180
+ p1 = point_one_abs_pos % 360.0
181
+ p2 = point_two_abs_pos % 360.0
104
182
 
105
- # Calculate if p1 is ahead or behind p2 relative to the aspect
106
- # We need to determine the direction of movement
107
- diff = difdeg2n(point_one_abs_pos, point_two_abs_pos)
183
+ # Normalize aspect to [0, 360) and then to [0, 180]
184
+ aspect = aspect_degrees % 360.0
185
+ if aspect > 180.0:
186
+ aspect = 360.0 - aspect # es. 240 -> 120
108
187
 
109
- # For conjunction (0°) or opposition (180°)
110
- if aspect_degrees == 0 or aspect_degrees == 360:
111
- # If p1 is behind p2 (negative diff), it's applying
112
- # If p1 is ahead of p2 (positive diff), it's separating
113
- return "Applying" if diff < 0 else "Separating"
188
+ # Special case: if one of the two points is effectively fixed (speed = 0),
189
+ # such as chart angles, we consider only the motion of the moving point
190
+ # relative to the fixed one. The order of the points must not affect
191
+ # the result.
192
+ if point_one_speed == 0 and point_two_speed == 0:
193
+ # Both points are fixed, the aspect never changes dynamically
194
+ return "Fixed"
114
195
 
115
- elif aspect_degrees == 180:
116
- # For opposition, the logic is reversed
117
- return "Applying" if abs(diff) < 180 else "Separating"
196
+ # Identify which point is moving and which one is fixed/slower.
197
+ # To correctly handle axes we always compute with respect to the
198
+ # moving point.
199
+ if point_one_speed == 0:
200
+ # point_one is fixed (e.g. axis), point_two moves
201
+ moving_pos = p2
202
+ fixed_pos = p1
203
+ moving_speed = point_two_speed
204
+ sep = difdeg2n(moving_pos, fixed_pos)
205
+
206
+ elif point_two_speed == 0:
207
+ # point_two is fixed (e.g. axis), point_one moves
208
+ moving_pos = p1
209
+ fixed_pos = p2
210
+ moving_speed = point_one_speed
211
+ sep = difdeg2n(moving_pos, fixed_pos)
118
212
 
119
213
  else:
120
- # For other aspects (60°, 90°, 120°, 150°)
121
- # Check if the distance is increasing or decreasing
122
- # If distance < aspect_degrees and diff < 0: applying
123
- # If distance > aspect_degrees or diff > 0: separating
124
- if abs(diff) < aspect_degrees:
125
- return "Applying" if diff < 0 else "Separating"
126
- else:
127
- return "Separating" if diff > 0 else "Applying"
214
+ # Both points move: use relative speed and standard separation
215
+ sep = difdeg2n(p2, p1)
216
+ moving_speed = point_two_speed - point_one_speed
217
+
218
+ sep_abs = abs(sep)
219
+
220
+ # If the (relative or absolute) speed is zero, the orb does not change
221
+ if moving_speed == 0:
222
+ return "Separating"
223
+
224
+ # Signs used to determine whether the orb grows or shrinks
225
+ sign_sep = _sign(sep) # sign of the separation
226
+ sign_delta = _sign(sep_abs - aspect) # whether we are "before" or "beyond" the exact aspect
227
+ sign_rel = _sign(moving_speed) # direction in which the separation evolves
228
+
229
+ # Qualitative sign of the orb derivative (d(orb)/dt):
230
+ # < 0 -> orb decreases -> Applying
231
+ # > 0 -> orb increases -> Separating
232
+ orb_derivative_sign = sign_delta * sign_sep * sign_rel
233
+
234
+ return "Applying" if orb_derivative_sign < 0 else "Separating"
128
235
 
129
236
 
130
237
  def planet_id_decoder(planets_settings: list[KerykeionSettingsCelestialPointModel], name: str) -> int:
@@ -1088,16 +1088,24 @@ class AstrologicalSubjectFactory:
1088
1088
  # Calculate axis points
1089
1089
  point_type = "AstrologicalPoint"
1090
1090
 
1091
+ # NOTE: Swiss Ephemeris does not provide direct speeds for angles (ASC/MC),
1092
+ # but in realtà si muovono molto velocemente rispetto ai pianeti.
1093
+ # Per rappresentare questo in modo coerente, assegniamo ai quattro assi
1094
+ # una speed sintetica fissa, molto più alta di quella planetaria tipica.
1095
+ # Questo permette a calculate_aspect_movement di considerarli sempre come
1096
+ # "più veloci" rispetto ai pianeti quando serve.
1097
+ axis_speed = 360.0 # gradi/giorno, valore simbolico ma coerente
1098
+
1091
1099
  # Calculate Ascendant if needed
1092
1100
  if should_calculate("Ascendant"):
1093
- data["ascendant"] = get_kerykeion_point_from_degree(ascmc[0], "Ascendant", point_type=point_type)
1101
+ data["ascendant"] = get_kerykeion_point_from_degree(ascmc[0], "Ascendant", point_type=point_type, speed=axis_speed)
1094
1102
  data["ascendant"].house = get_planet_house(data["ascendant"].abs_pos, data["_houses_degree_ut"])
1095
1103
  data["ascendant"].retrograde = False
1096
1104
  calculated_axial_cusps.append("Ascendant")
1097
1105
 
1098
1106
  # Calculate Medium Coeli if needed
1099
1107
  if should_calculate("Medium_Coeli"):
1100
- data["medium_coeli"] = get_kerykeion_point_from_degree(ascmc[1], "Medium_Coeli", point_type=point_type)
1108
+ data["medium_coeli"] = get_kerykeion_point_from_degree(ascmc[1], "Medium_Coeli", point_type=point_type, speed=axis_speed)
1101
1109
  data["medium_coeli"].house = get_planet_house(data["medium_coeli"].abs_pos, data["_houses_degree_ut"])
1102
1110
  data["medium_coeli"].retrograde = False
1103
1111
  calculated_axial_cusps.append("Medium_Coeli")
@@ -1105,7 +1113,7 @@ class AstrologicalSubjectFactory:
1105
1113
  # Calculate Descendant if needed
1106
1114
  if should_calculate("Descendant"):
1107
1115
  dsc_deg = math.fmod(ascmc[0] + 180, 360)
1108
- data["descendant"] = get_kerykeion_point_from_degree(dsc_deg, "Descendant", point_type=point_type)
1116
+ data["descendant"] = get_kerykeion_point_from_degree(dsc_deg, "Descendant", point_type=point_type, speed=axis_speed)
1109
1117
  data["descendant"].house = get_planet_house(data["descendant"].abs_pos, data["_houses_degree_ut"])
1110
1118
  data["descendant"].retrograde = False
1111
1119
  calculated_axial_cusps.append("Descendant")
@@ -1113,7 +1121,7 @@ class AstrologicalSubjectFactory:
1113
1121
  # Calculate Imum Coeli if needed
1114
1122
  if should_calculate("Imum_Coeli"):
1115
1123
  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)
1124
+ data["imum_coeli"] = get_kerykeion_point_from_degree(ic_deg, "Imum_Coeli", point_type=point_type, speed=axis_speed)
1117
1125
  data["imum_coeli"].house = get_planet_house(data["imum_coeli"].abs_pos, data["_houses_degree_ut"])
1118
1126
  data["imum_coeli"].retrograde = False
1119
1127
  calculated_axial_cusps.append("Imum_Coeli")
@@ -1463,89 +1471,92 @@ class AstrologicalSubjectFactory:
1463
1471
  # TRANS-NEPTUNIAN OBJECTS
1464
1472
  # ==================
1465
1473
 
1474
+ # For TNOs we compute ecliptic longitude for zodiac placement and
1475
+ # declination from equatorial coordinates, same as other bodies.
1476
+
1466
1477
  # Calculate Eris
1467
1478
  if should_calculate("Eris"):
1468
1479
  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")
1480
+ AstrologicalSubjectFactory._calculate_single_planet(
1481
+ data, "Eris", swe.AST_OFFSET + 136199, julian_day, iflag,
1482
+ houses_degree_ut, point_type, calculated_planets, active_points
1483
+ )
1474
1484
  except Exception as e:
1475
1485
  logging.warning(f"Could not calculate Eris position: {e}")
1476
- active_points.remove("Eris") # Remove if not calculated
1486
+ if "Eris" in active_points:
1487
+ active_points.remove("Eris") # Remove if not calculated
1477
1488
 
1478
1489
  # Calculate Sedna
1479
1490
  if should_calculate("Sedna"):
1480
1491
  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")
1492
+ AstrologicalSubjectFactory._calculate_single_planet(
1493
+ data, "Sedna", swe.AST_OFFSET + 90377, julian_day, iflag,
1494
+ houses_degree_ut, point_type, calculated_planets, active_points
1495
+ )
1486
1496
  except Exception as e:
1487
1497
  logging.warning(f"Could not calculate Sedna position: {e}")
1488
- active_points.remove("Sedna")
1498
+ if "Sedna" in active_points:
1499
+ active_points.remove("Sedna")
1489
1500
 
1490
1501
  # Calculate Haumea
1491
1502
  if should_calculate("Haumea"):
1492
1503
  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")
1504
+ AstrologicalSubjectFactory._calculate_single_planet(
1505
+ data, "Haumea", swe.AST_OFFSET + 136108, julian_day, iflag,
1506
+ houses_degree_ut, point_type, calculated_planets, active_points
1507
+ )
1498
1508
  except Exception as e:
1499
1509
  logging.warning(f"Could not calculate Haumea position: {e}")
1500
- active_points.remove("Haumea") # Remove if not calculated
1510
+ if "Haumea" in active_points:
1511
+ active_points.remove("Haumea") # Remove if not calculated
1501
1512
 
1502
1513
  # Calculate Makemake
1503
1514
  if should_calculate("Makemake"):
1504
1515
  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")
1516
+ AstrologicalSubjectFactory._calculate_single_planet(
1517
+ data, "Makemake", swe.AST_OFFSET + 136472, julian_day, iflag,
1518
+ houses_degree_ut, point_type, calculated_planets, active_points
1519
+ )
1510
1520
  except Exception as e:
1511
1521
  logging.warning(f"Could not calculate Makemake position: {e}")
1512
- active_points.remove("Makemake") # Remove if not calculated
1522
+ if "Makemake" in active_points:
1523
+ active_points.remove("Makemake") # Remove if not calculated
1513
1524
 
1514
1525
  # Calculate Ixion
1515
1526
  if should_calculate("Ixion"):
1516
1527
  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")
1528
+ AstrologicalSubjectFactory._calculate_single_planet(
1529
+ data, "Ixion", swe.AST_OFFSET + 28978, julian_day, iflag,
1530
+ houses_degree_ut, point_type, calculated_planets, active_points
1531
+ )
1522
1532
  except Exception as e:
1523
1533
  logging.warning(f"Could not calculate Ixion position: {e}")
1524
- active_points.remove("Ixion") # Remove if not calculated
1534
+ if "Ixion" in active_points:
1535
+ active_points.remove("Ixion") # Remove if not calculated
1525
1536
 
1526
1537
  # Calculate Orcus
1527
1538
  if should_calculate("Orcus"):
1528
1539
  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")
1540
+ AstrologicalSubjectFactory._calculate_single_planet(
1541
+ data, "Orcus", swe.AST_OFFSET + 90482, julian_day, iflag,
1542
+ houses_degree_ut, point_type, calculated_planets, active_points
1543
+ )
1534
1544
  except Exception as e:
1535
1545
  logging.warning(f"Could not calculate Orcus position: {e}")
1536
- active_points.remove("Orcus") # Remove if not calculated
1546
+ if "Orcus" in active_points:
1547
+ active_points.remove("Orcus") # Remove if not calculated
1537
1548
 
1538
1549
  # Calculate Quaoar
1539
1550
  if should_calculate("Quaoar"):
1540
1551
  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")
1552
+ AstrologicalSubjectFactory._calculate_single_planet(
1553
+ data, "Quaoar", swe.AST_OFFSET + 50000, julian_day, iflag,
1554
+ houses_degree_ut, point_type, calculated_planets, active_points
1555
+ )
1546
1556
  except Exception as e:
1547
1557
  logging.warning(f"Could not calculate Quaoar position: {e}")
1548
- active_points.remove("Quaoar") # Remove if not calculated
1558
+ if "Quaoar" in active_points:
1559
+ active_points.remove("Quaoar") # Remove if not calculated
1549
1560
 
1550
1561
  # ==================
1551
1562
  # FIXED STARS
@@ -1555,33 +1566,41 @@ class AstrologicalSubjectFactory:
1555
1566
  if should_calculate("Regulus"):
1556
1567
  try:
1557
1568
  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
1569
+ # Ecliptic longitude for zodiac placement
1570
+ pos_ecl = swe.fixstar_ut(star_name, julian_day, iflag)[0]
1571
+ regulus_deg = pos_ecl[0]
1572
+ regulus_speed = pos_ecl[3] if len(pos_ecl) > 3 else 0.0 # Fixed stars have very slow speed
1573
+ # Equatorial coordinates for true declination
1574
+ pos_eq = swe.fixstar_ut(star_name, julian_day, iflag | swe.FLG_EQUATORIAL)[0]
1575
+ regulus_dec = pos_eq[1] if len(pos_eq) > 1 else None
1562
1576
  data["regulus"] = get_kerykeion_point_from_degree(regulus_deg, "Regulus", point_type=point_type, speed=regulus_speed, declination=regulus_dec)
1563
1577
  data["regulus"].house = get_planet_house(regulus_deg, houses_degree_ut)
1564
1578
  data["regulus"].retrograde = False # Fixed stars are never retrograde
1565
1579
  calculated_planets.append("Regulus")
1566
1580
  except Exception as e:
1567
1581
  logging.warning(f"Could not calculate Regulus position: {e}")
1568
- active_points.remove("Regulus") # Remove if not calculated
1582
+ if "Regulus" in active_points:
1583
+ active_points.remove("Regulus") # Remove if not calculated
1569
1584
 
1570
1585
  # Calculate Spica (example fixed star)
1571
1586
  if should_calculate("Spica"):
1572
1587
  try:
1573
1588
  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
1589
+ # Ecliptic longitude for zodiac placement
1590
+ pos_ecl = swe.fixstar_ut(star_name, julian_day, iflag)[0]
1591
+ spica_deg = pos_ecl[0]
1592
+ spica_speed = pos_ecl[3] if len(pos_ecl) > 3 else 0.0 # Fixed stars have very slow speed
1593
+ # Equatorial coordinates for true declination
1594
+ pos_eq = swe.fixstar_ut(star_name, julian_day, iflag | swe.FLG_EQUATORIAL)[0]
1595
+ spica_dec = pos_eq[1] if len(pos_eq) > 1 else None
1578
1596
  data["spica"] = get_kerykeion_point_from_degree(spica_deg, "Spica", point_type=point_type, speed=spica_speed, declination=spica_dec)
1579
1597
  data["spica"].house = get_planet_house(spica_deg, houses_degree_ut)
1580
1598
  data["spica"].retrograde = False # Fixed stars are never retrograde
1581
1599
  calculated_planets.append("Spica")
1582
1600
  except Exception as e:
1583
1601
  logging.warning(f"Could not calculate Spica position: {e}")
1584
- active_points.remove("Spica") # Remove if not calculated
1602
+ if "Spica" in active_points:
1603
+ active_points.remove("Spica") # Remove if not calculated
1585
1604
 
1586
1605
  # ==================
1587
1606
  # ARABIC PARTS / LOTS