# kerykeion.aspects.natal_aspects

This is part of Kerykeion (C) 2025 Giacomo Battaglia

View Source

      1# -*- coding: utf-8 -*-
      2"""
      3    This is part of Kerykeion (C) 2025 Giacomo Battaglia
      4"""
      5
      6from pathlib import Path
      7from kerykeion import AstrologicalSubject
      8from kerykeion.kr_types import CompositeSubjectModel
      9import logging
     10from typing import Union, List
     11from kerykeion.settings.kerykeion_settings import get_settings
     12from dataclasses import dataclass, field
     13from functools import cached_property
     14from kerykeion.aspects.aspects_utils import planet_id_decoder, get_aspect_from_two_points, get_active_points_list
     15from kerykeion.kr_types.kr_models import AstrologicalSubjectModel, AspectModel, ActiveAspect
     16from kerykeion.kr_types.kr_literals import AxialCusps, Planet
     17from kerykeion.kr_types.settings_models import KerykeionSettingsModel
     18from kerykeion.settings.config_constants import DEFAULT_ACTIVE_POINTS, DEFAULT_ACTIVE_ASPECTS
     19
     20
     21
     22AXES_LIST = [
     23    "Ascendant",
     24    "Medium_Coeli",
     25    "Descendant",
     26    "Imum_Coeli",
     27]
     28
     29
     30@dataclass
     31class NatalAspects:
     32    """
     33    Generates an object with all the aspects of a birthcart.
     34    """
     35
     36    user: Union[AstrologicalSubject, AstrologicalSubjectModel, CompositeSubjectModel]
     37    new_settings_file: Union[Path, KerykeionSettingsModel, dict, None] = None
     38    active_points: List[Union[AxialCusps, Planet]] = field(default_factory=lambda: DEFAULT_ACTIVE_POINTS)
     39    active_aspects: List[ActiveAspect] = field(default_factory=lambda: DEFAULT_ACTIVE_ASPECTS)
     40
     41    def __post_init__(self):
     42        self.settings = get_settings(self.new_settings_file)
     43
     44        self.celestial_points = self.settings.celestial_points
     45        self.aspects_settings = self.settings.aspects
     46        self.axes_orbit_settings = self.settings.general_settings.axes_orbit
     47        self.active_points = self.active_points
     48
     49    @cached_property
     50    def all_aspects(self):
     51        """
     52        Return all the aspects of the points in the natal chart in a dictionary,
     53        first all the individual aspects of each planet, second the aspects
     54        without repetitions.
     55        """
     56
     57        active_points_list = get_active_points_list(self.user, self.settings, self.active_points)
     58
     59        # ---> TODO: Clean this up
     60        filtered_settings = []
     61        for a in self.aspects_settings:
     62            for aspect in self.active_aspects:
     63                if a["name"] == aspect["name"]:
     64                    a["orb"] = aspect["orb"]  # Assign the aspect's orb
     65                    filtered_settings.append(a)
     66                    break  # Exit the inner loop once a match is found
     67        self.aspects_settings = filtered_settings
     68        # <--- TODO: Clean this up
     69
     70        self.all_aspects_list = []
     71        for first in range(len(active_points_list)):
     72            # Generates the aspects list without repetitions
     73            for second in range(first + 1, len(active_points_list)):
     74                # AC/DC, MC/IC and North/South nodes are always in opposition
     75                opposite_pairs = {
     76                    ("Ascendant", "Descendant"),
     77                    ("Descendant", "Ascendant"),
     78                    ("Medium_Coeli", "Imum_Coeli"),
     79                    ("Imum_Coeli", "Medium_Coeli"),
     80                    ("True_Node", "True_South_Node"),
     81                    ("Mean_Node", "Mean_South_Node"),
     82                    ("True_South_Node", "True_Node"),
     83                    ("Mean_South_Node", "Mean_Node"),
     84                }
     85                if (active_points_list[first]["name"], active_points_list[second]["name"]) in opposite_pairs:
     86                    continue
     87
     88                aspect = get_aspect_from_two_points(
     89                    self.aspects_settings,
     90                    active_points_list[first]["abs_pos"],
     91                    active_points_list[second]["abs_pos"]
     92                )
     93
     94                verdict = aspect["verdict"]
     95                name = aspect["name"]
     96                orbit = aspect["orbit"]
     97                aspect_degrees = aspect["aspect_degrees"]
     98                diff = aspect["diff"]
     99
    100                if verdict == True:
    101                    aspect_model = AspectModel(
    102                        p1_name=active_points_list[first]["name"],
    103                        p1_owner=self.user.name,
    104                        p1_abs_pos=active_points_list[first]["abs_pos"],
    105                        p2_name=active_points_list[second]["name"],
    106                        p2_owner=self.user.name,
    107                        p2_abs_pos=active_points_list[second]["abs_pos"],
    108                        aspect=name,
    109                        orbit=orbit,
    110                        aspect_degrees=aspect_degrees,
    111                        diff=diff,
    112                        p1=planet_id_decoder(self.celestial_points, active_points_list[first]["name"]),
    113                        p2=planet_id_decoder(self.celestial_points, active_points_list[second]["name"]),
    114                    )
    115                    self.all_aspects_list.append(aspect_model)
    116
    117        return self.all_aspects_list
    118
    119    @cached_property
    120    def relevant_aspects(self):
    121        """
    122        Filters the aspects list with the desired points, in this case
    123        the most important are hardcoded.
    124        Set the list with set_points and creating a list with the names
    125        or the numbers of the houses.
    126        The relevant aspects are the ones that are set as looping in the available_aspects list.
    127        """
    128
    129        logging.debug("Relevant aspects not already calculated, calculating now...")
    130        self.all_aspects
    131
    132        axes_list = AXES_LIST
    133        counter = 0
    134
    135        # Remove aspects where the orbits exceed the maximum orb thresholds specified in the settings
    136        # (specified usually in kr.config.json file)
    137        aspects_filtered = self.all_aspects
    138        aspects_list_subtract = []
    139        for a in aspects_filtered:
    140            counter += 1
    141            name_p1 = str(a["p1_name"])
    142            name_p2 = str(a["p2_name"])
    143
    144            if name_p1 in axes_list:
    145                if abs(a["orbit"]) >= self.axes_orbit_settings:
    146                    aspects_list_subtract.append(a)
    147
    148            elif name_p2 in axes_list:
    149                if abs(a["orbit"]) >= self.axes_orbit_settings:
    150                    aspects_list_subtract.append(a)
    151
    152        self.aspects = [item for item in aspects_filtered if item not in aspects_list_subtract]
    153
    154        return self.aspects
    155
    156
    157if __name__ == "__main__":
    158    from kerykeion.utilities import setup_logging
    159
    160    setup_logging(level="debug")
    161
    162    johnny = AstrologicalSubject("Johnny Depp", 1963, 6, 9, 0, 0, "Owensboro", "US")
    163
    164    # All aspects as a list of dictionaries
    165    aspects = NatalAspects(johnny)
    166    #print([a.model_dump() for a in aspects.all_aspects])
    167
    168    print("\n")
    169
    170    # Relevant aspects as a list of dictionaries
    171    aspects = NatalAspects(johnny)
    172    print([a.model_dump() for a in aspects.relevant_aspects])

