Install
$ agentstack add skill-relationalai-rai-agent-skills-rai-prescriptive-results-interpretation ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Solution Interpretation
> Requires relationalai>=1.11.0. Sensitivity analysis (shadow prices, reduced costs, basis status) and conflict / IIS diagnosis depend on the dual-bearing solver; earlier versions reject solve("highs", sensitivity=True) and omit the dual fields (sensitivity, dual_status, …) on solve_info(). See rai-setup.
Summary
What: Post-solve activities — solution extraction, status interpretation, quality assessment, result explanation, and sensitivity analysis.
When to use:
- Extracting solution values after a successful solve
- Communicating any solver status to stakeholders (OPTIMAL, INFEASIBLE, DUALINFEASIBLE, TIMELIMIT)
- Diagnosing why a solved-OPTIMAL result is trivial, wrong, or has coverage gaps (all zeros, all at bounds, concentrated on one entity)
- Assessing TIME_LIMIT results (gap interpretation, whether to increase time or simplify model)
- Explaining results to stakeholders in business language
- Running sensitivity / what-if analysis
Root-cause diagnosis of INFEASIBLE/DUAL_INFEASIBLE (demand vs capacity, contradictory constraints, missing bounds) lives in this skill — see Status Interpretation below. For solver-level error messages and engine-driven debugging (si.error, print_format=), see rai-prescriptive-solver-management.
When NOT to use:
- Designing or fixing the formulation itself (adding constraints, changing variables) — see
rai-prescriptive-problem-formulation. In particular, if the result is OPTIMAL and technically valid but the user rejects it on preference grounds ("that's too much X", "I don't like this allocation"), this indicates latent constraints, not a solver or formulation bug — route torai-prescriptive-problem-formulation> Constraint Elicitation > Post-Solve: Iterative Refinement. - Solver configuration, parameter tuning, or solver-level failures — see
rai-prescriptive-solver-management - Query syntax (select, aggregation, joins) — see
rai-querying - PyRel syntax (imports, types, properties) — see
rai-pyrel-coding
Overview:
- Recall the optimization goal captured in the problem's variables and objective — what decisions were being made, and what should success look like?
- Check termination status
- Extract solution values (query decision variable properties)
- Assess solution quality against the original goal (trivial solution detection, reasonableness checks)
- Explain results to stakeholders (what was decided, why, business impact)
- Run sensitivity analysis (parameter sweeps, what-if scenarios)
Interpretation Workflow
Precondition — you must have a finished solve. A formulation description, an in-progress or backgrounded solve, or "the solver is still running" is not a result and must never be presented as the answer; run the solve to completion first (see rai-prescriptive-solver-management > Run to completion before reporting). Once it has returned a terminal status and you hold the objective and variable values, interpret results in this order:
- Recall the goal — Before inspecting solver output, review what the formulation was trying to achieve: what decisions were being made, what objective was set, and what constraints were imposed. This context is essential for judging whether results are meaningful or trivial — an OPTIMAL status means nothing if the solution doesn't address the original intent.
- Check status (Status Interpretation) — Is the solve OPTIMAL, INFEASIBLE, DUALINFEASIBLE, or TIMELIMIT?
- If INFEASIBLE, DUAL_INFEASIBLE, or any error status: stop, diagnose root cause (Diagnosis Checklist), do not present results
- If TIME_LIMIT with large gap (>10%): flag uncertainty, consider increasing time or simplifying
- Assess quality (Quality Assessment) — Is the solution meaningful or trivially empty?
- Check non-zero ratio, objective value plausibility, variable distribution
- If trivial (all zeros, all at bounds): diagnose and fix before presenting
- Extract and filter — Remove noise from raw solver output
- Filter near-zero values ( 0.5).inspect() # or .to_df() for Python analysis
**`populate=False` approach: `Variable.values()` on ProblemVariable**
`solve_for()` returns a `ProblemVariable` — a Concept with **back-pointer attributes** for each non-value field in the Property's format string. Call `.values(sol_index, value_ref)` on it to extract solution values:
```python
# Property: f"{Assignment} has {Float:x}" → back-pointer var.assignment, value field x
assign_var = problem.solve_for(Assignment.x, type="bin", populate=False)
problem.solve("highs")
value_ref = Float.ref()
df = model.select(
assign_var.assignment.worker.name.alias("worker"),
value_ref.alias("value"),
).where(assign_var.values(0, value_ref), value_ref > 0.5).to_df()
Extraction principles:
- Binary variables: Filter with
> 0.5, not== 1(numeric tolerance in MIP solvers) - Continuous variables: Filter with
> 1e-6to remove near-zero solver noise - Scalar extraction:
problem.solve_info().objective_valuefor single values (no query needed) - Always alias: Use
.alias()on every output column for clean DataFrames - Int128 handling: Integer aggregates (
aggs.count,aggs.sumover Integer) and integer-typed solver columns return as nullableInt128Array— pandas reductions raiseTypeErrorwithout a cast. Inline the cast at extraction time, not later:
``python df = model.select( Route.name.alias("route"), aggs.count(Trip).per(Route).alias("trips"), # Integer aggregate → Int128Array ).to_df() df["trips"] = df["trips"].astype("int64") # Cast before .sum() / .groupby().agg() / .iloc[] ``
For per-pattern variations (multiple solutions, iterative solving, scenario/parametric extraction, full Variable.values() back-pointer naming rules with table of examples, silent-failure warnings, and Snowflake table export), see [references/solution-extraction-details.md](references/solution-extraction-details.md).
Sensitivity & conflict attributes
Together these flags make a solve interrogable. Use sensitivity=True to learn, at the optimum, what each constraint is worth and how close each unused option is to entering the plan; use conflict=True to learn why an infeasible model has no plan. Each result reads back joined to the entity it describes.
solve(sensitivity=True) and solve(conflict=True) populate extra attributes on the objects solve_for() and satisfy() already return. Read them straight off those objects — like .name or .lower, not through a separate API. Each is single-valued (no sol_index axis, unlike Variable.values()) and joins to its entity through the back-pointer key, never by parsing a constraint's name string. Request the flags on the solve itself — see rai-prescriptive-solver-management > First-class solve parameters for the request side; this is the read side. sensitivity=True needs an objective (it returns duals at the optimum); conflict=True needs no objective (it diagnoses an infeasible model).
Marginals are single-valued — don't confuse their shape with the indexed solution accessor:
| Accessor | Shape | Read like | |---|---|---| | var.reduced_cost / con.shadow_price | single-valued (no sol_index) | .name / .lower — model.select(var.reduced_cost) | | var.values(k, ref) | indexed (needs a sol_index k + a value ref) | where(var.values(k, ref)) (the multi-solution accessor) |
Variable attributes (off the solve_for() return):
| Attribute | Type | Meaning | |---|---|---| | var.reduced_cost | Float | Reduced cost (Gurobi RC convention). **Sign depends on the objective sense and which bound is active** — under minimize, nonbasic at lower prices ≥ 0, at upper ≤ 0; under maximize both flip. Empty for MIP. | | var.basis_status | String | MOI VariableBasisStatus ("BASIC", "NONBASIC_AT_LOWER", …). Absent for interior-point (Ipopt). | | var.lower_in_conflict / var.upper_in_conflict / var.integrality_in_conflict | predicate | True if the solver flagged that bound / integrality as conflicting — a lead, not proof (collapses IN_CONFLICT + MAYBE_IN_CONFLICT; for integrality Gurobi only ever reports the "maybe"). Use bare: where(var.lower_in_conflict). |
Constraint attributes (off the satisfy() return):
| Attribute | Type | Meaning | |---|---|---| | con.shadow_price | Float | ∂obj/∂RHS (Gurobi Pi). Empty for MIP. | | con.basis_status | String | MOI ConstraintBasisStatus. Absent for interior-point. | | con.in_conflict | predicate | True if the solver flagged the constraint as conflicting — a lead, not proof (collapses IN_CONFLICT + MAYBE_IN_CONFLICT). Use bare: where(con.in_conflict). |
food_vars = problem.solve_for(Food.amount, name=Food.name, lower=0)
qty = Float.ref()
intake = sum(qty * Food.amount).where(Food.contains(Nutrient, qty)).per(Nutrient)
floor = problem.satisfy(model.require(intake >= Nutrient.min), keyed_by={"nutrient": Nutrient})
problem.minimize(sum(Food.cost * Food.amount)) # sensitivity=True needs an objective — duals are read at the optimum
problem.solve("highs", sensitivity=True)
# Marginals read straight off the returned objects (like .name) and join to entity
# data through the back-pointer key — never by parsing the constraint name string:
model.select(food_vars.food.name, food_vars.reduced_cost, food_vars.basis_status).inspect()
model.select(floor.nutrient.name, floor.nutrient.min, floor.shadow_price).inspect()
Reading a marginal or conflict flag by entity (floor.nutrient) requires the constraint's grounding key to be declared at formulation time — satisfy(..., keyed_by={"nutrient": Nutrient}); constraints expose no back-pointer without it (a variable's, like food_vars.food, is automatic). For the full keyed_by idiom — multi-entity keys, scalar keys, the key-uniqueness rule — see rai-prescriptive-problem-formulation/references/constraint-formulation.md. For the dual economics and conflict (IIS) reading in depth, see [references/sensitivity-analysis.md](references/sensitivity-analysis.md) and [references/conflict-analysis.md](references/conflict-analysis.md).
Post-solve constraint verification
problem.verify(*fragments) temporarily installs constraint ICs, triggers a query to evaluate them, and removes them. Useful for checking that the solver's solution satisfies constraints — particularly for exact solvers (HiGHS MIP, MiniZinc):
coverage_ic = model.where(...).require(...)
problem.satisfy(coverage_ic)
problem.solve("minizinc", time_limit_sec=60)
problem.verify(coverage_ic) # Warns if any constraint is violated
verify() checks termination_status first — warns and returns early for non-successful solves. ICs are cleaned up in a finally block even on exceptions.
Status Interpretation
Canonical statuses and the action each demands:
| Status | Meaning | Action | |---|---|---| | OPTIMAL | Best possible solution within solver tolerance (typically 1e-6 LP, 0.01% MIP gap) | Proceed to quality assessment, then explain results | | INFEASIBLE | No solution satisfies all constraints — the problem as stated is impossible | Diagnose before presenting — see Infeasible diagnosis below | | DUAL_INFEASIBLE | Objective can improve infinitely (unbounded); the status is "DUAL_INFEASIBLE", not "UNBOUNDED" | See Unbounded diagnosis below | | "Feasible" (MIP) | HiGHS found a solution but didn't prove optimality within the default MIP gap | Treat the same as TIME_LIMIT for gap interpretation | | TIME_LIMIT | Feasible solution found, optimality unproven in the time allowed | Read the realized gap from si.ancillary — schema-less Mapping[str, str], so discover the key first (si.display() or iterate .items()) and read with .get(key), never si.ancillary[key] (a hardcoded key risks KeyError across solvers); fall back to the solver log. Gap 10%: uncertain — increase time limit, tighten Big-M, add symmetry breaking, or simplify | | Error / Unknown | Compilation or solver error prevented a solution (undefined properties, type mismatches, expression syntax, solver license) | Check compilation output, fix expression syntax, verify all referenced properties exist; solver-level errors → rai-prescriptive-solver-management |
A non-optimal status is information about problem structure, not necessarily a failure to fix: intentionally solving a proposed constraint set to test feasibility boundaries is a valid technique (INFEASIBLE = the set is too tight), and a TIMELIMIT gap under 5% with business-sensible values is presentable as "near-optimal" — don't automatically increase time limits ("Is a 2% improvement worth waiting 10 minutes?"). Iterate on INFEASIBLE with no clear conflict, DUALINFEASIBLE, gap > 10%, or a trivial solution; accept OPTIMAL, gap = 10 and x upper_bound`?
- Bisect: rebuild the Problem with one
problem.satisfy(...)call removed — or one variable bound relaxed, since bounds alone can be the source — at a time and re-solve. The member whose removal restores feasibility is the binding conflict. (Problem accumulatessatisfycalls — there's no in-placeremove_constraintAPI; build a fresh Problem each iteration.)
After localizing: present trade-off options, add slack/penalty variables. A common and valuable path is moving the conflicting hard constraint to the objective with a penalty — feasibility restoration through softening is often more useful than pure diagnosis.
Unbounded (DUAL_INFEASIBLE) diagnosis
- Check that all variables have appropriate bounds (especially upper bounds for maximize, lower bounds for minimize).
- Verify budget/capacity/resource constraints are present.
- Check objective direction: minimizing when should maximize, or vice versa?
- Check coefficient signs in the objective.
Audit / witness mode — status-aware verdict (CSP-style)
When the question is "does the property hold?" or "is there any configuration where X happens?", the answer comes from the termination status, not the objective value. This inverts MIP intuition: INFEASIBLE is the desired outcome.
| Termination status | Audit verdict | |--------------------|---------------| | INFEASIBLE | PASS — no configuration satisfies the witness condition. The property holds. | | OPTIMAL or SOLUTION_LIMIT | FAIL — at least one configuration was found that violates the property. Extract witnesses for the report. | | Other (TIME_LIMIT, error, LOCALLY_SOLVED on a CSP) | INCONCLUSIVE — solver did not exhaust the search. Do not interpret as PASS. |
Critical: num_points() == 0 does not prove the property holds — the solver may have crashed, hit a time limit, or produced zero solutions for any other reason. Always check the termination status first.
For the full pattern (counterexample-IC-as-negation, verdict-gated extraction, witness reporting), see rai-prescriptive-problem-formulation/references/csp-formulation.md § 5 and rai-prescriptive-problem-formulation/examples/audit_witness.py.
Solvability Ladder
The solvability ladder defines progressive quality gates for an optimization formulation. Each level subsumes the previous — reaching "non-trivial" means all prior gates also passed. Use this to classify where a formulation stands and what to fix next.
| Level | Gate | What it proves | Check | |-------|------|---------------|-------| | generates | Code generates | LLM produced syntactically valid PyRel | Code parses without syntax errors | | compiles | Compiles | display() succeeds — formulation converts to solver-ready form | display() returns without error; variables, constraints, objective all registered | | solves | Solves | Solver accepts the problem and returns a result (any status, no crash/error) | solve() completes without exception; `problem.solve_
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: RelationalAI
- Source: RelationalAI/rai-agent-skills
- License: Apache-2.0
- Homepage: https://relational.ai
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.