Skip to content

Problem

relationalai.semantics.reasoners.prescriptive.problem
Problem(model: b.Model, numeric_type: b.Concept, *, name: str | None = None)

Define and solve a decision problem on a model.

Use Problem.solve_for to declare decision variables, Problem.minimize/ Problem.maximize to add objectives, and Problem.satisfy to add constraints. Then call Problem.solve for eager execution, or Problem.configure to register a declarative request for model deployment. Read eager results via populated properties (the default — e.g. model.select(X.v)), the Variable.values Property for engine-side queries (model.select(sol_idx, val).where(var.values(sol_idx, val))), or Problem.solve_info for a Python-side metadata snapshot. Pass solve(sensitivity=True) for post-solve duals (reduced costs, shadow prices, basis statuses) or solve(conflict=True) to diagnose an infeasible model (an irreducible infeasible subsystem); see Problem.solve and the Variable / Constraint accessors below.

Parameters

  • model

    (Model) - The Model to attach solver constructs (variables, objectives, constraints, and results) to.
  • numeric_type

    (Concept) - Numeric type used for bounds, solution values, and numeric literals. Use semantics.frontend.core.Float for HiGHS/Gurobi/Ipopt (even for MIPs), and semantics.frontend.core.Integer for MiniZinc.
  • name

    (str, default: None) - Stable deployment name. Required on every Problem when more than one Prescriptive Problem is configured on the same Model.

Examples

Declare a variable and objective:

from relationalai.semantics import Float, Model
from relationalai.semantics.reasoners.prescriptive import Problem
m = Model("demo")
x = m.Relationship(f"{Float:x}")
problem = Problem(m, Float)
problem.solve_for(x, name="x", lower=0)
problem.minimize(x)

Notes

Calling Problem.solve invalidates the Python-side solve_info() cache. Result accessors like termination_status() return engine-side Relationships that reflect the most recent successful solve.

Attributes

.numeric_type

Problem.numeric_type: Concept

semantics.frontend.core.Float or semantics.frontend.core.Integer.

.variables

Problem.variables: list[ProblemVariable]

Variable components registered via Problem.solve_for.

.objectives

Problem.objectives: list[ProblemObjective]

Objective components registered via Problem.minimize / Problem.maximize.

.constraints

Problem.constraints: list[ProblemConstraint]

Constraint components registered via Problem.satisfy.

.Variable

Problem.Variable: Concept

Engine-side aggregate Concept covering all declared decision variables. Use it to query across all variables of this problem — for example, model.select(p.Variable.name, p.Variable.lower).where(p.Variable) to list every variable’s name and lower bound. After solve(sensitivity=True) / solve(conflict=True) it also carries reduced_cost, basis_status, and the *_in_conflict predicates (see ProblemVariable).

.Objective

Problem.Objective: Concept

Engine-side aggregate Concept covering all objectives. Each ProblemObjective extends this Concept; use it to query names, types, (in future) duals, and — after Problem.install_display_stringsdisplay_string.

.Constraint

Problem.Constraint: Concept

Engine-side aggregate Concept covering all constraints. Each ProblemConstraint extends this Concept; use it to query names and types, after solve(sensitivity=True) / solve(conflict=True)shadow_price, basis_status, and in_conflict (see ProblemConstraint) — and after Problem.install_display_stringsdisplay_string.

Methods

.install_display_strings()

Problem.install_display_strings() -> None

Install the rules that render each expression’s display_string.

Binds the Expression.display_string property to each expression’s rendered text — for example "(x_0 ^ 2.0) + x_0 >= 1.0" — making it queryable directly, as in model.select(cap.name, cap.display_string) for a constraint cap returned by Problem.satisfy. The property is always declared, so querying it is valid without this call, but every display_string is null until the rules are installed.

Call this whenever you query display_string yourself. Only Problem.display installs on demand; a direct select does not, and every row comes back null with no error to say why. Under Deploy Mode call it before deploying, since the rules must exist at deploy time. A problem that neither displays nor installs pays no rendering cost.

Installing is cheap; what follows it is not free. On the SQL backend the first query after an install renders every expression in the problem, even one that selects no display_string, and mutating the model makes the next query render them again. What it costs tracks the number of constraint DECLARATIONS rather than the number of expressions, because installing re-plans every block in the model: twenty-five constraints declared separately cost about nine times what the same twenty-five cost when they are grounded from one declaration. Install once, after the model is fully declared, rather than per query: any later mutation, a new Concept or objective included, re-pays on the next query. With several problems on one model install for all of them before displaying any — installing lazily, one display at a time, costs O(N^2) over N problems for the same reason. A second call is a no-op.

The height of a single expression is the other scaling limit, and it compounds: rendering a chain of binary operators costs about 1.35x per added link, easing to 1.18x by twenty links, where one render takes roughly two minutes. Nothing caps it — an expression deep enough to matter is a slow query rather than an error — so prefer aggregating over data to writing a deep arithmetic chain in one constraint.

Notes:

Rendering is supported on the LQP and SQL backends.

A row with no rendered string has a null display_string, and Problem.display shows it as <unrendered>. That means the rules are not installed, a decision variable in the expression was declared without name= (re-installing does not fix that one — a second call is a no-op), the engine read came back incomplete, or the expression holds an argument the renderer cannot consume (a special ordered set with a duplicate user index, say) — in which case the whole expression fails closed rather than rendering partially. A direct query keeps the row and returns a null; only a where on the property drops it.

Bag arguments (sum terms and the like) print sorted as text rather than numerically, so a member reading x_10 precedes one reading x_2. Bags settle one level per rendering pass, counting only the levels whose members are not themselves plain variables, so three such levels nested inside one another can print the outermost bag’s members out of order; every member is still present and each member’s own text is exact. A float literal at or above 1e6 renders differently on LQP than on the SQL backend, which can move member order with it.

.solve_for()

Problem.solve_for(
expr: b.Relationship | b.Chain | b.Expression,
where: Optional[list[Any]] = None,
populate: bool = True,
name: Optional[Any | list[Any]] = None,
type: Optional[str] = None,
lower: Optional[std.NumberValue] = None,
upper: Optional[std.NumberValue] = None,
start: Optional[std.NumberValue] = None,
) -> "ProblemVariable"

Declare decision variables for the problem.

Call this before adding objectives or constraints. The returned ProblemVariable IS a Concept — use it directly with model.define(), model.select(), and .ref() to annotate and query declared variables.

Parameters:

  • expr

    (Relationship or Chain or Expression) - Expression describing the variable(s) to create (for example, a scalar relationship like x or an indexed property like Item.cost).
  • where

    (list[Any], default: None) - Optional conditions restricting which variable instances are created.
  • populate

    (bool, default: True) - If True (default), write solved values back to the original relationship/property after Problem.solve. Set to False when you create multiple Problem instances that solve for the same relationship on the same model.
  • name

    (Any or list[Any], default: None) - Display name for variables. Use a string for scalars or a list pattern for indexed variables (for example, ["x", Item.i]). The name has to discriminate: Problem.display rejects only null names, so a constant name="x" on a variable that grounds once per entity gives every instance the same name and every constraint the same rendered text, with no warning.
  • type

    (str, default: None) - Variable type: "cont" (default for Float), "int" (default for Integer), or "bin" (binary 0/1).
  • lower

    (Variable or float or int or Decimal, default: None) - Lower/upper bounds and an optional initial value hint.
  • upper

    (Variable or float or int or Decimal, default: None) - Lower/upper bounds and an optional initial value hint.
  • start

    (Variable or float or int or Decimal, default: None) - Lower/upper bounds and an optional initial value hint.

Returns:

  • ProblemVariable - The variable subconcept. Annotate via model.define(var.lower(0)), query values after solve via model.select(sol_idx, val).where(var.values(sol_idx, val)).

    Back-pointer fields. The subconcept exposes one property per non-value field of the underlying relationship, named after the field’s own name (the explicit :name from the format string, or the lowercased type name if no explicit name was given). Use these to query “which domain entity does this variable represent?”. Examples for the common shapes:

    # Entity property: f"{Queen} is in {Integer:column}"
    # → var.queen (back to the Queen instance)
    var = p.solve_for(Queen.column, ...)
    model.select(sol_idx, var.queen.row, val).where(var.values(sol_idx, val))
    # Explicit field name: f"{Edge:e} has {Float:flow}"
    # → var.e (uses the explicit name, not "edge")
    var = p.solve_for(Edge.flow, ...)
    model.select(sol_idx, var.e.id, val).where(var.values(sol_idx, val))
    # Multi-arity entity property:
    # f"{Player} in {Integer:week} is in {Integer:group}"
    # → var.player + var.week
    var = p.solve_for(Player.assign(w, x), ...)
    model.select(sol_idx, var.player.p, var.week, val).where(var.values(sol_idx, val))
    # Bare multi-arity (no entity):
    # f"cell {Integer:i} {Integer:j} is {Integer:x}"
    # → var.i + var.j
    var = p.solve_for(cell(i, j, x), ...)
    model.select(sol_idx, var.i, var.j, val).where(var.values(sol_idx, val))
    # Bare scalar relationship: f"{Float:x}" — no back-pointer.
    # Query var.values directly.
    var = p.solve_for(x, name="x", ...)
    model.select(sol_idx, val).where(var.values(sol_idx, val))

Raises:

  • ValueError - If variables are already defined for this relationship, if an argument has an invalid value (for example, an unknown type), or if a non-value field name would shadow an intrinsic attribute on the Variable subconcept. Shadow categories include engine-side Properties (name, type, lower, upper, start, values), Python @property descriptors (dsl_expr, concept_name, property_name, var_type, var_where, populate), and Concept.RESERVED_NAMES methods (ref, new, alias, where, select, define, require, etc.). See Problem._reserved_variable_field_names for the authoritative list — the error message lists the full set. Rename the conflicting field in the relationship’s format string. A ValueError is also raised if the value field’s declared type (Integer / Float) does not match the Problem’s numeric type: a decision variable’s value field must match Problem(model, …); set integrality with type='int' / 'bin', not the property type.
  • TypeError - If an argument has an invalid type.

.minimize()

Problem.minimize(
expr: b.Variable | float | int | b.Fragment, name: Optional[Any | list[Any]] = None
) -> "ProblemObjective"

Add a minimization objective.

The expression must reference at least one decision variable declared via Problem.solve_for.

Parameters:

  • expr

    (Variable or float or int or Fragment) - Objective expression to minimize.
  • name

    (Any or list[Any], default: None) - Optional objective name (string for scalar, or a list pattern for indexed objectives).

Returns:

  • ProblemObjective - The objective subconcept (IS a Concept).

Raises:

  • ValueError - If the objective does not reference any declared decision variables.

Notes:

Under PyRel relational semantics, an objective expression whose body evaluates to an empty relation produces no row, so no objective is registered for that call. The behavior is intentional but can be surprising when the modeler expected the objective to apply. It covers:

  • sum(X.v).where(<filter matching no rows>)
  • sum(select(X.v).where(<empty>)) — filter encapsulated inside the aggregate body
  • sum(model.union(<all-empty branches>))
  • mixed shapes like sum(<empty>) + sum(<populated>) — when any sub-aggregate’s body is empty, the arithmetic produces no row

For per-grouping objectives (...per(X.group).where(...)) the behavior is per-grouping: the objective registers for groupings whose body has rows; groupings whose body is empty produce no row, so no per-grouping objective registers for them.

Aggregates follow multiset (bag) semantics: every grouping cell or row in the aggregate body contributes a term, matching standard optimization modeling. This can differ from a plain relational read-back of the same expression (model.select(expr).to_df()), which follows PyRel’s relational set semantics and collapses rows that project to equal values. For the additive aggregates that reach the solver wire (sum, count) whose contributions can share a value — a per-grouped sub-aggregate with colliding cell values, or an unanchored body with no projection key — the solver objective and a relational read-back may therefore differ; min/max are unaffected (idempotent under duplicates), and avg/string_join over decision variables are rejected at rewrite rather than lowered. The solver objective is authoritative for the optimization.

Problem.num_min_objectives returns the count as a Relationship. To read it as a Python scalar, query the model: n = model.select(p.num_min_objectives()).to_df().iloc[0, 0].

.maximize()

Problem.maximize(
expr: b.Variable | float | int | b.Fragment, name: Optional[Any | list[Any]] = None
) -> "ProblemObjective"

Add a maximization objective.

The expression must reference at least one decision variable declared via Problem.solve_for.

Parameters:

  • expr

    (Variable or float or int or Fragment) - Objective expression to maximize.
  • name

    (Any or list[Any], default: None) - Optional objective name (string for scalar, or a list pattern for indexed objectives).

Returns:

  • ProblemObjective - The objective subconcept (IS a Concept).

Raises:

  • ValueError - If the objective does not reference any declared decision variables.

Notes:

