added resource constraints
This commit is contained in:
parent
3e604324ad
commit
17020b857c
4 changed files with 109 additions and 1 deletions
25
constraint_validator.py
Normal file
25
constraint_validator.py
Normal 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__": {}})
|
||||||
33
solve.py
33
solve.py
|
|
@ -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
|
# 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.
|
# at the START of that step. Types: 'H' Hub, 'F' Foundry, 'M' Metropolis, 'N' Monument.
|
||||||
# Arriving cities act that same step.
|
# Arriving cities act that same step.
|
||||||
|
|
@ -983,6 +989,7 @@ def solve(
|
||||||
num_workers=8,
|
num_workers=8,
|
||||||
verbose=True,
|
verbose=True,
|
||||||
fixed_choices=FIXED_CHOICES,
|
fixed_choices=FIXED_CHOICES,
|
||||||
|
resource_constraints=None,
|
||||||
):
|
):
|
||||||
|
|
||||||
# ---- build the city list -----------------------------------------
|
# ---- 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]
|
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 ----
|
# ---- Phase 1: real per-resource ceilings ----
|
||||||
def _ceiling(var):
|
def _ceiling(var):
|
||||||
s = cp_model.CpSolver()
|
s = cp_model.CpSolver()
|
||||||
|
|
@ -1759,9 +1788,11 @@ def _report(
|
||||||
print(" step: E B S C")
|
print(" step: E B S C")
|
||||||
for t in range(1, NUM_STEPS + 2):
|
for t in range(1, NUM_STEPS + 2):
|
||||||
label = f"after5" if t == NUM_STEPS + 1 else f"start {t}"
|
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(
|
print(
|
||||||
f" {label:>8}: {solver.Value(E[t]) / 10:6.1f} {solver.Value(B[t]) / 10:6.1f} "
|
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:
|
if capital_spent is not None:
|
||||||
|
|
|
||||||
|
|
@ -449,6 +449,16 @@
|
||||||
Constraint</button>
|
Constraint</button>
|
||||||
</div>
|
</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 -->
|
<!-- Buttons -->
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<button type="submit" class="btn-solve">Solve</button>
|
<button type="submit" class="btn-solve">Solve</button>
|
||||||
|
|
@ -473,6 +483,7 @@
|
||||||
let cityCount = 0;
|
let cityCount = 0;
|
||||||
let actionConstraintCount = 0;
|
let actionConstraintCount = 0;
|
||||||
let governorConstraintCount = 0;
|
let governorConstraintCount = 0;
|
||||||
|
let resourceConstraintCount = 0;
|
||||||
|
|
||||||
const AVAILABLE_ACTIONS = {{actions | tojson }};
|
const AVAILABLE_ACTIONS = {{actions | tojson }};
|
||||||
const AVAILABLE_AGENTS = {{agents | tojson }};
|
const AVAILABLE_AGENTS = {{agents | tojson }};
|
||||||
|
|
@ -652,6 +663,21 @@
|
||||||
container.appendChild(row);
|
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) => {
|
document.getElementById('solverForm').addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
|
@ -687,6 +713,15 @@
|
||||||
fixedGovernors[`${city},${step},${agent}`] = enabled;
|
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
|
// Add constraints to data if any exist
|
||||||
if (Object.keys(fixedActions).length > 0) {
|
if (Object.keys(fixedActions).length > 0) {
|
||||||
data.fixed_actions = fixedActions;
|
data.fixed_actions = fixedActions;
|
||||||
|
|
@ -694,6 +729,9 @@
|
||||||
if (Object.keys(fixedGovernors).length > 0) {
|
if (Object.keys(fixedGovernors).length > 0) {
|
||||||
data.fixed_governors = fixedGovernors;
|
data.fixed_governors = fixedGovernors;
|
||||||
}
|
}
|
||||||
|
if (resourceConstraints.length > 0) {
|
||||||
|
data.resource_constraints = resourceConstraints;
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('loading').classList.add('show');
|
document.getElementById('loading').classList.add('show');
|
||||||
document.getElementById('outputSection').classList.remove('show');
|
document.getElementById('outputSection').classList.remove('show');
|
||||||
|
|
|
||||||
14
web_solve.py
14
web_solve.py
|
|
@ -7,6 +7,7 @@ import solve
|
||||||
import io
|
import io
|
||||||
import sys
|
import sys
|
||||||
from contextlib import redirect_stdout
|
from contextlib import redirect_stdout
|
||||||
|
from constraint_validator import make_constraint_lambda
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|
@ -135,6 +136,18 @@ def solve_handler():
|
||||||
city, step = int(city), int(step)
|
city, step = int(city), int(step)
|
||||||
fixed_choices["governors"][(city, step, agent)] = enabled
|
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
|
# Capture solver output
|
||||||
output_buffer = io.StringIO()
|
output_buffer = io.StringIO()
|
||||||
with redirect_stdout(output_buffer):
|
with redirect_stdout(output_buffer):
|
||||||
|
|
@ -147,6 +160,7 @@ def solve_handler():
|
||||||
num_workers=8,
|
num_workers=8,
|
||||||
verbose=verbose,
|
verbose=verbose,
|
||||||
fixed_choices=fixed_choices,
|
fixed_choices=fixed_choices,
|
||||||
|
resource_constraints=resource_constraints,
|
||||||
)
|
)
|
||||||
|
|
||||||
output = output_buffer.getvalue()
|
output = output_buffer.getvalue()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue