Skip to content

This feature is currently in Preview.

Solve a decision problem

Use prescriptive reasoning when you need to make choices that satisfy constraints and, optionally, optimize an objective. This overview introduces the core workflow and mental model for solving decision problems in PyRel.

A decision problem asks you to make a choice—often a version of “what should we do next?” You tell the solver what it is allowed to choose, what must be true, and what “best” means.

Decision problems are solved by prescriptive reasoners, which are optimization and constraint-solving engines that find solutions to problems you formulate in PyRel. You can pick from a variety of solver backends, including open-source and commercial solvers.

Use the following mapping to sanity-check your formulation before you write any code:

  • Inputs: The facts in your semantic model that describe the world as it is.
  • Decision variables: The unknown values the solver will assign.
  • Solution constraints: Conditions that must hold for a solution to be acceptable.
  • Objective functions (optional): A formula for what “best” means in your problem, either by minimizing or maximizing some mathematical expression of your decision variables and model parameters.

In PyRel, your semantic model and your decision problem are related but not the same thing:

  • A Model object represents your domain and the inputs to the decision problem.
  • A Problem object formulates the decision problem and solves it with a prescriptive reasoner.

The following table summarizes the two layers and what belongs in each:

LayerWhat it containsWhat it is for
ModelConcepts, properties, relationships, base and derived facts.Represent your domain and compute reusable logic and parameters.
ProblemDecision variables, solution constraints, objective functions, solve metadata, and solution accessors.Compile a specific decision problem and use a prescriptive reasoner to solve it.

The core workflow is always the same:

Build a ModelFormulate a ProblemSolveInspect the solution