See Problem.minimize Notes — the empty-body relational semantics and the multiset/bag aggregate semantics apply identically here. Problem.num_max_objectives returns the count as a Relationship; query as a scalar via model.select(p.num_max_objectives()).to_df().iloc[0, 0].

.satisfy()

Problem.satisfy(
expr: b.Fragment,
name: Optional[Any | list[Any]] = None,
keyed_by: Optional[dict[str, Any]] = None,
) -> "ProblemConstraint"

Add constraints from a model.require(...) fragment.

Use this to turn a require-clause fragment into solver constraints. The returned ProblemConstraint IS a Concept — annotate via model.define(constr.name("budget")), query via model.select(constr.name).where(constr).

Passing a model.require(...) fragment to satisfy detaches it from the model’s active integrity constraints — the solver enforces it instead, and it no longer fires engine-side. To also check it engine-side against the current solution after Problem.solve, call Problem.verify — it temporarily reinstalls the fragment as an IC, evaluates it, and removes it (one-shot).

.. note:

LP and MIP solvers return floating-point solutions that satisfy
constraints within solver tolerance (e.g. ``1e-8``), but engine
ICs check exact inequality. For continuous-variable constraints,
use a tolerant ``model.require()`` post-solve instead
(e.g. ``model.require(x <= bound + 1e-6)``).

Parameters:

  • expr

    (Fragment) - A fragment created by Model.require (optionally scoped with Model.where).
  • name

    (Any or list[Any], default: None) - Optional constraint name (string for scalar, or a list pattern for indexed constraints).
  • keyed_by

    (dict[str, ref], default: None) - Maps a back-pointer name to the grounding reference each constraint instance is 1:1 with. The reference is an entity ({"shift": Shift}, so con.shift joins to the shift’s data) or a value such as an identifier ({"i": X.i}, so con.i is the scalar index); a bare primitive type is rejected. Each entry becomes an identifying Property on the constraint, so the instance is identified by — and joins to the model by — its key (con.shift.min_coverage), the same way a variable points back to its entity (var.food). The keys must uniquely determine the constraint: a key set that does not (including two conjuncts of a single require(A, B) sharing keys) makes two instances collide on one identity and raises at solve (an engine-side functional-dependency error). name= plays no part in this identity — it is a display label only; a family is read back by key only if keyed_by declares one. Key names must be valid identifiers and must not clash with the managed constraint properties (root, name, type, display_string, shadow_price, basis_status, in_conflict, id). Omit it for constraints whose marginals you do not read back by key.

Returns:

  • ProblemConstraint - The constraint subconcept (IS a Concept).

Raises:

  • TypeError - If expr is not a fragment.
  • ValueError - If the fragment has no require clause, or if it includes select/define clauses; if a keyed_by name is not an identifier, clashes with a managed constraint property, or its reference is neither an entity nor a value reference; or, at solve, if the keyed_by key set is not 1:1 with the constraint instances (an engine-side functional-dependency error).

Notes:

Under PyRel relational semantics, a require clause whose body evaluates to an empty relation produces no row, so no constraint is registered for that call. The behavior is intentional but can be surprising when the modeler expected the constraint to apply. It covers:

  • sum(X.v) <= sum(X.v).where(<empty filter>)
  • filter-encapsulated empties like sum(select(X.v).where(<empty>)) <= 5
  • all-empty-branch unions like sum(model.union(<empty>, <empty>)) <= 5
  • any arithmetic combination where one operand’s body is empty

For per-grouping constraints (...per(X.group).where(...)) the behavior is per-grouping: the constraint registers for groupings whose body has rows; groupings whose body is empty produce no row, so no per-grouping constraint registers for them.

Aggregates in a require clause follow the same multiset/bag semantics as objectives (see Problem.minimize Notes): a relational read-back may differ from the solved value for additive aggregates over a per-grouped body whose cells share a value, and all_different keeps equal-valued members distinct so that pairwise distinctness is required over all of them.

Problem.num_constraints returns the count as a Relationship. To read it as a Python scalar, query the model: n = model.select(p.num_constraints()).to_df().iloc[0, 0].

.verify()

Problem.verify(*fragments: b.Fragment) -> None

One-shot constraint verification against the current solution.

Temporarily installs each fragment as an integrity constraint, triggers a model query to evaluate them, then removes them. A ModelWarning is raised if any constraint is violated.

Emits a UserWarning and returns without checking if the most recent solve did not produce a successful solution (i.e. termination_status is not OPTIMAL, LOCALLY_SOLVED, or SOLUTION_LIMIT).

