|
8 | 8 | """ |
9 | 9 |
|
10 | 10 | import math |
11 | | -from typing import Tuple |
| 11 | +from typing import Iterable, Optional, Tuple |
12 | 12 |
|
13 | | -from harmonizer.divine_invitation_engine_V2 import Coordinates |
| 13 | +from harmonizer.divine_invitation_engine_V2 import Coordinates, Dimension |
14 | 14 |
|
15 | 15 |
|
16 | 16 | class CoordinateUtils: |
@@ -194,6 +194,157 @@ def from_tuple(coord_tuple: Tuple[float, float, float, float]) -> Coordinates: |
194 | 194 | wisdom=coord_tuple[3], |
195 | 195 | ) |
196 | 196 |
|
| 197 | + @staticmethod |
| 198 | + def blend( |
| 199 | + base: Coordinates, |
| 200 | + overlay: Coordinates, |
| 201 | + ratio: float = 0.5, |
| 202 | + ) -> Coordinates: |
| 203 | + """ |
| 204 | + Blend two coordinates together with the supplied ratio. |
| 205 | +
|
| 206 | + Args: |
| 207 | + base: Primary coordinate that receives the overlay. |
| 208 | + overlay: Secondary coordinate to blend into the base. |
| 209 | + ratio: Value between 0.0 and 1.0 indicating overlay strength. |
| 210 | +
|
| 211 | + Returns: |
| 212 | + Blended Coordinates object. |
| 213 | + """ |
| 214 | + ratio = max(0.0, min(1.0, ratio)) |
| 215 | + inverse = 1.0 - ratio |
| 216 | + return Coordinates( |
| 217 | + love=(base.love * inverse) + (overlay.love * ratio), |
| 218 | + justice=(base.justice * inverse) + (overlay.justice * ratio), |
| 219 | + power=(base.power * inverse) + (overlay.power * ratio), |
| 220 | + wisdom=(base.wisdom * inverse) + (overlay.wisdom * ratio), |
| 221 | + ) |
| 222 | + |
| 223 | + @staticmethod |
| 224 | + def weighted_average( |
| 225 | + coords: Iterable[Coordinates], |
| 226 | + weights: Optional[Iterable[float]] = None, |
| 227 | + ) -> Coordinates: |
| 228 | + """ |
| 229 | + Compute a weighted average of coordinates. |
| 230 | +
|
| 231 | + Args: |
| 232 | + coords: Iterable of Coordinates objects. |
| 233 | + weights: Optional iterable of weights (defaults to equal weighting). |
| 234 | +
|
| 235 | + Returns: |
| 236 | + Coordinates representing the weighted average. |
| 237 | + """ |
| 238 | + coords = list(coords) |
| 239 | + if not coords: |
| 240 | + return Coordinates(0.0, 0.0, 0.0, 0.0) |
| 241 | + |
| 242 | + if weights is None: |
| 243 | + weights = [1.0] * len(coords) |
| 244 | + else: |
| 245 | + weights = list(weights) |
| 246 | + if len(weights) != len(coords): |
| 247 | + raise ValueError("weights length must match coords length") |
| 248 | + |
| 249 | + total_weight = sum(weights) |
| 250 | + if total_weight == 0.0: |
| 251 | + return Coordinates(0.0, 0.0, 0.0, 0.0) |
| 252 | + |
| 253 | + love_sum = justice_sum = power_sum = wisdom_sum = 0.0 |
| 254 | + for coord, weight in zip(coords, weights): |
| 255 | + love_sum += coord.love * weight |
| 256 | + justice_sum += coord.justice * weight |
| 257 | + power_sum += coord.power * weight |
| 258 | + wisdom_sum += coord.wisdom * weight |
| 259 | + |
| 260 | + return Coordinates( |
| 261 | + love=love_sum / total_weight, |
| 262 | + justice=justice_sum / total_weight, |
| 263 | + power=power_sum / total_weight, |
| 264 | + wisdom=wisdom_sum / total_weight, |
| 265 | + ) |
| 266 | + |
| 267 | + @staticmethod |
| 268 | + def ensure_power_floor( |
| 269 | + coord: Tuple[float, float, float, float], |
| 270 | + minimum_power: float = 0.2, |
| 271 | + ) -> Tuple[float, float, float, float]: |
| 272 | + """ |
| 273 | + Ensure that a coordinate tuple has at least ``minimum_power`` in the power dimension. |
| 274 | +
|
| 275 | + If power is already at or above the floor, the coordinate is returned unchanged. |
| 276 | + Otherwise the deficit is proportionally reallocated from the remaining dimensions. |
| 277 | + """ |
| 278 | + love, justice, power, wisdom = coord |
| 279 | + if power >= minimum_power: |
| 280 | + return coord |
| 281 | + |
| 282 | + deficit = minimum_power - power |
| 283 | + remaining = love + justice + wisdom |
| 284 | + if remaining == 0.0: |
| 285 | + # Nothing to redistribute, so just set the floor directly. |
| 286 | + return (0.0, 0.0, minimum_power, 0.0) |
| 287 | + |
| 288 | + love_ratio = love / remaining |
| 289 | + justice_ratio = justice / remaining |
| 290 | + wisdom_ratio = wisdom / remaining |
| 291 | + |
| 292 | + love -= deficit * love_ratio |
| 293 | + justice -= deficit * justice_ratio |
| 294 | + wisdom -= deficit * wisdom_ratio |
| 295 | + power = minimum_power |
| 296 | + |
| 297 | + # Normalize to keep the tuple on the unit simplex. |
| 298 | + total = love + justice + power + wisdom |
| 299 | + if total == 0.0: |
| 300 | + return (0.0, 0.0, power, 0.0) |
| 301 | + |
| 302 | + return (love / total, justice / total, power / total, wisdom / total) |
| 303 | + |
| 304 | + @staticmethod |
| 305 | + def prioritize_dimension( |
| 306 | + coord: Tuple[float, float, float, float], |
| 307 | + dimension: Dimension, |
| 308 | + boost: float = 0.05, |
| 309 | + ) -> Tuple[float, float, float, float]: |
| 310 | + """ |
| 311 | + Aggressively boosts a selected dimension by reallocating weight from others. |
| 312 | +
|
| 313 | + Args: |
| 314 | + coord: Original coordinate tuple. |
| 315 | + dimension: Dimension enum value to prioritize (e.g., Dimension.POWER). |
| 316 | + boost: Amount to move into the prioritized dimension. |
| 317 | +
|
| 318 | + Returns: |
| 319 | + Tuple with the prioritized dimension amplified. |
| 320 | + """ |
| 321 | + index_map = { |
| 322 | + Dimension.LOVE: 0, |
| 323 | + Dimension.JUSTICE: 1, |
| 324 | + Dimension.POWER: 2, |
| 325 | + Dimension.WISDOM: 3, |
| 326 | + } |
| 327 | + if dimension not in index_map: |
| 328 | + raise ValueError("dimension must be a primary LJPW dimension") |
| 329 | + |
| 330 | + values = list(coord) |
| 331 | + idx = index_map[dimension] |
| 332 | + boost = max(0.0, boost) |
| 333 | + available_indices = [i for i in range(4) if i != idx] |
| 334 | + available_total = sum(values[i] for i in available_indices) |
| 335 | + if available_total == 0.0: |
| 336 | + values[idx] = min(1.0, values[idx] + boost) |
| 337 | + else: |
| 338 | + for i in available_indices: |
| 339 | + share = boost * (values[i] / available_total) |
| 340 | + values[i] = max(0.0, values[i] - share) |
| 341 | + values[idx] = min(1.0, values[idx] + boost) |
| 342 | + |
| 343 | + total = sum(values) |
| 344 | + if total == 0.0: |
| 345 | + return (0.0, 0.0, 0.0, 0.0) |
| 346 | + return tuple(v / total for v in values) |
| 347 | + |
197 | 348 | @staticmethod |
198 | 349 | def get_dominant_dimension(coord: Tuple[float, float, float, float]) -> str: |
199 | 350 | """ |
|
0 commit comments