Files
Meal-Planner/backend/app/services/planner/select.py
T
adminandClaude Sonnet 4.6 35f736a052 fix(planner): correct set_size to 3 dinners and switch cost filter to per-serving
- set_size 21→3, top_k 20→10: generate 3 weekly dinners not full 3×7 matrix
- All 3 items assigned MealType.DINNER on Mon/Wed/Fri
- RecipeCost gains servings field + cost_per_serving property
- compute_recipe_cost accepts servings param (default 4)
- filter.py gates on cost_per_serving instead of total_cost
- max_meal_cost 500→50 (now a meaningful $/serving threshold)
- Email displays ~$X/serving instead of inflated raw total
- select_set: candidate_pool uses max(top_k, set_size) to prevent
  combinations(n<set_size) returning empty iterator

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 07:20:17 -07:00

55 lines
1.7 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)
# top_k must cover at least set_size items or combinations() yields nothing
candidate_pool = pool[: max(config.top_k, config.set_size)]
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