AXES_LIST = ['Ascendant', 'Medium_Coeli', 'Descendant', 'Imum_Coeli']
@dataclass
class NatalAspects: View Source

     31@dataclass
     32class NatalAspects:
     33    """
     34    Generates an object with all the aspects of a birthcart.
     35    """
     36
     37    user: Union[AstrologicalSubject, AstrologicalSubjectModel, CompositeSubjectModel]
     38    new_settings_file: Union[Path, KerykeionSettingsModel, dict, None] = None
     39    active_points: List[Union[AxialCusps, Planet]] = field(default_factory=lambda: DEFAULT_ACTIVE_POINTS)
     40    active_aspects: List[ActiveAspect] = field(default_factory=lambda: DEFAULT_ACTIVE_ASPECTS)
     41
     42    def __post_init__(self):
     43        self.settings = get_settings(self.new_settings_file)
     44
     45        self.celestial_points = self.settings.celestial_points
     46        self.aspects_settings = self.settings.aspects
     47        self.axes_orbit_settings = self.settings.general_settings.axes_orbit
     48        self.active_points = self.active_points
     49
     50    @cached_property
     51    def all_aspects(self):
     52        """
     53        Return all the aspects of the points in the natal chart in a dictionary,
     54        first all the individual aspects of each planet, second the aspects
     55        without repetitions.
     56        """
     57
     58        active_points_list = get_active_points_list(self.user, self.settings, self.active_points)
     59
     60        # ---> TODO: Clean this up
     61        filtered_settings = []
     62        for a in self.aspects_settings:
     63            for aspect in self.active_aspects:
     64                if a["name"] == aspect["name"]:
     65                    a["orb"] = aspect["orb"]  # Assign the aspect's orb
     66                    filtered_settings.append(a)
     67                    break  # Exit the inner loop once a match is found
     68        self.aspects_settings = filtered_settings
     69        # <--- TODO: Clean this up
     70
     71        self.all_aspects_list = []
     72        for first in range(len(active_points_list)):
     73            # Generates the aspects list without repetitions
     74            for second in range(first + 1, len(active_points_list)):
     75                # AC/DC, MC/IC and North/South nodes are always in opposition
     76                opposite_pairs = {
     77                    ("Ascendant", "Descendant"),
     78                    ("Descendant", "Ascendant"),
     79                    ("Medium_Coeli", "Imum_Coeli"),
     80                    ("Imum_Coeli", "Medium_Coeli"),
     81                    ("True_Node", "True_South_Node"),
     82                    ("Mean_Node", "Mean_South_Node"),
     83                    ("True_South_Node", "True_Node"),
     84                    ("Mean_South_Node", "Mean_Node"),
     85                }
     86                if (active_points_list[first]["name"], active_points_list[second]["name"]) in opposite_pairs:
     87                    continue
     88
     89                aspect = get_aspect_from_two_points(
     90                    self.aspects_settings,
     91                    active_points_list[first]["abs_pos"],
     92                    active_points_list[second]["abs_pos"]
     93                )
     94
     95                verdict = aspect["verdict"]
     96                name = aspect["name"]
     97                orbit = aspect["orbit"]
     98                aspect_degrees = aspect["aspect_degrees"]
     99                diff = aspect["diff"]
    100
    101                if verdict == True:
    102                    aspect_model = AspectModel(
    103                        p1_name=active_points_list[first]["name"],
    104                        p1_owner=self.user.name,
    105                        p1_abs_pos=active_points_list[first]["abs_pos"],
    106                        p2_name=active_points_list[second]["name"],
    107                        p2_owner=self.user.name,
    108                        p2_abs_pos=active_points_list[second]["abs_pos"],
    109                        aspect=name,
    110                        orbit=orbit,
    111                        aspect_degrees=aspect_degrees,
    112                        diff=diff,
    113                        p1=planet_id_decoder(self.celestial_points, active_points_list[first]["name"]),
    114                        p2=planet_id_decoder(self.celestial_points, active_points_list[second]["name"]),
    115                    )
    116                    self.all_aspects_list.append(aspect_model)
    117
    118        return self.all_aspects_list
    119
    120    @cached_property
    121    def relevant_aspects(self):
    122        """
    123        Filters the aspects list with the desired points, in this case
    124        the most important are hardcoded.
    125        Set the list with set_points and creating a list with the names
    126        or the numbers of the houses.
    127        The relevant aspects are the ones that are set as looping in the available_aspects list.
    128        """
    129
    130        logging.debug("Relevant aspects not already calculated, calculating now...")
    131        self.all_aspects
    132
    133        axes_list = AXES_LIST
    134        counter = 0
    135
    136        # Remove aspects where the orbits exceed the maximum orb thresholds specified in the settings
    137        # (specified usually in kr.config.json file)
    138        aspects_filtered = self.all_aspects
    139        aspects_list_subtract = []
    140        for a in aspects_filtered:
    141            counter += 1
    142            name_p1 = str(a["p1_name"])
    143            name_p2 = str(a["p2_name"])
    144
    145            if name_p1 in axes_list:
    146                if abs(a["orbit"]) >= self.axes_orbit_settings:
    147                    aspects_list_subtract.append(a)
    148
    149            elif name_p2 in axes_list:
    150                if abs(a["orbit"]) >= self.axes_orbit_settings:
    151                    aspects_list_subtract.append(a)
    152
    153        self.aspects = [item for item in aspects_filtered if item not in aspects_list_subtract]
    154
    155        return self.aspects

