Analyzing Results
Every solve persists an execution, and the execution page is where you read the result. It is built around one principle: lead with facts that are exact for your solution, and label everything that is approximate.
In the Model Workspace, the results drawer that opens after a solve shows a compact summary — status, objective, how many variables were assigned — and a View full results link to the execution page described here.
The structured solution
A solved assignment or routing model used to read as a wall of assign_v3_o107 = 1 rows. The execution page now recovers the index structure those flat names came from and leads with a grouped view: one block per variable family, sub-grouped by the first index — assign → v3 → the members chosen for it.
- Non-zero only is on by default: the grouped view shows the decisions (binary members that are on show just their label; anything else shows its value).
- The Table toggle switches to the full flat table, including per-variable sensitivity columns.
- Variables with no recoverable structure render flat in an "Other variables" bucket — nothing regresses for continuous or custom-named models.
- On very large solutions the grouped view renders a bounded prefix (500 values) with a Show all opt-in, so the page stays responsive.
Structure recovery is server-side and honest: a model compiled from a JModel source carries its real index sets, authoritatively. For flat or imported models, JAOT parses the conventional name_1_2 numeric-suffix pattern only — anything ambiguous is left flat rather than guessed.
The solve summary
The Solve summary card states how the model solved, honestly:
- "Proven optimal at the root node — no branching" — the presolver and cuts closed the problem immediately.
- "Optimal proven after N nodes" — real branch-and-bound work happened.
- "Time limit reached — best solution has gap X%" — you got the incumbent, not a proof.
Below the headline: the objective value, the final gap, node and iteration counts, and wall-clock solve time. (This card replaced a live convergence chart that was empirically a flat line for real models — solvers find the incumbent almost immediately and spend the rest of the run proving optimality, so a curve suggested progress that wasn't the story.)
Exact analysis
The Analysis section is computed on demand from the solution x* and the problem data — not from the LP relaxation — so every figure is exact for the integer solution and solver-agnostic:
- Binding constraints — "12 of 439 constraints are binding at the optimum (no slack)", with the tightest constraints listed first: activity, operator, and right-hand side per row.
- Constraint families — the per-row facts aggregated by constraint family (the same structure recovery the solution view uses, applied to constraint names): share of binding rows, slack min/mean/max, and utilization per family, ranked so the saturated families lead and the headroom sits at the bottom. A 10,000-row model reads as a ten-line table.
- Utilization — per ≤-constraint, activity as a percentage of its right-hand side, so "capacity is at 97%" is one glance.
- Objective contributions — which terms c·x* actually drive the objective, as a ranked bar list — plus the same totals rolled up by variable family, so "transport is 80% of the cost" is one glance too.
The family KPIs are computed over all analysed rows, not the capped display lists, so they hold at model scale. Very large models degrade gracefully: the analysis is bounded (the most informative rows lead, a truncation note appears) rather than slow.
The classic LP-relaxation shadow prices are still available, demoted into a collapsed section underneath and deduplicated ("47 constraints · shadow price 1.0" instead of 47 identical rows). For a MIP they are duals of a different, easier problem — directional at best — which is exactly why they no longer lead the page. See Understanding Your Solution for how to read them, and for the plain-language AI explanation of the result.
What-if analysis (re-solves)
The exact analysis tells you what is tight. It cannot tell you what loosening it is worth — in a MIP that question has no reliable closed form, which is why the shadow prices above are demoted. What if things changed? answers it the honest way: it perturbs the model and solves it again.
Two kinds of scenario, both measured on the real model:
- Price of one unit — each top binding constraint is loosened and tightened by δ (one whole unit on integral data, 1% otherwise) and re-solved. The tornado chart shows the objective change per unit of right-hand side, so rows with different δ compare directly: "one more hour on machine 3 is worth −420 €, one more truck −80 €". Green improves the objective, red worsens it.
- Cost of deciding otherwise — each key binary decision is forced to its opposite value and re-solved, pricing the regret of overruling the model. A decision whose overrule makes the model infeasible is reported as such: not expensive, impossible.
Because every row is a full solve, the batch is opt-in (a button), runs in the background on the solver queue, and is bounded twice: a per-scenario time limit derived from your original solve time, and a wall-clock budget for the whole batch. Defaults are 20 re-solves — 8 constraints ranged both ways plus 4 decisions — with 30s per scenario and 5 minutes per batch; a self-hosted deployment can change all of them in the admin panel.
These limits bound the analysis only; your solve time limits are untouched.
When the budget runs out, the remaining scenarios come back marked as not run and the result says it is partial — it is never padded with estimates. A scenario stopped at its own time limit is shown with a ≈: an incumbent bounds the true change, it does not equal it. Relaxations are always solved before tightenings, so a batch cut short still spent its budget on the more informative half. The finished analysis is cached on the execution, so reloading the page does not re-solve anything, and asking twice joins the batch already running.
Explain this to me
Under the results, Explain this to me has the assistant read the scenarios back in plain business language: what actually limits you, what is not worth buying, and what deciding otherwise would cost. It is handed the measured scenarios and nothing else — it cannot invent a scenario that was not run, cannot extrapolate a per-unit figure beyond the change actually tested, and has to respect each row's status (exact, bound, infeasible, never run). Like the batch, it is opt-in and cached on the execution, so reading it again is free. It uses your organization's own API key when you have configured one; otherwise it counts against the platform's AI budget.
The variables chart
The Visualization tab charts variable magnitudes. When every non-zero variable has the same value — the normal case for a binary assignment solution, where a bar chart of identical bars carries zero information — it collapses to an aggregate ("N variables at 1, M at zero") with a Show chart anyway override.
API
Everything on the page is available programmatically:
GET /api/v2/models/executions/{execution_id}
GET /api/v2/models/executions/{execution_id}/exact-analysis
POST /api/v2/models/executions/{execution_id}/scenario-analysis
GET /api/v2/models/executions/{execution_id}/scenario-analysis
POST /api/v2/llm/executions/{execution_id}/explain-scenarios
The first returns the persisted execution (solution variables include the recovered family and index_tuple); the second computes the exact analysis on demand — binding constraints, slack/utilization, objective contributions, and the per-family KPI aggregates (families, contribution_families).
The scenario POST queues the what-if batch and the matching GET polls it. Both return the same job envelope — status (absent / running / completed / failed) plus, once finished, analysis with rhs_scenarios, decision_scenarios, and the budget accounting (resolves_used, resolves_planned, seconds_used, partial). The POST is idempotent: it joins a batch already running and serves a finished one from the cache.
explain-scenarios returns { explanation, cached } for a finished batch, and 422 when there is nothing measured to explain yet.