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.
What a decision problem is
Section titled “What a decision problem is”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.
How PyRel represents a decision problem
Section titled “How PyRel represents a decision problem”In PyRel, your semantic model and your decision problem are related but not the same thing:
- A
Modelobject represents your domain and the inputs to the decision problem. - A
Problemobject formulates the decision problem and solves it with a prescriptive reasoner.
The following table summarizes the two layers and what belongs in each:
| Layer | What it contains | What it is for |
|---|---|---|
Model | Concepts, properties, relationships, base and derived facts. | Represent your domain and compute reusable logic and parameters. |
Problem | Decision variables, solution constraints, objective functions, solve metadata, and solution accessors. | Compile a specific decision problem and use a prescriptive reasoner to solve it. |
How solving a decision problem works
Section titled “How solving a decision problem works”The core workflow is always the same:
Here’s a step-by-step example:
-
Build a semantic model
First you create a
Modelobject and add concepts, properties, relationships, and facts about the domain in question.In the following examples, a
Modelrepresenting workers and shifts is created, and facts about which workers and shifts exist are loaded into the model:from relationalai.semantics import Float, Integer, Modelfrom relationalai.semantics.reasoners.prescriptive import Problemfrom relationalai.semantics.std import aggregates as aggm = 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()),) -
Create a
ProblemobjectThe
Problemobject is the container for the decision problem you want to solve:from relationalai.semantics import Float, Integer, Modelfrom relationalai.semantics.reasoners.prescriptive import Problemfrom relationalai.semantics.std import aggregates as aggm = Model("WorkerShiftAssignment")17 collapsed linesWorker = 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) -
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, Modelfrom relationalai.semantics.reasoners.prescriptive import Problemfrom relationalai.semantics.std import aggregates as aggm = Model("WorkerShiftAssignment")17 collapsed linesWorker = 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 variablelower=0, # Lower bound of 0upper=1, # Upper bound of 1) -
Add solution constraints
Use
Model.require()to write constraints in terms of your model, and then register them with the solver usingProblem.satisfy():from relationalai.semantics import Float, Integer, Modelfrom relationalai.semantics.reasoners.prescriptive import Problemfrom relationalai.semantics.std import aggregates as aggm = Model("WorkerShiftAssignment")33 collapsed linesWorker = 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 1assigned_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]) -
Solve and check status
Call
Problem.solve()to run with the name of the solver backend you want to use and then checkProblem.termination_statusto see if a solution was found:from relationalai.semantics import Float, Integer, Modelfrom relationalai.semantics.reasoners.prescriptive import Problemfrom relationalai.semantics.std import aggregates as aggm = Model("WorkerShiftAssignment")36 collapsed linesWorker = 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)
Where to go next
Section titled “Where to go next”Pick the next guide based on which step you are on:
Problem object and choose the numeric type that matches your solver workflow. Problem.solve_for() to declare the unknowns the solver will assign. Model.require() and register them as solver constraints with Problem.satisfy(). Problem.solve() and interpret termination status. Problem after solving.