Diagnosed with Explainer¶
Explainer takes a model that already exists and tells you why it behaves as it does — why it has no solution, what the smallest change is that would give it one, and how much the inputs can move before the answer stops holding.
Its three use cases need a model with something to diagnose, so the tabs below add two delivery contracts to the facility model. That is enough to make it infeasible, and enough to walk all three.
Modeler left the facility model with every test passing. Explainer picks up where that stops being true — when the model you are running comes back infeasible, and the question is not what should the model be but why does this one have no answer.
To get there, the model needs a reason to fail. Sales has committed to two delivery contracts with plant 4: warehouse 2 takes its full 18 from plant 4, and warehouse 1 takes at least 8 from plant 4 as well. Both are ordinary business requirements, added to the model as two more constraints:
model.addConstr(ship[4, 2] >= 18, name="contract_w2")
model.addConstr(ship[4, 1] >= 8, name="contract_w1")
The model now has 11 constraints instead of 9. It also has no solution:
Model status: INFEASIBLE
Nothing in that message says which of the eleven constraints is at fault. With 11 you could find it by inspection; the real network has thousands, and that is the situation Explainer is built for.
What you upload
Explainer takes the model file and computes the IIS itself — you do not need to
call computeIIS first:
Alongside it, upload the context. This is the part that changes the quality of the answer rather than just its speed: the same constraint is immovable in one business and negotiable in another, and nothing in an LP file says which. Here that means the source code and the specification Modeler produced — the specification already records, in business terms, that demand must be met exactly and why.
This exchange, and the two that follow it in the next tabs, are driven for real against the Explainer MCP tool — not invented — with the model, the code and the specification attached exactly as described above.
facility_contracts.lp, facility_modeler.py, facility_spec.md attached
This came back infeasible after we added two delivery contracts with plant 4. The contracts are commitments sales already made, so I would rather not break them. Capacity is a physical limit on the plant.
The user has given me the model, the implementation, and the specification, plus a ranking of what is negotiable: contracts are committed but are business policy, capacity is physical. Computing the IIS on the model file first, then reading it against that ranking.
Computing the IIS.
The IIS is small — 3 constraints and 2 bounds, all of them about plant 4. That is a clean result: the conflict is local, and none of the demand constraints are involved. Searching the knowledge base for the relevant guidance before writing this up.
The IIS contains exactly 3 constraints and 2 bounds, all confined to plant 4:
Subject To
capacity[4]: ship[4,1] + ship[4,2] + ship[4,3] + ship[4,4]
- 19 open[4] <= 0
contract_w2: ship[4,2] >= 18
contract_w1: ship[4,1] >= 8
Bounds
open[4] [B, LB=0, UB=1]
ship[4,3] >= 0
ship[4,4] >= 0
The root cause. The two contracts together commit plant 4 to shipping at least 18 + 8 = 26 thousand units. Plant 4’s capacity is 19. There is no assignment of any variable that satisfies both — you are 7 thousand units short, and no other plant can help, because the contracts name plant 4 specifically.
Two things worth noticing about this IIS:
open[4] is in the IIS but is not the problem. With open[4] = 1 the capacity row reads ≤ 19; with open[4] = 0 it reads ≤ 0, which is worse. Both branches fail, which is why the binary appears without offering an escape.The non-negativity bounds on ship[4,3] and ship[4,4] are in the IIS
because they rule out the one remaining escape: borrowing negative shipments
on those lanes to free up room on the other two. Ignore them as remediation
candidates — shipping a negative quantity is not a thing.
Since contracts are negotiable as a last resort and capacity is not, there is
no configuration that satisfies both constraints as stated. The full report,
including ranked remediation options, is saved to the workspace as
facility_contracts_infeasibility_report.md — the next tab.
Why the names matter
That diagnosis is readable because the constraints have names. Modeler named them
demand, capacity, contract_w1 and contract_w2, so the IIS names
the business rules directly. Had they been unnamed, the same IIS would have come
back as R7, R10, R11 — mathematically identical, and telling you
nothing about which commitment to renegotiate.
Explainer flags this when it sees it: part of the report is a note on whether constraint naming is good enough to interpret, and where it is not. It is the cheapest thing you can do in advance to make an infeasibility diagnosable.
When the IIS is too expensive to compute
Computing an IIS means repeatedly re-solving subsets of the model, so on a large model it can take considerably longer than the original solve. If it does — or if the model is too large to upload — compute it locally and upload just the result:
gurobi_cl ResultFile=facility_contracts.ilp facility_contracts.lp
model.computeIIS()
model.write("facility_contracts.ilp")
The ILP file holds only the irreducible subsystem — for this model, 3 constraints out of 11 — so it stays small no matter how large the model was:
Explainer accepts it in place of the model file, compressed if you like:
gz, bz2, zip, 7z, and xz.
Alongside the chat answer, infeasibility diagnosis produces a Markdown report
saved to the workspace as facility_contracts_infeasibility_report.md. It opens
in a side panel next to the conversation, and you can copy or download it — which
is the point: the diagnosis usually has to go to whoever owns the constraint, and
that person is not in your chat window.
The report follows a fixed four-part structure. What Explainer actually returned, filled in with this model, follows below verbatim.
1. IIS overview
Element |
Type |
Role |
|---|---|---|
|
Constraint ( |
Physical shipment cap for plant 4 |
|
Constraint ( |
Committed delivery to warehouse 2 |
|
Constraint ( |
Committed delivery to warehouse 1 |
|
Binary variable bound |
Plant 4 open/close decision |
|
Continuous variable bounds |
Non-negativity of the other two plant-4 lanes |
The report notes that the two non-negativity bounds appear in the IIS because they
rule out the one indirect escape — shipping a negative quantity on lanes 3 or 4 to
free up room for the contracted lanes — confirming the conflict has no workaround.
It also confirms the naming is good enough to interpret directly: every element
maps to a business rule, and [4] identifies the plant throughout.
2. Infeasibility summary
The plain-language version, for the reader who will not read section 3:
The two new delivery contracts together commit plant 4 to a minimum of 26
units of outbound shipment (8 to warehouse 1 and 18 to warehouse 2), but plant
4's physical capacity is only 19 units. This is a direct, irreconcilable
clash: even if plant 4 is kept open and ships nothing to warehouses 3 and 4,
it cannot simultaneously honour both contracts. There are no other constraints
in the IIS — the rest of the model is irrelevant to this conflict. The problem
is entirely confined to plant 4.
3. Detailed analysis
Explainer breaks the IIS into the two groups that collide, rather than walking the constraint list in isolation:
Group 1 — Plant 4 capacity (the ceiling)
capacity[4]: ship[4,1] + ship[4,2] + ship[4,3] + ship[4,4] - 19*open[4] <= 0
with bounds open[4] [B, LB=0, UB=1], ship[4,3] [C, LB=0], ship[4,4] [C, LB=0].
This caps all outbound shipment from plant 4 at 19 when open, and at 0 when closed —
the binary cannot push the ceiling any higher, and the two non-negativity bounds
mean nothing on lanes 3 or 4 can go negative to make room.
Group 2 — Delivery contracts (the floor)
contract_w2: ship[4,2] >= 18
contract_w1: ship[4,1] >= 8
Together these impose a combined minimum outbound shipment from plant 4 of 26 units. The IIS solver certified that both constraints jointly participate in the infeasibility alongside the capacity row above: no assignment of any variable resolves a floor of 26 against a ceiling of 19 on the same plant’s total output.
4. Remediation options
Ranked from least to most disruptive. Given the stated constraints — contracts negotiable only as a last resort, capacity a physical limit — none of these resolve the conflict within those rules; they are what the business has to choose between:
# |
Change |
Pros |
Cons |
|---|---|---|---|
A |
Partially relax |
Resolves infeasibility with the least disruption to intent — other plants fill warehouse 2’s gap at a modest cost increase. |
Requires renegotiating the larger of the two contracts. |
B |
Relax |
Directly frees capacity headroom. |
|
C |
Relax both contracts proportionally |
Spreads the commercial impact across two customers instead of concentrating it on one. |
Two renegotiations instead of one; exact split needs FeasRelax to compute. |
D |
Raise plant 4’s physical capacity (listed for completeness) |
Eliminates the conflict without touching either contract. |
You told Explainer capacity is not negotiable — included only in case a short-term expansion turns out to be possible. |
The report’s own qualitative preference — spread the least-disruptive change onto
the larger contract, contract_w2 — is not what FeasRelax actually computes when
you ask it to minimize total violation instead: that is the payoff of running it
directly rather than trusting the ranking alone. The next tab does exactly that.
Would you like me to run the Feasibility Restoration workflow? It will use Gurobi’s FeasRelax on the two contract constraints and return the exact minimum reduction required — precise numbers you can take directly into a customer negotiation, rather than guessing at a target.
The IIS tells you which constraints conflict. It does not tell you how much to move them. Feasibility Restoration answers the second question by running Gurobi’s FeasRelax: it searches for the smallest set of adjustments to right-hand sides and bounds that makes the model solvable again, and returns both the adjustments and the resulting plan.
“Smallest” needs defining, and the definition changes the answer.
Both contracts are already signed, so tell me the least I have to give back. Capacity cannot move.
The user has restricted the relaxable set to the two contract constraints
and excluded capacity. That is exactly what FeasRelax’s constraint subset
argument is for — relaxing only contract_w1 and contract_w2, with
equal penalties since neither was named as the more flexible. Minimizing the
total violation first, since “the least I have to give back” is a quantity.
Running FeasRelax on the negotiable subset.
7 thousand units, off one contract. The minimum total violation is 7,
and it lands entirely on contract_w1:
Constraint |
Committed |
Achievable |
Shortfall |
|---|---|---|---|
|
18 |
18 |
none |
|
8 |
1 |
7 |
That is not a coincidence — 26 − 19 = 7 is exactly the overcommitment the
IIS identified, so no repair can do better than 7 in total. What FeasRelax
adds is where to put it: warehouse 2 keeps its full contract, and warehouse
1 drops from 8 to 1. The repaired model is saved to the workspace as
facility_contracts_repaired.lp. Would you like a sensitivity analysis on
it next?
What does the repaired plan look like in full, and what is its total cost (transportation + fixed), using the actual transportation costs from facility_contracts.lp rather than a placeholder objective?
Before quoting a dollar figure, checking that the repaired LP it rebuilt internally actually carried over the real per-lane rates rather than a structural placeholder — the open/closed and shipment pattern can be trusted, but the objective value needs a sanity check first.
Here is the full open/closed and shipment picture — that part is solid:
Plant |
Status |
Fixed cost |
|---|---|---|
1 |
Open |
12,000 |
2 |
Open |
15,000 |
3 |
Closed |
0 |
4 |
Open |
13,000 |
5 |
Open |
16,000 |
But the cost figure needs a flag before you use it: the solver reports an
objective of 56,067 for this repaired plan, and 56,000 of that is the fixed
cost above — the remaining 67 is exactly the total number of units shipped.
That means every transport lane in the rebuilt LP is carrying a coefficient
of 1, a placeholder rather than the real per-lane rate. The actual rates live
in facility.csv, which is not in the workspace yet — please upload it and
I will rebuild with the real rates and give you the true total.
facility.csv attached
Please rebuild the repaired plan with these actual rates and give me the true total cost.
With the real per-lane rates in hand, re-solving the same repaired model — contract_w1 at its floor of 1, contract_w2 unchanged, capacity unchanged — to get an honest total.
210,500 — identical to the specification’s baseline T1 cost:
Route |
Units |
Rate |
Lane cost |
|---|---|---|---|
Plant 1 → Warehouse 3 |
14 |
1,200 |
16,800 |
Plant 1 → Warehouse 4 |
6 |
2,200 |
13,200 |
Plant 2 → Warehouse 1 |
14 |
2,000 |
28,000 |
Plant 2 → Warehouse 4 |
8 |
2,600 |
20,800 |
Plant 4 → Warehouse 1 |
1 |
2,500 |
2,500 |
Plant 4 → Warehouse 2 |
18 |
3,000 |
54,000 |
Plant 5 → Warehouse 4 |
6 |
3,200 |
19,200 |
Transportation 154,500 + fixed 56,000 = 210,500. The contracts add no net cost: the optimizer routed around plant 4’s obligation by leaning on Plant 2’s cheap warehouse-1 rate (2,000) and Plant 1’s cheap warehouse-3 rate (1,200), so the same four plants stay open, plant 3 stays closed, and the total is unchanged from the pre-contract baseline.
Note
The middle exchange above is worth keeping, not editing out: Explainer’s first
pass at “the repaired plan’s cost” silently lost the real transport-cost
coefficients when it reconstructed the LP internally, and reported 56,067 as if
it were the answer. It caught the placeholder itself — every transport
coefficient was exactly 1 — and asked for the source rates rather than
presenting a wrong number with confidence. Uploading facility.csv is what
fixed it. Worth knowing before you trust a repaired-plan cost figure on a model
whose real rates came from a file Explainer has not seen since the last time
the LP was rebuilt.
Why it put all 7 on one contract
Because you asked to minimize the total violation, and that objective is indifferent between 7 on one contract and 3.5 on each: both sum to 7. It returned a vertex solution, which is a corner rather than a compromise.
That is worth knowing, because it is often not what you want. FeasRelax offers three objectives, and on this model each gives a genuinely different repair:
Objective |
Repair |
What it optimizes for |
|---|---|---|
Total violation |
7 off |
Give back as little as possible overall — accepting that one party absorbs all of it. |
Sum of squares |
≈ 2.33 off each of |
Spread the pain. Squaring makes one large violation cost more than several small ones. |
Number of violations |
7 off |
Break as few rules as possible, regardless of how far — here, one constraint instead of two. |
The squared objective needs a caution: it will find a solution that violates many constraints slightly, including ones you consider immovable. On this model it spreads a small violation across all four demand rows and all five capacity rows — a mathematically valid answer that is operationally meaningless, since you cannot ship 0.0015 of a unit past a physical capacity limit. Restrict the relaxable set before reaching for it.
The third is the one to be most careful with. Minimizing the count of violations
put all 7 on capacity[4] — the physical constraint you explicitly excluded —
because breaking one rule beats breaking two under that objective. Explainer only
avoided it here because the relaxable set was restricted to the two contracts.
The restriction is the important input
Left unrestricted, FeasRelax on this model relaxes capacity[4] by 6 and
contract_w1 by 1 — a “cheaper” repair by its own measure, and a useless one,
because it assumes plant 4 can exceed its physical capacity.
Nothing in the LP file distinguishes a physical limit from a policy choice. Both are just rows. That distinction only reaches the agent because you stated it, and it is the difference between a repair plan you can act on and one you have to throw away:
Relaxable set |
Repair returned |
Usable? |
|---|---|---|
Everything |
|
No — assumes plant 4 exceeds its capacity. |
The two contracts only |
|
Yes. |
Reading the result
FeasRelax returns a repair plan, not a decision. The numbers above say warehouse 1 must drop from 8 to 1 — they say nothing about whether that customer will accept it, what the contract’s penalty clause is, or whether renegotiating warehouse 2 down by 3 and warehouse 1 down by 4 would land better commercially even though it sums to the same 7.
That is why the output is a list of adjustments and their cost consequences rather than a change applied to your model. You take it to the two account managers and come back with the split. Then the change goes through Modeler, which puts it in the specification first and propagates it to the code and tests — the loop the Built with Modeler section walks through in its Changing the model tab.
The first two use cases deal with a model that has no answer. Sensitivity analysis deals with one that does, and asks a different question: how much do I trust it? The facility model returns “close plant 3, total cost 210,500”. That is a recommendation someone is going to act on, and the useful follow-up is how far the inputs can drift before it stops being the right call.
Note
Sensitivity analysis in the classic sense — shadow prices, reduced costs, allowable ranges — is an LP concept and does not apply once binary variables are in the model. Asked directly, Explainer said so rather than fixing the binaries and reporting LP ranges anyway: a MIP’s answer moves in discrete steps (a plant opens or closes), and an LP built around one fixed step cannot see the next one. Its actual approach — shown below — is to re-solve named scenarios and report the exact cost premium and break-even thresholds, which is slower per question but never misleads about a step it cannot see.
With plants 1, 2, 4 and 5 open and plant 3 closed, the baseline is 210,500 — transportation 154,500 plus fixed cost 56,000.
facility_modeler.py, facility_spec.md attached
The model says close plant 3 at a total of 210,500. How solid is that? I need to know what would change my mind before I take it to the operations review.
The open/closed decisions are binary, so classic LP shadow prices and ranges do not exist here — reporting them anyway, computed against one fixed configuration of the binaries, would silently hide the point at which the plan itself changes shape. The honest tool for a MIP is a scenario comparison: solve the alternative directly and report the exact difference, not an estimate from a linearization.
Standard LP shadow prices and ranging do not exist for binary variables, so I
am not going to report ranges around plant 3’s closure as if it were an LP.
What the solve does tell you directly: the LP relaxation reaches 199,833, the
actual MIP optimum is 210,500, and that 10,667 gap is the cost of forcing the
binaries to integer values. With only one binary fractional at the root, that
is almost certainly open[3] — every other plant is unambiguously open.
That bounds the question but does not answer it: the true break-even sits somewhere between 0 and 10,667, and the exact number needs a scenario solve of “plant 3 forced open” against the real data. Three inputs are the ones worth stress-testing at the review — plant 3’s fixed cost, plant 3’s transport rates (especially to whichever warehouse is closest to the margin), and capacity at the plants that are currently open, since any of the three could make plant 3 attractive as overflow.
To compute the exact premium I need the real numbers — my file-reader could
not access the attached .csv/.py/.md in this session. Could you
paste facility.csv’s contents directly, or attach the model as a .lp
file?
facility.lp attached (the baseline model, no contracts), plus the facility.csv rate table pasted inline, plant 3’s capacity (17) and fixed cost (17,000)
Please build the “plant 3 forced open” scenario and give me the exact cost premium and break-even threshold.
With an .lp file the reader has no trouble with, this is a direct
re-solve: add open[3] = 1, solve to optimality, and diff every route and
every cost component against the baseline rather than reporting a single
number.
The premium for forcing plant 3 open is exactly 400 — solved directly, not estimated:
Baseline (plant 3 closed) |
Forced open (plant 3 open) |
|
|---|---|---|
Plants open |
1, 2, 4, 5 |
1, 2, 3, 4 |
Fixed costs |
56,000 |
57,000 |
Transport costs |
154,500 |
153,900 |
Total |
210,500 |
210,900 |
That is a margin of less than 0.2% on a 210,500 plan. What actually changes: plant 3 replaces plant 5 on the warehouse-4 lane —
Route |
Baseline |
Forced open |
|---|---|---|
Plant 2 → Warehouse 1 |
14 |
15 |
Plant 2 → Warehouse 4 |
8 |
7 |
Plant 3 → Warehouse 4 |
0 |
7 |
Plant 5 → Warehouse 4 |
6 |
0 |
— at 3,100/unit versus plant 5’s 3,200, a 700 transport saving on 7 units. That is outweighed by plant 3’s fixed cost being 1,000 higher than plant 5’s, netting the +400 premium.
Break-even thresholds for the review:
Plant 3’s fixed cost would need to fall to 16,600 (400 below its actual 17,000) before opening it becomes optimal.
Plant 3’s warehouse-4 rate would need to fall to roughly 3,043/unit or lower to close the same gap on transport alone.
Any capacity tightening at plant 2 or plant 5 shrinks the 400 margin directly — demand growth that pushes plant 2 past its 22-unit ceiling would make plant 3 attractive at no premium at all.
Take this framing to the review: close plant 3, but the margin is 400 and three independent inputs can each flip it. It is not “close plant 3 with confidence” — it is “close plant 3 unless its fixed cost drops below 16,600, its warehouse-4 rate drops below 3,043, or plant 2’s utilization approaches its capacity ceiling.”
Why a scenario re-solve instead of LP shadow prices
The standard shortcut — fix the binaries at the recommended plan, solve the transportation LP that remains, and read its shadow prices and ranges — was available here, and Explainer did not reach for it. The reason is exactly what its own numbers expose: an LP fixed at “plants 1, 2, 4, 5 open” cannot represent the alternative “plant 3 open, plant 5 closed” at all, because that alternative moves a binary, not a continuous variable inside a fixed structure. A shadow price computed on the fixed-binary LP would describe how cost responds to nudging plant 4’s capacity up or down within the current plan — a real and useful number, but a different question from “should a different plant be open,” which is the one the operations review actually needs answered.
The gap between the LP relaxation (199,833) and the true MIP optimum (210,500) is the tell: 10,667 of “value” that a pure LP view of this model cannot realize, because realizing it requires an integer decision. Reporting a range against the relaxed or fixed-binary LP would understate how close the real decision is — plant 3’s fixed cost is only 400 away from flipping the plan, not 10,667.
This is the general shape of the limitation, not a quirk of this instance. A MIP’s answer moves in steps, and the steps are where the interesting decisions live. It is also why Modeler’s own test scenarios (T2, T3, T4) are full re-solves of the MIP rather than sensitivity ranges, and why T2’s demand spike correctly reports plant 5 closing instead of plant 3 — a step no fixed-binary LP could have predicted from the baseline alone.