Add decision variables
Declare decision variables with Problem.solve_for() so a solver can assign values to the unknowns in your decision problem.
This guide covers creating decision variables, choosing where solved values appear, scoping variable creation with where=[...], and setting variable type and bounds.
Declare decision variables
Section titled “Declare decision variables”Decision variables are the unknowns that the solver assigns values to when it finds a solution to the problem. They represent the outcome you are trying to find.
Before you declare a decision variable, decide where solved values should appear after you solve:
| What to use | When to use it |
|---|---|
solve_for(..., populate=True) | You are solving one problem and want solved values written back into your model automatically. This is the simplest workflow and the default. |
solve_for(..., populate=False) | You want to keep solved values on the Problem instead of writing them into the model. This is useful for multiple solves over the same model, such as scenario analysis. You will read results from Problem.variable_values(). |
Option 1: Populate a relationship (populate=True)
Section titled “Option 1: Populate a relationship (populate=True)”Pass populate=True to Problem.solve_for() to have the solver write solved values into a relationship you declare as a decision variable:
from relationalai.semantics import Float, Integer, Modelfrom relationalai.semantics.reasoners.prescriptive import Problem
m = Model("ShiftAssignment")
# Declare the model's schemaWorker = m.Concept("Worker", identify_by={"id": Integer})Shift = m.Concept("Shift", identify_by={"id": Integer})
Worker.available_shifts = m.Relationship(f"{Worker} is available for {Shift}")Worker.cost_for_shift = m.Relationship(f"{Worker} working {Shift} has cost {Float:cost}")Shift.required_workers = m.Property(f"{Shift} requires {Integer:n} workers")
# Define base facts.48 collapsed lines
workers = m.data([ {"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}, {"id": 3, "name": "Chen"},])shifts = m.data([ {"id": 10, "name": "Morning"}, {"id": 20, "name": "Evening"},])availability = m.data([ {"worker_id": 1, "shift_id": 10}, {"worker_id": 2, "shift_id": 10}, {"worker_id": 2, "shift_id": 20}, {"worker_id": 3, "shift_id": 20},])costs = m.data([ {"worker_id": 1, "shift_id": 10, "cost": 9.0}, {"worker_id": 2, "shift_id": 10, "cost": 10.0}, {"worker_id": 2, "shift_id": 20, "cost": 8.0}, {"worker_id": 3, "shift_id": 20, "cost": 11.0},])required = m.data([ {"shift_id": 10, "required_workers": 1}, {"shift_id": 20, "required_workers": 1},])
m.define( Worker.new(workers.to_schema()), Shift.new(shifts.to_schema()),)
worker = Worker.filter_by(id=availability.worker_id)shift = Shift.filter_by(id=availability.shift_id)m.define(worker.available_shifts(shift))
worker = Worker.filter_by(id=costs.worker_id)shift = Shift.filter_by(id=costs.shift_id)m.define(worker.cost_for_shift(shift, costs.cost))
shift = Shift.filter_by(id=required.shift_id)m.define(shift.required_workers(required.required_workers))
p = Problem(m, Float)
# Declare a decision relationship. This is the relationship the solver# will populate with solved values.Worker.x_assign = m.Relationship( f"{Worker} is assigned to {Shift} if {Float:assigned}")
# Create a float variable. This is the PyRel DSL object that represents the# unknown value that the solver will assign to each worker-shift pair.X_ASSIGN = Float.ref("x_assign")
# Solve for `X_ASSIGN` and use it to populate the `Worker.x_assign`# relationship with solved values.p.solve_for( Worker.x_assign(Shift, X_ASSIGN), populate=True, # Optional, defaults to True name=["assign", Worker.id, Shift.id], where=[Worker.available_shifts(Shift)], type="bin", lower=0, upper=1,)In this example:
Worker.x_assignis a relationship that the solver will load values into once the problem has been solved.X_ASSIGN = Float.ref("x_assign")creates a PyRel DSL variable of typeFloatthat represents the unknown value the solver will assign to each worker–shift pair.p.solve_for()creates decision variables for each worker–shift pair in theWorker.x_assignrelationship and tells the solver to populate that relationship with solved values after solving.Worker.x_assign(Shift, X_ASSIGN)says to solve forX_ASSIGNin that relationship.populate=Truetells the solver to write solved values into theWorker.x_assignrelationship so you can query them after the solve.name=["assign", Worker.id, Shift.id]gives each variable a unique name based on the worker and shift IDs.type="bin"defines the variable type as binary, andlower=0andupper=1restrict the variable to 0 and 1 values.where=[Worker.available_shifts(Shift)]scopes variable creation. If a worker–shift pair is not available, the corresponding decision variable does not exist.
Option 2: Keep solved values on the Problem object (populate=False)
Section titled “Option 2: Keep solved values on the Problem object (populate=False)”Pass populate=False to Problem.solve_for() to read solved values directly from the Problem after solving instead of having them automatically populate a relationship:
from relationalai.semantics import Float, Integer, Modelfrom relationalai.semantics.reasoners.prescriptive import Problem
m = Model("ShiftAssignment")
# Declare the model's schemaWorker = m.Concept("Worker", identify_by={"id": Integer})Shift = m.Concept("Shift", identify_by={"id": Integer})
Worker.available_shifts = m.Relationship(f"{Worker} is available for {Shift}")Worker.cost_for_shift = m.Relationship(f"{Worker} working {Shift} has cost {Float:cost}")Shift.required_workers = m.Property(f"{Shift} requires {Integer:n} workers")
# Define base facts.48 collapsed lines
workers = m.data([ {"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}, {"id": 3, "name": "Chen"},])shifts = m.data([ {"id": 10, "name": "Morning"}, {"id": 20, "name": "Evening"},])availability = m.data([ {"worker_id": 1, "shift_id": 10}, {"worker_id": 2, "shift_id": 10}, {"worker_id": 2, "shift_id": 20}, {"worker_id": 3, "shift_id": 20},])costs = m.data([ {"worker_id": 1, "shift_id": 10, "cost": 9.0}, {"worker_id": 2, "shift_id": 10, "cost": 10.0}, {"worker_id": 2, "shift_id": 20, "cost": 8.0}, {"worker_id": 3, "shift_id": 20, "cost": 11.0},])required = m.data([ {"shift_id": 10, "required_workers": 1}, {"shift_id": 20, "required_workers": 1},])
m.define( Worker.new(workers.to_schema()), Shift.new(shifts.to_schema()),)
worker = Worker.filter_by(id=availability.worker_id)shift = Shift.filter_by(id=availability.shift_id)m.define(worker.available_shifts(shift))
worker = Worker.filter_by(id=costs.worker_id)shift = Shift.filter_by(id=costs.shift_id)m.define(worker.cost_for_shift(shift, costs.cost))
shift = Shift.filter_by(id=required.shift_id)m.define(shift.required_workers(required.required_workers))
p = Problem(m, Float)
# Declare a decision relationship. This relationship describes the meaning# of the decision variable and what parts of the model it connects to.Worker.x_assign = m.Relationship( f"{Worker} is assigned to {Shift} if {Float:assigned}")
# Create a float variable. This is the PyRel DSL object that represents the# unknown value that the solver will assign values to.X_ASSIGN = Float.ref("x_assign")
# Solve for `X_ASSIGN`, but do not populate the `Worker.x_assign` relationship.# Instead, you will need to read values using `Problem.variable_values()` after solving.p.solve_for( Worker.x_assign(Shift, X_ASSIGN), populate=False, # Do not automatically populate the relationship name=["assign", Worker.id, Shift.id], where=[Worker.available_shifts(Shift)], type="bin", lower=0, upper=1,)In this example:
Worker.x_assignis a relationship that the solver will load values into once the problem has been solved.p.solve_for()turns that relationship into one variable for each worker–shift pair.Worker.x_assign(Shift, X_ASSIGN)says to solve forX_ASSIGNin that relationship.populate=Falsetells the solver not to automatically write solved values intoWorker.x_assign. You can then read the results from theProblem(for example withProblem.variable_values()).name=["assign", Worker.id, Shift.id]gives each variable a unique name based on the worker and shift IDs.type="bin"defines the variable type as binary, andlower=0andupper=1restrict the variable to 0 and 1 values.
populate=Falseis useful when you want to keep multiple solutions around without overwriting a relationship in your model. If you only need one solution,populate=Trueis usually simpler.
Choose variable types and bounds
Section titled “Choose variable types and bounds”Use this table to choose type and bounds when you call solve_for(...):
| What to use | When to use it |
|---|---|
type="bin" with lower=0, upper=1 | You are making yes/no decisions (assign, select, open/close). |
type="int" with integer bounds | You are deciding a countable quantity (units to produce, people to schedule). |
type="cont" with numeric bounds | You are deciding a divisible quantity (flow, allocation, weights). |
Choose bounds intentionally.
If you set upper too high to be safe, you can make solves slower or harder to diagnose.
Scope variable creation with where=[...]
Section titled “Scope variable creation with where=[...]”Use where=[...] to control which entity combinations get variables.
This example scopes variable creation to available worker–shift pairs:
from relationalai.semantics import Float, Integer, Modelfrom relationalai.semantics.reasoners.prescriptive import Problem
m = Model("ShiftAssignment")
# Declare the model's schemaWorker = m.Concept("Worker", identify_by={"id": Integer})Shift = m.Concept("Shift", identify_by={"id": Integer})
Worker.available_shifts = m.Relationship(f"{Worker} is available for {Shift}")Worker.cost_for_shift = m.Relationship(f"{Worker} working {Shift} has cost {Float:cost}")Shift.required_workers = m.Property(f"{Shift} requires {Integer:n} workers")
# Define base facts.48 collapsed lines
workers = m.data([ {"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}, {"id": 3, "name": "Chen"},])shifts = m.data([ {"id": 10, "name": "Morning"}, {"id": 20, "name": "Evening"},])availability = m.data([ {"worker_id": 1, "shift_id": 10}, {"worker_id": 2, "shift_id": 10}, {"worker_id": 2, "shift_id": 20}, {"worker_id": 3, "shift_id": 20},])costs = m.data([ {"worker_id": 1, "shift_id": 10, "cost": 9.0}, {"worker_id": 2, "shift_id": 10, "cost": 10.0}, {"worker_id": 2, "shift_id": 20, "cost": 8.0}, {"worker_id": 3, "shift_id": 20, "cost": 11.0},])required = m.data([ {"shift_id": 10, "required_workers": 1}, {"shift_id": 20, "required_workers": 1},])
m.define( Worker.new(workers.to_schema()), Shift.new(shifts.to_schema()),)
worker = Worker.filter_by(id=availability.worker_id)shift = Shift.filter_by(id=availability.shift_id)m.define(worker.available_shifts(shift))
worker = Worker.filter_by(id=costs.worker_id)shift = Shift.filter_by(id=costs.shift_id)m.define(worker.cost_for_shift(shift, costs.cost))
shift = Shift.filter_by(id=required.shift_id)m.define(shift.required_workers(required.required_workers))
p = Problem(m, Float)
# Declare a decision-variable relationshipWorker.x_assign = m.Relationship( f"{Worker} is assigned to {Shift} if {Float:assigned}")
# Define `Worker.x_assign` as a decision variable to be# solved for, scoped to available worker-shift pairs, and with binary values of 0 or 1.X_ASSIGN = Float.ref("x_assign")p.solve_for( Worker.x_assign(Shift, X_ASSIGN), populate=True, name=["assign", Worker.id, Shift.id], where=[Worker.available_shifts(Shift)], type="bin", lower=0, upper=1,)Inspect Problem variables
Section titled “Inspect Problem variables”Use p.num_variables to quickly confirm that your solve_for(...) calls actually created variables:
from relationalai.semantics import Float, Integer, Modelfrom relationalai.semantics.reasoners.prescriptive import Problem
m = Model("ShiftAssignment")
# Declare the model's schemaWorker = m.Concept("Worker", identify_by={"id": Integer})Shift = m.Concept("Shift", identify_by={"id": Integer})
Worker.available_shifts = m.Relationship(f"{Worker} is available for {Shift}")Worker.cost_for_shift = m.Relationship(f"{Worker} working {Shift} has cost {Float:cost}")Shift.required_workers = m.Property(f"{Shift} requires {Integer:n} workers")
# Define base facts.48 collapsed lines
workers = m.data([ {"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}, {"id": 3, "name": "Chen"},])shifts = m.data([ {"id": 10, "name": "Morning"}, {"id": 20, "name": "Evening"},])availability = m.data([ {"worker_id": 1, "shift_id": 10}, {"worker_id": 2, "shift_id": 10}, {"worker_id": 2, "shift_id": 20}, {"worker_id": 3, "shift_id": 20},])costs = m.data([ {"worker_id": 1, "shift_id": 10, "cost": 9.0}, {"worker_id": 2, "shift_id": 10, "cost": 10.0}, {"worker_id": 2, "shift_id": 20, "cost": 8.0}, {"worker_id": 3, "shift_id": 20, "cost": 11.0},])required = m.data([ {"shift_id": 10, "required_workers": 1}, {"shift_id": 20, "required_workers": 1},])
m.define( Worker.new(workers.to_schema()), Shift.new(shifts.to_schema()),)
worker = Worker.filter_by(id=availability.worker_id)shift = Shift.filter_by(id=availability.shift_id)m.define(worker.available_shifts(shift))
worker = Worker.filter_by(id=costs.worker_id)shift = Shift.filter_by(id=costs.shift_id)m.define(worker.cost_for_shift(shift, costs.cost))
shift = Shift.filter_by(id=required.shift_id)m.define(shift.required_workers(required.required_workers))
p = Problem(m, Float)
# Declare a decision-variable relationshipWorker.x_assign = m.Relationship( f"{Worker} is assigned to {Shift} if {Float:assigned}")
# Define `Worker.x_assign` as a decision variable to be# solved for, scoped to available worker-shift pairs, and with binary values of 0 or 1.X_ASSIGN = Float.ref("x_assign")p.solve_for( Worker.x_assign(Shift, X_ASSIGN), populate=True, name=["assign", Worker.id, Shift.id], where=[Worker.available_shifts(Shift)], type="bin", lower=0, upper=1,)
print(p.num_variables)Avoid common pitfalls
Section titled “Avoid common pitfalls”Use this table to troubleshoot common issues when declaring decision variables:
| Symptom | Likely cause | What to try |
|---|---|---|
p.num_variables prints 0 | Your where=[...] scope matched nothing. | Verify the scope relationship has matches and that your joins and base facts are correct. |
| Variables are created for unexpected combinations | Your where=[...] scope is too broad or missing a join that ties entities together. | Tighten the scope to only valid combinations and join on the right attributes in a where=[...] argument. |