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
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.
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.
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
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.
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.
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
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
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.
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).
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.
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