# Configurable Maximize Criteria for `solve()` **Date:** 2026-06-11 **Status:** Approved ## Problem The objective in `solve.py` is hardcoded at the bottom of `solve()` (the "OBJECTIVE IS SET HERE" block): it maximizes the monomial `finalE² · finalB¹ · finalS²` via hand-chained `AddMultiplicationEquality` calls, with intermediate bounds derived from the Phase-1 per-resource ceilings. Changing the criteria means editing solver internals. The actual usage pattern (per git history) is varying the per-resource powers, so the criteria should be a parameter of `solve()`. ## API Two new keyword-only arguments on `solve()`, with module-level defaults in the PARAMETERS section (matching the existing `INITIAL` / `FIXED_CHOICES` pattern): ```python OBJECTIVE_FACTORS = {"E": 2, "B": 1, "S": 2} # missing keys = 0 (excluded) OBJECTIVE_MODE = "product" # "product" or "sum" def solve(*, ..., objective_factors=None, objective_mode=None): ``` - `None` for either argument falls back to the module-level default, the same convention `resource_constraints` already uses. - Defaults reproduce the current hardcoded objective (`E²·B·S²`) exactly. - Valid factor keys: `"E"`, `"B"`, `"S"`, `"C"` (Capital becomes targetable). Values are integers. ### Validation (fail fast with `ValueError`) - Unknown key in `objective_factors`. - All factors zero (or empty dict) — degenerate objective. - `objective_mode` not in `{"product", "sum"}`. - Non-integer factor values. - Negative factor values **in product mode only**: a negative exponent means division, which integer CP-SAT cannot express. Sum mode accepts negative weights — CP-SAT handles negative coefficients in linear objectives natively. ## Semantics The factor means **exponent** in product mode and **weight** in sum mode. - **`product`**: maximize `Πₖ finalₖ^factorₖ`. Built generically: expand the factors to a flat list of final-resource vars (e.g. `[finalE, finalE, finalB, finalS, finalS]`), then fold pairwise with `AddMultiplicationEquality`, carrying a running upper bound multiplied from the Phase-1 caps. This replaces the hand-chained `prodEE`/`prodSS`/ `prodBB`/`prodEB`/`obj` block and the `Eprod`/`Bprod`/`Sprod` bound helpers. - **`sum`**: maximize `Σₖ factorₖ · finalₖ` — a single `m.Maximize(...)` on a linear expression; no auxiliary variables. May be negative when negative weights are used. A factor of 0 drops the resource from the objective (exponent 0 → factor of 1 in product mode), matching current behavior where Capital simply isn't in the objective. It does not force the resource to zero. ## Phase-1 ceilings Ceilings (`_ceiling`) are computed only for resources with a nonzero factor. Rationale: - Each ceiling solve costs up to 20 s; skipping unused resources is a real win. - `C` gets a ceiling only when it appears in a product objective, where the bound is required for the intermediate product variables. - Sum mode does not need caps for bounds, but the redundant `final ≤ cap` constraints are still added for computed ceilings since they prune the search. ## Reporting `_report` receives the resolved objective spec (factors + mode) and prints the expression that was actually maximized along with its value, replacing the hardcoded `product(scaled) = E·B·S` line. Examples: - product mode: `objective E^2*B*S^2 = ` with the value descaled by `10^(sum of exponents)`. - sum mode: `objective 2E + B - S = ` descaled by 10. `finalC` (`C[NUM_STEPS + 1]`) participates in the printout when `"C"` has a nonzero factor; `_report` already receives the `C` pool dict. The per-resource FINAL line and the intermediate solution printer (E/B/S) remain unchanged. ## Unchanged - The intermediate solution printer still tracks E/B/S. - `web_solve.py` keeps working unmodified — both new arguments are optional with behavior-preserving defaults. ## Bounds note With `MAX_RES = 2000`, the default product bound is `2000⁵ = 3.2 × 10¹⁶`, comfortably inside CP-SAT's int64 objective range. Extreme factor values could overflow; the fold computes the running bound explicitly, so an overflow would surface as a CP-SAT model error rather than silent wraparound. No additional guard is included (YAGNI).