kerykeion 5.0.0b1__py3-none-any.whl → 5.0.0b4__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 +601 -206
- kerykeion/charts/charts_utils.py +440 -255
- kerykeion/charts/templates/aspect_grid_only.xml +269 -312
- kerykeion/charts/templates/chart.xml +302 -328
- kerykeion/charts/templates/wheel_only.xml +271 -312
- kerykeion/charts/themes/black-and-white.css +148 -0
- kerykeion/relationship_score_factory.py +12 -2
- kerykeion/schemas/chart_template_model.py +27 -0
- kerykeion/schemas/kr_literals.py +1 -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.0b1.dist-info → kerykeion-5.0.0b4.dist-info}/METADATA +304 -204
- {kerykeion-5.0.0b1.dist-info → kerykeion-5.0.0b4.dist-info}/RECORD +25 -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.0b1.dist-info → kerykeion-5.0.0b4.dist-info}/WHEEL +0 -0
- {kerykeion-5.0.0b1.dist-info → kerykeion-5.0.0b4.dist-info}/licenses/LICENSE +0 -0
kerykeion/charts/charts_utils.py
CHANGED
|
@@ -1,12 +1,231 @@
|
|
|
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
|
+
|
|
183
|
+
_SECOND_COLUMN_THRESHOLD = 20
|
|
184
|
+
_THIRD_COLUMN_THRESHOLD = 28
|
|
185
|
+
_FOURTH_COLUMN_THRESHOLD = 36
|
|
186
|
+
|
|
187
|
+
_DOUBLE_CHART_TYPES: tuple[ChartType, ...] = ("Synastry", "Transit", "DualReturnChart")
|
|
188
|
+
_GRID_COLUMN_WIDTH = 125
|
|
189
|
+
|
|
190
|
+
|
|
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]:
|
|
205
|
+
"""Return horizontal offset and row index for planet grids."""
|
|
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:
|
|
213
|
+
column = 0
|
|
214
|
+
row = index
|
|
215
|
+
elif index < third_threshold:
|
|
216
|
+
column = 1
|
|
217
|
+
row = index - second_threshold
|
|
218
|
+
elif index < fourth_threshold:
|
|
219
|
+
column = 2
|
|
220
|
+
row = index - third_threshold
|
|
221
|
+
else:
|
|
222
|
+
column = 3
|
|
223
|
+
row = index - fourth_threshold
|
|
224
|
+
|
|
225
|
+
offset = -(_GRID_COLUMN_WIDTH * column)
|
|
226
|
+
return offset, row
|
|
227
|
+
|
|
228
|
+
|
|
10
229
|
|
|
11
230
|
def get_decoded_kerykeion_celestial_point_name(input_planet_name: str, celestial_point_language: KerykeionLanguageCelestialPointModel) -> str:
|
|
12
231
|
"""
|
|
@@ -687,7 +906,8 @@ def draw_transit_aspect_list(
|
|
|
687
906
|
aspects_per_column: int = 14,
|
|
688
907
|
column_width: int = 100,
|
|
689
908
|
line_height: int = 14,
|
|
690
|
-
max_columns: int = 6
|
|
909
|
+
max_columns: int = 6,
|
|
910
|
+
chart_height: Optional[int] = None,
|
|
691
911
|
) -> str:
|
|
692
912
|
"""
|
|
693
913
|
Generates the SVG output for the aspect transit grid.
|
|
@@ -701,6 +921,8 @@ def draw_transit_aspect_list(
|
|
|
701
921
|
- column_width: Width in pixels for each column (default: 100).
|
|
702
922
|
- line_height: Height in pixels for each line (default: 14).
|
|
703
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).
|
|
704
926
|
|
|
705
927
|
Returns:
|
|
706
928
|
- A string containing the SVG path data for the aspect transit grid.
|
|
@@ -716,61 +938,86 @@ def draw_transit_aspect_list(
|
|
|
716
938
|
# Type narrowing: at this point aspects_list contains AspectModel instances
|
|
717
939
|
typed_aspects_list: list[AspectModel] = aspects_list # type: ignore
|
|
718
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
|
+
|
|
719
953
|
inner_path = ""
|
|
720
954
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
if
|
|
735
|
-
|
|
736
|
-
if
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
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})">'
|
|
771
1018
|
out += f'<text y="-15" x="0" style="fill: var(--kerykeion-chart-color-paper-0); font-size: 14px;">{grid_title}:</text>'
|
|
772
1019
|
out += inner_path
|
|
773
|
-
out +=
|
|
1020
|
+
out += "</g>"
|
|
774
1021
|
|
|
775
1022
|
return out
|
|
776
1023
|
|
|
@@ -960,16 +1207,13 @@ def draw_main_planet_grid(
|
|
|
960
1207
|
f'</g>'
|
|
961
1208
|
)
|
|
962
1209
|
|
|
963
|
-
line_height = LINE_START
|
|
964
|
-
offset = 0
|
|
965
|
-
|
|
966
1210
|
end_of_line = "</g>"
|
|
967
1211
|
|
|
1212
|
+
column_thresholds = _select_planet_grid_thresholds(chart_type)
|
|
1213
|
+
|
|
968
1214
|
for i, planet in enumerate(available_kerykeion_celestial_points):
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
line_height = LINE_START
|
|
972
|
-
offset = -125
|
|
1215
|
+
offset, row_index = _planet_grid_layout_position(i, column_thresholds)
|
|
1216
|
+
line_height = LINE_START + (row_index * LINE_STEP)
|
|
973
1217
|
|
|
974
1218
|
decoded_name = get_decoded_kerykeion_celestial_point_name(
|
|
975
1219
|
planet["name"],
|
|
@@ -988,7 +1232,6 @@ def draw_main_planet_grid(
|
|
|
988
1232
|
svg_output += '<g transform="translate(74,-6)"><use transform="scale(.5)" xlink:href="#retrograde" /></g>'
|
|
989
1233
|
|
|
990
1234
|
svg_output += end_of_line
|
|
991
|
-
line_height += LINE_STEP
|
|
992
1235
|
|
|
993
1236
|
# Close the wrapper group
|
|
994
1237
|
svg_output += "</g>"
|
|
@@ -1051,13 +1294,18 @@ def draw_secondary_planet_grid(
|
|
|
1051
1294
|
line_height = LINE_START
|
|
1052
1295
|
end_of_line = "</g>"
|
|
1053
1296
|
|
|
1054
|
-
|
|
1297
|
+
column_thresholds = _select_planet_grid_thresholds(chart_type)
|
|
1298
|
+
|
|
1299
|
+
for i, t_planet in enumerate(second_subject_available_kerykeion_celestial_points):
|
|
1300
|
+
offset, row_index = _planet_grid_layout_position(i, column_thresholds)
|
|
1301
|
+
line_height = LINE_START + (row_index * LINE_STEP)
|
|
1302
|
+
|
|
1055
1303
|
second_decoded_name = get_decoded_kerykeion_celestial_point_name(
|
|
1056
1304
|
t_planet["name"],
|
|
1057
1305
|
celestial_point_language,
|
|
1058
1306
|
)
|
|
1059
1307
|
svg_output += (
|
|
1060
|
-
f'<g transform="translate(
|
|
1308
|
+
f'<g transform="translate({offset},{BASE_Y + line_height})">'
|
|
1061
1309
|
f'<text text-anchor="end" style="fill:{text_color}; font-size: 10px;">{second_decoded_name}</text>'
|
|
1062
1310
|
f'<g transform="translate(5,-8)"><use transform="scale(0.4)" xlink:href="#{t_planet["name"]}" /></g>'
|
|
1063
1311
|
f'<text text-anchor="start" x="19" style="fill:{text_color}; font-size: 10px;">{convert_decimal_to_degree_string(t_planet["position"])}</text>'
|
|
@@ -1068,7 +1316,7 @@ def draw_secondary_planet_grid(
|
|
|
1068
1316
|
svg_output += '<g transform="translate(74,-6)"><use transform="scale(.5)" xlink:href="#retrograde" /></g>'
|
|
1069
1317
|
|
|
1070
1318
|
svg_output += end_of_line
|
|
1071
|
-
|
|
1319
|
+
|
|
1072
1320
|
|
|
1073
1321
|
# Close wrapper group
|
|
1074
1322
|
svg_output += "</g>"
|
|
@@ -1197,121 +1445,89 @@ def format_datetime_with_timezone(iso_datetime_string: str) -> str:
|
|
|
1197
1445
|
|
|
1198
1446
|
|
|
1199
1447
|
def calculate_element_points(
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
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]:
|
|
1204
1455
|
"""
|
|
1205
|
-
Calculate elemental
|
|
1456
|
+
Calculate elemental totals for a subject using the selected strategy.
|
|
1206
1457
|
|
|
1207
1458
|
Args:
|
|
1208
|
-
planets_settings
|
|
1209
|
-
celestial_points_names
|
|
1210
|
-
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.
|
|
1211
1464
|
|
|
1212
1465
|
Returns:
|
|
1213
|
-
|
|
1214
|
-
"""
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
{"name": "Cap", "element": "earth"},
|
|
1226
|
-
{"name": "Aqu", "element": "air"},
|
|
1227
|
-
{"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,
|
|
1228
1478
|
)
|
|
1229
1479
|
|
|
1230
|
-
# Initialize element point totals
|
|
1231
|
-
element_totals = {
|
|
1232
|
-
"fire": 0.0,
|
|
1233
|
-
"earth": 0.0,
|
|
1234
|
-
"air": 0.0,
|
|
1235
|
-
"water": 0.0
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
# Make list of the points sign
|
|
1239
|
-
points_sign = [subject.get(planet).sign_num for planet in celestial_points_names]
|
|
1240
|
-
|
|
1241
|
-
for i in range(len(planets_settings)):
|
|
1242
|
-
# Add points to appropriate element
|
|
1243
|
-
element = ZODIAC[points_sign[i]]["element"]
|
|
1244
|
-
element_totals[element] += planets_settings[i]["element_points"]
|
|
1245
|
-
|
|
1246
|
-
return element_totals
|
|
1247
|
-
|
|
1248
1480
|
|
|
1249
1481
|
def calculate_synastry_element_points(
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
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]:
|
|
1255
1490
|
"""
|
|
1256
|
-
Calculate
|
|
1491
|
+
Calculate combined element percentages for a synastry chart.
|
|
1257
1492
|
|
|
1258
1493
|
Args:
|
|
1259
|
-
planets_settings
|
|
1260
|
-
celestial_points_names
|
|
1261
|
-
subject1: First astrological subject
|
|
1262
|
-
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.
|
|
1263
1500
|
|
|
1264
1501
|
Returns:
|
|
1265
|
-
|
|
1266
|
-
"""
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
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,
|
|
1280
1522
|
)
|
|
1281
1523
|
|
|
1282
|
-
|
|
1283
|
-
combined_totals = {
|
|
1284
|
-
"fire": 0.0,
|
|
1285
|
-
"earth": 0.0,
|
|
1286
|
-
"air": 0.0,
|
|
1287
|
-
"water": 0.0
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
# Make list of the points sign for both subjects
|
|
1291
|
-
subject1_points_sign = [subject1.get(planet).sign_num for planet in celestial_points_names]
|
|
1292
|
-
subject2_points_sign = [subject2.get(planet).sign_num for planet in celestial_points_names]
|
|
1293
|
-
|
|
1294
|
-
# Calculate element points for subject 1
|
|
1295
|
-
for i in range(len(planets_settings)):
|
|
1296
|
-
# Add points to appropriate element
|
|
1297
|
-
element1 = ZODIAC[subject1_points_sign[i]]["element"]
|
|
1298
|
-
combined_totals[element1] += planets_settings[i]["element_points"]
|
|
1299
|
-
|
|
1300
|
-
# Calculate element points for subject 2
|
|
1301
|
-
for i in range(len(planets_settings)):
|
|
1302
|
-
# Add points to appropriate element
|
|
1303
|
-
element2 = ZODIAC[subject2_points_sign[i]]["element"]
|
|
1304
|
-
combined_totals[element2] += planets_settings[i]["element_points"]
|
|
1305
|
-
|
|
1306
|
-
# Calculate total points across all elements
|
|
1524
|
+
combined_totals = {key: subject1_totals[key] + subject2_totals[key] for key in _ELEMENT_KEYS}
|
|
1307
1525
|
total_points = sum(combined_totals.values())
|
|
1308
1526
|
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
for element in combined_totals:
|
|
1312
|
-
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}
|
|
1313
1529
|
|
|
1314
|
-
return combined_totals
|
|
1530
|
+
return {key: (combined_totals[key] / total_points) * 100.0 for key in _ELEMENT_KEYS}
|
|
1315
1531
|
|
|
1316
1532
|
|
|
1317
1533
|
def draw_house_comparison_grid(
|
|
@@ -1512,117 +1728,86 @@ def makeLunarPhase(degrees_between_sun_and_moon: float, latitude: float) -> str:
|
|
|
1512
1728
|
|
|
1513
1729
|
|
|
1514
1730
|
def calculate_quality_points(
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
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]:
|
|
1519
1738
|
"""
|
|
1520
|
-
Calculate
|
|
1739
|
+
Calculate modality totals for a subject using the selected strategy.
|
|
1521
1740
|
|
|
1522
1741
|
Args:
|
|
1523
|
-
planets_settings
|
|
1524
|
-
celestial_points_names
|
|
1525
|
-
subject: Astrological subject with
|
|
1526
|
-
|
|
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.
|
|
1527
1747
|
|
|
1528
1748
|
Returns:
|
|
1529
|
-
|
|
1530
|
-
"""
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
{"name": "Cap", "quality": "cardinal"},
|
|
1542
|
-
{"name": "Aqu", "quality": "fixed"},
|
|
1543
|
-
{"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,
|
|
1544
1761
|
)
|
|
1545
1762
|
|
|
1546
|
-
# Initialize quality point totals
|
|
1547
|
-
quality_totals = {
|
|
1548
|
-
"cardinal": 0.0,
|
|
1549
|
-
"fixed": 0.0,
|
|
1550
|
-
"mutable": 0.0
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
# Make list of the points sign
|
|
1554
|
-
points_sign = [subject.get(planet).sign_num for planet in celestial_points_names]
|
|
1555
|
-
|
|
1556
|
-
for i in range(len(planets_settings)):
|
|
1557
|
-
# Add points to appropriate quality
|
|
1558
|
-
quality = ZODIAC[points_sign[i]]["quality"]
|
|
1559
|
-
quality_totals[quality] += planets_settings[i]["element_points"]
|
|
1560
|
-
|
|
1561
|
-
return quality_totals
|
|
1562
|
-
|
|
1563
1763
|
|
|
1564
1764
|
def calculate_synastry_quality_points(
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
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]:
|
|
1570
1773
|
"""
|
|
1571
|
-
Calculate
|
|
1774
|
+
Calculate combined modality percentages for a synastry chart.
|
|
1572
1775
|
|
|
1573
1776
|
Args:
|
|
1574
|
-
planets_settings
|
|
1575
|
-
celestial_points_names
|
|
1576
|
-
subject1: First astrological subject
|
|
1577
|
-
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.
|
|
1578
1783
|
|
|
1579
1784
|
Returns:
|
|
1580
|
-
|
|
1581
|
-
"""
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
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,
|
|
1595
1805
|
)
|
|
1596
1806
|
|
|
1597
|
-
|
|
1598
|
-
combined_totals = {
|
|
1599
|
-
"cardinal": 0.0,
|
|
1600
|
-
"fixed": 0.0,
|
|
1601
|
-
"mutable": 0.0
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
# Make list of the points sign for both subjects
|
|
1605
|
-
subject1_points_sign = [subject1.get(planet).sign_num for planet in celestial_points_names]
|
|
1606
|
-
subject2_points_sign = [subject2.get(planet).sign_num for planet in celestial_points_names]
|
|
1607
|
-
|
|
1608
|
-
# Calculate quality points for subject 1
|
|
1609
|
-
for i in range(len(planets_settings)):
|
|
1610
|
-
# Add points to appropriate quality
|
|
1611
|
-
quality1 = ZODIAC[subject1_points_sign[i]]["quality"]
|
|
1612
|
-
combined_totals[quality1] += planets_settings[i]["element_points"]
|
|
1613
|
-
|
|
1614
|
-
# Calculate quality points for subject 2
|
|
1615
|
-
for i in range(len(planets_settings)):
|
|
1616
|
-
# Add points to appropriate quality
|
|
1617
|
-
quality2 = ZODIAC[subject2_points_sign[i]]["quality"]
|
|
1618
|
-
combined_totals[quality2] += planets_settings[i]["element_points"]
|
|
1619
|
-
|
|
1620
|
-
# Calculate total points across all qualities
|
|
1807
|
+
combined_totals = {key: subject1_totals[key] + subject2_totals[key] for key in _QUALITY_KEYS}
|
|
1621
1808
|
total_points = sum(combined_totals.values())
|
|
1622
1809
|
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
for quality in combined_totals:
|
|
1626
|
-
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}
|
|
1627
1812
|
|
|
1628
|
-
return combined_totals
|
|
1813
|
+
return {key: (combined_totals[key] / total_points) * 100.0 for key in _QUALITY_KEYS}
|