Skip to content

This feature is currently in Preview.

Work with solutions

After you call Problem.solve(), you can access solutions to your decision problems for use in your model or application logic. This guide covers how to access solution values.

Where solved values appear depends on how you declared decision variables with Problem.solve_for(). In other words, you don’t choose an output method after the solve. You determine where the results are based on what you already set when you declared the decision variables.

Use this table to determine where to look:

What you usedHow to access results
solve_for(..., populate=True)Query the populated decision relationship like any other relationship in your model. This is the most straightforward option and is ideal for workflows with a single Problem or multiple Problem instances that do not share decision variables.
solve_for(..., populate=False)Read solver-level variable values from the Problem with Problem.variable_values(). This is ideal for multi-Problem workflows that share decision variables, like scenario analysis.

If you used populate=True, the solve writes solved values into the relationship you declared as a decision variable. Query that relationship to read entity-aware results after the 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("ShiftAssignment")
# Declare the model's schema
Worker = 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.
51 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))
# Create a decision problem
p = Problem(m, Float)
# Declare a decision relationship
Worker.x_assign = m.Relationship(
f"{Worker} is assigned to {Shift} if {Float:assigned}"
)
# Define decision variables with populate=True to write solved values back into the model.
X_ASSIGN = Float.ref("x")
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,
)
# Coverage: each shift must be assigned the required number of workers.
assigned_per_shift = agg.sum(X_ASSIGN).where(Worker.x_assign(Shift, X_ASSIGN)).per(Shift)
p.satisfy(m.require(assigned_per_shift == Shift.required_workers))
# Capacity: each worker can be assigned to at most one shift.
assigned_per_worker = agg.sum(X_ASSIGN).where(Worker.x_assign(Shift, X_ASSIGN)).per(Worker)
p.satisfy(m.require(assigned_per_worker <= 1))
# Minimize total cost of assignments
cost = Float.ref("cost")
p.minimize(
agg.sum(cost * X_ASSIGN).where(
Worker.x_assign(Shift, X_ASSIGN),
Worker.cost_for_shift(Shift, cost),
)
)
# Solve the problem with a 1% relative optimality gap tolerance.
p.solve("highs", relative_gap_tolerance=0.01, _server_side_import=False)
# Check the termination status and objective value.
print("status:", p.termination_status)
print("objective:", p.objective_value)
# Query “active” assignments from the populated relationship.
assigned = Float.ref("assigned")
q = (
m
.select(Worker.id, Shift.id, assigned)
.where(Worker.x_assign(Shift, assigned), assigned > 0.5)
)
# Inspect query results
q.inspect()
  • p.solve_for(..., populate=True) writes solved values back into the model, so you can query them like any other relationship.
  • This model has a ternary decision relationship Worker.x_assign(Shift, X_ASSIGN) that is populated with solved values.
  • You can use conditions on Worker.x_assign to derive new concepts and relationships in your model based on the solver solution.

If you used populate=False, the solve does not write values back into your model. Instead, read solved variable values from the Problem. This is the safer default when you want to run multiple Problem instances over the same Model, especially if they share decision variables, like in scenario analysis workflows.

After solving, call p.variable_values() and materialize it to a DataFrame:

from relationalai.semantics import Float, Integer, Model
from relationalai.semantics.reasoners.prescriptive import Problem
from relationalai.semantics.std import aggregates as agg
m = Model("ShiftAssignment")
# Declare the model's schema
Worker = 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.
51 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))
# Create a decision problem
p = Problem(m, Float)
# Declare a decision relationship
Worker.x_assign = m.Relationship(
f"{Worker} is assigned to {Shift} if {Float:assigned}"
)
# Define decision variables with populate=False to keep solved values at the Problem-level.
X_ASSIGN = Float.ref("x")
p.solve_for(
Worker.x_assign(Shift, X_ASSIGN),
populate=False,
name=["assign", Worker.id, Shift.id],
where=[Worker.available_shifts(Shift)],
type="bin",
lower=0,
upper=1,
)
# Coverage: each shift must be assigned the required number of workers.
assigned_per_shift = agg.sum(X_ASSIGN).where(Worker.x_assign(Shift, X_ASSIGN)).per(Shift)
p.satisfy(m.require(assigned_per_shift == Shift.required_workers))
# Capacity: each worker can be assigned to at most one shift.
assigned_per_worker = agg.sum(X_ASSIGN).where(Worker.x_assign(Shift, X_ASSIGN)).per(Worker)
p.satisfy(m.require(assigned_per_worker <= 1))
# Minimize total cost of assignments
cost = Float.ref("cost")
p.minimize(
agg.sum(cost * X_ASSIGN).where(
Worker.x_assign(Shift, X_ASSIGN),
Worker.cost_for_shift(Shift, cost),
)
)
# Solve the problem with a 1% relative optimality gap tolerance.
p.solve("highs", relative_gap_tolerance=0.01, _server_side_import=False)
# Get solver-level variable values as a DataFrame.
df = p.variable_values().to_df()
print(df)
  • variable_values() returns a DataFrame with name and value columns, where name is the variable name you set with name=[...] in solve_for().
  • Using name=[...] is optional but highly recommended when using populate=False to make sense of solver-level variable values.
  • variable_values() is solver-level, meaning it returns data that is not connected to the model in any way. If you want to connect results back to model entities, use populate=True and query the populated relationship instead.
  • You can use the DataFrame returned by variable_values() to send results to downstream workflows.