added resource constraints

This commit is contained in:
Pagwin 2026-06-09 16:53:35 -04:00
parent 3e604324ad
commit 17020b857c
No known key found for this signature in database
GPG key ID: 81137023740CA260
4 changed files with 109 additions and 1 deletions

25
constraint_validator.py Normal file
View file

@ -0,0 +1,25 @@
"""Create constraint lambdas from expressions."""
import re
def make_constraint_lambda(expr_str):
"""Create a lambda function from a constraint expression.
Transforms resource identifiers to res dict access and creates a lambda
with restricted builtins.
Args:
expr_str: Constraint expression like "E[3] >= 50" or "B[2] + S[2] >= 100"
Returns:
A lambda function: lambda res: <evaluated_expression>
Raises:
Exception: If expression fails to eval
"""
# Transform identifiers to res subscript access: E -> res["E"], B -> res["B"], etc.
expr = re.sub(r'\b([A-Z])\b', r'res["\1"]', expr_str)
# Create lambda with restricted builtins (no imports, no dangerous functions)
return eval(f"lambda res: {expr}", {"__builtins__": {}})

View file

@ -61,6 +61,12 @@ FIXED_CHOICES = {
}
}
# Resource constraints: list of callables that receive a dict with keys E, B, S, C
# mapping to resource variables indexed by step. Each callable returns a constraint
# (or None to skip) to be passed to m.Add(). Example:
# lambda res: res["E"][3] >= 50 # Ensure at least 50 E at step 3
RESOURCE_CONSTRAINTS = []
# Arrival schedule. Key = step (1..5), value = list of city types that arrive
# at the START of that step. Types: 'H' Hub, 'F' Foundry, 'M' Metropolis, 'N' Monument.
# Arriving cities act that same step.
@ -983,6 +989,7 @@ def solve(
num_workers=8,
verbose=True,
fixed_choices=FIXED_CHOICES,
resource_constraints=None,
):
# ---- build the city list -----------------------------------------
@ -1505,6 +1512,28 @@ def solve(
finalE, finalB, finalS = E[NUM_STEPS + 1], B[NUM_STEPS + 1], S[NUM_STEPS + 1]
# ---- Apply resource constraints ----
if resource_constraints is None:
resource_constraints = RESOURCE_CONSTRAINTS
if verbose and resource_constraints:
print(f"Adding {len(resource_constraints)} resource constraint(s)")
res_dict = {
"E": E,
"B": B,
"S": S,
"C": C,
}
for i, constraint_fn in enumerate(resource_constraints):
constraint = constraint_fn(res_dict)
if verbose:
print(f" Constraint {i + 1}: {constraint}")
print(f" Type: {type(constraint)}")
print(f" Constraint object: {constraint}")
if constraint is not None:
m.Add(constraint)
# ---- Phase 1: real per-resource ceilings ----
def _ceiling(var):
s = cp_model.CpSolver()
@ -1759,9 +1788,11 @@ def _report(
print(" step: E B S C")
for t in range(1, NUM_STEPS + 2):
label = f"after5" if t == NUM_STEPS + 1 else f"start {t}"
c_val = solver.Value(C[t])
c_str = f"{c_val / 10:6.1f}"
print(
f" {label:>8}: {solver.Value(E[t]) / 10:6.1f} {solver.Value(B[t]) / 10:6.1f} "
f"{solver.Value(S[t]) / 10:6.1f} {solver.Value(C[t]) / 10:6.1f}"
f"{solver.Value(S[t]) / 10:6.1f} {c_str}"
)
if capital_spent is not None:

View file

@ -449,6 +449,16 @@
Constraint</button>
</div>
<!-- Resource Constraints Section -->
<div class="form-section">
<h2>Resource Constraints (Optional)</h2>
<p style="color: #666; font-size: 13px; margin-bottom: 15px;">
Enforce minimum resource levels at specific steps. Examples: <code style="background: #f0f0f0; padding: 2px 4px;">E[3] >= 50</code>, <code style="background: #f0f0f0; padding: 2px 4px;">B[2] + S[2] >= 100</code>
</p>
<div class="constraint-rows" id="resourceConstraints"></div>
<button type="button" class="btn-add" onclick="addResourceConstraint()">+ Add Resource Constraint</button>
</div>
<!-- Buttons -->
<div class="button-group">
<button type="submit" class="btn-solve">Solve</button>
@ -473,6 +483,7 @@
let cityCount = 0;
let actionConstraintCount = 0;
let governorConstraintCount = 0;
let resourceConstraintCount = 0;
const AVAILABLE_ACTIONS = {{actions | tojson }};
const AVAILABLE_AGENTS = {{agents | tojson }};
@ -652,6 +663,21 @@
container.appendChild(row);
}
function addResourceConstraint() {
const container = document.getElementById('resourceConstraints');
const id = resourceConstraintCount++;
const row = document.createElement('div');
row.className = 'constraint-row';
row.id = `resource-constraint-${id}`;
row.style.gridTemplateColumns = '1fr auto';
row.innerHTML = `
<input type="text" name="resource_expr_${id}" placeholder="e.g., E[3] >= 50 or B[2] + S[2] >= 100" style="flex: 1;">
<button type="button" class="btn-remove" onclick="document.getElementById('resource-constraint-${id}').remove()">Remove</button>
`;
container.appendChild(row);
}
document.getElementById('solverForm').addEventListener('submit', async (e) => {
e.preventDefault();
@ -687,6 +713,15 @@
fixedGovernors[`${city},${step},${agent}`] = enabled;
});
// Collect resource constraints
const resourceConstraints = [];
form.querySelectorAll('[id^="resource-constraint-"]').forEach(row => {
const expr = row.querySelector('[name^="resource_expr_"]').value.trim();
if (expr) {
resourceConstraints.push(expr);
}
});
// Add constraints to data if any exist
if (Object.keys(fixedActions).length > 0) {
data.fixed_actions = fixedActions;
@ -694,6 +729,9 @@
if (Object.keys(fixedGovernors).length > 0) {
data.fixed_governors = fixedGovernors;
}
if (resourceConstraints.length > 0) {
data.resource_constraints = resourceConstraints;
}
document.getElementById('loading').classList.add('show');
document.getElementById('outputSection').classList.remove('show');

View file

@ -7,6 +7,7 @@ import solve
import io
import sys
from contextlib import redirect_stdout
from constraint_validator import make_constraint_lambda
app = Flask(__name__)
@ -135,6 +136,18 @@ def solve_handler():
city, step = int(city), int(step)
fixed_choices["governors"][(city, step, agent)] = enabled
# Parse resource constraints
resource_constraints = None
resource_constraints_data = data.get("resource_constraints", [])
if resource_constraints_data:
resource_constraints = []
for expr in resource_constraints_data:
try:
constraint_lambda = make_constraint_lambda(expr)
resource_constraints.append(constraint_lambda)
except Exception as e:
raise ValueError(f"Invalid resource constraint '{expr}': {e}")
# Capture solver output
output_buffer = io.StringIO()
with redirect_stdout(output_buffer):
@ -147,6 +160,7 @@ def solve_handler():
num_workers=8,
verbose=verbose,
fixed_choices=fixed_choices,
resource_constraints=resource_constraints,
)
output = output_buffer.getvalue()