Public Access
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>
This commit is contained in:
@@ -10,7 +10,7 @@ class PlannerConfig:
|
||||
recency_weeks: int = 4 # constraint #3: no repeat within N weeks
|
||||
calorie_tolerance_pct: int = 20 # constraint #4: ±X% of family.calorie_target
|
||||
max_total_minutes: int = 45 # constraint #5: prep + cook
|
||||
max_meal_cost: float = 30.00 # constraint #6: dollars per meal
|
||||
max_meal_cost: float = 50.00 # constraint #6: dollars per serving (dimensionless proxy — unit conversion not yet implemented)
|
||||
|
||||
# Scoring weights (must sum to 1.0)
|
||||
w_savings: float = 0.30
|
||||
@@ -27,8 +27,8 @@ class PlannerConfig:
|
||||
recency_full_weeks: int = 12 # full bonus when last cooked >= this many weeks ago
|
||||
|
||||
# Set selection
|
||||
top_k: int = 20 # how many feasible recipes to enumerate over
|
||||
set_size: int = 21 # 21 meals/week (3 per day × 7 days)
|
||||
top_k: int = 10 # how many feasible recipes to enumerate over
|
||||
set_size: int = 3 # 3 dinners/week
|
||||
p_protein: float = 0.15 # diversity penalty per shared-protein pair
|
||||
p_cuisine: float = 0.10 # diversity penalty per shared-cuisine pair
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ def compute_recipe_cost(
|
||||
ingredients: Iterable[dict],
|
||||
match_index: Dict[UUID, List[dict]],
|
||||
pantry_ingredient_ids: Set[UUID],
|
||||
servings: int = 4,
|
||||
) -> RecipeCost:
|
||||
line_items: List[IngredientCost] = []
|
||||
total_cost = Decimal("0.00")
|
||||
@@ -119,5 +120,6 @@ def compute_recipe_cost(
|
||||
matched_ingredient_count=matched_count,
|
||||
total_ingredient_count=total,
|
||||
pantry_hit_count=pantry_hits,
|
||||
servings=max(servings, 1),
|
||||
line_items=line_items,
|
||||
)
|
||||
|
||||
@@ -79,7 +79,7 @@ def filter_recipes(
|
||||
if cost is None:
|
||||
rejected[rid] = "no_cost"
|
||||
continue
|
||||
if float(cost.total_cost) > config.max_meal_cost:
|
||||
if float(cost.cost_per_serving) > config.max_meal_cost:
|
||||
rejected[rid] = "cost"
|
||||
continue
|
||||
|
||||
|
||||
@@ -113,6 +113,7 @@ def generate_meal_plan(
|
||||
"protein_type": r.protein_type,
|
||||
"cuisine_tags": list(r.cuisine_tags or []),
|
||||
"ingredients": list(r.ingredients or []),
|
||||
"servings": r.servings or 4,
|
||||
}
|
||||
for r in recipes
|
||||
]
|
||||
@@ -139,6 +140,7 @@ def generate_meal_plan(
|
||||
ingredients=r["ingredients"],
|
||||
match_index=match_index,
|
||||
pantry_ingredient_ids=pantry_ids,
|
||||
servings=r["servings"],
|
||||
)
|
||||
for r in recipe_dicts
|
||||
}
|
||||
@@ -176,10 +178,10 @@ def generate_meal_plan(
|
||||
db.add(plan)
|
||||
db.flush()
|
||||
|
||||
_dinner_days = [1, 3, 5] # Mon, Wed, Fri — spread across the week
|
||||
for index, scored_recipe in enumerate(chosen):
|
||||
day = (index % 7) + 1 # 1..7 (Mon..Sun)
|
||||
meal_type_index = index // 7 # 0=breakfast, 1=lunch, 2=dinner
|
||||
meal_type = [MealType.BREAKFAST, MealType.LUNCH, MealType.DINNER][meal_type_index]
|
||||
day = _dinner_days[index] if index < len(_dinner_days) else index + 1
|
||||
meal_type = MealType.DINNER
|
||||
item = MealPlanItem(
|
||||
meal_plan_id=plan.id,
|
||||
recipe_id=scored_recipe.recipe_id,
|
||||
|
||||
@@ -41,7 +41,8 @@ def select_set(
|
||||
return pool, _set_score(pool, config)
|
||||
|
||||
pool.sort(key=lambda s: s.score, reverse=True)
|
||||
candidate_pool = pool[: config.top_k]
|
||||
# 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")
|
||||
|
||||
@@ -31,8 +31,14 @@ class RecipeCost:
|
||||
matched_ingredient_count: int
|
||||
total_ingredient_count: int
|
||||
pantry_hit_count: int
|
||||
servings: int = 4
|
||||
line_items: List[IngredientCost] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def cost_per_serving(self) -> Decimal:
|
||||
s = max(self.servings, 1)
|
||||
return (self.total_cost / Decimal(s)).quantize(Decimal("0.01"))
|
||||
|
||||
@property
|
||||
def sale_coverage_pct(self) -> float:
|
||||
if self.total_ingredient_count == 0:
|
||||
|
||||
Reference in New Issue
Block a user