Skip to content

Problem

relationalai.semantics.reasoners.prescriptive.problem
Problem(model: b.Model, numeric_type: b.Concept)

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 and read 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.

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)

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.

Problem.variables: list[ProblemVariable]

Variable components registered via Problem.solve_for.

Problem.objectives: list[ProblemObjective]

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

Problem.constraints: list[ProblemConstraint]

Constraint components registered via Problem.satisfy.

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.

Problem.Objective: Concept

Engine-side aggregate Concept covering all objectives. Each ProblemObjective extends this Concept; use it to query names, types, and (in future) duals or printed expressions.

Problem.Constraint: Concept

Engine-side aggregate Concept covering all constraints. Each ProblemConstraint extends this Concept; use it to query names, types, and (in future) duals or printed expressions.

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]).
  • 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.
  • TypeError - If an argument has an invalid type.

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Prescriptive reasoning > Solve a decision problem
        ├──  Overview
        │   └──  How solving a decision problem works
        ├──  Create a Problem object
        │   └──  Choose a default numeric type
        ├──  Add decision variables
        │   ├──  Declare decision variables
        │   └──  Choose variable types and bounds
        ├──  Solve a decision problem
        │   └──  Avoid common pitfalls
        └──  Work with solutions
            └──  Determine how to access results
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.
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.
Problem.satisfy(
expr: b.Fragment, name: Optional[Any | list[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).

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.

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Prescriptive reasoning > Solve a decision problem
        ├──  Overview
        └──  Add constraints
            ├──  Add constraints with Problem.satisfy()
            └──  Avoid common pitfalls
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)``).

Parameters:

Problem.num_variables() -> b.Relationship

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

Returns:

  • Relationship - An Integer Relationship counting the declared variables.
Problem.num_constraints() -> b.Relationship

Number of declared constraints. Usable in rules and ICs.

Returns:

  • Relationship - An Integer Relationship counting the declared constraints.
Problem.num_min_objectives() -> b.Relationship

Number of minimization objectives. Usable in rules and ICs.

Returns:

  • Relationship - An Integer Relationship counting the minimization 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.
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.
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.
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.
Problem.num_points() -> b.Relationship

Number of solution points. Usable in rules and ICs.

Returns:

  • Relationship - An Integer Relationship counting the solution points.
Problem.solver_version() -> b.Relationship

Solver version string. Usable in rules and ICs.

Returns:

  • Relationship - A String Relationship containing the solver version.
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.
Problem.error() -> b.Relationship

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

Returns:

  • Relationship - A String Relationship containing solver error messages.
Problem.display(part: Optional[b.Concept] = None, *, print_output: bool = True) -> str

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

With no arguments, this shows a header with the numeric type, a bullet-list summary of variable/objective/constraint counts (zero categories are omitted), and tables of all declared variables, objectives, and constraints. Nothing is truncated — long expressions and large tables are emitted in full.

Pass a ProblemVariable, ProblemObjective, or ProblemConstraint returned by Problem.solve_for, Problem.minimize, Problem.maximize, or Problem.satisfy to display only that component.

Parameters:

  • part

    (ProblemVariable or ProblemObjective or ProblemConstraint, default: None) - Specific component to display. Pass the value returned by the builder methods directly.
  • print_output

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

Returns:

  • str - The formatted summary.

Raises:

Examples:

Display the full problem:

>>> 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)
>>> x_vars = problem.solve_for(x, name="x", lower=0)
>>> problem.minimize(x)
>>> problem.display()

Display a single component (returned by solve_for/minimize/satisfy):

>>> problem.display(x_vars)

Capture output without printing:

>>> text = problem.display(print_output=False)

Notes:

This method queries the model.

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Prescriptive reasoning > Solve a decision problem
        ├──  Create a Problem object
        │   └──  Inspect a Problem with display()
        ├──  Add constraints
        │   └──  Inspect Problem constraints
        └──  Solve a decision problem
            └──  Avoid common pitfalls
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,
**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, inspect results via Problem.variable_values and result accessors such as Problem.termination_status and Problem.objective_value, or use 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).
  • **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.
  • TypeError - If any solver-specific parameter value is not an int, float, str, or bool.
  • RuntimeError - 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.

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Prescriptive reasoning
        ├──  Choose a backend
        │   ├──  Use Gurobi
        │   │   └──  Example
        │   ├──  Use Ipopt
        │   │   └──  Example
        │   └──  Use MiniZinc
        │       └──  Example
        └──  Solve a decision problem
            ├──  Overview
            ├──  Solve a decision problem
            │   ├──  What happens when you solve a problem
            │   ├──  Solve for feasibility
            │   ├──  Solve optimally
            │   ├──  Set a time limit
            │   ├──  Accept a near-optimal solution
            │   ├──  Tune solver backend behavior
            │   ├──  Print the translated solver model
            │   ├──  Check error details when termination status is not OPTIMAL
            │   └──  Handle solve failures
            └──  Work with solutions
Problem.variable_values(multiple: bool = False) -> b.Fragment

Return decision variable values from the first solution.

.. deprecated:

Use ``Variable.values`` instead for composable engine-side queries::
Var = problem.Variable
val = Float.ref() # or Integer.ref() for Integer-typed problems
model.select(Var.name, val).where(Var.values(0, val)) # first solution
# Multi-solution form (matches multiple=True):
sol_idx = Integer.ref()
model.select(sol_idx, Var.name, val).where(Var.values(sol_idx, val))

Use this after Problem.solve to read variable values as a fragment you can materialize with .to_df() or print with .inspect(). Without multiple, the returned fragment reflects the first solution only.

Parameters:

  • multiple

    (bool, default: False) - If True, return values for all solutions from the most recent Problem.solve call and include a 0-based sol_index column.

Returns:

  • Fragment - A fragment with columns name and value (and sol_index if multiple is True). Returns an empty DataFrame if Problem.solve has not been called.

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Prescriptive reasoning > Solve a decision problem
        ├──  Add decision variables
        │   └──  Declare decision variables
        └──  Work with solutions
            ├──  Determine how to access results
            └──  Read solver-level variable values
Problem.load_point(point_index: int) -> None

Deprecated: switching the active solution point is no longer supported.

.. deprecated:

Populated properties and :meth:`variable_values` always reflect the
first solution (``sol_index = 0``, matching Python 0-based
indexing). ``load_point(0)`` is a no-op kept for backward
compatibility and emits a :class:`DeprecationWarning`. Any other
index raises :class:`NotImplementedError` — use
``Variable.values(sol_idx, val)`` to query a specific solution::
Var = problem.Variable
val = Float.ref() # or Integer.ref() for Integer-typed problems
model.select(Var.name, val).where(Var.values(2, val)) # 3rd solution

Parameters:

  • point_index

    (int) - Must be 0. Any other value raises NotImplementedError.

Raises:

  • ValueError - If point_index is not a non-negative integer.
  • NotImplementedError - If point_index is not 0.
Problem.solve_info() -> SolveInfoData

Return solver result metadata as a cached SolveInfoData.

Fetches all result metadata in a single query. Subsequent calls return the cached snapshot until the next Problem.solve.

Returns:

  • SolveInfoData - Frozen dataclass with typed fields. Use print(si) or si.display() for a formatted summary. If Problem.solve has not been called, all fields are None (error is ()).
RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Prescriptive reasoning
        ├──  Choose a backend
        │   ├──  Use HiGHS
        │   │   └──  Example
        │   ├──  Use Gurobi
        │   │   └──  Example
        │   ├──  Use Ipopt
        │   │   └──  Example
        │   └──  Use MiniZinc
        │       └──  Example
        └──  Solve a decision problem
            ├──  Overview
            │   ├──  How PyRel represents a decision problem
            │   └──  How solving a decision problem works
            ├──  Create a Problem object
            │   ├──  Choose a default numeric type
            │   ├──  Create a Problem object
            │   └──  Inspect a Problem with display()
            ├──  Add decision variables
            │   ├──  Declare decision variables
            │   ├──  Choose variable types and bounds
            │   └──  Inspect Problem variables
            ├──  Add constraints
            │   ├──  Add constraints with Problem.satisfy()
            │   ├──  Inspect Problem constraints
            │   └──  Avoid common pitfalls
            ├──  Solve a decision problem
            │   ├──  What happens when you solve a problem
            │   ├──  Validate before you solve
            │   ├──  Solve for feasibility
            │   ├──  Solve optimally
            │   ├──  Inspect solve metadata
            │   ├──  Set a time limit
            │   ├──  Accept a near-optimal solution
            │   ├──  Tune solver backend behavior
            │   ├──  Print the translated solver model
            │   ├──  Check error details when termination status is not OPTIMAL
            │   ├──  Handle solve failures
            │   └──  Avoid common pitfalls
            └──  Work with solutions
                ├──  Determine how to access results
                └──  Read solver-level variable values