kerykeion.utilities

  1from kerykeion.kr_types import KerykeionPointModel, KerykeionException, ZodiacSignModel, AstrologicalSubjectModel, LunarPhaseModel
  2from kerykeion.kr_types.kr_literals import LunarPhaseEmoji, LunarPhaseName, PointType, Planet, Houses, AxialCusps
  3from typing import Union, get_args, TYPE_CHECKING
  4import logging
  5import math
  6
  7if TYPE_CHECKING:
  8    from kerykeion import AstrologicalSubject
  9
 10
 11def get_number_from_name(name: Planet) -> int:
 12    """Utility function, gets planet id from the name."""
 13
 14    if name == "Sun":
 15        return 0
 16    elif name == "Moon":
 17        return 1
 18    elif name == "Mercury":
 19        return 2
 20    elif name == "Venus":
 21        return 3
 22    elif name == "Mars":
 23        return 4
 24    elif name == "Jupiter":
 25        return 5
 26    elif name == "Saturn":
 27        return 6
 28    elif name == "Uranus":
 29        return 7
 30    elif name == "Neptune":
 31        return 8
 32    elif name == "Pluto":
 33        return 9
 34    elif name == "Mean_Node":
 35        return 10
 36    elif name == "True_Node":
 37        return 11
 38    # Note: Swiss ephemeris library has no constants for south nodes. We're using integers >= 1000 for them.
 39    elif name == "Mean_South_Node":
 40        return 1000
 41    elif name == "True_South_Node":
 42        return 1100
 43    elif name == "Chiron":
 44        return 15
 45    elif name == "Mean_Lilith":
 46        return 12
 47    elif name == "Ascendant": # TODO: Is this needed?
 48        return 9900
 49    elif name == "Descendant": # TODO: Is this needed?
 50        return 9901
 51    elif name == "Medium_Coeli": # TODO: Is this needed?
 52        return 9902
 53    elif name == "Imum_Coeli": # TODO: Is this needed?
 54        return 9903
 55    else:
 56        raise KerykeionException(f"Error in getting number from name! Name: {name}")
 57
 58
 59def get_kerykeion_point_from_degree(
 60    degree: Union[int, float], name: Union[Planet, Houses, AxialCusps], point_type: PointType
 61) -> KerykeionPointModel:
 62    """
 63    Returns a KerykeionPointModel object based on the given degree.
 64
 65    Args:
 66        degree (Union[int, float]): The degree of the celestial point.
 67        name (str): The name of the celestial point.
 68        point_type (PointType): The type of the celestial point.
 69
 70    Raises:
 71        KerykeionException: If the degree is not within the valid range (0-360).
 72
 73    Returns:
 74        KerykeionPointModel: The model representing the celestial point.
 75    """
 76
 77    if degree < 0 or degree >= 360:
 78        raise KerykeionException(f"Error in calculating positions! Degrees: {degree}")
 79
 80    ZODIAC_SIGNS = {
 81        0: ZodiacSignModel(sign="Ari", quality="Cardinal", element="Fire", emoji="♈️", sign_num=0),
 82        1: ZodiacSignModel(sign="Tau", quality="Fixed", element="Earth", emoji="♉️", sign_num=1),
 83        2: ZodiacSignModel(sign="Gem", quality="Mutable", element="Air", emoji="♊️", sign_num=2),
 84        3: ZodiacSignModel(sign="Can", quality="Cardinal", element="Water", emoji="♋️", sign_num=3),
 85        4: ZodiacSignModel(sign="Leo", quality="Fixed", element="Fire", emoji="♌️", sign_num=4),
 86        5: ZodiacSignModel(sign="Vir", quality="Mutable", element="Earth", emoji="♍️", sign_num=5),
 87        6: ZodiacSignModel(sign="Lib", quality="Cardinal", element="Air", emoji="♎️", sign_num=6),
 88        7: ZodiacSignModel(sign="Sco", quality="Fixed", element="Water", emoji="♏️", sign_num=7),
 89        8: ZodiacSignModel(sign="Sag", quality="Mutable", element="Fire", emoji="♐️", sign_num=8),
 90        9: ZodiacSignModel(sign="Cap", quality="Cardinal", element="Earth", emoji="♑️", sign_num=9),
 91        10: ZodiacSignModel(sign="Aqu", quality="Fixed", element="Air", emoji="♒️", sign_num=10),
 92        11: ZodiacSignModel(sign="Pis", quality="Mutable", element="Water", emoji="♓️", sign_num=11),
 93    }
 94
 95    sign_index = int(degree // 30)
 96    sign_degree = degree % 30
 97    zodiac_sign = ZODIAC_SIGNS[sign_index]
 98
 99    return KerykeionPointModel(
100        name=name,
101        quality=zodiac_sign.quality,
102        element=zodiac_sign.element,
103        sign=zodiac_sign.sign,
104        sign_num=zodiac_sign.sign_num,
105        position=sign_degree,
106        abs_pos=degree,
107        emoji=zodiac_sign.emoji,
108        point_type=point_type,
109    )
110
111def setup_logging(level: str) -> None:
112    """
113    Setup logging for testing.
114
115    Args:
116        level: Log level as a string, options: debug, info, warning, error
117    """
118    logging_options: dict[str, int] = {
119        "debug": logging.DEBUG,
120        "info": logging.INFO,
121        "warning": logging.WARNING,
122        "error": logging.ERROR,
123        "critical": logging.CRITICAL,
124    }
125    format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
126    loglevel: int = logging_options.get(level, logging.INFO)
127    logging.basicConfig(format=format, level=loglevel)
128
129
130def is_point_between(
131    start_point: Union[int, float],
132    end_point: Union[int, float],
133    evaluated_point: Union[int, float]
134) -> bool:
135    """
136    Determines if a point is between two others on a circle, with additional rules:
137    - If evaluated_point == start_point, it is considered between.
138    - If evaluated_point == end_point, it is NOT considered between.
139    - The range between start_point and end_point must not exceed 180°.
140
141    Args:
142        - start_point: The first point on the circle.
143        - end_point: The second point on the circle.
144        - evaluated_point: The point to check.
145
146    Returns:
147        - True if evaluated_point is between start_point and end_point, False otherwise.
148    """
149
150    # Normalize angles to [0, 360)
151    start_point = start_point % 360
152    end_point = end_point % 360
153    evaluated_point = evaluated_point % 360
154
155    # Compute angular difference
156    angular_difference = math.fmod(end_point - start_point + 360, 360)
157
158    # Ensure the range is not greater than 180°. Otherwise, it is not truly defined what
159    # being located in between two points on a circle actually means.
160    if angular_difference > 180:
161        raise KerykeionException(f"The angle between start and end point is not allowed to exceed 180°, yet is: {angular_difference}")
162
163    # Handle explicitly when evaluated_point == start_point. Note: It may happen for mathematical
164    # reasons that evaluated_point and start_point deviate very slightly from each other, but
165    # should really be same value. This case is captured later below by the term 0 <= p1_p3.
166    if evaluated_point == start_point:
167        return True
168
169    # Handle explicitly when evaluated_point == end_point
170    if evaluated_point == end_point:
171        return False
172
173    # Compute angular differences for evaluation
174    p1_p3 = math.fmod(evaluated_point - start_point + 360, 360)
175
176    # Check if point lies in the interval
177    return (0 <= p1_p3) and (p1_p3 < angular_difference)
178
179
180
181def get_planet_house(planet_position_degree: Union[int, float], houses_degree_ut_list: list) -> Houses:
182    """
183    Determines the house in which a planet is located based on its position in degrees.
184
185    Args:
186        planet_position_degree (Union[int, float]): The position of the planet in degrees.
187        houses_degree_ut_list (list): A list of the houses in degrees (0-360).
188
189    Returns:
190        str: The house in which the planet is located.
191
192    Raises:
193        ValueError: If the planet's position does not fall within any house range.
194    """
195
196    house_names = get_args(Houses)
197
198    # Iterate through the house boundaries to find the correct house
199    for i in range(len(house_names)):
200        start_degree = houses_degree_ut_list[i]
201        end_degree = houses_degree_ut_list[(i + 1) % len(houses_degree_ut_list)]
202
203        if is_point_between(start_degree, end_degree, planet_position_degree):
204            return house_names[i]
205
206    # If no house is found, raise an error
207    raise ValueError(f"Error in house calculation, planet: {planet_position_degree}, houses: {houses_degree_ut_list}")
208
209
210def get_moon_emoji_from_phase_int(phase: int) -> LunarPhaseEmoji:
211    """
212    Returns the emoji of the moon phase.
213
214    Args:
215        - phase: The phase of the moon (0-28)
216
217    Returns:
218        - The emoji of the moon phase
219    """
220
221    lunar_phase_emojis = get_args(LunarPhaseEmoji)
222
223    if phase == 1:
224        result = lunar_phase_emojis[0]
225    elif phase < 7:
226        result = lunar_phase_emojis[1]
227    elif 7 <= phase <= 9:
228        result = lunar_phase_emojis[2]
229    elif phase < 14:
230        result = lunar_phase_emojis[3]
231    elif phase == 14:
232        result = lunar_phase_emojis[4]
233    elif phase < 20:
234        result = lunar_phase_emojis[5]
235    elif 20 <= phase <= 22:
236        result = lunar_phase_emojis[6]
237    elif phase <= 28:
238        result = lunar_phase_emojis[7]
239
240    else:
241        raise KerykeionException(f"Error in moon emoji calculation! Phase: {phase}")
242
243    return result
244
245def get_moon_phase_name_from_phase_int(phase: int) -> LunarPhaseName:
246    """
247    Returns the name of the moon phase.
248
249    Args:
250        - phase: The phase of the moon (0-28)
251
252    Returns:
253        - The name of the moon phase
254    """
255    lunar_phase_names = get_args(LunarPhaseName)
256
257
258    if phase == 1:
259        result = lunar_phase_names[0]
260    elif phase < 7:
261        result =  lunar_phase_names[1]
262    elif 7 <= phase <= 9:
263        result = lunar_phase_names[2]
264    elif phase < 14:
265        result = lunar_phase_names[3]
266    elif phase == 14:
267        result = lunar_phase_names[4]
268    elif phase < 20:
269        result = lunar_phase_names[5]
270    elif 20 <= phase <= 22:
271        result = lunar_phase_names[6]
272    elif phase <= 28:
273        result = lunar_phase_names[7]
274
275    else:
276        raise KerykeionException(f"Error in moon name calculation! Phase: {phase}")
277
278    return result
279
280
281def check_and_adjust_polar_latitude(latitude: float) -> float:
282    """
283        Utility function to check if the location is in the polar circle.
284        If it is, it sets the latitude to 66 or -66 degrees.
285    """
286    if latitude > 66.0:
287        latitude = 66.0
288        logging.info("Polar circle override for houses, using 66 degrees")
289
290    elif latitude < -66.0:
291        latitude = -66.0
292        logging.info("Polar circle override for houses, using -66 degrees")
293
294    return latitude
295
296
297def get_houses_list(subject: Union["AstrologicalSubject", AstrologicalSubjectModel]) -> list[KerykeionPointModel]:
298    """
299    Return the names of the houses in the order of the houses.
300    """
301    houses_absolute_position_list = []
302    for house in subject.houses_names_list:
303            houses_absolute_position_list.append(subject[house.lower()])
304
305    return houses_absolute_position_list
306
307
308def get_available_astrological_points_list(subject: Union["AstrologicalSubject", AstrologicalSubjectModel]) -> list[KerykeionPointModel]:
309    """
310    Return the names of the planets in the order of the planets.
311    The names can be used to access the planets from the AstrologicalSubject object with the __getitem__ method or the [] operator.
312    """
313    planets_absolute_position_list = []
314    for planet in subject.planets_names_list:
315            planets_absolute_position_list.append(subject[planet.lower()])
316
317    for axis in subject.axial_cusps_names_list:
318        planets_absolute_position_list.append(subject[axis.lower()])
319
320    return planets_absolute_position_list
321
322
323def circular_mean(first_position: Union[int, float], second_position: Union[int, float]) -> float:
324    """
325    Computes the circular mean of two astrological positions (e.g., house cusps, planets).
326
327    This function ensures that positions crossing 0° Aries (360°) are correctly averaged,
328    avoiding errors that occur with simple linear means.
329
330    Args:
331        position1 (Union[int, float]): First position in degrees (0-360).
332        position2 (Union[int, float]): Second position in degrees (0-360).
333
334    Returns:
335        float: The circular mean position in degrees (0-360).
336    """
337    x = (math.cos(math.radians(first_position)) + math.cos(math.radians(second_position))) / 2
338    y = (math.sin(math.radians(first_position)) + math.sin(math.radians(second_position))) / 2
339    mean_position = math.degrees(math.atan2(y, x))
340
341    # Ensure the result is within 0-360°
342    if mean_position < 0:
343        mean_position += 360
344
345    return mean_position
346
347
348def calculate_moon_phase(moon_abs_pos: float, sun_abs_pos: float) -> LunarPhaseModel:
349    """
350    Calculate the lunar phase based on the positions of the moon and sun.
351
352    Args:
353    - moon_abs_pos (float): The absolute position of the moon.
354    - sun_abs_pos (float): The absolute position of the sun.
355
356    Returns:
357    - dict: A dictionary containing the lunar phase information.
358    """
359    # Initialize moon_phase and sun_phase to None in case of an error
360    moon_phase, sun_phase = None, None
361
362    # Calculate the anti-clockwise degrees between the sun and moon
363    degrees_between = (moon_abs_pos - sun_abs_pos) % 360
364
365    # Calculate the moon phase (1-28) based on the degrees between the sun and moon
366    step = 360.0 / 28.0
367    moon_phase = int(degrees_between // step) + 1
368
369    # Define the sun phase steps
370    sunstep = [
371        0, 30, 40, 50, 60, 70, 80, 90, 120, 130, 140, 150, 160, 170, 180,
372        210, 220, 230, 240, 250, 260, 270, 300, 310, 320, 330, 340, 350
373    ]
374
375    # Calculate the sun phase (1-28) based on the degrees between the sun and moon
376    for x in range(len(sunstep)):
377        low = sunstep[x]
378        high = sunstep[x + 1] if x < len(sunstep) - 1 else 360
379        if low <= degrees_between < high:
380            sun_phase = x + 1
381            break
382
383    # Create a dictionary with the lunar phase information
384    lunar_phase_dictionary = {
385        "degrees_between_s_m": degrees_between,
386        "moon_phase": moon_phase,
387        "sun_phase": sun_phase,
388        "moon_emoji": get_moon_emoji_from_phase_int(moon_phase),
389        "moon_phase_name": get_moon_phase_name_from_phase_int(moon_phase)
390    }
391
392    return LunarPhaseModel(**lunar_phase_dictionary)
393
394
395def circular_sort(degrees: list[Union[int, float]]) -> list[Union[int, float]]:
396    """
397    Sort a list of degrees in a circular manner, starting from the first element
398    and progressing clockwise around the circle.
399
400    Args:
401        degrees: A list of numeric values representing degrees
402
403    Returns:
404        A list sorted based on circular clockwise progression from the first element
405
406    Raises:
407        ValueError: If the list is empty or contains non-numeric values
408    """
409    # Input validation
410    if not degrees:
411        raise ValueError("Input list cannot be empty")
412
413    if not all(isinstance(degree, (int, float)) for degree in degrees):
414        invalid = next(d for d in degrees if not isinstance(d, (int, float)))
415        raise ValueError(f"All elements must be numeric, found: {invalid} of type {type(invalid).__name__}")
416
417    # If list has 0 or 1 element, return it as is
418    if len(degrees) <= 1:
419        return degrees.copy()
420
421    # Save the first element as the reference
422    reference = degrees[0]
423
424    # Define a function to calculate clockwise distance from reference
425    def clockwise_distance(angle: Union[int, float]) -> Union[int, float]:
426        # Normalize angles to 0-360 range
427        ref_norm = reference % 360
428        angle_norm = angle % 360
429
430        # Calculate clockwise distance
431        distance = angle_norm - ref_norm
432        if distance < 0:
433            distance += 360
434
435        return distance
436
437    # Sort the rest of the elements based on circular distance
438    remaining = degrees[1:]
439    sorted_remaining = sorted(remaining, key=clockwise_distance)
440
441    # Return the reference followed by the sorted remaining elements
442    return [reference] + sorted_remaining
def get_number_from_name( name: Literal['Sun', 'Moon', 'Mercury', 'Venus', 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune', 'Pluto', 'Mean_Node', 'True_Node', 'Mean_South_Node', 'True_South_Node', 'Chiron', 'Mean_Lilith']) -> int:
12def get_number_from_name(name: Planet) -> int:
13    """Utility function, gets planet id from the name."""
14
15    if name == "Sun":
16        return 0
17    elif name == "Moon":
18        return 1
19    elif name == "Mercury":
20        return 2
21    elif name == "Venus":
22        return 3
23    elif name == "Mars":
24        return 4
25    elif name == "Jupiter":
26        return 5
27    elif name == "Saturn":
28        return 6
29    elif name == "Uranus":
30        return 7
31    elif name == "Neptune":
32        return 8
33    elif name == "Pluto":
34        return 9
35    elif name == "Mean_Node":
36        return 10
37    elif name == "True_Node":
38        return 11
39    # Note: Swiss ephemeris library has no constants for south nodes. We're using integers >= 1000 for them.
40    elif name == "Mean_South_Node":
41        return 1000
42    elif name == "True_South_Node":
43        return 1100
44    elif name == "Chiron":
45        return 15
46    elif name == "Mean_Lilith":
47        return 12
48    elif name == "Ascendant": # TODO: Is this needed?
49        return 9900
50    elif name == "Descendant": # TODO: Is this needed?
51        return 9901
52    elif name == "Medium_Coeli": # TODO: Is this needed?
53        return 9902
54    elif name == "Imum_Coeli": # TODO: Is this needed?
55        return 9903
56    else:
57        raise KerykeionException(f"Error in getting number from name! Name: {name}")

Utility function, gets planet id from the name.

def get_kerykeion_point_from_degree( degree: Union[int, float], name: Union[Literal['Sun', 'Moon', 'Mercury', 'Venus', 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune', 'Pluto', 'Mean_Node', 'True_Node', 'Mean_South_Node', 'True_South_Node', 'Chiron', 'Mean_Lilith'], Literal['First_House', 'Second_House', 'Third_House', 'Fourth_House', 'Fifth_House', 'Sixth_House', 'Seventh_House', 'Eighth_House', 'Ninth_House', 'Tenth_House', 'Eleventh_House', 'Twelfth_House'], Literal['Ascendant', 'Medium_Coeli', 'Descendant', 'Imum_Coeli']], point_type: Literal['Planet', 'House', 'AxialCusps']) -> kerykeion.kr_types.kr_models.KerykeionPointModel:
 60def get_kerykeion_point_from_degree(
 61    degree: Union[int, float], name: Union[Planet, Houses, AxialCusps], point_type: PointType
 62) -> KerykeionPointModel:
 63    """
 64    Returns a KerykeionPointModel object based on the given degree.
 65
 66    Args:
 67        degree (Union[int, float]): The degree of the celestial point.
 68        name (str): The name of the celestial point.
 69        point_type (PointType): The type of the celestial point.
 70
 71    Raises:
 72        KerykeionException: If the degree is not within the valid range (0-360).
 73
 74    Returns:
 75        KerykeionPointModel: The model representing the celestial point.
 76    """
 77
 78    if degree < 0 or degree >= 360:
 79        raise KerykeionException(f"Error in calculating positions! Degrees: {degree}")
 80
 81    ZODIAC_SIGNS = {
 82        0: ZodiacSignModel(sign="Ari", quality="Cardinal", element="Fire", emoji="♈️", sign_num=0),
 83        1: ZodiacSignModel(sign="Tau", quality="Fixed", element="Earth", emoji="♉️", sign_num=1),
 84        2: ZodiacSignModel(sign="Gem", quality="Mutable", element="Air", emoji="♊️", sign_num=2),
 85        3: ZodiacSignModel(sign="Can", quality="Cardinal", element="Water", emoji="♋️", sign_num=3),
 86        4: ZodiacSignModel(sign="Leo", quality="Fixed", element="Fire", emoji="♌️", sign_num=4),
 87        5: ZodiacSignModel(sign="Vir", quality="Mutable", element="Earth", emoji="♍️", sign_num=5),
 88        6: ZodiacSignModel(sign="Lib", quality="Cardinal", element="Air", emoji="♎️", sign_num=6),
 89        7: ZodiacSignModel(sign="Sco", quality="Fixed", element="Water", emoji="♏️", sign_num=7),
 90        8: ZodiacSignModel(sign="Sag", quality="Mutable", element="Fire", emoji="♐️", sign_num=8),
 91        9: ZodiacSignModel(sign="Cap", quality="Cardinal", element="Earth", emoji="♑️", sign_num=9),
 92        10: ZodiacSignModel(sign="Aqu", quality="Fixed", element="Air", emoji="♒️", sign_num=10),
 93        11: ZodiacSignModel(sign="Pis", quality="Mutable", element="Water", emoji="♓️", sign_num=11),
 94    }
 95
 96    sign_index = int(degree // 30)
 97    sign_degree = degree % 30
 98    zodiac_sign = ZODIAC_SIGNS[sign_index]
 99
100    return KerykeionPointModel(
101        name=name,
102        quality=zodiac_sign.quality,
103        element=zodiac_sign.element,
104        sign=zodiac_sign.sign,
105        sign_num=zodiac_sign.sign_num,
106        position=sign_degree,
107        abs_pos=degree,
108        emoji=zodiac_sign.emoji,
109        point_type=point_type,
110    )

Returns a KerykeionPointModel object based on the given degree.

Args: degree (Union[int, float]): The degree of the celestial point. name (str): The name of the celestial point. point_type (PointType): The type of the celestial point.

Raises: KerykeionException: If the degree is not within the valid range (0-360).

Returns: KerykeionPointModel: The model representing the celestial point.

def setup_logging(level: str) -> None:
112def setup_logging(level: str) -> None:
113    """
114    Setup logging for testing.
115
116    Args:
117        level: Log level as a string, options: debug, info, warning, error
118    """
119    logging_options: dict[str, int] = {
120        "debug": logging.DEBUG,
121        "info": logging.INFO,
122        "warning": logging.WARNING,
123        "error": logging.ERROR,
124        "critical": logging.CRITICAL,
125    }
126    format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
127    loglevel: int = logging_options.get(level, logging.INFO)
128    logging.basicConfig(format=format, level=loglevel)

Setup logging for testing.

Args: level: Log level as a string, options: debug, info, warning, error

def is_point_between( start_point: Union[int, float], end_point: Union[int, float], evaluated_point: Union[int, float]) -> bool:
131def is_point_between(
132    start_point: Union[int, float],
133    end_point: Union[int, float],
134    evaluated_point: Union[int, float]
135) -> bool:
136    """
137    Determines if a point is between two others on a circle, with additional rules:
138    - If evaluated_point == start_point, it is considered between.
139    - If evaluated_point == end_point, it is NOT considered between.
140    - The range between start_point and end_point must not exceed 180°.
141
142    Args:
143        - start_point: The first point on the circle.
144        - end_point: The second point on the circle.
145        - evaluated_point: The point to check.
146
147    Returns:
148        - True if evaluated_point is between start_point and end_point, False otherwise.
149    """
150
151    # Normalize angles to [0, 360)
152    start_point = start_point % 360
153    end_point = end_point % 360
154    evaluated_point = evaluated_point % 360
155
156    # Compute angular difference
157    angular_difference = math.fmod(end_point - start_point + 360, 360)
158
159    # Ensure the range is not greater than 180°. Otherwise, it is not truly defined what
160    # being located in between two points on a circle actually means.
161    if angular_difference > 180:
162        raise KerykeionException(f"The angle between start and end point is not allowed to exceed 180°, yet is: {angular_difference}")
163
164    # Handle explicitly when evaluated_point == start_point. Note: It may happen for mathematical
165    # reasons that evaluated_point and start_point deviate very slightly from each other, but
166    # should really be same value. This case is captured later below by the term 0 <= p1_p3.
167    if evaluated_point == start_point:
168        return True
169
170    # Handle explicitly when evaluated_point == end_point
171    if evaluated_point == end_point:
172        return False
173
174    # Compute angular differences for evaluation
175    p1_p3 = math.fmod(evaluated_point - start_point + 360, 360)
176
177    # Check if point lies in the interval
178    return (0 <= p1_p3) and (p1_p3 < angular_difference)

Determines if a point is between two others on a circle, with additional rules:

  • If evaluated_point == start_point, it is considered between.
  • If evaluated_point == end_point, it is NOT considered between.
  • The range between start_point and end_point must not exceed 180°.

Args: - start_point: The first point on the circle. - end_point: The second point on the circle. - evaluated_point: The point to check.

Returns: - True if evaluated_point is between start_point and end_point, False otherwise.

def get_planet_house( planet_position_degree: Union[int, float], houses_degree_ut_list: list) -> Literal['First_House', 'Second_House', 'Third_House', 'Fourth_House', 'Fifth_House', 'Sixth_House', 'Seventh_House', 'Eighth_House', 'Ninth_House', 'Tenth_House', 'Eleventh_House', 'Twelfth_House']:
182def get_planet_house(planet_position_degree: Union[int, float], houses_degree_ut_list: list) -> Houses:
183    """
184    Determines the house in which a planet is located based on its position in degrees.
185
186    Args:
187        planet_position_degree (Union[int, float]): The position of the planet in degrees.
188        houses_degree_ut_list (list): A list of the houses in degrees (0-360).
189
190    Returns:
191        str: The house in which the planet is located.
192
193    Raises:
194        ValueError: If the planet's position does not fall within any house range.
195    """
196
197    house_names = get_args(Houses)
198
199    # Iterate through the house boundaries to find the correct house
200    for i in range(len(house_names)):
201        start_degree = houses_degree_ut_list[i]
202        end_degree = houses_degree_ut_list[(i + 1) % len(houses_degree_ut_list)]
203
204        if is_point_between(start_degree, end_degree, planet_position_degree):
205            return house_names[i]
206
207    # If no house is found, raise an error
208    raise ValueError(f"Error in house calculation, planet: {planet_position_degree}, houses: {houses_degree_ut_list}")

Determines the house in which a planet is located based on its position in degrees.

Args: planet_position_degree (Union[int, float]): The position of the planet in degrees. houses_degree_ut_list (list): A list of the houses in degrees (0-360).

Returns: str: The house in which the planet is located.

Raises: ValueError: If the planet's position does not fall within any house range.

def get_moon_emoji_from_phase_int(phase: int) -> Literal['🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘']:
211def get_moon_emoji_from_phase_int(phase: int) -> LunarPhaseEmoji:
212    """
213    Returns the emoji of the moon phase.
214
215    Args:
216        - phase: The phase of the moon (0-28)
217
218    Returns:
219        - The emoji of the moon phase
220    """
221
222    lunar_phase_emojis = get_args(LunarPhaseEmoji)
223
224    if phase == 1:
225        result = lunar_phase_emojis[0]
226    elif phase < 7:
227        result = lunar_phase_emojis[1]
228    elif 7 <= phase <= 9:
229        result = lunar_phase_emojis[2]
230    elif phase < 14:
231        result = lunar_phase_emojis[3]
232    elif phase == 14:
233        result = lunar_phase_emojis[4]
234    elif phase < 20:
235        result = lunar_phase_emojis[5]
236    elif 20 <= phase <= 22:
237        result = lunar_phase_emojis[6]
238    elif phase <= 28:
239        result = lunar_phase_emojis[7]
240
241    else:
242        raise KerykeionException(f"Error in moon emoji calculation! Phase: {phase}")
243
244    return result

Returns the emoji of the moon phase.

Args: - phase: The phase of the moon (0-28)

Returns: - The emoji of the moon phase

def get_moon_phase_name_from_phase_int( phase: int) -> Literal['New Moon', 'Waxing Crescent', 'First Quarter', 'Waxing Gibbous', 'Full Moon', 'Waning Gibbous', 'Last Quarter', 'Waning Crescent']:
246def get_moon_phase_name_from_phase_int(phase: int) -> LunarPhaseName:
247    """
248    Returns the name of the moon phase.
249
250    Args:
251        - phase: The phase of the moon (0-28)
252
253    Returns:
254        - The name of the moon phase
255    """
256    lunar_phase_names = get_args(LunarPhaseName)
257
258
259    if phase == 1:
260        result = lunar_phase_names[0]
261    elif phase < 7:
262        result =  lunar_phase_names[1]
263    elif 7 <= phase <= 9:
264        result = lunar_phase_names[2]
265    elif phase < 14:
266        result = lunar_phase_names[3]
267    elif phase == 14:
268        result = lunar_phase_names[4]
269    elif phase < 20:
270        result = lunar_phase_names[5]
271    elif 20 <= phase <= 22:
272        result = lunar_phase_names[6]
273    elif phase <= 28:
274        result = lunar_phase_names[7]
275
276    else:
277        raise KerykeionException(f"Error in moon name calculation! Phase: {phase}")
278
279    return result

Returns the name of the moon phase.

Args: - phase: The phase of the moon (0-28)

Returns: - The name of the moon phase

def check_and_adjust_polar_latitude(latitude: float) -> float:
282def check_and_adjust_polar_latitude(latitude: float) -> float:
283    """
284        Utility function to check if the location is in the polar circle.
285        If it is, it sets the latitude to 66 or -66 degrees.
286    """
287    if latitude > 66.0:
288        latitude = 66.0
289        logging.info("Polar circle override for houses, using 66 degrees")
290
291    elif latitude < -66.0:
292        latitude = -66.0
293        logging.info("Polar circle override for houses, using -66 degrees")
294
295    return latitude

Utility function to check if the location is in the polar circle. If it is, it sets the latitude to 66 or -66 degrees.

298def get_houses_list(subject: Union["AstrologicalSubject", AstrologicalSubjectModel]) -> list[KerykeionPointModel]:
299    """
300    Return the names of the houses in the order of the houses.
301    """
302    houses_absolute_position_list = []
303    for house in subject.houses_names_list:
304            houses_absolute_position_list.append(subject[house.lower()])
305
306    return houses_absolute_position_list

Return the names of the houses in the order of the houses.

309def get_available_astrological_points_list(subject: Union["AstrologicalSubject", AstrologicalSubjectModel]) -> list[KerykeionPointModel]:
310    """
311    Return the names of the planets in the order of the planets.
312    The names can be used to access the planets from the AstrologicalSubject object with the __getitem__ method or the [] operator.
313    """
314    planets_absolute_position_list = []
315    for planet in subject.planets_names_list:
316            planets_absolute_position_list.append(subject[planet.lower()])
317
318    for axis in subject.axial_cusps_names_list:
319        planets_absolute_position_list.append(subject[axis.lower()])
320
321    return planets_absolute_position_list

Return the names of the planets in the order of the planets. The names can be used to access the planets from the AstrologicalSubject object with the __getitem__ method or the [] operator.

def circular_mean( first_position: Union[int, float], second_position: Union[int, float]) -> float:
324def circular_mean(first_position: Union[int, float], second_position: Union[int, float]) -> float:
325    """
326    Computes the circular mean of two astrological positions (e.g., house cusps, planets).
327
328    This function ensures that positions crossing 0° Aries (360°) are correctly averaged,
329    avoiding errors that occur with simple linear means.
330
331    Args:
332        position1 (Union[int, float]): First position in degrees (0-360).
333        position2 (Union[int, float]): Second position in degrees (0-360).
334
335    Returns:
336        float: The circular mean position in degrees (0-360).
337    """
338    x = (math.cos(math.radians(first_position)) + math.cos(math.radians(second_position))) / 2
339    y = (math.sin(math.radians(first_position)) + math.sin(math.radians(second_position))) / 2
340    mean_position = math.degrees(math.atan2(y, x))
341
342    # Ensure the result is within 0-360°
343    if mean_position < 0:
344        mean_position += 360
345
346    return mean_position

Computes the circular mean of two astrological positions (e.g., house cusps, planets).

This function ensures that positions crossing 0° Aries (360°) are correctly averaged, avoiding errors that occur with simple linear means.

Args: position1 (Union[int, float]): First position in degrees (0-360). position2 (Union[int, float]): Second position in degrees (0-360).

Returns: float: The circular mean position in degrees (0-360).

def calculate_moon_phase( moon_abs_pos: float, sun_abs_pos: float) -> kerykeion.kr_types.kr_models.LunarPhaseModel:
349def calculate_moon_phase(moon_abs_pos: float, sun_abs_pos: float) -> LunarPhaseModel:
350    """
351    Calculate the lunar phase based on the positions of the moon and sun.
352
353    Args:
354    - moon_abs_pos (float): The absolute position of the moon.
355    - sun_abs_pos (float): The absolute position of the sun.
356
357    Returns:
358    - dict: A dictionary containing the lunar phase information.
359    """
360    # Initialize moon_phase and sun_phase to None in case of an error
361    moon_phase, sun_phase = None, None
362
363    # Calculate the anti-clockwise degrees between the sun and moon
364    degrees_between = (moon_abs_pos - sun_abs_pos) % 360
365
366    # Calculate the moon phase (1-28) based on the degrees between the sun and moon
367    step = 360.0 / 28.0
368    moon_phase = int(degrees_between // step) + 1
369
370    # Define the sun phase steps
371    sunstep = [
372        0, 30, 40, 50, 60, 70, 80, 90, 120, 130, 140, 150, 160, 170, 180,
373        210, 220, 230, 240, 250, 260, 270, 300, 310, 320, 330, 340, 350
374    ]
375
376    # Calculate the sun phase (1-28) based on the degrees between the sun and moon
377    for x in range(len(sunstep)):
378        low = sunstep[x]
379        high = sunstep[x + 1] if x < len(sunstep) - 1 else 360
380        if low <= degrees_between < high:
381            sun_phase = x + 1
382            break
383
384    # Create a dictionary with the lunar phase information
385    lunar_phase_dictionary = {
386        "degrees_between_s_m": degrees_between,
387        "moon_phase": moon_phase,
388        "sun_phase": sun_phase,
389        "moon_emoji": get_moon_emoji_from_phase_int(moon_phase),
390        "moon_phase_name": get_moon_phase_name_from_phase_int(moon_phase)
391    }
392
393    return LunarPhaseModel(**lunar_phase_dictionary)

Calculate the lunar phase based on the positions of the moon and sun.

Args:

  • moon_abs_pos (float): The absolute position of the moon.
  • sun_abs_pos (float): The absolute position of the sun.

Returns:

  • dict: A dictionary containing the lunar phase information.
def circular_sort( degrees: list[typing.Union[int, float]]) -> list[typing.Union[int, float]]:
396def circular_sort(degrees: list[Union[int, float]]) -> list[Union[int, float]]:
397    """
398    Sort a list of degrees in a circular manner, starting from the first element
399    and progressing clockwise around the circle.
400
401    Args:
402        degrees: A list of numeric values representing degrees
403
404    Returns:
405        A list sorted based on circular clockwise progression from the first element
406
407    Raises:
408        ValueError: If the list is empty or contains non-numeric values
409    """
410    # Input validation
411    if not degrees:
412        raise ValueError("Input list cannot be empty")
413
414    if not all(isinstance(degree, (int, float)) for degree in degrees):
415        invalid = next(d for d in degrees if not isinstance(d, (int, float)))
416        raise ValueError(f"All elements must be numeric, found: {invalid} of type {type(invalid).__name__}")
417
418    # If list has 0 or 1 element, return it as is
419    if len(degrees) <= 1:
420        return degrees.copy()
421
422    # Save the first element as the reference
423    reference = degrees[0]
424
425    # Define a function to calculate clockwise distance from reference
426    def clockwise_distance(angle: Union[int, float]) -> Union[int, float]:
427        # Normalize angles to 0-360 range
428        ref_norm = reference % 360
429        angle_norm = angle % 360
430
431        # Calculate clockwise distance
432        distance = angle_norm - ref_norm
433        if distance < 0:
434            distance += 360
435
436        return distance
437
438    # Sort the rest of the elements based on circular distance
439    remaining = degrees[1:]
440    sorted_remaining = sorted(remaining, key=clockwise_distance)
441
442    # Return the reference followed by the sorted remaining elements
443    return [reference] + sorted_remaining

Sort a list of degrees in a circular manner, starting from the first element and progressing clockwise around the circle.

Args: degrees: A list of numeric values representing degrees

Returns: A list sorted based on circular clockwise progression from the first element

Raises: ValueError: If the list is empty or contains non-numeric values