spec: configurable maximize criteria for solve()

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Pagwin 2026-06-11 19:07:58 -04:00
parent 023fdefe89
commit d18e533559
No known key found for this signature in database
GPG key ID: 81137023740CA260

View file

@ -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 = <value>` with the value descaled by
`10^(sum of exponents)`.
- sum mode: `objective 2E + B - S = <value>` 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).