.. note:

LP and MIP solvers return floating-point solutions that satisfy
constraints within solver tolerance (e.g. ``1e-8``), but engine
ICs check exact inequality. For continuous-variable constraints,
use a tolerant ``model.require()`` post-solve instead
(e.g. ``model.require(x <= bound + 1e-6)``).

.. note:

``verify()`` re-evaluates each constraint with PyRel's relational
(set) semantics, whereas the solver enforces aggregates with
multiset (bag) semantics (see :meth:`minimize` Notes). For an
additive aggregate over a body whose contributions can collapse on
a relational read-back, the relational value can be smaller than
the enforced bag value, so depending on the constraint's sense
``verify()`` may go wrong in *either* direction: it may report a
spurious violation for a solution the solver correctly satisfied
(false-raise, ``>=`` sense), or silently accept a solution the bag
constraint actually violates (false-pass, ``<=``/``==`` sense). For
these shapes a passing ``verify()`` is therefore **not** a
certificate; the solver's enforcement is authoritative and any
``verify()`` result on them is advisory.

Parameters:

  • *fragments

    (Fragment, default: ()) - One or more fragments previously passed to Problem.satisfy.

.num_variables()

Problem.num_variables() -> b.Relationship

Number of declared decision variables. Usable in rules and ICs.

Returns:

  • Relationship - An Integer Relationship counting the declared variables.

.num_constraints()

Problem.num_constraints() -> b.Relationship

Number of declared constraints. Usable in rules and ICs.

Returns:

  • Relationship - An Integer Relationship counting the declared constraints.

.num_min_objectives()

Problem.num_min_objectives() -> b.Relationship

Number of minimization objectives. Usable in rules and ICs.

Returns:

  • Relationship - An Integer Relationship counting the minimization objectives.

.num_max_objectives()

Problem.num_max_objectives() -> b.Relationship

Number of maximization objectives. Usable in rules and ICs.

Returns:

  • Relationship - An Integer Relationship counting the maximization objectives.

.termination_status()

Problem.termination_status() -> b.Relationship

Solver termination status (e.g. "OPTIMAL"). Usable in rules and ICs.

Returns:

  • Relationship - A String Relationship containing the termination status.

.objective_value()

Problem.objective_value() -> b.Relationship

Objective value reported by the solver. Usable in rules and ICs.

Solvers report a single objective per solve (the primary/best solution), not per-point objectives. This Relationship reflects the solver-reported value.

Returns:

  • Relationship - A numeric Relationship containing the objective value.

.solve_time_sec()

Problem.solve_time_sec() -> b.Relationship

Solve time in seconds (Float Relationship). Usable in rules and ICs.

Returns:

  • Relationship - A Float Relationship containing the solve time in seconds.

.num_points()

Problem.num_points() -> b.Relationship

Number of solution points. Usable in rules and ICs.

Returns:

  • Relationship - An Integer Relationship counting the solution points.

.solver_version()

Problem.solver_version() -> b.Relationship

Solver version string. Usable in rules and ICs.

Returns:

  • Relationship - A String Relationship containing the solver version.

.printed_model()

Problem.printed_model() -> b.Relationship

Solver-provided text representation of the problem. Usable in rules and ICs.

Returns:

  • Relationship - A String Relationship containing the printed model text.

.error()

Problem.error() -> b.Relationship

Solver error message(s). Usable in rules and ICs.

A message here is not necessarily a whole-request failure: a successful solve whose secondary step failed (e.g. conflict/IIS extraction, with conflict_status == "FAILED") reports its reason here alongside a valid result. Read a failure’s scope from the accompanying status, not from the mere presence of a message.

Returns:

  • Relationship - A String Relationship containing solver error messages.

.display()

Problem.display(
*removed_positional: Any,
limit: int | None = None,
print_output: bool = True,
**removed: Any
) -> str

Print and return a human-readable summary of the problem.

.. versionchanged:: 1.29.0 part and where were removed. Select display_string instead, as shown below, and filter it with a where. To cap a scoped read, use std.aggregates.limit in a where clause, also shown below; Fragment.limit is not the replacement, since it raises “Feature unavailable” when querying RAI directly. limit survives, but only in its whole-problem form: it caps the printed tables and no longer takes a part. It also samples different rows, since it now takes the first N of the plain-text sort rather than 1.28’s natural-sort top N: over x_1..x_12, limit=5 prints x_1, x_10, x_11, x_12, x_2 where 1.28 printed x_1..x_5.

One other change is visible in the output: a problem with an unnamed decision variable now raises ValueError instead of printing a table that cannot name it. Row ordering also changed; see below.

With no limit, renders a count summary and full tables of variables, objectives, and constraints.

To render a subset, or to get composable data rather than formatted text, query the display_string property directly — it returns a DataFrame that composes with any other select. Install the rules first: this method installs them on its own first use — which makes the next query in the session render every expression, whether or not it selects one — but a direct select does not install, and every row is null until they are. For a constraint you already hold, say cap = problem.satisfy(..., name="cap"):

problem.install_display_strings()
model.select(cap.name, cap.display_string).to_df()

or across every constraint in the problem:

problem.install_display_strings()
c = problem.Constraint
model.select(c.name, c.display_string).to_df()

which a where narrows, in place of the removed where=:

model.select(c.name, c.display_string).where(
c.name == "cap_3"
).to_df()

and std.aggregates.limit caps a scoped read’s row count, which limit no longer does:

model.where(std.aggregates.limit(3, c.name)).select(
c.name, c.display_string
).to_df()

Install once, after the model is fully declared, rather than per query — Problem.install_display_strings has the cost and says what re-pays it. Under Deploy Mode install before deploying, since the rules must exist at deploy time.

Everything printed sorts as plain text: bag arguments (sum, min, max, count, all_different members) inside the model, the variable table and expression rows on the client. So x_10 precedes x_2 throughout. See Problem.install_display_strings for the ordering’s current limits.

An expression the renderer cannot render prints <unrendered> in place of its text. This method installs the rules first, so a missing install is not a cause here; the causes that remain are listed under Problem.install_display_strings.

Parameters:

  • limit

    ((int, keyword - only), default: None) - Cap each printed table at its first limit rows, after the sort described above, so a large problem prints a readable sample. The counts in the summary header stay true, so they still report the whole problem. None, the default, prints every row.
  • print_output

    ((bool, keyword - only), default: True) - If True, print the formatted output to stdout. Set to False to receive the string without printing.

Returns:

  • str - The formatted summary.

Raises:

  • TypeError - If part or where is passed, or any positional argument is. Both were removed in 1.29.0; the message carries the display_string replacement shown above.
  • ValueError - If limit is not a positive integer, or if any decision variable is unnamed. Every variable is identified by name in the output, and an expression over an unnamed variable renders no string at all.

Examples:

:

problem.display()

or, on a large problem, a capped sample:

problem.display(limit=5)

.configure()

Problem.configure(
solver: str,
*,
time_limit_sec: float | None = None,
silent: bool | None = None,
solution_limit: int | None = None,
relative_gap_tolerance: float | None = None,
absolute_gap_tolerance: float | None = None,
log_to_console: bool = False,
print_only: bool = False,
print_format: str | None = None,
sensitivity: bool = False,
conflict: bool = False,
**solver_params: int | float | str | bool
) -> None

Configure this problem for declarative execution in a model deployment.

This method validates and stores the same solver request accepted by Problem.solve. It performs no exports, queries, or service calls. A Problem may be configured once; a Model may contain multiple configured Problems.

.solve()

Problem.solve(
solver: str,
*,
time_limit_sec: float | None = None,
silent: bool | None = None,
solution_limit: int | None = None,
relative_gap_tolerance: float | None = None,
absolute_gap_tolerance: float | None = None,
log_to_console: bool = False,
print_only: bool = False,
print_format: str | None = None,
sensitivity: bool = False,
conflict: bool = False,
**solver_params: int | float | str | bool
) -> None

Solve the decision problem using a solver backend.

Declare decision variables first with Problem.solve_for. After solving, read back solution values via the populated properties (e.g. model.select(X.v)) or the Variable.values Property for engine-side queries, result accessors such as Problem.termination_status and Problem.objective_value, or Problem.solve_info for a Python-side snapshot.

