plugins/rai/skills/rai-prescriptive-solver-management/SKILL.md
Covers solver lifecycle including problem type classification, solver selection and creation, global constraints, pre-solve validation, solve execution (including requesting sensitivity and conflict / IIS diagnostics), and solver-level diagnostics. Use when configuring or running optimization solvers, not for interpreting post-solve results (see rai-prescriptive-results-interpretation).
npx skillsauth add RelationalAI/rai-agent-skills rai-prescriptive-solver-managementInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Requires
relationalai>=1.11.0. Requesting sensitivity and conflict / IIS diagnostics onsolve()requires the dual-bearing solver; earlier versions rejectsensitivity=Trueas an unsupported solver attribute. Seerai-setup.
What: Solver lifecycle — selection, creation, formulation inspection, execution, and diagnostics.
When to use:
When NOT to use:
rai-prescriptive-results-interpretation.rai-prescriptive-problem-formulationrai-pyrel-codingOverview:
from relationalai.semantics import Float, Integer, Model, sum
from relationalai.semantics.reasoners.prescriptive import Problem
# 1. Create Problem (Float for LP/MILP/NLP, Integer for CP)
problem = Problem(model, Float)
# 2. Register variables (type: "cont", "int", "bin"; bounds; naming)
problem.solve_for(Route.x_flow, type="cont", lower=0, upper=Route.capacity,
name=["flow", Route.origin, Route.dest])
problem.solve_for(Facility.x_open, type="bin", name=["open", Facility.id])
# 3. Add constraints (model.require inside problem.satisfy)
problem.satisfy(model.require(sum(Route.x_flow).per(Customer) >= Customer.demand))
problem.satisfy(model.require(Route.x_flow <= Route.capacity * Facility.x_open))
# 4. Set objective (exactly one minimize or maximize)
problem.minimize(sum(Route.cost * Route.x_flow))
# 5. Pre-solve check — always inspect before solving
problem.display()
model.require(problem.num_variables() > 0)
model.require(problem.num_constraints() > 0)
# 6. Solve — solver choice depends on problem type and user license
# See "Solver Selection" section for decision rules
problem.solve(solver_name, time_limit_sec=120)
model.require(problem.termination_status() == "OPTIMAL")
problem.solve_info().display()
# Solvers: "highs" (LP/MILP, open-source), "gurobi" (LP/MILP/QP/QCP, license required),
# "minizinc" (CP, open-source), "ipopt" (NLP, open-source)
# Check: problem.termination_status() → "OPTIMAL" | "INFEASIBLE" | "DUAL_INFEASIBLE" | "TIME_LIMIT"
problem.solve() returns None -- do NOT assign its return value. Access solve results through separate methods:
problem.solve_info().display() prints a human-readable solve summary.problem.solve_info().termination_status (str), problem.solve_info().objective_value (float).model.select() — solved values are written back into model properties.Variable.values(sol_index, value_ref) on the ProblemVariable returned by solve_for().# CORRECT usage
problem.solve("highs", time_limit_sec=60)
si = problem.solve_info()
si.display() # Print status summary
print(si.termination_status) # "OPTIMAL", "INFEASIBLE", etc.
# Always check termination_status before reading objective_value or extracting
# results — si.objective_value is None on infeasible / unbounded solves and will
# silently render as "None" in print() / math / comparisons otherwise.
if si.termination_status in ("OPTIMAL", "LOCALLY_SOLVED"):
print(si.objective_value) # Objective function value
# For populate=False workflows, use Variable.values() (gate the extraction on
# termination_status for the same reason):
assign_var = problem.solve_for(Assignment.x, type="bin", populate=False)
problem.solve("highs")
si = problem.solve_info()
if si.termination_status in ("OPTIMAL", "LOCALLY_SOLVED"):
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()
Warning:
result = problem.solve(...); result.statusfails becausesolve()returnsNoneregardless of solver. Accessing any attribute onNoneraisesAttributeError. Always callproblem.solve()on its own line, then useproblem.solve_info()separately.
Before classifying or configuring, read the existing formulation (built in rai-prescriptive-problem-formulation) to extract solver-relevant characteristics:
Identify the problem type before choosing a solver:
Linear Programming (LP): All variables continuous, objective and constraints all linear. No products of variables, no nonlinear functions.
Mixed-Integer Linear Programming (MILP): Some variables integer or binary, but objective and constraints remain linear. No products of integer variables (that makes it nonlinear). Keep Big-M tight.
Quadratic Programming (QP): Quadratic terms in objective only (e.g., risk minimization with covariance). Constraints remain linear. Check convexity (Q matrix positive semi-definite) for global optimum.
Quadratically Constrained Programming (QCP): Quadratic terms in constraints (e.g., norm constraints). More restrictive solver requirements than QP. Check if constraints are convex.
Nonlinear Programming (NLP): Nonlinear functions (exp, log, sqrt, sin, cos). Integer + NLP is very difficult (MINLP). May have local optima -- solution may not be global.
Constraint Satisfaction Problem (CSP): No meaningful objective function. Goal is finding any feasible solution. Often discrete variables with combinatorial constraints. Benefits from global constraints.
Choose the solver based on variable types and objective/constraint structure:
| Problem Type | Gurobi | HiGHS | Ipopt | MiniZinc | |---|---|---|---|---| | Linear Programs (LP) | YES | YES | YES | NO | | Mixed-Integer Linear (MILP) | YES | YES | NO | NO | | Quadratic Programs (QP) | YES (convex obj only) | YES | YES | NO | | Quadratically Constrained (QCP) | YES | NO | YES | NO | | Nonlinear Programs (NLP) | YES | NO | YES | NO | | Constraint Programming (CP) | NO | NO | NO | YES | | Discrete Variables (int/bin) | YES | YES | NO | YES | | Continuous Variables | YES | YES | YES | NO |
HiGHS (solver="highs"): Open-source. Best for LP, MILP, convex QP objectives. Fast simplex/IPM LP solver, good MILP for moderate problems. Params: time_limit, mip_rel_gap, presolve ("choose"/"on"/"off"), threads.
HiGHS limitations (specific):
implies() will fail. Use Big-M reformulation instead.special_ordered_set_type_1() / special_ordered_set_type_2() will fail. Use explicit binary variable formulations instead.math.exp, math.log, math.sqrt, x**n) — use Gurobi (preferred when licensed) or Ipopt.Gurobi (solver="gurobi"): Commercial (available via RAI). Best for large-scale MILP, QP, QCP, and nonlinear problems. Industry-leading MILP performance, discrete + continuous + quadratic + nonlinear (math.exp, math.log, math.sqrt, x ** n), excellent diagnostics, multi-objective support. Params: TimeLimit, MIPGap, MIPFocus (0=balanced, 1=feasibility, 2=optimality, 3=bound), Presolve (2 for aggressive), Threads (0 for auto). License required: Gurobi requires a named prescriptive engine with a Snowflake secret and external access integration configured in raiconfig.yaml. See rai-setup for setup. If unavailable, fall back to HiGHS (LP/MILP) or Ipopt (NLP). Large MIP problems may solve significantly faster with Gurobi than HiGHS.
MiniZinc (solver="minizinc"): Open-source (Chuffed backend). Best for CP, combinatorial, constraint satisfaction. Powerful propagation, native support for all_different and implies, multiple solutions via solution_limit. Params: time_limit_sec, solution_limit. Cannot handle continuous variables, LP, QP, NLP, or SOS constraints. MiniZinc's native global-constraint library is far richer (circuit, cumulative, element, ...) but only all_different and implies are exposed from PyRel to MiniZinc today; SOS1/SOS2 are PyRel-exposed but Gurobi-only.
MiniZinc-specific requirements and behavior:
Problem(model, Integer) — a single Float decision variable or Float data coerces the problem to MIP and MiniZinc will reject it.solution_limit=K enables multi-solution enumeration. Termination status SOLUTION_LIMIT (not OPTIMAL) when search stopped at K with more feasible remaining; OPTIMAL means search exhausted with ≤ K found.log_to_console=True to stream logs.For the full CSP-style formulation guide, see csp-formulation.md.
Ipopt (solver="ipopt"): Open-source. Best for continuous nonlinear optimization. Interior-point for smooth NLP, handles nonlinear objectives AND constraints. Finds local optima only. Params: max_iter, max_wall_time, tol (e.g. 1e-8), print_level, mu_strategy. Cannot handle integer or binary variables -- will FAIL.
Use these rules in order to pick a solver. Gurobi outperforms open-source solvers (HiGHS, Ipopt) on every problem type it supports — faster solve times, tighter MIP gaps, better scaling. Always prefer Gurobi when the user has a license. Only recommend open-source when Gurobi is unavailable or the problem type requires it (CSP → MiniZinc, smooth NLP → Ipopt).
Check variable types first.
Check for nonlinearity.
math.exp(x), math.log(x), math.sqrt(x), or x ** n?
HiGHS and MiniZinc are invalid.math.sin, math.cos, etc.) and division between two decision variables are not lowered to the solver by the prescriptive library — reformulate as piecewise-linear approximations or via auxiliary variables.Check for quadratic constraints.
No objective (constraint satisfaction)?
Quick reference:
| 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 (LP) | Gurobi | HiGHS | | Continuous + quadratic objective (QP) | Gurobi | HiGHS | | Continuous + quadratic constraints (QCP) | Gurobi | Ipopt | | Continuous + nonlinear (NLP) | Gurobi or Ipopt | Ipopt | | Discrete + constraint satisfaction (CSP) | MiniZinc | MiniZinc | | Need multiple solutions | MiniZinc (best) | MiniZinc |
Problem size guidelines (small/medium/large thresholds) and Problem initialization patterns (Problem(model, Float) vs Integer, solver string names) are in solver-details.md. Always select solver based on problem type and confirm license availability.
Global constraints (all_different, implies, SOS1, SOS2) provide solver-exploitable combinatorial structure. Each has specific solver requirements:
| Constraint | Requires | Alternatives |
|---|---|---|
| all_different | MiniZinc | O(n^2) pairwise inequalities in MIP |
| implies | Gurobi or MiniZinc | Big-M reformulation for HiGHS |
| special_ordered_set_type_1 | Gurobi | Binary variables + sum constraints |
| special_ordered_set_type_2 | Gurobi | Binary segment selection variables |
For syntax, code examples, and a CP vs MIP decision guide, see global-constraints.md.
Use problem.display() to inspect variables, objectives, and constraints before solving. Check problem.num_variables(), problem.num_constraints(), and problem.num_min_objectives() / problem.num_max_objectives() against expectations (these are Relationships — use in model.require() or model.select()). Use problem.display(part) for targeted inspection of a single constraint or objective; for variables, query rows via model.select(var_ref.name, var_ref.lower, var_ref.upper).to_df(). Use problem.printed_model() (Relationship, with print_format= on problem.solve()) to get LP/MPS/LaTeX text representations. The same Problem can be re-solved multiple times — constraints accumulate across calls.
See formulation-display.md for display output structure, diagnostic tables, and targeted inspection patterns.
Run five checks before calling problem.solve(): (1) entity population — problem.num_variables() > 0; (2) constraint population — problem.num_constraints() > 0 with at least one forcing constraint; (3) objective — exactly one minimize/maximize for an optimization (or sensitivity=True) solve, zero for a pure feasibility / CSP solve (conflict=True is objective-agnostic: it needs none, but tolerates the objective of an infeasible optimization model you're diagnosing); (4) data integrity — no nulls, no negatives in costs/capacities, total capacity >= total demand; (5) formulation structure via problem.display().
# Minimum pre-solve checklist
problem.display()
model.require(problem.num_variables() > 0)
model.require(problem.num_constraints() > 0)
model.require(problem.num_min_objectives() + problem.num_max_objectives() == 1) # optimization/sensitivity; use == 0 for pure feasibility/CSP (conflict=True works with either)
See pre-solve-validation.md for full checks, diagnostic queries, and data integrity patterns.
For prescriptive-context compile errors (entity reference passed as scalar, zero entities, type mismatch, undefined concept), see compilation-errors.md. General PyRel compile errors live in rai-pyrel-coding/references/common-pitfalls.md. For numerical stability categories and MIP formulation techniques (big-M, indicator constraints), see numerical-and-mip.md.
Status interpretation and prose-level diagnosis live in rai-prescriptive-results-interpretation > Status Interpretation (the natural reading order is status → diagnose → fix). The structured diagnostic codes that map status to fix-action types — unbounded_variable, missing_upper_bound, penalty_structure, constraint_conflict, capacity_mismatch — live here in diagnostic-taxonomy.md, since they're a solver-side classification used to drive automated fix routing.
For an infeasible model, solve(conflict=True) is the structured first-line localizer — one solve returns the minimal conflicting subset (IIS) of constraints and variable bounds, replacing manual bisection (the constraint_conflict code maps to its in_conflict / *_in_conflict membership). Read the membership and act on conflict_status per rai-prescriptive-results-interpretation > Sensitivity & conflict attributes (and its conflict-analysis.md reference).
For si.error and print_format= semantics, see Solve Execution and the solve-info table below in this skill. For solver-log patterns and numerical-error categorization, see numerical-and-mip.md.
# Solver choice depends on problem type and license — see Decision Rules above
problem.solve(solver_name, time_limit_sec=60)
# Post-solve: engine-side status check + Python-side summary
model.require(problem.termination_status() == "OPTIMAL")
problem.solve_info().display()
Termination status: problem.termination_status() (Relationship) returns a status string. Common values: "OPTIMAL", "INFEASIBLE", "DUAL_INFEASIBLE" (unbounded), "LOCALLY_SOLVED" (NLP), "TIME_LIMIT", "SOLUTION_LIMIT". Use in model.require() for engine-side checks. For Python-side access: si = problem.solve_info() then si.termination_status.
Debugging failed solves: After a non-optimal solve, check problem.error() (Relationship) or si.error (Python tuple) for the solver-level error message:
problem.solve("highs", time_limit_sec=60)
si = problem.solve_info()
if si.termination_status != "OPTIMAL":
print(si.error) # Solver-level error details
Checking solver version: Use problem.solve_info().solver_version after any solve to see the exact version that ran. Do not hardcode version numbers — they change with solver service updates.
Post-solve constraint verification: problem.verify(*fragments) checks that the solver's solution satisfies constraints — see rai-prescriptive-results-interpretation > Post-solve constraint verification.
For verifying what the solver actually sees before solving, see formulation-display.md.
problem.solve(...) blocks until the solver returns. Run the solve in the foreground and read terminal status + objective + variable values in the same run before composing any answer. A formulation summary, a "solver is still running" status, or a backgrounded solve whose output you never collected is not a result — never present one as the answer. If a solve is slow, wait for it (raise time_limit_sec deliberately) rather than reporting the plan as if it were solved.
Each problem.satisfy(...) accumulates, so build a fresh Problem per scenario / sweep point. Loop, and extract each point's results before moving on so a later failure can't erase earlier rows. Before re-solving a point, check whether the active constraint set actually changed — if the registered variables and bounds match a point you already solved, reuse that result instead of paying for an identical solve. Derive per-point inputs (e.g., which entities fall in each tier at a given threshold) once, up front, rather than re-deriving inside the loop.
These parameters are solver-independent — every solver accepts them. The two diagnostic flags are the caveat: any solver accepts sensitivity / conflict, but their output depends on model class and solver capability (see their rows — unsupported cases degrade to empty results, a warning, or NOT_SUPPORTED, not an error at the call):
| Parameter | Type | Description |
|-----------|------|-------------|
| time_limit_sec | float | Maximum solve time in seconds (Python kwarg defaults to None; if not provided, the solver service applies its own default — currently 300s) |
| silent | bool | Suppress solver output |
| solution_limit | int | Maximum number of solutions to find |
| relative_gap_tolerance | float | Relative optimality gap in [0, 1] (e.g., 0.01 = 1%) |
| absolute_gap_tolerance | float | Absolute optimality gap (>= 0) |
| log_to_console | bool | Stream solver logs to the console during solve |
| print_only | bool | Print the solver model without actually solving |
| print_format | str | Request text representation: "moi", "latex", "mof", "lp", "mps", "nl" |
| sensitivity | bool | Populate post-solve duals (reduced_cost, shadow_price, basis_status). LP/QP only; requires an objective (ValueError without one); not with solution_limit or print_only |
| conflict | bool | Populate a conflict / IIS diagnosis for an infeasible model (con.in_conflict, var.*_in_conflict, solve_info().conflict_status). No objective required; not with print_only |
# Solver-independent options (portable across all solvers)
problem.solve("highs", time_limit_sec=300, silent=True)
problem.solve("highs", relative_gap_tolerance=0.01) # 1% MIP gap
problem.solve("minizinc", solution_limit=10) # Multiple solutions
# Debugging: get LP format of the solver model
problem.solve("highs", print_format="lp", print_only=True)
print(problem.solve_info().printed_model) # Access the text representation
sensitivity and conflict preconditions. sensitivity=True returns the optimal solution and its duals in one solve — it needs an objective (an objectiveless solve(sensitivity=True) raises a ValueError before submitting: duals are marginals of the objective; add minimize()/maximize() or, for infeasibility diagnostics, use conflict=True), is LP/QP only (a MIP returns empty duals and fires a warning; interior-point solvers like Ipopt populate marginals but no basis_status), and is incompatible with solution_limit and print_only. conflict=True needs no objective (it works on pure-feasibility models and on MIP) and is incompatible with print_only. Request either on the solve whose results you'll read — they're solve options, not post-processing steps. They also pin the Problem to the diagnostic result schema: plain and diagnostic solves can't mix on one Problem, and a later re-solve may add a diagnostic family but never drop a requested one — either raises a ValueError before submitting; rebuild a fresh Problem to change regime.
# Duals (LP/QP, needs an objective):
problem.solve("highs", sensitivity=True)
# Conflict / IIS for an infeasible model (no objective required):
problem.solve("highs", conflict=True)
Read the populated attributes per rai-prescriptive-results-interpretation > Sensitivity & conflict attributes; which solvers yield duals / basis / IIS is in solver-parameters.md.
Solver-specific parameters (HiGHS, Gurobi, Ipopt kwargs), tuning guidance, cloud pipeline details, re-solve behavior, warm starting, and scenario analysis patterns are in solver-parameters.md.
The following operators are not lowered to the solver and will raise errors if used inside solve_for/satisfy/minimize/maximize: % (modulo), // (floor division), math.floor, math.ceil, math.round, math.sign, math.clip, trig (math.sin, math.cos, math.tan and their hyperbolic/inverse variants), math.factorial, math.erf. Note: // works on concrete data and property-constant combinations (e.g., Player.p // group_size), but fails when both operands are decision variables. There is no if_then_else operator in the prescriptive library — use implies() (Gurobi/MiniZinc) or Big-M reformulation (HiGHS) for conditional logic. Use piecewise-linear approximations or reformulations for unsupported operators.
See also: Full operator/construct compatibility table by solver →
numerical-and-mip.md> Operator and Construct Compatibility by Solver. Reformulation techniques (Big-M linearization, McCormick envelopes, epigraph, SOS2) →numerical-and-mip.md> Reformulation Techniques for Solver Compatibility.
| Mistake | Cause | Fix |
|---------|-------|-----|
| Reported a solve that hadn't finished | Backgrounded a long solve, or printed the formulation, and ended the turn on an "in progress" status | Run the solve to completion in the foreground; read terminal status and extract values before composing the answer (see Run to completion before reporting) |
| Constraint has no decision variable | problem.satisfy(model.require(Operation.cost >= 0.01)) is a data assertion | Constraints must reference solve_for-registered properties |
| Cannot remove a constraint | Every problem.satisfy() call accumulates | Create a new Problem for different constraint sets |
| Binary variable has no effect | Defined but not linked to quantities via big-M or capacity | Add flow <= capacity * x_open style linking constraints |
| Disconnected objective | Objective references variables with no constraints | Solver sets variables to bound values; add meaningful constraints |
| Numerical issues | Coefficients differing by >1e6 | Rescale data to similar magnitudes |
| problem.termination_status == "OPTIMAL" is always False | Missing parens — problem.termination_status returns a bound method, not a string | Use problem.termination_status() (with parens) in model.require(), or problem.solve_info().termination_status for Python-side |
| AttributeError on problem.solve() return value | Assigning result = problem.solve() and accessing result.status | problem.solve() returns None. Use problem.solve_info() for status. For solution values, use model.select() (populate=True) or Variable.values() (populate=False). |
| Problem(model, Float) + solver="minizinc" returns a server error | MiniZinc requires Problem(model, Integer). A Float problem cannot be lowered to the MiniZinc backend | Use Problem(model, Integer) for MiniZinc. If the problem has continuous data or decisions, choose MIP-style (Problem(model, Float) + HiGHS / Gurobi / Ipopt). See Problem(model, Float) vs Problem(model, Integer) pairing rules in pre-solve-validation.md > CSP-style cross-check. |
| Problem(model, Integer) + solver="highs" returns an Int128 result-extraction error | The Integer problem registers Int128 result tuples that the HiGHS extraction path cannot decode | Pair Problem(model, Integer) with solver="minizinc" (or solver="gurobi"). If staying on HiGHS, use Problem(model, Float) and rely on integer type hints in solve_for(..., type="int"). |
For formulation-time pitfalls (wrong aggregation scope, loose Big-M, missing forcing requirement, unwired relationships, etc.), see rai-prescriptive-problem-formulation > Common Pitfalls.
Post-solve diagnosis (trivial all-zero solutions, infeasibility root causes, quality assessment) is covered in rai-prescriptive-results-interpretation. The unified lifecycle failure taxonomy (generates → compiles → solves → optimal → non-trivial → meaningful) lives at rai-prescriptive-results-interpretation/references/failure-taxonomy.md — consult it for the solves and optimal levels.
| Pattern | Description | File |
|---|---|---|
| Scenario Concept (parameter sweep) | Scenario as data concept, single solve, multi-arg variables, model.select() results | examples/scenario_concept_parameter_sweep.py |
| Scenario Concept (multi-binary MILP) | Two binary variable types indexed by Scenario, .per(Entity, Scenario) grouping, cross-variable budget | examples/scenario_concept_milp.py |
| Entity exclusion (disruption) | Loop + where=[] with != filter to exclude entities, populate=False, Variable.values() results | examples/entity_exclusion_disruption.py |
| Partitioned sub-problems (loop) | Loop + where=[] filter per partition, populate=False, Variable.values() results | examples/partitioned_iteration_scenarios.py |
| Scenario Concept (CSP) | CSP-style analog of scenario_concept_milp.py — Scenario as data concept indexing integer decisions; single MiniZinc solve over all scenarios | examples/scenario_concept_csp.py |
| Reference | Description | File |
|-----------|-------------|------|
| Numerical stability & MIP | Numerical stability categories, big-M, indicator constraints | numerical-and-mip.md |
| Formulation display | problem.display() output structure and how to read it | formulation-display.md |
| Pre-solve validation | Entity population checks, data integrity queries, copy-paste checklist | pre-solve-validation.md |
| Scenario analysis | Scenario Concept vs Loop + where= patterns, decision matrix, code examples | scenario-analysis.md |
| Solver details | Problem size guidelines, Problem initialization patterns | solver-details.md |
| Compilation errors | Entity reference errors, type mismatch, zero entities, fix taxonomy | compilation-errors.md |
| Diagnostic taxonomy | Root cause codes, fix action types, status-specific fix direction | diagnostic-taxonomy.md |
| Solver parameters | Solver-specific kwargs, tuning, cloud pipeline, warm starting, scenario analysis | solver-parameters.md |
| CSP-style formulation | When this style fits, decision-variable shapes, constraint idioms, multi-solution mode semantics, audit / witness verdict mapping, verify() three-regime guidance for solver-only-IC bodies | csp-formulation.md |
data-ai
Configure and train graph neural network (GNN) models, generate predictions, evaluate results, and manage trained models. Use when ready to train, generate predictions, evaluate, or manage models; for concepts, data loading, edges, and feature configuration, see `rai-predictive-modeling`.
development
Build graph neural network (GNN) models — concepts, Snowflake data loading, task relationships, graph edges, and PropertyTransformer features. Use for node classification, regression, and link prediction tasks; for training, predictions, and evaluation, see `rai-predictive-training`.
development
Setup and configuration for RelationalAI — first-time install walkthrough and all raiconfig.yaml tuning. Use when installing RAI, connecting to Snowflake, or editing raiconfig.yaml. Not for writing PyRel model code (see rai-pyrel-coding) or solver usage and diagnostics (see rai-prescriptive-solver-management).
testing
Converts natural language business rules into PyRel derived properties — validation, classification, derivation, alerting, and reconciliation. Use whenever a task assigns each entity a new tier, segment, score, or flag, or derives a new property; author it here as a derived property, then query it with rai-querying.