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_abs_pos=active_points_list[first]["abs_pos"],
104                        p2_name=active_points_list[second]["name"],
105                        p2_abs_pos=active_points_list[second]["abs_pos"],
106                        aspect=name,
107                        orbit=orbit,
108                        aspect_degrees=aspect_degrees,
109                        diff=diff,
110                        p1=planet_id_decoder(self.celestial_points, active_points_list[first]["name"]),
111                        p2=planet_id_decoder(self.celestial_points, active_points_list[second]["name"]),
112                    )
113                    self.all_aspects_list.append(aspect_model)
114
115        return self.all_aspects_list
116
117    @cached_property
118    def relevant_aspects(self):
119        """
120        Filters the aspects list with the desired points, in this case
121        the most important are hardcoded.
122        Set the list with set_points and creating a list with the names
123        or the numbers of the houses.
124        The relevant aspects are the ones that are set as looping in the available_aspects list.
125        """
126
127        logging.debug("Relevant aspects not already calculated, calculating now...")
128        self.all_aspects
129
130        axes_list = AXES_LIST
131        counter = 0
132
133        # Remove aspects where the orbits exceed the maximum orb thresholds specified in the settings
134        # (specified usually in kr.config.json file)
135        aspects_filtered = self.all_aspects
136        aspects_list_subtract = []
137        for a in aspects_filtered:
138            counter += 1
139            name_p1 = str(a["p1_name"])
140            name_p2 = str(a["p2_name"])
141
142            if name_p1 in axes_list:
143                if abs(a["orbit"]) >= self.axes_orbit_settings:
144                    aspects_list_subtract.append(a)
145
146            elif name_p2 in axes_list:
147                if abs(a["orbit"]) >= self.axes_orbit_settings:
148                    aspects_list_subtract.append(a)
149
150        self.aspects = [item for item in aspects_filtered if item not in aspects_list_subtract]
151
152        return self.aspects
153
154
155if __name__ == "__main__":
156    from kerykeion.utilities import setup_logging
157
158    setup_logging(level="debug")
159
160    johnny = AstrologicalSubject("Johnny Depp", 1963, 6, 9, 0, 0, "Owensboro", "US")
161
162    # All aspects as a list of dictionaries
163    aspects = NatalAspects(johnny)
164    #print([a.model_dump() for a in aspects.all_aspects])
165
166    print("\n")
167
168    # Relevant aspects as a list of dictionaries
169    aspects = NatalAspects(johnny)
170    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_abs_pos=active_points_list[first]["abs_pos"],
105                        p2_name=active_points_list[second]["name"],
106                        p2_abs_pos=active_points_list[second]["abs_pos"],
107                        aspect=name,
108                        orbit=orbit,
109                        aspect_degrees=aspect_degrees,
110                        diff=diff,
111                        p1=planet_id_decoder(self.celestial_points, active_points_list[first]["name"]),
112                        p2=planet_id_decoder(self.celestial_points, active_points_list[second]["name"]),
113                    )
114                    self.all_aspects_list.append(aspect_model)
115
116        return self.all_aspects_list
117
118    @cached_property
119    def relevant_aspects(self):
120        """
121        Filters the aspects list with the desired points, in this case
122        the most important are hardcoded.
123        Set the list with set_points and creating a list with the names
124        or the numbers of the houses.
125        The relevant aspects are the ones that are set as looping in the available_aspects list.
126        """
127
128        logging.debug("Relevant aspects not already calculated, calculating now...")
129        self.all_aspects
130
131        axes_list = AXES_LIST
132        counter = 0
133
134        # Remove aspects where the orbits exceed the maximum orb thresholds specified in the settings
135        # (specified usually in kr.config.json file)
136        aspects_filtered = self.all_aspects
137        aspects_list_subtract = []
138        for a in aspects_filtered:
139            counter += 1
140            name_p1 = str(a["p1_name"])
141            name_p2 = str(a["p2_name"])
142
143            if name_p1 in axes_list:
144                if abs(a["orbit"]) >= self.axes_orbit_settings:
145                    aspects_list_subtract.append(a)
146
147            elif name_p2 in axes_list:
148                if abs(a["orbit"]) >= self.axes_orbit_settings:
149                    aspects_list_subtract.append(a)
150
151        self.aspects = [item for item in aspects_filtered if item not in aspects_list_subtract]
152
153        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.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.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_abs_pos=active_points_list[first]["abs_pos"],
105                        p2_name=active_points_list[second]["name"],
106                        p2_abs_pos=active_points_list[second]["abs_pos"],
107                        aspect=name,
108                        orbit=orbit,
109                        aspect_degrees=aspect_degrees,
110                        diff=diff,
111                        p1=planet_id_decoder(self.celestial_points, active_points_list[first]["name"]),
112                        p2=planet_id_decoder(self.celestial_points, active_points_list[second]["name"]),
113                    )
114                    self.all_aspects_list.append(aspect_model)
115
116        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
118    @cached_property
119    def relevant_aspects(self):
120        """
121        Filters the aspects list with the desired points, in this case
122        the most important are hardcoded.
123        Set the list with set_points and creating a list with the names
124        or the numbers of the houses.
125        The relevant aspects are the ones that are set as looping in the available_aspects list.
126        """
127
128        logging.debug("Relevant aspects not already calculated, calculating now...")
129        self.all_aspects
130
131        axes_list = AXES_LIST
132        counter = 0
133
134        # Remove aspects where the orbits exceed the maximum orb thresholds specified in the settings
135        # (specified usually in kr.config.json file)
136        aspects_filtered = self.all_aspects
137        aspects_list_subtract = []
138        for a in aspects_filtered:
139            counter += 1
140            name_p1 = str(a["p1_name"])
141            name_p2 = str(a["p2_name"])
142
143            if name_p1 in axes_list:
144                if abs(a["orbit"]) >= self.axes_orbit_settings:
145                    aspects_list_subtract.append(a)
146
147            elif name_p2 in axes_list:
148                if abs(a["orbit"]) >= self.axes_orbit_settings:
149                    aspects_list_subtract.append(a)
150
151        self.aspects = [item for item in aspects_filtered if item not in aspects_list_subtract]
152
153        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.