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.
Determine how to access results
Section titled “Determine how to access results”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 used | How 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. |
Query a populated relationship
Section titled “Query a populated relationship”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, Modelfrom relationalai.semantics.reasoners.prescriptive import Problemfrom relationalai.semantics.std import aggregates as agg
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.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 problemp = Problem(m, Float)
# Declare a decision relationshipWorker.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 assignmentscost = 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 resultsq.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_assignto derive new concepts and relationships in your model based on the solver solution.
Read solver-level variable values
Section titled “Read solver-level variable values”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, Modelfrom relationalai.semantics.reasoners.prescriptive import Problemfrom relationalai.semantics.std import aggregates as agg
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.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 problemp = Problem(m, Float)
# Declare a decision relationshipWorker.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 assignmentscost = 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 withnameandvaluecolumns, wherenameis the variable name you set withname=[...]insolve_for().- Using
name=[...]is optional but highly recommended when usingpopulate=Falseto 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, usepopulate=Trueand query the populated relationship instead.- You can use the DataFrame returned by
variable_values()to send results to downstream workflows.