Files
site/WESP_REL/app/lab/calc/formulate_optimize.py
T
2026-07-17 12:57:18 +03:00

113 lines
3.2 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Callable
from scipy.optimize import minimize
from app.models import Component
ShareTuple = tuple[float, float, float]
ScoreFn = Callable[[ShareTuple], float]
@dataclass
class OptimizeSharesResult:
shares: ShareTuple
score: float
evaluations: int
def _normalize_shares(s1: float, s2: float, min_share: float) -> ShareTuple | None:
s3 = 1.0 - s1 - s2
ms = max(min_share, 0.0)
if s1 < ms - 1e-9 or s2 < ms - 1e-9 or s3 < ms - 1e-9:
return None
if abs(s1 + s2 + s3 - 1.0) > 1e-6:
return None
return (round(s1, 8), round(s2, 8), round(s3, 8))
def _start_points(min_share: float, grid_step: float) -> list[tuple[float, float]]:
"""Multi-start seeds: simplex center, corners, and a few grid_step hints."""
ms = max(min_share, 0.0)
max_pair = max(1.0 - 2 * ms, ms)
center = round((1.0 - ms) / 3.0, 6)
points: list[tuple[float, float]] = [
(center, center),
(ms, ms),
(max_pair, ms),
(ms, max_pair),
(max_pair, max_pair),
]
step = max(grid_step, 0.1)
if ms <= step <= max_pair:
points.append((step, ms))
points.append((ms, step))
deduped: list[tuple[float, float]] = []
seen: set[tuple[float, float]] = set()
for s1, s2 in points:
if s1 + s2 > 1.0 - ms + 1e-9:
continue
key = (round(s1, 6), round(s2, 6))
if key in seen:
continue
seen.add(key)
deduped.append(key)
return deduped
def optimize_shares_for_triplet(
triplet: tuple[Component, Component, Component],
score_fn: ScoreFn,
*,
min_share: float,
grid_step: float = 0.1,
) -> OptimizeSharesResult | None:
del triplet
ms = max(min_share, 0.0)
max_s1 = max(1.0 - 2 * ms, ms)
evaluations = 0
def objective(x: Any) -> float:
nonlocal evaluations
evaluations += 1
shares = _normalize_shares(float(x[0]), float(x[1]), ms)
if shares is None:
return 1e18
return score_fn(shares)
bounds = [(ms, max_s1), (ms, max_s1)]
constraints = [{"type": "ineq", "fun": lambda x: 1.0 - ms - float(x[0]) - float(x[1])}]
best_score: float | None = None
best_shares: ShareTuple | None = None
for s1, s2 in _start_points(ms, grid_step):
if s1 + s2 > 1.0 - ms + 1e-9:
continue
try:
res = minimize(
objective,
[s1, s2],
method="SLSQP",
bounds=bounds,
constraints=constraints,
options={"ftol": 1e-8, "maxiter": 40},
)
except Exception:
continue
if not res.success and res.fun >= 1e17:
continue
shares = _normalize_shares(float(res.x[0]), float(res.x[1]), ms)
if shares is None:
continue
score = float(res.fun)
if best_score is None or score < best_score:
best_score = score
best_shares = shares
if best_shares is None or best_score is None:
return None
return OptimizeSharesResult(shares=best_shares, score=best_score, evaluations=evaluations)