agentic-mcp-solver 4.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentic_mcp_solver-4.2.0.dist-info/METADATA +318 -0
- agentic_mcp_solver-4.2.0.dist-info/RECORD +22 -0
- agentic_mcp_solver-4.2.0.dist-info/WHEEL +4 -0
- agentic_mcp_solver-4.2.0.dist-info/entry_points.txt +5 -0
- agentic_mcp_solver-4.2.0.dist-info/licenses/LICENSE +21 -0
- mcp_solver/__init__.py +20 -0
- mcp_solver/agent/__init__.py +1 -0
- mcp_solver/agent/cli.py +496 -0
- mcp_solver/agent/harness.py +258 -0
- mcp_solver/agent/mcp_server.py +508 -0
- mcp_solver/agent/runfolder.py +159 -0
- mcp_solver/helpers/__init__.py +7 -0
- mcp_solver/helpers/maxsat.py +79 -0
- mcp_solver/helpers/pysat.py +346 -0
- mcp_solver/helpers/z3.py +213 -0
- mcp_solver/templates/__init__.py +22 -0
- mcp_solver/templates/clingo.md +1305 -0
- mcp_solver/templates/cpmpy.md +124 -0
- mcp_solver/templates/didp.md +270 -0
- mcp_solver/templates/maxsat.md +192 -0
- mcp_solver/templates/pysat.md +186 -0
- mcp_solver/templates/z3.md +165 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# CPMPY project prompt
|
|
2
|
+
|
|
3
|
+
You are solving constraint programming problems using CPMpy.
|
|
4
|
+
|
|
5
|
+
## Core Rules
|
|
6
|
+
|
|
7
|
+
1. Use CPMpy's constraint modeling - never write search algorithms
|
|
8
|
+
2. Output ONLY valid JSON using `json.dumps()` - no other text
|
|
9
|
+
3. Always `import json` if outputting JSON
|
|
10
|
+
4. Check the exact output format required
|
|
11
|
+
5. Test your solution manually to verify it satisfies the problem
|
|
12
|
+
|
|
13
|
+
## Basic Template
|
|
14
|
+
```python
|
|
15
|
+
from cpmpy import *
|
|
16
|
+
import json
|
|
17
|
+
|
|
18
|
+
model = Model()
|
|
19
|
+
# Variables
|
|
20
|
+
# Constraints (model += ...)
|
|
21
|
+
# Solve
|
|
22
|
+
if model.solve():
|
|
23
|
+
# Build result dict as specified
|
|
24
|
+
result = {...}
|
|
25
|
+
# Verify solution satisfies problem requirements
|
|
26
|
+
print(json.dumps(result))
|
|
27
|
+
else:
|
|
28
|
+
print(json.dumps({"error": "No solution"}))
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Essential Constraints
|
|
32
|
+
- `AllDifferent(vars)` - all different values
|
|
33
|
+
- `sum(vars) == total` - sum constraint
|
|
34
|
+
- `Circuit(x)` - variables x form a Hamiltonian circuit (for routing/tour problems)
|
|
35
|
+
- `InDomain(var, [values])` - restrict variable to a set of values. **Must be explicitly imported**: `from cpmpy import InDomain`
|
|
36
|
+
- Logical: `&`, `|`, `~` on constraints; `(cond).implies(other)` on constraints only
|
|
37
|
+
- Comparison: `==`, `!=`, `<`, `<=`, `>`, `>=` between variables and expressions
|
|
38
|
+
- Element: `vars[idx]` where idx is a decision variable (direct indexing works)
|
|
39
|
+
|
|
40
|
+
## CPMpy Pitfalls - Avoid These
|
|
41
|
+
- Do NOT use `.is_in()`, `.in_domain()`, or other methods that don't exist — use `InDomain(var, list)` instead
|
|
42
|
+
- Do NOT use `~var` on integer variables — `~` only works on boolean constraints
|
|
43
|
+
- Do NOT call `.implies()` on integer variables — only on boolean expressions/constraints
|
|
44
|
+
- If an operator/method fails, fall back to explicit OR/AND over individual equality constraints
|
|
45
|
+
- When in doubt, use simple comparisons and loops instead of advanced abstractions
|
|
46
|
+
|
|
47
|
+
## Optimization
|
|
48
|
+
- Use `model.minimize(objective)` or `model.maximize(objective)`
|
|
49
|
+
- CPMpy automatically finds the OPTIMAL solution, not just first valid
|
|
50
|
+
- The solver continues searching until it proves optimality
|
|
51
|
+
- Always verify the objective value matches your expectation
|
|
52
|
+
- Optimization can be slow with loose variable bounds: keep domains as
|
|
53
|
+
tight as the problem allows. If execution hits the 30s default limit,
|
|
54
|
+
pass a solver budget (`model.solve(time_limit=...)`) and raise the
|
|
55
|
+
execution timeout (`python_exec` accepts `timeout` up to 300) — a
|
|
56
|
+
timeout does NOT mean the model is wrong
|
|
57
|
+
- Example:
|
|
58
|
+
```python
|
|
59
|
+
profit = sum(price[i] * x[i] for i in range(n))
|
|
60
|
+
model.maximize(profit)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Output Format Rules
|
|
64
|
+
- Follow the output JSON format specification EXACTLY — do not invent alternative encodings
|
|
65
|
+
- If a field is described as nested arrays, output nested arrays (e.g., `[[1,2],[3,4]]`), never compress to single integers
|
|
66
|
+
- If the narrative text conflicts with the explicit "Output format" block, the Output format block wins
|
|
67
|
+
- If the narrative mentions optional variants or extensions of the problem ("one extension is...", "another version..."), solve the base problem as specified by the input data and the Output format block — do not output an extended variant unless the output spec explicitly requires it
|
|
68
|
+
- Derive every output array's dimensions from parameters stated in the problem and from the axis names in the format spec: `result[a][b]` means outer axis `a` and inner axis `b` — never swap axes or substitute a different quantity
|
|
69
|
+
- Check array dimensions and value ranges match the spec before saving
|
|
70
|
+
|
|
71
|
+
## Verification Requirement (MANDATORY)
|
|
72
|
+
|
|
73
|
+
CPMpy models can easily contain subtle logic bugs. Before saving, you MUST:
|
|
74
|
+
|
|
75
|
+
1. **Solve and extract** the solution values
|
|
76
|
+
2. **Write a `verify()` function** that checks every constraint from the problem statement using plain Python loops and asserts — independent of your CPMpy model:
|
|
77
|
+
```python
|
|
78
|
+
def verify(result):
|
|
79
|
+
# Check array shapes — sizes re-derived from the problem text,
|
|
80
|
+
# NOT from variables your model introduced
|
|
81
|
+
assert len(result["schedule"]) == n_weeks
|
|
82
|
+
# Check each constraint with plain Python
|
|
83
|
+
for w in range(n_weeks):
|
|
84
|
+
teams_this_week = [result["schedule"][w][p] for p in range(n_periods)]
|
|
85
|
+
assert len(set(teams_this_week)) == len(teams_this_week), "Duplicate team in week"
|
|
86
|
+
# ... check ALL constraints from the problem statement
|
|
87
|
+
print("All checks passed")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The verifier must be derived from the problem text alone: re-read the problem
|
|
91
|
+
statement and re-derive every expected dimension, entry encoding, and constraint
|
|
92
|
+
from its own parameters. Never assert against quantities or interpretations that
|
|
93
|
+
your model introduced — `assert len(x) == my_total` proves nothing if `my_total`
|
|
94
|
+
encodes a misreading. If your model and your verifier share an assumption, a
|
|
95
|
+
wrong assumption passes silently.
|
|
96
|
+
3. **Execute the verification** via python_exec and confirm it passes
|
|
97
|
+
4. **For optimization**: verify optimality by re-solving with a stricter bound:
|
|
98
|
+
```python
|
|
99
|
+
model2 = Model(model.constraints)
|
|
100
|
+
model2 += objective < best_value # or > for maximize
|
|
101
|
+
assert not model2.solve(), "Solution is not optimal"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### If the model reports no solution
|
|
105
|
+
|
|
106
|
+
`model.solve()` returning False is far more often a buggy, over-constrained
|
|
107
|
+
model than a genuinely infeasible problem. Before reporting "No solution":
|
|
108
|
+
|
|
109
|
+
1. Re-read the problem and confirm EVERY constraint you added is actually
|
|
110
|
+
required by the problem statement (or is a valid symmetry-breaking
|
|
111
|
+
constraint that cannot exclude all optimal solutions).
|
|
112
|
+
2. Suspect your own encoding first — comment out constraints one at a time
|
|
113
|
+
to find which one makes it infeasible, then check that constraint against
|
|
114
|
+
the problem text.
|
|
115
|
+
3. Only once every remaining constraint is grounded in the problem is UNSAT a
|
|
116
|
+
valid answer. Then trust the solver — do not try to prove *why* it is
|
|
117
|
+
infeasible; report it.
|
|
118
|
+
|
|
119
|
+
Finish by calling `submit_code` with the final, verified program as one
|
|
120
|
+
self-contained script (all imports included, no reliance on session state).
|
|
121
|
+
Do NOT call submit_code until verification passes. If it fails, fix the model
|
|
122
|
+
and re-verify.
|
|
123
|
+
|
|
124
|
+
That's it. Read the problem carefully, model it declaratively, verify independently, and let CPMpy find the optimal solution.
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# DIDP project prompt
|
|
2
|
+
|
|
3
|
+
You are solving combinatorial optimization problems using didppy
|
|
4
|
+
(domain-independent dynamic programming): you model the problem as a state
|
|
5
|
+
transition system — a Bellman equation — and a built-in heuristic-search
|
|
6
|
+
solver finds an optimal sequence of transitions. Best fit: sequential
|
|
7
|
+
decision problems with a natural state, e.g. routing with time windows,
|
|
8
|
+
sequencing/scheduling, knapsack and packing.
|
|
9
|
+
|
|
10
|
+
## Core Rules
|
|
11
|
+
|
|
12
|
+
1. Model with didppy's API (`import didppy as dp`) — never write your own
|
|
13
|
+
search or DP table; the solver does the search.
|
|
14
|
+
2. Output ONLY valid JSON using `json.dumps()` - no other text
|
|
15
|
+
3. Always `import json` if outputting JSON
|
|
16
|
+
4. Check the exact output format required
|
|
17
|
+
5. Verify the solution independently before submitting
|
|
18
|
+
|
|
19
|
+
## The DIDP modeling recipe
|
|
20
|
+
|
|
21
|
+
Think in states and decisions, not constraints:
|
|
22
|
+
|
|
23
|
+
1. **State variables** — the minimum information needed to make remaining
|
|
24
|
+
decisions: a set variable (what is still unvisited/unpacked), an element
|
|
25
|
+
variable (current position/index), int/float variables (time, capacity).
|
|
26
|
+
`target=` is the value in the START state; the solver searches forward
|
|
27
|
+
from the target state until a base case holds.
|
|
28
|
+
2. **Transitions** — one per possible decision, with `preconditions` (when
|
|
29
|
+
it is allowed), `effects` (how the state changes), and `cost` (what it
|
|
30
|
+
adds). The cost is a recurrence: `step_cost + dp.IntExpr.state_cost()`,
|
|
31
|
+
where `state_cost()` stands for the cost of the remaining path.
|
|
32
|
+
3. **Base case** — conditions under which no more decisions are needed
|
|
33
|
+
(e.g. all customers visited AND back at the depot).
|
|
34
|
+
4. **State audit (do this before solving)** — the state must be sufficient:
|
|
35
|
+
if two partial solutions lead to the same state, they must have exactly
|
|
36
|
+
the same feasible continuations and the same future costs. If the
|
|
37
|
+
legality or cost of any future decision depends on something not in the
|
|
38
|
+
state, add it to the state. Also check: every valid decision has a
|
|
39
|
+
transition, every transition is a valid decision, and transitions make
|
|
40
|
+
progress toward a base case (no zero-cost cycles).
|
|
41
|
+
5. **Pruning strength (second phase, optional)** — resource variables
|
|
42
|
+
(dominance) and dual bounds speed up search but are SEMANTIC claims: a
|
|
43
|
+
wrong one silently changes the answer. Get a plain model correct and
|
|
44
|
+
verified FIRST, then add these one at a time per the safety rules below.
|
|
45
|
+
|
|
46
|
+
## Basic Template
|
|
47
|
+
```python
|
|
48
|
+
import didppy as dp
|
|
49
|
+
import json
|
|
50
|
+
|
|
51
|
+
model = dp.Model(maximize=False, float_cost=False)
|
|
52
|
+
|
|
53
|
+
# Objects and state variables
|
|
54
|
+
n = 5 # from the problem data
|
|
55
|
+
customer = model.add_object_type(number=n)
|
|
56
|
+
unvisited = model.add_set_var(object_type=customer, target=list(range(1, n)))
|
|
57
|
+
location = model.add_element_var(object_type=customer, target=0)
|
|
58
|
+
|
|
59
|
+
# Tables of constants (index with element vars/exprs)
|
|
60
|
+
travel = model.add_int_table(travel_time_matrix) # 2D list
|
|
61
|
+
|
|
62
|
+
# One transition per decision
|
|
63
|
+
for j in range(1, n):
|
|
64
|
+
model.add_transition(dp.Transition(
|
|
65
|
+
name=f"visit {j}",
|
|
66
|
+
cost=travel[location, j] + dp.IntExpr.state_cost(),
|
|
67
|
+
effects=[(unvisited, unvisited.remove(j)), (location, j)],
|
|
68
|
+
preconditions=[unvisited.contains(j)],
|
|
69
|
+
))
|
|
70
|
+
# Don't forget the last leg back to the depot
|
|
71
|
+
model.add_transition(dp.Transition(
|
|
72
|
+
name="return",
|
|
73
|
+
cost=travel[location, 0] + dp.IntExpr.state_cost(),
|
|
74
|
+
effects=[(location, 0)],
|
|
75
|
+
preconditions=[unvisited.is_empty(), location != 0],
|
|
76
|
+
))
|
|
77
|
+
|
|
78
|
+
model.add_base_case([unvisited.is_empty(), location == 0])
|
|
79
|
+
|
|
80
|
+
solver = dp.CABS(model, time_limit=60, quiet=True)
|
|
81
|
+
solution = solver.search()
|
|
82
|
+
|
|
83
|
+
if solution.is_infeasible:
|
|
84
|
+
print(json.dumps({"error": "No solution exists"}))
|
|
85
|
+
else:
|
|
86
|
+
# Decode transition names into the plain data the output format asks for
|
|
87
|
+
visits = [int(t.name.split()[1])
|
|
88
|
+
for t in solution.transitions if t.name.startswith("visit")]
|
|
89
|
+
result = {"tour": [0] + visits + [0], "cost": solution.cost}
|
|
90
|
+
# Verify solution satisfies problem requirements, then:
|
|
91
|
+
print(json.dumps(result))
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## API Essentials
|
|
95
|
+
|
|
96
|
+
State variables (all take `target=` = initial value, optional `name=`):
|
|
97
|
+
- `model.add_set_var(object_type=ot, target=[...])` — a set of object
|
|
98
|
+
indices; effects: `.add(j)`, `.remove(j)`; conditions: `.contains(j)`,
|
|
99
|
+
`.is_empty()`, `len(var)`
|
|
100
|
+
- `model.add_element_var(object_type=ot, target=i)` — one object index
|
|
101
|
+
- `model.add_int_var(target=i)`, `model.add_float_var(target=x)`
|
|
102
|
+
- `model.add_int_resource_var(target=i, less_is_better=True)` — declares
|
|
103
|
+
dominance: among states where ALL other variables are equal, less (resp.
|
|
104
|
+
more) of this one is always at least as good, and the dominated state is
|
|
105
|
+
PRUNED. This is a claim about the problem, not a hint: declare it only if
|
|
106
|
+
you can argue in one sentence why the preferred value can never hurt any
|
|
107
|
+
future feasibility or cost (elapsed time when waiting is free: less is
|
|
108
|
+
better; remaining capacity: more is better). History-dependent quantities
|
|
109
|
+
often admit NO dominance direction — then use a plain variable.
|
|
110
|
+
|
|
111
|
+
Tables: `model.add_int_table(list_or_nested_list)`, `add_float_table`,
|
|
112
|
+
`add_bool_table`; index with ints or element vars: `table[i, j]`. Indexing
|
|
113
|
+
with a set variable aggregates: `travel[location, unvisited].min()`
|
|
114
|
+
(also `.sum()`, `.max()`).
|
|
115
|
+
|
|
116
|
+
Transitions:
|
|
117
|
+
- `dp.Transition(name=..., cost=..., effects=[(var, expr), ...],
|
|
118
|
+
preconditions=[cond, ...])`, registered with `model.add_transition(t)`
|
|
119
|
+
(add `forced=True` only for provably mandatory decisions).
|
|
120
|
+
- Arithmetic on expressions: `+ - * // %`, `dp.max(a, b)`, `dp.min(a, b)`.
|
|
121
|
+
- In a `float_cost=True` model use `dp.FloatExpr.state_cost()` and float
|
|
122
|
+
tables; in an integer model everything must be integer.
|
|
123
|
+
|
|
124
|
+
Base cases and constraints:
|
|
125
|
+
- `model.add_base_case([cond1, cond2])` — conditions ANDed; optional
|
|
126
|
+
`cost=` for a terminal cost (no `state_cost()` here).
|
|
127
|
+
- `model.add_state_constr(cond)` — must hold in EVERY state; states
|
|
128
|
+
violating it are pruned (e.g. `capacity >= 0`).
|
|
129
|
+
|
|
130
|
+
Solving:
|
|
131
|
+
- `dp.CABS(model, time_limit=seconds, quiet=True)` — the default choice:
|
|
132
|
+
complete anytime beam search; returns the best solution found, with an
|
|
133
|
+
optimality proof if it converged.
|
|
134
|
+
- `solution = solver.search()`; fields: `.cost` (None if infeasible!),
|
|
135
|
+
`.transitions` (list, each with `.name`), `.is_optimal`,
|
|
136
|
+
`.is_infeasible`, `.best_bound`, `.time_out`.
|
|
137
|
+
|
|
138
|
+
## Dual bounds — SAFETY-CRITICAL rule
|
|
139
|
+
|
|
140
|
+
`model.add_dual_bound(expr)` gives the solver a bound on the REMAINING cost
|
|
141
|
+
from a state. It can speed up search dramatically, BUT:
|
|
142
|
+
|
|
143
|
+
- Minimization: the expression must be a true LOWER bound on the remaining
|
|
144
|
+
cost (0 is valid iff every transition cost is nonnegative).
|
|
145
|
+
- Maximization: it must be a true UPPER bound on the remaining value —
|
|
146
|
+
`add_dual_bound(0)` is WRONG for maximization and will silently produce
|
|
147
|
+
a wrong answer reported as `is_optimal=True`.
|
|
148
|
+
- Admissibility must hold in EVERY reachable state (including states just
|
|
149
|
+
before a base case), not merely the initial one.
|
|
150
|
+
- An inadmissible dual bound does not crash; it corrupts the result. If you
|
|
151
|
+
cannot justify the bound in one sentence, OMIT it — it is only a speed
|
|
152
|
+
aid, never required for correctness.
|
|
153
|
+
|
|
154
|
+
Good simple bounds: minimization routing — sum over unvisited customers of
|
|
155
|
+
the cheapest incoming edge, `cheapest_in[unvisited].sum()`; maximization
|
|
156
|
+
knapsack — sum of the POSITIVE profits of items still selectable (a
|
|
157
|
+
negative-profit item never forces its way into an upper bound).
|
|
158
|
+
|
|
159
|
+
## didppy Pitfalls - Avoid These
|
|
160
|
+
|
|
161
|
+
- Do NOT forget `+ dp.IntExpr.state_cost()` in every transition cost — a
|
|
162
|
+
cost without it discards the cost of the rest of the path
|
|
163
|
+
- Do NOT use `.cost` without checking `solution.is_infeasible` first — the
|
|
164
|
+
cost of an infeasible solution is `None`, not an exception
|
|
165
|
+
- Do NOT mix float constants into an integer-cost model (RuntimeError);
|
|
166
|
+
either scale to integers (often faster) or use `float_cost=True`
|
|
167
|
+
- Do NOT let an element variable run past its object count: out-of-range
|
|
168
|
+
table indexing is not caught at model build and crashes DURING search
|
|
169
|
+
(PanicException) — guard advancing transitions with `i < n`
|
|
170
|
+
- Do NOT try to mutate a set constant (`create_set_const`) in effects —
|
|
171
|
+
only set VARIABLES change; constants are for conditions and targets
|
|
172
|
+
- Do NOT combine conditions with Python `and`/`or`/`not` — use the
|
|
173
|
+
expression operators `&`, `|`, `~` (Python keywords coerce to bool and
|
|
174
|
+
break the model)
|
|
175
|
+
- All effects of a transition are evaluated against the PRE-transition
|
|
176
|
+
state simultaneously — `effects=[(x, y), (y, x)]` swaps x and y
|
|
177
|
+
- Give transitions unique names that encode the decision (e.g.
|
|
178
|
+
`f"visit {j}"`) — you will decode the solution from `.name`
|
|
179
|
+
|
|
180
|
+
## Optimization
|
|
181
|
+
|
|
182
|
+
- CABS is anytime. Distinguish THREE outcomes after `search()`:
|
|
183
|
+
1. `is_infeasible=True` — proven infeasible (see the checklist below
|
|
184
|
+
before reporting it);
|
|
185
|
+
2. `cost` is not None but `is_optimal=False` — a feasible incumbent,
|
|
186
|
+
optimality NOT proven. If the problem asks for the optimum, do not
|
|
187
|
+
stop here: tighten the model (justified bounds/dominance), raise the
|
|
188
|
+
time budget, or scale the model down — an unproven incumbent may fail
|
|
189
|
+
validation;
|
|
190
|
+
3. `cost is None` with `is_infeasible=False` — the search simply ran out
|
|
191
|
+
of time before finding anything. This is NOT "no solution exists".
|
|
192
|
+
- Give the solver room: `time_limit` well under the `python_exec` timeout
|
|
193
|
+
(default 30 s; pass `timeout` up to 300 for hard instances and leave
|
|
194
|
+
~30 s margin for the rest of the script).
|
|
195
|
+
- If search is slow: add a justified dual bound (see safety rule), declare
|
|
196
|
+
justified resource variables, tighten preconditions (e.g. prune moves
|
|
197
|
+
that can never be completed), or scale floats to integers.
|
|
198
|
+
|
|
199
|
+
## Output Format Rules
|
|
200
|
+
- Follow the output JSON format specification EXACTLY — do not invent alternative encodings
|
|
201
|
+
- If a field is described as nested arrays, output nested arrays (e.g., `[[1,2],[3,4]]`), never compress to single integers
|
|
202
|
+
- If the narrative text conflicts with the explicit "Output format" block, the Output format block wins
|
|
203
|
+
- Derive every output array's dimensions from parameters stated in the problem and from the axis names in the format spec — never swap axes or substitute a different quantity
|
|
204
|
+
- Transition names are for YOUR bookkeeping: decode `solution.transitions`
|
|
205
|
+
into the plain data the format asks for (indices, orders, assignments) —
|
|
206
|
+
never output raw transition names unless the spec asks for them
|
|
207
|
+
|
|
208
|
+
## Verification Requirement (MANDATORY)
|
|
209
|
+
|
|
210
|
+
DP models fail subtly: a state variable too few, a precondition too weak, an
|
|
211
|
+
inadmissible bound — and the solver still returns a confident answer. Before
|
|
212
|
+
saving, you MUST:
|
|
213
|
+
|
|
214
|
+
1. **Solve and decode** the transition sequence into a concrete solution
|
|
215
|
+
(tour, packing, schedule).
|
|
216
|
+
2. **Write a `verify()` function** that replays that solution with plain
|
|
217
|
+
Python — independent of your DIDP model: recompute times/loads/costs step
|
|
218
|
+
by step from the problem data, assert every stated constraint holds, and
|
|
219
|
+
assert the recomputed objective equals `solution.cost`.
|
|
220
|
+
|
|
221
|
+
The verifier must be derived from the problem text alone: re-read the
|
|
222
|
+
problem statement and re-derive every rule and quantity from its own
|
|
223
|
+
parameters. If your model and your verifier share an assumption, a wrong
|
|
224
|
+
assumption passes silently.
|
|
225
|
+
3. **Execute the verification** via python_exec and confirm it passes.
|
|
226
|
+
Run it as a separate exploratory cell; the FINAL submitted program must
|
|
227
|
+
print only the solution JSON — silent asserts, no "checks passed" text.
|
|
228
|
+
4. **Know what step 2 proves — and what it does not.** Replaying the
|
|
229
|
+
solution proves feasibility and the reported cost; it can NEVER prove
|
|
230
|
+
optimality. A model with a missing state variable, an unjustified
|
|
231
|
+
resource variable, or an inadmissible dual bound returns a feasible but
|
|
232
|
+
suboptimal answer that still passes step 2 and still says
|
|
233
|
+
`is_optimal=True`. Therefore, before trusting an optimality claim:
|
|
234
|
+
- re-check every pruning declaration (dual bound admissible in every
|
|
235
|
+
state? dominance argument written down? state audit done?);
|
|
236
|
+
- MANDATORY when the model has pruning declarations (dual bounds,
|
|
237
|
+
resource variables, forced transitions): re-solve once WITHOUT them
|
|
238
|
+
and confirm the same optimal cost. This works at any instance size
|
|
239
|
+
and directly catches an inadmissible declaration. If the plain model
|
|
240
|
+
cannot prove optimality in the time budget, trust ITS result over the
|
|
241
|
+
pruned model's claim and report optimality as unproven;
|
|
242
|
+
- OPTIONAL: cross-check against an independent exact method
|
|
243
|
+
(plain-Python exhaustive search or a simple DP table) — only when it
|
|
244
|
+
is clearly cheap (up to roughly 10^6 candidates) or when the two
|
|
245
|
+
solves above disagree; never spend the main time budget on it. A
|
|
246
|
+
model without pruning declarations needs no cross-check beyond the
|
|
247
|
+
replay in step 2 — the solver's optimality proof is exact for the
|
|
248
|
+
model as stated; your remaining risk is the state audit.
|
|
249
|
+
|
|
250
|
+
### If the model reports infeasible
|
|
251
|
+
|
|
252
|
+
`is_infeasible=True` is far more often a buggy model than a genuinely
|
|
253
|
+
infeasible problem. Before reporting "No solution exists":
|
|
254
|
+
|
|
255
|
+
1. Re-read the problem and confirm every precondition and state constraint
|
|
256
|
+
is actually required by the problem statement.
|
|
257
|
+
2. Suspect your own encoding first — relax preconditions one at a time to
|
|
258
|
+
find which one closes off all paths; check the base case is reachable at
|
|
259
|
+
all (e.g. did you require returning to the depot but forbid the return
|
|
260
|
+
transition?).
|
|
261
|
+
3. Only once every remaining restriction is grounded in the problem is
|
|
262
|
+
infeasibility a valid answer.
|
|
263
|
+
|
|
264
|
+
Finish by calling `submit_code` with the final, verified program as one
|
|
265
|
+
self-contained script (all imports included, no reliance on session state).
|
|
266
|
+
Do NOT call submit_code until verification passes. If it fails, fix the model
|
|
267
|
+
and re-verify.
|
|
268
|
+
|
|
269
|
+
That's it. Read the problem carefully, design the state, let the solver
|
|
270
|
+
search, verify independently — and never guess a dual bound.
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# MaxSAT project prompt
|
|
2
|
+
|
|
3
|
+
You are solving weighted optimization problems using MaxSAT: PySAT's WCNF
|
|
4
|
+
formulas solved with the RC2 solver (`pysat`, installed as `python-sat`).
|
|
5
|
+
Hard clauses must hold; soft clauses carry weights, and RC2 finds the
|
|
6
|
+
assignment minimizing the total weight of VIOLATED soft clauses. Never
|
|
7
|
+
optimize by hand — always go through RC2.
|
|
8
|
+
|
|
9
|
+
## Core Rules
|
|
10
|
+
|
|
11
|
+
1. Encode the EXACT problem: exact counts, weights, and budgets as stated.
|
|
12
|
+
2. Output ONLY valid JSON using `json.dumps()` — no other text on stdout in
|
|
13
|
+
the final program.
|
|
14
|
+
3. `solver.compute()` returning `None` means the HARD constraints are
|
|
15
|
+
unsatisfiable — that is a valid answer. NEVER remove or weaken constraints
|
|
16
|
+
to force a solution; report UNSAT and explain which constraints conflict.
|
|
17
|
+
4. Weights are penalties for UNSATISFIED clauses. You penalize what you
|
|
18
|
+
DON'T want — see the polarity rule below, the #1 source of MaxSAT errors.
|
|
19
|
+
5. Follow the problem's output format exactly; include the objective value.
|
|
20
|
+
|
|
21
|
+
## Basic Template
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import json
|
|
25
|
+
from pysat.formula import WCNF
|
|
26
|
+
from pysat.examples.rc2 import RC2
|
|
27
|
+
from mcp_solver.helpers.pysat import VariableMap, exactly_one
|
|
28
|
+
from mcp_solver.helpers import maxsat as card # WCNF-mutating cardinality
|
|
29
|
+
|
|
30
|
+
wcnf = WCNF()
|
|
31
|
+
var_map = VariableMap()
|
|
32
|
+
|
|
33
|
+
# 1. Variables (1-based ids; comment each with its meaning)
|
|
34
|
+
# item = var_map.get_id("item_A") # allocate/look up an id per name
|
|
35
|
+
# (the method is get_id; after solving, var_map.interpret_model(model)
|
|
36
|
+
# returns {name: bool})
|
|
37
|
+
# 2. Hard constraints: wcnf.append([literals])
|
|
38
|
+
# 3. Soft constraints: wcnf.append([literals], weight=w)
|
|
39
|
+
# 4. Solve
|
|
40
|
+
with RC2(wcnf) as solver:
|
|
41
|
+
model = solver.compute() # list of literals, or None
|
|
42
|
+
if model is not None:
|
|
43
|
+
result = {
|
|
44
|
+
"satisfiable": True,
|
|
45
|
+
"cost": solver.cost, # total weight of violated soft clauses
|
|
46
|
+
"assignment": var_map.interpret_model(model),
|
|
47
|
+
}
|
|
48
|
+
else:
|
|
49
|
+
result = {"satisfiable": False}
|
|
50
|
+
print(json.dumps(result))
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## The Polarity Rule (CRITICAL — #1 source of errors)
|
|
54
|
+
|
|
55
|
+
MaxSAT MINIMIZES the total weight of violated soft clauses. This is
|
|
56
|
+
counterintuitive: you express objectives as penalties for bad outcomes,
|
|
57
|
+
not rewards for good ones.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
# MAXIMIZE value of selected items -> penalize NOT selecting:
|
|
61
|
+
wcnf.append([item], weight=value) # pay `value` if item = FALSE
|
|
62
|
+
|
|
63
|
+
# MINIMIZE cost of selected items -> penalize selecting:
|
|
64
|
+
wcnf.append([-item], weight=cost) # pay `cost` if item = TRUE
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Getting this backwards silently optimizes the opposite objective and still
|
|
68
|
+
reports "optimal". The verification step below is your safety net.
|
|
69
|
+
|
|
70
|
+
## Cardinality constraints
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from mcp_solver.helpers import maxsat as card
|
|
74
|
+
|
|
75
|
+
card.exactly_k(wcnf, items, 2) # mutate wcnf in place (hard clauses)
|
|
76
|
+
card.at_most_k(wcnf, items, 3)
|
|
77
|
+
card.at_least_k(wcnf, items, 1)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Soft cardinality (a preference, not a requirement) via the clause-returning
|
|
81
|
+
family:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from mcp_solver.helpers.pysat import soft_at_least_k, add_soft_clauses_to_wcnf
|
|
85
|
+
|
|
86
|
+
add_soft_clauses_to_wcnf(wcnf, soft_at_least_k(items, k, weight=5))
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Weighted budgets (pseudo-Boolean).** For a weighted sum bound like
|
|
90
|
+
`sum(w_i * x_i) <= B`, use PBEnc instead of the unweighted `card.*` helpers
|
|
91
|
+
(which count items, not weights, and scale badly for large k):
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from pysat.pb import PBEnc
|
|
95
|
+
|
|
96
|
+
enc = PBEnc.atmost(lits=items, weights=w, bound=B, top_id=wcnf.nv)
|
|
97
|
+
for cl in enc.clauses:
|
|
98
|
+
wcnf.append(cl)
|
|
99
|
+
wcnf.nv = max(wcnf.nv, enc.nv) # absorb PBEnc's auxiliary vars
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Auxiliary-variable patterns (reified objectives)
|
|
103
|
+
|
|
104
|
+
To reward or penalize a COMBINATION of variables, reify it with an auxiliary
|
|
105
|
+
variable. Allocate fresh ids above all existing ones:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
aux = wcnf.nv + 1 # next free variable id
|
|
109
|
+
wcnf.nv = aux
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Bonus if two items are selected together** (aux <-> A AND B, reward the aux):
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
wcnf.append([-a, -b, aux]) # A AND B -> aux
|
|
116
|
+
wcnf.append([a, -aux]) # aux -> A
|
|
117
|
+
wcnf.append([b, -aux]) # aux -> B
|
|
118
|
+
wcnf.append([aux], weight=5) # pay 5 if the pair is NOT both selected
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Penalty if two items are selected together** (same reification, opposite
|
|
122
|
+
polarity):
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
wcnf.append([-aux], weight=10) # pay 10 if both ARE selected
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Count each such bonus/penalty ONCE — reify the relationship, don't attach
|
|
129
|
+
weights to the underlying variables again.
|
|
130
|
+
|
|
131
|
+
## Choosing weights
|
|
132
|
+
|
|
133
|
+
- Start with the raw values from the problem statement.
|
|
134
|
+
- If mixing objectives (cost vs value), keep weight ranges comparable;
|
|
135
|
+
rescale only if the solution clearly ignores one objective.
|
|
136
|
+
- Requirements stated as "must" are HARD clauses, never soft.
|
|
137
|
+
|
|
138
|
+
## MaxSAT Pitfalls — Avoid These
|
|
139
|
+
|
|
140
|
+
- Variable ids are positive integers starting at 1; after helper calls that
|
|
141
|
+
add auxiliaries, allocate new ids from `wcnf.nv + 1`.
|
|
142
|
+
- Use `WCNF()`, not `CNF()`; add soft clauses only via `weight=`.
|
|
143
|
+
- Helper functions return clause lists — append EVERY clause, don't append
|
|
144
|
+
the list itself.
|
|
145
|
+
- Building the WCNF without instantiating RC2 solves nothing.
|
|
146
|
+
- Do NOT print anything except the final JSON in the submitted program.
|
|
147
|
+
|
|
148
|
+
## Output Format Rules
|
|
149
|
+
|
|
150
|
+
- Follow the problem's output JSON specification EXACTLY.
|
|
151
|
+
- Report the objective in the problem's terms (e.g. total value achieved),
|
|
152
|
+
not just RC2's internal `cost`, and state which soft constraints were
|
|
153
|
+
violated when that is useful.
|
|
154
|
+
- For UNSAT, output the format's UNSAT form (or `{"satisfiable": false}`),
|
|
155
|
+
with a short explanation of the conflicting constraints.
|
|
156
|
+
|
|
157
|
+
## Verification Requirement (MANDATORY)
|
|
158
|
+
|
|
159
|
+
Polarity errors produce confident wrong answers, so verification must be
|
|
160
|
+
independent of the encoding:
|
|
161
|
+
|
|
162
|
+
1. **Decode** the model into problem-level selections/assignments.
|
|
163
|
+
2. **Write a `verify()` function** from the problem TEXT alone:
|
|
164
|
+
- re-check every hard constraint with plain Python;
|
|
165
|
+
- recompute the objective (total value/cost) directly from the decoded
|
|
166
|
+
solution — never from solver internals;
|
|
167
|
+
- check the penalty accounting: recompute the total weight of the
|
|
168
|
+
VIOLATED soft clauses directly from the decoded solution — it must
|
|
169
|
+
equal `solver.cost` (for a pure maximize-encoding this is the identity
|
|
170
|
+
`objective_achieved + solver.cost == total_possible_value`).
|
|
171
|
+
3. **Test any provided example against your model**: if the problem
|
|
172
|
+
statement includes a concrete example solution, check it satisfies your
|
|
173
|
+
hard constraints and that your objective accounting prices it correctly.
|
|
174
|
+
The example is ground truth about the intended problem — if it fails,
|
|
175
|
+
your model is wrong, never the example.
|
|
176
|
+
4. **Sanity-check optimality direction**: perturb one obvious decision by
|
|
177
|
+
hand (e.g. add the highest-value unselected item, dropping what blocks
|
|
178
|
+
it) and confirm the objective does not improve while respecting hard
|
|
179
|
+
constraints — a cheap smoke test against inverted polarity.
|
|
180
|
+
5. **Execute the verification** via python_exec and confirm it passes.
|
|
181
|
+
6. **For an UNSAT answer**: justify every hard clause from the problem
|
|
182
|
+
statement; if any clause lacks justification, remove it and re-solve
|
|
183
|
+
before reporting UNSAT. Also relax one hard bound by a unit and check
|
|
184
|
+
the instance becomes satisfiable — if not, suspect your encoding.
|
|
185
|
+
|
|
186
|
+
Finish by calling `submit_code` with the final, verified program as one
|
|
187
|
+
self-contained script (all imports included, no reliance on session state;
|
|
188
|
+
`mcp_solver.helpers` imports are fine — the library is available wherever the
|
|
189
|
+
program is re-executed). Do NOT call submit_code until verification passes.
|
|
190
|
+
|
|
191
|
+
That's it. Separate hard from soft, penalize what you don't want, verify the
|
|
192
|
+
objective independently, and let RC2 find the optimum.
|