From d18e533559a58655d2e8672e8f4c95c1f8ad9809 Mon Sep 17 00:00:00 2001 From: Pagwin Date: Thu, 11 Jun 2026 19:07:58 -0400 Subject: [PATCH] spec: configurable maximize criteria for solve() Co-Authored-By: Claude Fable 5 --- ...026-06-11-configurable-objective-design.md | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-11-configurable-objective-design.md diff --git a/docs/superpowers/specs/2026-06-11-configurable-objective-design.md b/docs/superpowers/specs/2026-06-11-configurable-objective-design.md new file mode 100644 index 0000000..8477f61 --- /dev/null +++ b/docs/superpowers/specs/2026-06-11-configurable-objective-design.md @@ -0,0 +1,105 @@ +# 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).