AgentStack
SKILL verified Apache-2.0 Self-run

Rai Prescriptive Results Interpretation

skill-relationalai-rai-agent-skills-rai-prescriptive-results-interpretation · by RelationalAI

Interprets optimization solver output including solution extraction, status codes, quality assessment, result explanation, sensitivity analysis (shadow prices, reduced costs, basis status), and infeasibility diagnosis (conflict / IIS). Use when analyzing solve results, reading dual or marginal values, diagnosing why a model is infeasible, or communicating optimization outcomes — not solver config…

No reviews yet
0 installs
7 views
0.0% view→install

Install

$ agentstack add skill-relationalai-rai-agent-skills-rai-prescriptive-results-interpretation

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No 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.

Are you the author of Rai Prescriptive Results Interpretation? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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 to rai-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:

  1. Recall the optimization goal captured in the problem's variables and objective — what decisions were being made, and what should success look like?
  2. Check termination status
  3. Extract solution values (query decision variable properties)
  4. Assess solution quality against the original goal (trivial solution detection, reasonableness checks)
  5. Explain results to stakeholders (what was decided, why, business impact)
  6. 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:

  1. 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.
  2. 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
  1. 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
  1. 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-6 to remove near-zero solver noise
  • Scalar extraction: problem.solve_info().objective_value for single values (no query needed)
  • Always alias: Use .alias() on every output column for clean DataFrames
  • Int128 handling: Integer aggregates (aggs.count, aggs.sum over Integer) and integer-typed solver columns return as nullable Int128Array — pandas reductions raise TypeError without 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 / .lowermodel.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`?

  1. 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 accumulates satisfy calls — there's no in-place remove_constraint API; 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

  1. Check that all variables have appropriate bounds (especially upper bounds for maximize, lower bounds for minimize).
  2. Verify budget/capacity/resource constraints are present.
  3. Check objective direction: minimizing when should maximize, or vice versa?
  4. 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.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.