kerykeion 5.0.0b2__py3-none-any.whl → 5.0.0b5__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 +3 -2
- kerykeion/aspects/aspects_factory.py +60 -21
- kerykeion/aspects/aspects_utils.py +1 -1
- kerykeion/backword.py +111 -18
- kerykeion/chart_data_factory.py +72 -7
- kerykeion/charts/chart_drawer.py +524 -203
- kerykeion/charts/charts_utils.py +416 -253
- kerykeion/charts/templates/aspect_grid_only.xml +269 -312
- kerykeion/charts/templates/chart.xml +248 -304
- kerykeion/charts/templates/wheel_only.xml +271 -312
- kerykeion/charts/themes/black-and-white.css +148 -0
- kerykeion/kr_types/__init__.py +70 -0
- kerykeion/kr_types/chart_template_model.py +20 -0
- kerykeion/kr_types/kerykeion_exception.py +20 -0
- kerykeion/kr_types/kr_literals.py +20 -0
- kerykeion/kr_types/kr_models.py +20 -0
- kerykeion/kr_types/settings_models.py +20 -0
- kerykeion/relationship_score_factory.py +12 -2
- kerykeion/schemas/__init__.py +7 -0
- kerykeion/schemas/kr_literals.py +12 -1
- kerykeion/settings/__init__.py +16 -2
- kerykeion/settings/chart_defaults.py +444 -0
- kerykeion/settings/config_constants.py +0 -5
- kerykeion/settings/kerykeion_settings.py +31 -74
- kerykeion/settings/translation_strings.py +1479 -0
- kerykeion/settings/translations.py +74 -0
- kerykeion/transits_time_range_factory.py +10 -1
- {kerykeion-5.0.0b2.dist-info → kerykeion-5.0.0b5.dist-info}/METADATA +333 -207
- {kerykeion-5.0.0b2.dist-info → kerykeion-5.0.0b5.dist-info}/RECORD +31 -26
- kerykeion/settings/kr.config.json +0 -1474
- kerykeion/settings/legacy/__init__.py +0 -0
- kerykeion/settings/legacy/legacy_celestial_points_settings.py +0 -299
- kerykeion/settings/legacy/legacy_chart_aspects_settings.py +0 -71
- kerykeion/settings/legacy/legacy_color_settings.py +0 -42
- {kerykeion-5.0.0b2.dist-info → kerykeion-5.0.0b5.dist-info}/WHEEL +0 -0
- {kerykeion-5.0.0b2.dist-info → kerykeion-5.0.0b5.dist-info}/licenses/LICENSE +0 -0
kerykeion/charts/charts_utils.py
CHANGED
|
@@ -1,32 +1,226 @@
|
|
|
1
1
|
import math
|
|
2
2
|
import datetime
|
|
3
|
+
from typing import Mapping, Optional, Sequence, Union, Literal
|
|
4
|
+
|
|
3
5
|
from kerykeion.schemas import KerykeionException, ChartType
|
|
4
6
|
from kerykeion.schemas.kr_literals import AstrologicalPoint
|
|
5
|
-
from typing import Union, Literal
|
|
6
7
|
from kerykeion.schemas.kr_models import AspectModel, KerykeionPointModel, CompositeSubjectModel, PlanetReturnModel, AstrologicalSubjectModel, HouseComparisonModel
|
|
7
8
|
from kerykeion.schemas.settings_models import KerykeionLanguageCelestialPointModel, KerykeionSettingsCelestialPointModel
|
|
8
9
|
|
|
9
10
|
|
|
11
|
+
ElementQualityDistributionMethod = Literal["pure_count", "weighted"]
|
|
12
|
+
"""Supported strategies for calculating element and modality distributions."""
|
|
13
|
+
|
|
14
|
+
_SIGN_TO_ELEMENT: tuple[str, ...] = (
|
|
15
|
+
"fire", # Aries
|
|
16
|
+
"earth", # Taurus
|
|
17
|
+
"air", # Gemini
|
|
18
|
+
"water", # Cancer
|
|
19
|
+
"fire", # Leo
|
|
20
|
+
"earth", # Virgo
|
|
21
|
+
"air", # Libra
|
|
22
|
+
"water", # Scorpio
|
|
23
|
+
"fire", # Sagittarius
|
|
24
|
+
"earth", # Capricorn
|
|
25
|
+
"air", # Aquarius
|
|
26
|
+
"water", # Pisces
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
_SIGN_TO_QUALITY: tuple[str, ...] = (
|
|
30
|
+
"cardinal", # Aries
|
|
31
|
+
"fixed", # Taurus
|
|
32
|
+
"mutable", # Gemini
|
|
33
|
+
"cardinal", # Cancer
|
|
34
|
+
"fixed", # Leo
|
|
35
|
+
"mutable", # Virgo
|
|
36
|
+
"cardinal", # Libra
|
|
37
|
+
"fixed", # Scorpio
|
|
38
|
+
"mutable", # Sagittarius
|
|
39
|
+
"cardinal", # Capricorn
|
|
40
|
+
"fixed", # Aquarius
|
|
41
|
+
"mutable", # Pisces
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
_ELEMENT_KEYS: tuple[str, ...] = ("fire", "earth", "air", "water")
|
|
45
|
+
_QUALITY_KEYS: tuple[str, ...] = ("cardinal", "fixed", "mutable")
|
|
46
|
+
|
|
47
|
+
_DEFAULT_WEIGHTED_FALLBACK = 1.0
|
|
48
|
+
DEFAULT_WEIGHTED_POINT_WEIGHTS: dict[str, float] = {
|
|
49
|
+
# Core luminaries & angles
|
|
50
|
+
"sun": 2.0,
|
|
51
|
+
"moon": 2.0,
|
|
52
|
+
"ascendant": 2.0,
|
|
53
|
+
"medium_coeli": 1.5,
|
|
54
|
+
"descendant": 1.5,
|
|
55
|
+
"imum_coeli": 1.5,
|
|
56
|
+
"vertex": 0.8,
|
|
57
|
+
"anti_vertex": 0.8,
|
|
58
|
+
# Personal planets
|
|
59
|
+
"mercury": 1.5,
|
|
60
|
+
"venus": 1.5,
|
|
61
|
+
"mars": 1.5,
|
|
62
|
+
# Social planets
|
|
63
|
+
"jupiter": 1.0,
|
|
64
|
+
"saturn": 1.0,
|
|
65
|
+
# Outer/transpersonal
|
|
66
|
+
"uranus": 0.5,
|
|
67
|
+
"neptune": 0.5,
|
|
68
|
+
"pluto": 0.5,
|
|
69
|
+
# Lunar nodes (mean/true variants)
|
|
70
|
+
"mean_north_lunar_node": 0.5,
|
|
71
|
+
"true_north_lunar_node": 0.5,
|
|
72
|
+
"mean_south_lunar_node": 0.5,
|
|
73
|
+
"true_south_lunar_node": 0.5,
|
|
74
|
+
# Chiron, Lilith variants
|
|
75
|
+
"chiron": 0.6,
|
|
76
|
+
"mean_lilith": 0.5,
|
|
77
|
+
"true_lilith": 0.5,
|
|
78
|
+
# Asteroids / centaurs
|
|
79
|
+
"ceres": 0.5,
|
|
80
|
+
"pallas": 0.4,
|
|
81
|
+
"juno": 0.4,
|
|
82
|
+
"vesta": 0.4,
|
|
83
|
+
"pholus": 0.3,
|
|
84
|
+
# Dwarf planets & TNOs
|
|
85
|
+
"eris": 0.3,
|
|
86
|
+
"sedna": 0.3,
|
|
87
|
+
"haumea": 0.3,
|
|
88
|
+
"makemake": 0.3,
|
|
89
|
+
"ixion": 0.3,
|
|
90
|
+
"orcus": 0.3,
|
|
91
|
+
"quaoar": 0.3,
|
|
92
|
+
# Arabic Parts
|
|
93
|
+
"pars_fortunae": 0.8,
|
|
94
|
+
"pars_spiritus": 0.7,
|
|
95
|
+
"pars_amoris": 0.6,
|
|
96
|
+
"pars_fidei": 0.6,
|
|
97
|
+
# Fixed stars
|
|
98
|
+
"regulus": 0.2,
|
|
99
|
+
"spica": 0.2,
|
|
100
|
+
# Other
|
|
101
|
+
"earth": 0.3,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _prepare_weight_lookup(
|
|
106
|
+
method: ElementQualityDistributionMethod,
|
|
107
|
+
custom_weights: Optional[Mapping[str, float]] = None,
|
|
108
|
+
) -> tuple[dict[str, float], float]:
|
|
109
|
+
"""
|
|
110
|
+
Normalize and merge default weights with any custom overrides.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
method: Calculation strategy to use.
|
|
114
|
+
custom_weights: Optional mapping of point name (case-insensitive) to weight.
|
|
115
|
+
Supports special key "__default__" as fallback weight.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
A tuple containing the weight lookup dictionary and fallback weight.
|
|
119
|
+
"""
|
|
120
|
+
normalized_custom = (
|
|
121
|
+
{key.lower(): float(value) for key, value in custom_weights.items()}
|
|
122
|
+
if custom_weights
|
|
123
|
+
else {}
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if method == "weighted":
|
|
127
|
+
weight_lookup: dict[str, float] = dict(DEFAULT_WEIGHTED_POINT_WEIGHTS)
|
|
128
|
+
fallback_weight = _DEFAULT_WEIGHTED_FALLBACK
|
|
129
|
+
else:
|
|
130
|
+
weight_lookup = {}
|
|
131
|
+
fallback_weight = 1.0
|
|
132
|
+
|
|
133
|
+
fallback_weight = normalized_custom.get("__default__", fallback_weight)
|
|
134
|
+
|
|
135
|
+
for key, value in normalized_custom.items():
|
|
136
|
+
if key == "__default__":
|
|
137
|
+
continue
|
|
138
|
+
weight_lookup[key] = float(value)
|
|
139
|
+
|
|
140
|
+
return weight_lookup, fallback_weight
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _calculate_distribution_for_subject(
|
|
144
|
+
subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
|
|
145
|
+
celestial_points_names: Sequence[str],
|
|
146
|
+
sign_to_group_map: Sequence[str],
|
|
147
|
+
group_keys: Sequence[str],
|
|
148
|
+
weight_lookup: Mapping[str, float],
|
|
149
|
+
fallback_weight: float,
|
|
150
|
+
) -> dict[str, float]:
|
|
151
|
+
"""
|
|
152
|
+
Accumulate distribution totals for a single subject.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
subject: Subject providing planetary positions.
|
|
156
|
+
celestial_points_names: Names of celestial points to consider (lowercase).
|
|
157
|
+
sign_to_group_map: Mapping from sign index to element/modality key.
|
|
158
|
+
group_keys: Iterable of expected keys for the resulting totals.
|
|
159
|
+
weight_lookup: Precomputed mapping of weights per point.
|
|
160
|
+
fallback_weight: Default weight if point missing in lookup.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Dictionary with accumulated totals keyed by element/modality.
|
|
164
|
+
"""
|
|
165
|
+
totals = {key: 0.0 for key in group_keys}
|
|
166
|
+
|
|
167
|
+
for point_name in celestial_points_names:
|
|
168
|
+
point = subject.get(point_name)
|
|
169
|
+
if point is None:
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
sign_index = getattr(point, "sign_num", None)
|
|
173
|
+
if sign_index is None or not (0 <= sign_index < len(sign_to_group_map)):
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
group_key = sign_to_group_map[sign_index]
|
|
177
|
+
weight = weight_lookup.get(point_name, fallback_weight)
|
|
178
|
+
totals[group_key] += weight
|
|
179
|
+
|
|
180
|
+
return totals
|
|
181
|
+
|
|
182
|
+
|
|
10
183
|
_SECOND_COLUMN_THRESHOLD = 20
|
|
11
184
|
_THIRD_COLUMN_THRESHOLD = 28
|
|
12
185
|
_FOURTH_COLUMN_THRESHOLD = 36
|
|
186
|
+
|
|
187
|
+
_DOUBLE_CHART_TYPES: tuple[ChartType, ...] = ("Synastry", "Transit", "DualReturnChart")
|
|
13
188
|
_GRID_COLUMN_WIDTH = 125
|
|
14
189
|
|
|
15
190
|
|
|
16
|
-
def
|
|
191
|
+
def _select_planet_grid_thresholds(chart_type: ChartType) -> tuple[int, int, int]:
|
|
192
|
+
"""Return column thresholds for the planet grids based on chart type."""
|
|
193
|
+
if chart_type in _DOUBLE_CHART_TYPES:
|
|
194
|
+
return (
|
|
195
|
+
1_000_000, # effectively disable first column
|
|
196
|
+
1_000_008, # effectively disable second column
|
|
197
|
+
1_000_016, # effectively disable third column
|
|
198
|
+
)
|
|
199
|
+
return _SECOND_COLUMN_THRESHOLD, _THIRD_COLUMN_THRESHOLD, _FOURTH_COLUMN_THRESHOLD
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _planet_grid_layout_position(
|
|
203
|
+
index: int, thresholds: Optional[tuple[int, int, int]] = None
|
|
204
|
+
) -> tuple[int, int]:
|
|
17
205
|
"""Return horizontal offset and row index for planet grids."""
|
|
18
|
-
|
|
206
|
+
second_threshold, third_threshold, fourth_threshold = (
|
|
207
|
+
thresholds
|
|
208
|
+
if thresholds is not None
|
|
209
|
+
else (_SECOND_COLUMN_THRESHOLD, _THIRD_COLUMN_THRESHOLD, _FOURTH_COLUMN_THRESHOLD)
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
if index < second_threshold:
|
|
19
213
|
column = 0
|
|
20
214
|
row = index
|
|
21
|
-
elif index <
|
|
215
|
+
elif index < third_threshold:
|
|
22
216
|
column = 1
|
|
23
|
-
row = index -
|
|
24
|
-
elif index <
|
|
217
|
+
row = index - second_threshold
|
|
218
|
+
elif index < fourth_threshold:
|
|
25
219
|
column = 2
|
|
26
|
-
row = index -
|
|
220
|
+
row = index - third_threshold
|
|
27
221
|
else:
|
|
28
222
|
column = 3
|
|
29
|
-
row = index -
|
|
223
|
+
row = index - fourth_threshold
|
|
30
224
|
|
|
31
225
|
offset = -(_GRID_COLUMN_WIDTH * column)
|
|
32
226
|
return offset, row
|
|
@@ -712,7 +906,8 @@ def draw_transit_aspect_list(
|
|
|
712
906
|
aspects_per_column: int = 14,
|
|
713
907
|
column_width: int = 100,
|
|
714
908
|
line_height: int = 14,
|
|
715
|
-
max_columns: int = 6
|
|
909
|
+
max_columns: int = 6,
|
|
910
|
+
chart_height: Optional[int] = None,
|
|
716
911
|
) -> str:
|
|
717
912
|
"""
|
|
718
913
|
Generates the SVG output for the aspect transit grid.
|
|
@@ -726,6 +921,8 @@ def draw_transit_aspect_list(
|
|
|
726
921
|
- column_width: Width in pixels for each column (default: 100).
|
|
727
922
|
- line_height: Height in pixels for each line (default: 14).
|
|
728
923
|
- max_columns: Maximum number of columns before vertical adjustment (default: 6).
|
|
924
|
+
- chart_height: Total chart height. When provided, columns from the 12th onward
|
|
925
|
+
leverage the taller layout capacity (default: None).
|
|
729
926
|
|
|
730
927
|
Returns:
|
|
731
928
|
- A string containing the SVG path data for the aspect transit grid.
|
|
@@ -741,61 +938,86 @@ def draw_transit_aspect_list(
|
|
|
741
938
|
# Type narrowing: at this point aspects_list contains AspectModel instances
|
|
742
939
|
typed_aspects_list: list[AspectModel] = aspects_list # type: ignore
|
|
743
940
|
|
|
941
|
+
translate_x = 565
|
|
942
|
+
translate_y = 273
|
|
943
|
+
title_clearance = 18
|
|
944
|
+
top_limit_y: float = -translate_y + title_clearance
|
|
945
|
+
bottom_padding = 40
|
|
946
|
+
baseline_index = aspects_per_column - 1
|
|
947
|
+
top_limit_index = math.ceil(top_limit_y / line_height)
|
|
948
|
+
# `top_limit_index` identifies the highest row index we can reach without
|
|
949
|
+
# touching the title block. Combined with the baseline index we know how many
|
|
950
|
+
# rows a "tall" column may contain.
|
|
951
|
+
max_capacity_by_top = baseline_index - top_limit_index + 1
|
|
952
|
+
|
|
744
953
|
inner_path = ""
|
|
745
954
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
if
|
|
760
|
-
|
|
761
|
-
if
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
955
|
+
full_height_column_index = 10 # 0-based index → 11th column onward
|
|
956
|
+
if chart_height is not None:
|
|
957
|
+
available_height = max(chart_height - translate_y - bottom_padding, line_height)
|
|
958
|
+
allowed_capacity = max(aspects_per_column, int(available_height // line_height))
|
|
959
|
+
full_height_capacity = max(aspects_per_column, min(allowed_capacity, max_capacity_by_top))
|
|
960
|
+
else:
|
|
961
|
+
full_height_capacity = aspects_per_column
|
|
962
|
+
|
|
963
|
+
# Bucket aspects into columns while respecting the capacity of each column.
|
|
964
|
+
columns: list[list[AspectModel]] = []
|
|
965
|
+
column_capacities: list[int] = []
|
|
966
|
+
|
|
967
|
+
for aspect in typed_aspects_list:
|
|
968
|
+
if not columns or len(columns[-1]) >= column_capacities[-1]:
|
|
969
|
+
new_col_index = len(columns)
|
|
970
|
+
capacity = aspects_per_column if new_col_index < full_height_column_index else full_height_capacity
|
|
971
|
+
capacity = max(capacity, 1)
|
|
972
|
+
columns.append([])
|
|
973
|
+
column_capacities.append(capacity)
|
|
974
|
+
columns[-1].append(aspect)
|
|
975
|
+
|
|
976
|
+
for col_idx, column in enumerate(columns):
|
|
977
|
+
capacity = column_capacities[col_idx]
|
|
978
|
+
horizontal_position = col_idx * column_width
|
|
979
|
+
column_len = len(column)
|
|
980
|
+
|
|
981
|
+
for row_idx, aspect in enumerate(column):
|
|
982
|
+
# Default top-aligned placement
|
|
983
|
+
vertical_position = row_idx * line_height
|
|
984
|
+
|
|
985
|
+
# Full-height columns reuse the shared baseline so every column
|
|
986
|
+
# finishes at the same vertical position and grows upwards.
|
|
987
|
+
if col_idx >= full_height_column_index:
|
|
988
|
+
vertical_index = baseline_index - (column_len - 1 - row_idx)
|
|
989
|
+
vertical_position = vertical_index * line_height
|
|
990
|
+
# Legacy overflow columns (before the 12th) keep the older behaviour:
|
|
991
|
+
# once we exceed the configured column count, bottom-align the content
|
|
992
|
+
# so the shorter columns do not look awkwardly padded at the top.
|
|
993
|
+
elif col_idx >= max_columns and capacity == aspects_per_column:
|
|
994
|
+
top_offset_lines = max(0, capacity - len(column))
|
|
995
|
+
vertical_position = (top_offset_lines + row_idx) * line_height
|
|
996
|
+
|
|
997
|
+
inner_path += f'<g transform="translate({horizontal_position},{vertical_position})">'
|
|
998
|
+
|
|
999
|
+
# First planet symbol
|
|
1000
|
+
inner_path += f'<use transform="scale(0.4)" x="0" y="3" xlink:href="#{celestial_point_language[aspect["p1"]]["name"]}" />'
|
|
1001
|
+
|
|
1002
|
+
# Aspect symbol
|
|
1003
|
+
aspect_name = aspect["aspect"]
|
|
1004
|
+
id_value = next((a["degree"] for a in aspects_settings if a["name"] == aspect_name), None) # type: ignore
|
|
1005
|
+
inner_path += f'<use x="15" y="0" xlink:href="#orb{id_value}" />'
|
|
1006
|
+
|
|
1007
|
+
# Second planet symbol
|
|
1008
|
+
inner_path += '<g transform="translate(30,0)">'
|
|
1009
|
+
inner_path += f'<use transform="scale(0.4)" x="0" y="3" xlink:href="#{celestial_point_language[aspect["p2"]]["name"]}" />'
|
|
1010
|
+
inner_path += "</g>"
|
|
1011
|
+
|
|
1012
|
+
# Difference in degrees
|
|
1013
|
+
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>'
|
|
1014
|
+
|
|
1015
|
+
inner_path += "</g>"
|
|
1016
|
+
|
|
1017
|
+
out = f'<g transform="translate({translate_x},{translate_y})">'
|
|
796
1018
|
out += f'<text y="-15" x="0" style="fill: var(--kerykeion-chart-color-paper-0); font-size: 14px;">{grid_title}:</text>'
|
|
797
1019
|
out += inner_path
|
|
798
|
-
out +=
|
|
1020
|
+
out += "</g>"
|
|
799
1021
|
|
|
800
1022
|
return out
|
|
801
1023
|
|
|
@@ -987,8 +1209,10 @@ def draw_main_planet_grid(
|
|
|
987
1209
|
|
|
988
1210
|
end_of_line = "</g>"
|
|
989
1211
|
|
|
1212
|
+
column_thresholds = _select_planet_grid_thresholds(chart_type)
|
|
1213
|
+
|
|
990
1214
|
for i, planet in enumerate(available_kerykeion_celestial_points):
|
|
991
|
-
offset, row_index = _planet_grid_layout_position(i)
|
|
1215
|
+
offset, row_index = _planet_grid_layout_position(i, column_thresholds)
|
|
992
1216
|
line_height = LINE_START + (row_index * LINE_STEP)
|
|
993
1217
|
|
|
994
1218
|
decoded_name = get_decoded_kerykeion_celestial_point_name(
|
|
@@ -1070,8 +1294,10 @@ def draw_secondary_planet_grid(
|
|
|
1070
1294
|
line_height = LINE_START
|
|
1071
1295
|
end_of_line = "</g>"
|
|
1072
1296
|
|
|
1297
|
+
column_thresholds = _select_planet_grid_thresholds(chart_type)
|
|
1298
|
+
|
|
1073
1299
|
for i, t_planet in enumerate(second_subject_available_kerykeion_celestial_points):
|
|
1074
|
-
offset, row_index = _planet_grid_layout_position(i)
|
|
1300
|
+
offset, row_index = _planet_grid_layout_position(i, column_thresholds)
|
|
1075
1301
|
line_height = LINE_START + (row_index * LINE_STEP)
|
|
1076
1302
|
|
|
1077
1303
|
second_decoded_name = get_decoded_kerykeion_celestial_point_name(
|
|
@@ -1219,121 +1445,89 @@ def format_datetime_with_timezone(iso_datetime_string: str) -> str:
|
|
|
1219
1445
|
|
|
1220
1446
|
|
|
1221
1447
|
def calculate_element_points(
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1448
|
+
planets_settings: Sequence[KerykeionSettingsCelestialPointModel],
|
|
1449
|
+
celestial_points_names: Sequence[str],
|
|
1450
|
+
subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
|
|
1451
|
+
*,
|
|
1452
|
+
method: ElementQualityDistributionMethod = "weighted",
|
|
1453
|
+
custom_weights: Optional[Mapping[str, float]] = None,
|
|
1454
|
+
) -> dict[str, float]:
|
|
1226
1455
|
"""
|
|
1227
|
-
Calculate elemental
|
|
1456
|
+
Calculate elemental totals for a subject using the selected strategy.
|
|
1228
1457
|
|
|
1229
1458
|
Args:
|
|
1230
|
-
planets_settings
|
|
1231
|
-
celestial_points_names
|
|
1232
|
-
subject: Astrological subject with
|
|
1459
|
+
planets_settings: Planet configuration list (kept for API compatibility).
|
|
1460
|
+
celestial_points_names: Celestial point names to include.
|
|
1461
|
+
subject: Astrological subject with planetary data.
|
|
1462
|
+
method: Calculation method (pure_count or weighted). Defaults to weighted.
|
|
1463
|
+
custom_weights: Optional overrides for point weights keyed by name.
|
|
1233
1464
|
|
|
1234
1465
|
Returns:
|
|
1235
|
-
|
|
1236
|
-
"""
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
{"name": "Cap", "element": "earth"},
|
|
1248
|
-
{"name": "Aqu", "element": "air"},
|
|
1249
|
-
{"name": "Pis", "element": "water"},
|
|
1466
|
+
Dictionary mapping each element to its accumulated total.
|
|
1467
|
+
"""
|
|
1468
|
+
normalized_names = [name.lower() for name in celestial_points_names]
|
|
1469
|
+
weight_lookup, fallback_weight = _prepare_weight_lookup(method, custom_weights)
|
|
1470
|
+
|
|
1471
|
+
return _calculate_distribution_for_subject(
|
|
1472
|
+
subject,
|
|
1473
|
+
normalized_names,
|
|
1474
|
+
_SIGN_TO_ELEMENT,
|
|
1475
|
+
_ELEMENT_KEYS,
|
|
1476
|
+
weight_lookup,
|
|
1477
|
+
fallback_weight,
|
|
1250
1478
|
)
|
|
1251
1479
|
|
|
1252
|
-
# Initialize element point totals
|
|
1253
|
-
element_totals = {
|
|
1254
|
-
"fire": 0.0,
|
|
1255
|
-
"earth": 0.0,
|
|
1256
|
-
"air": 0.0,
|
|
1257
|
-
"water": 0.0
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
# Make list of the points sign
|
|
1261
|
-
points_sign = [subject.get(planet).sign_num for planet in celestial_points_names]
|
|
1262
|
-
|
|
1263
|
-
for i in range(len(planets_settings)):
|
|
1264
|
-
# Add points to appropriate element
|
|
1265
|
-
element = ZODIAC[points_sign[i]]["element"]
|
|
1266
|
-
element_totals[element] += planets_settings[i]["element_points"]
|
|
1267
|
-
|
|
1268
|
-
return element_totals
|
|
1269
|
-
|
|
1270
1480
|
|
|
1271
1481
|
def calculate_synastry_element_points(
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1482
|
+
planets_settings: Sequence[KerykeionSettingsCelestialPointModel],
|
|
1483
|
+
celestial_points_names: Sequence[str],
|
|
1484
|
+
subject1: AstrologicalSubjectModel,
|
|
1485
|
+
subject2: AstrologicalSubjectModel,
|
|
1486
|
+
*,
|
|
1487
|
+
method: ElementQualityDistributionMethod = "weighted",
|
|
1488
|
+
custom_weights: Optional[Mapping[str, float]] = None,
|
|
1489
|
+
) -> dict[str, float]:
|
|
1277
1490
|
"""
|
|
1278
|
-
Calculate
|
|
1491
|
+
Calculate combined element percentages for a synastry chart.
|
|
1279
1492
|
|
|
1280
1493
|
Args:
|
|
1281
|
-
planets_settings
|
|
1282
|
-
celestial_points_names
|
|
1283
|
-
subject1: First astrological subject
|
|
1284
|
-
subject2: Second astrological subject
|
|
1494
|
+
planets_settings: Planet configuration list (unused but preserved).
|
|
1495
|
+
celestial_points_names: Celestial point names to process.
|
|
1496
|
+
subject1: First astrological subject.
|
|
1497
|
+
subject2: Second astrological subject.
|
|
1498
|
+
method: Calculation strategy (pure_count or weighted).
|
|
1499
|
+
custom_weights: Optional overrides for point weights.
|
|
1285
1500
|
|
|
1286
1501
|
Returns:
|
|
1287
|
-
|
|
1288
|
-
"""
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1502
|
+
Dictionary with element percentages summing to 100.
|
|
1503
|
+
"""
|
|
1504
|
+
normalized_names = [name.lower() for name in celestial_points_names]
|
|
1505
|
+
weight_lookup, fallback_weight = _prepare_weight_lookup(method, custom_weights)
|
|
1506
|
+
|
|
1507
|
+
subject1_totals = _calculate_distribution_for_subject(
|
|
1508
|
+
subject1,
|
|
1509
|
+
normalized_names,
|
|
1510
|
+
_SIGN_TO_ELEMENT,
|
|
1511
|
+
_ELEMENT_KEYS,
|
|
1512
|
+
weight_lookup,
|
|
1513
|
+
fallback_weight,
|
|
1514
|
+
)
|
|
1515
|
+
subject2_totals = _calculate_distribution_for_subject(
|
|
1516
|
+
subject2,
|
|
1517
|
+
normalized_names,
|
|
1518
|
+
_SIGN_TO_ELEMENT,
|
|
1519
|
+
_ELEMENT_KEYS,
|
|
1520
|
+
weight_lookup,
|
|
1521
|
+
fallback_weight,
|
|
1302
1522
|
)
|
|
1303
1523
|
|
|
1304
|
-
|
|
1305
|
-
combined_totals = {
|
|
1306
|
-
"fire": 0.0,
|
|
1307
|
-
"earth": 0.0,
|
|
1308
|
-
"air": 0.0,
|
|
1309
|
-
"water": 0.0
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
# Make list of the points sign for both subjects
|
|
1313
|
-
subject1_points_sign = [subject1.get(planet).sign_num for planet in celestial_points_names]
|
|
1314
|
-
subject2_points_sign = [subject2.get(planet).sign_num for planet in celestial_points_names]
|
|
1315
|
-
|
|
1316
|
-
# Calculate element points for subject 1
|
|
1317
|
-
for i in range(len(planets_settings)):
|
|
1318
|
-
# Add points to appropriate element
|
|
1319
|
-
element1 = ZODIAC[subject1_points_sign[i]]["element"]
|
|
1320
|
-
combined_totals[element1] += planets_settings[i]["element_points"]
|
|
1321
|
-
|
|
1322
|
-
# Calculate element points for subject 2
|
|
1323
|
-
for i in range(len(planets_settings)):
|
|
1324
|
-
# Add points to appropriate element
|
|
1325
|
-
element2 = ZODIAC[subject2_points_sign[i]]["element"]
|
|
1326
|
-
combined_totals[element2] += planets_settings[i]["element_points"]
|
|
1327
|
-
|
|
1328
|
-
# Calculate total points across all elements
|
|
1524
|
+
combined_totals = {key: subject1_totals[key] + subject2_totals[key] for key in _ELEMENT_KEYS}
|
|
1329
1525
|
total_points = sum(combined_totals.values())
|
|
1330
1526
|
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
for element in combined_totals:
|
|
1334
|
-
combined_totals[element] = (combined_totals[element] / total_points) * 100.0
|
|
1527
|
+
if total_points == 0:
|
|
1528
|
+
return {key: 0.0 for key in _ELEMENT_KEYS}
|
|
1335
1529
|
|
|
1336
|
-
return combined_totals
|
|
1530
|
+
return {key: (combined_totals[key] / total_points) * 100.0 for key in _ELEMENT_KEYS}
|
|
1337
1531
|
|
|
1338
1532
|
|
|
1339
1533
|
def draw_house_comparison_grid(
|
|
@@ -1534,117 +1728,86 @@ def makeLunarPhase(degrees_between_sun_and_moon: float, latitude: float) -> str:
|
|
|
1534
1728
|
|
|
1535
1729
|
|
|
1536
1730
|
def calculate_quality_points(
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1731
|
+
planets_settings: Sequence[KerykeionSettingsCelestialPointModel],
|
|
1732
|
+
celestial_points_names: Sequence[str],
|
|
1733
|
+
subject: Union[AstrologicalSubjectModel, CompositeSubjectModel, PlanetReturnModel],
|
|
1734
|
+
*,
|
|
1735
|
+
method: ElementQualityDistributionMethod = "weighted",
|
|
1736
|
+
custom_weights: Optional[Mapping[str, float]] = None,
|
|
1737
|
+
) -> dict[str, float]:
|
|
1541
1738
|
"""
|
|
1542
|
-
Calculate
|
|
1739
|
+
Calculate modality totals for a subject using the selected strategy.
|
|
1543
1740
|
|
|
1544
1741
|
Args:
|
|
1545
|
-
planets_settings
|
|
1546
|
-
celestial_points_names
|
|
1547
|
-
subject: Astrological subject with
|
|
1548
|
-
|
|
1742
|
+
planets_settings: Planet configuration list (kept for API compatibility).
|
|
1743
|
+
celestial_points_names: Celestial point names to include.
|
|
1744
|
+
subject: Astrological subject with planetary data.
|
|
1745
|
+
method: Calculation method (pure_count or weighted). Defaults to weighted.
|
|
1746
|
+
custom_weights: Optional overrides for point weights keyed by name.
|
|
1549
1747
|
|
|
1550
1748
|
Returns:
|
|
1551
|
-
|
|
1552
|
-
"""
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
{"name": "Cap", "quality": "cardinal"},
|
|
1564
|
-
{"name": "Aqu", "quality": "fixed"},
|
|
1565
|
-
{"name": "Pis", "quality": "mutable"},
|
|
1749
|
+
Dictionary mapping each modality to its accumulated total.
|
|
1750
|
+
"""
|
|
1751
|
+
normalized_names = [name.lower() for name in celestial_points_names]
|
|
1752
|
+
weight_lookup, fallback_weight = _prepare_weight_lookup(method, custom_weights)
|
|
1753
|
+
|
|
1754
|
+
return _calculate_distribution_for_subject(
|
|
1755
|
+
subject,
|
|
1756
|
+
normalized_names,
|
|
1757
|
+
_SIGN_TO_QUALITY,
|
|
1758
|
+
_QUALITY_KEYS,
|
|
1759
|
+
weight_lookup,
|
|
1760
|
+
fallback_weight,
|
|
1566
1761
|
)
|
|
1567
1762
|
|
|
1568
|
-
# Initialize quality point totals
|
|
1569
|
-
quality_totals = {
|
|
1570
|
-
"cardinal": 0.0,
|
|
1571
|
-
"fixed": 0.0,
|
|
1572
|
-
"mutable": 0.0
|
|
1573
|
-
}
|
|
1574
|
-
|
|
1575
|
-
# Make list of the points sign
|
|
1576
|
-
points_sign = [subject.get(planet).sign_num for planet in celestial_points_names]
|
|
1577
|
-
|
|
1578
|
-
for i in range(len(planets_settings)):
|
|
1579
|
-
# Add points to appropriate quality
|
|
1580
|
-
quality = ZODIAC[points_sign[i]]["quality"]
|
|
1581
|
-
quality_totals[quality] += planets_settings[i]["element_points"]
|
|
1582
|
-
|
|
1583
|
-
return quality_totals
|
|
1584
|
-
|
|
1585
1763
|
|
|
1586
1764
|
def calculate_synastry_quality_points(
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1765
|
+
planets_settings: Sequence[KerykeionSettingsCelestialPointModel],
|
|
1766
|
+
celestial_points_names: Sequence[str],
|
|
1767
|
+
subject1: AstrologicalSubjectModel,
|
|
1768
|
+
subject2: AstrologicalSubjectModel,
|
|
1769
|
+
*,
|
|
1770
|
+
method: ElementQualityDistributionMethod = "weighted",
|
|
1771
|
+
custom_weights: Optional[Mapping[str, float]] = None,
|
|
1772
|
+
) -> dict[str, float]:
|
|
1592
1773
|
"""
|
|
1593
|
-
Calculate
|
|
1774
|
+
Calculate combined modality percentages for a synastry chart.
|
|
1594
1775
|
|
|
1595
1776
|
Args:
|
|
1596
|
-
planets_settings
|
|
1597
|
-
celestial_points_names
|
|
1598
|
-
subject1: First astrological subject
|
|
1599
|
-
subject2: Second astrological subject
|
|
1777
|
+
planets_settings: Planet configuration list (unused but preserved).
|
|
1778
|
+
celestial_points_names: Celestial point names to process.
|
|
1779
|
+
subject1: First astrological subject.
|
|
1780
|
+
subject2: Second astrological subject.
|
|
1781
|
+
method: Calculation strategy (pure_count or weighted).
|
|
1782
|
+
custom_weights: Optional overrides for point weights.
|
|
1600
1783
|
|
|
1601
1784
|
Returns:
|
|
1602
|
-
|
|
1603
|
-
"""
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1785
|
+
Dictionary with modality percentages summing to 100.
|
|
1786
|
+
"""
|
|
1787
|
+
normalized_names = [name.lower() for name in celestial_points_names]
|
|
1788
|
+
weight_lookup, fallback_weight = _prepare_weight_lookup(method, custom_weights)
|
|
1789
|
+
|
|
1790
|
+
subject1_totals = _calculate_distribution_for_subject(
|
|
1791
|
+
subject1,
|
|
1792
|
+
normalized_names,
|
|
1793
|
+
_SIGN_TO_QUALITY,
|
|
1794
|
+
_QUALITY_KEYS,
|
|
1795
|
+
weight_lookup,
|
|
1796
|
+
fallback_weight,
|
|
1797
|
+
)
|
|
1798
|
+
subject2_totals = _calculate_distribution_for_subject(
|
|
1799
|
+
subject2,
|
|
1800
|
+
normalized_names,
|
|
1801
|
+
_SIGN_TO_QUALITY,
|
|
1802
|
+
_QUALITY_KEYS,
|
|
1803
|
+
weight_lookup,
|
|
1804
|
+
fallback_weight,
|
|
1617
1805
|
)
|
|
1618
1806
|
|
|
1619
|
-
|
|
1620
|
-
combined_totals = {
|
|
1621
|
-
"cardinal": 0.0,
|
|
1622
|
-
"fixed": 0.0,
|
|
1623
|
-
"mutable": 0.0
|
|
1624
|
-
}
|
|
1625
|
-
|
|
1626
|
-
# Make list of the points sign for both subjects
|
|
1627
|
-
subject1_points_sign = [subject1.get(planet).sign_num for planet in celestial_points_names]
|
|
1628
|
-
subject2_points_sign = [subject2.get(planet).sign_num for planet in celestial_points_names]
|
|
1629
|
-
|
|
1630
|
-
# Calculate quality points for subject 1
|
|
1631
|
-
for i in range(len(planets_settings)):
|
|
1632
|
-
# Add points to appropriate quality
|
|
1633
|
-
quality1 = ZODIAC[subject1_points_sign[i]]["quality"]
|
|
1634
|
-
combined_totals[quality1] += planets_settings[i]["element_points"]
|
|
1635
|
-
|
|
1636
|
-
# Calculate quality points for subject 2
|
|
1637
|
-
for i in range(len(planets_settings)):
|
|
1638
|
-
# Add points to appropriate quality
|
|
1639
|
-
quality2 = ZODIAC[subject2_points_sign[i]]["quality"]
|
|
1640
|
-
combined_totals[quality2] += planets_settings[i]["element_points"]
|
|
1641
|
-
|
|
1642
|
-
# Calculate total points across all qualities
|
|
1807
|
+
combined_totals = {key: subject1_totals[key] + subject2_totals[key] for key in _QUALITY_KEYS}
|
|
1643
1808
|
total_points = sum(combined_totals.values())
|
|
1644
1809
|
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
for quality in combined_totals:
|
|
1648
|
-
combined_totals[quality] = (combined_totals[quality] / total_points) * 100.0
|
|
1810
|
+
if total_points == 0:
|
|
1811
|
+
return {key: 0.0 for key in _QUALITY_KEYS}
|
|
1649
1812
|
|
|
1650
|
-
return combined_totals
|
|
1813
|
+
return {key: (combined_totals[key] / total_points) * 100.0 for key in _QUALITY_KEYS}
|