Prescriptive Problem
Requires relationalai>=1.11.0. The dual-guided multi-objective methods here read solver sensitivity via solve("highs", sensitivity=True); earlier versions reject the request. See rai-setup.
Summary
What: Everything before the solve runs — decision variables, constraints, objectives, problem-type classification, solver selection, and pre-solve validation. Assumes a problem has already been selected via discovery.
When to use:
- Formulating variables, constraints, and objectives for a selected problem; reviewing or validating an existing formulation
- Translating business requirements into mathematical formulation (and eliciting the constraints users can't articulate upfront)
- Debugging formulations that would produce trivial or infeasible solutions (missing constraints, conflicting bounds, wrong aggregation scope), or relaxing an over-constrained formulation to resolve a reported conflict / IIS
- Classifying the problem type (LP / MILP / QP / QCP / NLP / CSP) and choosing a solver
- Designing multi-concept coordination (flow networks, selection + quantity)
When NOT to use:
- Executing the solve, reading status/duals/IIS, quality assessment, or explaining results — see
rai-prescriptive-results
- Question discovery (what can this ontology answer) — see
rai-discovery
- PyRel syntax — see
rai-pyrel; ontology modeling or enrichment — see rai-ontology
Overview:
- Ground in the base ontology via
inspect.schema(model)
- Define decision variables (type, bounds, scope, naming)
- Define constraints (forcing, capacity, balance, linking)
- Define objective(s) (direction, coefficients, multi-component)
- Validate the complete formulation — including the pre-solver audit that variables/constraints/objectives registered, bound, and grounded
- Classify the problem type, select the solver, solve and refine (targeted
display(ref) diagnosis)
- Post-solve refinement — present results, surface reactions, iterate on the formulation
Quick Reference — Problem API
from relationalai.semantics.reasoners.prescriptive import Problem
from relationalai.semantics.std import aggregates as aggs
# Problem(model, Float) for MIP-style; Problem(model, Integer) for CSP-style (see references/csp-formulation.md).
problem = Problem(model, Float)
# Decision variable — prefer scoped form (where=[...]); capture the ref for targeted diagnostics.
x_flow = problem.solve_for(Lane.flow, where=[Lane.active], name=["item_id", "src_id"], lower=0.0, type="cont")
# Constraint — capture the ref; name=[Entity.id] makes display rows identifiable.
cap = problem.satisfy(
model.require(aggs.sum(Lane.flow).per(Source).where(Lane.from_source(Source)) <= Source.capacity),
name=["cap", Source.id])
cost = problem.minimize(aggs.sum(Lane.flow * Lane.unit_cost))
| Method |
Signature |
Purpose |
solve_for |
(expr, where=, populate=True, name=, type=, lower=, upper=, start=) |
Declare decision variable. Returns ProblemVariable (a Concept). Row inspection: model.select(var.name, var.lower, var.upper).to_df(). type: "cont", "int", "bin". name= entries may be strings, scalar entity properties, or (relationalai>=1.13) enum members; on 1.12.0 pass Member.name |
satisfy |
(expr, name=, keyed_by=) |
Add constraint. Returns ProblemConstraint. keyed_by={"key": Concept} declares the family's grounding keys as entity back-pointers for post-solve readback — see constraint-formulation.md > Declaring constraint keys |
minimize / maximize |
(expr, name=) |
Set the (single) objective. Returns ProblemObjective |
solve |
(solver, time_limit_sec=, ...) |
Execute — parameters, diagnostics requests, and everything after are rai-prescriptive-results |
verify |
(*fragments) |
Post-solve constraint check — pass the original model.require(...) values (see rai-prescriptive-results) |
display |
(part=None, *, where=None, limit=None) |
Print materialized formulation; display(ref) for one constraint/objective; limit=N caps rows. Variable subconcepts raise — query via model.select(...) |
num_variables / num_constraints / num_min_objectives / num_max_objectives |
() |
Engine-queryable counts; usable in model.require(...) before solve |
problem.variables / .constraints / .objectives |
attributes |
Registered refs in declaration order — iterate to walk an unfamiliar Problem |
Problem Type Classification and Solver Selection
Classify from the formulation itself — variable types, then nonlinearity, then constraint structure:
| Type |
Signature |
| LP |
All variables continuous; objective and constraints linear |
| MILP |
Some integer/binary variables; everything linear (products of integer variables make it nonlinear). Keep Big-M tight |
| QP |
Quadratic terms in the objective only (e.g. covariance risk); constraints linear. Check convexity for a global optimum |
| QCP |
Quadratic terms in constraints (norm bounds); more restrictive solver support |
| NLP |
Nonlinear functions (exp, log, sqrt); local optima possible; integer + NLP (MINLP) is very hard |
| CSP |
No meaningful objective — find any feasible solution; discrete combinatorial constraints; benefits from global constraints |
Support matrix and profiles
| Problem type |
Gurobi |
HiGHS |
Ipopt |
MiniZinc |
| LP / MILP |
YES / YES |
YES / YES |
YES / NO |
NO / NO |
| QP / QCP |
YES / YES |
YES (convex obj) / NO |
YES / YES |
NO / NO |
| NLP |
YES |
NO |
YES |
NO |
| Constraint programming |
NO |
NO |
NO |
YES |
| Discrete vars / continuous vars |
YES / YES |
YES / YES |
NO / YES |
YES / NO |
- Gurobi (commercial, via RAI): best on every type it supports — always prefer it when the user has a license. Needs a named prescriptive engine with a Snowflake secret + external access integration in
raiconfig.yaml (see rai-setup); fall back to HiGHS/Ipopt without one.
- HiGHS (open-source): LP, MILP, convex QP objectives. No indicator constraints (
implies fails — use Big-M), no SOS, no quadratic constraints, no nonlinear functions.
- MiniZinc (open-source, Chuffed backend): CP/CSP. Requires
Problem(model, Integer) — any Float decision or data coerces to MIP and MiniZinc rejects it. solution_limit=K enumerates multiple solutions (SOLUTION_LIMIT status = stopped at K; OPTIMAL = search exhausted). Only all_different and implies are exposed from PyRel today. Native logs need log_to_console=True.
- Ipopt (open-source): smooth continuous NLP, local optima only; no integer/binary variables (will FAIL).
Decision rules, in order: (1) any integer/binary variable → Ipopt invalid; Gurobi preferred, HiGHS without license; MiniZinc only for pure discrete constraint satisfaction. (2) nonlinearity (math.exp/log/sqrt, x**n) → HiGHS and MiniZinc invalid; continuous → Gurobi or Ipopt; discrete + nonlinear → Gurobi only. Trig and division between two decision variables are not lowered to any solver — reformulate piecewise-linear. (3) quadratic constraints → HiGHS invalid. (4) no objective → MiniZinc for discrete CSP; otherwise Gurobi/HiGHS feasibility solve.
| Your problem has... |
Gurobi available |
No Gurobi license |
| Binary/integer + linear (MILP) |
Gurobi |
HiGHS |
| Binary/integer + quadratic (MIQP) |
Gurobi |
(no open-source alternative) |
| Continuous + linear/quadratic-objective |
Gurobi |
HiGHS |
| Continuous + quadratic constraints (QCP) |
Gurobi |
Ipopt |
| Continuous + nonlinear (NLP) |
Gurobi or Ipopt |
Ipopt |
| Discrete constraint satisfaction / multiple solutions |
MiniZinc |
MiniZinc |
Problem-size guidelines and Problem initialization patterns: solver-details.md.
Global constraints
all_different, implies, SOS1/SOS2 give the solver combinatorial structure, with solver requirements: all_different → MiniZinc (else O(n²) pairwise inequalities); implies → Gurobi or MiniZinc (else Big-M); SOS1/SOS2 → Gurobi only (else binary reformulations). Syntax, examples, CP-vs-MIP decision guide: global-constraints.md.
Formulation Workflow
Interaction mode: ask the user first — Guided (confirm variables, constraints, objective at each step; framing involves judgment about hard vs soft and scoping) or One-shot (best single pass, review after).
Step 1: Ground in the base ontology
Before writing solve_for / satisfy / minimize / maximize, confirm every name and type against the real schema:
from relationalai.semantics import inspect
schema = inspect.schema(model)
for name in referenced_concepts:
assert name in schema, f"Concept {name} not in model"
Catches the two silent failure modes behind most prescriptive errors: hallucinated surface (Customer.tier when the real property is Customer.category — the solver happily runs on wrong variables) and wrong-type inference (Integer treated as Float, masking incorrect bound derivation).
Reuse upstream derived surface. Subtypes and properties earlier steps wrote back (classification subtypes, centrality scores, flags) are in inspect.schema(model) — reference them directly rather than re-deriving. See examples/chained_rules_prescriptive.py.
Confirm modeled dimensions. If the problem spans periods/scenarios/categories, that dimension must exist — as a Concept (Week, Scenario), an Integer ref scoped via std.common.range(), or a model.Enum (relationalai>=1.12; enum-indexed solve_for creates one binary per (entity, member) — csp-formulation.md § Enum-indexed decision). Never iterate the dimension in Python or hand-enumerate its values. See rai-ontology if enrichment is needed.
When to skip: small greenfield models or one-shot formulations you just wrote yourself.
Step 2: Define Variables
What can the solver control? Identify the primary decision entity, then auxiliary/aggregation variables; choose types (cont/int/bin) and bounds from data; for minimize objectives include a slack/unmet variable to avoid infeasibility. Full treatment (context integration, entity creation, parametric/time-indexed, slack): variable-formulation.md.
Step 3: Define Constraints
What must the solution satisfy? Add forcing constraints first (they prevent trivial zero solutions on minimize), then capacity/resource from data properties, then flow conservation for network structure. Derive parameters from data ranges. For multi-period recurrence (inventory balance, sequencing), use declarative quantification — Concept.ref() adjacency joins (w_prev.num == w.num - 1) or where=[t == std.common.range(start, stop)] — one satisfy() grounds all periods: constraint-formulation.md > Temporal recurrence, examples/multi_period_flow_conservation.py. Canonical constraint shapes per problem family: problem-patterns-and-validation.md.
Step 4: Define Objective(s)
Map the stated goal to minimize/maximize (objective-formulation.md). Check trivial-solution risk: "if all variables = 0, are all constraints satisfied?" — if yes, Step 3 needs a forcing constraint. Competing objectives? A penalty bundling two concerns, or a constraint representing a competing goal, may mean the user wants the tradeoff explored — multi-objective-formulation.md (epsilon constraint). Parameter what-if? Parameterize with the Scenario Concept pattern — one solve over all scenarios, results stay in the ontology: scenario-analysis.md.
Step 5: Validate
Structural checks: every variable in ≥1 constraint (a variable appearing only in the objective gets pushed straight to a bound — usually a missing constraint, not a modeling choice); every constraint references ≥1 decision variable; .where() join paths connect to actual data; bounds consistent (lower ≤ upper); exactly one objective for an optimization solve, zero for pure feasibility/CSP.
- Trivial-solution gate (before presenting or solving): substitute every decision variable with its objective-preferred bound (typically zero for minimize) and evaluate against the actual input data. If every constraint still holds, the solver will return that trivial point as optimal — the formulation is wrong. The non-obvious failure: a forcing constraint that exists but is vacuous against current data (its
where= matches no rows). Do not present a formulation that fails this gate; if the outcome depends on input data, state that with row-count evidence.
- Data-driven feasibility precheck: when bounds derive from data, verify a feasible point exists — at the constraint's natural disaggregation level (per-entity for per-entity constraints; a global
Σ supply ≥ Σ demand can mask per-slice infeasibility). Run as PyRel aggregation queries so the check pushes down to the warehouse. See examples/presolve_feasibility_gate.py.
Pre-solver audit (a-d): run before problem.solve(...).
(a) Registration. solve_for/satisfy/minimize/maximize register Variable_<id>/Constraint_<id>/Objective_<id> concepts — confirm one per call in inspect.schema(model).concepts.
(b) Binding cardinality. Registration does NOT mean binding: solve_for(..., where=[always_false]) registers a variable with zero bindings and the solver runs on an empty decision set:
for var_concept in variables:
n = len(model.select(model.concept_index[var_concept.name]).to_df())
if n == 0:
raise ValueError(f"{var_concept.name} has 0 bindings") # fix the where= predicate
(c) Coefficient presence. Objective/constraint coefficient Properties must be populated by model.define(...) — an unbound coefficient has no tuples, the join produces no row, and the term silently vanishes (OPTIMAL with a vacuous objective). Check len(model.select(coef_prop).to_df()) > 0 per coefficient.
(d) Constraint binding cardinality. A per-grouping body that references a bound empty for some entities produces no row for them — those entities are silently unconstrained. Capture the satisfy() ref and compare cardinalities:
cap_constr = problem.satisfy(model.require(usage <= Entity.cap), name=["cap", Entity.id])
if len(model.select(cap_constr).to_df()) != len(model.select(Entity).to_df()):
raise AssertionError("cap_constr short — Entity.cap unpopulated for some entities")
(a)-(d) are the downstream complement of Step 1: Step 1 verifies formulation inputs exist; Step 5 verifies the outputs registered, bound, weighted, and grounded. Full pattern (drill-in with display(ref, limit=10), per-failure-mode lookup): diagnostic-workflow.md and pre-solve-validation.md. Generation/compile failures before any solve: the failure taxonomy at rai-prescriptive-results/references/failure-taxonomy.md (generates/compiles levels); prescriptive-context compile errors (entity ref as scalar, zero entities, type mismatch): compilation-errors.md.
Step 6: Solve and refine (iterate)
Select the solver (section above), solve, and triage. Solve execution mechanics — parameters, diagnostics requests, run-to-completion discipline — live in rai-prescriptive-results; this step is the formulation-side debugging loop, using the refs captured at write time:
si = problem.solve_info(); si.display() first; branch by status: INFEASIBLE → walk constraints; OPTIMAL with suspicious values → unbound coefficients or vacuous forcing constraints; right shape → problem.verify(*original_fragments) if tolerance-sensitive.
model.select(var_ref.name, var_ref.lower, var_ref.upper).to_df() — bounds and tuples per instance; catches where= over-/under-scoping.
problem.display(constr_ref) — grounded sums per row; catches .per() mis-scoping (the silent-OPTIMAL trap), contradictions, and bodies that didn't ground (Step 5(d)'s runtime view).
problem.display(obj_ref) — expanded objective; catches unbound coefficients. for c in problem.constraints: problem.display(c) when one of many is the offender.
- Sampling very large constraints (
limit=N, where= filter): formulation-display.md > Targeted Inspection.
- When proposing a fix, ground it in the root-cause taxonomy: fix-generation-guidelines.md.
Simplify once correct: static parameters over dynamic calculations; objective terms for goals, constraints for hard requirements; group-level over pairwise. formulation-simplification.md.
Step 7: Present, React, Refine
Steps 1-6 build the best formulation from what the user told you; Step 7 discovers what they couldn't articulate until they saw a result. Solve, present (highlighting large shifts from status quo, domain-norm anchors, trade-off costs, boundary solutions — presentation format: rai-prescriptive-results > Result Explanation Guide), ask reaction questions, disambiguate rejections into constraint types, add, re-solve — until acceptance or feasibility pressure forces explicit prioritization. Most real formulations need at least one pass. If the result is technically wrong (infeasible, all-zero, solver error), route to rai-prescriptive-results for diagnosis — it routes back here when the issue is a missing constraint. The full elicitation playbook (diagnostic questions, disambiguation tables, reaction questions, stop conditions, documentation trail): constraint-elicitation.md.
Formulation Principles
- Context-aware and specific: formulations use the model's actual entities, never generic templates.
- Rationale-driven and goal-aligned: explain WHY each element fits THIS problem; every element supports the user's stated goals.
- Valid variations welcome: there is no single right answer — grounded, feasible alternatives are encouraged.
- All decision variables must be used: a variable appearing in no constraint and no objective is almost always a bug.
Business language framing: describe variables/constraints/objectives in business terms first ("don't exceed warehouse capacity"), then the technical form. Every user-facing description/rationale/business_mapping field reads like a business analyst wrote it (sum(X.quantity).per(Entity) → "total X quantity per entity"); code fields stay valid PyRel.
Constraint vs objective decision rule: if violating it makes the solution invalid → constraint; if it's a preference → objective term. Ambiguous phrases ("keep costs under X", "try to balance") must be clarified — when unclear, default to soft and ask: "if violating this saved 20% on cost, acceptable?" Full disambiguation tables: constraint-elicitation.md.
Business-to-formulation mapping derives from the ontology: concepts/properties/relationships tell you what can be controlled (variables), what has limits (constraints), what the user wants (objective).
Multi-Concept Coordination
When suggesting MULTIPLE cross-product/junction decision concepts, coordinate them:
- Flow networks (production → transport → fulfillment): conservation constraints — inflow = outflow at transshipment nodes, inventory balance at storage. Note in rationale which base entity needs the balance constraint.
- Selection + quantity (binary use/don't-use + continuous quantity): linking constraint
quantity <= capacity * selection.
- Shared base entities (multiple decision concepts touching the same Site): balance/conservation at that entity; state "links to [OtherConcept] via [SharedBase]" in the rationale.
Without linking constraints, multiple decision concepts produce trivial (independently-optimized zeros), unbounded, or inconsistent solutions. Not always wrong — the user may intend unlinked variables — so flag it to verify, don't auto-correct.
Common Pitfalls
| Mistake |
Cause |
Fix |
| All-zero solution on minimize |
Missing forcing constraints |
Add sum(x).per(Entity) >= Entity.demand or equivalent |
| Objective-only variable lands exactly on a bound |
Variable appears in the objective but no constraint — solver pushes it to its bound |
Connect it to a constraint, or drop it from the formulation |
| Infeasible after adding constraints |
Conflicting bounds or over-specified assignments |
Organize constraints into essential/full tiers; add incrementally |
| Variables created but unused |
solve_for registered but objective references different properties |
Verify the objective includes all decision-variable properties |
| Constraint has no decision variable |
problem.satisfy(model.require(Operation.cost >= 0.01)) is a data assertion |
Constraints must reference solve_for-registered properties |
| Wrong aggregation scope |
.per(Y) but Y not joined to the summed concept |
Add explicit relationship join in .where() |
| Big-M too loose → slow solve |
Arbitrary 999999 instead of data-driven bound |
M = capacity or M = max_demand from entity properties |
| Missing forcing requirement |
MINIMIZE with no forcing constraint yields zero |
Identify what real-world requirement forces positive activity |
| Forcing constraint added when the objective already penalizes inaction |
>= 1 forcing alongside a cost-penalty objective over-constrains — OPTIMAL-with-tradeoff becomes INFEASIBLE |
Only add forcing the problem statement requires; check whether the objective already penalizes zero activity |
| Constraint references unwired relationship |
Relationship declared but no define() binding — joins to zero rows, constraint produces no rows, OPTIMAL with vacuous objective |
Verify all relationships in .where() joins have define() rules. Detail: constraint-formulation.md > Unwired Relationships |
Per-entity constraint applies to fewer entities than .per(Entity) suggests |
Sparse bound (Entity.bound empty for some entities) → no row for them; those entities silently unconstrained |
Step 5(d) cardinality check; populate the missing values, coalesce (Entity.bound | 0.0), or join via a fully-populated relationship; localize with problem.display(constr_ref, limit=10) |
Inline cast, subtype filter, or multi-hop join inside satisfy(...)/minimize(...) crashes |
Constraint scope is less expressive than query .where() — floats.float(...), subtype applications, multi-hop joins fail with AttributeError: ... '_short_name' |
Pre-derive casts as Float properties via model.define(); resolve subtype/join membership to id sets and scope with Entity.id.in_([...]) |
problem.satisfy() / model.define() in a Python loop, or hardcoded per-period constraints |
Iterating a modeled dimension in Python or hand-writing N near-identical satisfy() calls — both bypass declarative quantification |
One constraint quantifying over the dimension: Concept + ref() adjacency, Integer ref via range(), or .per(Entity). See examples/multi_period_flow_conservation.py, scenario-analysis.md |
Duplicate relationship / FDError on re-solve |
Multi-scenario solving with populate=True writes conflicting results |
populate=False + Variable.values(); fresh Problem per iteration. known-limitations.md > Re-Solve Behavior |
| Infeasible but not caught before solve |
Feasibility arithmetic unvalidated (50 entities, 4 periods × 5/period = 20 slots) |
Verify entity_count / periods / capacity fits before formulating |
| Linear objective over continuous variables collapses to one entity |
LP pushes to the boundary — max-coefficient entity absorbs all budget |
Per-entity upper cap, concave objective (sqrt, log), or piecewise-linear saturation |
solve_for(where=expr) errors on __bool__ guard |
where is iterated as a tuple; a bare expression triggers Python truthiness |
Wrap in a list: where=[Concept.prop >= threshold] |
problem.satisfy(<expr>) raises TypeError expecting a Fragment |
Bare comparison passed instead of model.require(...) |
Wrap with model.require(<expr>) / model.where(...).require(...) |
solve_for(concept_ref.property) raises TypeError about Chain start |
First arg requires bare Concept.property; refs are valid only inside where=[...] |
Pass bare Concept.property; reserve refs for where= joins |
minimize()/maximize() raises ValueError: no decision variables |
Objective references a detached model.Property(...) handle instead of a Concept attribute |
Attach first (Opt.x = model.Property(...)), then solve_for(Opt.x, ...) and reference Opt.x in the objective |
Decision-variable equality in outer where(...) prunes the search |
model.where(x == k).require(...) filters states the solver never sees (multi-arity-property invocation in where is a binder and is correct) |
Put decision-var comparisons inside count(X, x == k) or inside require(x == k); keep binders in where. See csp-formulation.md § 3a, rai-pyrel/references/expression-rules.md |
Integer-ID decision + implies cascade over a sparse Ref table |
Solver picks an ID absent from the table; the cascade doesn't fire; aux variable silently free (and verify() won't catch it) |
Membership IC (count(Ref, Ref.id == Decision.id).per(Decision) == 1) + pre-solve lookup-totality check. csp-formulation.md §§ 2c, 3d |
Problem(model, Float) + solver="minizinc" server error |
MiniZinc requires Problem(model, Integer); Float problems can't lower to it |
Problem(model, Integer) for MiniZinc; continuous data/decisions → MIP-style + HiGHS/Gurobi/Ipopt. Pairing rules: pre-solve-validation.md > CSP-style cross-check |
Problem(model, Integer) + solver="highs" Int128 extraction error |
Integer problems register Int128 result tuples HiGHS can't decode |
Pair Integer problems with MiniZinc (or Gurobi); on HiGHS use Problem(model, Float) + type="int" hints |
| Cannot remove a constraint |
Every satisfy() accumulates |
New Problem for a different constraint set (see Known Limitations) |
| Binary variable has no effect |
Defined but not linked to quantities |
Add flow <= capacity * x_open linking constraints |
| Numerical issues |
Coefficients differing by >1e6 |
Rescale data to similar magnitudes; categories and Big-M/indicator techniques: numerical-and-mip.md |
Silent verify() limitations on solver-only-IC bodies and post-solve misreads are rai-prescriptive-results Common Pitfalls.
Known Limitations
Problem is additive — solve_for/satisfy/minimize accumulate (PyRel append-only; see rai-pyrel > Definitions). No remove/replace: to weaken or drop an element, build a new Problem. Re-calling problem.solve() on the same Problem is safe for adding constraints and re-solving. Only one minimize()/maximize() per Problem (HiGHS rejects multiples); for competing objectives, scalarize and sweep — with sensitivity=True on LP/QP the epsilon-bound's shadow_price is the exact frontier slope, enabling adaptive sampling: multi-objective-formulation.md.
Multi-component objectives combine per-entity terms across concept groups via model.union() (set-union semantics — distinct from | ordered fallback) with an outer sum():
problem.minimize(sum(model.union(
ResourceGroup.holding_cost * sum(x_inv).per(ResourceGroup).where(...),
Arc.transport_cost * Arc.x_flow,
Factory.unit_cost * Factory.x_production)))
Parametric-variable pitfalls (name=[] scalar resolution, cross-concept join attribute collisions), constraint naming, | 0 fallback limitation, numpy casting: known-limitations.md. CSP-style gotchas (MiniZinc pairing, empty-aggregate silent-drop, unexposed globals): known-limitations.md § CSP-style and csp-formulation.md.
Examples
For all example problems and the patterns they demonstrate, see examples-index.md. When reviewing an existing formulation, see formulation-analysis-context.md.
Reference files
| Reference |
Description |
File |
| Variable formulation |
Types, bounds, scope, entity creation, slack, parametric/time-indexed |
variable-formulation.md |
| Constraint formulation |
Forcing, capacity, balance, linking, keyed_by, temporal recurrence, unwired relationships |
constraint-formulation.md |
| Objective formulation |
Direction, multi-component, penalty terms |
objective-formulation.md |
| Constraint elicitation |
Pre-solve diagnostic questions, business-phrase disambiguation, post-solve refinement loop, reaction questions, stop conditions |
constraint-elicitation.md |
| Problem patterns & validation |
Assignment, flow, knapsack patterns; validation checklist |
problem-patterns-and-validation.md |
| Global constraints |
all_different, implies, SOS1/SOS2, per-solver coverage, CP-vs-MIP decision flow |
global-constraints.md |
| CSP-style formulation |
When it fits, decision shapes, constraint idioms, multi-solution, audit/witness |
csp-formulation.md |
| Scenario analysis |
Scenario Concept vs Loop + where=, decision matrix |
scenario-analysis.md |
| Formulation simplification |
Static vs dynamic parameters, goals vs constraints, over-specification |
formulation-simplification.md |
| Multi-objective formulation |
Dual-guided scalarization, shadow-price = frontier slope, sampling policies |
multi-objective-formulation.md |
| Diagnostic workflow |
Capture-ref pattern, targeted display(ref), solve_info triage, INFEASIBLE walk |
diagnostic-workflow.md |
| Pre-solve validation |
Entity/constraint population checks, data integrity queries, checklist |
pre-solve-validation.md |
| Formulation display |
problem.display() output structure, targeted inspection |
formulation-display.md |
| Compilation errors |
Entity-reference errors, type mismatch, zero entities |
compilation-errors.md |
| Numerical stability & MIP |
Stability categories, Big-M, indicator constraints, reformulation techniques, operator compatibility by solver |
numerical-and-mip.md |
| Solver details |
Problem-size guidelines, Problem initialization patterns |
solver-details.md |
| Fix generation guidelines |
Root-cause taxonomy, grounding rules, join-path fixes |
fix-generation-guidelines.md |
| Examples index |
All example problems with patterns demonstrated |
examples-index.md |
| Formulation analysis context |
Naming conventions, expression parsing for review |
formulation-analysis-context.md |
| Known limitations |
Constraint naming, re-solve behavior, fallback limitation, numpy casting, CSP-style gotchas |
known-limitations.md |
1---2name: rai-prescriptive-problem3description: Formulates optimization and constraint-satisfaction problems from ontology models — decision variables, constraints, objectives, problem-type classification, solver selection, global constraints, and pre-solve validation. Use when building, reviewing, debugging, or relaxing a formulation, through the point where a validated Problem and chosen solver are ready to run. Not for executing the solve or interpreting output — status, extraction, sensitivity, conflicts (see rai-prescriptive-results).4---56# Prescriptive Problem7<!-- v1-SENSITIVE -->89> **Requires `relationalai>=1.11.0`.** The dual-guided multi-objective methods here read solver sensitivity via `solve("highs", sensitivity=True)`; earlier versions reject the request. See `rai-setup`.1011## Summary1213**What:** Everything before the solve runs — decision variables, constraints, objectives, problem-type classification, solver selection, and pre-solve validation. Assumes a problem has already been selected via discovery.1415**When to use:**16- Formulating variables, constraints, and objectives for a selected problem; reviewing or validating an existing formulation17- Translating business requirements into mathematical formulation (and eliciting the constraints users can't articulate upfront)18- Debugging formulations that would produce trivial or infeasible solutions (missing constraints, conflicting bounds, wrong aggregation scope), or relaxing an over-constrained formulation to resolve a reported conflict / IIS19- Classifying the problem type (LP / MILP / QP / QCP / NLP / CSP) and choosing a solver20- Designing multi-concept coordination (flow networks, selection + quantity)2122**When NOT to use:**23- Executing the solve, reading status/duals/IIS, quality assessment, or explaining results — see `rai-prescriptive-results`24- Question discovery (what can this ontology answer) — see `rai-discovery`25- PyRel syntax — see `rai-pyrel`; ontology modeling or enrichment — see `rai-ontology`2627**Overview:**281. Ground in the base ontology via `inspect.schema(model)`292. Define decision variables (type, bounds, scope, naming)303. Define constraints (forcing, capacity, balance, linking)314. Define objective(s) (direction, coefficients, multi-component)325. Validate the complete formulation — including the pre-solver audit that variables/constraints/objectives registered, bound, and grounded336. Classify the problem type, select the solver, solve and refine (targeted `display(ref)` diagnosis)347. Post-solve refinement — present results, surface reactions, iterate on the formulation3536---3738## Quick Reference — Problem API3940```python41from relationalai.semantics.reasoners.prescriptive import Problem42from relationalai.semantics.std import aggregates as aggs4344# Problem(model, Float) for MIP-style; Problem(model, Integer) for CSP-style (see references/csp-formulation.md).45problem = Problem(model, Float)4647# Decision variable — prefer scoped form (where=[...]); capture the ref for targeted diagnostics.48x_flow = problem.solve_for(Lane.flow, where=[Lane.active], name=["item_id", "src_id"], lower=0.0, type="cont")4950# Constraint — capture the ref; name=[Entity.id] makes display rows identifiable.51cap = problem.satisfy(52 model.require(aggs.sum(Lane.flow).per(Source).where(Lane.from_source(Source)) <= Source.capacity),53 name=["cap", Source.id])5455cost = problem.minimize(aggs.sum(Lane.flow * Lane.unit_cost))56```5758| Method | Signature | Purpose |59|--------|-----------|---------|60| `solve_for` | `(expr, where=, populate=True, name=, type=, lower=, upper=, start=)` | Declare decision variable. Returns `ProblemVariable` (a Concept). Row inspection: `model.select(var.name, var.lower, var.upper).to_df()`. `type`: `"cont"`, `"int"`, `"bin"`. `name=` entries may be strings, scalar entity properties, or (relationalai>=1.13) enum members; on 1.12.0 pass `Member.name` |61| `satisfy` | `(expr, name=, keyed_by=)` | Add constraint. Returns `ProblemConstraint`. `keyed_by={"key": Concept}` declares the family's grounding keys as entity back-pointers for post-solve readback — see [constraint-formulation.md](references/constraint-formulation.md) > Declaring constraint keys |62| `minimize` / `maximize` | `(expr, name=)` | Set the (single) objective. Returns `ProblemObjective` |63| `solve` | `(solver, time_limit_sec=, ...)` | Execute — parameters, diagnostics requests, and everything after are `rai-prescriptive-results` |64| `verify` | `(*fragments)` | Post-solve constraint check — pass the original `model.require(...)` values (see `rai-prescriptive-results`) |65| `display` | `(part=None, *, where=None, limit=None)` | Print materialized formulation; `display(ref)` for one constraint/objective; `limit=N` caps rows. Variable subconcepts raise — query via `model.select(...)` |66| `num_variables` / `num_constraints` / `num_min_objectives` / `num_max_objectives` | `()` | Engine-queryable counts; usable in `model.require(...)` before solve |67| `problem.variables` / `.constraints` / `.objectives` | attributes | Registered refs in declaration order — iterate to walk an unfamiliar Problem |6869---7071## Problem Type Classification and Solver Selection7273Classify from the formulation itself — variable types, then nonlinearity, then constraint structure:7475| Type | Signature |76|------|-----------|77| **LP** | All variables continuous; objective and constraints linear |78| **MILP** | Some integer/binary variables; everything linear (products of integer variables make it nonlinear). Keep Big-M tight |79| **QP** | Quadratic terms in the *objective* only (e.g. covariance risk); constraints linear. Check convexity for a global optimum |80| **QCP** | Quadratic terms in *constraints* (norm bounds); more restrictive solver support |81| **NLP** | Nonlinear functions (`exp`, `log`, `sqrt`); local optima possible; integer + NLP (MINLP) is very hard |82| **CSP** | No meaningful objective — find any feasible solution; discrete combinatorial constraints; benefits from global constraints |8384### Support matrix and profiles8586| Problem type | Gurobi | HiGHS | Ipopt | MiniZinc |87|---|---|---|---|---|88| LP / MILP | YES / YES | YES / YES | YES / NO | NO / NO |89| QP / QCP | YES / YES | YES (convex obj) / NO | YES / YES | NO / NO |90| NLP | YES | NO | YES | NO |91| Constraint programming | NO | NO | NO | YES |92| Discrete vars / continuous vars | YES / YES | YES / YES | NO / YES | YES / NO |9394- **Gurobi** (commercial, via RAI): best on every type it supports — always prefer it when the user has a license. Needs a named prescriptive engine with a Snowflake secret + external access integration in `raiconfig.yaml` (see `rai-setup`); fall back to HiGHS/Ipopt without one.95- **HiGHS** (open-source): LP, MILP, convex QP objectives. No indicator constraints (`implies` fails — use Big-M), no SOS, no quadratic constraints, no nonlinear functions.96- **MiniZinc** (open-source, Chuffed backend): CP/CSP. Requires `Problem(model, Integer)` — any Float decision or data coerces to MIP and MiniZinc rejects it. `solution_limit=K` enumerates multiple solutions (`SOLUTION_LIMIT` status = stopped at K; `OPTIMAL` = search exhausted). Only `all_different` and `implies` are exposed from PyRel today. Native logs need `log_to_console=True`.97- **Ipopt** (open-source): smooth continuous NLP, local optima only; no integer/binary variables (will FAIL).9899**Decision rules, in order:** (1) any integer/binary variable → Ipopt invalid; Gurobi preferred, HiGHS without license; MiniZinc only for pure discrete constraint satisfaction. (2) nonlinearity (`math.exp/log/sqrt`, `x**n`) → HiGHS and MiniZinc invalid; continuous → Gurobi or Ipopt; discrete + nonlinear → Gurobi only. Trig and division between two decision variables are not lowered to any solver — reformulate piecewise-linear. (3) quadratic constraints → HiGHS invalid. (4) no objective → MiniZinc for discrete CSP; otherwise Gurobi/HiGHS feasibility solve.100101| Your problem has... | Gurobi available | No Gurobi license |102|---|---|---|103| Binary/integer + linear (MILP) | **Gurobi** | HiGHS |104| Binary/integer + quadratic (MIQP) | **Gurobi** | (no open-source alternative) |105| Continuous + linear/quadratic-objective | **Gurobi** | HiGHS |106| Continuous + quadratic constraints (QCP) | **Gurobi** | Ipopt |107| Continuous + nonlinear (NLP) | **Gurobi** or Ipopt | Ipopt |108| Discrete constraint satisfaction / multiple solutions | MiniZinc | MiniZinc |109110Problem-size guidelines and `Problem` initialization patterns: [solver-details.md](references/solver-details.md).111112### Global constraints113114`all_different`, `implies`, SOS1/SOS2 give the solver combinatorial structure, with solver requirements: `all_different` → MiniZinc (else O(n²) pairwise inequalities); `implies` → Gurobi or MiniZinc (else Big-M); SOS1/SOS2 → Gurobi only (else binary reformulations). Syntax, examples, CP-vs-MIP decision guide: [global-constraints.md](references/global-constraints.md).115116---117118## Formulation Workflow119120**Interaction mode:** ask the user first — **Guided** (confirm variables, constraints, objective at each step; framing involves judgment about hard vs soft and scoping) or **One-shot** (best single pass, review after).121122### Step 1: Ground in the base ontology123124Before writing `solve_for` / `satisfy` / `minimize` / `maximize`, confirm every name and type against the real schema:125126```python127from relationalai.semantics import inspect128schema = inspect.schema(model)129for name in referenced_concepts:130 assert name in schema, f"Concept {name} not in model"131```132133Catches the two silent failure modes behind most prescriptive errors: **hallucinated surface** (`Customer.tier` when the real property is `Customer.category` — the solver happily runs on wrong variables) and **wrong-type inference** (Integer treated as Float, masking incorrect bound derivation).134135**Reuse upstream derived surface.** Subtypes and properties earlier steps wrote back (classification subtypes, centrality scores, flags) are in `inspect.schema(model)` — reference them directly rather than re-deriving. See [examples/chained_rules_prescriptive.py](examples/chained_rules_prescriptive.py).136137**Confirm modeled dimensions.** If the problem spans periods/scenarios/categories, that dimension must exist — as a Concept (`Week`, `Scenario`), an `Integer` ref scoped via `std.common.range()`, or a `model.Enum` (relationalai>=1.12; enum-indexed `solve_for` creates one binary per (entity, member) — [csp-formulation.md](references/csp-formulation.md) § Enum-indexed decision). **Never** iterate the dimension in Python or hand-enumerate its values. See `rai-ontology` if enrichment is needed.138139**When to skip:** small greenfield models or one-shot formulations you just wrote yourself.140141### Step 2: Define Variables142143What can the solver control? Identify the primary decision entity, then auxiliary/aggregation variables; choose types (cont/int/bin) and bounds from data; for minimize objectives include a slack/unmet variable to avoid infeasibility. Full treatment (context integration, entity creation, parametric/time-indexed, slack): [variable-formulation.md](references/variable-formulation.md).144145### Step 3: Define Constraints146147What must the solution satisfy? Add **forcing constraints first** (they prevent trivial zero solutions on minimize), then capacity/resource from data properties, then flow conservation for network structure. Derive parameters from data ranges. For multi-period recurrence (inventory balance, sequencing), use declarative quantification — `Concept.ref()` adjacency joins (`w_prev.num == w.num - 1`) or `where=[t == std.common.range(start, stop)]` — one `satisfy()` grounds all periods: [constraint-formulation.md](references/constraint-formulation.md) > Temporal recurrence, [examples/multi_period_flow_conservation.py](examples/multi_period_flow_conservation.py). Canonical constraint shapes per problem family: [problem-patterns-and-validation.md](references/problem-patterns-and-validation.md).148149### Step 4: Define Objective(s)150151Map the stated goal to minimize/maximize ([objective-formulation.md](references/objective-formulation.md)). Check trivial-solution risk: "if all variables = 0, are all constraints satisfied?" — if yes, Step 3 needs a forcing constraint. **Competing objectives?** A penalty bundling two concerns, or a constraint representing a competing goal, may mean the user wants the tradeoff explored — [multi-objective-formulation.md](references/multi-objective-formulation.md) (epsilon constraint). **Parameter what-if?** Parameterize with the Scenario Concept pattern — one solve over all scenarios, results stay in the ontology: [scenario-analysis.md](references/scenario-analysis.md).152153### Step 5: Validate154155Structural checks: every variable in ≥1 constraint (a variable appearing *only* in the objective gets pushed straight to a bound — usually a missing constraint, not a modeling choice); every constraint references ≥1 decision variable; `.where()` join paths connect to actual data; bounds consistent (lower ≤ upper); exactly one objective for an optimization solve, zero for pure feasibility/CSP.156157- **Trivial-solution gate (before presenting or solving):** substitute every decision variable with its objective-preferred bound (typically zero for minimize) and evaluate against the *actual input data*. If every constraint still holds, the solver will return that trivial point as optimal — the formulation is wrong. The non-obvious failure: a forcing constraint that exists but is vacuous against current data (its `where=` matches no rows). Do not present a formulation that fails this gate; if the outcome depends on input data, state that with row-count evidence.158- **Data-driven feasibility precheck:** when bounds derive from data, verify a feasible point exists — at the **constraint's natural disaggregation level** (per-entity for per-entity constraints; a global `Σ supply ≥ Σ demand` can mask per-slice infeasibility). Run as PyRel aggregation queries so the check pushes down to the warehouse. See [examples/presolve_feasibility_gate.py](examples/presolve_feasibility_gate.py).159160**Pre-solver audit (a-d):** run before `problem.solve(...)`.161162**(a) Registration.** `solve_for`/`satisfy`/`minimize`/`maximize` register `Variable_<id>`/`Constraint_<id>`/`Objective_<id>` concepts — confirm one per call in `inspect.schema(model).concepts`.163164**(b) Binding cardinality.** Registration does NOT mean binding: `solve_for(..., where=[always_false])` registers a variable with zero bindings and the solver runs on an empty decision set:165166```python167for var_concept in variables:168 n = len(model.select(model.concept_index[var_concept.name]).to_df())169 if n == 0:170 raise ValueError(f"{var_concept.name} has 0 bindings") # fix the where= predicate171```172173**(c) Coefficient presence.** Objective/constraint coefficient Properties must be populated by `model.define(...)` — an unbound coefficient has no tuples, the join produces no row, and the term silently vanishes (OPTIMAL with a vacuous objective). Check `len(model.select(coef_prop).to_df()) > 0` per coefficient.174175**(d) Constraint binding cardinality.** A per-grouping body that references a bound empty for some entities produces no row for them — those entities are silently unconstrained. Capture the `satisfy()` ref and compare cardinalities:176177```python178cap_constr = problem.satisfy(model.require(usage <= Entity.cap), name=["cap", Entity.id])179if len(model.select(cap_constr).to_df()) != len(model.select(Entity).to_df()):180 raise AssertionError("cap_constr short — Entity.cap unpopulated for some entities")181```182183(a)-(d) are the downstream complement of Step 1: Step 1 verifies formulation *inputs* exist; Step 5 verifies the *outputs* registered, bound, weighted, and grounded. Full pattern (drill-in with `display(ref, limit=10)`, per-failure-mode lookup): [diagnostic-workflow.md](references/diagnostic-workflow.md) and [pre-solve-validation.md](references/pre-solve-validation.md). Generation/compile failures before any solve: the failure taxonomy at `rai-prescriptive-results/references/failure-taxonomy.md` (`generates`/`compiles` levels); prescriptive-context compile errors (entity ref as scalar, zero entities, type mismatch): [compilation-errors.md](references/compilation-errors.md).184185### Step 6: Solve and refine (iterate)186187Select the solver (section above), solve, and triage. Solve execution mechanics — parameters, diagnostics requests, run-to-completion discipline — live in `rai-prescriptive-results`; this step is the *formulation-side* debugging loop, using the refs captured at write time:188189- `si = problem.solve_info(); si.display()` first; branch by status: `INFEASIBLE` → walk constraints; `OPTIMAL` with suspicious values → unbound coefficients or vacuous forcing constraints; right shape → `problem.verify(*original_fragments)` if tolerance-sensitive.190- `model.select(var_ref.name, var_ref.lower, var_ref.upper).to_df()` — bounds and tuples per instance; catches `where=` over-/under-scoping.191- `problem.display(constr_ref)` — grounded sums per row; catches `.per()` mis-scoping (the silent-OPTIMAL trap), contradictions, and bodies that didn't ground (Step 5(d)'s runtime view).192- `problem.display(obj_ref)` — expanded objective; catches unbound coefficients. `for c in problem.constraints: problem.display(c)` when one of many is the offender.193- Sampling very large constraints (`limit=N`, `where=` filter): [formulation-display.md](references/formulation-display.md) > Targeted Inspection.194- When proposing a fix, ground it in the root-cause taxonomy: [fix-generation-guidelines.md](references/fix-generation-guidelines.md).195196**Simplify once correct:** static parameters over dynamic calculations; objective terms for goals, constraints for hard requirements; group-level over pairwise. [formulation-simplification.md](references/formulation-simplification.md).197198### Step 7: Present, React, Refine199200Steps 1-6 build the best formulation from what the user *told* you; Step 7 discovers what they couldn't articulate until they saw a result. Solve, present (highlighting large shifts from status quo, domain-norm anchors, trade-off costs, boundary solutions — presentation format: `rai-prescriptive-results` > Result Explanation Guide), ask reaction questions, disambiguate rejections into constraint types, add, re-solve — until acceptance or feasibility pressure forces explicit prioritization. Most real formulations need at least one pass. If the result is *technically* wrong (infeasible, all-zero, solver error), route to `rai-prescriptive-results` for diagnosis — it routes back here when the issue is a missing constraint. The full elicitation playbook (diagnostic questions, disambiguation tables, reaction questions, stop conditions, documentation trail): [constraint-elicitation.md](references/constraint-elicitation.md).201202---203204## Formulation Principles2052061. **Context-aware and specific:** formulations use the model's actual entities, never generic templates.2072. **Rationale-driven and goal-aligned:** explain WHY each element fits THIS problem; every element supports the user's stated goals.2083. **Valid variations welcome:** there is no single right answer — grounded, feasible alternatives are encouraged.2094. **All decision variables must be used:** a variable appearing in no constraint and no objective is almost always a bug.210211**Business language framing:** describe variables/constraints/objectives in business terms first ("don't exceed warehouse capacity"), then the technical form. Every user-facing `description`/`rationale`/`business_mapping` field reads like a business analyst wrote it (`sum(X.quantity).per(Entity)` → "total X quantity per entity"); code fields stay valid PyRel.212213**Constraint vs objective decision rule:** if violating it makes the solution invalid → constraint; if it's a preference → objective term. Ambiguous phrases ("keep costs under X", "try to balance") must be clarified — when unclear, default to soft and ask: "if violating this saved 20% on cost, acceptable?" Full disambiguation tables: [constraint-elicitation.md](references/constraint-elicitation.md).214215**Business-to-formulation mapping** derives from the ontology: concepts/properties/relationships tell you what can be controlled (variables), what has limits (constraints), what the user wants (objective).216217---218219## Multi-Concept Coordination220221When suggesting MULTIPLE cross-product/junction decision concepts, coordinate them:2222231. **Flow networks** (production → transport → fulfillment): conservation constraints — inflow = outflow at transshipment nodes, inventory balance at storage. Note in rationale which base entity needs the balance constraint.2242. **Selection + quantity** (binary use/don't-use + continuous quantity): linking constraint `quantity <= capacity * selection`.2253. **Shared base entities** (multiple decision concepts touching the same Site): balance/conservation at that entity; state "links to [OtherConcept] via [SharedBase]" in the rationale.226227Without linking constraints, multiple decision concepts produce trivial (independently-optimized zeros), unbounded, or inconsistent solutions. Not always wrong — the user may intend unlinked variables — so flag it to verify, don't auto-correct.228229---230231## Common Pitfalls232233| Mistake | Cause | Fix |234|---------|-------|-----|235| All-zero solution on minimize | Missing forcing constraints | Add `sum(x).per(Entity) >= Entity.demand` or equivalent |236| Objective-only variable lands exactly on a bound | Variable appears in the objective but no constraint — solver pushes it to its bound | Connect it to a constraint, or drop it from the formulation |237| Infeasible after adding constraints | Conflicting bounds or over-specified assignments | Organize constraints into essential/full tiers; add incrementally |238| Variables created but unused | `solve_for` registered but objective references different properties | Verify the objective includes all decision-variable properties |239| Constraint has no decision variable | `problem.satisfy(model.require(Operation.cost >= 0.01))` is a data assertion | Constraints must reference `solve_for`-registered properties |240| Wrong aggregation scope | `.per(Y)` but Y not joined to the summed concept | Add explicit relationship join in `.where()` |241| Big-M too loose → slow solve | Arbitrary `999999` instead of data-driven bound | `M = capacity` or `M = max_demand` from entity properties |242| Missing forcing requirement | MINIMIZE with no forcing constraint yields zero | Identify what real-world requirement forces positive activity |243| Forcing constraint added when the objective already penalizes inaction | `>= 1` forcing alongside a cost-penalty objective over-constrains — OPTIMAL-with-tradeoff becomes INFEASIBLE | Only add forcing the problem statement requires; check whether the objective already penalizes zero activity |244| Constraint references unwired relationship | Relationship declared but no `define()` binding — joins to zero rows, constraint produces no rows, OPTIMAL with vacuous objective | Verify all relationships in `.where()` joins have `define()` rules. Detail: [constraint-formulation.md](references/constraint-formulation.md) > Unwired Relationships |245| Per-entity constraint applies to fewer entities than `.per(Entity)` suggests | Sparse bound (`Entity.bound` empty for some entities) → no row for them; those entities silently unconstrained | Step 5(d) cardinality check; populate the missing values, coalesce (`Entity.bound \| 0.0`), or join via a fully-populated relationship; localize with `problem.display(constr_ref, limit=10)` |246| Inline cast, subtype filter, or multi-hop join inside `satisfy(...)`/`minimize(...)` crashes | Constraint scope is less expressive than query `.where()` — `floats.float(...)`, subtype applications, multi-hop joins fail with `AttributeError: ... '_short_name'` | Pre-derive casts as Float properties via `model.define()`; resolve subtype/join membership to id sets and scope with `Entity.id.in_([...])` |247| `problem.satisfy()` / `model.define()` in a Python loop, or hardcoded per-period constraints | Iterating a modeled dimension in Python or hand-writing N near-identical `satisfy()` calls — both bypass declarative quantification | One constraint quantifying over the dimension: Concept + `ref()` adjacency, `Integer` ref via `range()`, or `.per(Entity)`. See [examples/multi_period_flow_conservation.py](examples/multi_period_flow_conservation.py), [scenario-analysis.md](references/scenario-analysis.md) |248| `Duplicate relationship` / `FDError` on re-solve | Multi-scenario solving with `populate=True` writes conflicting results | `populate=False` + `Variable.values()`; fresh `Problem` per iteration. [known-limitations.md](references/known-limitations.md) > Re-Solve Behavior |249| Infeasible but not caught before solve | Feasibility arithmetic unvalidated (50 entities, 4 periods × 5/period = 20 slots) | Verify `entity_count / periods / capacity` fits before formulating |250| Linear objective over continuous variables collapses to one entity | LP pushes to the boundary — max-coefficient entity absorbs all budget | Per-entity upper cap, concave objective (`sqrt`, `log`), or piecewise-linear saturation |251| `solve_for(where=expr)` errors on `__bool__` guard | `where` is iterated as a tuple; a bare expression triggers Python truthiness | Wrap in a list: `where=[Concept.prop >= threshold]` |252| `problem.satisfy(<expr>)` raises `TypeError` expecting a `Fragment` | Bare comparison passed instead of `model.require(...)` | Wrap with `model.require(<expr>)` / `model.where(...).require(...)` |253| `solve_for(concept_ref.property)` raises `TypeError` about Chain start | First arg requires bare `Concept.property`; refs are valid only inside `where=[...]` | Pass bare `Concept.property`; reserve refs for `where=` joins |254| `minimize()`/`maximize()` raises `ValueError: no decision variables` | Objective references a detached `model.Property(...)` handle instead of a Concept attribute | Attach first (`Opt.x = model.Property(...)`), then `solve_for(Opt.x, ...)` and reference `Opt.x` in the objective |255| Decision-variable **equality** in outer `where(...)` prunes the search | `model.where(x == k).require(...)` filters states the solver never sees (multi-arity-property invocation in `where` is a *binder* and is correct) | Put decision-var comparisons inside `count(X, x == k)` or inside `require(x == k)`; keep binders in `where`. See [csp-formulation.md](references/csp-formulation.md) § 3a, `rai-pyrel/references/expression-rules.md` |256| Integer-ID decision + `implies` cascade over a sparse Ref table | Solver picks an ID absent from the table; the cascade doesn't fire; aux variable silently free (and `verify()` won't catch it) | Membership IC (`count(Ref, Ref.id == Decision.id).per(Decision) == 1`) + pre-solve lookup-totality check. [csp-formulation.md](references/csp-formulation.md) §§ 2c, 3d |257| `Problem(model, Float)` + `solver="minizinc"` server error | MiniZinc requires `Problem(model, Integer)`; Float problems can't lower to it | `Problem(model, Integer)` for MiniZinc; continuous data/decisions → MIP-style + HiGHS/Gurobi/Ipopt. Pairing rules: [pre-solve-validation.md](references/pre-solve-validation.md) > CSP-style cross-check |258| `Problem(model, Integer)` + `solver="highs"` Int128 extraction error | Integer problems register Int128 result tuples HiGHS can't decode | Pair Integer problems with MiniZinc (or Gurobi); on HiGHS use `Problem(model, Float)` + `type="int"` hints |259| Cannot remove a constraint | Every `satisfy()` accumulates | New `Problem` for a different constraint set (see Known Limitations) |260| Binary variable has no effect | Defined but not linked to quantities | Add `flow <= capacity * x_open` linking constraints |261| Numerical issues | Coefficients differing by >1e6 | Rescale data to similar magnitudes; categories and Big-M/indicator techniques: [numerical-and-mip.md](references/numerical-and-mip.md) |262263Silent `verify()` limitations on solver-only-IC bodies and post-solve misreads are `rai-prescriptive-results` Common Pitfalls.264265---266267## Known Limitations268269**Problem is additive** — `solve_for`/`satisfy`/`minimize` accumulate (PyRel append-only; see `rai-pyrel` > Definitions). No remove/replace: to weaken or drop an element, build a **new Problem**. Re-calling `problem.solve()` on the same Problem is safe for *adding* constraints and re-solving. Only one `minimize()`/`maximize()` per Problem (HiGHS rejects multiples); for competing objectives, scalarize and sweep — with `sensitivity=True` on LP/QP the epsilon-bound's `shadow_price` is the exact frontier slope, enabling adaptive sampling: [multi-objective-formulation.md](references/multi-objective-formulation.md).270271**Multi-component objectives** combine per-entity terms across concept groups via `model.union()` (set-union semantics — distinct from `|` ordered fallback) with an outer `sum()`:272273```python274problem.minimize(sum(model.union(275 ResourceGroup.holding_cost * sum(x_inv).per(ResourceGroup).where(...),276 Arc.transport_cost * Arc.x_flow,277 Factory.unit_cost * Factory.x_production)))278```279280Parametric-variable pitfalls (`name=[]` scalar resolution, cross-concept join attribute collisions), constraint naming, `| 0` fallback limitation, numpy casting: [known-limitations.md](references/known-limitations.md). CSP-style gotchas (MiniZinc pairing, empty-aggregate silent-drop, unexposed globals): [known-limitations.md](references/known-limitations.md) § CSP-style and [csp-formulation.md](references/csp-formulation.md).281282---283284## Examples285286For all example problems and the patterns they demonstrate, see [examples-index.md](references/examples-index.md). When reviewing an existing formulation, see [formulation-analysis-context.md](references/formulation-analysis-context.md).287288---289290## Reference files291292| Reference | Description | File |293|-----------|-------------|------|294| Variable formulation | Types, bounds, scope, entity creation, slack, parametric/time-indexed | [variable-formulation.md](references/variable-formulation.md) |295| Constraint formulation | Forcing, capacity, balance, linking, `keyed_by`, temporal recurrence, unwired relationships | [constraint-formulation.md](references/constraint-formulation.md) |296| Objective formulation | Direction, multi-component, penalty terms | [objective-formulation.md](references/objective-formulation.md) |297| Constraint elicitation | Pre-solve diagnostic questions, business-phrase disambiguation, post-solve refinement loop, reaction questions, stop conditions | [constraint-elicitation.md](references/constraint-elicitation.md) |298| Problem patterns & validation | Assignment, flow, knapsack patterns; validation checklist | [problem-patterns-and-validation.md](references/problem-patterns-and-validation.md) |299| Global constraints | `all_different`, `implies`, SOS1/SOS2, per-solver coverage, CP-vs-MIP decision flow | [global-constraints.md](references/global-constraints.md) |300| CSP-style formulation | When it fits, decision shapes, constraint idioms, multi-solution, audit/witness | [csp-formulation.md](references/csp-formulation.md) |301| Scenario analysis | Scenario Concept vs Loop + `where=`, decision matrix | [scenario-analysis.md](references/scenario-analysis.md) |302| Formulation simplification | Static vs dynamic parameters, goals vs constraints, over-specification | [formulation-simplification.md](references/formulation-simplification.md) |303| Multi-objective formulation | Dual-guided scalarization, shadow-price = frontier slope, sampling policies | [multi-objective-formulation.md](references/multi-objective-formulation.md) |304| Diagnostic workflow | Capture-ref pattern, targeted `display(ref)`, `solve_info` triage, INFEASIBLE walk | [diagnostic-workflow.md](references/diagnostic-workflow.md) |305| Pre-solve validation | Entity/constraint population checks, data integrity queries, checklist | [pre-solve-validation.md](references/pre-solve-validation.md) |306| Formulation display | `problem.display()` output structure, targeted inspection | [formulation-display.md](references/formulation-display.md) |307| Compilation errors | Entity-reference errors, type mismatch, zero entities | [compilation-errors.md](references/compilation-errors.md) |308| Numerical stability & MIP | Stability categories, Big-M, indicator constraints, reformulation techniques, operator compatibility by solver | [numerical-and-mip.md](references/numerical-and-mip.md) |309| Solver details | Problem-size guidelines, `Problem` initialization patterns | [solver-details.md](references/solver-details.md) |310| Fix generation guidelines | Root-cause taxonomy, grounding rules, join-path fixes | [fix-generation-guidelines.md](references/fix-generation-guidelines.md) |311| Examples index | All example problems with patterns demonstrated | [examples-index.md](references/examples-index.md) |312| Formulation analysis context | Naming conventions, expression parsing for review | [formulation-analysis-context.md](references/formulation-analysis-context.md) |313| Known limitations | Constraint naming, re-solve behavior, fallback limitation, numpy casting, CSP-style gotchas | [known-limitations.md](references/known-limitations.md) |