Generates an object with all the aspects of a birthcart.

NatalAspects( user: Union[kerykeion.astrological_subject.AstrologicalSubject, kerykeion.kr_types.kr_models.AstrologicalSubjectModel, kerykeion.kr_types.kr_models.CompositeSubjectModel], new_settings_file: Union[pathlib._local.Path, kerykeion.kr_types.settings_models.KerykeionSettingsModel, dict, NoneType] = None, active_points: List[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['Ascendant', 'Medium_Coeli', 'Descendant', 'Imum_Coeli']]] = <factory>, active_aspects: List[kerykeion.kr_types.kr_models.ActiveAspect] = <factory>)
user: Union[kerykeion.astrological_subject.AstrologicalSubject, kerykeion.kr_types.kr_models.AstrologicalSubjectModel, kerykeion.kr_types.kr_models.CompositeSubjectModel]
new_settings_file: Union[pathlib._local.Path, kerykeion.kr_types.settings_models.KerykeionSettingsModel, dict, NoneType] = None
active_points: List[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['Ascendant', 'Medium_Coeli', 'Descendant', 'Imum_Coeli']]]
active_aspects: List[kerykeion.kr_types.kr_models.ActiveAspect]
all_aspects View Source

     50    @cached_property
     51    def all_aspects(self):
     52        """
     53        Return all the aspects of the points in the natal chart in a dictionary,
     54        first all the individual aspects of each planet, second the aspects
     55        without repetitions.
     56        """
     57
     58        active_points_list = get_active_points_list(self.user, self.settings, self.active_points)
     59
     60        # ---> TODO: Clean this up
     61        filtered_settings = []
     62        for a in self.aspects_settings:
     63            for aspect in self.active_aspects:
     64                if a["name"] == aspect["name"]:
     65                    a["orb"] = aspect["orb"]  # Assign the aspect's orb
     66                    filtered_settings.append(a)
     67                    break  # Exit the inner loop once a match is found
     68        self.aspects_settings = filtered_settings
     69        # <--- TODO: Clean this up
     70
     71        self.all_aspects_list = []
     72        for first in range(len(active_points_list)):
     73            # Generates the aspects list without repetitions
     74            for second in range(first + 1, len(active_points_list)):
     75                # AC/DC, MC/IC and North/South nodes are always in opposition
     76                opposite_pairs = {
     77                    ("Ascendant", "Descendant"),
     78                    ("Descendant", "Ascendant"),
     79                    ("Medium_Coeli", "Imum_Coeli"),
     80                    ("Imum_Coeli", "Medium_Coeli"),
     81                    ("True_Node", "True_South_Node"),
     82                    ("Mean_Node", "Mean_South_Node"),
     83                    ("True_South_Node", "True_Node"),
     84                    ("Mean_South_Node", "Mean_Node"),
     85                }
     86                if (active_points_list[first]["name"], active_points_list[second]["name"]) in opposite_pairs:
     87                    continue
     88
     89                aspect = get_aspect_from_two_points(
     90                    self.aspects_settings,
     91                    active_points_list[first]["abs_pos"],
     92                    active_points_list[second]["abs_pos"]
     93                )
     94
     95                verdict = aspect["verdict"]
     96                name = aspect["name"]
     97                orbit = aspect["orbit"]
     98                aspect_degrees = aspect["aspect_degrees"]
     99                diff = aspect["diff"]
    100
    101                if verdict == True:
    102                    aspect_model = AspectModel(
    103                        p1_name=active_points_list[first]["name"],
    104                        p1_owner=self.user.name,
    105                        p1_abs_pos=active_points_list[first]["abs_pos"],
    106                        p2_name=active_points_list[second]["name"],
    107                        p2_owner=self.user.name,
    108                        p2_abs_pos=active_points_list[second]["abs_pos"],
    109                        aspect=name,
    110                        orbit=orbit,
    111                        aspect_degrees=aspect_degrees,
    112                        diff=diff,
    113                        p1=planet_id_decoder(self.celestial_points, active_points_list[first]["name"]),
    114                        p2=planet_id_decoder(self.celestial_points, active_points_list[second]["name"]),
    115                    )
    116                    self.all_aspects_list.append(aspect_model)
    117
    118        return self.all_aspects_list

Return all the aspects of the points in the natal chart in a dictionary, first all the individual aspects of each planet, second the aspects without repetitions.

relevant_aspects View Source

    120    @cached_property
    121    def relevant_aspects(self):
    122        """
    123        Filters the aspects list with the desired points, in this case
    124        the most important are hardcoded.
    125        Set the list with set_points and creating a list with the names
    126        or the numbers of the houses.
    127        The relevant aspects are the ones that are set as looping in the available_aspects list.
    128        """
    129
    130        logging.debug("Relevant aspects not already calculated, calculating now...")
    131        self.all_aspects
    132
    133        axes_list = AXES_LIST
    134        counter = 0
    135
    136        # Remove aspects where the orbits exceed the maximum orb thresholds specified in the settings
    137        # (specified usually in kr.config.json file)
    138        aspects_filtered = self.all_aspects
    139        aspects_list_subtract = []
    140        for a in aspects_filtered:
    141            counter += 1
    142            name_p1 = str(a["p1_name"])
    143            name_p2 = str(a["p2_name"])
    144
    145            if name_p1 in axes_list:
    146                if abs(a["orbit"]) >= self.axes_orbit_settings:
    147                    aspects_list_subtract.append(a)
    148
    149            elif name_p2 in axes_list:
    150                if abs(a["orbit"]) >= self.axes_orbit_settings:
    151                    aspects_list_subtract.append(a)
    152
    153        self.aspects = [item for item in aspects_filtered if item not in aspects_list_subtract]
    154
    155        return self.aspects

Filters the aspects list with the desired points, in this case the most important are hardcoded. Set the list with set_points and creating a list with the names or the numbers of the houses. The relevant aspects are the ones that are set as looping in the available_aspects list.