Here’s a step-by-step example:

  1. Build a semantic model

    First you create a Model object and add concepts, properties, relationships, and facts about the domain in question.

    In the following examples, a Model representing workers and shifts is created, and facts about which workers and shifts exist are loaded into the model:

    from relationalai.semantics import Float, Integer, Model
    from relationalai.semantics.reasoners.prescriptive import Problem
    from relationalai.semantics.std import aggregates as agg
    m = Model("WorkerShiftAssignment")
    Worker = m.Concept("Worker", identify_by={"id": Integer})
    Shift = m.Concept("Shift", identify_by={"id": Integer})
    workers = m.data([
    {"id": 1, "name": "Alice"},
    {"id": 2, "name": "Bob"},
    ])
    shifts = m.data([
    {"id": 10, "name": "Morning"},
    {"id": 20, "name": "Evening"},
    {"id": 30, "name": "Night"},
    ])
    m.define(
    Worker.new(workers.to_schema()),
    Shift.new(shifts.to_schema()),
    )
  2. Create a Problem object

    The Problem object is the container for the decision problem you want to solve:

    from relationalai.semantics import Float, Integer, Model
    from relationalai.semantics.reasoners.prescriptive import Problem
    from relationalai.semantics.std import aggregates as agg
    m = Model("WorkerShiftAssignment")
    17 collapsed lines
    Worker = m.Concept("Worker", identify_by={"id": Integer})
    Shift = m.Concept("Shift", identify_by={"id": Integer})
    workers = m.data([
    {"id": 1, "name": "Alice"},
    {"id": 2, "name": "Bob"},
    ])
    shifts = m.data([
    {"id": 10, "name": "Morning"},
    {"id": 20, "name": "Evening"},
    {"id": 30, "name": "Night"},
    ])
    m.define(
    Worker.new(workers.to_schema()),
    Shift.new(shifts.to_schema()),
    )
    p = Problem(m, Float)
  3. Add decision variables

    Use Problem.solve_for() to declare decision variables and tell the solver what it is allowed to choose:

    from relationalai.semantics import Float, Integer, Model
    from relationalai.semantics.reasoners.prescriptive import Problem
    from relationalai.semantics.std import aggregates as agg
    m = Model("WorkerShiftAssignment")
    17 collapsed lines
    Worker = m.Concept("Worker", identify_by={"id": Integer})
    Shift = m.Concept("Shift", identify_by={"id": Integer})
    workers = m.data([
    {"id": 1, "name": "Alice"},
    {"id": 2, "name": "Bob"},
    ])
    shifts = m.data([
    {"id": 10, "name": "Morning"},
    {"id": 20, "name": "Evening"},
    {"id": 30, "name": "Night"},
    ])
    m.define(
    Worker.new(workers.to_schema()),
    Shift.new(shifts.to_schema()),
    )
    p = Problem(m, Float)
    # Declare a decision relationship. This ties solution values to the model and makes them queryable after the solve.
    Worker.x_assign = m.Relationship(
    f"{Worker} is assigned to {Shift} if {Float:assigned}"
    )
    # Define a decision variable `X_ASSIGN` that the solver will assign values to for each worker-shift pair, and tell the solver to populate the `Worker.x_assign` relationship with those values after solving.
    X_ASSIGN = Float.ref()
    p.solve_for(
    Worker.x_assign(Shift, X_ASSIGN),
    name=["assign", Worker.id, Shift.id], # Becomes "assign_1_10" for worker 1 and shift 10, etc.
    type="bin", # Binary variable
    lower=0, # Lower bound of 0
    upper=1, # Upper bound of 1
    )
  4. Add solution constraints

    Use Model.require() to write constraints in terms of your model, and then register them with the solver using Problem.satisfy():

    from relationalai.semantics import Float, Integer, Model
    from relationalai.semantics.reasoners.prescriptive import Problem
    from relationalai.semantics.std import aggregates as agg
    m = Model("WorkerShiftAssignment")
    33 collapsed lines
    Worker = m.Concept("Worker", identify_by={"id": Integer})
    Shift = m.Concept("Shift", identify_by={"id": Integer})
    workers = m.data([
    {"id": 1, "name": "Alice"},
    {"id": 2, "name": "Bob"},
    ])
    shifts = m.data([
    {"id": 10, "name": "Morning"},
    {"id": 20, "name": "Evening"},
    {"id": 30, "name": "Night"},
    ])
    m.define(
    Worker.new(workers.to_schema()),
    Shift.new(shifts.to_schema()),
    )
    p = Problem(m, Float)
    Worker.x_assign = m.Relationship(
    f"{Worker} is assigned to {Shift} if {Float:assigned}"
    )
    x = Float.ref("x")
    p.solve_for(
    Worker.x_assign(Shift, x),
    populate=True,
    name=["assign", Worker.id, Shift.id],
    type="bin",
    lower=0,
    upper=1,
    )
    # Every shift gets filled: sum of assignments per shift equals 1
    assigned_per_shift = agg.sum(x).where(Worker.x_assign(Shift, x)).per(Shift)
    p.satisfy(m.require(assigned_per_shift == 1), name=["fill-shift", Shift.id])
  5. Solve and check status

    Call Problem.solve() to run with the name of the solver backend you want to use and then check Problem.termination_status to see if a solution was found:

    from relationalai.semantics import Float, Integer, Model
    from relationalai.semantics.reasoners.prescriptive import Problem
    from relationalai.semantics.std import aggregates as agg
    m = Model("WorkerShiftAssignment")
    36 collapsed lines
    Worker = m.Concept("Worker", identify_by={"id": Integer})
    Shift = m.Concept("Shift", identify_by={"id": Integer})
    workers = m.data([
    {"id": 1, "name": "Alice"},
    {"id": 2, "name": "Bob"},
    ])
    shifts = m.data([
    {"id": 10, "name": "Morning"},
    {"id": 20, "name": "Evening"},
    {"id": 30, "name": "Night"},
    ])
    m.define(
    Worker.new(workers.to_schema()),
    Shift.new(shifts.to_schema()),
    )
    p = Problem(m, Float)
    Worker.x_assign = m.Relationship(
    f"{Worker} is assigned to {Shift} if {Float:assigned}"
    )
    x = Float.ref("x")
    p.solve_for(
    Worker.x_assign(Shift, x),
    populate=True,
    name=["assign", Worker.id, Shift.id],
    type="bin",
    lower=0,
    upper=1,
    )
    assigned_per_shift = agg.sum(x).where(Worker.x_assign(Shift, x)).per(Shift)
    p.satisfy(m.require(assigned_per_shift == 1), name=["fill-shift", Shift.id])
    p.solve("highs")
    print("status:", p.termination_status)

Pick the next guide based on which step you are on: