kerykeion.aspects.natal_aspects

This is part of Kerykeion (C) 2025 Giacomo Battaglia

  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:
 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>)
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']]]
all_aspects
 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
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.