dws-city-res-solve/docs/superpowers/specs/2026-06-11-configurable-objective-design.md
Pagwin d18e533559
spec: configurable maximize criteria for solve()
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 19:07:58 -04:00

105 lines
4.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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).