kerykeion 4.26.2__py3-none-any.whl → 5.0.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of kerykeion might be problematic. Click here for more details.
- kerykeion/__init__.py +8 -5
- kerykeion/aspects/aspects_utils.py +14 -8
- kerykeion/aspects/natal_aspects.py +26 -17
- kerykeion/aspects/synastry_aspects.py +32 -15
- kerykeion/aspects/transits_time_range.py +2 -2
- kerykeion/astrological_subject_factory.py +1132 -0
- kerykeion/charts/charts_utils.py +583 -85
- kerykeion/charts/draw_planets.py +9 -8
- kerykeion/charts/draw_planets_v2.py +639 -0
- kerykeion/charts/kerykeion_chart_svg.py +1289 -592
- kerykeion/charts/templates/chart.xml +178 -79
- kerykeion/charts/templates/wheel_only.xml +13 -12
- kerykeion/charts/themes/classic.css +91 -76
- kerykeion/charts/themes/dark-high-contrast.css +129 -107
- kerykeion/charts/themes/dark.css +130 -107
- kerykeion/charts/themes/light.css +130 -103
- kerykeion/charts/themes/strawberry.css +143 -0
- kerykeion/composite_subject_factory.py +26 -43
- kerykeion/ephemeris_data.py +6 -10
- kerykeion/house_comparison/__init__.py +3 -0
- kerykeion/house_comparison/house_comparison_factory.py +70 -0
- kerykeion/house_comparison/house_comparison_models.py +38 -0
- kerykeion/house_comparison/house_comparison_utils.py +98 -0
- kerykeion/kr_types/chart_types.py +9 -3
- kerykeion/kr_types/kr_literals.py +34 -6
- kerykeion/kr_types/kr_models.py +122 -160
- kerykeion/kr_types/settings_models.py +107 -143
- kerykeion/planetary_return_factory.py +299 -0
- kerykeion/relationship_score/relationship_score.py +3 -3
- kerykeion/relationship_score/relationship_score_factory.py +9 -12
- kerykeion/report.py +4 -4
- kerykeion/settings/config_constants.py +35 -6
- kerykeion/settings/kerykeion_settings.py +1 -0
- kerykeion/settings/kr.config.json +1301 -1255
- kerykeion/settings/legacy/__init__.py +0 -0
- kerykeion/settings/legacy/legacy_celestial_points_settings.py +299 -0
- kerykeion/settings/legacy/legacy_chart_aspects_settings.py +71 -0
- kerykeion/settings/legacy/legacy_color_settings.py +42 -0
- kerykeion/transits_time_range.py +13 -9
- kerykeion/utilities.py +228 -31
- {kerykeion-4.26.2.dist-info → kerykeion-5.0.0a1.dist-info}/METADATA +3 -3
- kerykeion-5.0.0a1.dist-info/RECORD +56 -0
- {kerykeion-4.26.2.dist-info → kerykeion-5.0.0a1.dist-info}/WHEEL +1 -1
- kerykeion/astrological_subject.py +0 -841
- kerykeion-4.26.2.dist-info/LICENSE +0 -661
- kerykeion-4.26.2.dist-info/RECORD +0 -46
- /LICENSE → /kerykeion-5.0.0a1.dist-info/LICENSE +0 -0
- {kerykeion-4.26.2.dist-info → kerykeion-5.0.0a1.dist-info}/entry_points.txt +0 -0
kerykeion/charts/charts_utils.py
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import math
|
|
2
2
|
import datetime
|
|
3
3
|
from kerykeion.kr_types import KerykeionException, ChartType
|
|
4
|
-
from
|
|
5
|
-
from
|
|
6
|
-
from kerykeion.kr_types.
|
|
4
|
+
from kerykeion.kr_types.kr_literals import AstrologicalPoint
|
|
5
|
+
from typing import Union, Literal, TYPE_CHECKING
|
|
6
|
+
from kerykeion.kr_types.kr_models import AspectModel, KerykeionPointModel, CompositeSubjectModel, PlanetReturnModel, AstrologicalSubjectModel
|
|
7
|
+
from kerykeion.kr_types.settings_models import KerykeionLanguageCelestialPointModel, KerykeionSettingsCelestialPointModel
|
|
7
8
|
|
|
8
9
|
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from kerykeion import HouseComparisonModel
|
|
12
|
+
|
|
9
13
|
def get_decoded_kerykeion_celestial_point_name(input_planet_name: str, celestial_point_language: KerykeionLanguageCelestialPointModel) -> str:
|
|
10
14
|
"""
|
|
11
15
|
Decode the given celestial point name based on the provided language model.
|
|
@@ -186,7 +190,7 @@ def draw_zodiac_slice(
|
|
|
186
190
|
# pie slices
|
|
187
191
|
offset = 360 - seventh_house_degree_ut
|
|
188
192
|
# check transit
|
|
189
|
-
if chart_type == "Transit" or chart_type == "Synastry":
|
|
193
|
+
if chart_type == "Transit" or chart_type == "Synastry" or chart_type == "Return":
|
|
190
194
|
dropin: Union[int, float] = 0
|
|
191
195
|
else:
|
|
192
196
|
dropin = c1
|
|
@@ -195,7 +199,7 @@ def draw_zodiac_slice(
|
|
|
195
199
|
# symbols
|
|
196
200
|
offset = offset + 15
|
|
197
201
|
# check transit
|
|
198
|
-
if chart_type == "Transit" or chart_type == "Synastry":
|
|
202
|
+
if chart_type == "Transit" or chart_type == "Synastry" or chart_type == "Return":
|
|
199
203
|
dropin = 54
|
|
200
204
|
else:
|
|
201
205
|
dropin = 18 + c1
|
|
@@ -289,6 +293,7 @@ def draw_aspect_line(
|
|
|
289
293
|
f"</g>"
|
|
290
294
|
)
|
|
291
295
|
|
|
296
|
+
|
|
292
297
|
def convert_decimal_to_degree_string(dec: float, format_type: Literal["1", "2", "3"] = "3") -> str:
|
|
293
298
|
"""
|
|
294
299
|
Converts a decimal float to a degrees string in the specified format.
|
|
@@ -415,7 +420,7 @@ def draw_first_circle(
|
|
|
415
420
|
Returns:
|
|
416
421
|
str: The SVG path of the first circle.
|
|
417
422
|
"""
|
|
418
|
-
if chart_type == "Synastry" or chart_type == "Transit":
|
|
423
|
+
if chart_type == "Synastry" or chart_type == "Transit" or chart_type == "Return":
|
|
419
424
|
return f'<circle cx="{r}" cy="{r}" r="{r - 36}" style="fill: none; stroke: {stroke_color}; stroke-width: 1px; stroke-opacity:.4;" />'
|
|
420
425
|
else:
|
|
421
426
|
if c1 is None:
|
|
@@ -443,7 +448,7 @@ def draw_second_circle(
|
|
|
443
448
|
str: The SVG path of the second circle.
|
|
444
449
|
"""
|
|
445
450
|
|
|
446
|
-
if chart_type == "Synastry" or chart_type == "Transit":
|
|
451
|
+
if chart_type == "Synastry" or chart_type == "Transit" or chart_type == "Return":
|
|
447
452
|
return f'<circle cx="{r}" cy="{r}" r="{r - 72}" style="fill: {fill_color}; fill-opacity:.4; stroke: {stroke_color}; stroke-opacity:.4; stroke-width: 1px" />'
|
|
448
453
|
|
|
449
454
|
else:
|
|
@@ -473,7 +478,7 @@ def draw_third_circle(
|
|
|
473
478
|
Returns:
|
|
474
479
|
- str: The SVG element as a string.
|
|
475
480
|
"""
|
|
476
|
-
if chart_type in {"Synastry", "Transit"}:
|
|
481
|
+
if chart_type in {"Synastry", "Transit", "Return"}:
|
|
477
482
|
# For Synastry and Transit charts, use a fixed radius adjustment of 160
|
|
478
483
|
return f'<circle cx="{radius}" cy="{radius}" r="{radius - 160}" style="fill: {fill_color}; fill-opacity:.8; stroke: {stroke_color}; stroke-width: 1px" />'
|
|
479
484
|
|
|
@@ -485,7 +490,7 @@ def draw_aspect_grid(
|
|
|
485
490
|
stroke_color: str,
|
|
486
491
|
available_planets: list,
|
|
487
492
|
aspects: list,
|
|
488
|
-
x_start: int =
|
|
493
|
+
x_start: int = 510,
|
|
489
494
|
y_start: int = 468,
|
|
490
495
|
) -> str:
|
|
491
496
|
"""
|
|
@@ -506,7 +511,7 @@ def draw_aspect_grid(
|
|
|
506
511
|
box_size = 14
|
|
507
512
|
|
|
508
513
|
# Filter active planets
|
|
509
|
-
active_planets = [planet for planet in available_planets if planet
|
|
514
|
+
active_planets = [planet for planet in available_planets if planet["is_active"]]
|
|
510
515
|
|
|
511
516
|
# Reverse the list of active planets for the first iteration
|
|
512
517
|
reversed_planets = active_planets[::-1]
|
|
@@ -580,7 +585,7 @@ def draw_houses_cusps_and_text_number(
|
|
|
580
585
|
|
|
581
586
|
for i in range(xr):
|
|
582
587
|
# Determine offsets based on chart type
|
|
583
|
-
dropin, roff, t_roff = (160, 72, 36) if chart_type in ["Transit", "Synastry"] else (c3, c1, False)
|
|
588
|
+
dropin, roff, t_roff = (160, 72, 36) if chart_type in ["Transit", "Synastry", "Return"] else (c3, c1, False)
|
|
584
589
|
|
|
585
590
|
# Calculate the offset for the current house cusp
|
|
586
591
|
offset = (int(first_subject_houses_list[int(xr / 2)].abs_pos) / -1) + int(first_subject_houses_list[i].abs_pos)
|
|
@@ -602,7 +607,7 @@ def draw_houses_cusps_and_text_number(
|
|
|
602
607
|
i, standard_house_cusp_color
|
|
603
608
|
)
|
|
604
609
|
|
|
605
|
-
if chart_type in ["Transit", "Synastry"]:
|
|
610
|
+
if chart_type in ["Transit", "Synastry", "Return"]:
|
|
606
611
|
if second_subject_houses_list is None or transit_house_cusp_color is None:
|
|
607
612
|
raise KerykeionException("second_subject_houses_list_ut or transit_house_cusp_color is None")
|
|
608
613
|
|
|
@@ -637,7 +642,7 @@ def draw_houses_cusps_and_text_number(
|
|
|
637
642
|
path += f"</g>"
|
|
638
643
|
|
|
639
644
|
# Adjust dropin based on chart type
|
|
640
|
-
dropin = {"Transit": 84, "Synastry": 84, "ExternalNatal": 100}.get(chart_type, 48)
|
|
645
|
+
dropin = {"Transit": 84, "Synastry": 84, "Return": 84, "ExternalNatal": 100}.get(chart_type, 48)
|
|
641
646
|
xtext = sliceToX(0, (r - dropin), text_offset) + dropin
|
|
642
647
|
ytext = sliceToY(0, (r - dropin), text_offset) + dropin
|
|
643
648
|
|
|
@@ -658,7 +663,12 @@ def draw_transit_aspect_list(
|
|
|
658
663
|
grid_title: str,
|
|
659
664
|
aspects_list: Union[list[AspectModel], list[dict]],
|
|
660
665
|
celestial_point_language: Union[KerykeionLanguageCelestialPointModel, dict],
|
|
661
|
-
aspects_settings:
|
|
666
|
+
aspects_settings: dict,
|
|
667
|
+
*,
|
|
668
|
+
aspects_per_column: int = 14,
|
|
669
|
+
column_width: int = 100,
|
|
670
|
+
line_height: int = 14,
|
|
671
|
+
max_columns: int = 6
|
|
662
672
|
) -> str:
|
|
663
673
|
"""
|
|
664
674
|
Generates the SVG output for the aspect transit grid.
|
|
@@ -666,8 +676,12 @@ def draw_transit_aspect_list(
|
|
|
666
676
|
Parameters:
|
|
667
677
|
- grid_title: Title of the grid.
|
|
668
678
|
- aspects_list: List of aspects.
|
|
669
|
-
-
|
|
679
|
+
- celestial_point_language: Dictionary containing the celestial point language data.
|
|
670
680
|
- aspects_settings: Dictionary containing the aspect settings.
|
|
681
|
+
- aspects_per_column: Number of aspects to display per column (default: 14).
|
|
682
|
+
- column_width: Width in pixels for each column (default: 100).
|
|
683
|
+
- line_height: Height in pixels for each line (default: 14).
|
|
684
|
+
- max_columns: Maximum number of columns before vertical adjustment (default: 6).
|
|
671
685
|
|
|
672
686
|
Returns:
|
|
673
687
|
- A string containing the SVG path data for the aspect transit grid.
|
|
@@ -676,65 +690,52 @@ def draw_transit_aspect_list(
|
|
|
676
690
|
if isinstance(celestial_point_language, dict):
|
|
677
691
|
celestial_point_language = KerykeionLanguageCelestialPointModel(**celestial_point_language)
|
|
678
692
|
|
|
679
|
-
if isinstance(aspects_settings, dict):
|
|
680
|
-
aspects_settings = KerykeionSettingsAspectModel(**aspects_settings)
|
|
681
|
-
|
|
682
693
|
# If not instance of AspectModel, convert to AspectModel
|
|
683
|
-
if isinstance(aspects_list[0], dict):
|
|
684
|
-
aspects_list = [AspectModel(**aspect) for aspect in aspects_list]
|
|
694
|
+
if aspects_list and isinstance(aspects_list[0], dict):
|
|
695
|
+
aspects_list = [AspectModel(**aspect) for aspect in aspects_list] # type: ignore
|
|
685
696
|
|
|
686
|
-
line = 0
|
|
687
|
-
nl = 0
|
|
688
697
|
inner_path = ""
|
|
698
|
+
|
|
689
699
|
for i, aspect in enumerate(aspects_list):
|
|
690
|
-
#
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
# aspect symbol
|
|
721
|
-
# TODO: Remove the "degree" element EVERYWHERE!
|
|
722
|
-
aspect_name = aspects_list[i]["aspect"]
|
|
723
|
-
id_value = next((a["degree"] for a in aspects_settings if a["name"] == aspect_name), None) # type: ignore
|
|
724
|
-
inner_path += f'<use x="15" y="0" xlink:href="#orb{id_value}" />'
|
|
725
|
-
|
|
726
|
-
# second planet symbol
|
|
700
|
+
# Calculate which column this aspect belongs in
|
|
701
|
+
current_column = i // aspects_per_column
|
|
702
|
+
|
|
703
|
+
# Calculate horizontal position based on column
|
|
704
|
+
horizontal_position = current_column * column_width
|
|
705
|
+
|
|
706
|
+
# Calculate vertical position within the column
|
|
707
|
+
current_line = i % aspects_per_column
|
|
708
|
+
vertical_position = current_line * line_height
|
|
709
|
+
|
|
710
|
+
# Special handling for many aspects - if we exceed max_columns
|
|
711
|
+
if current_column >= max_columns:
|
|
712
|
+
# Calculate how many aspects will overflow beyond the max columns
|
|
713
|
+
overflow_aspects = len(aspects_list) - (aspects_per_column * max_columns)
|
|
714
|
+
if overflow_aspects > 0:
|
|
715
|
+
# Adjust the starting vertical position to move text up
|
|
716
|
+
vertical_position = vertical_position - (overflow_aspects * line_height)
|
|
717
|
+
|
|
718
|
+
inner_path += f'<g transform="translate({horizontal_position},{vertical_position})">'
|
|
719
|
+
|
|
720
|
+
# First planet symbol
|
|
721
|
+
inner_path += f'<use transform="scale(0.4)" x="0" y="3" xlink:href="#{celestial_point_language[aspect["p1"]]["name"]}" />'
|
|
722
|
+
|
|
723
|
+
# Aspect symbol
|
|
724
|
+
aspect_name = aspect["aspect"]
|
|
725
|
+
id_value = next((a["degree"] for a in aspects_settings if a["name"] == aspect_name), None) # type: ignore
|
|
726
|
+
inner_path += f'<use x="15" y="0" xlink:href="#orb{id_value}" />'
|
|
727
|
+
|
|
728
|
+
# Second planet symbol
|
|
727
729
|
inner_path += f'<g transform="translate(30,0)">'
|
|
728
|
-
inner_path += f'<use transform="scale(0.4)" x="0" y="3" xlink:href="#{celestial_point_language[
|
|
730
|
+
inner_path += f'<use transform="scale(0.4)" x="0" y="3" xlink:href="#{celestial_point_language[aspect["p2"]]["name"]}" />'
|
|
729
731
|
inner_path += f"</g>"
|
|
730
732
|
|
|
731
|
-
#
|
|
732
|
-
inner_path += f'<text y="8" x="45" style="fill: var(--kerykeion-chart-color-paper-0); font-size: 10px;">{convert_decimal_to_degree_string(
|
|
733
|
-
|
|
733
|
+
# Difference in degrees
|
|
734
|
+
inner_path += f'<text y="8" x="45" style="fill: var(--kerykeion-chart-color-paper-0); font-size: 10px;">{convert_decimal_to_degree_string(aspect["orbit"])}</text>'
|
|
735
|
+
|
|
734
736
|
inner_path += f"</g>"
|
|
735
|
-
line = line + 14
|
|
736
737
|
|
|
737
|
-
out = '<g transform="translate(
|
|
738
|
+
out = '<g transform="translate(565,273)">'
|
|
738
739
|
out += f'<text y="-15" x="0" style="fill: var(--kerykeion-chart-color-paper-0); font-size: 14px;">{grid_title}:</text>'
|
|
739
740
|
out += inner_path
|
|
740
741
|
out += '</g>'
|
|
@@ -804,6 +805,7 @@ def calculate_moon_phase_chart_params(
|
|
|
804
805
|
"lunar_phase_rotate": lunar_phase_rotate,
|
|
805
806
|
}
|
|
806
807
|
|
|
808
|
+
|
|
807
809
|
def draw_house_grid(
|
|
808
810
|
main_subject_houses_list: list[KerykeionPointModel],
|
|
809
811
|
chart_type: ChartType,
|
|
@@ -825,10 +827,10 @@ def draw_house_grid(
|
|
|
825
827
|
- str: The SVG code for the grid of houses.
|
|
826
828
|
"""
|
|
827
829
|
|
|
828
|
-
if chart_type in ["Synastry", "Transit"] and secondary_subject_houses_list is None:
|
|
830
|
+
if chart_type in ["Synastry", "Transit", "Return"] and secondary_subject_houses_list is None:
|
|
829
831
|
raise KerykeionException("secondary_houses is None")
|
|
830
832
|
|
|
831
|
-
svg_output = '<g transform="translate(
|
|
833
|
+
svg_output = '<g transform="translate(700,0)">'
|
|
832
834
|
|
|
833
835
|
line_increment = 10
|
|
834
836
|
for i, house in enumerate(main_subject_houses_list):
|
|
@@ -844,9 +846,9 @@ def draw_house_grid(
|
|
|
844
846
|
|
|
845
847
|
svg_output += "</g>"
|
|
846
848
|
|
|
847
|
-
if chart_type == "Synastry":
|
|
849
|
+
if chart_type == "Synastry" or chart_type == "Return":
|
|
848
850
|
svg_output += '<!-- Synastry Houses -->'
|
|
849
|
-
svg_output += '<g transform="translate(
|
|
851
|
+
svg_output += '<g transform="translate(950, 0)">'
|
|
850
852
|
line_increment = 10
|
|
851
853
|
|
|
852
854
|
for i, house in enumerate(secondary_subject_houses_list): # type: ignore
|
|
@@ -895,18 +897,33 @@ def draw_planet_grid(
|
|
|
895
897
|
offset = 0
|
|
896
898
|
offset_between_lines = 14
|
|
897
899
|
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
900
|
+
if chart_type == "Synastry":
|
|
901
|
+
svg_output = (
|
|
902
|
+
f'<g transform="translate(0, -15)">'
|
|
903
|
+
f'<text style="fill:{text_color}; font-size: 14px;">{planets_and_houses_grid_title} {subject_name}</text>'
|
|
904
|
+
f'</g>'
|
|
905
|
+
)
|
|
906
|
+
elif chart_type == "Transit":
|
|
907
|
+
svg_output = (
|
|
908
|
+
f'<g transform="translate(0, -15)">'
|
|
909
|
+
f'<text style="fill:{text_color}; font-size: 14px;">{planets_and_houses_grid_title} {subject_name}</text>'
|
|
910
|
+
f'</g>'
|
|
911
|
+
)
|
|
912
|
+
elif chart_type == "Return":
|
|
913
|
+
svg_output = (
|
|
914
|
+
f'<g transform="translate(0, -15)">'
|
|
915
|
+
f'<text style="fill:{text_color}; font-size: 14px;">{planets_and_houses_grid_title} {subject_name}</text>'
|
|
916
|
+
f'</g>'
|
|
917
|
+
)
|
|
918
|
+
else:
|
|
919
|
+
svg_output = ""
|
|
903
920
|
|
|
904
921
|
end_of_line = "</g>"
|
|
905
922
|
|
|
906
923
|
for i, planet in enumerate(available_kerykeion_celestial_points):
|
|
907
|
-
if i ==
|
|
924
|
+
if i == 22:
|
|
908
925
|
line_height = 10
|
|
909
|
-
offset = -
|
|
926
|
+
offset = -125
|
|
910
927
|
|
|
911
928
|
decoded_name = get_decoded_kerykeion_celestial_point_name(planet["name"], celestial_point_language)
|
|
912
929
|
svg_output += (
|
|
@@ -923,19 +940,24 @@ def draw_planet_grid(
|
|
|
923
940
|
svg_output += end_of_line
|
|
924
941
|
line_height += offset_between_lines
|
|
925
942
|
|
|
926
|
-
if chart_type in ["Transit", "Synastry"]:
|
|
943
|
+
if chart_type in ["Transit", "Synastry", "Return"]:
|
|
927
944
|
if second_subject_available_kerykeion_celestial_points is None:
|
|
928
945
|
raise KerykeionException("second_subject_available_kerykeion_celestial_points is None")
|
|
929
946
|
|
|
930
947
|
if chart_type == "Transit":
|
|
931
948
|
svg_output += (
|
|
932
|
-
f'<g transform="translate(
|
|
933
|
-
f'<text
|
|
949
|
+
f'<g transform="translate(200, -15)">'
|
|
950
|
+
f'<text style="fill:{text_color}; font-size: 14px;">{second_subject_name}</text>'
|
|
951
|
+
)
|
|
952
|
+
elif chart_type == "Return":
|
|
953
|
+
svg_output += (
|
|
954
|
+
f'<g transform="translate(250, -15)">'
|
|
955
|
+
f'<text style="fill:{text_color}; font-size: 14px;">{planets_and_houses_grid_title} {second_subject_name}</text>'
|
|
934
956
|
)
|
|
935
957
|
else:
|
|
936
958
|
svg_output += (
|
|
937
|
-
f'<g transform="translate(
|
|
938
|
-
f'<text
|
|
959
|
+
f'<g transform="translate(250, -15)">'
|
|
960
|
+
f'<text style="fill:{text_color}; font-size: 14px;">{planets_and_houses_grid_title} {second_subject_name}</text>'
|
|
939
961
|
)
|
|
940
962
|
|
|
941
963
|
svg_output += end_of_line
|
|
@@ -944,9 +966,9 @@ def draw_planet_grid(
|
|
|
944
966
|
second_offset = 250
|
|
945
967
|
|
|
946
968
|
for i, t_planet in enumerate(second_subject_available_kerykeion_celestial_points):
|
|
947
|
-
if i == 27:
|
|
948
|
-
|
|
949
|
-
|
|
969
|
+
# if i == 27:
|
|
970
|
+
# second_line_height = 10
|
|
971
|
+
# second_offset = -120
|
|
950
972
|
|
|
951
973
|
second_decoded_name = get_decoded_kerykeion_celestial_point_name(t_planet["name"], celestial_point_language)
|
|
952
974
|
svg_output += (
|
|
@@ -994,7 +1016,7 @@ def draw_transit_aspect_grid(
|
|
|
994
1016
|
y_start = y_indent
|
|
995
1017
|
|
|
996
1018
|
# Filter active planets
|
|
997
|
-
active_planets = [planet for planet in available_planets if planet
|
|
1019
|
+
active_planets = [planet for planet in available_planets if planet["is_active"]]
|
|
998
1020
|
|
|
999
1021
|
# Reverse the list of active planets for the first iteration
|
|
1000
1022
|
reversed_planets = active_planets[::-1]
|
|
@@ -1040,3 +1062,479 @@ def draw_transit_aspect_grid(
|
|
|
1040
1062
|
svg_output += f'<use x="{x_aspect - box_size + 1}" y="{y_aspect + 1}" xlink:href="#orb{aspect["aspect_degrees"]}" />'
|
|
1041
1063
|
|
|
1042
1064
|
return svg_output
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
def format_location_string(location: str, max_length: int = 35) -> str:
|
|
1068
|
+
"""
|
|
1069
|
+
Format a location string to ensure it fits within a specified maximum length.
|
|
1070
|
+
|
|
1071
|
+
If the location is longer than max_length, it attempts to shorten by using only
|
|
1072
|
+
the first and last parts separated by commas. If still too long, it truncates
|
|
1073
|
+
and adds ellipsis.
|
|
1074
|
+
|
|
1075
|
+
Args:
|
|
1076
|
+
location: The original location string
|
|
1077
|
+
max_length: Maximum allowed length for the output string (default: 35)
|
|
1078
|
+
|
|
1079
|
+
Returns:
|
|
1080
|
+
Formatted location string that fits within max_length
|
|
1081
|
+
"""
|
|
1082
|
+
if len(location) > max_length:
|
|
1083
|
+
split_location = location.split(",")
|
|
1084
|
+
if len(split_location) > 1:
|
|
1085
|
+
shortened = split_location[0] + ", " + split_location[-1]
|
|
1086
|
+
if len(shortened) > max_length:
|
|
1087
|
+
return shortened[:max_length] + "..."
|
|
1088
|
+
return shortened
|
|
1089
|
+
else:
|
|
1090
|
+
return location[:max_length] + "..."
|
|
1091
|
+
return location
|
|
1092
|
+
|
|
1093
|
+
|
|
1094
|
+
def format_datetime_with_timezone(iso_datetime_string: str) -> str:
|
|
1095
|
+
"""
|
|
1096
|
+
Format an ISO datetime string with a custom format that includes properly formatted timezone.
|
|
1097
|
+
|
|
1098
|
+
Args:
|
|
1099
|
+
iso_datetime_string: ISO formatted datetime string
|
|
1100
|
+
|
|
1101
|
+
Returns:
|
|
1102
|
+
Formatted datetime string with properly formatted timezone offset (HH:MM)
|
|
1103
|
+
"""
|
|
1104
|
+
dt = datetime.datetime.fromisoformat(iso_datetime_string)
|
|
1105
|
+
custom_format = dt.strftime('%Y-%m-%d %H:%M [%z]')
|
|
1106
|
+
custom_format = custom_format[:-3] + ':' + custom_format[-3:]
|
|
1107
|
+
|
|
1108
|
+
return custom_format
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
def calculate_element_points(
|
|
1112
|
+
planets_settings: list[KerykeionSettingsCelestialPointModel],
|
|
1113
|
+
celestial_points_names: list[str],
|
|
1114
|
+
subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
|
|
1115
|
+
):
|
|
1116
|
+
"""
|
|
1117
|
+
Calculate elemental point totals based on planetary positions.
|
|
1118
|
+
|
|
1119
|
+
Args:
|
|
1120
|
+
planets_settings (list): List of planet configuration dictionaries
|
|
1121
|
+
celestial_points_names (list): List of celestial point names to process
|
|
1122
|
+
subject: Astrological subject with get() method for accessing planet data
|
|
1123
|
+
|
|
1124
|
+
Returns:
|
|
1125
|
+
dict: Dictionary with element point totals for 'fire', 'earth', 'air', and 'water'
|
|
1126
|
+
"""
|
|
1127
|
+
ZODIAC = (
|
|
1128
|
+
{"name": "Ari", "element": "fire"},
|
|
1129
|
+
{"name": "Tau", "element": "earth"},
|
|
1130
|
+
{"name": "Gem", "element": "air"},
|
|
1131
|
+
{"name": "Can", "element": "water"},
|
|
1132
|
+
{"name": "Leo", "element": "fire"},
|
|
1133
|
+
{"name": "Vir", "element": "earth"},
|
|
1134
|
+
{"name": "Lib", "element": "air"},
|
|
1135
|
+
{"name": "Sco", "element": "water"},
|
|
1136
|
+
{"name": "Sag", "element": "fire"},
|
|
1137
|
+
{"name": "Cap", "element": "earth"},
|
|
1138
|
+
{"name": "Aqu", "element": "air"},
|
|
1139
|
+
{"name": "Pis", "element": "water"},
|
|
1140
|
+
)
|
|
1141
|
+
|
|
1142
|
+
# Initialize element point totals
|
|
1143
|
+
element_totals = {
|
|
1144
|
+
"fire": 0.0,
|
|
1145
|
+
"earth": 0.0,
|
|
1146
|
+
"air": 0.0,
|
|
1147
|
+
"water": 0.0
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
# Make list of the points sign
|
|
1151
|
+
points_sign = [subject.get(planet).sign_num for planet in celestial_points_names]
|
|
1152
|
+
|
|
1153
|
+
for i in range(len(planets_settings)):
|
|
1154
|
+
# Add points to appropriate element
|
|
1155
|
+
element = ZODIAC[points_sign[i]]["element"]
|
|
1156
|
+
element_totals[element] += planets_settings[i]["element_points"]
|
|
1157
|
+
|
|
1158
|
+
return element_totals
|
|
1159
|
+
|
|
1160
|
+
|
|
1161
|
+
def calculate_synastry_element_points(
|
|
1162
|
+
planets_settings: list[KerykeionSettingsCelestialPointModel],
|
|
1163
|
+
celestial_points_names: list[str],
|
|
1164
|
+
subject1: AstrologicalSubjectModel,
|
|
1165
|
+
subject2: AstrologicalSubjectModel,
|
|
1166
|
+
):
|
|
1167
|
+
"""
|
|
1168
|
+
Calculate elemental point totals for both subjects in a synastry chart.
|
|
1169
|
+
|
|
1170
|
+
Args:
|
|
1171
|
+
planets_settings (list): List of planet configuration dictionaries
|
|
1172
|
+
celestial_points_names (list): List of celestial point names to process
|
|
1173
|
+
subject1: First astrological subject with get() method for accessing planet data
|
|
1174
|
+
subject2: Second astrological subject with get() method for accessing planet data
|
|
1175
|
+
|
|
1176
|
+
Returns:
|
|
1177
|
+
dict: Dictionary with element point totals as percentages, where the sum equals 100%
|
|
1178
|
+
"""
|
|
1179
|
+
ZODIAC = (
|
|
1180
|
+
{"name": "Ari", "element": "fire"},
|
|
1181
|
+
{"name": "Tau", "element": "earth"},
|
|
1182
|
+
{"name": "Gem", "element": "air"},
|
|
1183
|
+
{"name": "Can", "element": "water"},
|
|
1184
|
+
{"name": "Leo", "element": "fire"},
|
|
1185
|
+
{"name": "Vir", "element": "earth"},
|
|
1186
|
+
{"name": "Lib", "element": "air"},
|
|
1187
|
+
{"name": "Sco", "element": "water"},
|
|
1188
|
+
{"name": "Sag", "element": "fire"},
|
|
1189
|
+
{"name": "Cap", "element": "earth"},
|
|
1190
|
+
{"name": "Aqu", "element": "air"},
|
|
1191
|
+
{"name": "Pis", "element": "water"},
|
|
1192
|
+
)
|
|
1193
|
+
|
|
1194
|
+
# Initialize combined element point totals
|
|
1195
|
+
combined_totals = {
|
|
1196
|
+
"fire": 0.0,
|
|
1197
|
+
"earth": 0.0,
|
|
1198
|
+
"air": 0.0,
|
|
1199
|
+
"water": 0.0
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
# Make list of the points sign for both subjects
|
|
1203
|
+
subject1_points_sign = [subject1.get(planet).sign_num for planet in celestial_points_names]
|
|
1204
|
+
subject2_points_sign = [subject2.get(planet).sign_num for planet in celestial_points_names]
|
|
1205
|
+
|
|
1206
|
+
# Calculate element points for subject 1
|
|
1207
|
+
for i in range(len(planets_settings)):
|
|
1208
|
+
# Add points to appropriate element
|
|
1209
|
+
element1 = ZODIAC[subject1_points_sign[i]]["element"]
|
|
1210
|
+
combined_totals[element1] += planets_settings[i]["element_points"]
|
|
1211
|
+
|
|
1212
|
+
# Calculate element points for subject 2
|
|
1213
|
+
for i in range(len(planets_settings)):
|
|
1214
|
+
# Add points to appropriate element
|
|
1215
|
+
element2 = ZODIAC[subject2_points_sign[i]]["element"]
|
|
1216
|
+
combined_totals[element2] += planets_settings[i]["element_points"]
|
|
1217
|
+
|
|
1218
|
+
# Calculate total points across all elements
|
|
1219
|
+
total_points = sum(combined_totals.values())
|
|
1220
|
+
|
|
1221
|
+
# Convert to percentages (total = 100%)
|
|
1222
|
+
if total_points > 0:
|
|
1223
|
+
for element in combined_totals:
|
|
1224
|
+
combined_totals[element] = (combined_totals[element] / total_points) * 100.0
|
|
1225
|
+
|
|
1226
|
+
return combined_totals
|
|
1227
|
+
|
|
1228
|
+
|
|
1229
|
+
def draw_house_comparison_grid(
|
|
1230
|
+
house_comparison: "HouseComparisonModel",
|
|
1231
|
+
celestial_point_language: KerykeionLanguageCelestialPointModel,
|
|
1232
|
+
active_points: list[AstrologicalPoint],
|
|
1233
|
+
*,
|
|
1234
|
+
points_owner_subject_number: Literal[1, 2] = 1,
|
|
1235
|
+
text_color: str = "var(--kerykeion-color-neutral-content)",
|
|
1236
|
+
house_position_comparison_label: str = "House Position Comparison",
|
|
1237
|
+
return_point_label: str = "Return Point",
|
|
1238
|
+
return_label: str = "Return",
|
|
1239
|
+
radix_label: str = "Radix",
|
|
1240
|
+
x_position: int = 1030,
|
|
1241
|
+
y_position: int = 0,
|
|
1242
|
+
) -> str:
|
|
1243
|
+
"""
|
|
1244
|
+
Generate SVG code for displaying a comparison of points across houses between two charts.
|
|
1245
|
+
|
|
1246
|
+
Parameters:
|
|
1247
|
+
- house_comparison ("HouseComparisonModel"): Model containing house comparison data,
|
|
1248
|
+
including first_subject_name, second_subject_name, and points in houses.
|
|
1249
|
+
- celestial_point_language (KerykeionLanguageCelestialPointModel): Language model for celestial points
|
|
1250
|
+
- active_celestial_points (list[KerykeionPointModel]): List of active celestial points to display
|
|
1251
|
+
- text_color (str): Color for the text elements
|
|
1252
|
+
|
|
1253
|
+
Returns:
|
|
1254
|
+
- str: SVG code for the house comparison grid.
|
|
1255
|
+
"""
|
|
1256
|
+
if points_owner_subject_number == 1:
|
|
1257
|
+
comparison_data = house_comparison.first_points_in_second_houses
|
|
1258
|
+
else:
|
|
1259
|
+
comparison_data = house_comparison.second_points_in_first_houses
|
|
1260
|
+
|
|
1261
|
+
svg_output = f'<g transform="translate({x_position},{y_position})">'
|
|
1262
|
+
|
|
1263
|
+
# Add title
|
|
1264
|
+
svg_output += f'<text text-anchor="start" x="0" y="-15" style="fill:{text_color}; font-size: 14px;">{house_position_comparison_label}</text>'
|
|
1265
|
+
|
|
1266
|
+
# Add column headers
|
|
1267
|
+
line_increment = 10
|
|
1268
|
+
svg_output += (
|
|
1269
|
+
f'<g transform="translate(0,{line_increment})">'
|
|
1270
|
+
f'<text text-anchor="start" x="0" style="fill:{text_color}; font-weight: bold; font-size: 10px;">{return_point_label}</text>'
|
|
1271
|
+
f'<text text-anchor="start" x="77" style="fill:{text_color}; font-weight: bold; font-size: 10px;">{return_label}</text>'
|
|
1272
|
+
f'<text text-anchor="start" x="132" style="fill:{text_color}; font-weight: bold; font-size: 10px;">{radix_label}</text>'
|
|
1273
|
+
f'</g>'
|
|
1274
|
+
)
|
|
1275
|
+
line_increment += 15
|
|
1276
|
+
|
|
1277
|
+
# Create a dictionary to store all points by name for combined display
|
|
1278
|
+
all_points_by_name = {}
|
|
1279
|
+
|
|
1280
|
+
for point in comparison_data:
|
|
1281
|
+
# Only process points that are active
|
|
1282
|
+
if point.point_name in active_points and point.point_name not in all_points_by_name:
|
|
1283
|
+
all_points_by_name[point.point_name] = {
|
|
1284
|
+
"name": point.point_name,
|
|
1285
|
+
"secondary_house": point.projected_house_number,
|
|
1286
|
+
"native_house": point.point_owner_house_number
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
# Display all points organized by name
|
|
1290
|
+
for name, point_data in all_points_by_name.items():
|
|
1291
|
+
native_house = point_data.get("native_house", "-")
|
|
1292
|
+
secondary_house = point_data.get("secondary_house", "-")
|
|
1293
|
+
|
|
1294
|
+
svg_output += (
|
|
1295
|
+
f'<g transform="translate(0,{line_increment})">'
|
|
1296
|
+
f'<g transform="translate(0,-9)"><use transform="scale(0.4)" xlink:href="#{name}" /></g>'
|
|
1297
|
+
f'<text text-anchor="start" x="15" style="fill:{text_color}; font-size: 10px;">{get_decoded_kerykeion_celestial_point_name(name, celestial_point_language)}</text>'
|
|
1298
|
+
f'<text text-anchor="start" x="90" style="fill:{text_color}; font-size: 10px;">{native_house}</text>'
|
|
1299
|
+
f'<text text-anchor="start" x="140" style="fill:{text_color}; font-size: 10px;">{secondary_house}</text>'
|
|
1300
|
+
f'</g>'
|
|
1301
|
+
)
|
|
1302
|
+
line_increment += 12
|
|
1303
|
+
|
|
1304
|
+
svg_output += "</g>"
|
|
1305
|
+
|
|
1306
|
+
return svg_output
|
|
1307
|
+
|
|
1308
|
+
|
|
1309
|
+
def draw_single_house_comparison_grid(
|
|
1310
|
+
house_comparison: "HouseComparisonModel",
|
|
1311
|
+
celestial_point_language: KerykeionLanguageCelestialPointModel,
|
|
1312
|
+
active_points: list[AstrologicalPoint],
|
|
1313
|
+
*,
|
|
1314
|
+
points_owner_subject_number: Literal[1, 2] = 1,
|
|
1315
|
+
text_color: str = "var(--kerykeion-color-neutral-content)",
|
|
1316
|
+
house_position_comparison_label: str = "House Position Comparison",
|
|
1317
|
+
return_point_label: str = "Return Point",
|
|
1318
|
+
natal_house_label: str = "Natal House",
|
|
1319
|
+
x_position: int = 1030,
|
|
1320
|
+
y_position: int = 0,
|
|
1321
|
+
) -> str:
|
|
1322
|
+
"""
|
|
1323
|
+
Generate SVG code for displaying celestial points and their house positions.
|
|
1324
|
+
|
|
1325
|
+
Parameters:
|
|
1326
|
+
- house_comparison ("HouseComparisonModel"): Model containing house comparison data,
|
|
1327
|
+
including first_subject_name, second_subject_name, and points in houses.
|
|
1328
|
+
- celestial_point_language (KerykeionLanguageCelestialPointModel): Language model for celestial points
|
|
1329
|
+
- active_points (list[AstrologicalPoint]): List of active celestial points to display
|
|
1330
|
+
- points_owner_subject_number (Literal[1, 2]): Which subject's points to display (1 for first, 2 for second)
|
|
1331
|
+
- text_color (str): Color for the text elements
|
|
1332
|
+
- house_position_comparison_label (str): Label for the house position comparison grid
|
|
1333
|
+
- return_point_label (str): Label for the return point column
|
|
1334
|
+
- house_position_label (str): Label for the house position column
|
|
1335
|
+
- x_position (int): X position for the grid
|
|
1336
|
+
- y_position (int): Y position for the grid
|
|
1337
|
+
|
|
1338
|
+
Returns:
|
|
1339
|
+
- str: SVG code for the house position grid.
|
|
1340
|
+
"""
|
|
1341
|
+
if points_owner_subject_number == 1:
|
|
1342
|
+
comparison_data = house_comparison.first_points_in_second_houses
|
|
1343
|
+
else:
|
|
1344
|
+
comparison_data = house_comparison.second_points_in_first_houses
|
|
1345
|
+
|
|
1346
|
+
svg_output = f'<g transform="translate({x_position},{y_position})">'
|
|
1347
|
+
|
|
1348
|
+
# Add title
|
|
1349
|
+
svg_output += f'<text text-anchor="start" x="0" y="-15" style="fill:{text_color}; font-size: 14px;">{house_position_comparison_label}</text>'
|
|
1350
|
+
|
|
1351
|
+
# Add column headers
|
|
1352
|
+
line_increment = 10
|
|
1353
|
+
svg_output += (
|
|
1354
|
+
f'<g transform="translate(0,{line_increment})">'
|
|
1355
|
+
f'<text text-anchor="start" x="0" style="fill:{text_color}; font-weight: bold; font-size: 10px;">{return_point_label}</text>'
|
|
1356
|
+
f'<text text-anchor="start" x="77" style="fill:{text_color}; font-weight: bold; font-size: 10px;">{natal_house_label}</text>'
|
|
1357
|
+
f'</g>'
|
|
1358
|
+
)
|
|
1359
|
+
line_increment += 15
|
|
1360
|
+
|
|
1361
|
+
# Create a dictionary to store all points by name for combined display
|
|
1362
|
+
all_points_by_name = {}
|
|
1363
|
+
|
|
1364
|
+
for point in comparison_data:
|
|
1365
|
+
# Only process points that are active
|
|
1366
|
+
if point.point_name in active_points and point.point_name not in all_points_by_name:
|
|
1367
|
+
all_points_by_name[point.point_name] = {
|
|
1368
|
+
"name": point.point_name,
|
|
1369
|
+
"house": point.projected_house_number
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
# Display all points organized by name
|
|
1373
|
+
for name, point_data in all_points_by_name.items():
|
|
1374
|
+
house = point_data.get("house", "-")
|
|
1375
|
+
|
|
1376
|
+
svg_output += (
|
|
1377
|
+
f'<g transform="translate(0,{line_increment})">'
|
|
1378
|
+
f'<g transform="translate(0,-9)"><use transform="scale(0.4)" xlink:href="#{name}" /></g>'
|
|
1379
|
+
f'<text text-anchor="start" x="15" style="fill:{text_color}; font-size: 10px;">{get_decoded_kerykeion_celestial_point_name(name, celestial_point_language)}</text>'
|
|
1380
|
+
f'<text text-anchor="start" x="90" style="fill:{text_color}; font-size: 10px;">{house}</text>'
|
|
1381
|
+
f'</g>'
|
|
1382
|
+
)
|
|
1383
|
+
line_increment += 12
|
|
1384
|
+
|
|
1385
|
+
svg_output += "</g>"
|
|
1386
|
+
|
|
1387
|
+
return svg_output
|
|
1388
|
+
|
|
1389
|
+
|
|
1390
|
+
def makeLunarPhase(degrees_between_sun_and_moon: float, latitude: float) -> str:
|
|
1391
|
+
"""
|
|
1392
|
+
Generate SVG representation of lunar phase.
|
|
1393
|
+
|
|
1394
|
+
Parameters:
|
|
1395
|
+
- degrees_between_sun_and_moon (float): Angle between sun and moon in degrees
|
|
1396
|
+
- latitude (float): Observer's latitude for correct orientation
|
|
1397
|
+
|
|
1398
|
+
Returns:
|
|
1399
|
+
- str: SVG representation of lunar phase
|
|
1400
|
+
"""
|
|
1401
|
+
# Calculate parameters for the lunar phase visualization
|
|
1402
|
+
params = calculate_moon_phase_chart_params(degrees_between_sun_and_moon, latitude)
|
|
1403
|
+
|
|
1404
|
+
# Extract the calculated values
|
|
1405
|
+
lunar_phase_circle_center_x = params["circle_center_x"]
|
|
1406
|
+
lunar_phase_circle_radius = params["circle_radius"]
|
|
1407
|
+
lunar_phase_rotate = params["lunar_phase_rotate"]
|
|
1408
|
+
|
|
1409
|
+
# Generate the SVG for the lunar phase
|
|
1410
|
+
svg = (
|
|
1411
|
+
f'<g transform="rotate({lunar_phase_rotate} 20 10)">\n'
|
|
1412
|
+
f' <defs>\n'
|
|
1413
|
+
f' <clipPath id="moonPhaseCutOffCircle">\n'
|
|
1414
|
+
f' <circle cx="20" cy="10" r="10" />\n'
|
|
1415
|
+
f' </clipPath>\n'
|
|
1416
|
+
f' </defs>\n'
|
|
1417
|
+
f' <circle cx="20" cy="10" r="10" style="fill: var(--kerykeion-chart-color-lunar-phase-0)" />\n'
|
|
1418
|
+
f' <circle cx="{lunar_phase_circle_center_x}" cy="10" r="{lunar_phase_circle_radius}" style="fill: var(--kerykeion-chart-color-lunar-phase-1)" clip-path="url(#moonPhaseCutOffCircle)" />\n'
|
|
1419
|
+
f' <circle cx="20" cy="10" r="10" style="fill: none; stroke: var(--kerykeion-chart-color-lunar-phase-0); stroke-width: 0.5px; stroke-opacity: 0.5" />\n'
|
|
1420
|
+
f'</g>'
|
|
1421
|
+
)
|
|
1422
|
+
|
|
1423
|
+
return svg
|
|
1424
|
+
|
|
1425
|
+
|
|
1426
|
+
def calculate_quality_points(
|
|
1427
|
+
planets_settings: list[KerykeionSettingsCelestialPointModel],
|
|
1428
|
+
celestial_points_names: list[str],
|
|
1429
|
+
subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
|
|
1430
|
+
):
|
|
1431
|
+
"""
|
|
1432
|
+
Calculate quality point totals based on planetary positions.
|
|
1433
|
+
|
|
1434
|
+
Args:
|
|
1435
|
+
planets_settings (list): List of planet configuration dictionaries
|
|
1436
|
+
celestial_points_names (list): List of celestial point names to process
|
|
1437
|
+
subject: Astrological subject with get() method for accessing planet data
|
|
1438
|
+
planet_in_zodiac_extra_points (int): Extra points awarded for planets in their home sign
|
|
1439
|
+
|
|
1440
|
+
Returns:
|
|
1441
|
+
dict: Dictionary with quality point totals for 'cardinal', 'fixed', and 'mutable'
|
|
1442
|
+
"""
|
|
1443
|
+
ZODIAC = (
|
|
1444
|
+
{"name": "Ari", "quality": "cardinal"},
|
|
1445
|
+
{"name": "Tau", "quality": "fixed"},
|
|
1446
|
+
{"name": "Gem", "quality": "mutable"},
|
|
1447
|
+
{"name": "Can", "quality": "cardinal"},
|
|
1448
|
+
{"name": "Leo", "quality": "fixed"},
|
|
1449
|
+
{"name": "Vir", "quality": "mutable"},
|
|
1450
|
+
{"name": "Lib", "quality": "cardinal"},
|
|
1451
|
+
{"name": "Sco", "quality": "fixed"},
|
|
1452
|
+
{"name": "Sag", "quality": "mutable"},
|
|
1453
|
+
{"name": "Cap", "quality": "cardinal"},
|
|
1454
|
+
{"name": "Aqu", "quality": "fixed"},
|
|
1455
|
+
{"name": "Pis", "quality": "mutable"},
|
|
1456
|
+
)
|
|
1457
|
+
|
|
1458
|
+
# Initialize quality point totals
|
|
1459
|
+
quality_totals = {
|
|
1460
|
+
"cardinal": 0.0,
|
|
1461
|
+
"fixed": 0.0,
|
|
1462
|
+
"mutable": 0.0
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
# Make list of the points sign
|
|
1466
|
+
points_sign = [subject.get(planet).sign_num for planet in celestial_points_names]
|
|
1467
|
+
|
|
1468
|
+
for i in range(len(planets_settings)):
|
|
1469
|
+
# Add points to appropriate quality
|
|
1470
|
+
quality = ZODIAC[points_sign[i]]["quality"]
|
|
1471
|
+
quality_totals[quality] += planets_settings[i]["element_points"]
|
|
1472
|
+
|
|
1473
|
+
return quality_totals
|
|
1474
|
+
|
|
1475
|
+
|
|
1476
|
+
def calculate_synastry_quality_points(
|
|
1477
|
+
planets_settings: list[KerykeionSettingsCelestialPointModel],
|
|
1478
|
+
celestial_points_names: list[str],
|
|
1479
|
+
subject1: AstrologicalSubjectModel,
|
|
1480
|
+
subject2: AstrologicalSubjectModel,
|
|
1481
|
+
):
|
|
1482
|
+
"""
|
|
1483
|
+
Calculate quality point totals for both subjects in a synastry chart.
|
|
1484
|
+
|
|
1485
|
+
Args:
|
|
1486
|
+
planets_settings (list): List of planet configuration dictionaries
|
|
1487
|
+
celestial_points_names (list): List of celestial point names to process
|
|
1488
|
+
subject1: First astrological subject with get() method for accessing planet data
|
|
1489
|
+
subject2: Second astrological subject with get() method for accessing planet data
|
|
1490
|
+
|
|
1491
|
+
Returns:
|
|
1492
|
+
dict: Dictionary with quality point totals as percentages, where the sum equals 100%
|
|
1493
|
+
"""
|
|
1494
|
+
ZODIAC = (
|
|
1495
|
+
{"name": "Ari", "quality": "cardinal"},
|
|
1496
|
+
{"name": "Tau", "quality": "fixed"},
|
|
1497
|
+
{"name": "Gem", "quality": "mutable"},
|
|
1498
|
+
{"name": "Can", "quality": "cardinal"},
|
|
1499
|
+
{"name": "Leo", "quality": "fixed"},
|
|
1500
|
+
{"name": "Vir", "quality": "mutable"},
|
|
1501
|
+
{"name": "Lib", "quality": "cardinal"},
|
|
1502
|
+
{"name": "Sco", "quality": "fixed"},
|
|
1503
|
+
{"name": "Sag", "quality": "mutable"},
|
|
1504
|
+
{"name": "Cap", "quality": "cardinal"},
|
|
1505
|
+
{"name": "Aqu", "quality": "fixed"},
|
|
1506
|
+
{"name": "Pis", "quality": "mutable"},
|
|
1507
|
+
)
|
|
1508
|
+
|
|
1509
|
+
# Initialize combined quality point totals
|
|
1510
|
+
combined_totals = {
|
|
1511
|
+
"cardinal": 0.0,
|
|
1512
|
+
"fixed": 0.0,
|
|
1513
|
+
"mutable": 0.0
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
# Make list of the points sign for both subjects
|
|
1517
|
+
subject1_points_sign = [subject1.get(planet).sign_num for planet in celestial_points_names]
|
|
1518
|
+
subject2_points_sign = [subject2.get(planet).sign_num for planet in celestial_points_names]
|
|
1519
|
+
|
|
1520
|
+
# Calculate quality points for subject 1
|
|
1521
|
+
for i in range(len(planets_settings)):
|
|
1522
|
+
# Add points to appropriate quality
|
|
1523
|
+
quality1 = ZODIAC[subject1_points_sign[i]]["quality"]
|
|
1524
|
+
combined_totals[quality1] += planets_settings[i]["element_points"]
|
|
1525
|
+
|
|
1526
|
+
# Calculate quality points for subject 2
|
|
1527
|
+
for i in range(len(planets_settings)):
|
|
1528
|
+
# Add points to appropriate quality
|
|
1529
|
+
quality2 = ZODIAC[subject2_points_sign[i]]["quality"]
|
|
1530
|
+
combined_totals[quality2] += planets_settings[i]["element_points"]
|
|
1531
|
+
|
|
1532
|
+
# Calculate total points across all qualities
|
|
1533
|
+
total_points = sum(combined_totals.values())
|
|
1534
|
+
|
|
1535
|
+
# Convert to percentages (total = 100%)
|
|
1536
|
+
if total_points > 0:
|
|
1537
|
+
for quality in combined_totals:
|
|
1538
|
+
combined_totals[quality] = (combined_totals[quality] / total_points) * 100.0
|
|
1539
|
+
|
|
1540
|
+
return combined_totals
|