Files
Meal-Planner/backend/app/services/planner/select.py
T

54 lines
1.6 KiB
Python

"""Top-K set enumeration with diversity penalty.
Takes the top K (=20) scored recipes, enumerates all C(K, set_size)
combinations, applies a pairwise diversity penalty for shared
protein and cuisine, and returns the highest-scoring combination.
"""
from __future__ import annotations
from itertools import combinations
from typing import Iterable, List, Tuple
from app.services.planner.config import PlannerConfig
from app.services.planner.types import ScoredRecipe
def set_diversity_penalty(
chosen: List[ScoredRecipe],
config: PlannerConfig,
) -> float:
penalty = 0.0
for a, b in combinations(chosen, 2):
if a.protein and b.protein and a.protein == b.protein:
penalty += config.p_protein
if a.cuisine and b.cuisine and a.cuisine == b.cuisine:
penalty += config.p_cuisine
return penalty
def _set_score(chosen: List[ScoredRecipe], config: PlannerConfig) -> float:
return sum(s.score for s in chosen) - set_diversity_penalty(chosen, config)
def select_set(
scored: Iterable[ScoredRecipe],
config: PlannerConfig,
) -> Tuple[List[ScoredRecipe], float]:
pool = list(scored)
if not pool:
return [], 0.0
if len(pool) <= config.set_size:
return pool, _set_score(pool, config)
pool.sort(key=lambda s: s.score, reverse=True)
candidate_pool = pool[: config.top_k]
best: List[ScoredRecipe] = []
best_score = float("-inf")
for combo in combinations(candidate_pool, config.set_size):
s = _set_score(list(combo), config)
if s > best_score:
best_score = s
best = list(combo)
return best, best_score