Parameters:

  • solver

    (str) - Solver name (for example "highs", "minizinc", "ipopt").
  • time_limit_sec

    (float, default: None) - Maximum solve time in seconds. The solver service defaults to 300s if not provided.
  • silent

    (bool, default: None) - Whether to suppress solver output.
  • solution_limit

    (int, default: None) - Maximum number of solutions to return (when supported).
  • relative_gap_tolerance

    (float, default: None) - Relative optimality gap tolerance in [0, 1].
  • absolute_gap_tolerance

    (float, default: None) - Absolute optimality gap tolerance (>= 0).
  • log_to_console

    (bool, default: False) - Whether to stream solver logs to stdout while the job runs.
  • print_only

    (bool, default: False) - If True, request a text representation without solving. Results such as Problem.printed_model are still accessible afterward.
  • print_format

    (str, default: None) - Text format for the printed model. Supported formats: "moi" (MOI text), "latex", "mof" (MOI JSON), "lp", "mps", "nl" (AMPL).
  • sensitivity

    (bool, default: False) - Request post-solve sensitivity analysis — reduced costs, shadow prices, and basis statuses — exposed via Variable.reduced_cost, Constraint.shadow_price, and .basis_status. Requires an objective and is incompatible with solution_limit (sensitivity is defined for the single optimal point). Meaningful for continuous (LP/QP) solves; integer/MIP models have no meaningful duals, so the accessors come back empty (a warning is emitted). Coefficient/RHS ranging (allowable increase/decrease) is not provided. Uses wire schema version 2. Read them straight off the returned variable/constraint, the same attribute style as .name/.lower — e.g. model.select(var.name, var.reduced_cost); see ProblemVariable / ProblemConstraint for the query idiom and the dual sign convention (it depends on the objective sense and the constraint direction / active bound — not the objective sense alone).
  • conflict

    (bool, default: False) - For an infeasible model, request a conflict / irreducible infeasible subsystem (IIS) — the constraints and variable bounds that cannot all be satisfied — exposed via Constraint.in_conflict and Var.{lower,upper,integrality}_in_conflict, with the overall outcome in solve_info().conflict_status. Needs no objective (unlike sensitivity), so a pure feasibility problem can be diagnosed too. Uses wire schema version 2. Read membership with the bare predicate, e.g. where(con.in_conflict) (see ProblemConstraint).
  • **solver_params

    (int or float or str or bool, default: {}) - Raw solver-specific parameters passed through to the solver service.

Raises:

  • ValueError - If no decision variables have been declared via Problem.solve_for; if print_only=True is combined with sensitivity or conflict; if sensitivity=True is combined with a satisfaction problem (no objective) or with solution_limit; or if the solver service is too old to support wire schema version 2 (every solve now requires it — upgrade the RelationalAI Native App).
  • TypeError - If any solver-specific parameter value is not an int, float, str, or bool, or if numeric_type is passed via **solver_params (declare it via Problem(model, Float) or Problem(model, Integer) instead).
  • NotImplementedError - If deployments are enabled in the model configuration. Use Problem.configure to declare deployment execution instead.
  • RuntimeError - If this Problem has already been configured for deployment, or if the solver job fails.
  • TimeoutError - If the solver job does not reach a terminal state in time.

Notes:

Passing **solver_params emits a warning because options may not be portable across solvers.

A failed solve leaves prior result data intact. If a solve raises — a rejected request (ValueError), a failed job (RuntimeError), or a timeout (TimeoutError) — the last successful solve’s values, Problem.solve_info, and any sensitivity / conflict data all remain queryable, solve_info() still describes that last successful solve, and the failed attempt’s partial output is never imported. A fresh successful solve replaces the prior results; if the first solve fails before any data loads, a retry simply re-runs it.

Every solve sends wire schema version 2, so one Problem may freely mix plain, sensitivity, and conflict solves. Re-solving with a narrower set of flags clears the dropped family’s earlier rows rather than leaving them stale.

.solve_info()

Problem.solve_info() -> SolveInfoData

Return solver result metadata as a cached SolveInfoData.

Fetches all result metadata in a single query. Eager results are cached until the next Problem.solve. Configured deployment results are queried on every call because a scheduled deployment may refresh them without a local Python method call.

Returns:

  • SolveInfoData - Frozen dataclass with typed fields. Beyond the always-present termination_status / objective_value / solve_time_sec / num_points, it carries the boolean sensitivity / conflict flags echoing what the solve requested, conflict_status (after solve(conflict=True)), primal_status / dual_status (from point_summary, populated on every solve; None when the service emits no point, e.g. infeasible), and a raw ancillary map of any other solver metadata the service reports; see SolveInfoData. Use print(si) or si.display() for a formatted summary. If Problem.solve has not been called, all fields are None (error is ()).

Referenced By